Raxol.AgentClientProtocol.Ext.AttachPolicy.Runner (Raxol Agent Client Protocol v0.1.0)

Copy Markdown View Source

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)

Summary

Functions

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

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.

Functions

authorize(policy, ctx)

@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(policy, ctx, opts)

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

deny_wire()

@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.