From 7b8a3e755823fd6bdf5bff3e06773592d73068bb Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Tue, 13 Oct 2026 09:34:37 +0000 Subject: [PATCH] docs: spec-level sync with the single-publish fix + port-debt repair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec docs had not caught up with the stub-sweep fix commits (4eae18f..3317ea0), and the ported docs still carried alknet numbering that now collides with alkcall's own ADR range. Spec sync with the landed code: - call-protocol.md: register_imported/register_imported_all return Result and refuse marked specs; marked-spec registration refusal contract spelled out (ADR-047 am. 4 §4.8, ADR-022 am. 2026-10-13) - client-and-adapters.md: from_call flow gains the Pub sink-forwarding branch and the marked-op refusal (resolves OQ-37 — no from_call relay wrapper; sanctioned compositions are ChannelRelay::register_relay_openable / ChannelClient) - ADR-051: "channel/control is not implemented" stale sentence replaced with the ADR-052 per-leg control contract - OQ-37 resolved; OQ-22 split (primitive resolved; broker placement out of scope) with new OQ-42 for the downstream hub broker (deferred(scope), concretely blocked); OQ-24 resolved by the shipped add/strip surface; OQ-41 → deferred(scope) with blocking condition - README: OQ bullets renumbered to tracker IDs; alknet-collision note rewritten (range 001..053, alknet prefix convention); new principle 11 (implement-or-stop-advertising); AGENTS.md ADR range 001..053 Port-debt repair: - 149 broken decision links across 17 docs — ported with alknet numbers/paths, most now remapped to alkcall files by slug; the rest link the alknet repo per the README convention - stale OQ numbering rewritten to tracker IDs in call-protocol.md, call-README.md, operation-registry.md, client-and-adapters.md, ADR-025, channels-overview/README/wire, channel-client - unambiguous "alknet ADR-NNN" labels where an alknet number collides with an alkcall ADR (052 TTY vs channel/control, 033 storage vs rust-canonical, 025/026/027+020 vault vs PeerEntry/auth ADRs) Verified: cargo test (697 + guard), fmt, clippy --all-targets -D warnings; full markdown link scan clean. --- AGENTS.md | 10 +- docs/architecture/README.md | 50 +++-- docs/architecture/call-README.md | 86 ++++----- docs/architecture/call-protocol.md | 109 ++++++----- docs/architecture/channel-client.md | 10 +- docs/architecture/channel-operations.md | 2 +- docs/architecture/channels-README.md | 52 ++--- docs/architecture/channels-connection.md | 10 +- docs/architecture/channels-overview.md | 15 +- docs/architecture/channels-wire.md | 24 ++- docs/architecture/client-and-adapters.md | 181 ++++++++++-------- ...ction-from-stream-generic-single-stream.md | 2 +- .../decisions/008-bidistreamsource-trait.md | 4 +- ...ownership-for-runtime-spawned-resources.md | 6 +- ...pc-never-integrated-hand-rolled-framing.md | 4 +- ...7-privilege-model-and-authority-context.md | 4 +- .../020-abort-cascade-for-nested-calls.md | 4 +- ...ll-protocol-client-and-adapter-contract.md | 20 +- ...25-peerentry-and-identity-id-decoupling.md | 7 +- .../decisions/031-crate-decomposition.md | 2 +- ...40-backpressure-channel-limits-id-reuse.md | 2 +- .../045-alknetclient-native-dial-seam.md | 14 +- ...sh-operation-type-and-handler-kind-sink.md | 14 ++ .../051-channel-relay-and-hub-leg.md | 9 +- docs/architecture/open-questions.md | 40 ++-- docs/architecture/operation-registry.md | 83 ++++---- 26 files changed, 432 insertions(+), 332 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b9596b9..918755c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -238,7 +238,7 @@ directions: ## Architecture Context - `docs/architecture/` — the authoritative spec. Read it before - non-trivial changes. ADRs are numbered 001..050; OQs (open questions) + non-trivial changes. ADRs are numbered 001..053; OQs (open questions) track resolved/deferred decisions. - This crate unifies `alknet-call` and `alknet-channels` from the alknet mono-repo (`/workspace/@alkdev/alknet`). The source @@ -296,6 +296,14 @@ directions: `from_connection` primary; dial lives in the consumer) - ADR-041 — per-identity channel cap (256 per `PeerId` via `ChannelLifecyclePolicy`) + - ADR-052 — `channel/control` control-handle surface + (establisher-contributed `ChannelControl`; + `channel:no_control_handle` capability answer) + - ADR-053 — `channel/resources/subscribe` live resource enumerators + (snapshot-then-changes; scan-once aggregation) + - ADR-047 amendment 4 — the marked-pub open contract and the + registration invariants (§4.8): marked specs refuse the generic + registration paths; stubs are unregistrable **Shared (vendored core types):** - ADR-001 — ALPN-based protocol dispatch diff --git a/docs/architecture/README.md b/docs/architecture/README.md index bb01103..9a5f288 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-08-12 +last_updated: 2026-10-13 --- # alkcall @@ -112,16 +112,22 @@ are wire-stable and unchanged — see ADR-004. See [open-questions.md](open-questions.md) for the full tracker. Key questions affecting this crate: -- **OQ-01**: Call protocol pub/sub primitive (partially resolved) — - ADR-046 adds the `Pub` primitive (producer→consumer streaming). The - fan-out/broker is deferred to channels (Gap B in ADR-047 is named - out-of-scope for alkcall; the hub composes the broker on top). -- **OQ-02**: Full channel-level flow-control windowing (deferred(scope)) +- **OQ-22** (resolved) / **OQ-42** (deferred(scope)): The call-protocol + pub/sub primitive is delivered (ADR-046); the fan-out/message broker + is placed out of scope for alkcall — the hub composes it on the + `Pub`/`Sub` primitives — and its design remains deferred downstream, + blocked on a concrete producing-crate consumer (Gap B in ADR-047). +- **OQ-23** (deferred(scope)): Full channel-level flow-control windowing — bounded-buffer is decided (ADR-040); full windowing blocked on a real HOL-blocking deployment observation. -- **OQ-03**: Channels add/strip API shape (open) — whether the 8-byte - header add/strip is built into the read/write path or a standalone - utility. +- **OQ-24** (resolved): Channels add/strip API shape — integral to the + read/write path (handlers never see headers); low-level header + primitives stay in `channels::wire` for tooling. +- **OQ-37** (resolved): No `from_call` relay wrapper for marked ops — + marked announcements and marked-spec imports are refused (ADR-022 + amendment 2026-10-13, ADR-047 amendment 4 §4.8); the sanctioned + compositions are `ChannelRelay::register_relay_openable` (ADR-051) and + `ChannelClient`. ## Key Design Principles @@ -145,6 +151,14 @@ questions affecting this crate: channel 0. See ADR-036, ADR-037. 10. **Wire formats are stable**: EventEnvelope shape and the 8-byte chunk header are one-way doors. See ADR-014, ADR-034. +11. **Implement-or-stop-advertising**: every registration on a + discoverable surface must be capable of succeeding — no + advertised-always-failing bodies, no deferral buckets ("not this + session" is the only permitted deferral statement). Marked + (`channel_open`) specs refuse the generic registration paths + (ADR-047 amendment 4 §4.8); the `not_implemented` guard test is + release-blocking (`tests/stub_guard.rs`). See reviews 010/011 for + the incident that pinned this. ## Roles and Composition @@ -288,10 +302,14 @@ feature flags) or in the downstream alknet crate. > preserved here as the authoritative spec for alkcall; the alknet > mono-repo will consume alkcall's versions when it is reworked. > -> **Cross-references to non-ported ADRs**: Some spec docs and ADRs -> reference alknet ADRs by their original numbers (e.g., ADR-052 for -> TTY's wire format, ADR-082 for alknet-tls, ADR-086 for endpoint types). -> These are ADRs for sibling crates that are not part of alkcall. They -> retain their alknet numbering (052, 082, 086, etc.) — any ADR number -> outside the alkcall range 001..048 is an alknet source ADR, found at -> `/workspace/@alkdev/alknet/docs/architecture/decisions/`. \ No newline at end of file +> **Cross-references to non-ported ADRs**: alkcall's own ADRs are +> `decisions/001..053` in this directory. Some ported docs also cite +> sibling-crate ADRs that were **not** ported (e.g., alknet ADR-052 for +> TTY's wire format, alknet ADR-034 for outgoing-only X.509, alknet +> ADR-082 for alknet-tls, alknet ADR-086 for endpoint types). The alknet +> number space collides with alkcall's (alknet ADR-052 has nothing to do +> with alkcall's `channel/control` ADR-052), so unambiguous references +> carry the `alknet` prefix (e.g., "alknet ADR-052") and link to the +> alknet repo: `/workspace/@alkdev/alknet/docs/architecture/decisions/` +> (a few legacy cites without the prefix still mean the alknet ADR — +> check the link target). \ No newline at end of file diff --git a/docs/architecture/call-README.md b/docs/architecture/call-README.md index 3853f17..5304e6c 100644 --- a/docs/architecture/call-README.md +++ b/docs/architecture/call-README.md @@ -22,54 +22,54 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi |-----|-------|-----------| | [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN `alk/call` | | [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | CallAdapter implements ProtocolHandler | -| [003](decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-call depends on alknet-core (no irpc — ADR-014) | -| [013](decisions/013-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Adapter traits defined in Rust; TS is reference/browser adaptation | -| [004](decisions/004-auth-as-shared-core.md) | Auth as Shared Core | AuthContext passed to call handlers | -| [005](decisions/005-irpc-as-call-protocol-foundation.md) | ~~irpc as Call Protocol Foundation~~ | ~~Accepted~~ → **Superseded** by [ADR-014](decisions/064-irpc-never-integrated-hand-rolled-framing.md) (irpc was never integrated; framing is hand-rolled) | -| [064](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | Hand-Rolled EventEnvelope Framing | Wire format, registry, dispatch are hand-rolled in alknet-call; supersedes ADR-013 | -| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | Generic single-stream connections; unblocks TCP+TLS/SSH/WT/wasm dispatch | -| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | `alk/call` ALPN, one ALPN per connection | -| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | CallAdapter receives Connection, not BiStream | -| [008](decisions/008-secret-service-integration.md) | Vault Integration Point | Vault accessed at assembly layer, not on the wire | -| [010](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Static handler registration | -| [012](decisions/012-call-protocol-stream-model.md) | Call Protocol Stream Model | Bidirectional streams, EventEnvelope, ID-based correlation | -| [014](decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Call protocol carries no secret material; capabilities injected at assembly layer | -| [015](decisions/015-privilege-model-and-authority-context.md) | Privilege Model and Authority Context | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env | -| [016](decisions/016-abort-cascade-for-nested-calls.md) | Abort Cascade for Nested Calls | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in | -| [017](decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction | -| [066](decisions/066-from-jsonschema-as-http-adapter.md) | `from_jsonschema` as HTTP-Backed Single-Endpoint Adapter in alknet-http | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf | -| [022](decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Registration bundle carries provenance, composition authority, scoped env, capabilities | -| [023](decisions/023-operation-error-schemas.md) | Operation Error Schemas | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity | -| [024](decisions/024-operation-registry-layering.md) | Operation Registry Layering | Curated (static) + session/connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) | -| [028](decisions/028-callclient-peer-scoped-registry-filtering.md) | ~~Peer-Scoped Registry Filtering~~ | ~~Accepted~~ → **Superseded** by ADR-024 (flat-namespace single-peer model couldn't express head→N-workers; parallel auth system duplicated `AccessControl`) | -| [029](decisions/029-peer-graph-routing-model.md) | Peer-Graph Routing Model | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` | -| [030](decisions/030-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | `PeerId` source = `Identity.id` = `PeerEntry.peer_id` (stable); supersedes ADR-024's UUID source | -| [032](decisions/032-forwarded-for-identity.md) | Forwarded-For Identity | `forwarded_for` on `OperationContext` and `call.requested`; metadata only, never used by `AccessControl::check` | -| [033](decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Storage Boundary and Repo/Adapter Pattern | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | -| [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | The dial is in `alknet-client`; `CallClient` is `spawn_dispatch` only; `alknet-call` is a pure protocol crate with no TLS/transport deps | -| [091](decisions/091-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — Decouple Dial from Call Protocol | `ConnectionCredentials`/`RemoteIdentity` in `alknet-core` (not `alknet-call`); `auth_token` is a per-request payload field | +| [031](decisions/031-crate-decomposition.md) | Crate Decomposition | alknet-call depends on alknet-core (no irpc — ADR-014) | +| [033](decisions/033-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Adapter traits defined in Rust; TS is reference/browser adaptation | +| [003](decisions/003-auth-as-shared-core.md) | Auth as Shared Core | AuthContext passed to call handlers | +| [013](decisions/013-irpc-as-call-protocol-foundation.md) | ~~irpc as Call Protocol Foundation~~ | ~~Accepted~~ → **Superseded** by [ADR-014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) (irpc was never integrated; framing is hand-rolled) | +| [014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) | Hand-Rolled EventEnvelope Framing | Wire format, registry, dispatch are hand-rolled in alknet-call; supersedes ADR-013 | +| [007](decisions/007-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | Generic single-stream connections; unblocks TCP+TLS/SSH/WT/wasm dispatch | +| [004](decisions/004-alpn-convention-and-connection-model.md) | ALPN String Convention | `alk/call` ALPN, one ALPN per connection | +| [005](decisions/005-bistream-type-definition.md) | BiStream Type Definition | CallAdapter receives Connection, not BiStream | +| [008[008](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) | Vault Integration Point | Vault accessed at assembly layer, not on the wire | +| [010[010](/workspace/@alkdev/alknet/docs/architecture/decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Static handler registration | +| [015](decisions/015-call-protocol-stream-model.md) | Call Protocol Stream Model | Bidirectional streams, EventEnvelope, ID-based correlation | +| [010](decisions/010-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Call protocol carries no secret material; capabilities injected at assembly layer | +| [017](decisions/017-privilege-model-and-authority-context.md) | Privilege Model and Authority Context | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env | +| [020](decisions/020-abort-cascade-for-nested-calls.md) | Abort Cascade for Nested Calls | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in | +| [022](decisions/022-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction | +| [027](decisions/027-from-jsonschema-as-http-adapter.md) | `from_jsonschema` as HTTP-Backed Single-Endpoint Adapter in alknet-http | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf | +| [018](decisions/018-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Registration bundle carries provenance, composition authority, scoped env, capabilities | +| [016](decisions/016-operation-error-schemas.md) | Operation Error Schemas | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity | +| [019](decisions/019-operation-registry-layering.md) | Operation Registry Layering | Curated (static) + session/connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) | +| [023](decisions/023-callclient-peer-scoped-registry-filtering.md) | ~~Peer-Scoped Registry Filtering~~ | ~~Accepted~~ → **Superseded** by ADR-024 (flat-namespace single-peer model couldn't express head→N-workers; parallel auth system duplicated `AccessControl`) | +| [024](decisions/024-peer-graph-routing-model.md) | Peer-Graph Routing Model | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` | +| [025](decisions/025-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | `PeerId` source = `Identity.id` = `PeerEntry.peer_id` (stable); supersedes ADR-024's UUID source | +| [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | `forwarded_for` on `OperationContext` and `call.requested`; metadata only, never used by `AccessControl::check` | +| [033[033](/workspace/@alkdev/alknet/docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Storage Boundary and Repo/Adapter Pattern | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | +| [045](decisions/045-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | The dial is in `alknet-client`; `CallClient` is `spawn_dispatch` only; `alknet-call` is a pure protocol crate with no TLS/transport deps | +| [012](decisions/012-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — Decouple Dial from Call Protocol | `ConnectionCredentials`/`RemoteIdentity` in `alknet-core` (not `alknet-call`); `auth_token` is a per-request payload field | ## Relevant Open Questions | OQ | Title | Status | Relevance | |----|-------|--------|-----------| -| OQ-07 | Call protocol scope within a connection | resolved (ADR-015) | Stream model, multiplexing, scope | -| OQ-13 | Operation path format and routing scope | resolved | `/{service}/{op}` is the correct design; remote dispatch is a separate layer | -| OQ-14 | Batch operation semantics | resolved | Correlated `call.requested` events is the correct protocol design | -| OQ-16 | Safe vault operations for call protocol exposure | resolved (ADR-010) | None exposed for now | -| OQ-19 | Session-scoped operation registries | resolved | Agent-written operations overlaid on curated registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. Generalized by ADR-019 to cover connection-scoped overlays. | -| OQ-25 | ~~Remote-safe marking shape~~ | **dissolved** (ADR-024) | `remote_safe`/`trusted_peer` retired; peer authorization is `AccessControl::check(peer_identity)` | -| OQ-26 | OperationAdapter error type (AdapterError variants) | **resolved** | `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`; `#[non_exhaustive]` | -| OQ-27 | from_call re-import trigger | **resolved** | `from_call` is a manual free function; the assembly layer calls it after the dial (in `AlknetClient`). `refresh()` is a genuine feature addition. See ADR-028. | -| OQ-28 | from_call namespace collision | **resolved** | Same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays) | -| OQ-29 | CallClient TLS client-auth | **resolved** | Wire quinn client-auth; key-type-aware server cert verification; fingerprint normalization | -| OQ-30 | `PeerRef::Any` routing policy | **resolved** | Insertion-order first-match; richer routing is a feature extension | -| OQ-31 | `services/list-peers` re-export semantics | **resolved** | Opt-in `services/list-peers`; `services/list` is "own ops only" | -| OQ-32 | Multi-hop federation | open (feature extension) | One-hop model is the commitment; multi-hop is a feature extension, not a deferral | -| OQ-33 | PeerId — crypto identity vs stable logical id | **resolved** (ADR-025) | `PeerId = Identity.id = PeerEntry.peer_id` (stable across key rotation) | -| OQ-34 | Persistent peer registry | **resolved** (ADR-025+033) | Core trait + in-memory default; persistence adapters are separate crates | -| OQ-35 | ~~API key asymmetry~~ | **dissolved** | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity | -| OQ-37 | X.509 outgoing-only case | **resolved** (ADR-034) | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; verifier by `PeerEntry` presence | +| OQ-01 | Call protocol scope within a connection | resolved (ADR-015) | Stream model, multiplexing, scope | +| OQ-02 | Operation path format and routing scope | resolved | `/{service}/{op}` is the correct design; remote dispatch is a separate layer | +| OQ-03 | Batch operation semantics | resolved | Correlated `call.requested` events is the correct protocol design | +| OQ-04 | Session-scoped operation registries | resolved | Agent-written operations overlaid on curated registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. Generalized by ADR-019 to cover connection-scoped overlays. | +| OQ-09 | Safe vault operations for call protocol exposure | resolved (ADR-010) | None exposed for now | +| OQ-10 | ~~Remote-safe marking shape~~ | **dissolved** (ADR-024) | `remote_safe`/`trusted_peer` retired; peer authorization is `AccessControl::check(peer_identity)` | +| OQ-11 | OperationAdapter error type (AdapterError variants) | **resolved** | `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`; `#[non_exhaustive]` | +| OQ-12 | from_call re-import trigger | **resolved** | `from_call` is a manual free function; the assembly layer calls it after the dial (in `AlknetClient`). `refresh()` is a genuine feature addition. See ADR-028. | +| OQ-13 | from_call namespace collision | **resolved** | Same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays) | +| OQ-14 | CallClient TLS client-auth | **resolved** | Wire quinn client-auth; key-type-aware server cert verification; fingerprint normalization | +| OQ-15 | `PeerRef::Any` routing policy | **resolved** | Insertion-order first-match; richer routing is a feature extension | +| OQ-16 | `services/list-peers` re-export semantics | **resolved** | Opt-in `services/list-peers`; `services/list` is "own ops only" | +| OQ-17 | Multi-hop federation | deferred(scope) (feature extension) | One-hop model is the commitment; multi-hop is a feature extension, not a deferral | +| OQ-18 | PeerId — crypto identity vs stable logical id | **resolved** (ADR-025) | `PeerId = Identity.id = PeerEntry.peer_id` (stable across key rotation) | +| OQ-19 | Persistent peer registry | **resolved** (ADR-025; persistence adapters are alknet-side) | Core trait + in-memory default; persistence adapters are separate crates | +| OQ-20 | ~~API key asymmetry~~ | **dissolved** | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity | +| OQ-21 | X.509 outgoing-only case | **resolved** (alknet ADR-034, not ported) | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; verifier by `PeerEntry` presence | ## Key Design Principles diff --git a/docs/architecture/call-protocol.md b/docs/architecture/call-protocol.md index 72af52d..1e77589 100644 --- a/docs/architecture/call-protocol.md +++ b/docs/architecture/call-protocol.md @@ -129,18 +129,16 @@ pub struct CallConnection { impl CallConnection { /// Register an imported operation into this connection's overlay /// (Layer 2, ADR-019). Called by `from_call` after discovery. - pub fn register_imported(&self, registration: HandlerRegistration) { - let name = registration.spec.name.clone(); - self.imported_operations.write().insert(name, registration); - } + /// Refuses marked (`channel_open`) specs — they are not + /// overlay-importable (ADR-047 amendment 4 §4.8). + pub fn register_imported(&self, registration: HandlerRegistration) + -> Result<(), String>; /// Register multiple imported operations (bulk variant for `from_call`). - pub fn register_imported_all(&self, registrations: Vec) { - let mut overlay = self.imported_operations.write(); - for reg in registrations { - overlay.insert(reg.spec.name.clone(), reg); - } - } + /// Same marked-spec refusal; on refusal the entries inserted before the + /// marked one stay in the overlay and the error is returned. + pub fn register_imported_all(&self, registrations: Vec) + -> Result<(), String>; /// Build an `OperationEnv` impl for this connection's overlay. Used by /// the `CallAdapter` when composing the root `OperationContext.env`. @@ -167,6 +165,24 @@ Layer-0-only; runtime overlay registration uses `CallConnection` methods. When the connection drops, the overlay (and all imported ops) is dropped — no explicit deregistration needed. +**Marked-spec registration refusal** (ADR-047 amendment 4 §4.8; ADR-022 +amendment 2026-10-13): every generic registration path — +`OperationRegistry::register`, `CallConnection::register_imported`, +`register_imported_all`, and the `op/register` announcement path — refuses +a marked (`channel_open`) spec loudly at registration time. Marked ops are +channel-machinery ops (the data plane is the allocated channel's +`BiStream`); a marked spec can never land as an advertised-but-unusable +overlay body. The sanctioned marked-op imports are the channel-wrapper +family: `register_openable*`, `register_pub_openable`, +`register_relay_openable`. `op/register` rejects a marked announcement +with the typed error `MARKED_SPEC_UNSUPPORTED` (ADR-016 error schema), +naming `ChannelRelay::register_relay_openable` (relaying hub) and +`ChannelClient` (direct consumption) as the sanctioned compositions. +Unmarked `Sub`/`Pub` announcements register the real forwarding handler +shapes (ADR-022 amendment 2026-10-13). This closes the stub class: an +advertised op is always a capable one — implement-or-stop-advertising is +release-blocking (`tests/stub_guard.rs`). + The adapter: 1. Accepts bidirectional streams on the connection 2. Reads length-prefixed JSON `EventEnvelope` frames from each stream @@ -185,7 +201,7 @@ differs (accept vs dial). Peer authorization flows through the existing (ADR-024 §3). The composition env is peer-keyed (`PeerCompositeEnv`, ADR-024 §1) to handle head→N-workers routing. See [client-and-adapters.md](client-and-adapters.md) for the `Dispatcher` mechanism -and [ADR-024](decisions/029-peer-graph-routing-model.md) for the +and [ADR-024](decisions/024-peer-graph-routing-model.md) for the peer-graph routing model. ### Stream Model @@ -265,7 +281,7 @@ The `payload` of a `call.requested` event has this shape: - `operationId` — the operation to invoke, **with a leading slash** on the wire (e.g., `/fs/readFile`, `/agent/chat`, `/services/list`). This is the display form of the operation name. The registry stores names without the leading slash (`fs/readFile` — see [operation-registry.md](operation-registry.md#operationspec)); the wire format adds it. The `CallAdapter` strips the leading slash before registry lookup. - `input` — the operation input, matching the operation's `input_schema` (JSON Schema). Always a `serde_json::Value`. - `auth_token` — optional. If present, the `CallAdapter` resolves it via `IdentityProvider::resolve_from_token()` and the resulting `Identity` takes precedence over the connection-level identity for this request. See [Identity Resolution](#authcontext-and-identity-resolution) below. -- `forwarded_for` — optional (ADR-026). Present when a `from_call` forwarding handler propagates the originator's identity to a spoke. Carries a serialized `Identity` (id, scopes, resources) — the end user the hub authenticated. **Metadata only** — `AccessControl::check` never reads it; the spoke authorizes the hub (its direct caller), not the end user. The hub may set `forwarded_for: None` if it doesn't want to disclose the originator. See [ADR-026](decisions/032-forwarded-for-identity.md). +- `forwarded_for` — optional (ADR-026). Present when a `from_call` forwarding handler propagates the originator's identity to a spoke. Carries a serialized `Identity` (id, scopes, resources) — the end user the hub authenticated. **Metadata only** — `AccessControl::check` never reads it; the spoke authorizes the hub (its direct caller), not the end user. The hub may set `forwarded_for: None` if it doesn't want to disclose the originator. See [ADR-026](decisions/026-forwarded-for-identity.md). The `call.requested` payload does **not** carry an abort policy field. The abort policy (`abort-dependents` vs `continue-running`, ADR-020) is set on `OperationContext` and propagated through `OperationEnv::invoke()` — the composing handler decides the child's policy, not the wire caller. See [Abort Cascade and Nested Calls](#abort-cascade-and-nested-calls) below. @@ -558,7 +574,7 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is - The call protocol does not depend on any database. `PendingRequestMap` is in-memory. Durable session storage is a consumer concern. - Operation specs use JSON Schema. The envelope is always JSON. Binary payloads may be base64-encoded in the `payload` field. - Batch is not a protocol primitive — multiple `call.requested` events with correlated IDs provide equivalent semantics. See OQ-14. -- The call protocol is transport-agnostic at the envelope level. The `EventEnvelope` framing can run over QUIC streams, WebSocket frames, or Worker `postMessage`. The `CallAdapter` is the `ProtocolHandler` implementation that receives a `Connection` (any transport — ADR-007) and dispatches `EventEnvelope` frames. **The `EventEnvelope` shape (`{ type, id, payload }`) was derived from the `@alkdev/pubsub` `EventEnvelope` (`/workspace/@alkdev/pubsub/src/types.ts`), which already has a working WebSocket client/server implementation (`event-target-websocket-client.ts` / `event-target-websocket-server.ts`) and a generalized "event target" abstraction. The call protocol refined the envelope with typed event names (`call.requested`, `call.responded`, etc.) and structured payloads; the delta is small and well-defined, making a browser (and Node) WebSocket client straightforward to derive from the pubsub prior art. See ADR-044, [ADR-048](decisions/048-websocket-native-session-not-gateway.md), and [websocket.md](../http/websocket.md). +- The call protocol is transport-agnostic at the envelope level. The `EventEnvelope` framing can run over QUIC streams, WebSocket frames, or Worker `postMessage`. The `CallAdapter` is the `ProtocolHandler` implementation that receives a `Connection` (any transport — ADR-007) and dispatches `EventEnvelope` frames. **The `EventEnvelope` shape (`{ type, id, payload }`) was derived from the `@alkdev/pubsub` `EventEnvelope` (`/workspace/@alkdev/pubsub/src/types.ts`), which already has a working WebSocket client/server implementation (`event-target-websocket-client.ts` / `event-target-websocket-server.ts`) and a generalized "event target" abstraction. The call protocol refined the envelope with typed event names (`call.requested`, `call.responded`, etc.) and structured payloads; the delta is small and well-defined, making a browser (and Node) WebSocket client straightforward to derive from the pubsub prior art. See alknet ADR-044, [ADR-048](/workspace/@alkdev/alknet/docs/architecture/decisions/048-websocket-native-session-not-gateway.md), and [websocket.md](/workspace/@alkdev/alknet/docs/architecture/crates/http/websocket.md). - `OperationEnv::invoke()` dispatches through the local registry. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer. See ADR-014 and OQ-13. - **The call protocol carries no secret material.** Secret material (private keys, API keys, mnemonics, decrypted credentials, raw tokens) must not appear in `call.requested` payloads, `call.responded` payloads, or `OperationContext.metadata`. The wire format carries `serde_json::Value` and cannot enforce this at the type level — the constraint is architectural, enforced by the operation registry and by convention. Operations that need to share public key material use a dedicated operation that returns only the public component. See ADR-010. - **Abort cascades to descendants.** `call.aborted` for a parent request cascades to all non-terminal descendants in the call tree. Default policy is `abort-dependents`; `continue-running` is an opt-in. See ADR-020. @@ -567,19 +583,19 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is | Decision | ADR | Summary | |----------|-----|---------| -| Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled length-prefixed JSON framing, operation registry, dispatch; supersedes ADR-013 (irpc was never imported) | -| Call protocol stream model | [ADR-015](decisions/012-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | -| ALPN per connection | [ADR-004](decisions/006-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN | -| ProtocolHandler receives Connection | [ADR-005](decisions/007-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams | -| Vault integration point | [ADR-008](decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time | -| Secret material flow | [ADR-010](decisions/014-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer | -| Privilege model and authority context | [ADR-017](decisions/015-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env | -| Abort cascade for nested calls | [ADR-020](decisions/016-abort-cascade-for-nested-calls.md) | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in | -| Call protocol client and adapter contract | [ADR-022](decisions/017-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction. Client/adapter surface specced in [client-and-adapters.md](client-and-adapters.md) | -| Handler registration, provenance, and composition authority | [ADR-018](decisions/022-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle | -| Peer-graph routing model (supersedes ADR-023) | [ADR-024](decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` | -| Forwarded-for identity | [ADR-026](decisions/032-forwarded-for-identity.md) | `forwarded_for` field on `call.requested` and `OperationContext`; metadata only — `AccessControl::check` never reads it; the `from_call` handler populates it | -| Operation error schemas | [ADR-016](decisions/023-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details` | +| Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled length-prefixed JSON framing, operation registry, dispatch; supersedes ADR-013 (irpc was never imported) | +| Call protocol stream model | [ADR-015](decisions/015-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | +| ALPN per connection | [ADR-004](decisions/004-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN | +| ProtocolHandler receives Connection | [ADR-005](decisions/005-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams | +| Vault integration point | [ADR-008](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time | +| Secret material flow | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer | +| Privilege model and authority context | [ADR-017](decisions/017-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env | +| Abort cascade for nested calls | [ADR-020](decisions/020-abort-cascade-for-nested-calls.md) | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in | +| Call protocol client and adapter contract | [ADR-022](decisions/022-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction. Client/adapter surface specced in [client-and-adapters.md](client-and-adapters.md) | +| Handler registration, provenance, and composition authority | [ADR-018](decisions/018-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle | +| Peer-graph routing model (supersedes ADR-023) | [ADR-024](decisions/024-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` | +| Forwarded-for identity | [ADR-026](decisions/026-forwarded-for-identity.md) | `forwarded_for` field on `call.requested` and `OperationContext`; metadata only — `AccessControl::check` never reads it; the `from_call` handler populates it | +| Operation error schemas | [ADR-016](decisions/016-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details` | | Streaming handler for subscriptions | [ADR-021](decisions/021-streaming-handler-for-subscriptions.md) | `StreamingHandler` type, `invoke_streaming()` dispatch path, `INVALID_OPERATION_TYPE` protocol code; the server-side streaming branch in `handle_stream` | | Publish operation type and HandlerKind::Sink | [ADR-046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | `OperationType::Pub` (producer→consumer streaming); `SinkHandler` + `HandlerKind::Sink`; `call.published` wire event; `invoke_sink()` dispatch; `Subscription` renamed to `Sub` | @@ -587,45 +603,48 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is See [open-questions.md](open-questions.md) for full details. -- **OQ-13** (resolved): Operation path format is `/{service}/{op}`. Remote dispatch is a separate mechanism, not a path prefix. -- **OQ-14** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive. -- **OQ-16** (resolved by ADR-010): No vault operations are exposed over the call protocol for now. -- **OQ-19** (resolved): Session-scoped operation registries — agent-written operations overlaid on global registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. -- **OQ-25** (dissolved by ADR-024): `remote_safe` marking shape — moot; +- **OQ-02** (resolved): Operation path format is `/{service}/{op}`. Remote dispatch is a separate mechanism, not a path prefix. +- **OQ-03** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive. +- **OQ-09** (resolved by ADR-010): No vault operations are exposed over the call protocol for now. +- **OQ-04** (resolved): Session-scoped operation registries — agent-written operations overlaid on global registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. +- **OQ-10** (dissolved by ADR-024): `remote_safe` marking shape — moot; `remote_safe`/`trusted_peer` retired; peer authorization is `AccessControl::check(peer_identity)`. -- **OQ-26** (resolved): `OperationAdapter` error type — `AdapterError` +- **OQ-11** (resolved): `OperationAdapter` error type — `AdapterError` variants (`DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`); `#[non_exhaustive]`. See [client-and-adapters.md](client-and-adapters.md). -- **OQ-27** (resolved): `from_call` re-import trigger — `from_call` is a +- **OQ-12** (resolved): `from_call` re-import trigger — `from_call` is a manual free function; the assembly layer calls it after the dial (in `AlknetClient`). See - [ADR-028](decisions/069-from-call-manual-free-function.md). -- **OQ-28** (resolved): `from_call` namespace collision — same-peer collision + [ADR-028](decisions/028-from-call-manual-free-function.md). +- **OQ-13** (resolved): `from_call` namespace collision — same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays). See [client-and-adapters.md](client-and-adapters.md). -- **OQ-29** (resolved): `CallClient` TLS client-auth — wire quinn client-auth +- **OQ-14** (resolved): `CallClient` TLS client-auth — wire quinn client-auth (present Ed25519 key as raw public key client cert); key-type-aware server cert verification; fingerprint normalization. See [client-and-adapters.md](client-and-adapters.md). -- **OQ-30** (resolved): `PeerRef::Any` routing policy — insertion-order +- **OQ-15** (resolved): `PeerRef::Any` routing policy — insertion-order first-match. See [client-and-adapters.md](client-and-adapters.md). -- **OQ-31** (resolved): `services/list-peers` re-export semantics — opt-in; +- **OQ-16** (resolved): `services/list-peers` re-export semantics — opt-in; `services/list` is "own ops only." See [client-and-adapters.md](client-and-adapters.md). -- **OQ-32** (open, feature extension): Multi-hop federation — the one-hop +- **OQ-17** (deferred(scope), feature extension): Multi-hop federation — the one-hop model is the architectural commitment; multi-hop is a feature extension that doesn't break downstream. See [client-and-adapters.md](client-and-adapters.md). -- **OQ-33** (resolved by ADR-025): `PeerId` source — `Identity.id` from +- **OQ-18** (resolved by ADR-025): `PeerId` source — `Identity.id` from `IdentityProvider` resolution (= `PeerEntry.peer_id`, stable across key rotation), not a connection-assigned UUID. -- **OQ-34** (resolved by ADR-025 + ADR-033): Persistent peer registry — +- **OQ-19** (resolved by ADR-025): Persistent peer registry — the storage boundary is `core trait + in-memory default`; persistence - adapters are separate crates. -- **OQ-37** (resolved by ADR-034): X.509 outgoing-only case — three remote + adapters are separate crates (the concrete adapter shapes were named in + alknet research, not re-specced here). +- **OQ-21** (resolved): X.509 outgoing-only case — three remote roles named (public X.509 endpoint, transport relay, hub); pure-client - X.509 connections are not in the peer graph on the client side. See - [client-and-adapters.md](client-and-adapters.md). + X.509 connections are not in the peer graph on the client side. The + deciding ADR (alknet ADR-034, "Outgoing-Only X.509 and Three Peer + Roles") was not ported into alkcall; the surviving alkcall surface is + ADR-025's `PeerEntry` asymmetry. See [client-and-adapters.md](client-and-adapters.md). ## References diff --git a/docs/architecture/channel-client.md b/docs/architecture/channel-client.md index df1e936..c800309 100644 --- a/docs/architecture/channel-client.md +++ b/docs/architecture/channel-client.md @@ -185,7 +185,7 @@ request/response role. `ChannelClient`'s *API* is transport-agnostic — `from_connection` takes a pre-established `Connection`. The shared *dial+TLS* seam -(`AlknetClient`, OQ-55) is [`alknet-client`](../client/README.md), which +(`AlknetClient`, alknet OQ-55) is [`alknet-client`](/workspace/@alkdev/alknet/docs/architecture/crates/client/README.md), which provides `AlknetClient` with three dial methods (`dial_quic` / `dial_tcp_tls` / `dial_iroh`), each producing a `Connection` that `from_connection` consumes. The dial is transport-specific (QUIC, @@ -195,8 +195,8 @@ transport-agnostic. The two concerns are separated. `AlknetClient::dial_quic` is the dial that feeds `from_connection`. A caller that needs transport selection (QUIC with TCP+TLS fallback) uses `AlknetClient` directly; the fallback policy is a caller concern. See -[ADR-045](decisions/089-alknetclient-native-dial-seam.md) for the -full decision and [OQ-55](../../questions/055-alknetclient-establishment-extraction.md) +[ADR-045](decisions/045-alknetclient-native-dial-seam.md) for the +full decision and [OQ-55](/workspace/@alkdev/alknet/docs/architecture/questions/055-alknetclient-establishment-extraction.md) (resolved). ## Design Decisions @@ -210,7 +210,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). ## Open Questions -- **OQ-55** (resolved by ADR-045): `AlknetClient` core **dial+TLS seam** +- **alknet OQ-55** (resolved by ADR-045): `AlknetClient` core **dial+TLS seam** — `alknet-client` with three dial methods. `ChannelClient`'s API is transport-agnostic (`from_connection`); the dial is the shared seam. See [ADR-045](decisions/045-alknetclient-native-dial-seam.md). @@ -225,6 +225,6 @@ All design decisions are documented as ADRs in [decisions/](decisions/). - ADR-039: ChannelManager (the shared state `ChannelClient` holds) - ADR-047: openable ALPNs are operations (per-ALPN open ops dissolve the generic `channel/open`) -- OQ-55: AlknetClient / client establishment extraction +- alknet OQ-55: AlknetClient / client establishment extraction - `docs/architecture/crates/call/client-and-adapters.md` — `CallClient` (the shape `ChannelClient` mirrors) \ No newline at end of file diff --git a/docs/architecture/channel-operations.md b/docs/architecture/channel-operations.md index 4070993..4a209f2 100644 --- a/docs/architecture/channel-operations.md +++ b/docs/architecture/channel-operations.md @@ -340,7 +340,7 @@ follows is on channel 0 and is ordered after the data pump completes the channels layer has no `stream_type` concept — it carries the handler's framing transparently in the payload. TTY's `STREAM_CTRL_IN` (stream_type 3) and `STREAM_CTRL_OUT` (stream_type 4) are stream_types -in TTY's 5-byte format (ADR-052, amended by Phase 7), not +in TTY's 5-byte format (alknet ADR-052, amended by Phase 7), not channels-layer concepts. The channels layer routes by `channel_id` only; the handler owns its sub-stream multiplexing on the `BiStream` it receives. diff --git a/docs/architecture/channels-README.md b/docs/architecture/channels-README.md index 66313d4..9a474b5 100644 --- a/docs/architecture/channels-README.md +++ b/docs/architecture/channels-README.md @@ -20,7 +20,7 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. | Document | Status | Description | |----------|--------|-------------| -| [overview.md](overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, ALPN, transport agnosticism, WASM, relationship to existing crates | +| [overview.md](channels-overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, ALPN, transport agnosticism, WASM, relationship to existing crates | | [channels-wire.md](channels-wire.md) | draft | The 8-byte chunk format (`[channel_id:u32 be][length:u32 be][payload]`), the add/strip composition, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) | | [channels-connection.md](channels-connection.md) | draft | `ChannelBidiStreamSource` (implements `BidiStreamSource` — ADR-008/074, as amended by ADR-035), `accept_bi` yields one `BiStream` per channel, recursive composition | | [channels-adapter.md](channels-adapter.md) | draft | `ChannelsAdapter` (`ProtocolHandler` on `alk/channels`), `ChannelManager`, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) | @@ -31,35 +31,35 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. | ADR | Title | Relevance | |-----|-------|-----------| -| [071](decisions/071-channels-wire-format.md) | channels Wire Format — 8-Byte Chunk Header | The chunk format; channels layer has no `stream_type` concept (amended by ADR-035); substrate-agnostic; one-way door | -| [093](decisions/093-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `into_sub_streams` removed, `BiStream`-only, TTY always 5-byte | -| [072](decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alk/call` | Channel 0 = call protocol; no special control plane | -| [073](decisions/073-channel-lifecycle-operations.md) | Channel Lifecycle Operations on the Call Protocol | `channel/open`/`close`/`control`/`resources/subscribe`; `direction` semantics; subscribe not poll | -| [074](decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel `BidiStreamSource` impl; `accept_bi` yields `BiStream` (amended by ADR-035 — `into_sub_streams` removed) | -| [075](decisions/075-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts | -| [076](decisions/076-backpressure-channel-limits-id-reuse.md) | Backpressure, Channel Limits, and ID Reuse | Bounded-buffer (1 MiB default), 256-channel per-connection memory bound, monotonic IDs with wrap (DoS defense reframed by ADR-041) | -| [094](decisions/094-per-identity-channel-cap.md) | Per-Identity Channel Cap | 256 per `PeerId`, enforced via `ChannelLifecyclePolicy` in `channels-call`; per-connection `max_channels` reframed as a memory bound; symmetric (both sides enforce); spoke caps hub (direct caller), not browser (forwarded_for is metadata) | -| [077](decisions/077-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); TTY always uses its 5-byte format, carried transparently in the channels payload | -| [078](decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer | -| [079](decisions/079-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite | -| [080](decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045, resolves OQ-55) | -| [081](decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers, not sub-crates | -| [070](decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements | -| [092](decisions/092-bistream-as-the-handler-leaf.md) | `BiStream` as the Handler Leaf | `accept_bi` returns `BiStream`; the transport-leaf decision ADR-035 builds on | -| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on | -| [052](decisions/052-alknet-tty-wire-format-and-two-carriage.md) | alknet-tty Wire Format | The 5-byte format carried transparently in the channels payload (control bidirectional via `STREAM_CTRL_IN`/`OUT` — Phase 7 amendment) | -| [049](decisions/049-streaming-handler-for-subscriptions.md) | StreamingHandler for Subscriptions | The machinery `channel/resources/subscribe` uses | -| [032](decisions/032-forwarded-for-identity.md) | Forwarded-For Identity | The auth chain for hub-relayed channel opens | -| [003](decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-channels depends on alknet-core only; no handler-depends-on-handler | +| [034](decisions/034-channels-wire-format.md) | channels Wire Format — 8-Byte Chunk Header | The chunk format; channels layer has no `stream_type` concept (amended by ADR-035); substrate-agnostic; one-way door | +| [035](decisions/035-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `into_sub_streams` removed, `BiStream`-only, TTY always 5-byte | +| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alk/call` | Channel 0 = call protocol; no special control plane | +| [037](decisions/037-channel-lifecycle-operations.md) | Channel Lifecycle Operations on the Call Protocol | `channel/open`/`close`/`control`/`resources/subscribe`; `direction` semantics; subscribe not poll | +| [038](decisions/038-channelconnection-bidistreamsource.md) | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel `BidiStreamSource` impl; `accept_bi` yields `BiStream` (amended by ADR-035 — `into_sub_streams` removed) | +| [039](decisions/039-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts | +| [040](decisions/040-backpressure-channel-limits-id-reuse.md) | Backpressure, Channel Limits, and ID Reuse | Bounded-buffer (1 MiB default), 256-channel per-connection memory bound, monotonic IDs with wrap (DoS defense reframed by ADR-041) | +| [041](decisions/041-per-identity-channel-cap.md) | Per-Identity Channel Cap | 256 per `PeerId`, enforced via `ChannelLifecyclePolicy` in `channels-call`; per-connection `max_channels` reframed as a memory bound; symmetric (both sides enforce); spoke caps hub (direct caller), not browser (forwarded_for is metadata) | +| [077[077](/workspace/@alkdev/alknet/docs/architecture/decisions/077-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); TTY always uses its 5-byte format, carried transparently in the channels payload | +| [050](decisions/050-pump-bidi-two-pump-helper.md) | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer | +| [042](decisions/042-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite | +| [043](decisions/043-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045, resolves OQ-55) | +| [044](decisions/044-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers, not sub-crates | +| [008](decisions/008-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements | +| [009](decisions/009-bistream-as-the-handler-leaf.md) | `BiStream` as the Handler Leaf | `accept_bi` returns `BiStream`; the transport-leaf decision ADR-035 builds on | +| [007](decisions/007-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on | +| [052[052](/workspace/@alkdev/alknet/docs/architecture/decisions/052-alknet-tty-wire-format-and-two-carriage.md) | alknet-tty Wire Format | The 5-byte format carried transparently in the channels payload (control bidirectional via `STREAM_CTRL_IN`/`OUT` — Phase 7 amendment) | +| [021](decisions/021-streaming-handler-for-subscriptions.md) | StreamingHandler for Subscriptions | The machinery `channel/resources/subscribe` uses | +| [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | The auth chain for hub-relayed channel opens | +| [031](decisions/031-crate-decomposition.md) | Crate Decomposition | alknet-channels depends on alknet-core only; no handler-depends-on-handler | ## Relevant Open Questions | OQ | Title | Status | Relevance | |----|-------|--------|-----------| -| OQ-55 | AlknetClient / Client Establishment Extraction | resolved (ADR-045) | `ChannelClient`'s API is decided (ADR-043): transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (`alknet-client`, ADR-045) | -| OQ-56 | Full channel-level flow-control windowing | deferred(scope) | Bounded-buffer is decided (ADR-040); full windowing is an extension blocked on "a real deployment observes HOL blocking on a saturated channel where bounded buffer is insufficient" | -| OQ-57 | Two-pump helper extraction to alknet-core | deferred(scope) | The shutdown-on-completion *contract* is decided (ADR-078); the *helper* extraction is blocked on a second two-pump handler existing (shape convergence) | -| OQ-68 | Add/strip API shape (built-in vs utility) | open | Whether the 8-byte header add/strip is built into the channels read/write path or exposed as a standalone utility. The *contract* is decided (ADR-035); the *function surface* is not | +| alknet OQ-55 | AlknetClient / Client Establishment Extraction | resolved (ADR-045) | `ChannelClient`'s API is decided (ADR-043): transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (`alknet-client`, ADR-045) | +| OQ-23 (tracker; alknet OQ-56) | Full channel-level flow-control windowing | deferred(scope) | Bounded-buffer is decided (ADR-040); full windowing is an extension blocked on "a real deployment observes HOL blocking on a saturated channel where bounded buffer is insufficient" | +| alknet OQ-57 | Two-pump helper extraction to alknet-core | deferred(scope) (alknet-scope) | The shutdown-on-completion *contract* is decided and the helper landed in-tree (`channels::pump_bidi`, ADR-050); any alknet-core extraction is alknet-scope | +| OQ-24 (tracker; alknet OQ-68) | Add/strip API shape (built-in vs utility) | resolved | Whether the 8-byte header add/strip is built into the channels read/write path or exposed as a standalone utility. The *contract* is decided (ADR-035); the *function surface* is not | ## Key Design Principles @@ -69,7 +69,7 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. in how they're *opened* (negotiation via `channel/open` on channel 0) and what *multiplexing layer* carries them (the 8-byte chunk format). Once normalized, every channel is an ALPN routed through the same - `HandlerRegistry`. See [overview.md](overview.md) and ADR-034 (as + `HandlerRegistry`. See [overview.md](channels-overview.md) and ADR-034 (as amended by ADR-035). 2. **Channel 0 is `alk/call` pre-negotiated, not a special control diff --git a/docs/architecture/channels-connection.md b/docs/architecture/channels-connection.md index 73d1ce0..8acaa22 100644 --- a/docs/architecture/channels-connection.md +++ b/docs/architecture/channels-connection.md @@ -158,11 +158,11 @@ All design decisions are documented as ADRs in [decisions/](decisions/). | ADR | Decision | Summary | |-----|----------|---------| -| [074](decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel `BidiStreamSource`; yield-once `accept_bi` is the only accessor | -| [093](decisions/093-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `BiStream`-only | -| [070](decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | The extension point `ChannelBidiStreamSource` implements | -| [092](decisions/092-bistream-as-the-handler-leaf.md) | `BiStream` as the Handler Leaf | `accept_bi` returns `BiStream` (the transport-leaf decision this doc builds on) | -| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The yield-once path generalized for channels | +| [038](decisions/038-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel `BidiStreamSource`; yield-once `accept_bi` is the only accessor | +| [035](decisions/035-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `BiStream`-only | +| [008](decisions/008-bidistreamsource-trait.md) | BidiStreamSource Trait | The extension point `ChannelBidiStreamSource` implements | +| [009](decisions/009-bistream-as-the-handler-leaf.md) | `BiStream` as the Handler Leaf | `accept_bi` returns `BiStream` (the transport-leaf decision this doc builds on) | +| [007](decisions/007-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The yield-once path generalized for channels | ## References diff --git a/docs/architecture/channels-overview.md b/docs/architecture/channels-overview.md index db1d258..0bdf6ab 100644 --- a/docs/architecture/channels-overview.md +++ b/docs/architecture/channels-overview.md @@ -89,7 +89,7 @@ on the `BiStream` the channels layer gives them (ADR-035). inner layer's framing byte, carried transparently in the payload. - **The control channel is handler-internal.** TTY sub-demuxes control from its io `BiStream` using its 5-byte format (`STREAM_CTRL_IN` / - `STREAM_CTRL_OUT` — ADR-052 amended by Phase 7). The channels layer + `STREAM_CTRL_OUT` — alknet ADR-052 amended by Phase 7). The channels layer doesn't carry control. - **Recursive composition is literal.** A channel with ALPN `alk/channels` runs another channels demux on its `BiStream`. The @@ -270,18 +270,19 @@ All design decisions are documented as ADRs in [decisions/](decisions/). Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this crate: -- **OQ-55** (resolved by ADR-045): `AlknetClient` core **dial+TLS seam** +- **alknet OQ-55** (resolved by ADR-045): `AlknetClient` core **dial+TLS seam** — extracted as `alknet-client` with three dial methods. `ChannelClient`'s API is transport-agnostic (`from_connection`); the dial is the shared seam, now extracted. See [ADR-045](decisions/045-alknetclient-native-dial-seam.md). -- **OQ-56** (deferred(scope)): Full channel-level flow-control windowing — +- **OQ-23** (tracker; alknet OQ-56) (deferred(scope)): Full channel-level flow-control windowing — bounded-buffer is decided (ADR-040); full windowing is an extension blocked on a real HOL-blocking deployment observation. -- **OQ-57** (deferred(scope)): Two-pump helper extraction to alknet-core — - the *contract* is decided (ADR-078); the *helper* is blocked on a second - two-pump handler existing. -- **OQ-68** (open): Add/strip API shape — whether the 8-byte header +- **alknet OQ-57** (deferred(scope)): Two-pump helper extraction to alknet-core — + the *contract* is decided (the in-tree helper landed as + `channels::pump_bidi`, ADR-050); any extraction to alknet-core is + alknet-scope. +- **OQ-24** (tracker; alknet OQ-68) (resolved): Add/strip API shape — whether the 8-byte header add/strip is built into the channels read/write path or exposed as a standalone utility. The *contract* (channels strips, handler parses payload) is decided (ADR-035); the *function surface* is not. \ No newline at end of file diff --git a/docs/architecture/channels-wire.md b/docs/architecture/channels-wire.md index e1d8bd7..a57ff5b 100644 --- a/docs/architecture/channels-wire.md +++ b/docs/architecture/channels-wire.md @@ -36,7 +36,7 @@ hand-rolled (the hot path); the BAST document is the contract. The payload is opaque to the channels layer. The handler parses its own framing from the payload — TTY's `[stream_type:u8][length:u32][payload]` -(5-byte format, ADR-052), call's length-prefixed JSON (`EventEnvelope` +(5-byte format, alknet ADR-052), call's length-prefixed JSON (`EventEnvelope` framing, ADR-014), tunnel's raw bytes, SSH's channel protocol. The channels layer carries the bytes transparently. @@ -72,7 +72,7 @@ its framing entirely. See ADR-035 for the full cost/benefit analysis. ## `MAX_CHUNK_LEN` -`16 * 1024 * 1024` (16 MiB), matching TTY's cap (ADR-052 §5). A chunk with +`16 * 1024 * 1024` (16 MiB), matching TTY's cap (alknet ADR-052 §5). A chunk with `length > MAX_CHUNK_LEN` returns `ChunkTooLarge` and does not corrupt the stream — the demux drops the chunk and continues. The header is always exactly 8 bytes, so the demux can always resync by reading the next @@ -110,7 +110,7 @@ which is internal to TTY's 5-byte format). A zero-length chunk (`length = 0`) is delivered as an empty payload, which the reassembled stream interprets as EOF. This is the clean-shutdown -signal for a `channel_id` — the same convention as TTY (ADR-052 +signal for a `channel_id` — the same convention as TTY (alknet ADR-052 §Sentinels), now at the channels layer (one sentinel per channel, not per `(channel_id, stream_type)`). @@ -245,9 +245,13 @@ same shape, each level. The exact API shape of the add/strip pair (built into the read/write path vs. a standalone utility) is an implementation detail for the channels -crate, tracked as OQ-68. The *contract* — the channels layer strips its -8-byte header on read and the handler parses its own framing from the -payload — is decided; the *function surface* is not. +crate; the shipped surface resolves it (OQ-24, formerly alknet OQ-68): +the strip is integrated into the read/write path — the demux loop +consumes `read_header`/`write_chunk` so a handler's `BiStream` yields +payload chunks and never sees the 8-byte header — while the low-level +primitives (`parse_header`/`write_header`/`read_header`/`write_chunk`/ +`write_eof` in `channels::wire`) stay exposed as the standalone utility +for tooling. ## Channel lifecycle (summary) @@ -293,10 +297,10 @@ All design decisions are documented as ADRs in [decisions/](decisions/). Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this doc: -- **OQ-68** (open): Add/strip API shape — whether the 8-byte header - add/strip is built into the channels read/write path or exposed as a - standalone utility. The *contract* (channels strips, handler parses - payload) is decided; the *function surface* is not. +- **OQ-24** (tracker; alknet OQ-68) (resolved): Add/strip API shape — + built into the read/write path (the demux loop strips headers; handlers + never see them), with the low-level header primitives exposed in + `channels::wire` for tooling. ## References diff --git a/docs/architecture/client-and-adapters.md b/docs/architecture/client-and-adapters.md index 229e097..073c351 100644 --- a/docs/architecture/client-and-adapters.md +++ b/docs/architecture/client-and-adapters.md @@ -40,7 +40,7 @@ This document specifies three components, all in `alknet-call`: > It is now an HTTP-backed single-endpoint adapter for non-standard / > non-OpenAPI / basic REST endpoints, functionally similar to > `from_openapi` but one endpoint at a time. See -> [`crates/http/http-adapters.md`](../http/http-adapters.md) §"from_jsonschema". +> [`crates/http/http-adapters.md`](/workspace/@alkdev/alknet/docs/architecture/crates/http/http-adapters.md) §"from_jsonschema". > The `FromJsonSchema` provenance variant stays in `alknet-call` > (`OperationProvenance`); only the adapter implementation moved. @@ -80,8 +80,8 @@ primary use case) and that its parallel `remote_safe`/`trusted_peer` authorization system duplicated the existing `AccessControl`/`Identity` machinery. **ADR-024 supersedes ADR-023**: peer-keyed overlays + `PeerRef` routing, and peer authorization through the existing `AccessControl::check(peer_identity)`. -DC-2/3/4 are two-way-door defaults recorded here (DC-2→OQ-27, DC-3→OQ-28 -cross-peer dissolved / same-peer stays, DC-4→OQ-26). +DC-2/3/4 are two-way-door defaults recorded here (DC-2→OQ-12, DC-3→OQ-13 +cross-peer dissolved / same-peer stays, DC-4→OQ-11). ## Architecture @@ -136,7 +136,7 @@ the op's required scopes/resources are satisfied, the call dispatches; if not, `FORBIDDEN` before the handler runs (capabilities never populated — the security property). An op that should never be callable from the wire uses `Visibility::Internal` (existing mechanism, `NOT_FOUND` before ACL). See -[ADR-024](decisions/029-peer-graph-routing-model.md) §3 for the full +[ADR-024](decisions/024-peer-graph-routing-model.md) §3 for the full mapping of the three `remote_safe` cases to `AccessControl`/`Visibility`. The connection is symmetric after establishment (ADR-022 §2): both sides can @@ -212,7 +212,7 @@ pub type PeerId = String; // = Identity.id from IdentityProvider resolution `OperationEnv` gains a peer-routing method with a `PeerRef` selector (`Specific(PeerId)` / `Any`), default-impl for back-compat. See -[ADR-024](decisions/029-peer-graph-routing-model.md) §2 for the full +[ADR-024](decisions/024-peer-graph-routing-model.md) §2 for the full `invoke_peer` signature and `ScopedPeerEnv` peer-qualified reachability. The per-`CallConnection` overlay stays flat (one connection = one peer); the peer-keying is at the aggregation layer (the head node's composition env). @@ -225,7 +225,7 @@ single `AccessControl`-filtered handler (no `peer_scoped` variant, no `remote_safe` filter — both retired by ADR-024). `services/list-peers` is the opt-in for peer-attributed re-export listing (each peer's sub-overlay listed with attribution, filtered by the calling peer's -authorization). See [ADR-024](decisions/029-peer-graph-routing-model.md) §6. +authorization). See [ADR-024](decisions/024-peer-graph-routing-model.md) §6. ### Credential sources for connections @@ -286,7 +286,7 @@ There is no call-protocol credential bundle. The transport dimensions `alknet-core` per ADR-012. - **TLS identity** — the local node's Ed25519 raw key (RFC 7250) or X.509 cert, - derived from the vault at startup (ADR-020, ADR-026, ADR-027). + derived from the vault at startup (alknet ADR-020, ADR-026, ADR-027). - **Auth token** — an opaque call-protocol-level token, decrypted from the vault or derived from a shared secret. - **Remote identity verification** — the expected fingerprint/cert of the @@ -301,7 +301,7 @@ invariant (below). The concrete shapes of `TlsIdentity`, `AuthToken`, and `RemoteIdentity` are implementation-detail two-way doors; the one-way constraints are that they come from `Capabilities`, not env vars (ADR-010). -**TLS client-auth presentation** (OQ-29 #1, wired): the client presents +**TLS client-auth presentation** (OQ-14 #1, wired): the client presents its Ed25519 key as an RFC 7250 raw public key client cert — the client-side equivalent of the server's `RawKeyCertResolver`. This is **wired now**, not additive: it is what activates the `PeerEntry` fingerprint → `peer_id` @@ -310,13 +310,13 @@ peer graph doesn't populate for quinn connections — `PeerId` resolution fails because the server has no client cert to extract a fingerprint from. The iroh path already works (iroh uses RFC 7250 raw keys and exchanges Ed25519 public keys during the TLS handshake automatically); the gap was -quinn-only, and OQ-29 #1 resolves it by replacing `with_no_client_auth()` +quinn-only, and OQ-14 #1 resolves it by replacing `with_no_client_auth()` with presenting the key. The one-way constraint (credentials from `Capabilities`, not env vars, ADR-010) is unaffected — the `auth_token` dimension flows through the call-protocol `auth_token` payload field, not TLS, so the no-env-vars invariant holds independently of the TLS layer. -**Remote-identity verification** (OQ-29 #2, additive): verifying the +**Remote-identity verification** (OQ-14 #2, additive): verifying the server's fingerprint against an expected value (`credentials.remote_identity`) is **additive** — the server-side fingerprint extraction is what matters for `PeerId`, not the client-side verification. The verifier for raw keys can @@ -324,7 +324,7 @@ start as "accept any, extract fingerprint" and add fingerprint-pinning later. This is a two-way-door remainder; the one-way constraint (credentials from `Capabilities`, not env vars) is unaffected. -**Server cert verifier selection** (OQ-29 #2 + ADR-034 §3): the client-side +**Server cert verifier selection** (OQ-14 #2 + alknet ADR-034 §3): the client-side `ServerCertVerifier` is selected by whether the local node has a `PeerEntry` for the remote, not by key type alone. A pure-client connection to a **public X.509 endpoint** (no `PeerEntry` on the local @@ -339,7 +339,7 @@ with mixed Ed25519 + X.509 fingerprints) uses fingerprint pinning on both cert paths and does enter the peer graph. An unknown Ed25519 raw-key remote fails closed (no CA to fall back to — raw-key remotes are always known peers). See -[ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md) +[ADR-034](/workspace/@alkdev/alknet/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md) for the verifier selection rule and the three-role naming. ### from_call @@ -353,7 +353,7 @@ forwarding handlers. The discovery mechanism (`services/list` + ```rust pub struct FromCallConfig { /// Namespace prefix applied to imported operation names. Optional — - /// default no prefix. Collision on import is an error (DC-3, OQ-28), + /// default no prefix. Collision on import is an error (DC-3, OQ-13), /// not last-wins. pub namespace_prefix: Option, /// Optional filter — import only operations whose names match. None @@ -384,31 +384,50 @@ The flow (ADR-022 §3): sends `call.requested` via `CallConnection::call_with_payload()`, awaits the single `call.responded` (or `call.error`), returns the `ResponseEnvelope`. - - `Subscription` → a `StreamingHandler` (registered as - `HandlerKind::Stream`): calls `CallConnection::subscribe()`, which - returns `impl Stream` (the client-side - streaming path, already implemented), maps it to a - `BoxStream`. The remote stream flows end-to-end: - each `call.responded` the remote sends becomes a stream item; the - remote's `call.completed` ends the stream (→ wire `call.completed`); - `call.aborted` drops the stream (cascade per ADR-020). No truncation, - no first-value fallback — a `from_call`-imported subscription forwards - the full remote stream. - - `provenance: FromCall`, `composition_authority: None`, `scoped_env: None` - (leaf — ADR-018). + - `Subscription` → a `StreamingHandler` (registered as + `HandlerKind::Stream`): calls `CallConnection::subscribe()`, which + returns `impl Stream` (the client-side + streaming path, already implemented), maps it to a + `BoxStream`. The remote stream flows end-to-end: + each `call.responded` the remote sends becomes a stream item; the + remote's `call.completed` ends the stream (→ wire `call.completed`); + `call.aborted` drops the stream (cascade per ADR-020). No truncation, + no first-value fallback — a `from_call`-imported subscription forwards + the full remote stream. + - `Pub` → the sink forwarding shape (registered as `HandlerKind::Sink`, + ADR-046 + ADR-022 amendment 2026-10-13): a real forwarding body, not + an advertised-always-failing stub. A local `publish()` on the + imported op streams the `call.published` chunks across the + connection; the remote peer's `SinkHandler` consumes them. + - `provenance: FromCall`, `composition_authority: None`, `scoped_env: None` + (leaf — ADR-018). 4. The caller registers the bundles via `CallConnection::register_imported_all()`. -**Re-import on reconnection** (DC-2, OQ-27): `from_call` is a free function; +**Marked ops are not importable** (ADR-047 amendment 4 §4.8): +`from_call`'s discovery reconstructs the `channel_open` marker (ADR-047 +Gap F) so the importing side can *report* the remote channel-machinery +ops, but `register_imported` / `register_imported_all` refuse marked +specs — the marker's data plane is the allocated channel's `BiStream`, +which has no overlay path (`OperationEnv::invoke` is request/response +only, ADR-046 §6). A hub that needs a marked op composes it with the +channel machinery instead: `ChannelRelay::register_relay_openable` +(ADR-051) on the relaying side, or direct consumption via `ChannelClient` +(ADR-043). Likewise `op/register` refuses a marked announcement with +`MARKED_SPEC_UNSUPPORTED` (ADR-022 amendment 2026-10-13). This resolves +OQ-37: there is no `from_call` relay wrapper for marked ops — the relay +composition is the sanctioned path. + +**Re-import on reconnection** (DC-2, OQ-12): `from_call` is a free function; the assembly layer calls it after the dial (in `AlknetClient`). The overlay is per-connection (Layer 2, ADR-019), so a stale overlay dies with the connection; re-import on reconnect is naturally scoped to the new connection. A `CallConnection::refresh()` method for mid-connection re-discovery is a genuine feature addition — non-breaking, additive — if a deployment needs manual re-discovery without drop-and-reconnect. See -[ADR-028](decisions/069-from-call-manual-free-function.md). +[ADR-028](decisions/028-from-call-manual-free-function.md). -**Namespace collision** (DC-3, OQ-28): under the peer-graph model (ADR-024), +**Namespace collision** (DC-3, OQ-13): under the peer-graph model (ADR-024), cross-peer collision dissolves — same name on different peers is fine (they live in separate peer sub-overlays, no prefix needed). Same-peer collision stays an error (a peer shouldn't expose two ops with the same name). @@ -432,7 +451,7 @@ forwarding. The spoke receives it as metadata on its `OperationContext` — available for logging, auditing, per-user rate limiting, but never used by `AccessControl::check` (the spoke authorizes the hub, its direct caller, not the end user). The hub may set `forwarded_for: None` if it doesn't -want to disclose the originator. See [ADR-026](decisions/032-forwarded-for-identity.md). +want to disclose the originator. See [ADR-026](decisions/026-forwarded-for-identity.md). ### from_jsonschema @@ -443,7 +462,7 @@ and the "schema-only, no handler" concept conflated schema validation (a planning activity that doesn't need a registry entry) with operation registration (which always needs a handler). -[ADR-027](decisions/066-from-jsonschema-as-http-adapter.md) moved +[ADR-027](decisions/027-from-jsonschema-as-http-adapter.md) moved `from_jsonschema` to `alknet-http` as an HTTP-backed single-endpoint adapter: the caller supplies an `OperationSpec` + `HttpServiceConfig` + path template + method, and the adapter builds one @@ -451,7 +470,7 @@ path template + method, and the adapter builds one `FromJsonSchema` provenance. It is functionally similar to `from_openapi` but one endpoint at a time, for non-standard / non-OpenAPI / basic REST endpoints that don't have a full OpenAPI document. See -[`crates/http/http-adapters.md`](../http/http-adapters.md) §"from_jsonschema". +[`crates/http/http-adapters.md`](/workspace/@alkdev/alknet/docs/architecture/crates/http/http-adapters.md) §"from_jsonschema". The schema-validation-without-a-handler use case (the original stated purpose) is served by consuming `OperationSpec` directly — the spec @@ -482,7 +501,7 @@ which may be QUIC, TCP+TLS, or any other transport). Sync adapters trait — their `import()` bodies contain no `.await` points. This is locked by ADR-022 §5. -The **error type** (DC-4, OQ-26) is `Result, +The **error type** (DC-4, OQ-11) is `Result, AdapterError>` where `AdapterError` is a crate-level enum covering the failure modes real implementations hit: discovery transport failure (`from_call` remote unreachable), schema parse failure (`from_openapi`, @@ -711,14 +730,14 @@ Based on the gap analysis and the downstream unblock chain: via `PeerRef::Specific` / `PeerRef::Any` (ADR-024 §1-2). - **`from_call` is a manual free function.** The assembly layer calls it after the dial (in `AlknetClient`). The overlay is per-connection so - re-import on reconnect is naturally scoped (DC-2, OQ-27). See - [ADR-028](decisions/069-from-call-manual-free-function.md). + re-import on reconnect is naturally scoped (DC-2, OQ-12). See + [ADR-028](decisions/028-from-call-manual-free-function.md). - **`from_call` namespace collision is same-peer only.** Cross-peer collision dissolves (same name on different peers is fine — separate sub-overlays, ADR-024 §5). Same-peer collision stays an error. `namespace_prefix` is - optional local-naming sugar, not the disambiguation mechanism (DC-3, OQ-28). + optional local-naming sugar, not the disambiguation mechanism (DC-3, OQ-13). - **`OperationAdapter::import()` returns `Result`.** Failures surface as - `AdapterError` (DC-4, OQ-26). + `AdapterError` (DC-4, OQ-11). - **MCP stdio transport is not built.** Streamable HTTP is the only supported MCP transport in alknet. stdio = spawn arbitrary executable = built-in RCE. Recorded as an explicit security position, not a feature gap. @@ -730,7 +749,7 @@ Based on the gap analysis and the downstream unblock chain: invoked through the `CallConnection` handle. The client-side `ServerCertVerifier` uses CA verification (`WebPkiServerVerifier`) for such remotes; known peers (hub with `PeerEntry`) use fingerprint - pinning. See [ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md). + pinning. See [ADR-034](/workspace/@alkdev/alknet/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md). - **`ConnectionCredentials.remote_identity: None` is load-bearing.** `None` means "no `PeerEntry` for this remote → use CA verification (X.509) or fail closed (Ed25519 raw key)" per the ADR-034 §3 verifier rule. @@ -743,79 +762,73 @@ Based on the gap analysis and the downstream unblock chain: | Decision | ADR | Summary | |----------|-----|---------| -| Call protocol client and adapter contract | [ADR-022](decisions/017-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction; trait is async; adapters produce `HandlerRegistration` bundles | -| `from_jsonschema` as HTTP-backed single-endpoint adapter in alknet-http | [ADR-027](decisions/066-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf | -| Peer-graph routing model (DC-1, supersedes ADR-023) | [ADR-024](decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via existing `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` | -| PeerEntry and Identity.id decoupling | [ADR-025](decisions/030-peerentry-and-identity-id-decoupling.md) | `PeerId` source changes from UUID to `Identity.id` (= `PeerEntry.peer_id`, stable across key rotation); `Identity.id` decoupled from crypto material on the fingerprint path | -| Forwarded-for identity | [ADR-026](decisions/032-forwarded-for-identity.md) | `forwarded_for` field on `call.requested` and `OperationContext`; the `from_call` handler populates it; metadata only, never used by `AccessControl::check` | -| Storage boundary and repo/adapter pattern | [ADR-033](decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | -| Secret material flow and capability injection | [ADR-010](decisions/014-secret-material-flow-and-capability-injection.md) | The no-env-vars invariant's foundation; capabilities injected at assembly layer | -| Handler registration, provenance, and composition authority | [ADR-018](decisions/022-handler-registration-provenance-and-composition-authority.md) | The registration bundle adapters produce; `composition_authority: None` for leaves | -| Operation registry layering | [ADR-019](decisions/024-operation-registry-layering.md) | Layer 2 per-connection overlay where `from_call` imports land | -| Privilege model and authority context | [ADR-017](decisions/015-privilege-model-and-authority-context.md) | Adapter-registered ops are `Internal` by default; default-deny posture | -| Abort cascade for nested calls | [ADR-020](decisions/016-abort-cascade-for-nested-calls.md) | Cross-node abort through `from_call` forwarding handler's `parent_request_id` | -| Operation error schemas | [ADR-016](decisions/023-operation-error-schemas.md) | `error_schemas` mirrored by `from_call` from remote op's spec | -| Streaming handler for subscriptions | [ADR-021](decisions/049-streaming-handler-for-subscriptions.md) | `from_call` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`) that calls `CallConnection::subscribe()` and forwards the remote stream; `Query`/`Mutation` stay `HandlerKind::Once` | -| TLS identity redesign | [ADR-027](decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | RFC 7250 raw key / X.509 cert dimensions of the local `TlsIdentity` (now carried by `ConnectionCredentials.local_identity`) | -| Outgoing-only X.509 and three peer roles | [ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Public X.509 endpoint is not a `PeerEntry` on the client side (no `PeerId`, not in peer graph); client-side verifier by `PeerEntry` presence (CA vs fingerprint pin); hub = mixed-fingerprint `PeerEntry` | -| HD derivation for encryption keys | [ADR-020](decisions/020-hd-derivation-for-encryption-keys.md) | Vault-derived TLS identity material | -| Vault key model | [ADR-026](decisions/026-vault-key-model-hd-derivation.md) | Vault-derived TLS identity material | -| Vault local-only dispatch | [ADR-025](decisions/025-vault-local-only-dispatch.md) | Vault access at assembly layer only; the credential injection path's first hop | -| Crate decomposition | [ADR-031](decisions/003-crate-decomposition.md) | `alknet-http` depends on `alknet-call` (protocol-foundation exception, noted in Adapter Location Map) | -| One-way door decision framework | [ADR-032](decisions/009-one-way-door-decision-framework.md) | Door-type classification for DC-1..4 | +| Call protocol client and adapter contract | [ADR-022](decisions/022-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction; trait is async; adapters produce `HandlerRegistration` bundles | +| `from_jsonschema` as HTTP-backed single-endpoint adapter in alknet-http | [ADR-027](decisions/027-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf | +| Peer-graph routing model (DC-1, supersedes ADR-023) | [ADR-024](decisions/024-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via existing `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` | +| PeerEntry and Identity.id decoupling | [ADR-025](decisions/025-peerentry-and-identity-id-decoupling.md) | `PeerId` source changes from UUID to `Identity.id` (= `PeerEntry.peer_id`, stable across key rotation); `Identity.id` decoupled from crypto material on the fingerprint path | +| Forwarded-for identity | [ADR-026](decisions/026-forwarded-for-identity.md) | `forwarded_for` field on `call.requested` and `OperationContext`; the `from_call` handler populates it; metadata only, never used by `AccessControl::check` | +| Storage boundary and repo/adapter pattern | [alknet ADR-033](/workspace/@alkdev/alknet/docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | +| Secret material flow and capability injection | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | The no-env-vars invariant's foundation; capabilities injected at assembly layer | +| Handler registration, provenance, and composition authority | [ADR-018](decisions/018-handler-registration-provenance-and-composition-authority.md) | The registration bundle adapters produce; `composition_authority: None` for leaves | +| Operation registry layering | [ADR-019](decisions/019-operation-registry-layering.md) | Layer 2 per-connection overlay where `from_call` imports land | +| Privilege model and authority context | [ADR-017](decisions/017-privilege-model-and-authority-context.md) | Adapter-registered ops are `Internal` by default; default-deny posture | +| Abort cascade for nested calls | [ADR-020](decisions/020-abort-cascade-for-nested-calls.md) | Cross-node abort through `from_call` forwarding handler's `parent_request_id` | +| Operation error schemas | [ADR-016](decisions/016-operation-error-schemas.md) | `error_schemas` mirrored by `from_call` from remote op's spec | +| Streaming handler for subscriptions | [ADR-021](decisions/021-streaming-handler-for-subscriptions.md) | `from_call` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`) that calls `CallConnection::subscribe()` and forwards the remote stream; `Query`/`Mutation` stay `HandlerKind::Once` | +| TLS identity redesign | [alknet ADR-027](/workspace/@alkdev/alknet/docs/architecture/decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | RFC 7250 raw key / X.509 cert dimensions of the local `TlsIdentity` (now carried by `ConnectionCredentials.local_identity`) | +| Outgoing-only X.509 and three peer roles | [alknet ADR-034](/workspace/@alkdev/alknet/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Public X.509 endpoint is not a `PeerEntry` on the client side (no `PeerId`, not in peer graph); client-side verifier by `PeerEntry` presence (CA vs fingerprint pin); hub = mixed-fingerprint `PeerEntry` | +| HD derivation for encryption keys | [alknet ADR-020](/workspace/@alkdev/alknet/docs/architecture/decisions/020-hd-derivation-for-encryption-keys.md) | Vault-derived TLS identity material | +| Vault key model | [alknet ADR-026](/workspace/@alkdev/alknet/docs/architecture/decisions/026-vault-key-model-hd-derivation.md) | Vault-derived TLS identity material | +| Vault local-only dispatch | [alknet ADR-025](/workspace/@alkdev/alknet/docs/architecture/decisions/025-vault-local-only-dispatch.md) | Vault access at assembly layer only; the credential injection path's first hop | +| Crate decomposition | [ADR-031](decisions/031-crate-decomposition.md) | `alknet-http` depends on `alknet-call` (protocol-foundation exception, noted in Adapter Location Map) | +| One-way door decision framework | [ADR-032](decisions/032-one-way-door-decision-framework.md) | Door-type classification for DC-1..4 | ## Open Questions See [open-questions.md](open-questions.md) for full details. -- **OQ-25** (dissolved by ADR-024): `remote_safe` marking shape — moot. +- **OQ-10** (dissolved by ADR-024): `remote_safe` marking shape — moot. `remote_safe`/`trusted_peer` are retired; peer authorization is `AccessControl::check(peer_identity)`. No marking to shape. -- **OQ-26** (resolved): `AdapterError` variants — `DiscoveryFailed`, +- **OQ-11** (resolved): `AdapterError` variants — `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision` (replaces flat `Conflict`). `#[non_exhaustive]`. -- **OQ-27** (resolved): `from_call` re-import trigger — `from_call` is a +- **OQ-12** (resolved): `from_call` re-import trigger — `from_call` is a manual free function; the assembly layer calls it after the dial (in `AlknetClient`). A `CallConnection::refresh()` method is a genuine feature addition — non-breaking, additive. See - [ADR-028](decisions/069-from-call-manual-free-function.md). -- **OQ-28** (resolved): `from_call` namespace collision — same-peer + [ADR-028](decisions/028-from-call-manual-free-function.md). +- **OQ-13** (resolved): `from_call` namespace collision — same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays). `namespace_prefix` is optional local-naming sugar. -- **OQ-29** (resolved): `CallClient` TLS client-auth — wire quinn +- **OQ-14** (resolved): `CallClient` TLS client-auth — wire quinn client-auth (present Ed25519 key as raw public key client cert); key-type-aware server cert verification (raw key = fingerprint match, X.509 = CA verification); fingerprint normalization (`ed25519:` across quinn/iroh). The iroh path already works; the gap was quinn-only. - See OQ-29 in open-questions.md. -- **OQ-30** (resolved): `PeerRef::Any` routing policy — insertion-order + See OQ-14 in open-questions.md. +- **OQ-15** (resolved): `PeerRef::Any` routing policy — insertion-order first-match. A richer `RoutingPolicy` is a feature extension. -- **OQ-31** (resolved): `services/list-peers` — opt-in; `services/list` +- **OQ-16** (resolved): `services/list-peers` — opt-in; `services/list` is "own ops only." -- **OQ-32** (open, feature extension): Multi-hop federation — the one-hop +- **OQ-17** (deferred(scope), feature extension): Multi-hop federation — the one-hop model is the architectural commitment; multi-hop is a feature extension that doesn't break downstream. The peer-keyed model extends to multi-hop without redesign; petgraph is the candidate if path-finding becomes real (ADR-024 §3.7). -- **OQ-33** (resolved by ADR-025): `PeerId` is a logical id. Source is +- **OQ-18** (resolved by ADR-025): `PeerId` is a logical id. Source is `Identity.id` from `IdentityProvider` resolution (= `PeerEntry.peer_id`, - stable across key rotation). See OQ-33 in open-questions.md. -- **OQ-34** (resolved by ADR-025 + ADR-033): Persistent peer registry — + stable across key rotation). See OQ-18 in open-questions.md. +- **OQ-19** (resolved by ADR-025): Persistent peer registry — the storage boundary is `core trait + in-memory default` (config-backed `ConfigIdentityProvider` now; persistence adapters additive in separate - crates). See OQ-34 in open-questions.md. -- **OQ-35** (dissolved): the "API key asymmetry" framing was wrong; + crates). See OQ-19 in open-questions.md. +- **OQ-20** (dissolved): the "API key asymmetry" framing was wrong; `PeerEntry` supports multiple credential paths (fingerprints + auth_token_hash), `ApiKeyEntry` is for tokens that ARE the identity. - See OQ-35 in open-questions.md. -- **OQ-36** (resolved by ADR-035): Concrete persistence adapter shapes — - read-sync / write-async split (`IdentityStore` async write trait - extends the sync `IdentityProvider` read trait); SQLite adapter caches - in memory and uses honker NOTIFY/LISTEN for no-restart cache - invalidation; `alknet-store-sqlite` crate implements both - `IdentityStore` and `CredentialStore`. See ADR-035 and OQ-36 in - open-questions.md. -- **OQ-37** (resolved by ADR-034): X.509 outgoing-only case — three + See OQ-20 in open-questions.md. +- **OQ-21** (resolved, alknet ADR-034 — not ported): X.509 outgoing-only + case — three remote roles named (public X.509 endpoint, transport relay, hub). `PeerEntry` asymmetry is correct: a pure-client connection to a public X.509 endpoint is **not** in the call-protocol peer graph on the @@ -824,8 +837,10 @@ See [open-questions.md](open-questions.md) for full details. land in the connection's Layer 2 overlay and are invoked through the connection handle. The client-side `ServerCertVerifier` is selected by `PeerEntry` presence: known peer → fingerprint pin; unknown X.509 - remote → CA verification (`WebPkiServerVerifier`). See ADR-034 and - OQ-37 in open-questions.md. + remote → CA verification (`WebPkiServerVerifier`). The deciding ADR + (alknet ADR-034, "Outgoing-Only X.509 and Three Peer Roles") was not + ported into alkcall; the surviving alkcall surface is ADR-025's + `PeerEntry` asymmetry. See OQ-21 in open-questions.md. ## References diff --git a/docs/architecture/decisions/007-connection-from-stream-generic-single-stream.md b/docs/architecture/decisions/007-connection-from-stream-generic-single-stream.md index 4c0c084..8919b4f 100644 --- a/docs/architecture/decisions/007-connection-from-stream-generic-single-stream.md +++ b/docs/architecture/decisions/007-connection-from-stream-generic-single-stream.md @@ -232,5 +232,5 @@ WebTransport stream). - OQ-09: WASM target boundaries (resolution amended — the server-side dispatch door is no longer closed by `Connection` being QUIC-bound) - Transport generalization findings: - [`docs/research/transport-generalization/findings.md`](../../research/transport-generalization/findings.md) + [`docs/research/transport-generalization/findings.md`](/workspace/@alkdev/alknet/docs/research/transport-generalization/findings.md) - Implementation commit: `865fef6` (2026-07-09) \ No newline at end of file diff --git a/docs/architecture/decisions/008-bidistreamsource-trait.md b/docs/architecture/decisions/008-bidistreamsource-trait.md index 4040dc9..3b8c2f3 100644 --- a/docs/architecture/decisions/008-bidistreamsource-trait.md +++ b/docs/architecture/decisions/008-bidistreamsource-trait.md @@ -295,8 +295,8 @@ the args are optional for their transport. `from_bidi` constructors and the yield-once contract are preserved via `StreamBidiStreamSource`) - Channels POC summary: - [`docs/research/alknet-channels/poc-summary.md`](../../research/alknet-channels/poc-summary.md) + [`docs/research/alknet-channels/poc-summary.md`](/workspace/@alkdev/alknet/docs/research/alknet-channels/poc-summary.md) §"Issues Surfaced" #1 (OQ-CH-13 confirmed +EV), #2 (REQ-CORE-02) - Channels Phase 0 findings: - [`docs/research/alknet-channels/phase-0-findings.md`](../../research/alknet-channels/phase-0-findings.md) + [`docs/research/alknet-channels/phase-0-findings.md`](/workspace/@alkdev/alknet/docs/research/alknet-channels/phase-0-findings.md) §POC-Validated Requirements — REQ-CORE-01, REQ-CORE-02 \ No newline at end of file diff --git a/docs/architecture/decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md b/docs/architecture/decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md index 6a232b3..ee41125 100644 --- a/docs/architecture/decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md +++ b/docs/architecture/decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md @@ -625,9 +625,9 @@ provider confirms it owns the container (dynamic). - ADR-022: Call Protocol Client and Adapter Contract (`from_call` — the adapter that imports a peer's operations and re-registers them locally; the proxy pattern's re-export mechanism) -- [auth.md](../crates/core/auth.md) (`Identity.resources`, +- [auth.md](/workspace/@alkdev/alknet/docs/architecture/crates/core/auth.md) (`Identity.resources`, `AccessControl::check` interaction — both under edit by this decision) -- [operation-registry.md](../crates/call/operation-registry.md) +- [operation-registry.md](../operation-registry.md) (`AccessControl`, `OperationSpec` — `resource_id_path` addition) -- [alknet-docker POC summary](../../research/alknet-docker/poc-summary.md) +- [alknet-docker POC summary](/workspace/@alkdev/alknet/docs/research/alknet-docker/poc-summary.md) §"Open Unknowns" #3 (the research finding that surfaced this question) \ No newline at end of file diff --git a/docs/architecture/decisions/014-irpc-never-integrated-hand-rolled-framing.md b/docs/architecture/decisions/014-irpc-never-integrated-hand-rolled-framing.md index 3532d57..8d41f42 100644 --- a/docs/architecture/decisions/014-irpc-never-integrated-hand-rolled-framing.md +++ b/docs/architecture/decisions/014-irpc-never-integrated-hand-rolled-framing.md @@ -79,7 +79,7 @@ were removed in commit `668d777` (2026-07-09). `irpc` may be re-added as `iroh-blobs 0.103`), but that would be a *transitive* dependency of `alknet-blobs`, not a direct dependency of `alknet-call` — alknet-call does not import irpc and has no plans to. See -[`docs/research/transport-generalization/findings.md`](../../research/transport-generalization/findings.md) +[`docs/research/transport-generalization/findings.md`](/workspace/@alkdev/alknet/docs/research/transport-generalization/findings.md) §3.1 for the removal trace. ## Decision @@ -159,6 +159,6 @@ not import irpc and has no plans to. See dispatch path) - Call protocol wire format: `crates/alknet-call/src/protocol/wire.rs` - Transport generalization findings: - [`docs/research/transport-generalization/findings.md`](../../research/transport-generalization/findings.md) + [`docs/research/transport-generalization/findings.md`](/workspace/@alkdev/alknet/docs/research/transport-generalization/findings.md) §3.1 (dead `irpc` dep removal) - Removal commit: `668d777` (2026-07-09) \ No newline at end of file diff --git a/docs/architecture/decisions/017-privilege-model-and-authority-context.md b/docs/architecture/decisions/017-privilege-model-and-authority-context.md index a2c6ac4..b4f78f2 100644 --- a/docs/architecture/decisions/017-privilege-model-and-authority-context.md +++ b/docs/architecture/decisions/017-privilege-model-and-authority-context.md @@ -302,8 +302,8 @@ Principle of least privilege. - OQ-17: Abort cascade (the call tree is the agency chain — `parent_request_id` traces principal → agent) - OQ-19: Session-scoped registries (session operations are always `Internal`) -- [operation-registry.md](../crates/call/operation-registry.md) -- [call-protocol.md](../crates/call/call-protocol.md) +- [operation-registry.md](../operation-registry.md) +- [call-protocol.md](../call-protocol.md) - TypeScript `@alkdev/operations` `buildEnv()` with `allowedNamespaces` — prior art for scoped composition env - POC at `/workspace/toolEnv` — demonstrated the sandbox-to-registry bridge with diff --git a/docs/architecture/decisions/020-abort-cascade-for-nested-calls.md b/docs/architecture/decisions/020-abort-cascade-for-nested-calls.md index c249ade..da038aa 100644 --- a/docs/architecture/decisions/020-abort-cascade-for-nested-calls.md +++ b/docs/architecture/decisions/020-abort-cascade-for-nested-calls.md @@ -253,5 +253,5 @@ not through the wire and not through `OperationSpec`. tree and participate in cascade) - `@alkdev/flowgraph` TypeScript package — prior art for call-graph tracking with `descendants()`, `FailurePolicy`, reactive status propagation -- [call-protocol.md](../crates/call/call-protocol.md) -- [operation-registry.md](../crates/call/operation-registry.md) \ No newline at end of file +- [call-protocol.md](../call-protocol.md) +- [operation-registry.md](../operation-registry.md) \ No newline at end of file diff --git a/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md b/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md index f9cfdea..5424e3d 100644 --- a/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md +++ b/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md @@ -349,9 +349,9 @@ same as `from_openapi` receives HTTP credentials. - OQ-25..28: Two-way-door remainders from the call-completion gap analysis (DC-1 shape, DC-4 error type, DC-2 re-import trigger, DC-3 namespace collision — see [open-questions.md](../open-questions.md)) -- [call-protocol.md](../crates/call/call-protocol.md) -- [operation-registry.md](../crates/call/operation-registry.md) -- [client-and-adapters.md](../crates/call/client-and-adapters.md) — the spec +- [call-protocol.md](../call-protocol.md) +- [operation-registry.md](../operation-registry.md) +- [client-and-adapters.md](../client-and-adapters.md) — the spec that operationally fills the gap this ADR left to implementation - `docs/research/alknet-call-completion/gap-analysis.md` — DC-1..4, the decisions that needed resolution before implementation @@ -524,7 +524,7 @@ overlays), not by a parallel boolean gate. §5 showed `async fn import(&self) -> Vec` with no error type. The trait returns `Result, AdapterError>` where `AdapterError` is a crate-level enum. The *presence* of the error type -is recorded in [client-and-adapters.md](../crates/call/client-and-adapters.md); +is recorded in [client-and-adapters.md](../client-and-adapters.md); the exact variants are the two-way-door remainder, tracked as OQ-26. ### DC-2 — from_call re-import on reconnection: manual free function @@ -537,7 +537,7 @@ overlay dies with the connection. A `CallConnection::refresh()` method for mid-connection re-discovery is a genuine feature addition — non-breaking, additive — if a deployment needs manual re-discovery without drop-and-reconnect. Two-way door; recorded in -[client-and-adapters.md](../crates/call/client-and-adapters.md); tracked as +[client-and-adapters.md](../client-and-adapters.md); tracked as OQ-27. See [ADR-028](028-from-call-manual-free-function.md) for the full rationale. @@ -547,7 +547,7 @@ rationale. collision = error**. A node importing from two remotes that both expose the same unprefixed op name should fail loudly. The operator adds prefixes when importing from multiple sources. Two-way door; recorded in -[client-and-adapters.md](../crates/call/client-and-adapters.md); tracked as +[client-and-adapters.md](../client-and-adapters.md); tracked as OQ-28. ### Operational spec @@ -556,7 +556,7 @@ The gap this ADR left to implementation — the `CallClient` API, the `from_call` flow, the trait signature, the adapter location map, the no-env-vars invariant, and the exchange-of-operations pattern — is specified in -[client-and-adapters.md](../crates/call/client-and-adapters.md). That document +[client-and-adapters.md](../client-and-adapters.md). That document is the operational complement to this ADR; this ADR remains the architectural authority. @@ -576,7 +576,7 @@ served by consuming `OperationSpec` directly. The §5 "adapters live in alknet-call" one-way-door statement is corrected above to "the adapter trait lives in `alknet-call`; implementations live with their transport." See [ADR-027](027-from-jsonschema-as-http-adapter.md) and -[client-and-adapters.md](../crates/call/client-and-adapters.md) §"from_jsonschema". +[client-and-adapters.md](../client-and-adapters.md) §"from_jsonschema". ## Amendments (2026-07-13) @@ -585,7 +585,7 @@ See [ADR-027](027-from-jsonschema-as-http-adapter.md) and The §1 Decision framed `CallClient::connect(addr: SocketAddr, credentials)` as the primary constructor and described it as "opens a QUIC connection." The operational spec -([client-and-adapters.md](../crates/call/client-and-adapters.md)) +([client-and-adapters.md](../client-and-adapters.md)) framed `spawn_dispatch(connection)` as the "lower-level API" that `connect()` uses after the QUIC dial. That framing welded the client-side one-way-door API to QUIC — the same welding ADR-007 @@ -621,7 +621,7 @@ convenience)~~ `connect` is **removed** (ADR-045 §5). The shared dial seam is `alknet-client`; `spawn_dispatch` is the protocol-crate take-over that consumes the dial's `Connection`. -See [client-and-adapters.md](../crates/call/client-and-adapters.md) +See [client-and-adapters.md](../client-and-adapters.md) §"CallClient" for the reframed operational spec. ## Amendment (2026-10-13 — review 010 S-04: announced Sub/Pub forward-through, marked-spec refusal) diff --git a/docs/architecture/decisions/025-peerentry-and-identity-id-decoupling.md b/docs/architecture/decisions/025-peerentry-and-identity-id-decoupling.md index 4ad7716..114c623 100644 --- a/docs/architecture/decisions/025-peerentry-and-identity-id-decoupling.md +++ b/docs/architecture/decisions/025-peerentry-and-identity-id-decoupling.md @@ -270,7 +270,7 @@ There are three credential types in the alknet auth model: 2. **X.509 cert** — for domain-facing endpoints (`api.alk.dev`, relays, ACME/Let's Encrypt). Fingerprint format: `SHA256:`. Requires CA verification on the client side. The outgoing-only case (a client - connects to a public X.509 endpoint) is tracked as OQ-37. + connects to a public X.509 endpoint) is tracked as OQ-21. 3. **Bearer token** (auth_token) — for HTTP clients that can't do TLS client-auth (browsers, curl), or as a secondary credential path. Carried @@ -427,8 +427,9 @@ rotation (`PeerEntry`) or not (`ApiKeyEntry`). paths, and `ApiKeyEntry` is for tokens that ARE the identity) - OQ-29: CallClient TLS Client-Auth (resolved by this ADR's §6 fingerprint normalization + the client-auth wiring decision recorded in OQ-29) -- OQ-37: X.509 outgoing-only case (the three auth types and how X.509 - server identity fits — see OQ-37 in open-questions.md) +- OQ-21: X.509 outgoing-only case (the three auth types and how X.509 + server identity fits — see OQ-21 in open-questions.md; the deciding + ADR, alknet ADR-034, was not ported into alkcall) - `docs/research/alknet-storage-strategy/findings.md` §4 (the `PeerEntry` model and resolution path) - `docs/architecture/crates/core/auth.md` (the spec amended by this ADR) diff --git a/docs/architecture/decisions/031-crate-decomposition.md b/docs/architecture/decisions/031-crate-decomposition.md index af79170..f909b5f 100644 --- a/docs/architecture/decisions/031-crate-decomposition.md +++ b/docs/architecture/decisions/031-crate-decomposition.md @@ -123,7 +123,7 @@ implementations are independent. The Amendment 1 protocol-foundation exception remains for alknet-http/agent/napi (which use alknet-call's `OperationSpec`/`Handler`/`OperationAdapter` types — actual type reuse, not framing glue); it no longer covers alknet-tty. See -[ADR-057](057-alknet-tty-no-alknet-call-dep.md) for the full decision +[ADR-057](/workspace/@alkdev/alknet/docs/architecture/decisions/057-alknet-tty-no-alknet-call-dep.md) for the full decision and the three options considered (duplicate / promote to core / use alknet-call). diff --git a/docs/architecture/decisions/040-backpressure-channel-limits-id-reuse.md b/docs/architecture/decisions/040-backpressure-channel-limits-id-reuse.md index 2266ea3..cdca154 100644 --- a/docs/architecture/decisions/040-backpressure-channel-limits-id-reuse.md +++ b/docs/architecture/decisions/040-backpressure-channel-limits-id-reuse.md @@ -109,7 +109,7 @@ per `ChannelManager` (`buffer_cap` field). This prevents memory exhaustion without the complexity of SSH's sliding-window protocol. Full channel-level windowing (SSH-style sliding-window per channel) is a -deferred extension, tracked as [OQ-56](../questions/056-full-channel-level-flow-control-windowing.md) +deferred extension, tracked as [OQ-56](/workspace/@alkdev/alknet/docs/architecture/questions/056-full-channel-level-flow-control-windowing.md) (deferred(scope)). It is blocked on a real deployment observing head-of- line blocking where the bounded-buffer mitigation is insufficient. The bounded-buffer decision is made; the extension is not. diff --git a/docs/architecture/decisions/045-alknetclient-native-dial-seam.md b/docs/architecture/decisions/045-alknetclient-native-dial-seam.md index 9240692..a7b625d 100644 --- a/docs/architecture/decisions/045-alknetclient-native-dial-seam.md +++ b/docs/architecture/decisions/045-alknetclient-native-dial-seam.md @@ -21,7 +21,7 @@ reader); `auth_token` is a per-request payload field; the `from_call` OQ-55 deferred `AlknetClient::dial()` — the transport-polymorphic client dial seam — blocked on "a second transport's real dial existing." The reasoning (recorded in the OQ-55 file and in -[channel-client.md](../crates/channels/channel-client.md) §"Relationship +[channel-client.md](../channel-client.md) §"Relationship to `AlknetClient`") was sound at the time: extracting a QUIC-shaped connector and naming it `AlknetClient` would bake QUIC in as *the* establishment shape — the same welding ADR-007 unwound on the server @@ -528,24 +528,24 @@ dedicated ADR lands. - OQ-55 (resolved by this ADR) — `AlknetClient` / client establishment extraction -- [ADR-083](083-endpoint-as-accept-loop-runner.md) — `AlknetEndpoint` +- [ADR-083](/workspace/@alkdev/alknet/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md) — `AlknetEndpoint` as multi-transport accept-loop runner; the server-side shape this ADR mirrors on the client side -- [ADR-086](086-endpoint-types-and-entry-points.md) — endpoint types +- [ADR-086](/workspace/@alkdev/alknet/docs/architecture/decisions/086-endpoint-types-and-entry-points.md) — endpoint types (native has QUIC + TCP+TLS + iroh); entry-point vs. endpoint ALPN distinction (§2) -- [ADR-087](087-tlsclientconfig-not-blocked-on-dial.md) — +- [ADR-087](/workspace/@alkdev/alknet/docs/architecture/decisions/087-tlsclientconfig-not-blocked-on-dial.md) — `TlsClientConfig` not blocked on the dial seam; breaks the circular hedge; the TLS config is a prerequisite for the dial -- [ADR-082](082-alknet-tls-extraction.md) — `TlsServerConfig` / +- [ADR-082](/workspace/@alkdev/alknet/docs/architecture/decisions/082-alknet-tls-extraction.md) — `TlsServerConfig` / `TlsClientConfig` in `alknet-tls`; "iroh shares the key, not the config" - [ADR-007](007-connection-from-stream-generic-single-stream.md) — `Connection::from_stream` / `from_bidi`; the server-side generalization whose client-side analogue this ADR completes -- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) — +- [ADR-034](/workspace/@alkdev/alknet/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md) — client-side verifier selection (fingerprint pin vs CA vs fail-closed) -- [ADR-084](084-aws-lc-rs-crypto-provider.md) — aws-lc-rs crypto +- [ADR-084](/workspace/@alkdev/alknet/docs/architecture/decisions/084-aws-lc-rs-crypto-provider.md) — aws-lc-rs crypto provider on all paths - [ADR-043](043-channelclient.md) — `ChannelClient::from_connection` (the take-over `AlknetClient` feeds) diff --git a/docs/architecture/decisions/046-publish-operation-type-and-handler-kind-sink.md b/docs/architecture/decisions/046-publish-operation-type-and-handler-kind-sink.md index 827a190..598ec0d 100644 --- a/docs/architecture/decisions/046-publish-operation-type-and-handler-kind-sink.md +++ b/docs/architecture/decisions/046-publish-operation-type-and-handler-kind-sink.md @@ -49,6 +49,13 @@ primitive is the load-bearing call-protocol piece; the broker composes on top of it later, in channels, the same way the hub relay composes on top of existing call ops. +> **Status note (post-deferral)**: the channels work this deferral +> pointed at has since landed, and the placement is now decided — the +> broker is out of scope for alkcall (ADR-047 §"What this ADR decides", +> Gap B) and tracked downstream as OQ-42 (`deferred(scope)`, blocked on +> a concrete producing-crate consumer). Read "deferred to the channels +> session" below as this placement decision. + ### Gap A from the research findings: `Pub` handler shape The research findings' Gap A identified that the current @@ -441,6 +448,13 @@ one-way addition (the field is part of the spec's public API); its ## Fan-out (deferred — OQ-22 §fan-out scope) +> **Status note (post-deferral)**: see OQ-22/OQ-42 in +> [open-questions.md](../open-questions.md) — the placement is decided +> (out of scope for alkcall, ADR-047 Gap B); the broker design and +> build are downstream (OQ-42, deferred(scope)). "The channels session +> will add the broker" below predates that placement; read it as "the +> downstream hub composes the broker." + The fan-out / broker mechanism (one producer, N consumers, topic matching by `(op_name, params_hash)`) is **not** part of this ADR. It is deferred to the channels session for these reasons: diff --git a/docs/architecture/decisions/051-channel-relay-and-hub-leg.md b/docs/architecture/decisions/051-channel-relay-and-hub-leg.md index 81453c5..436c159 100644 --- a/docs/architecture/decisions/051-channel-relay-and-hub-leg.md +++ b/docs/architecture/decisions/051-channel-relay-and-hub-leg.md @@ -82,9 +82,12 @@ Consumer-side close → wrapper teardown → pump abort → send-half drop → EOF chunk → spoke handler exits → spoke's own wrapper decrements its ledger. Close propagates through the EOF cascade with correct ledger accounting on both legs; the hub never sees a `channel/close` to -translate. Control stays per-leg (`channel/control` is not implemented -upstream, OQ-39 — a hub wanting cross-leg control translation composes -it downstream). This supersedes the ADR-042 §mapping bullet's +translate. Control stays per-leg (ADR-052): the control surface is +establisher-contributed (`Establishment::with_control`) on each leg's +own allocation — the hub never relays a `channel/control` between legs; +a hub wanting cross-leg control composes it with the per-leg control +handles, not with a translation surface. This supersedes the ADR-042 +§mapping bullet's close-translation sentence as an implementation matter: the mapping entry the sentence's removal depended on does not exist. diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 766aad8..e73ba11 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -41,7 +41,11 @@ status, priority, and (when resolved) a resolution citing the ADR. | OQ | Title | Status | Priority | Resolution | |----|-------|--------|----------|------------| -| OQ-22 | Call protocol pub/sub primitive — pub to go with sub | partially resolved | high | ADR-046 resolves the primitive: `OperationType::Pub` + `HandlerKind::Sink` + `call.published` wire event + `invoke_sink()` dispatch path. The fan-out/broker mechanism (one producer, N consumers, topic matching) is deferred to the channels session — the call protocol is point-to-point; the broker is a routing concern that sits above it. ADR-047 §1 names the broker (Gap B) as out-of-scope for alkcall; the hub composes it on top of the `Pub`/`Sub` primitives. See §"Pub/Sub Gap" below. | +| OQ-22 | Call protocol pub/sub primitive — pub to go with sub | resolved | high | The question had two halves, both now decided. (1) The primitive: ADR-046 delivers `OperationType::Pub` + `HandlerKind::Sink` + `call.published` wire event + `invoke_sink()` dispatch + `CallConnection::publish()`. (2) Placement of the fan-out/broker: **out of scope for alkcall** — the call protocol is point-to-point; a topic registry that outlives individual calls is a routing concern that sits above it. The hub composes the broker on top of the `Pub`/`Sub` primitives (ADR-047 §"What this ADR decides" Gap B). The broker design itself is tracked as OQ-42. See §"Pub/Sub Gap" below. | +| OQ-42 | Hub message broker (topic registry, `Pub`↔`Sub` matching, N-consumer fan-out) | deferred(scope) | medium | Deferred downstream: the broker is an alknet-hub composition on top of alkcall's `Pub`/`Sub` primitives, not an alkcall deliverable (OQ-22 placement resolution, ADR-047 Gap B). **Blocked on**: a concrete producing crate committing to fan-out — a +spec that requires one produced topic with N independent spoke +subscribers (plausible first candidate: alktrader market-data +distribution; ADR-046 §fan-out records the consumer-blind rationale). Not circular — the primitives it composes on are delivered and load-bearing (ADR-046), and no alkcall API surface advertises or depends on the broker. | ### Pub/Sub Gap @@ -59,32 +63,44 @@ type. `call.published` is the wire event carrying stream chunks. the client method. The `Subscription` variant is renamed to `Sub` (wire string `"sub"`). -**Fan-out deferred.** The broker (topic registry, `Pub`↔`Sub` matching, -N-consumer fan-out) is deferred to the channels session. The call -protocol is point-to-point (one initiator, one responder, a stream -between them); a topic registry that outlives individual calls is a -different lifecycle and a different concern. The broker's first -consumer is the channels `channel/resources/subscribe` operation -(ADR-037) and the hub-as-broker pattern (ADR-042). The `Pub` primitive -is the load-bearing piece the broker composes on. +**Placement decided: the broker is not an alkcall deliverable.** The +fan-out/broker mechanism (topic registry, `Pub`↔`Sub` matching, +N-consumer fan-out) sits above the call protocol: the call protocol is +point-to-point (one initiator, one responder, a stream between them); +a topic registry that outlives individual calls is a different +lifecycle and a different concern. The broker is a hub composition on +top of the `Pub`/`Sub` primitives — ADR-047 §"What this ADR decides" +(Gap B) places it out of scope for alkcall. The `Pub` primitive is the +load-bearing piece the broker composes on (delivered, ADR-046); ADR-053 +served the broker's originally-named first consumer +(`channel/resources/subscribe`) with enumerators instead. + +**The broker design is downstream work (OQ-42, deferred(scope)).** It +unblocks when a concrete producing crate commits to fan-out. The +original deferral's rationale (ADR-046 §fan-out — designing the broker +without its first real consumer produces a half-design) still holds: +the honest state is a tracked, concretely-blocked deferral, not a +pending promise. The "channels session" phrasing in ADR-046 §fan-out +should be read as this placement decision (the channels work has since +landed; what remains unbuilt is the downstream hub broker). ## Channels | OQ | Title | Status | Priority | Resolution | |----|-------|--------|----------|------------| | OQ-23 | Full channel-level flow-control windowing | deferred(scope) | low | Bounded-buffer decided (ADR-040); full windowing blocked on HOL-blocking deployment observation | -| OQ-24 | Channels add/strip API shape | open | low | Whether the 8-byte header add/strip is built into the read/write path or a standalone utility. The contract (ADR-035) is decided; the function surface is not | +| OQ-24 | Channels add/strip API shape | resolved | low | Resolved by the shipped surface: the strip is **integrated into the read/write path** — the demux loop consumes `read_header`/`write_chunk` so a handler's `BiStream` yields payload chunks and never sees the 8-byte header (ADR-035; matches ADR-037's payload-parse contract). The low-level primitives (`parse_header`/`write_header`/`read_header`/`write_chunk`/`write_eof` in `channels::wire`) remain exposed as the standalone utility for tooling/codegen, but no handler-facing add/strip wrapper exists or is needed. | | OQ-31 | `channel/open` ACL granularity | resolved | high | ADR-047 — `channel/open` dissolves into per-ALPN ops; each op has its own `access_control` | | OQ-32 | Quota lifecycle (opener vs closer, transport drop) | resolved | high | ADR-047 §7 — the per-connection opener ledger; decrement on every teardown path, keyed by opener | | OQ-33 | Per-identity connection cap (endpoint layer) | deferred(scope) | low | Named as a separate layer (ADR-047 §"ALPN category reframe" references the findings); belongs at `alknet-endpoint`, not channels. Named to stop the re-tangle | | OQ-34 | `channel_open` marker wire format | resolved | medium | ADR-047 §2 — boolean `"channel_open": true` in `services/schema`; ALPN derivable from op name | | OQ-35 | `OperationEnv::channel_manager()` coupling | resolved | high | ADR-047 §4 — extension trait `ChannelOperationEnv` in `channels-call`; call crate stays free of channels types | | OQ-36 | `channel_id` allocation in Pub case | resolved | medium | ADR-047 §5 — "connection owner allocates" (the side that holds the `ChannelManager`); amends "responder allocates" | -| OQ-37 | `from_call` relay wrapper for marked ops | open | medium | ADR-047 §1 names it as a consumer (hub) concern; alkcall's `from_call` reconstructs the marker (Gap F resolved) so the consumer can branch on it | +| OQ-37 | `from_call` relay wrapper for marked ops | resolved | medium | ADR-022 amendment 2026-10-13 — there is no `from_call` relay wrapper, and there won't be one: marked (`channel_open`) announcements are refused with `MARKED_SPEC_UNSUPPORTED`, and `register_imported`/`register_imported_all` refuse marked specs (ADR-047 amendment 4 §4.8) — a marked op's data plane (the allocated channel's `BiStream`) has no overlay path. The sanctioned compositions are named in the refusal: `ChannelRelay::register_relay_openable` (ADR-051) for a relaying hub, `ChannelClient` (ADR-043) for direct consumption. `from_call`'s discovery still reconstructs the marker (ADR-047 Gap F) for reporting, but importing a marked spec is refused at the overlay landing. | | OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alk/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) | | OQ-39 | `channel/control` control-handle surface | resolved | medium | ADR-052 — the control-handle surface is implemented: per-channel control callbacks contributed by the ALPN establisher (`Establishment::with_control`), wrapper-installed and teardown-removed in the manager; the op routes the ALPN-specific `message` and answers `channel:no_control_handle` when the channel's ALPN registered none. The stub (`channel:control_not_implemented`) was removed with the single-publish fix (reviews 010 S-02 / 011). | | OQ-40 | `channel/resources/subscribe` live subscription | resolved | medium | ADR-053 — the live subscription is implemented: one `ResourceEnumerator` per openable ALPN (provided by the producing crate, snapshot-then-changes), aggregated by the handler over the scan-once snapshot; an empty registry emits one `{ "resources": [] }` seed and stays open. The one-shot error stub was removed with the single-publish fix (reviews 010 S-03 / 011). The alktunnels discovery-UI note (review 006 E-02) is served by the enumerator surface. | -| OQ-41 | QUIC-native multi-stream substrate | open | medium | Only the in-line substrate mode is implemented (single bidi stream, header-demuxed N channels). The QUIC-native multi-stream substrate (accept remaining bidi streams, read headers off each — ADR-034 §substrate modes) is deferred to the downstream alknet crate. The wire format and demux loop are correct for both substrates; only the outer `accept_bi()` loop is missing. The alknet crate owns the QUIC dial/accept loop and is the natural place for the multi-stream accept loop. This crate stays transport-agnostic (no QUIC dependency, WASM-compatible). | +| OQ-41 | QUIC-native multi-stream substrate | deferred(scope) | medium | **Deferred downstream** — the substrate lives in the dial/accept layer, which alkcall does not own. Only the in-line substrate mode is implemented (single bidi stream, header-demuxed N channels); the QUIC-native multi-stream mode (accept remaining bidi streams, read headers off each — ADR-034 §substrate modes) belongs in alknet's QUIC accept loop. **Blocked on**: alknet's multi-stream substrate integration. **Impacts**: none in-crate — the wire format and demux loop are correct for both substrates; alkcall stays transport-agnostic (no QUIC dependency, WASM-compatible). | ## Core Types diff --git a/docs/architecture/operation-registry.md b/docs/architecture/operation-registry.md index 3d3b044..0e883be 100644 --- a/docs/architecture/operation-registry.md +++ b/docs/architecture/operation-registry.md @@ -137,7 +137,7 @@ The `OwnershipProvider` trait (read side, sync — called on the dispatch hot path) and the `OwnershipStore` trait (write side, async — called by handlers that manage resource lifecycles) are defined in `alknet-core` per ADR-011's storage decision (fourth instance of the repo/adapter pattern, -ADR-033). See [auth.md](../core/auth.md) §"Ownership Provider and Store" +alknet ADR-033 — not ported). See [auth.md](/workspace/@alkdev/alknet/docs/architecture/crates/core/auth.md) §"Ownership Provider and Store" for the trait shapes and the in-memory default adapter. **The ownership provider is carried on `OperationContext`** (or threaded @@ -605,7 +605,7 @@ The `parent` parameter propagates the calling context: the nested call gets `par The `invoke_peer` / `peer_contains` methods (ADR-024 §2) take a `PeerRef` selector and a `PeerId`. These types are defined alongside the `PeerCompositeEnv` struct — see [client-and-adapters.md](client-and-adapters.md#peer-keyed-composition-env-adr-029) -and [ADR-024](decisions/029-peer-graph-routing-model.md) §2: +and [ADR-024](decisions/024-peer-graph-routing-model.md) §2: ```rust pub enum PeerRef { @@ -727,7 +727,7 @@ impl OperationEnv for PeerCompositeEnv { } // Peer-keyed overlay: iterate peers in insertion order, dispatch to // the first peer whose sub-overlay contains the op. This is the - // head→N-workers fan-out primitive (ADR-024 §2, OQ-30: insertion- + // head→N-workers fan-out primitive (ADR-024 §2, OQ-15: insertion- // order first-match). for peer_id in &self.connection_order { if let Some(conn_env) = self.connections.get(peer_id) { @@ -966,64 +966,65 @@ The `Capabilities` type holds non-serializable, zeroized secret material. It doe | Decision | ADR | Summary | |----------|-----|---------| -| Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled framing, registry, dispatch; supersedes ADR-013 | -| Call protocol stream model | [ADR-015](decisions/012-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | -| Static handler registration | [ADR-010](decisions/010-alpn-router-and-endpoint.md) | `HandlerRegistry` (ALPN-level) immutable after construction; `OperationRegistry` layered by ADR-019 (curated immutable, session/imported dynamic) | -| Vault integration via assembly layer | [ADR-008](decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time | -| Secret material flow and capability injection | [ADR-010](decisions/014-secret-material-flow-and-capability-injection.md) | Capabilities carry outbound credentials; call protocol carries no secret material | -| Privilege model and authority context | [ADR-017](decisions/015-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; composition authority + scoped env | -| Handler registration, provenance, and composition authority | [ADR-018](decisions/022-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle | -| Operation registry layering | [ADR-019](decisions/024-operation-registry-layering.md) | Curated (static, immutable) + session and connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) | -| Operation error schemas | [ADR-016](decisions/023-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity for `from_openapi`/`to_openapi` | -| Call protocol client and adapter contract | [ADR-022](decisions/017-call-protocol-client-and-adapter-contract.md) | `from_call`/`OperationAdapter` produce `HandlerRegistration` bundles; adapter-registered ops are `Internal` leaves. Surface specced in [client-and-adapters.md](client-and-adapters.md) | -| `from_jsonschema` as HTTP-backed single-endpoint adapter | [ADR-027](decisions/066-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf (now handler-bearing, not "no handler") | -| Peer-graph routing model (supersedes ADR-023) | [ADR-024](decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` (the field this doc's `HandlerRegistration` previously gained) | -| Forwarded-for identity | [ADR-026](decisions/032-forwarded-for-identity.md) | `forwarded_for` field on `OperationContext` and `call.requested`; metadata only — `AccessControl::check` never reads it; the `from_call` handler populates it | -| Streaming handler for subscriptions | [ADR-021](decisions/049-streaming-handler-for-subscriptions.md) | `StreamingHandler` type alongside `Handler`; `HandlerKind` enum on `HandlerRegistration` validated against `op_type`; `invoke_streaming()` on `OperationRegistry`; `invoke()` and `OperationEnv::invoke()` error with `INVALID_OPERATION_TYPE` on `Subscription` ops; composition stays request/response-only, stream composition is handler-level | -| Dynamic resource ownership for runtime-spawned resources | [ADR-011](decisions/050-dynamic-resource-ownership-for-runtime-spawned-resources.md) | `AccessControl::check` consults an `OwnershipProvider` (sync read trait, ADR-033 repo/adapter pattern); `OperationSpec` gains `resource_id_path` (JSON pointer into the input); proxy-only access pattern (spawner owns, proxy to share, teardown revokes); `list` = scope-gate + result-filter; teardown = automatic, handler-driven; composition = two orthogonal checks, ADR-017/022 unchanged | -| Publish operation type and HandlerKind::Sink | [ADR-046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | `OperationType::Pub` (producer→consumer streaming); `SinkHandler` + `HandlerKind::Sink` + `PublishStream`; `invoke_sink()` dispatch path; `call.published` wire event; `OperationSpec.publish_schema`; `Subscription` renamed to `Sub`; fan-out/broker deferred to channels | +| Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled framing, registry, dispatch; supersedes ADR-013 | +| Call protocol stream model | [ADR-015](decisions/015-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | +| Static handler registration | [ADR-010](/workspace/@alkdev/alknet/docs/architecture/decisions/010-alpn-router-and-endpoint.md) | `HandlerRegistry` (ALPN-level) immutable after construction; `OperationRegistry` layered by ADR-019 (curated immutable, session/imported dynamic) | +| Vault integration via assembly layer | [ADR-008](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time | +| Secret material flow and capability injection | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Capabilities carry outbound credentials; call protocol carries no secret material | +| Privilege model and authority context | [ADR-017](decisions/017-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; composition authority + scoped env | +| Handler registration, provenance, and composition authority | [ADR-018](decisions/018-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle | +| Operation registry layering | [ADR-019](decisions/019-operation-registry-layering.md) | Curated (static, immutable) + session and connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) | +| Operation error schemas | [ADR-016](decisions/016-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity for `from_openapi`/`to_openapi` | +| Call protocol client and adapter contract | [ADR-022](decisions/022-call-protocol-client-and-adapter-contract.md) | `from_call`/`OperationAdapter` produce `HandlerRegistration` bundles; adapter-registered ops are `Internal` leaves. Surface specced in [client-and-adapters.md](client-and-adapters.md) | +| `from_jsonschema` as HTTP-backed single-endpoint adapter | [ADR-027](decisions/027-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf (now handler-bearing, not "no handler") | +| Peer-graph routing model (supersedes ADR-023) | [ADR-024](decisions/024-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` (the field this doc's `HandlerRegistration` previously gained) | +| Forwarded-for identity | [ADR-026](decisions/026-forwarded-for-identity.md) | `forwarded_for` field on `OperationContext` and `call.requested`; metadata only — `AccessControl::check` never reads it; the `from_call` handler populates it | +| Streaming handler for subscriptions | [ADR-021](decisions/021-streaming-handler-for-subscriptions.md) | `StreamingHandler` type alongside `Handler`; `HandlerKind` enum on `HandlerRegistration` validated against `op_type`; `invoke_streaming()` on `OperationRegistry`; `invoke()` and `OperationEnv::invoke()` error with `INVALID_OPERATION_TYPE` on `Subscription` ops; composition stays request/response-only, stream composition is handler-level | +| Dynamic resource ownership for runtime-spawned resources | [ADR-011](decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md) | `AccessControl::check` consults an `OwnershipProvider` (sync read trait, alknet ADR-033 repo/adapter pattern); `OperationSpec` gains `resource_id_path` (JSON pointer into the input); proxy-only access pattern (spawner owns, proxy to share, teardown revokes); `list` = scope-gate + result-filter; teardown = automatic, handler-driven; composition = two orthogonal checks, ADR-017/022 unchanged | +| Publish operation type and HandlerKind::Sink | [ADR-046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | `OperationType::Pub` (producer→consumer streaming); `SinkHandler` + `HandlerKind::Sink` + `PublishStream`; `invoke_sink()` dispatch path; `call.published` wire event; `OperationSpec.publish_schema`; `Subscription` renamed to `Sub`; fan-out/broker out of scope for alkcall (OQ-22 resolved, OQ-42 downstream) | ## Open Questions See [open-questions.md](open-questions.md) for full details. -- **OQ-13** (resolved): Operation path format is `/{service}/{op}`. Remote dispatch is a separate mechanism, not a path prefix. -- **OQ-14** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive. -- **OQ-16** (resolved by ADR-010): No vault operations are exposed over the call protocol for now. -- **OQ-19** (resolved): Session-scoped operation registries — agent-written operations overlaid on the curated registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. Session ops are `Session` provenance (ADR-018) — always `Internal`, compose under restricted authority scoped down at sandbox creation. Generalized by ADR-019 to cover connection-scoped overlays as well. -- **OQ-25** (dissolved by ADR-024): `remote_safe` marking shape — moot. +- **OQ-02** (resolved): Operation path format is `/{service}/{op}`. Remote dispatch is a separate mechanism, not a path prefix. +- **OQ-03** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive. +- **OQ-09** (resolved by ADR-010): No vault operations are exposed over the call protocol for now. +- **OQ-04** (resolved): Session-scoped operation registries — agent-written operations overlaid on the curated registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. Session ops are `Session` provenance (ADR-018) — always `Internal`, compose under restricted authority scoped down at sandbox creation. Generalized by ADR-019 to cover connection-scoped overlays as well. +- **OQ-10** (dissolved by ADR-024): `remote_safe` marking shape — moot. `remote_safe`/`trusted_peer` are retired; peer authorization is `AccessControl::check(peer_identity)`, the existing mechanism. See [client-and-adapters.md](client-and-adapters.md) and ADR-024 §3. -- **OQ-26** (resolved): `OperationAdapter` error type — `AdapterError` +- **OQ-11** (resolved): `OperationAdapter` error type — `AdapterError` variants: `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision` (replaces flat `Conflict`). `#[non_exhaustive]`. See [client-and-adapters.md](client-and-adapters.md). -- **OQ-27** (resolved): `from_call` re-import trigger — `from_call` is a +- **OQ-12** (resolved): `from_call` re-import trigger — `from_call` is a manual free function; the assembly layer calls it after the dial (in `AlknetClient`). A `CallConnection::refresh()` method is a genuine feature addition — non-breaking, additive. See - [ADR-028](decisions/069-from-call-manual-free-function.md). -- **OQ-28** (resolved): `from_call` namespace collision — same-peer + [ADR-028](decisions/028-from-call-manual-free-function.md). +- **OQ-13** (resolved): `from_call` namespace collision — same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays). `namespace_prefix` is optional local-naming sugar. See [client-and-adapters.md](client-and-adapters.md). -- **OQ-29..37** are tracked in [client-and-adapters.md](client-and-adapters.md) - (they concern the `CallClient` / adapter surface and peer-graph routing, - not the registry layering this document covers). In brief: OQ-29 - (CallClient TLS client-auth) resolved; OQ-30 (`PeerRef::Any` routing - policy) resolved; OQ-31 (`services/list-peers` re-export semantics) - resolved; OQ-32 (multi-hop federation) open (feature extension); - OQ-33 (PeerId source) resolved by ADR-025; OQ-34 (persistent peer - registry) resolved by ADR-025+033; OQ-35 (API key asymmetry) dissolved; - OQ-36 (concrete persistence adapter shapes) resolved by ADR-035; - OQ-37 (X.509 outgoing-only) resolved by ADR-034. -- **OQ-42** (resolved by ADR-011): Dynamic resource ownership for +- The client-surface questions (the `CallClient` / adapter surface and + peer-graph routing) are tracked in + [client-and-adapters.md](client-and-adapters.md), not the registry + layering this document covers; in brief: OQ-14 + (`CallClient` TLS client-auth) resolved; OQ-15 (`PeerRef::Any` routing + policy) resolved; OQ-16 (`services/list-peers` re-export semantics) + resolved; OQ-17 (multi-hop federation) deferred(scope) (feature + extension); OQ-18 (`PeerId` source) resolved by ADR-025; OQ-19 + (persistent peer registry) resolved by ADR-025; OQ-20 (API key + asymmetry) dissolved; OQ-21 (X.509 outgoing-only) resolved — the + deciding ADR (alknet ADR-034, not ported) named the three remote + roles; see OQ-21 in [open-questions.md](open-questions.md). +- **OQ-30** (resolved by ADR-011): Dynamic resource ownership for runtime-spawned resources — `AccessControl::check` consults an `OwnershipProvider`; `OperationSpec` gains `resource_id_path`; proxy-only access pattern; four edge specifics pinned (`list`, teardown, fleet, - composition). See [auth.md](../core/auth.md) §"Ownership Provider and - Store" for the trait shapes. + composition). See ADR-011 for the trait shapes. ## References