# `Raxol.AgentClientProtocol.Ext.AttachPolicy.Runner`
[🔗](https://github.com/DROOdotFOO/raxol-agent/blob/raxol_agent_client_protocol-v0.1.0/packages/raxol_agent_client_protocol/lib/raxol/agent_client_protocol/ext/attach_policy.ex#L106)

THE single fail-closed admission funnel. There is exactly ONE funnel:
`authorize/2`. No second `try/catch` wrapper exists anywhere — a second funnel
with weaker timeout/crash semantics would be a dual-ownership hole.

`authorize/2 :: {:ok, %Grant{}} | {:denied, reason_atom}`. The bus/reattach
layer treats ANY `{:denied, _}` as deny and answers with the single deny wire
envelope (`-32000 "attach denied"`, NO `data`); the specific reason atom goes
to telemetry/`Logger` ONLY — a remote attacker gets one bit (anti-oracle).

## The exhaustive outcome table

| Policy did… | Runner returns |
|---|---|
| `{:ok, %Grant{}}` well-formed (`actor["id"]` binary, scope `:attach`), sid match | `{:ok, grant}` |
| `{:ok, %Grant{}}` `session_id != ctx.session_id` | `{:denied, :grant_session_mismatch}` |
| `{:ok, %Grant{}}` scope ∉ `[:attach]` | `{:denied, :scope_not_allowed}` |
| `{:ok, %Grant{}}` `actor` w/o binary `"id"` | `{:denied, :malformed_grant}` |
| `{:ok, not-a-Grant / nil field}` | `{:denied, :malformed_grant}` |
| `{:error, _}` | `{:denied, :policy_error}` |
| any other term | `{:denied, :non_conforming_return}` |
| raised / threw / exited | `{:denied, :policy_crash}` |
| hung past `policy_timeout_ms` | `{:denied, :policy_timeout}` (task brutally killed) |
| Task.Supervisor missing/dead | `{:denied, :policy_infra}` (never raises) |

# `authorize`

```elixir
@spec authorize(module(), map()) ::
  {:ok, Raxol.AgentClientProtocol.Ext.AttachPolicy.Grant.t()}
  | {:denied, atom()}
```

Run `policy.authorize_attach(ctx)` inside the fail-closed funnel.

Options:

  * `:task_supervisor` — the `Task.Supervisor` name/pid (default the
    package-level `Raxol.AgentClientProtocol.Ext.AttachPolicy.TaskSupervisor`, overridable per endpoint
    via `config :raxol_agent_client_protocol, :attach_task_supervisor`).
  * `:timeout_ms` — the bounded policy timeout (default 5000;
    `config :raxol_agent_client_protocol, :attach_policy_timeout_ms`).

# `authorize`

```elixir
@spec authorize(module(), map(), keyword()) ::
  {:ok, Raxol.AgentClientProtocol.Ext.AttachPolicy.Grant.t()}
  | {:denied, atom()}
```

# `deny_wire`

```elixir
@spec deny_wire() :: %{code: integer(), message: String.t()}
```

The canonical wire projection of a deny. ALL `{:denied, _}` reasons
collapse to this single frame (`-32000 "attach denied"`, no `data`) so the
wire observable is byte-identical across reasons (anti-oracle).
The transport layer owns emitting it; exposed here so the collapse is provable
in one place.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
