docs: spec-level sync with the single-publish fix + port-debt repair

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.
This commit is contained in:
glm-5.3-flash committed 2026-10-13 09:34:37 +00:00
1 parent 3317ea0963
commit 7b8a3e7558
26 files changed
+432 -332

No files matched your search

+9 -1
View File
@@ -238,7 +238,7 @@ directions:
## Architecture Context ## Architecture Context
- `docs/architecture/` — the authoritative spec. Read it before - `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. track resolved/deferred decisions.
- This crate unifies `alknet-call` and `alknet-channels` from the - This crate unifies `alknet-call` and `alknet-channels` from the
alknet mono-repo (`/workspace/@alkdev/alknet`). The source alknet mono-repo (`/workspace/@alkdev/alknet`). The source
@@ -296,6 +296,14 @@ directions:
`from_connection` primary; dial lives in the consumer) `from_connection` primary; dial lives in the consumer)
- ADR-041 — per-identity channel cap (256 per `PeerId` via - ADR-041 — per-identity channel cap (256 per `PeerId` via
`ChannelLifecyclePolicy`) `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):** **Shared (vendored core types):**
- ADR-001 — ALPN-based protocol dispatch - ADR-001 — ALPN-based protocol dispatch
+34 -16
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-08-12 last_updated: 2026-10-13
--- ---
# alkcall # 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 See [open-questions.md](open-questions.md) for the full tracker. Key
questions affecting this crate: questions affecting this crate:
- **OQ-01**: Call protocol pub/sub primitive (partially resolved) — - **OQ-22** (resolved) / **OQ-42** (deferred(scope)): The call-protocol
ADR-046 adds the `Pub` primitive (producer→consumer streaming). The pub/sub primitive is delivered (ADR-046); the fan-out/message broker
fan-out/broker is deferred to channels (Gap B in ADR-047 is named is placed out of scope for alkcall — the hub composes it on the
out-of-scope for alkcall; the hub composes the broker on top). `Pub`/`Sub` primitives — and its design remains deferred downstream,
- **OQ-02**: Full channel-level flow-control windowing (deferred(scope)) 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 — bounded-buffer is decided (ADR-040); full windowing blocked on a real
HOL-blocking deployment observation. HOL-blocking deployment observation.
- **OQ-03**: Channels add/strip API shape (open) — whether the 8-byte - **OQ-24** (resolved): Channels add/strip API shape — integral to the
header add/strip is built into the read/write path or a standalone read/write path (handlers never see headers); low-level header
utility. 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 ## Key Design Principles
@@ -145,6 +151,14 @@ questions affecting this crate:
channel 0. See ADR-036, ADR-037. channel 0. See ADR-036, ADR-037.
10. **Wire formats are stable**: EventEnvelope shape and the 8-byte chunk 10. **Wire formats are stable**: EventEnvelope shape and the 8-byte chunk
header are one-way doors. See ADR-014, ADR-034. 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 ## 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 > preserved here as the authoritative spec for alkcall; the alknet
> mono-repo will consume alkcall's versions when it is reworked. > mono-repo will consume alkcall's versions when it is reworked.
> >
> **Cross-references to non-ported ADRs**: Some spec docs and ADRs > **Cross-references to non-ported ADRs**: alkcall's own ADRs are
> reference alknet ADRs by their original numbers (e.g., ADR-052 for > `decisions/001..053` in this directory. Some ported docs also cite
> TTY's wire format, ADR-082 for alknet-tls, ADR-086 for endpoint types). > sibling-crate ADRs that were **not** ported (e.g., alknet ADR-052 for
> These are ADRs for sibling crates that are not part of alkcall. They > TTY's wire format, alknet ADR-034 for outgoing-only X.509, alknet
> retain their alknet numbering (052, 082, 086, etc.) — any ADR number > ADR-082 for alknet-tls, alknet ADR-086 for endpoint types). The alknet
> outside the alkcall range 001..048 is an alknet source ADR, found at > number space collides with alkcall's (alknet ADR-052 has nothing to do
> `/workspace/@alkdev/alknet/docs/architecture/decisions/`. > 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).
+43 -43
View File
@@ -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` | | [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 | | [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) | | [031](decisions/031-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 | | [033](decisions/033-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 | | [003](decisions/003-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) | | [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) |
| [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 | | [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 |
| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | Generic single-stream connections; unblocks TCP+TLS/SSH/WT/wasm dispatch | | [007](decisions/007-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 | | [004](decisions/004-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 | | [005](decisions/005-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 | | [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](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Static handler registration | | [010[010](/workspace/@alkdev/alknet/docs/architecture/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 | | [015](decisions/015-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 | | [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 |
| [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 | | [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 |
| [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 | | [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 |
| [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 | | [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 |
| [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 | | [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 |
| [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 | | [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 |
| [023](decisions/023-operation-error-schemas.md) | Operation Error Schemas | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity | | [016](decisions/016-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) | | [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) |
| [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`) | | [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`) |
| [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` | | [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` |
| [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 | | [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 |
| [032](decisions/032-forwarded-for-identity.md) | Forwarded-For Identity | `forwarded_for` on `OperationContext` and `call.requested`; metadata only, never used by `AccessControl::check` | | [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](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 | | [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 |
| [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 | | [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 |
| [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 | | [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 ## Relevant Open Questions
| OQ | Title | Status | Relevance | | OQ | Title | Status | Relevance |
|----|-------|--------|-----------| |----|-------|--------|-----------|
| OQ-07 | Call protocol scope within a connection | resolved (ADR-015) | Stream model, multiplexing, scope | | OQ-01 | 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-02 | 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-03 | 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-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-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-09 | Safe vault operations for call protocol exposure | resolved (ADR-010) | None exposed for now |
| OQ-25 | ~~Remote-safe marking shape~~ | **dissolved** (ADR-024) | `remote_safe`/`trusted_peer` retired; peer authorization is `AccessControl::check(peer_identity)` | | OQ-10 | ~~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-11 | 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-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-28 | from_call namespace collision | **resolved** | Same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays) | | OQ-13 | 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-14 | 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-15 | `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-16 | `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-17 | Multi-hop federation | deferred(scope) (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-18 | 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-19 | Persistent peer registry | **resolved** (ADR-025; persistence adapters are alknet-side) | 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-20 | ~~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-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 ## Key Design Principles
+64 -45
View File
@@ -129,18 +129,16 @@ pub struct CallConnection {
impl CallConnection { impl CallConnection {
/// Register an imported operation into this connection's overlay /// Register an imported operation into this connection's overlay
/// (Layer 2, ADR-019). Called by `from_call` after discovery. /// (Layer 2, ADR-019). Called by `from_call` after discovery.
pub fn register_imported(&self, registration: HandlerRegistration) { /// Refuses marked (`channel_open`) specs — they are not
let name = registration.spec.name.clone(); /// overlay-importable (ADR-047 amendment 4 §4.8).
self.imported_operations.write().insert(name, registration); pub fn register_imported(&self, registration: HandlerRegistration)
} -> Result<(), String>;
/// Register multiple imported operations (bulk variant for `from_call`). /// Register multiple imported operations (bulk variant for `from_call`).
pub fn register_imported_all(&self, registrations: Vec<HandlerRegistration>) { /// Same marked-spec refusal; on refusal the entries inserted before the
let mut overlay = self.imported_operations.write(); /// marked one stay in the overlay and the error is returned.
for reg in registrations { pub fn register_imported_all(&self, registrations: Vec<HandlerRegistration>)
overlay.insert(reg.spec.name.clone(), reg); -> Result<(), String>;
}
}
/// Build an `OperationEnv` impl for this connection's overlay. Used by /// Build an `OperationEnv` impl for this connection's overlay. Used by
/// the `CallAdapter` when composing the root `OperationContext.env`. /// 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 — When the connection drops, the overlay (and all imported ops) is dropped —
no explicit deregistration needed. 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: The adapter:
1. Accepts bidirectional streams on the connection 1. Accepts bidirectional streams on the connection
2. Reads length-prefixed JSON `EventEnvelope` frames from each stream 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 §3). The composition env is peer-keyed (`PeerCompositeEnv`,
ADR-024 §1) to handle head→N-workers routing. See ADR-024 §1) to handle head→N-workers routing. See
[client-and-adapters.md](client-and-adapters.md) for the `Dispatcher` mechanism [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. peer-graph routing model.
### Stream 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. - `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`. - `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. - `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. 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. - 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. - 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. - 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. - `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. - **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. - **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 | | 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) | | 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/012-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | | 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/006-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN | | 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/007-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams | | ProtocolHandler receives Connection | [ADR-005](decisions/005-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 | | 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/014-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer | | 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/015-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env | | 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/016-abort-cascade-for-nested-calls.md) | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in | | 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/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) | | 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/022-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle | | 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/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` | | 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/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 | | 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/023-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details` | | 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` | | 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` | | 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. 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-02** (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-03** (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-09** (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-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-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` retired; peer authorization is `remote_safe`/`trusted_peer` retired; peer authorization is
`AccessControl::check(peer_identity)`. `AccessControl::check(peer_identity)`.
- **OQ-26** (resolved): `OperationAdapter` error type — `AdapterError` - **OQ-11** (resolved): `OperationAdapter` error type — `AdapterError`
variants (`DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, variants (`DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`,
`SamePeerCollision`); `#[non_exhaustive]`. See `SamePeerCollision`); `#[non_exhaustive]`. See
[client-and-adapters.md](client-and-adapters.md). [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 manual free function; the assembly layer calls it after the dial (in
`AlknetClient`). See `AlknetClient`). See
[ADR-028](decisions/069-from-call-manual-free-function.md). [ADR-028](decisions/028-from-call-manual-free-function.md).
- **OQ-28** (resolved): `from_call` namespace collision — same-peer collision - **OQ-13** (resolved): `from_call` namespace collision — same-peer collision
= error; cross-peer dissolved by ADR-024 (separate sub-overlays). See = error; cross-peer dissolved by ADR-024 (separate sub-overlays). See
[client-and-adapters.md](client-and-adapters.md). [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 (present Ed25519 key as raw public key client cert); key-type-aware server
cert verification; fingerprint normalization. See cert verification; fingerprint normalization. See
[client-and-adapters.md](client-and-adapters.md). [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). 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). `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 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). 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 `IdentityProvider` resolution (= `PeerEntry.peer_id`, stable across key
rotation), not a connection-assigned UUID. 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 the storage boundary is `core trait + in-memory default`; persistence
adapters are separate crates. adapters are separate crates (the concrete adapter shapes were named in
- **OQ-37** (resolved by ADR-034): X.509 outgoing-only case — three remote 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 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 X.509 connections are not in the peer graph on the client side. The
[client-and-adapters.md](client-and-adapters.md). 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 ## References
+5 -5
View File
@@ -185,7 +185,7 @@ request/response role.
`ChannelClient`'s *API* is transport-agnostic — `from_connection` takes a `ChannelClient`'s *API* is transport-agnostic — `from_connection` takes a
pre-established `Connection`. The shared *dial+TLS* seam 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` / provides `AlknetClient` with three dial methods (`dial_quic` /
`dial_tcp_tls` / `dial_iroh`), each producing a `Connection` that `dial_tcp_tls` / `dial_iroh`), each producing a `Connection` that
`from_connection` consumes. The dial is transport-specific (QUIC, `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 `AlknetClient::dial_quic` is the dial that feeds `from_connection`. A
caller that needs transport selection (QUIC with TCP+TLS fallback) uses caller that needs transport selection (QUIC with TCP+TLS fallback) uses
`AlknetClient` directly; the fallback policy is a caller concern. See `AlknetClient` directly; the fallback policy is a caller concern. See
[ADR-045](decisions/089-alknetclient-native-dial-seam.md) for the [ADR-045](decisions/045-alknetclient-native-dial-seam.md) for the
full decision and [OQ-55](../../questions/055-alknetclient-establishment-extraction.md) full decision and [OQ-55](/workspace/@alkdev/alknet/docs/architecture/questions/055-alknetclient-establishment-extraction.md)
(resolved). (resolved).
## Design Decisions ## Design Decisions
@@ -210,7 +210,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
## Open Questions ## 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 — `alknet-client` with three dial methods. `ChannelClient`'s API is
transport-agnostic (`from_connection`); the dial is the shared seam. transport-agnostic (`from_connection`); the dial is the shared seam.
See [ADR-045](decisions/045-alknetclient-native-dial-seam.md). 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-039: ChannelManager (the shared state `ChannelClient` holds)
- ADR-047: openable ALPNs are operations (per-ALPN open ops dissolve - ADR-047: openable ALPNs are operations (per-ALPN open ops dissolve
the generic `channel/open`) 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 - `docs/architecture/crates/call/client-and-adapters.md` — `CallClient` (the
shape `ChannelClient` mirrors) shape `ChannelClient` mirrors)
+1 -1
View File
@@ -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 the channels layer has no `stream_type` concept — it carries the
handler's framing transparently in the payload. TTY's `STREAM_CTRL_IN` 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 (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; channels-layer concepts. The channels layer routes by `channel_id` only;
the handler owns its sub-stream multiplexing on the `BiStream` it the handler owns its sub-stream multiplexing on the `BiStream` it
receives. receives.
+26 -26
View File
@@ -20,7 +20,7 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives.
| Document | Status | Description | | 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-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-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) | | [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 | | 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 | | [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 |
| [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 | | [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 |
| [072](decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alk/call` | Channel 0 = call protocol; no special control plane | | [036](decisions/036-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 | | [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 |
| [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) | | [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) |
| [075](decisions/075-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts | | [039](decisions/039-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) | | [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) |
| [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) | | [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](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 | | [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 |
| [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 | | [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 |
| [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 | | [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 |
| [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) | | [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) |
| [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 | | [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 |
| [070](decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements | | [008](decisions/008-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 | | [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 |
| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on | | [007](decisions/007-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) | | [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) |
| [049](decisions/049-streaming-handler-for-subscriptions.md) | StreamingHandler for Subscriptions | The machinery `channel/resources/subscribe` uses | | [021](decisions/021-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 | | [026](decisions/026-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 | | [031](decisions/031-crate-decomposition.md) | Crate Decomposition | alknet-channels depends on alknet-core only; no handler-depends-on-handler |
## Relevant Open Questions ## Relevant Open Questions
| OQ | Title | Status | Relevance | | 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) | | 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-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-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" |
| 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) | | 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-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 | | 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 ## 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) in how they're *opened* (negotiation via `channel/open` on channel 0)
and what *multiplexing layer* carries them (the 8-byte chunk format). and what *multiplexing layer* carries them (the 8-byte chunk format).
Once normalized, every channel is an ALPN routed through the same 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). amended by ADR-035).
2. **Channel 0 is `alk/call` pre-negotiated, not a special control 2. **Channel 0 is `alk/call` pre-negotiated, not a special control
+5 -5
View File
@@ -158,11 +158,11 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
| ADR | Decision | Summary | | ADR | Decision | Summary |
|-----|----------|---------| |-----|----------|---------|
| [074](decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel `BidiStreamSource`; yield-once `accept_bi` is the only accessor | | [038](decisions/038-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 | | [035](decisions/035-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 | | [008](decisions/008-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) | | [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) |
| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The yield-once path generalized for channels | | [007](decisions/007-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The yield-once path generalized for channels |
## References ## References
+8 -7
View File
@@ -89,7 +89,7 @@ on the `BiStream` the channels layer gives them (ADR-035).
inner layer's framing byte, carried transparently in the payload. inner layer's framing byte, carried transparently in the payload.
- **The control channel is handler-internal.** TTY sub-demuxes control - **The control channel is handler-internal.** TTY sub-demuxes control
from its io `BiStream` using its 5-byte format (`STREAM_CTRL_IN` / 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. doesn't carry control.
- **Recursive composition is literal.** A channel with ALPN - **Recursive composition is literal.** A channel with ALPN
`alk/channels` runs another channels demux on its `BiStream`. The `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). Open questions are tracked in [open-questions.md](open-questions.md).
Key questions affecting this crate: 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. — extracted as `alknet-client` with three dial methods.
`ChannelClient`'s API is transport-agnostic (`from_connection`); the `ChannelClient`'s API is transport-agnostic (`from_connection`); the
dial is the shared seam, now extracted. See dial is the shared seam, now extracted. See
[ADR-045](decisions/045-alknetclient-native-dial-seam.md). [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 bounded-buffer is decided (ADR-040); full windowing is an extension
blocked on a real HOL-blocking deployment observation. blocked on a real HOL-blocking deployment observation.
- **OQ-57** (deferred(scope)): Two-pump helper extraction to alknet-core — - **alknet OQ-57** (deferred(scope)): Two-pump helper extraction to alknet-core —
the *contract* is decided (ADR-078); the *helper* is blocked on a second the *contract* is decided (the in-tree helper landed as
two-pump handler existing. `channels::pump_bidi`, ADR-050); any extraction to alknet-core is
- **OQ-68** (open): Add/strip API shape — whether the 8-byte header 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 add/strip is built into the channels read/write path or exposed as a
standalone utility. The *contract* (channels strips, handler parses standalone utility. The *contract* (channels strips, handler parses
payload) is decided (ADR-035); the *function surface* is not. payload) is decided (ADR-035); the *function surface* is not.
+14 -10
View File
@@ -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 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]` 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 framing, ADR-014), tunnel's raw bytes, SSH's channel protocol. The
channels layer carries the bytes transparently. 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` ## `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 `length > MAX_CHUNK_LEN` returns `ChunkTooLarge` and does not corrupt the
stream — the demux drops the chunk and continues. The header is always 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 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, A zero-length chunk (`length = 0`) is delivered as an empty payload,
which the reassembled stream interprets as EOF. This is the clean-shutdown 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 §Sentinels), now at the channels layer (one sentinel per channel, not
per `(channel_id, stream_type)`). 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 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 vs. a standalone utility) is an implementation detail for the channels
crate, tracked as OQ-68. The *contract* — the channels layer strips its crate; the shipped surface resolves it (OQ-24, formerly alknet OQ-68):
8-byte header on read and the handler parses its own framing from the the strip is integrated into the read/write path — the demux loop
payload — is decided; the *function surface* is not. 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) ## 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). Open questions are tracked in [open-questions.md](open-questions.md).
Key questions affecting this doc: Key questions affecting this doc:
- **OQ-68** (open): Add/strip API shape — whether the 8-byte header - **OQ-24** (tracker; alknet OQ-68) (resolved): Add/strip API shape —
add/strip is built into the channels read/write path or exposed as a built into the read/write path (the demux loop strips headers; handlers
standalone utility. The *contract* (channels strips, handler parses never see them), with the low-level header primitives exposed in
payload) is decided; the *function surface* is not. `channels::wire` for tooling.
## References ## References
+98 -83
View File
@@ -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 / > It is now an HTTP-backed single-endpoint adapter for non-standard /
> non-OpenAPI / basic REST endpoints, functionally similar to > non-OpenAPI / basic REST endpoints, functionally similar to
> `from_openapi` but one endpoint at a time. See > `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` > The `FromJsonSchema` provenance variant stays in `alknet-call`
> (`OperationProvenance`); only the adapter implementation moved. > (`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` authorization system duplicated the existing `AccessControl`/`Identity`
machinery. **ADR-024 supersedes ADR-023**: peer-keyed overlays + `PeerRef` machinery. **ADR-024 supersedes ADR-023**: peer-keyed overlays + `PeerRef`
routing, and peer authorization through the existing `AccessControl::check(peer_identity)`. 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 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-26). cross-peer dissolved / same-peer stays, DC-4→OQ-11).
## Architecture ## 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 `FORBIDDEN` before the handler runs (capabilities never populated — the
security property). An op that should never be callable from the wire uses security property). An op that should never be callable from the wire uses
`Visibility::Internal` (existing mechanism, `NOT_FOUND` before ACL). See `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`. mapping of the three `remote_safe` cases to `AccessControl`/`Visibility`.
The connection is symmetric after establishment (ADR-022 §2): both sides can 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 `OperationEnv` gains a peer-routing method with a `PeerRef` selector
(`Specific(PeerId)` / `Any`), default-impl for back-compat. See (`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 `invoke_peer` signature and `ScopedPeerEnv` peer-qualified reachability. The
per-`CallConnection` overlay stays flat (one connection = one peer); the per-`CallConnection` overlay stays flat (one connection = one peer); the
peer-keying is at the aggregation layer (the head node's composition env). 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` `remote_safe` filter — both retired by ADR-024). `services/list-peers`
is the opt-in for peer-attributed re-export listing (each peer's is the opt-in for peer-attributed re-export listing (each peer's
sub-overlay listed with attribution, filtered by the calling 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 ### Credential sources for connections
@@ -286,7 +286,7 @@ There is no call-protocol credential bundle. The transport dimensions
`alknet-core` per ADR-012. `alknet-core` per ADR-012.
- **TLS identity** — the local node's Ed25519 raw key (RFC 7250) or X.509 cert, - **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 - **Auth token** — an opaque call-protocol-level token, decrypted from the
vault or derived from a shared secret. vault or derived from a shared secret.
- **Remote identity verification** — the expected fingerprint/cert of the - **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 `RemoteIdentity` are implementation-detail two-way doors; the one-way
constraints are that they come from `Capabilities`, not env vars (ADR-010). 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 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 equivalent of the server's `RawKeyCertResolver`. This is **wired now**, not
additive: it is what activates the `PeerEntry` fingerprint → `peer_id` 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. 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 The iroh path already works (iroh uses RFC 7250 raw keys and exchanges
Ed25519 public keys during the TLS handshake automatically); the gap was 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 with presenting the key. The one-way constraint (credentials from
`Capabilities`, not env vars, ADR-010) is unaffected — the `auth_token` `Capabilities`, not env vars, ADR-010) is unaffected — the `auth_token`
dimension flows through the call-protocol `auth_token` payload field, not dimension flows through the call-protocol `auth_token` payload field, not
TLS, so the no-env-vars invariant holds independently of the TLS layer. 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`) server's fingerprint against an expected value (`credentials.remote_identity`)
is **additive** — the server-side fingerprint extraction is what matters for is **additive** — the server-side fingerprint extraction is what matters for
`PeerId`, not the client-side verification. The verifier for raw keys can `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 This is a two-way-door remainder; the one-way constraint (credentials from
`Capabilities`, not env vars) is unaffected. `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` `ServerCertVerifier` is selected by whether the local node has a `PeerEntry`
for the remote, not by key type alone. A pure-client for the remote, not by key type alone. A pure-client
connection to a **public X.509 endpoint** (no `PeerEntry` on the local 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 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 raw-key remote fails closed (no CA to fall back to — raw-key remotes
are always known peers). See 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. for the verifier selection rule and the three-role naming.
### from_call ### from_call
@@ -353,7 +353,7 @@ forwarding handlers. The discovery mechanism (`services/list` +
```rust ```rust
pub struct FromCallConfig { pub struct FromCallConfig {
/// Namespace prefix applied to imported operation names. Optional — /// 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. /// not last-wins.
pub namespace_prefix: Option<String>, pub namespace_prefix: Option<String>,
/// Optional filter — import only operations whose names match. None /// 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 sends `call.requested` via `CallConnection::call_with_payload()`, awaits
the single `call.responded` (or `call.error`), returns the the single `call.responded` (or `call.error`), returns the
`ResponseEnvelope`. `ResponseEnvelope`.
- `Subscription` → a `StreamingHandler` (registered as - `Subscription` → a `StreamingHandler` (registered as
`HandlerKind::Stream`): calls `CallConnection::subscribe()`, which `HandlerKind::Stream`): calls `CallConnection::subscribe()`, which
returns `impl Stream<Item = ResponseEnvelope>` (the client-side returns `impl Stream<Item = ResponseEnvelope>` (the client-side
streaming path, already implemented), maps it to a streaming path, already implemented), maps it to a
`BoxStream<ResponseEnvelope>`. The remote stream flows end-to-end: `BoxStream<ResponseEnvelope>`. The remote stream flows end-to-end:
each `call.responded` the remote sends becomes a stream item; the each `call.responded` the remote sends becomes a stream item; the
remote's `call.completed` ends the stream (→ wire `call.completed`); remote's `call.completed` ends the stream (→ wire `call.completed`);
`call.aborted` drops the stream (cascade per ADR-020). No truncation, `call.aborted` drops the stream (cascade per ADR-020). No truncation,
no first-value fallback — a `from_call`-imported subscription forwards no first-value fallback — a `from_call`-imported subscription forwards
the full remote stream. the full remote stream.
- `provenance: FromCall`, `composition_authority: None`, `scoped_env: None` - `Pub` → the sink forwarding shape (registered as `HandlerKind::Sink`,
(leaf — ADR-018). 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 4. The caller registers the bundles via
`CallConnection::register_imported_all()`. `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 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 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; re-import on reconnect is naturally scoped to the new
connection. A `CallConnection::refresh()` method for mid-connection connection. A `CallConnection::refresh()` method for mid-connection
re-discovery is a genuine feature addition — non-breaking, additive — if a re-discovery is a genuine feature addition — non-breaking, additive — if a
deployment needs manual re-discovery without drop-and-reconnect. See 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 cross-peer collision dissolves — same name on different peers is fine (they
live in separate peer sub-overlays, no prefix needed). Same-peer collision 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). 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 available for logging, auditing, per-user rate limiting, but never used by
`AccessControl::check` (the spoke authorizes the hub, its direct caller, `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 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 ### 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 (a planning activity that doesn't need a registry entry) with operation
registration (which always needs a handler). 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 `from_jsonschema` to `alknet-http` as an HTTP-backed single-endpoint
adapter: the caller supplies an `OperationSpec` + `HttpServiceConfig` + adapter: the caller supplies an `OperationSpec` + `HttpServiceConfig` +
path template + method, and the adapter builds one 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` `FromJsonSchema` provenance. It is functionally similar to `from_openapi`
but one endpoint at a time, for non-standard / non-OpenAPI / basic REST but one endpoint at a time, for non-standard / non-OpenAPI / basic REST
endpoints that don't have a full OpenAPI document. See 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 The schema-validation-without-a-handler use case (the original stated
purpose) is served by consuming `OperationSpec` directly — the spec 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 trait — their `import()` bodies contain no `.await` points. This is locked by
ADR-022 §5. ADR-022 §5.
The **error type** (DC-4, OQ-26) is `Result<Vec<HandlerRegistration>, The **error type** (DC-4, OQ-11) is `Result<Vec<HandlerRegistration>,
AdapterError>` where `AdapterError` is a crate-level enum covering the AdapterError>` where `AdapterError` is a crate-level enum covering the
failure modes real implementations hit: discovery transport failure failure modes real implementations hit: discovery transport failure
(`from_call` remote unreachable), schema parse failure (`from_openapi`, (`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). via `PeerRef::Specific` / `PeerRef::Any` (ADR-024 §1-2).
- **`from_call` is a manual free function.** The assembly layer calls it - **`from_call` is a manual free function.** The assembly layer calls it
after the dial (in `AlknetClient`). The overlay is per-connection so after the dial (in `AlknetClient`). The overlay is per-connection so
re-import on reconnect is naturally scoped (DC-2, OQ-27). See re-import on reconnect is naturally scoped (DC-2, OQ-12). See
[ADR-028](decisions/069-from-call-manual-free-function.md). [ADR-028](decisions/028-from-call-manual-free-function.md).
- **`from_call` namespace collision is same-peer only.** Cross-peer collision - **`from_call` namespace collision is same-peer only.** Cross-peer collision
dissolves (same name on different peers is fine — separate sub-overlays, dissolves (same name on different peers is fine — separate sub-overlays,
ADR-024 §5). Same-peer collision stays an error. `namespace_prefix` is 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 - **`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 stdio transport is not built.** Streamable HTTP is the only supported
MCP transport in alknet. stdio = spawn arbitrary executable = built-in RCE. MCP transport in alknet. stdio = spawn arbitrary executable = built-in RCE.
Recorded as an explicit security position, not a feature gap. 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 invoked through the `CallConnection` handle. The client-side
`ServerCertVerifier` uses CA verification (`WebPkiServerVerifier`) for `ServerCertVerifier` uses CA verification (`WebPkiServerVerifier`) for
such remotes; known peers (hub with `PeerEntry`) use fingerprint 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` - **`ConnectionCredentials.remote_identity: None` is load-bearing.** `None`
means "no `PeerEntry` for this remote → use CA verification (X.509) means "no `PeerEntry` for this remote → use CA verification (X.509)
or fail closed (Ed25519 raw key)" per the ADR-034 §3 verifier rule. 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 | | 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 | | 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/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 | | `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/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via existing `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` | | 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/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 | | 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/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` | | 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 | [ADR-033](decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | | 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/014-secret-material-flow-and-capability-injection.md) | The no-env-vars invariant's foundation; capabilities injected at assembly layer | | 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/022-handler-registration-provenance-and-composition-authority.md) | The registration bundle adapters produce; `composition_authority: None` for leaves | | 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/024-operation-registry-layering.md) | Layer 2 per-connection overlay where `from_call` imports land | | 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/015-privilege-model-and-authority-context.md) | Adapter-registered ops are `Internal` by default; default-deny posture | | 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/016-abort-cascade-for-nested-calls.md) | Cross-node abort through `from_call` forwarding handler's `parent_request_id` | | 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/023-operation-error-schemas.md) | `error_schemas` mirrored by `from_call` from remote op's spec | | 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/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` | | 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 | [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`) | | 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 | [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` | | 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 | [ADR-020](decisions/020-hd-derivation-for-encryption-keys.md) | Vault-derived TLS identity material | | 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 | [ADR-026](decisions/026-vault-key-model-hd-derivation.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 | [ADR-025](decisions/025-vault-local-only-dispatch.md) | Vault access at assembly layer only; the credential injection path's first hop | | 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/003-crate-decomposition.md) | `alknet-http` depends on `alknet-call` (protocol-foundation exception, noted in Adapter Location Map) | | 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/009-one-way-door-decision-framework.md) | Door-type classification for DC-1..4 | | One-way door decision framework | [ADR-032](decisions/032-one-way-door-decision-framework.md) | Door-type classification for DC-1..4 |
## Open Questions ## Open Questions
See [open-questions.md](open-questions.md) for full details. 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 `remote_safe`/`trusted_peer` are retired; peer authorization is
`AccessControl::check(peer_identity)`. No marking to shape. `AccessControl::check(peer_identity)`. No marking to shape.
- **OQ-26** (resolved): `AdapterError` variants — `DiscoveryFailed`, - **OQ-11** (resolved): `AdapterError` variants — `DiscoveryFailed`,
`SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision` `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`
(replaces flat `Conflict`). `#[non_exhaustive]`. (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 manual free function; the assembly layer calls it after the dial (in
`AlknetClient`). A `CallConnection::refresh()` method is a genuine `AlknetClient`). A `CallConnection::refresh()` method is a genuine
feature addition — non-breaking, additive. See feature addition — non-breaking, additive. See
[ADR-028](decisions/069-from-call-manual-free-function.md). [ADR-028](decisions/028-from-call-manual-free-function.md).
- **OQ-28** (resolved): `from_call` namespace collision — same-peer - **OQ-13** (resolved): `from_call` namespace collision — same-peer
collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays). collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays).
`namespace_prefix` is optional local-naming sugar. `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); client-auth (present Ed25519 key as raw public key client cert);
key-type-aware server cert verification (raw key = fingerprint match, key-type-aware server cert verification (raw key = fingerprint match,
X.509 = CA verification); fingerprint normalization (`ed25519:` across X.509 = CA verification); fingerprint normalization (`ed25519:` across
quinn/iroh). The iroh path already works; the gap was quinn-only. quinn/iroh). The iroh path already works; the gap was quinn-only.
See OQ-29 in open-questions.md. See OQ-14 in open-questions.md.
- **OQ-30** (resolved): `PeerRef::Any` routing policy — insertion-order - **OQ-15** (resolved): `PeerRef::Any` routing policy — insertion-order
first-match. A richer `RoutingPolicy` is a feature extension. 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." 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 model is the architectural commitment; multi-hop is a feature extension
that doesn't break downstream. The peer-keyed model extends to multi-hop that doesn't break downstream. The peer-keyed model extends to multi-hop
without redesign; petgraph is the candidate if path-finding becomes real without redesign; petgraph is the candidate if path-finding becomes real
(ADR-024 §3.7). (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`, `Identity.id` from `IdentityProvider` resolution (= `PeerEntry.peer_id`,
stable across key rotation). See OQ-33 in open-questions.md. stable across key rotation). See OQ-18 in open-questions.md.
- **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` (config-backed the storage boundary is `core trait + in-memory default` (config-backed
`ConfigIdentityProvider` now; persistence adapters additive in separate `ConfigIdentityProvider` now; persistence adapters additive in separate
crates). See OQ-34 in open-questions.md. crates). See OQ-19 in open-questions.md.
- **OQ-35** (dissolved): the "API key asymmetry" framing was wrong; - **OQ-20** (dissolved): the "API key asymmetry" framing was wrong;
`PeerEntry` supports multiple credential paths (fingerprints + `PeerEntry` supports multiple credential paths (fingerprints +
auth_token_hash), `ApiKeyEntry` is for tokens that ARE the identity. auth_token_hash), `ApiKeyEntry` is for tokens that ARE the identity.
See OQ-35 in open-questions.md. See OQ-20 in open-questions.md.
- **OQ-36** (resolved by ADR-035): Concrete persistence adapter shapes — - **OQ-21** (resolved, alknet ADR-034 — not ported): X.509 outgoing-only
read-sync / write-async split (`IdentityStore` async write trait case — three
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
remote roles named (public X.509 endpoint, transport relay, hub). remote roles named (public X.509 endpoint, transport relay, hub).
`PeerEntry` asymmetry is correct: a pure-client connection to a public `PeerEntry` asymmetry is correct: a pure-client connection to a public
X.509 endpoint is **not** in the call-protocol peer graph on the 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 land in the connection's Layer 2 overlay and are invoked through the
connection handle. The client-side `ServerCertVerifier` is selected connection handle. The client-side `ServerCertVerifier` is selected
by `PeerEntry` presence: known peer → fingerprint pin; unknown X.509 by `PeerEntry` presence: known peer → fingerprint pin; unknown X.509
remote → CA verification (`WebPkiServerVerifier`). See ADR-034 and remote → CA verification (`WebPkiServerVerifier`). The deciding ADR
OQ-37 in open-questions.md. (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 ## References
@@ -232,5 +232,5 @@ WebTransport stream).
- OQ-09: WASM target boundaries (resolution amended — the server-side - OQ-09: WASM target boundaries (resolution amended — the server-side
dispatch door is no longer closed by `Connection` being QUIC-bound) dispatch door is no longer closed by `Connection` being QUIC-bound)
- Transport generalization findings: - 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) - Implementation commit: `865fef6` (2026-07-09)
@@ -295,8 +295,8 @@ the args are optional for their transport.
`from_bidi` constructors and the yield-once contract are preserved via `from_bidi` constructors and the yield-once contract are preserved via
`StreamBidiStreamSource`) `StreamBidiStreamSource`)
- Channels POC summary: - 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) §"Issues Surfaced" #1 (OQ-CH-13 confirmed +EV), #2 (REQ-CORE-02)
- Channels Phase 0 findings: - 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 §POC-Validated Requirements — REQ-CORE-01, REQ-CORE-02
@@ -625,9 +625,9 @@ provider confirms it owns the container (dynamic).
- ADR-022: Call Protocol Client and Adapter Contract (`from_call` — the - ADR-022: Call Protocol Client and Adapter Contract (`from_call` — the
adapter that imports a peer's operations and re-registers them locally; adapter that imports a peer's operations and re-registers them locally;
the proxy pattern's re-export mechanism) 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) `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) (`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) §"Open Unknowns" #3 (the research finding that surfaced this question)
@@ -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 `iroh-blobs 0.103`), but that would be a *transitive* dependency of
`alknet-blobs`, not a direct dependency of `alknet-call` — alknet-call does `alknet-blobs`, not a direct dependency of `alknet-call` — alknet-call does
not import irpc and has no plans to. See 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. §3.1 for the removal trace.
## Decision ## Decision
@@ -159,6 +159,6 @@ not import irpc and has no plans to. See
dispatch path) dispatch path)
- Call protocol wire format: `crates/alknet-call/src/protocol/wire.rs` - Call protocol wire format: `crates/alknet-call/src/protocol/wire.rs`
- Transport generalization findings: - 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) §3.1 (dead `irpc` dep removal)
- Removal commit: `668d777` (2026-07-09) - Removal commit: `668d777` (2026-07-09)
@@ -302,8 +302,8 @@ Principle of least privilege.
- OQ-17: Abort cascade (the call tree is the agency chain — `parent_request_id` - OQ-17: Abort cascade (the call tree is the agency chain — `parent_request_id`
traces principal → agent) traces principal → agent)
- OQ-19: Session-scoped registries (session operations are always `Internal`) - OQ-19: Session-scoped registries (session operations are always `Internal`)
- [operation-registry.md](../crates/call/operation-registry.md) - [operation-registry.md](../operation-registry.md)
- [call-protocol.md](../crates/call/call-protocol.md) - [call-protocol.md](../call-protocol.md)
- TypeScript `@alkdev/operations` `buildEnv()` with `allowedNamespaces` — prior - TypeScript `@alkdev/operations` `buildEnv()` with `allowedNamespaces` — prior
art for scoped composition env art for scoped composition env
- POC at `/workspace/toolEnv` — demonstrated the sandbox-to-registry bridge with - POC at `/workspace/toolEnv` — demonstrated the sandbox-to-registry bridge with
@@ -253,5 +253,5 @@ not through the wire and not through `OperationSpec`.
tree and participate in cascade) tree and participate in cascade)
- `@alkdev/flowgraph` TypeScript package — prior art for call-graph tracking - `@alkdev/flowgraph` TypeScript package — prior art for call-graph tracking
with `descendants()`, `FailurePolicy`, reactive status propagation with `descendants()`, `FailurePolicy`, reactive status propagation
- [call-protocol.md](../crates/call/call-protocol.md) - [call-protocol.md](../call-protocol.md)
- [operation-registry.md](../crates/call/operation-registry.md) - [operation-registry.md](../operation-registry.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 - 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 (DC-1 shape, DC-4 error type, DC-2 re-import trigger, DC-3 namespace
collision — see [open-questions.md](../open-questions.md)) collision — see [open-questions.md](../open-questions.md))
- [call-protocol.md](../crates/call/call-protocol.md) - [call-protocol.md](../call-protocol.md)
- [operation-registry.md](../crates/call/operation-registry.md) - [operation-registry.md](../operation-registry.md)
- [client-and-adapters.md](../crates/call/client-and-adapters.md) — the spec - [client-and-adapters.md](../client-and-adapters.md) — the spec
that operationally fills the gap this ADR left to implementation that operationally fills the gap this ADR left to implementation
- `docs/research/alknet-call-completion/gap-analysis.md` — DC-1..4, the - `docs/research/alknet-call-completion/gap-analysis.md` — DC-1..4, the
decisions that needed resolution before implementation decisions that needed resolution before implementation
@@ -524,7 +524,7 @@ overlays), not by a parallel boolean gate.
§5 showed `async fn import(&self) -> Vec<HandlerRegistration>` with no error §5 showed `async fn import(&self) -> Vec<HandlerRegistration>` with no error
type. The trait returns `Result<Vec<HandlerRegistration>, AdapterError>` type. The trait returns `Result<Vec<HandlerRegistration>, AdapterError>`
where `AdapterError` is a crate-level enum. The *presence* of the error type 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. the exact variants are the two-way-door remainder, tracked as OQ-26.
### DC-2 — from_call re-import on reconnection: manual free function ### 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, mid-connection re-discovery is a genuine feature addition — non-breaking,
additive — if a deployment needs manual re-discovery without additive — if a deployment needs manual re-discovery without
drop-and-reconnect. Two-way door; recorded in 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 OQ-27. See [ADR-028](028-from-call-manual-free-function.md) for the full
rationale. rationale.
@@ -547,7 +547,7 @@ rationale.
collision = error**. A node importing from two remotes that both expose the collision = error**. A node importing from two remotes that both expose the
same unprefixed op name should fail loudly. The operator adds prefixes when same unprefixed op name should fail loudly. The operator adds prefixes when
importing from multiple sources. Two-way door; recorded in 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. OQ-28.
### Operational spec ### 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 `from_call` flow, the trait signature, the adapter location map, the
no-env-vars invariant, and the exchange-of-operations pattern — is no-env-vars invariant, and the exchange-of-operations pattern — is
specified in 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 is the operational complement to this ADR; this ADR remains the architectural
authority. 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 alknet-call" one-way-door statement is corrected above to "the adapter
trait lives in `alknet-call`; implementations live with their transport." trait lives in `alknet-call`; implementations live with their transport."
See [ADR-027](027-from-jsonschema-as-http-adapter.md) and 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) ## 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, The §1 Decision framed `CallClient::connect(addr: SocketAddr,
credentials)` as the primary constructor and described it as "opens a credentials)` as the primary constructor and described it as "opens a
QUIC connection." The operational spec 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 framed `spawn_dispatch(connection)` as the "lower-level API" that
`connect()` uses after the QUIC dial. That framing welded the `connect()` uses after the QUIC dial. That framing welded the
client-side one-way-door API to QUIC — the same welding ADR-007 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 shared dial seam is `alknet-client`; `spawn_dispatch` is the
protocol-crate take-over that consumes the dial's `Connection`. 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. §"CallClient" for the reframed operational spec.
## Amendment (2026-10-13 — review 010 S-04: announced Sub/Pub forward-through, marked-spec refusal) ## Amendment (2026-10-13 — review 010 S-04: announced Sub/Pub forward-through, marked-spec refusal)
@@ -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, 2. **X.509 cert** — for domain-facing endpoints (`api.alk.dev`, relays,
ACME/Let's Encrypt). Fingerprint format: `SHA256:<hex of DER>`. Requires ACME/Let's Encrypt). Fingerprint format: `SHA256:<hex of DER>`. Requires
CA verification on the client side. The outgoing-only case (a client 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 3. **Bearer token** (auth_token) — for HTTP clients that can't do TLS
client-auth (browsers, curl), or as a secondary credential path. Carried 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) paths, and `ApiKeyEntry` is for tokens that ARE the identity)
- OQ-29: CallClient TLS Client-Auth (resolved by this ADR's §6 fingerprint - OQ-29: CallClient TLS Client-Auth (resolved by this ADR's §6 fingerprint
normalization + the client-auth wiring decision recorded in OQ-29) 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 - OQ-21: X.509 outgoing-only case (the three auth types and how X.509
server identity fits — see OQ-37 in open-questions.md) 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` - `docs/research/alknet-storage-strategy/findings.md` §4 (the `PeerEntry`
model and resolution path) model and resolution path)
- `docs/architecture/crates/core/auth.md` (the spec amended by this ADR) - `docs/architecture/crates/core/auth.md` (the spec amended by this ADR)
@@ -123,7 +123,7 @@ implementations are independent. The Amendment 1 protocol-foundation
exception remains for alknet-http/agent/napi (which use alknet-call's exception remains for alknet-http/agent/napi (which use alknet-call's
`OperationSpec`/`Handler`/`OperationAdapter` types — actual type reuse, `OperationSpec`/`Handler`/`OperationAdapter` types — actual type reuse,
not framing glue); it no longer covers alknet-tty. See 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 and the three options considered (duplicate / promote to core / use
alknet-call). alknet-call).
@@ -109,7 +109,7 @@ per `ChannelManager` (`buffer_cap` field). This prevents memory exhaustion
without the complexity of SSH's sliding-window protocol. without the complexity of SSH's sliding-window protocol.
Full channel-level windowing (SSH-style sliding-window per channel) is a 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- (deferred(scope)). It is blocked on a real deployment observing head-of-
line blocking where the bounded-buffer mitigation is insufficient. The line blocking where the bounded-buffer mitigation is insufficient. The
bounded-buffer decision is made; the extension is not. bounded-buffer decision is made; the extension is not.
@@ -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 OQ-55 deferred `AlknetClient::dial()` — the transport-polymorphic client
dial seam — blocked on "a second transport's real dial existing." The dial seam — blocked on "a second transport's real dial existing." The
reasoning (recorded in the OQ-55 file and in 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 to `AlknetClient`") was sound at the time: extracting a QUIC-shaped
connector and naming it `AlknetClient` would bake QUIC in as *the* connector and naming it `AlknetClient` would bake QUIC in as *the*
establishment shape — the same welding ADR-007 unwound on the server 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 - OQ-55 (resolved by this ADR) — `AlknetClient` / client establishment
extraction 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 as multi-transport accept-loop runner; the server-side shape this
ADR mirrors on the client side 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 (native has QUIC + TCP+TLS + iroh); entry-point vs. endpoint ALPN
distinction (§2) 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 `TlsClientConfig` not blocked on the dial seam; breaks the circular
hedge; the TLS config is a prerequisite for the dial 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 `TlsClientConfig` in `alknet-tls`; "iroh shares the key, not the
config" config"
- [ADR-007](007-connection-from-stream-generic-single-stream.md) — - [ADR-007](007-connection-from-stream-generic-single-stream.md) —
`Connection::from_stream` / `from_bidi`; the server-side `Connection::from_stream` / `from_bidi`; the server-side
generalization whose client-side analogue this ADR completes 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) 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 provider on all paths
- [ADR-043](043-channelclient.md) — `ChannelClient::from_connection` - [ADR-043](043-channelclient.md) — `ChannelClient::from_connection`
(the take-over `AlknetClient` feeds) (the take-over `AlknetClient` feeds)
@@ -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 on top of it later, in channels, the same way the hub relay composes on
top of existing call ops. 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 ### Gap A from the research findings: `Pub` handler shape
The research findings' Gap A identified that the current 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) ## 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 The fan-out / broker mechanism (one producer, N consumers, topic
matching by `(op_name, params_hash)`) is **not** part of this ADR. It matching by `(op_name, params_hash)`) is **not** part of this ADR. It
is deferred to the channels session for these reasons: is deferred to the channels session for these reasons:
@@ -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 EOF chunk → spoke handler exits → spoke's own wrapper decrements its
ledger. Close propagates through the EOF cascade with correct ledger ledger. Close propagates through the EOF cascade with correct ledger
accounting on both legs; the hub never sees a `channel/close` to accounting on both legs; the hub never sees a `channel/close` to
translate. Control stays per-leg (`channel/control` is not implemented translate. Control stays per-leg (ADR-052): the control surface is
upstream, OQ-39 — a hub wanting cross-leg control translation composes establisher-contributed (`Establishment::with_control`) on each leg's
it downstream). This supersedes the ADR-042 §mapping bullet'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 close-translation sentence as an implementation matter: the mapping
entry the sentence's removal depended on does not exist. entry the sentence's removal depended on does not exist.
+28 -12
View File
@@ -41,7 +41,11 @@ status, priority, and (when resolved) a resolution citing the ADR.
| OQ | Title | Status | Priority | Resolution | | 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 ### 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` the client method. The `Subscription` variant is renamed to `Sub`
(wire string `"sub"`). (wire string `"sub"`).
**Fan-out deferred.** The broker (topic registry, `Pub`↔`Sub` matching, **Placement decided: the broker is not an alkcall deliverable.** The
N-consumer fan-out) is deferred to the channels session. The call fan-out/broker mechanism (topic registry, `Pub`↔`Sub` matching,
protocol is point-to-point (one initiator, one responder, a stream N-consumer fan-out) sits above the call protocol: the call protocol is
between them); a topic registry that outlives individual calls is a point-to-point (one initiator, one responder, a stream between them);
different lifecycle and a different concern. The broker's first a topic registry that outlives individual calls is a different
consumer is the channels `channel/resources/subscribe` operation lifecycle and a different concern. The broker is a hub composition on
(ADR-037) and the hub-as-broker pattern (ADR-042). The `Pub` primitive top of the `Pub`/`Sub` primitives — ADR-047 §"What this ADR decides"
is the load-bearing piece the broker composes on. (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 ## Channels
| OQ | Title | Status | Priority | Resolution | | 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-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-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-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-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-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-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-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-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-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-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 ## Core Types
+42 -41
View File
@@ -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 path) and the `OwnershipStore` trait (write side, async — called by
handlers that manage resource lifecycles) are defined in `alknet-core` per handlers that manage resource lifecycles) are defined in `alknet-core` per
ADR-011's storage decision (fourth instance of the repo/adapter pattern, 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. for the trait shapes and the in-memory default adapter.
**The ownership provider is carried on `OperationContext`** (or threaded **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` The `invoke_peer` / `peer_contains` methods (ADR-024 §2) take a `PeerRef`
selector and a `PeerId`. These types are defined alongside the 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) `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 ```rust
pub enum PeerRef { pub enum PeerRef {
@@ -727,7 +727,7 @@ impl OperationEnv for PeerCompositeEnv {
} }
// Peer-keyed overlay: iterate peers in insertion order, dispatch to // Peer-keyed overlay: iterate peers in insertion order, dispatch to
// the first peer whose sub-overlay contains the op. This is the // 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). // order first-match).
for peer_id in &self.connection_order { for peer_id in &self.connection_order {
if let Some(conn_env) = self.connections.get(peer_id) { 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 | | 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 | | 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/012-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | | Call protocol stream model | [ADR-015](decisions/015-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) | | 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](decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time | | 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/014-secret-material-flow-and-capability-injection.md) | Capabilities carry outbound credentials; call protocol carries no secret material | | 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/015-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; composition authority + scoped env | | 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/022-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle | | 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/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 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/023-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity for `from_openapi`/`to_openapi` | | 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/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) | | 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/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") | | `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/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) | | 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/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 | | 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/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 | | 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/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 | | 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 deferred to channels | | 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 ## Open Questions
See [open-questions.md](open-questions.md) for full details. 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-02** (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-03** (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-09** (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-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-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 `remote_safe`/`trusted_peer` are retired; peer authorization is
`AccessControl::check(peer_identity)`, the existing mechanism. See `AccessControl::check(peer_identity)`, the existing mechanism. See
[client-and-adapters.md](client-and-adapters.md) and ADR-024 §3. [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`, variants: `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`,
`SamePeerCollision` (replaces flat `Conflict`). `#[non_exhaustive]`. See `SamePeerCollision` (replaces flat `Conflict`). `#[non_exhaustive]`. See
[client-and-adapters.md](client-and-adapters.md). [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 manual free function; the assembly layer calls it after the dial (in
`AlknetClient`). A `CallConnection::refresh()` method is a genuine `AlknetClient`). A `CallConnection::refresh()` method is a genuine
feature addition — non-breaking, additive. See feature addition — non-breaking, additive. See
[ADR-028](decisions/069-from-call-manual-free-function.md). [ADR-028](decisions/028-from-call-manual-free-function.md).
- **OQ-28** (resolved): `from_call` namespace collision — same-peer - **OQ-13** (resolved): `from_call` namespace collision — same-peer
collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays). collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays).
`namespace_prefix` is optional local-naming sugar. See `namespace_prefix` is optional local-naming sugar. See
[client-and-adapters.md](client-and-adapters.md). [client-and-adapters.md](client-and-adapters.md).
- **OQ-29..37** are tracked in [client-and-adapters.md](client-and-adapters.md) - The client-surface questions (the `CallClient` / adapter surface and
(they concern the `CallClient` / adapter surface and peer-graph routing, peer-graph routing) are tracked in
not the registry layering this document covers). In brief: OQ-29 [client-and-adapters.md](client-and-adapters.md), not the registry
(CallClient TLS client-auth) resolved; OQ-30 (`PeerRef::Any` routing layering this document covers; in brief: OQ-14
policy) resolved; OQ-31 (`services/list-peers` re-export semantics) (`CallClient` TLS client-auth) resolved; OQ-15 (`PeerRef::Any` routing
resolved; OQ-32 (multi-hop federation) open (feature extension); policy) resolved; OQ-16 (`services/list-peers` re-export semantics)
OQ-33 (PeerId source) resolved by ADR-025; OQ-34 (persistent peer resolved; OQ-17 (multi-hop federation) deferred(scope) (feature
registry) resolved by ADR-025+033; OQ-35 (API key asymmetry) dissolved; extension); OQ-18 (`PeerId` source) resolved by ADR-025; OQ-19
OQ-36 (concrete persistence adapter shapes) resolved by ADR-035; (persistent peer registry) resolved by ADR-025; OQ-20 (API key
OQ-37 (X.509 outgoing-only) resolved by ADR-034. asymmetry) dissolved; OQ-21 (X.509 outgoing-only) resolved — the
- **OQ-42** (resolved by ADR-011): Dynamic resource ownership for 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 runtime-spawned resources — `AccessControl::check` consults an
`OwnershipProvider`; `OperationSpec` gains `resource_id_path`; proxy-only `OwnershipProvider`; `OperationSpec` gains `resource_id_path`; proxy-only
access pattern; four edge specifics pinned (`list`, teardown, fleet, access pattern; four edge specifics pinned (`list`, teardown, fleet,
composition). See [auth.md](../core/auth.md) §"Ownership Provider and composition). See ADR-011 for the trait shapes.
Store" for the trait shapes.
## References ## References