From 5ded720b7be5cad8ad5ce7cff8808f702ab67ce0 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sun, 1 Nov 2026 06:29:09 +0000 Subject: [PATCH] =?UTF-8?q?docs(adr):=20ADR-055=20=E2=80=94=20one=20identi?= =?UTF-8?q?ty=20per=20connection,=20dial=20after=20auth;=20resolve=20revie?= =?UTF-8?q?w=20014?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ADR-055: `set_identity` once-only is contract; no post-construction identity seam will be added (a mutable setter under the N-auth shared-handle case is last-writer-wins and would make `AccessControl::check` flap) - ruling: dial-after-auth (one fabric connection per authentication, identity set before wrapping) is the norm composition for per-auth doors; pre-dialed shared connections contract dial-time substrate identity with per-session gates as the door's job - scopes the payload `auth_token` path to its carve-out (alkhttp, git LFS); native consumers present key identity at establishment - review 014 resolved with the disposition correcting the framing: fact (a) reclassified as not-a-defect, fact (b) ruled; `take_call_connection` detach stays intentional - README ADR index + AGENTS.md range/list updated; no code change Verified: cargo test (701 passed), clippy -D warnings, fmt --check --- AGENTS.md | 7 +- docs/architecture/README.md | 5 +- .../055-one-identity-per-connection.md | 181 ++++++++++++++++++ ...construction-identity-seam-alkssh-input.md | 43 ++++- 4 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 docs/architecture/decisions/055-one-identity-per-connection.md diff --git a/AGENTS.md b/AGENTS.md index bce26bb..74b8d5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -241,7 +241,7 @@ directions: ## Architecture Context - `docs/architecture/` — the authoritative spec. Read it before - non-trivial changes. ADRs are numbered 001..054; OQs (open questions) + non-trivial changes. ADRs are numbered 001..055; OQs (open questions) track resolved/deferred decisions (OQ-01..43; OQ-43 tracks the composition layer's ownership and shape after the alknet replacement, deferred(scope)). @@ -338,6 +338,11 @@ directions: (verifier selection by `PeerEntry` presence; `RemoteIdentity` `None` semantics — re-owned in-crate from the source repo's alknet ADR-034; the dial/composition layer implements it) + - ADR-055 — one identity per connection, dial after auth + (`set_identity` once-only is contract; no post-construction + identity seam; per-auth doors dial one fabric connection per + authentication and set identity at establishment; resolves + review 014) - If a TODO references a "Phase B" or a design direction that an ADR has since decided against, the TODO is stale — remove it and align diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 3d1f22c..896f980 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-13 (composition re-key, ADR-054, alkcall reframing of ported docs) +last_updated: 2026-11-01 (ADR-055: one identity per connection ruling, review 014 resolution) --- # alkcall @@ -83,7 +83,7 @@ conventions: see the provenance note at the end of this document.) | [032](decisions/032-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Reversal cost classification | | [033](decisions/033-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Rust canonical, TS reference | -### Channels, pub/sub, and later amendments — ADR-034..054 +### Channels, pub/sub, and later amendments — ADR-034..055 | ADR | Title | Relevance | |-----|-------|-----------| @@ -108,6 +108,7 @@ conventions: see the provenance note at the end of this document.) | [052](decisions/052-channel-control-surface.md) | `channel/control` Control-Handle Surface | Establisher-contributed `ChannelControl` (`Establishment::with_control`), wrapper-installed/teardown-removed in the manager; `channel:no_control_handle` capability answer; resolves OQ-39 | | [053](decisions/053-resource-enumerators.md) | Live Resource Enumerators | `ResourceEnumerators` (peer-shared registry); snapshot-then-changes enumerator contract; scan-once aggregation; empty seed; resolves OQ-40 | | [054](decisions/054-outgoing-only-x509-three-remote-roles.md) | Outgoing-Only X.509 and the Three Remote Roles (Re-Owned) | Verifier-selection by `PeerEntry` presence; three remote roles; `RemoteIdentity` `None` semantics — re-owned in-crate (provenance: alknet ADR-034); resolves OQ-21's cross-repo authority gap | +| [055](decisions/055-one-identity-per-connection.md) | One Identity per Connection; Dial After Auth | `set_identity` once-only is contract — no post-construction identity seam; dial-after-auth is the per-auth-door norm; connection-per-auth composition; scopes token carriage to its carve-out; resolves review 014 | ## Relevant Open Questions diff --git a/docs/architecture/decisions/055-one-identity-per-connection.md b/docs/architecture/decisions/055-one-identity-per-connection.md new file mode 100644 index 0000000..ad366e1 --- /dev/null +++ b/docs/architecture/decisions/055-one-identity-per-connection.md @@ -0,0 +1,181 @@ +# ADR-055: One Identity per Connection; Dial After Auth + +## Status + +Accepted (2026-11-01). Resolves review 014 (post-construction identity +seam — consumer input from alkssh). No API change; this is a semantics +ruling. + +## Context + +Review 014 (`docs/reviews/014-post-construction-identity-seam-alkssh-input.md`) +filed two coupled findings from alkssh's SSH-server direction: + +**(a)** There is no non-detaching way to attach identity to a +connection after construction. `Connection::set_identity` is once-only +(`src/core/types.rs:541` — a second call errors); the only post-construction +route to a held `ChannelClient`'s connection is `take_call_connection` +(`src/channels/client.rs:521`), which detaches the client's calling +surface. The hub's establisher hook (`src/channels/hub_leg.rs:213`) has +an identity composition point, but it is accept-side, pre-serving. + +**(b)** The once-per-connection identity model does not fit "one fabric +connection, many per-session authentications" — an SSH door's shape: +the fabric connection can be dialed once by the assembly before any SSH +client exists, while russh's auth hooks fire per session and resolve +distinct `Identity` values per client key. + +The review framed (a) as a possible seam to build and (b) as the model +behind it, and listed three candidate resolutions: a non-detaching +seam, an identity-model decision (per-open carriage, connection-per-auth, +or a ruling that dial-time identity is substrate truth), and stating the +shared-handle corollary explicitly. + +What this ADR adds to the review's framing: **(a) and (b) are not +independent, and (a) is not a defect.** A mutable connection-level +identity setter would be last-writer-wins under N concurrent +authentications — the substrate's `AccessControl::check` +(ADR-024) would see whichever session authenticated last, racing +concurrent opens and flapping mid-connection, in a way that looks like +it works. The once-only slot is a load-bearing property, not a +limitation waiting for a seam: identity is set by whoever establishes +(or, for a door, whoever dials) the connection, at establishment time. +The alkssh review scenario that motivated (a) — a shared +`Arc` over one pre-dialed fabric connection serving all +sessions — is a consumer-side composition in which the fabric +connection, and therefore its identity, predates every auth it will +serve. Framed inside that composition, every solution is a patch on the +wrong layer. The correct composition for a per-auth door is to dial +after auth; then the once-only slot is exactly right and no +construction is shared. + +This is consistent with the crate's transport-agnostic posture: alkcall +does not care where the bytes come from (ADR-007 `from_stream`, +ADR-008/009 — TCP+TLS, QUIC, iroh, or "stream an alk protocol crate +over an SSH channel" are all equally out of scope at this layer). The +ruling below is about when an identity becomes part of a connection's +truth, not about any transport. + +## Decision + +### 1. A connection carries exactly one peer identity, consumed at establishment + +`Identity` (`{ id, scopes, resources }`, `src/core/auth.rs`) is set +once per `Connection` via `set_identity` and never changed. This +stays. **There is no post-construction identity mechanism, and none +will be added**: no non-detaching attach on a held `ChannelClient`, no +connection-level setter variant, no establisher-hook equivalent on the +consumer side that runs after serving starts. The absence is the +design. `take_call_connection`'s detach-on-take remains intentional +(ownership transfer, not identity surgery). + +The substrate truth for everything riding a connection is the identity +present at establishment. `AccessControl::check(peer_identity)` +(ADR-024) evaluates that identity and nothing else. + +### 2. Dial-after-auth composition rule (the per-auth door pattern) + +A door that must present a *per-session* resolved identity to the +substrate ACL dials one fabric connection per authentication and sets +the resolved identity at establishment (before wrapping in +`ChannelClient` / registering imported ops / spawning dispatch). N +authentications → N connections. Because the connection is dialed +after the auth it represents, the once-only slot is consumed exactly +once, with the correct value — no seam needed. + +This is the norm case for doors built like alkssh, not a workaround: +doors presenting N authentications-per-connection will always present +per-session auth; the connection is what the substrate authorizes, so +sessions that need distinct substrate identities ride distinct +connections. Native consumers hold real key material (e.g. the ed25519 +key a russh endpoint shares with the TLS and iroh endpoints); they +present it at establishment and never need token carriage. + +### 3. Connections that pre-date an auth: the substrate sees dial-time identity + +A door that dials once before any auth exists (shared +`Arc`) has chosen a composition in which the substrate +sees the assembly's dial-time identity. Per-session gates are then the +door's job. This is a valid posture when the substrate-side identity is +not the boundary the deployment cares about — the door-side gate is. +It is the contract now: documented, deliberate, and not silently +upgradeable — nothing in alkcall will let such a door claim a different +substrate identity later. + +### 4. Token carriage stays scoped to its carve-out + +The payload `auth_token` → `IdentityProvider` path (CF-005's serving +identity machinery, ADR-017 §7) exists for transports where in-band key +auth of the *end client* is impossible — alkhttp browser clients, git +LFS over HTTP. It is not the native path, and it does not extend onto +consumers that hold key material: `ChannelClient::{call_open_op, +pub_open}` gain no token parameter. A native consumer presents its +identity at establishment per §1–2. + +### 5. The shared-handle corollary, stated + +Doors that pre-dial serve N sessions over one connection by +composition, not by accident of the API. Whatever shape lands must +assume this exists as a norm (review 014 §point 3): `peer_identity` is +the *connection's* peer, and "one application session per connection" +is explicitly not assumed anywhere in this crate. + +## Consequences + +**Positive:** +- No API change, no wire change. The resolution is a ruling — walk-back + cost is doc edits only (ADR-032's cheap-reversal class). +- The once-only `IdentityAlreadySet` design is now load-bearing + contract rather than an incidental convenience; the review's "gap" + cannot recur as a feature request against it. +- Doors get an explicit choice: dial-after-auth (per-session substrate + identities) or pre-dial (door-side gates, dial identity at the + substrate). Both are contracts. +- Consistent with ADR-024 (peer identity = connection peer) and ADR-026 + (forwarded_for is metadata, never ACL authority — per-op carriage of + an *authenticated* end-client identity remains the token path's job + on transports that need it). + +**Negative:** +- Per-auth doors pay one fabric connection per session. Substrate + connections are cheap at this layer (no TLS config, no endpoint — + ADR-007); the assembly decides whether that cost matters. +- "One identity per connection" means a shared-connection door cannot + get substrate-side per-session ACL without changing its composition. + Consumers needing that must re-dial; this ADR does not soften §1. + +## Door type + +**Two-way** (ADR-032). No code, wire, or published-API change — a +semantics ruling documented in prose. If a real deployment shows +connection-per-auth is wrong for a door class, reversing this is a doc +edit plus, at most, a future ADR. (Contrast: a `set_identity`-style +safety-valve API would have been one-way — added surface is nearly +impossible to remove from a published crate once a consumer depends on +it, and it would have locked in the flapping-ACL behavior under the +shared-handle case.) + +## References + +- review 014 (`docs/reviews/014-post-construction-identity-seam-alkssh-input.md`) + — the consumer input this ADR resolves; alkssh's shipped no-detach + posture (`set_identity`/`take_call_connection` absent from its code) + is the §3 posture, now contract +- CF-005 / CF-006 (`docs/reviews/consumer-findings-ledger.md`) — the + connect-side serving-identity precedence chain (payload token → + `ServingConfig.identity` → transport identity) whose token tier §4 + scopes; the connection-half (§1) and channel-0-copy (§2) behaviors + already match this ruling +- ADR-024 — Peer-Graph Routing Model (AccessControl peer authorization; + `peer_identity` is the connection's peer) +- ADR-026 — Forwarded-For Identity (metadata, never ACL authority) +- ADR-017 §7 — the token path's origin (hub-forwarding / browser-token + fallback) +- ADR-003/006 — Identity/AuthToken/AuthContext structure (identity is + a resolved value; the key material stays transport-side) +- ADR-007/008/009 — transport-agnostic connection construction + (`from_stream`, `BidiStreamSource`, `BiStream`); the dial is + consumer-side, TLS is transport detail +- ADR-045 — the dial seam (spawn_dispatch / from_connection take-over; + dial lives in the consumer — the composition point §2 relies on) +- ADR-032 — One-Way Door Decision Framework (the two-way classification) \ No newline at end of file diff --git a/docs/reviews/014-post-construction-identity-seam-alkssh-input.md b/docs/reviews/014-post-construction-identity-seam-alkssh-input.md index 4dd89f9..e2a20fc 100644 --- a/docs/reviews/014-post-construction-identity-seam-alkssh-input.md +++ b/docs/reviews/014-post-construction-identity-seam-alkssh-input.md @@ -2,12 +2,53 @@ ## Status -Open — filed 2026-10-31. Consumer input from alkssh 0.1.0 (published +Resolved 2026-11-01 — ADR-055 +(`docs/architecture/decisions/055-one-identity-per-connection.md`). +No API change. See the Resolution section below before reading the +finding sections, which stand as filed (historical). + +Filed 2026-10-31. Consumer input from alkssh 0.1.0 (published 2026-10-31), under the family's upstream posture (file asks, never absorb silently). Booked alkssh-side at its wave-3 review gate (item 5) as the "non-detaching post-construction identity seam" note and carried unabsorbed through the alkssh 0.1.0 release; this is that filing. +## Resolution + +ADR-055 rules the review's §"What a resolution decides" as follows: + +- **Point 1 (the seam): denied.** Fact (a) is reclassified: not a + defect — a post-construction identity mechanism on a shared handle + would be last-writer-wins under the N-auth case this review itself + surfaced (fact (b)), making the substrate's `AccessControl::check` + see whichever session authenticated last. `set_identity`'s + once-only design is load-bearing contract. The review's §the finding + framing ("(a) no seam exists" / "(b) even with one, the + once-per-connection model does not fit") is resolved as: the + shared-handle composition is the thing that does not fit, and the + correct fix is consumer-side composition (dial-after-auth), not a + seam. +- **Point 2 (the identity model): the ruling.** A connection carries + exactly one peer identity, consumed at establishment; dial-after-auth + (one fabric connection per authentication, identity set before + wrapping) is the norm-case composition for per-auth doors; doors that + pre-dial have a contract — per-session gates are the door's job and + the substrate sees dial-time identity (the review's option 3, plus + connection-per-auth as the native composition). +- **Point 3 (the shared-handle corollary): stated** in ADR-055 §5 — + `peer_identity` means the connection's peer; "one application session + per connection" is not assumed anywhere. +- **Token carriage: out of scope for this class.** The ADR scopes the + payload `auth_token` path to its carve-out (alkhttp, git LFS — + transports where in-band key auth of the end client is impossible); + `ChannelClient` open surfaces gain no token parameter. + +alkssh's shipped posture (no-detach, door-side gates, dial-time +identity at the substrate) is now contract rather than "happened to +work." The alkssh-side wave-3 item 5 can close against this ADR; if a +real deployment later shows a door class where connection-per-auth does +not fit, ADR-055 is a two-way decision (doc-edit reversal cost). + This is the server-direction sibling of the identity-capture class the alkhttp ledger recorded as CF-005 ("no caller-identity capture on the connect-side serving path"): there, the transport-authenticated peer