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/load and _raxol/session.load converge on Ext.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

beam_ctx()

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

verdict()

@type verdict() ::
  {:ok, Raxol.AgentClientProtocol.Ext.AttachPolicy.Grant.t()}
  | {:denied, atom()}

The Runner verdict, which is exactly the authorize_fun contract.

Functions

authorizer(opts \\ [])

@spec authorizer(keyword()) :: (beam_ctx() -> verdict())

Build an authorize_fun for an in-process attach that defers to Ext.AttachPolicy.Runner.

Options:

  • :policy — the Ext.AttachPolicy module (default Ext.AttachPolicy.default_policy/0 = LocalNode, deny-by-default).
  • :transport — the Connection-sourced transport (%{kind:, peer:} | nil); NEVER peer-asserted. LocalNode denies a nil transport.

  • :actor — the audit actor map (%{"id" => binary}) | nil.

  • :capability — an RXC1 capability token for the Token policy | nil.

  • :task_supervisor, :timeout_ms — passed through to Runner.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.