# `Raxol.AgentClientProtocol.Ext.Reattach`
[🔗](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/reattach.ex#L1)

The register-before-`h` attach seam — the correctness core of the reattach
extension.

## The seam (both wire entrances converge here)

`session/load` + rider and `_raxol/session.load` both parse the same
`_meta["raxol.io"]` rider and call `attach/1`. The algorithm, per attach:

  1. **Authorize through the ONE funnel.** `attach/1` calls the
     injected `:authorize` fun — whose default is
     `Raxol.AgentClientProtocol.Ext.AttachPolicy.Runner.authorize/2`, the SOLE
     fail-closed funnel. There is NO second `FailClosed`/try-catch
     wrapper. Any `{:denied, _}` — or any
     non-`{:ok, %Grant{}}` — DENIES: `attach/1` returns the deny envelope
     (`-32000 "attach denied"`, NO `data`) and NOTHING is registered, no
     history is read. The request error IS the denial — no
     separate `_raxol/session.closed` frame.
  2. On grant, start a fresh subscriber process (this module's GenServer),
     transfer the load-request reply obligation to it via
     `Connection.delegate_reply/3`, then run the attach
     algorithm INSIDE that process:
       a. **REGISTER FIRST** — `Journal.subscribe` (routed to
          `Writer.subscribe`); live records queue to the subscriber from now
          on.
       b. **snapshot `h` SECOND** — `journal.high_watermark`, decision-time,
          read from the STORE (never a cached counter). The gate is armed
          (`h` bound) BEFORE any `{:reattach_live, ...}` is processed
          (gate-arm invariant): steps a–e run with no intervening
          live receive.
       c. reject `from_offset > h + 1` (`-32602`, minting a gap is illegal);
          else emit `history(max(from,1)..h)` as wire frames, under the
          stock projection (see Taint below).
       d. optional `_raxol/session.caught_up{h+1}` (offset-aware only).
       e. reply the `LoadSessionResponse` (`_meta.raxol.io.highWatermark = h`)
          — replay-before-respond (history frames and the reply leave the SAME
          process, so the response serializes after the last history frame).
       f. steady state — the PERMANENT, MONOTONE live gate: `offset <= h` ⇒
          drop (already in history), forever; `offset > h` ⇒ forward.

## Taint — annotate, NEVER filter

Every delivered frame carries the record's taint (in `_meta["raxol.io"]` on
`session/update` frames, first-class `taint` on `_raxol/session.record`). NO
code path here drops/withholds/reroutes a record by taint — a taint filter
anywhere breaks `history ++ live == durable stream`. The only
projection is by KIND (a stock, non-offset-aware attacher receives only
`session_update` frames), applied identically to history and live, and it
is orthogonal to taint. `grant.lens` is threaded opaquely and never
interpreted (reserved).

## Mid-attach expiry — force-close at `exp`

When `grant.expires_at != nil`, the subscriber arms a timer that at
`expires_at` emits `_raxol/session.closed{reason: "revoked"}` and detaches —
bounding how long a held live tail may read. A reconnect just before `exp`
re-authorizes fresh: each attach is its own subscriber process with its own timer
targeting only itself, so the timer-vs-reconnect race is closed by process
identity (the old subscriber's timer never touches the new one).

## Implementation notes

  * **The authorization funnel is injectable.** `attach/1` takes the funnel
    as `:authorize`; its default is `Ext.AttachPolicy.Runner.authorize/2` —
    the SOLE fail-closed funnel. Any non-`{:ok, %Grant{}}` result (incl.
    every `{:denied, _}`) denies, and a grant returned by an injected funnel
    is re-checked against the Grant contract.
  * **The attach subscriber is a dedicated process, not the turn `Session`.**
    Realizing the read path as its own small process keeps the attach
    algorithm (register-first, decision-time `h`, permanent monotone gate)
    provable in isolation and leaves the turn state machine untouched.
  * **`ctx.transport` is passed in, not read from `Connection`.** The
    Connection exposes no `transport/1` accessor; the reattach handler MUST
    source `transport` from Connection-side knowledge (never the peer) and
    pass it as a field. With no transport, the default `LocalNode` policy
    denies.
  * **Keyring is validated per-attach, not at boot.** The `Token` policy's
    Ed25519 keyring is a pure per-`verify/4` parameter (no process state, no
    boot check), so a malformed/empty keyring surfaces as a per-attach deny,
    not a startup failure — an endpoint that requires a configured keyring
    should validate it at its OWN boot (this seam never holds the keyring).

# `opts`

```elixir
@type opts() :: %{
  :conn =&gt; pid(),
  :session_id =&gt; String.t(),
  :reply_ref =&gt; reference(),
  :journal =&gt; {module(), term()},
  optional(:conn_mod) =&gt; module(),
  optional(:ctx) =&gt; map(),
  optional(:actor) =&gt; map() | nil,
  optional(:surface) =&gt; atom(),
  optional(:capability) =&gt; binary() | nil,
  optional(:transport) =&gt; %{kind: atom(), peer: term()} | nil,
  optional(:from_offset) =&gt; non_neg_integer(),
  optional(:history_policy) =&gt; :from_offset | :tip | :none,
  optional(:offset_aware?) =&gt; boolean(),
  optional(:authorize) =&gt; (map() -&gt; {:ok, map()} | {:denied, atom()}),
  optional(:policy) =&gt; module(),
  optional(:start_subscriber) =&gt; (keyword() -&gt; {:ok, pid()}),
  optional(:session_sup) =&gt; pid() | nil,
  optional(:now) =&gt; integer()
}
```

Attach inputs. `:journal` is `{module, handle}` (the durable store, per-session
handle). `:authorize` is the authorization funnel; when absent it binds to
`AttachPolicy.Runner.authorize(policy, ctx)`, where `policy` is `:policy` or,
when that is absent, `AttachPolicy.default_policy/0` (the configured
`:attach_policy`, else `LocalNode`).

# `rider`

```elixir
@type rider() :: %{
  from_offset: non_neg_integer(),
  history_policy: :from_offset | :tip | :none,
  capability: binary() | nil
}
```

The parsed `_meta["raxol.io"]` attach rider.

# `attach`

```elixir
@spec attach(opts()) :: :deferred | {:error, Raxol.AgentClientProtocol.Error.t()}
```

Run the attach seam. Returns `:deferred` (the reply obligation was delegated
to the fresh subscriber, which replies) on a granted attach, or
`{:error, %Error{}}` (the deny envelope) on a denied one — the caller
(the `session/load` / `_raxol/session.load` handler) returns this value.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `deny_envelope`

```elixir
@spec deny_envelope() :: Raxol.AgentClientProtocol.Error.t()
```

The deny envelope: `-32000 "attach denied"`, NO `data` (anti-oracle).

# `parse_rider`

```elixir
@spec parse_rider(map() | nil) :: rider()
```

Parse the `_meta["raxol.io"]` rider map. Total: a missing/garbled rider
yields stock defaults (`from_offset: 0`, `history_policy: :from_offset`,
`capability: nil`). A present-but-non-string `capability` is passed through
verbatim so the policy's `is_binary` check denies it — garbling must never widen
access, only narrow it.

---

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