diff --git a/docs/architecture/decisions/008-listen-metadata-forwarded-op.md b/docs/architecture/decisions/008-listen-metadata-forwarded-op.md index a13092c..01285b0 100644 --- a/docs/architecture/decisions/008-listen-metadata-forwarded-op.md +++ b/docs/architecture/decisions/008-listen-metadata-forwarded-op.md @@ -3,7 +3,9 @@ ## Status Accepted (2026-09-16); amended 2026-09-18 (the correlation token — -`listen` reply field + forwarded-params key, see Amendment 1) +`listen` reply field + forwarded-params key, see Amendment 1); amended +2026-11-03 (the forwarded op's serving-side establishment phase + the +correlation-miss vocabulary, see Amendment 2) ## Context @@ -185,9 +187,12 @@ the listen-opener** — accept-as-open, the `forwarded-tcpip` shape: exposing a listener and dialing egress are different capabilities. - The opener-side serving shape: `register_openable_with_establisher` as-is (no establisher — the forwarded channel needs no - establishment; its data plane starts immediately), ACL-gated by the - scope, handler = correlate the open (via `listen`, the correlation - token) to the waiting + establishment; its data plane starts immediately — **amended by + Amendment 2**: the served shape gains an establisher variant, the + injected correlation gate; this sentence records the pre-amendment + posture, which stays a valid shape — see Amendment 2), ACL-gated by + the scope, handler = correlate the open (via `listen`, the + correlation token) to the waiting session, accept the `BiStream`, pump against the local BIND client's halves. The correlation registry is assembly-side bookkeeping — the protocol provides the op and the params, not the @@ -338,4 +343,114 @@ establisher's reply surface (§2 gains `listen`). delta vs the pre-amendment ADR is exactly: the reply gains the optional `listen` field, the forwarded params rename `channel` → `listen` with token semantics. Nothing else shipped against either - (no consumer exists), so the change is pre-consumer and legal. \ No newline at end of file + (no consumer exists), so the change is pre-consumer and legal. + +## Amendment 2 (the serving-side establishment phase + the correlation-miss vocabulary, 2026-11-03) + +Resolves review 003 (`docs/reviews/003-correlation-miss-arrival-class-alkssh-input.md`) +via `docs/plans/003-correlation-miss-resolve.md` §Decisions 1–4. The +decision stands — bind-first establisher, accept-as-open, the +`bound`/`listen` reply fields, the forwarded op, `tunnel:forwarded`. +What changes: **the serving side of the forwarded op (§3) gains an +establishment phase** — §3's sentence "no establisher — the forwarded +channel needs no establishment; its data plane starts immediately" +(marked inline above) is the text this amendment amends. + +### The miss class this closes (the tri-vocabulary history) + +A forwarded open whose `listen` token no longer routes — the +stale/missing-token arrival class — is currently served +establisher-less everywhere (alkssh's door, alksocks' composed BIND, +and this crate's built-in shape). That is the one posture exception +to ADR-049's "a refused open is a typed call error, never a phantom +channel": the open succeeds at the call plane, declines post-open in +the assembly, and the producer-side pump EOFs with no signal. The +family has named the class three ways: + +- **alkssh** — `correlation-miss` at `debug`: + `observe::decline_arrival(row, "correlation-miss", listen)` + + teardown-at-birth (`src/tunnel_map.rs`; tokens removed at session + teardown by the shared per-fabric-leg `ListenRouter`). +- **alksocks** — prose at `warn`: "forwarded open with no waiting + session" (`src/composed/mod.rs:497`). +- **alktunnels** — the `handler_error`-class generic arm at `warn` + (the bind-first accept loop's failed-open branch, + `src/producer.rs:838-854`), which fires only for the + connection-gone/`NOT_FOUND`/ACL-refused subclass — an in-session + miss (route removed while the fabric connection lives) succeeds at + the call plane and logs nothing at all. + +Amendment 2 retires the prose variant and the fabric-side silence. + +### The decisions + +1. **The serving side gains an establisher variant — the gate is + injected.** The protocol crate cannot hold the assembly's + correlation map, so the establisher's check is an + assembly-provided gate closure over the forwarded params + (`TunnelForwardedParams`); the crate wraps that closure into the + alkcall `OpenEstablisher` dance and registers via + `register_openable_with_establisher` (alkcall 0.10.x, verified + present). Assemblies must NOT hand-roll the alkcall establisher + plumbing — the crate-owned registration surface is the only + supported path. The gate is a lookup, not a dial: establishment + stays fast, bounded by the usual establisher rules only. +2. **A stale/missing token fails the open as a typed + `channel:open_failed`.** The crate maps the gate's failure to + alkcall's `EstablishmentError::UnknownResource`, so the reply is + `channel:open_failed` with `details.reason = "unknown_resource"` — + no phantom channel is ever born (ADR-049's posture, extended here + to the forwarded op's serving side). The waiting session IS a + resource that is gone; **`unknown_resource` is reused as-is and + the wire vocabulary is NOT extended**: `wire.md`'s open-op sections + (the pinned `details.reason ∈ dial_failed / unknown_resource / + resource_shortage / handler_error / timeout` list and its + §The Forwarded Op) are untouched by this amendment — there is no + reason-class ADR, no new wire value. (The one serving-shape prose + line in `wire.md` — "no establisher" under §The Forwarded Op — + syncs with the API task's in-code doc updates, plan 003 W1; this + ADR does not touch `wire.md`.) +3. **Gate order is pinned: the scope gate precedes the establisher.** + The registry runs `tunnel:forwarded` ACL before the open op's + establishment phase (alkcall's open-op wrapper order: registry + gates — schema, then ACL — then the wrapper's allocate → + establisher → handler). A scope-less producer is `FORBIDDEN` + before any gate lookup; the gate never sees an unauthorized open. +4. **The residual post-open race keeps teardown-at-birth, named + `correlation-miss` at `debug`.** A token that vanishes between + establisher pass and handler run — the narrow race the gate cannot + close — keeps §3's opener-half posture, unchanged: the assembly + handler declines, its early return reaps the channel + teardown-at-birth (R-02). Its event name is **`correlation-miss`** + (alkssh's shipped vocabulary wins — it is published; alksocks' + prose variant retires), at `debug` **assembly-side** — the + protocol crate's built-in handler holds no map, so this is the + assemblies' convention this ADR records, not code this crate runs. + The producer-side generic arm (the accept loop's failed-open + branch) stays at `warn`, now carrying the typed reason from the + open failure instead of the prose one. + +### Additive-only surface + +The base `register_tunnel_forwarded_openable` (establisher-less, the +post-open shape) stays for compatibility; the establisher variant +(`register_tunnel_forwarded_openable_with_establisher`) is the +recommended posture, and new assemblies take it by default. Both ride +the same op id, params, scope, and reply surface — the wire delta of +this amendment is empty. + +### References + +- `docs/reviews/003-correlation-miss-arrival-class-alkssh-input.md` + (the ask; situation + framing this amendment resolves) +- `docs/plans/003-correlation-miss-resolve.md` §Decisions, §Verified + findings (the tri-vocabulary survey; W1 is this amendment + the API + task), §Ordering (W2 alkssh / W3 alksocks adoption waves) +- alkcall ADR-049 (the establishment phase and typed-error posture + extended here); alkcall 0.10.x + `register_openable_with_establisher` and + `EstablishmentError::UnknownResource → unknown_resource` (verified) +- alkssh `src/tunnel_map.rs` (`ListenRouter`, `decline_arrival`); + alksocks `src/composed/mod.rs:497`; alktunnels + `src/producer.rs:838-854` — the three miss sites this amendment + converges (source for §The miss class above) \ No newline at end of file diff --git a/tasks/tunnels/corrmiss-adr.md b/tasks/tunnels/corrmiss-adr.md index 3473322..e38afee 100644 --- a/tasks/tunnels/corrmiss-adr.md +++ b/tasks/tunnels/corrmiss-adr.md @@ -1,7 +1,7 @@ --- id: tunnels/corrmiss-adr name: "ADR: the forwarded op's establishment phase + the correlation-miss vocabulary (resolves review 003 — decisions)" -status: pending +status: completed depends_on: [] scope: narrow risk: medium @@ -77,18 +77,18 @@ format precedent). ## Acceptance Criteria -- [ ] Amendment 2 to ADR-008 (or the successor ADR) lands under +- [x] Amendment 2 to ADR-008 (or the successor ADR) lands under `docs/architecture/decisions/`, covering all four §Decisions and the tri-vocabulary history -- [ ] The amended/superseded §3 sentence is marked, not silently +- [x] The amended/superseded §3 sentence is marked, not silently replaced -- [ ] Wire-vocabulary non-extension is explicit (`unknown_resource` +- [x] Wire-vocabulary non-extension is explicit (`unknown_resource` reused; `wire.md` §The Open Ops untouched — state that) -- [ ] Status block of +- [x] Status block of `docs/reviews/003-correlation-miss-arrival-class-alkssh-input.md` is NOT updated here (the doc-sync task owns it — this task is the ADR itself) -- [ ] No code changes; doc build clean (`cargo doc --no-deps` clean is +- [x] No code changes; doc build clean (`cargo doc --no-deps` clean is NOT required for pure docs/ edits — verify md renders coherently instead) @@ -108,8 +108,48 @@ format precedent). ## Notes -> To be filled by implementation agent +- Shape chosen: **Amendment 2 to ADR-008** (the recommendation held — + it did not balloon; §4/§5 needed no restating). +- Before writing, verified the ground against the shipped sources: + alkcall 0.10.1 `EstablishmentError` (variants + `DialFailed`/`UnknownResource`/`ResourceShortage`/`HandlerError`; + `.reason()` maps `UnknownResource → "unknown_resource"`, + operations.rs:592-596) and `register_openable_with_establisher` + (operations.rs:761); the open-op wrapper order + (`policy.check_open` — the ACL — then allocate → establisher → + handler, operations.rs:1085-1103) pins the "scope gate precedes the + establisher" invariant. Cargo.toml pins `alkcall = "0.10.1"`, so + the ADR says "0.10.x, verified present". +- The amended §3 sentence (lines 187-196, the serving-shape bullet) + is marked inline with "**amended by Amendment 2**" — the + pre-amendment posture is recorded, not replaced (the Amendment 1 + style). +- Deliberately NOT touched: `wire.md` (the amendment states its + `details.reason` vocabulary is unextended; the one serving-shape + prose line — "no establisher" under §The Forwarded Op — syncs with + the API task per the amendment's note) and review 003's status + block (doc-sync task owns it). ## Summary -> To be filled on completion \ No newline at end of file +Amendment 2 landed at +`docs/architecture/decisions/008-listen-metadata-forwarded-op.md` +(2026-11-03): the forwarded op's serving side gains an establishment +phase — an assembly-provided gate closure (over +`TunnelForwardedParams`) the crate wraps into the alkcall +`OpenEstablisher` dance via `register_openable_with_establisher`, +with failures mapped to `EstablishmentError::UnknownResource` so a +stale/missing `listen` token is a typed +`channel:open_failed`/`unknown_resource` before any channel is born. +`unknown_resource` is reused as-is; the wire vocabulary is unextended +and `wire.md` is untouched. Gate order pinned (`tunnel:forwarded` ACL +precedes the establisher); the residual post-open race keeps +teardown-at-birth with the canonical assembly-side name +`correlation-miss` at `debug` (producer-side generic arm stays `warn` +with the typed reason); the base +`register_tunnel_forwarded_openable` stays, the establisher variant +is the recommended default posture; the wire delta of the amendment +is empty. The tri-vocabulary history (alkssh `correlation-miss` +debug, alksocks prose warn, alktunnels `handler_error`-class generic +warn + the fabric-side silence) is recorded with sources. Doc-only; +no code changes. \ No newline at end of file