Raxol. AgentClientProtocol. Ext. AttachPolicy. Bridge
(Raxol Agent Client Protocol v0.1.0)
Copy Markdown
View Source
The BEAM-local :authorize bridge — make an in-process attach seam defer
to the SAME fail-closed admission funnel (Ext.AttachPolicy.Runner) the
untrusted ACP boundary uses.
Why this exists (the two-wire boundary)
Admission is enforced at the process/trust boundary, NOT symmetrically on every wire:
- ACP boundary (untrusted peers) — HARD-GATED.
session/loadand_raxol/session.loadconverge onExt.Reattach.attach/1, which runs the Runner funnel before any read. This is the security boundary. - BEAM-local attach (in-process) — OPTIONAL SEAM. An in-process caller can already read the session's journal store directly, so a gate on an in-process attach function is bypassable — it is a SEAM for a host that fronts reattach with its own transport, not a boundary.
This module is that seam's ready-made implementation. authorizer/1 returns an
authorize_fun that translates the BEAM-local attach ctx into the Runner
ctx and returns the Runner's verdict verbatim. A host calls it before its own
in-process attach reads anything:
authorize =
Raxol.AgentClientProtocol.Ext.AttachPolicy.Bridge.authorizer(
policy: MyPolicy,
transport: %{kind: :process, peer: self()}
)
authorize.(%{session_id: session_id, from_offset: from_offset})Dependency direction (why it lives in THIS package)
This package has zero raxol-internal deps. The bridge references only THIS package's Runner / Grant / policies and produces a PLAIN closure; the BEAM ctx it receives is a duck-typed map. The host does the wiring.
Fail-closed
The Runner returns {:ok, %Grant{}} (admit) or {:denied, reason} (deny). A
host admits only on {:ok, _grant} and reads nothing otherwise, so a denied
verdict cannot "mostly" admit. transport is Connection-sourced knowledge the
host MUST supply; absent (nil) the default LocalNode policy denies
(:not_local → {:denied, :policy_error}).
Summary
Types
The BEAM-local attach ctx a host builds
(%{session_id, from_offset, policy, surface: :beam_local}). Grow-only and
duck-typed — only :session_id is dereferenced hard; the rest default.
The Runner verdict, which is exactly the authorize_fun contract.
Functions
Build an authorize_fun for an in-process attach that defers to
Ext.AttachPolicy.Runner.
Types
@type beam_ctx() :: %{ :session_id => String.t(), optional(:from_offset) => non_neg_integer(), optional(:surface) => atom(), optional(any()) => any() }
The BEAM-local attach ctx a host builds
(%{session_id, from_offset, policy, surface: :beam_local}). Grow-only and
duck-typed — only :session_id is dereferenced hard; the rest default.
@type verdict() :: {:ok, Raxol.AgentClientProtocol.Ext.AttachPolicy.Grant.t()} | {:denied, atom()}
The Runner verdict, which is exactly the authorize_fun contract.
Functions
Build an authorize_fun for an in-process attach that defers to
Ext.AttachPolicy.Runner.
Options:
:policy— theExt.AttachPolicymodule (defaultExt.AttachPolicy.default_policy/0=LocalNode, deny-by-default).:transport— the Connection-sourced transport (%{kind:, peer:}| nil); NEVER peer-asserted.LocalNodedenies a nil transport.:actor— the audit actor map (%{"id" => binary}) | nil.:capability— anRXC1capability token for theTokenpolicy | nil.:task_supervisor,:timeout_ms— passed through toRunner.authorize/3.
The returned fun reads the BEAM ctx's :session_id/:from_offset/:surface
and merges the config-supplied :actor/:capability/:transport into the
Runner ctx, then calls Runner.authorize/3. The BEAM ctx's :policy
(the HISTORY policy) is not an attach policy and is deliberately dropped —
admission is one decision gating both history and live.