diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9a5f288..3d1f22c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-13 +last_updated: 2026-10-13 (composition re-key, ADR-054, alkcall reframing of ported docs) --- # alkcall @@ -9,12 +9,13 @@ The call + channels RPC crate. Structured JSON RPC (operations, streaming subscriptions, service discovery) and N-channel multiplexing over one transport stream (channel 0 pre-negotiated as `alk/call`). -This crate unifies `alknet-call` and `alknet-channels` from the alknet -mono-repo, plus the vendored core types formerly in `alknet-core`. The -source architecture docs were ported from -`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as alkcall -ADRs (ADR-001..053). The ALPN strings (`alk/call`, `alk/channels`) -are wire-stable and unchanged — see ADR-004. +This crate is the extraction and unification of the former `alknet-call` +and `alknet-channels` crates plus the vendored core types formerly in +`alknet-core` (see ADR-031). It is a pure protocol crate: no networking, +no transport dependencies — dial, TLS config, and composition live in +downstream crates. The ALPN strings (`alk/call`, `alk/channels`) are +wire-stable — see ADR-004. (Historical origin and cross-repo citation +conventions: see the provenance note at the end of this document.) ## Documents @@ -69,7 +70,7 @@ are wire-stable and unchanged — see ADR-004. | [024](decisions/024-peer-graph-routing-model.md) | Peer-Graph Routing Model | PeerCompositeEnv, PeerRef, AccessControl peer auth | | [025](decisions/025-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | PeerId = Identity.id (stable) | | [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | Metadata only, never used by ACL | -| [027](decisions/027-from-jsonschema-as-http-adapter.md) | from_jsonschema as HTTP Adapter | FromJsonSchema provenance stays; impl in alknet-http | +| [027](decisions/027-from-jsonschema-as-http-adapter.md) | from_jsonschema as HTTP Adapter | FromJsonSchema provenance stays; impl moved to alkhttp (ex-`alknet-http`) | | [028](decisions/028-from-call-manual-free-function.md) | from_call Is a Manual Free Function | Assembly layer calls it after dial | | [029](decisions/029-aggregated-peer-env-wiring.md) | Aggregated Peer-Environment Wiring | Dispatcher hub wiring | | [030](decisions/030-peer-composite-env-peer-operations.md) | PeerCompositeEnv::peer_operations | OperationEnv::peer_operations override | @@ -82,7 +83,7 @@ are wire-stable and unchanged — see ADR-004. | [032](decisions/032-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Reversal cost classification | | [033](decisions/033-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Rust canonical, TS reference | -### Channels — ADR-034..045 +### Channels, pub/sub, and later amendments — ADR-034..054 | ADR | Title | Relevance | |-----|-------|-----------| @@ -106,6 +107,7 @@ are wire-stable and unchanged — see ADR-004. | [051](decisions/051-channel-relay-and-hub-leg.md) | In-Tree Channel Relay (`ChannelRelay`) and the Hub-Leg Assembly | ADR-042 amended — the relay implementation is an alkcall export; translate hop = ADR-049 establisher (`register_relay_openable`), byte-forward hop = `pump_bidi`; implicit per-channel id mapping; two-phase registration (discover/stash → fork-register); hub-leg install template; ACL layering note | | [052](decisions/052-channel-control-surface.md) | `channel/control` Control-Handle Surface | Establisher-contributed `ChannelControl` (`Establishment::with_control`), wrapper-installed/teardown-removed in the manager; `channel:no_control_handle` capability answer; resolves OQ-39 | | [053](decisions/053-resource-enumerators.md) | Live Resource Enumerators | `ResourceEnumerators` (peer-shared registry); snapshot-then-changes enumerator contract; scan-once aggregation; empty seed; resolves OQ-40 | +| [054](decisions/054-outgoing-only-x509-three-remote-roles.md) | Outgoing-Only X.509 and the Three Remote Roles (Re-Owned) | Verifier-selection by `PeerEntry` presence; three remote roles; `RemoteIdentity` `None` semantics — re-owned in-crate (provenance: alknet ADR-034); resolves OQ-21's cross-repo authority gap | ## Relevant Open Questions @@ -128,6 +130,11 @@ questions affecting this crate: amendment 2026-10-13, ADR-047 amendment 4 §4.8); the sanctioned compositions are `ChannelRelay::register_relay_openable` (ADR-051) and `ChannelClient`. +- **OQ-43** (deferred(scope)): Composition-layer ownership and shape + after the alknet replacement — the named receiver for the + downstream-side deferrals (OQ-33 endpoint layer, OQ-41 QUIC-native + substrate, OQ-42 hub broker, OQ-19 persistence adapters). Not + blocking any alkcall API surface. ## Key Design Principles @@ -192,8 +199,9 @@ Within a single connection, direction is independent of role: ``` ┌──────────────────────────────────────────────┐ -│ alknet / alknode │ -│ (networking + composition) │ +│ Composition layer │ +│ (today: the alknet glue — being replaced; │ +│ future owner and shape → OQ-43) │ │ │ │ • QUIC / TCP+TLS dial and accept │ │ • Hub: ChannelsAdapter + Dispatcher + │ @@ -201,7 +209,7 @@ Within a single connection, direction is independent of role: │ • Spoke: ChannelClient + CallConnection + │ │ from_call │ │ • Wires protocol crates' producers into │ -│ registries, consumers into clients │ +│ registries, consumers into clients │ └──────────────────┬───────────────────────────┘ │ depends on ┌──────────────┼──────────────┐ @@ -245,10 +253,12 @@ alkcall. They provide two halves: (e.g. `trader_client.status().await` instead of `call_connection.call("trader/status", ...).await`). -**alk/alknode** depends on alkcall + whichever protocol crates are -needed. It's the composition layer — the only place that knows about -network topology, peer routing, and which protocol crates are wired in. -Protocol crates never import a QUIC or TLS dependency. +**The composition layer** depends on alkcall + whichever protocol +crates are needed. It's the only place that knows about network +topology, peer routing, and which protocol crates are wired in. +Protocol crates never import a QUIC or TLS dependency. Today this +layer is the alknet glue; its post-replacement owner and shape is +tracked as OQ-43. ### Pattern for protocol crates @@ -285,31 +295,39 @@ A protocol crate that uses channels (e.g. alktty) follows this pattern: The protocol crate doesn't know which path it's on. It takes a `BiStream` (or `AsyncRead + AsyncWrite`) and drives the session. The two adapters are thin and live either in the protocol crate (behind -feature flags) or in the downstream alknet crate. +feature flags) or in the composition layer. ## References -- `@alkdev/alknet: docs/architecture/` — the source architecture docs - these were ported from (renumbered from alknet ADR-001..094 to alkcall - ADR-001..048; ADR-049 and later were authored in this crate) - `@alkdev/alktype` — the binary struct engine; compiles BAST documents (e.g. `chunk-header.bast.json`) into readers/writers/validators +- `@alkdev/alkhttp`, `@alkdev/alktls`, `@alkdev/alktty`, + `@alkdev/alktunnels`, `@alkdev/alktrader` — sibling crates; the + live homes of the surfaces older docs call `alknet-http`, + `alknet-tls`, `alknet-tty`, `alknet-tunnels`, `alknet-trader` - `@alkdev/pubsub` — the TypeScript EventEnvelope prior art the call wire format was derived from -> **Note**: The source ADRs and spec docs were ported from the parent -> `@alkdev/alknet` workspace where this crate originated. They are -> preserved here as the authoritative spec for alkcall; the alknet -> mono-repo will consume alkcall's versions when it is reworked. +> **Provenance and cross-repo citations.** The source ADRs and spec +> docs were ported from the parent `@alkdev/alknet` workspace where +> this crate originated, and renumbered to alkcall ADR-001..048 +> (ADR-049 and later were authored in this crate). They are the +> authoritative spec here; the alknet mono-repo is being replaced by +> thin glue over alkcall and its siblings, so alknet-specific citations +> are expected to decay into footnotes over time, and anything that +> genuinely constrains alkcall's surface gets re-owned by an in-crate +> ADR (e.g. ADR-054 re-owns alknet ADR-034's verifier-selection rule). > -> **Cross-references to non-ported ADRs**: alkcall's own ADRs are -> `decisions/001..053` in this directory. Some ported docs also cite -> sibling-crate ADRs that were **not** ported (e.g., alknet ADR-052 for -> TTY's wire format, alknet ADR-034 for outgoing-only X.509, alknet -> ADR-082 for alknet-tls, alknet ADR-086 for endpoint types). The alknet -> number space collides with alkcall's (alknet ADR-052 has nothing to do -> with alkcall's `channel/control` ADR-052), so unambiguous references -> carry the `alknet` prefix (e.g., "alknet ADR-052") and link to the -> alknet repo: `/workspace/@alkdev/alknet/docs/architecture/decisions/` +> **Citation conventions**: alkcall's own ADRs are `decisions/001..054` +> in this directory. Some ported docs also cite source-repo ADRs that +> were **not** ported (e.g. alknet ADR-052 for TTY's wire format). The +> alknet number space collides with alkcall's (alknet ADR-052 has +> nothing to do with alkcall's `channel/control` ADR-052), so unambiguous +> references carry the `alknet` prefix (e.g. "alknet ADR-052") and link +> to the alknet repo: `/workspace/@alkdev/alknet/docs/architecture/` > (a few legacy cites without the prefix still mean the alknet ADR — -> check the link target). \ No newline at end of file +> check the link target). Dead relative paths like +> `docs/research/alknet-channels/...` live in the alknet repo, not this +> one. Similarly, `alknet OQ-NN` citations refer to the alknet repo's +> question tracker — this crate's OQ tracker runs its own numbering +> (OQ-01..43, see [open-questions.md](open-questions.md)). \ No newline at end of file diff --git a/docs/architecture/call-README.md b/docs/architecture/call-README.md index 5304e6c..bef302b 100644 --- a/docs/architecture/call-README.md +++ b/docs/architecture/call-README.md @@ -1,12 +1,12 @@ --- status: draft -last_updated: 2026-07-17 -review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/014/015/016/017/022/023/024), security, and pattern-consistency checks all conformant; 159 unit/integration tests green; `cargo build`, `cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo test` clean. Call-completion gap (ADR-022 client/adapter surface) addressed 2026-06-26; ADR-024 migration landed. Transport generalization sweep (ADR-014 supersedes ADR-013; ADR-007 `from_stream`) synced 2026-07-09. Crate-extraction sweep (phases 0–5) landed 2026-07-17: `ConnectionCredentials`/`RemoteIdentity` in `alknet-core` (ADR-012); TLS helpers in `alknet-tls` (ADR-045 §5); dial in `alknet-client` (ADR-045); `alknet-call` is a pure protocol crate with no TLS/transport deps. +last_updated: 2026-10-13 (alkcall reframing — historical alknet framing moved to the provenance note) +review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/014/015/016/017/022/023/024), security, and pattern-consistency checks all conformant; 159 unit/integration tests green; `cargo build`, `cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo test` clean. Call-completion gap (ADR-022 client/adapter surface) addressed 2026-06-26; ADR-024 migration landed. Transport generalization sweep (ADR-014 supersedes ADR-013; ADR-007 `from_stream`) synced 2026-07-09. Crate-extraction sweep (phases 0–5) landed 2026-07-17: `ConnectionCredentials`/`RemoteIdentity` vendored into alkcall's core types (ADR-012); dial extracted as the composition layer's helper (ADR-045); alkcall is a pure protocol crate with no TLS/transport deps. --- -# alknet-call +# alkcall — Call Protocol -Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements `ProtocolHandler` on ALPN `alk/call`. Runs over QUIC (quinn/iroh) and, via `Connection::from_stream` (ADR-007), over any `AsyncRead + AsyncWrite` transport. A pure protocol crate — no TLS or transport deps (the dial is in `alknet-client`, the TLS config is in `alknet-tls`). +Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements `ProtocolHandler` on ALPN `alk/call`. Runs over QUIC (quinn/iroh) and, via `Connection::from_stream` (ADR-007), over any `AsyncRead + AsyncWrite` transport. A pure protocol crate — no TLS or transport deps. The dial and TLS config belong to the composition layer (the dial helper is `AlknetClient`; the TLS config types live in the sibling host crate `alktls` — see ADR-045 and OQ-43). ## Documents @@ -14,7 +14,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi |----------|--------|-------------| | [call-protocol.md](call-protocol.md) | draft | CallAdapter, hand-rolled EventEnvelope framing (no irpc — ADR-014), stream model, PendingRequestMap, bidirectional calls | | [operation-registry.md](operation-registry.md) | draft | OperationSpec, Handler, OperationRegistry, AccessControl, service discovery, hand-rolled framing (no irpc — ADR-014) | -| [client-and-adapters.md](client-and-adapters.md) | draft | CallClient (transport-agnostic `spawn_dispatch` primary; dial lives in `AlknetClient` per ADR-045), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (`from_jsonschema` in alknet-http per ADR-027) | +| [client-and-adapters.md](client-and-adapters.md) | draft | CallClient (transport-agnostic `spawn_dispatch` primary; dial lives in the composition layer's dial helper per ADR-045), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (`from_jsonschema` in alkhttp per ADR-027) | ## Applicable ADRs @@ -22,22 +22,20 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi |-----|-------|-----------| | [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN `alk/call` | | [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | CallAdapter implements ProtocolHandler | -| [031](decisions/031-crate-decomposition.md) | Crate Decomposition | alknet-call depends on alknet-core (no irpc — ADR-014) | +| [031](decisions/031-crate-decomposition.md) | Crate Decomposition | alkcall is self-contained: call + channels subsystems + vendored core types (no irpc — ADR-014) | | [033](decisions/033-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Adapter traits defined in Rust; TS is reference/browser adaptation | | [003](decisions/003-auth-as-shared-core.md) | Auth as Shared Core | AuthContext passed to call handlers | | [013](decisions/013-irpc-as-call-protocol-foundation.md) | ~~irpc as Call Protocol Foundation~~ | ~~Accepted~~ → **Superseded** by [ADR-014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) (irpc was never integrated; framing is hand-rolled) | -| [014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) | Hand-Rolled EventEnvelope Framing | Wire format, registry, dispatch are hand-rolled in alknet-call; supersedes ADR-013 | +| [014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) | Hand-Rolled EventEnvelope Framing | Wire format, registry, dispatch are hand-rolled in alkcall; supersedes ADR-013 | | [007](decisions/007-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | Generic single-stream connections; unblocks TCP+TLS/SSH/WT/wasm dispatch | | [004](decisions/004-alpn-convention-and-connection-model.md) | ALPN String Convention | `alk/call` ALPN, one ALPN per connection | | [005](decisions/005-bistream-type-definition.md) | BiStream Type Definition | CallAdapter receives Connection, not BiStream | -| [008[008](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) | Vault Integration Point | Vault accessed at assembly layer, not on the wire | -| [010[010](/workspace/@alkdev/alknet/docs/architecture/decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Static handler registration | | [015](decisions/015-call-protocol-stream-model.md) | Call Protocol Stream Model | Bidirectional streams, EventEnvelope, ID-based correlation | | [010](decisions/010-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Call protocol carries no secret material; capabilities injected at assembly layer | | [017](decisions/017-privilege-model-and-authority-context.md) | Privilege Model and Authority Context | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env | | [020](decisions/020-abort-cascade-for-nested-calls.md) | Abort Cascade for Nested Calls | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in | | [022](decisions/022-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction | -| [027](decisions/027-from-jsonschema-as-http-adapter.md) | `from_jsonschema` as HTTP-Backed Single-Endpoint Adapter in alknet-http | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf | +| [027](decisions/027-from-jsonschema-as-http-adapter.md) | `from_jsonschema` as HTTP-Backed Single-Endpoint Adapter | Moved `from_jsonschema` out of the call protocol (broken schema-only placeholder) to the HTTP host crate as a real reqwest-backed single-endpoint adapter — today `alkhttp` (ex-`alknet-http`); `FromJsonSchema` provenance stays in alkcall as a leaf | | [018](decisions/018-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Registration bundle carries provenance, composition authority, scoped env, capabilities | | [016](decisions/016-operation-error-schemas.md) | Operation Error Schemas | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity | | [019](decisions/019-operation-registry-layering.md) | Operation Registry Layering | Curated (static) + session/connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) | @@ -45,9 +43,10 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi | [024](decisions/024-peer-graph-routing-model.md) | Peer-Graph Routing Model | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` | | [025](decisions/025-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | `PeerId` source = `Identity.id` = `PeerEntry.peer_id` (stable); supersedes ADR-024's UUID source | | [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | `forwarded_for` on `OperationContext` and `call.requested`; metadata only, never used by `AccessControl::check` | -| [033[033](/workspace/@alkdev/alknet/docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Storage Boundary and Repo/Adapter Pattern | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | -| [045](decisions/045-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | The dial is in `alknet-client`; `CallClient` is `spawn_dispatch` only; `alknet-call` is a pure protocol crate with no TLS/transport deps | -| [012](decisions/012-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — Decouple Dial from Call Protocol | `ConnectionCredentials`/`RemoteIdentity` in `alknet-core` (not `alknet-call`); `auth_token` is a per-request payload field | +| [045](decisions/045-alknetclient-native-dial-seam.md) | Dial Seam — Dial Out of the Protocol Crate | The dial lives in the composition layer's dial helper (`AlknetClient`, today in the alknet glue — future owner OQ-43); `CallClient` is `spawn_dispatch` only; alkcall is a pure protocol crate with no TLS/transport deps | +| [012](decisions/012-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — Decouple Dial from Call Protocol | `ConnectionCredentials`/`RemoteIdentity` live with the vendored core types in alkcall (originally `alknet-core`); `auth_token` is a per-request payload field | + +Provenance (source-repo ADRs, not ported): [alknet ADR-008 — Vault Integration Point](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) (superseded in-crate by ADR-010) and [alknet ADR-010 — ALPN Router and Endpoint](/workspace/@alkdev/alknet/docs/architecture/decisions/010-alpn-router-and-endpoint.md) (superseded in-crate by ADR-001's ALPN dispatch and the decision that alkcall has no endpoint layer) are historical origins only. [alknet ADR-033 — Storage Boundary and Repo/Adapter Pattern](/workspace/@alkdev/alknet/docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md) is the repo/adapter pattern behind the `OwnershipProvider` persistence story (OQ-19); its persistence adapters belong to the composition layer (OQ-43). ## Relevant Open Questions @@ -60,16 +59,16 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi | OQ-09 | Safe vault operations for call protocol exposure | resolved (ADR-010) | None exposed for now | | OQ-10 | ~~Remote-safe marking shape~~ | **dissolved** (ADR-024) | `remote_safe`/`trusted_peer` retired; peer authorization is `AccessControl::check(peer_identity)` | | OQ-11 | OperationAdapter error type (AdapterError variants) | **resolved** | `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`; `#[non_exhaustive]` | -| OQ-12 | from_call re-import trigger | **resolved** | `from_call` is a manual free function; the assembly layer calls it after the dial (in `AlknetClient`). `refresh()` is a genuine feature addition. See ADR-028. | +| OQ-12 | from_call re-import trigger | **resolved** | `from_call` is a manual free function; the assembly layer calls it after the dial (in the composition layer's dial helper, `AlknetClient` — today the alknet glue; OQ-43). `refresh()` is a genuine feature addition. See ADR-028. | | OQ-13 | from_call namespace collision | **resolved** | Same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays) | | OQ-14 | CallClient TLS client-auth | **resolved** | Wire quinn client-auth; key-type-aware server cert verification; fingerprint normalization | | OQ-15 | `PeerRef::Any` routing policy | **resolved** | Insertion-order first-match; richer routing is a feature extension | | OQ-16 | `services/list-peers` re-export semantics | **resolved** | Opt-in `services/list-peers`; `services/list` is "own ops only" | | OQ-17 | Multi-hop federation | deferred(scope) (feature extension) | One-hop model is the commitment; multi-hop is a feature extension, not a deferral | | OQ-18 | PeerId — crypto identity vs stable logical id | **resolved** (ADR-025) | `PeerId = Identity.id = PeerEntry.peer_id` (stable across key rotation) | -| OQ-19 | Persistent peer registry | **resolved** (ADR-025; persistence adapters are alknet-side) | Core trait + in-memory default; persistence adapters are separate crates | +| OQ-19 | Persistent peer registry | **resolved** (ADR-025; persistence adapters are composition-layer work — OQ-43) | Core trait + in-memory default; persistence adapters are separate crates | | OQ-20 | ~~API key asymmetry~~ | **dissolved** | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity | -| OQ-21 | X.509 outgoing-only case | **resolved** (alknet ADR-034, not ported) | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; verifier by `PeerEntry` presence | +| OQ-21 | X.509 outgoing-only case | **resolved** (ADR-054; provenance: alknet ADR-034, not ported) | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; verifier by `PeerEntry` presence — re-owned in-crate by ADR-054 | ## Key Design Principles @@ -78,12 +77,12 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi 3. **Stream-agnostic correlation**: PendingRequestMap correlates by request ID, not by stream. The protocol works with any stream arrangement. 4. **Operation registry is layered**: The curated layer (`Local` provenance) is static — registered at startup by the CLI binary, immutable for the process lifetime. Session (`Session`) and imported (`FromCall` etc.) ops are dynamic overlays at their respective scopes (per-session, per-connection). The registry supports JSON Schema discovery. See ADR-019. 5. **Hand-rolled dispatch (no irpc)**: Operations dispatch through the hand-rolled `OperationRegistry` (ADR-014). The call protocol is the external interface; internal handler dispatch uses `Handler`/`StreamingHandler` trait objects (ADR-021), not an irpc service. -6. **Local dispatch only**: The operation registry dispatches to local handlers. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer, not a modification to alknet-call's path format. +6. **Local dispatch only**: The operation registry dispatches to local handlers. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer, not a modification to alkcall's path format. 7. **No secret material on the wire**: The call protocol carries no private keys, API keys, mnemonics, or decrypted credentials. Handlers receive outbound credentials through `OperationContext.capabilities`, injected at the assembly layer. See ADR-010. 8. **Abort cascades to descendants**: `call.aborted` for a parent request cascades to all non-terminal descendants. Default `abort-dependents`; `continue-running` opt-in. See ADR-020. 9. **Internal calls switch authority context, not skip ACL**: The `internal` flag marks composition-originated calls. ACL runs against the handler's composition authority, not the caller's and not as a blanket skip. Operations have External/Internal visibility. Scoped composition env bounds reachability. See ADR-017, ADR-018. 10. **Provenance determines composition capability**: Only `Local` and `Session` ops can compose. Leaves (`FromOpenAPI`, `FromMCP`, `FromCall`, `FromJsonSchema`) are forwarding stubs — they don't get composition authority or a scoped env. The assembly layer is the sole grantor of composition authority. See ADR-018. (`FromJsonSchema` is now a real HTTP-forwarding leaf per ADR-027, not a schema-only placeholder.) 11. **Connection direction is independent of call direction**: Who opens the connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The `CallAdapter` accepts connections; the `CallClient` takes them over (`spawn_dispatch` primary; dial in `AlknetClient` per ADR-045); both produce the same `CallConnection` and dispatch through the same loop. See ADR-022, [client-and-adapters.md](client-and-adapters.md). 12. **Peer authorization via `AccessControl`**: A remote peer's call is authorized by `AccessControl::check(peer_identity)` against the op's `AccessControl` — the same mechanism that gates every other call. No `remote_safe` flag, no `trusted_peer` bypass. An op with `AccessControl::default()` is callable by any peer; an op with `required_scopes` is callable only by peers whose `Identity.scopes` satisfy them; an op with `Visibility::Internal` is never callable from the wire. See ADR-024. -13. **Adapter trait lives with the types; implementations live with their transport**: `OperationAdapter` is in `alknet-call`; `from_call` is in `alknet-call` (QUIC); `from_jsonschema`/`from_openapi`/`from_mcp`/`to_openapi`/`to_mcp` are in `alknet-http` (reqwest / axum). `alknet-call` stays lean — no HTTP client, no HTTP server. (`from_jsonschema` was originally in `alknet-call` as a schema-only placeholder; ADR-027 moved it to `alknet-http` as a real HTTP-backed adapter.) See [client-and-adapters.md](client-and-adapters.md). +13. **Adapter trait lives with the types; implementations live with their transport**: `OperationAdapter` is in alkcall; `from_call` is in alkcall (QUIC); `from_jsonschema`/`from_openapi`/`from_mcp`/`to_openapi`/`to_mcp` are in the HTTP host crate, `alkhttp` (reqwest / axum). alkcall stays lean — no HTTP client, no HTTP server. (`from_jsonschema` was originally in the call protocol as a schema-only placeholder; ADR-027 moved it to `alkhttp` — then named `alknet-http` — as a real HTTP-backed adapter.) See [client-and-adapters.md](client-and-adapters.md). 14. **No handler reads outbound credentials from any source other than `OperationContext.capabilities`** (no-env-vars invariant): the credential injection path is vault → assembly layer → `Capabilities` → `HandlerRegistration.capabilities` → `OperationContext.capabilities` → handler. Downstream consumers' `std::env::var` reads are unreachable because the assembly layer never calls `Default::default()`. See ADR-010, [client-and-adapters.md](client-and-adapters.md). \ No newline at end of file diff --git a/docs/architecture/call-protocol.md b/docs/architecture/call-protocol.md index 1e77589..e882512 100644 --- a/docs/architecture/call-protocol.md +++ b/docs/architecture/call-protocol.md @@ -1,11 +1,11 @@ --- status: draft -last_updated: 2026-07-09 +last_updated: 2026-10-13 (alkcall reframing) --- # Call Protocol -The wire protocol, stream model, framing, and adapter that alknet-call implements on ALPN `alk/call`. +The wire protocol, stream model, framing, and adapter that alkcall implements on ALPN `alk/call`. ## What @@ -15,7 +15,7 @@ The `CallAdapter` implements `ProtocolHandler` for ALPN `alk/call`. It receives ## Why -The call protocol is the primary programmatic interface to an alknet node. While SSH provides interactive shell access and HTTP provides REST APIs, the call protocol provides structured, discoverable RPC — the same interface that NAPI clients, MCP tools, and other automation consumers use. +The call protocol is the primary programmatic interface to an alk node. While SSH provides interactive shell access and HTTP provides REST APIs, the call protocol provides structured, discoverable RPC — the same interface that NAPI clients, MCP tools, and other automation consumers use. The protocol must be: - **Cross-language**: JSON wire format consumable from TypeScript, Python, any language @@ -40,7 +40,7 @@ pub struct CallAdapter { registry: Arc, identity_provider: Arc, /// Layer 1 — optional session-overlay source (agent crate supplies this; - /// None for non-agent deployments). See ADR-019, OQ-19. + /// None for non-agent deployments). See ADR-019, OQ-04. session_source: Option>, /// Default timeout for wire calls (30s). Composed calls inherit the /// parent's remaining deadline via `OperationContext.deadline`. @@ -57,8 +57,9 @@ impl CallAdapter { default_timeout: Duration::from_secs(30) } } - /// Agent deployment: supply a session-overlay source. The agent crate - /// implements `SessionOverlaySource`; alknet-call defines the trait. + /// Agent deployment: supply a session-overlay source. A downstream + /// consumer (e.g. the agent crate) implements `SessionOverlaySource`; + /// alkcall defines the trait. pub fn with_session_source(mut self, source: Arc) -> Self { self.session_source = Some(source); self @@ -71,10 +72,11 @@ impl CallAdapter { } } -/// Session overlay integration point (ADR-019). Defined in alknet-call -/// because `CallAdapter` must name the type — alknet-call cannot depend on -/// alknet-agent (agent depends on call, not reverse). The agent crate -/// implements this trait; alknet-call defines it. This is the same pattern +/// Session overlay integration point (ADR-019). Defined in alkcall +/// because `CallAdapter` must name the type — alkcall cannot depend on +/// its downstream consumers (an agent depends on call, not reverse). The +/// consumer (e.g. the agent crate) implements this trait; alkcall +/// defines it. This is the same pattern /// as `IdentityProvider` (ADR-003: core defines the trait, handlers impl it). /// /// The session overlay is an `OperationEnv` impl that wraps the curated base @@ -234,7 +236,7 @@ The `Value` type is `serde_json::Value`. The envelope is JSON because it must be Binary payloads (postcard, protobuf) are base64-encoded as a JSON string within the `payload` field. The convention is: if an operation's output schema specifies a binary field, the handler encodes it as a base64 string and the client decodes it. The `EventEnvelope` structure is not aware of this convention — it carries a `serde_json::Value` and does not interpret the payload. This is a handler-level concern, not a protocol-level concern. This is hand-rolled length-prefixed JSON framing (ADR-014), coincidentally -the same shape irpc uses. The Rust implementation in alknet-call is +the same shape irpc uses. The Rust implementation in alkcall is canonical — the `@alkdev/pubsub` TypeScript adapters serve as a reference and browser adaptation, not a parallel implementation (see ADR-033). @@ -352,7 +354,7 @@ The call protocol defines five top-level operations, expressed through event typ | **batch** | multiple `call.requested` (different IDs) → multiple `call.responded` | Multiple operations in one round | | **schema** | `call.requested` name `services/list` or `services/schema` → `call.responded` | Discover available operations | -Batch is not a separate event type — it's multiple `call.requested` events with different request IDs. The client sends them (on one or many streams) and correlates the responses by ID. See OQ-14. +Batch is not a separate event type — it's multiple `call.requested` events with different request IDs. The client sends them (on one or many streams) and correlates the responses by ID. See OQ-03. ### Bidirectional Calls @@ -573,8 +575,8 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is - The call protocol does not depend on any database. `PendingRequestMap` is in-memory. Durable session storage is a consumer concern. - Operation specs use JSON Schema. The envelope is always JSON. Binary payloads may be base64-encoded in the `payload` field. -- Batch is not a protocol primitive — multiple `call.requested` events with correlated IDs provide equivalent semantics. See OQ-14. -- The call protocol is transport-agnostic at the envelope level. The `EventEnvelope` framing can run over QUIC streams, WebSocket frames, or Worker `postMessage`. The `CallAdapter` is the `ProtocolHandler` implementation that receives a `Connection` (any transport — ADR-007) and dispatches `EventEnvelope` frames. **The `EventEnvelope` shape (`{ type, id, payload }`) was derived from the `@alkdev/pubsub` `EventEnvelope` (`/workspace/@alkdev/pubsub/src/types.ts`), which already has a working WebSocket client/server implementation (`event-target-websocket-client.ts` / `event-target-websocket-server.ts`) and a generalized "event target" abstraction. The call protocol refined the envelope with typed event names (`call.requested`, `call.responded`, etc.) and structured payloads; the delta is small and well-defined, making a browser (and Node) WebSocket client straightforward to derive from the pubsub prior art. See 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). +- Batch is not a protocol primitive — multiple `call.requested` events with correlated IDs provide equivalent semantics. See OQ-03. +- 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. Provenance: alknet ADR-044 and the alknet websocket spec (not ported) recorded the WebSocket-native session decision; the WebSocket carrier for the alk protocols is implemented in the sibling host crate `alkhttp` (its `websocket` feature), not in alkcall — alkcall stays transport- and host-blind. - `OperationEnv::invoke()` dispatches through the local registry. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer. See ADR-014 and OQ-13. - **The call protocol carries no secret material.** Secret material (private keys, API keys, mnemonics, decrypted credentials, raw tokens) must not appear in `call.requested` payloads, `call.responded` payloads, or `OperationContext.metadata`. The wire format carries `serde_json::Value` and cannot enforce this at the type level — the constraint is architectural, enforced by the operation registry and by convention. Operations that need to share public key material use a dedicated operation that returns only the public component. See ADR-010. - **Abort cascades to descendants.** `call.aborted` for a parent request cascades to all non-terminal descendants in the call tree. Default policy is `abort-dependents`; `continue-running` is an opt-in. See ADR-020. @@ -587,7 +589,7 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is | Call protocol stream model | [ADR-015](decisions/015-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | | ALPN per connection | [ADR-004](decisions/004-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN | | ProtocolHandler receives Connection | [ADR-005](decisions/005-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams | -| Vault integration point | [ADR-008](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time | +| Vault integration point | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Vault is a capability source, accessed at assembly time (provenance: alknet ADR-008, superseded in-crate by ADR-010) | | Secret material flow | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer | | Privilege model and authority context | [ADR-017](decisions/017-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env | | Abort cascade for nested calls | [ADR-020](decisions/020-abort-cascade-for-nested-calls.md) | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in | @@ -615,8 +617,8 @@ See [open-questions.md](open-questions.md) for full details. `SamePeerCollision`); `#[non_exhaustive]`. See [client-and-adapters.md](client-and-adapters.md). - **OQ-12** (resolved): `from_call` re-import trigger — `from_call` is a - manual free function; the assembly layer calls it after the dial (in - `AlknetClient`). See + manual free function; after the dial (in the composition layer's dial helper + `AlknetClient` — today the alknet glue; OQ-43). See [ADR-028](decisions/028-from-call-manual-free-function.md). - **OQ-13** (resolved): `from_call` namespace collision — same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays). See @@ -637,14 +639,16 @@ See [open-questions.md](open-questions.md) for full details. rotation), not a connection-assigned UUID. - **OQ-19** (resolved by ADR-025): Persistent peer registry — the storage boundary is `core trait + in-memory default`; persistence - adapters are separate crates (the concrete adapter shapes were named in - alknet research, not re-specced here). -- **OQ-21** (resolved): X.509 outgoing-only case — three remote + adapters are separate crates (composition-layer work — OQ-43; the + concrete adapter shapes were named in the source repo's research + (alknet, provenance), not re-specced here). +- **OQ-21** (resolved by ADR-054): X.509 outgoing-only case — three remote roles named (public X.509 endpoint, transport relay, hub); pure-client - X.509 connections are not in the peer graph on the client side. The - deciding ADR (alknet ADR-034, "Outgoing-Only X.509 and Three Peer - Roles") was not ported into alkcall; the surviving alkcall surface is - ADR-025's `PeerEntry` asymmetry. See [client-and-adapters.md](client-and-adapters.md). + X.509 connections are not in the peer graph on the client side. + Re-owned in-crate by ADR-054 (verifier selection by `PeerEntry` + presence; provenance: alknet ADR-034, not ported); ADR-025's + `PeerEntry` asymmetry is the surviving registry surface. See + [client-and-adapters.md](client-and-adapters.md). ## References @@ -655,6 +659,6 @@ See [open-questions.md](open-questions.md) for full details. - ADR-024: Peer-graph routing model (peer-keyed overlays + `PeerRef` routing) - ADR-025: PeerEntry and Identity.id decoupling (`PeerId` source) - ADR-026: Forwarded-for identity (`forwarded_for` on `call.requested` and `OperationContext`) -- ADR-034: Outgoing-only X.509 and the three peer roles +- ADR-054: Outgoing-only X.509 and the three remote roles (re-owned in-crate; verifier selection by `PeerEntry` presence) - ADR-021: Streaming handler for subscriptions (server-side streaming dispatch path) -- Reference implementation: `/workspace/@alkdev/alknet-main/crates/alknet-core/src/call/` \ No newline at end of file +- Reference implementation: this crate (`src/`); historical origin: the alknet / alknet-main call-core sources (provenance) \ No newline at end of file diff --git a/docs/architecture/channel-client.md b/docs/architecture/channel-client.md index c800309..61b1ace 100644 --- a/docs/architecture/channel-client.md +++ b/docs/architecture/channel-client.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-07-18 +last_updated: 2026-10-13 (alkcall reframing) --- # channel-client.md — ChannelClient @@ -152,17 +152,19 @@ takes over channels establishment. The transport is the caller's concern: `Connection::from_bidi(tls_stream, ...)` for TCP+TLS, a quinn `Connection`, a WebTransport `BiStream`, an SSH `direct-tcpip` channel wrapped via `from_bidi`, a WebSocket carrying `alk/channels` (the browser path per -ADR-044) — all produce a `Connection` that `from_connection` accepts +alknet ADR-044 — websocket-native session, provenance) — all produce a `Connection` that `from_connection` accepts unchanged. This mirrors the server side's `ChannelsAdapter::handle(Connection)`, which is substrate-agnostic by the same mechanism. -The dial (QUIC, TCP+TLS, iroh) lives in `AlknetClient` (`alknet-client`, +The dial (QUIC, TCP+TLS, iroh) lives in the composition layer's dial +helper, `AlknetClient` (today in the alknet glue; future owner OQ-43 — ADR-045), not on `ChannelClient`. Callers compose `AlknetClient::dial_quic(...).await?` + `ChannelClient::from_connection(conn).await?` — two lines, the dial then the take-over. Keeping the dial off -`ChannelClient` avoids `alknet-channels-call` depending on `alknet-client`; +`ChannelClient` avoids alkcall depending on a dial crate; the protocol crates are parallel to the dial, not downstream of it. -The credential/verifier-selection rule (ADR-034) lives in the dial +The credential/verifier-selection rule (ADR-054, re-owned from the +source repo's alknet ADR-034) lives in the dial (`AlknetClient`), not in `from_connection` — `from_connection` receives an already-established, already-authenticated `Connection`, exactly as `ChannelsAdapter::handle` does on the server side. @@ -181,12 +183,13 @@ connect side's channel 0 (if the connect side registers any). name follows the `CallClient` convention (the side that dialed), not a request/response role. -## Relationship to `AlknetClient` +## Relationship to the composition layer's dial helper `ChannelClient`'s *API* is transport-agnostic — `from_connection` takes a -pre-established `Connection`. The shared *dial+TLS* seam -(`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` / +pre-established `Connection`. The shared *dial+TLS* seam is the +composition layer's dial helper, `AlknetClient` (ADR-045; today in the +alknet glue, future owner OQ-43), which +provides three dial methods (`dial_quic` / `dial_tcp_tls` / `dial_iroh`), each producing a `Connection` that `from_connection` consumes. The dial is transport-specific (QUIC, TCP+TLS, iroh); the take-over (`from_connection`) is @@ -194,10 +197,9 @@ transport-agnostic. The two concerns are separated. `AlknetClient::dial_quic` is the dial that feeds `from_connection`. A caller that needs transport selection (QUIC with TCP+TLS fallback) uses -`AlknetClient` directly; the fallback policy is a caller concern. See +the dial helper directly; the fallback policy is a caller concern. See [ADR-045](decisions/045-alknetclient-native-dial-seam.md) for the -full decision and [OQ-55](/workspace/@alkdev/alknet/docs/architecture/questions/055-alknetclient-establishment-extraction.md) -(resolved). +full decision. ## Design Decisions @@ -205,15 +207,15 @@ All design decisions are documented as ADRs in [decisions/](decisions/). | ADR | Decision | Summary | |-----|----------|---------| -| [043](decisions/043-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045, resolves OQ-55) | +| [043](decisions/043-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary; dial lives in the composition layer's dial helper (ADR-045) | | [035](decisions/035-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | No `stream_types` on `open_channel`/`Channel`; handler owns sub-stream multiplexing | ## Open Questions -- **alknet OQ-55** (resolved by ADR-045): `AlknetClient` core **dial+TLS seam** - — `alknet-client` with three dial methods. `ChannelClient`'s API is - transport-agnostic (`from_connection`); the dial is the shared seam. - See [ADR-045](decisions/045-alknetclient-native-dial-seam.md). +- **Dial seam ownership** (ADR-045): the dial helper (`AlknetClient`) + lives in the composition layer today; its post-alknet owner and shape + is OQ-43. `ChannelClient`'s API is transport-agnostic + (`from_connection`); the dial is the shared seam. ## References @@ -225,6 +227,6 @@ All design decisions are documented as ADRs in [decisions/](decisions/). - ADR-039: ChannelManager (the shared state `ChannelClient` holds) - ADR-047: openable ALPNs are operations (per-ALPN open ops dissolve the generic `channel/open`) -- alknet OQ-55: AlknetClient / client establishment extraction -- `docs/architecture/crates/call/client-and-adapters.md` — `CallClient` (the +- ADR-045: the dial seam (dial out of the protocol crate) +- [client-and-adapters.md](client-and-adapters.md) — `CallClient` (the shape `ChannelClient` mirrors) \ No newline at end of file diff --git a/docs/architecture/channel-operations.md b/docs/architecture/channel-operations.md index 4a209f2..2ba3e0c 100644 --- a/docs/architecture/channel-operations.md +++ b/docs/architecture/channel-operations.md @@ -1,6 +1,6 @@ --- status: current -last_updated: 2026-10-13 +last_updated: 2026-10-13 (alkcall reframing) --- # channel-operations.md — Channel Lifecycle on the Call Protocol @@ -217,7 +217,7 @@ decrement. chunks MUST be written and flushed before the `channel/close` operation is sent on channel 0. The side closing must observe the data-channel pump complete before issuing the call operation. For TTY this is the -exit-chunk-is-last invariant (ADR-055) carried forward — the exit +exit-chunk-is-last invariant (alknet ADR-055, provenance) carried forward — the exit control message rides on TTY's `STREAM_CTRL_OUT` (stream_type 4, inside TTY's 5-byte payload format); for tunnels it is the last data byte before close. This invariant crosses two channels (the data channel and @@ -327,7 +327,7 @@ would pressure consumers toward the stale-poll path. | Call operations on channel 0 (`channel/control`, `channel/close`) | Control that doesn't need ordering relative to data, or lifecycle events | resize, signal, keepalive, close | | Data-ordered bytes on the data channel's `BiStream` (handler-internal framing) | Control that MUST be ordered relative to data | EOF before exit, flush before close | -The TTY crate's exit-chunk-is-last invariant (ADR-055) is the canonical +The TTY crate's exit-chunk-is-last invariant (alknet ADR-055, provenance) is the canonical example of data-ordered control — it rides on TTY's `STREAM_CTRL_OUT` (stream_type 4, inside TTY's 5-byte payload format) because it must arrive after the last data on TTY's stdout stream_type, guaranteed by diff --git a/docs/architecture/channels-README.md b/docs/architecture/channels-README.md index 9a474b5..842ec8b 100644 --- a/docs/architecture/channels-README.md +++ b/docs/architecture/channels-README.md @@ -1,9 +1,9 @@ --- status: draft -last_updated: 2026-07-18 +last_updated: 2026-10-13 (alkcall reframing) --- -# alknet-channels +# alkcall — Channels Protocol A multiplexing proxy: a `ProtocolHandler` on `alk/channels` that decomposes a single bidirectional transport stream into N logical channels, @@ -22,10 +22,10 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. |----------|--------|-------------| | [overview.md](channels-overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, ALPN, transport agnosticism, WASM, relationship to existing crates | | [channels-wire.md](channels-wire.md) | draft | The 8-byte chunk format (`[channel_id:u32 be][length:u32 be][payload]`), the add/strip composition, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) | -| [channels-connection.md](channels-connection.md) | draft | `ChannelBidiStreamSource` (implements `BidiStreamSource` — ADR-008/074, as amended by ADR-035), `accept_bi` yields one `BiStream` per channel, recursive composition | -| [channels-adapter.md](channels-adapter.md) | draft | `ChannelsAdapter` (`ProtocolHandler` on `alk/channels`), `ChannelManager`, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) | +| [channels-connection.md](channels-connection.md) | draft | `ChannelBidiStreamSource` (implements `BidiStreamSource` — ADR-008 + `from_source`, 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-050) | | [channel-operations.md](channel-operations.md) | draft | `channel/open`, `channel/close`, `channel/control`, `channel/resources/subscribe` — call-protocol operations on channel 0, ACL flow, `direction` semantics, the hub relay contract (ADR-042) | -| [channel-client.md](channel-client.md) | draft | `ChannelClient` — the client side of a channels connection; transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045); bidirectionality preserved | +| [channel-client.md](channel-client.md) | draft | `ChannelClient` — the client side of a channels connection; transport-agnostic `from_connection` primary; dial lives in the composition layer's dial helper (ADR-045); bidirectionality preserved | ## Applicable ADRs @@ -39,27 +39,27 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. | [039](decisions/039-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts | | [040](decisions/040-backpressure-channel-limits-id-reuse.md) | Backpressure, Channel Limits, and ID Reuse | Bounded-buffer (1 MiB default), 256-channel per-connection memory bound, monotonic IDs with wrap (DoS defense reframed by ADR-041) | | [041](decisions/041-per-identity-channel-cap.md) | Per-Identity Channel Cap | 256 per `PeerId`, enforced via `ChannelLifecyclePolicy` in `channels-call`; per-connection `max_channels` reframed as a memory bound; symmetric (both sides enforce); spoke caps hub (direct caller), not browser (forwarded_for is metadata) | -| [077[077](/workspace/@alkdev/alknet/docs/architecture/decisions/077-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); TTY always uses its 5-byte format, carried transparently in the channels payload | -| [050](decisions/050-pump-bidi-two-pump-helper.md) | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer | +| [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` — the Two-Pump Helper | The two-pump deadlock contract; handler-level, not channels-layer | | [042](decisions/042-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite | -| [043](decisions/043-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045, resolves OQ-55) | -| [044](decisions/044-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers, not sub-crates | +| [043](decisions/043-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, transport-agnostic `from_connection` primary; dial lives in the composition layer's dial helper (ADR-045) | +| [044](decisions/044-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient) modules inside alkcall; hub and worker are consumers, not sub-crates | | [008](decisions/008-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements | | [009](decisions/009-bistream-as-the-handler-leaf.md) | `BiStream` as the Handler Leaf | `accept_bi` returns `BiStream`; the transport-leaf decision ADR-035 builds on | | [007](decisions/007-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on | -| [052[052](/workspace/@alkdev/alknet/docs/architecture/decisions/052-alknet-tty-wire-format-and-two-carriage.md) | alknet-tty Wire Format | The 5-byte format carried transparently in the channels payload (control bidirectional via `STREAM_CTRL_IN`/`OUT` — Phase 7 amendment) | | [021](decisions/021-streaming-handler-for-subscriptions.md) | StreamingHandler for Subscriptions | The machinery `channel/resources/subscribe` uses | | [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | The auth chain for hub-relayed channel opens | -| [031](decisions/031-crate-decomposition.md) | Crate Decomposition | alknet-channels depends on alknet-core only; no handler-depends-on-handler | +| [031](decisions/031-crate-decomposition.md) | Crate Decomposition | The channels subsystem lives inside alkcall with the call protocol and vendored core types; no handler-depends-on-handler | + +Provenance (source-repo ADRs, not ported): [alknet ADR-077 — TTY Inside Channels, Sub-Streams Not Wire Format](/workspace/@alkdev/alknet/docs/architecture/decisions/077-tty-inside-channels.md): TTY's two modes (direct vs channels); TTY always uses its 5-byte format, carried transparently in the channels payload. [alknet ADR-052 — TTY Wire Format](/workspace/@alkdev/alknet/docs/architecture/decisions/052-alknet-tty-wire-format-and-two-carriage.md): the 5-byte format carried transparently in the channels payload (control bidirectional via `STREAM_CTRL_IN`/`OUT` — Phase 7 amendment). TTY is now a sibling protocol crate (`alktty`). ## Relevant Open Questions | OQ | Title | Status | Relevance | |----|-------|--------|-----------| -| alknet OQ-55 | AlknetClient / Client Establishment Extraction | resolved (ADR-045) | `ChannelClient`'s API is decided (ADR-043): transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (`alknet-client`, ADR-045) | -| OQ-23 (tracker; alknet OQ-56) | Full channel-level flow-control windowing | deferred(scope) | Bounded-buffer is decided (ADR-040); full windowing is an extension blocked on "a real deployment observes HOL blocking on a saturated channel where bounded buffer is insufficient" | -| alknet OQ-57 | Two-pump helper extraction to alknet-core | deferred(scope) (alknet-scope) | The shutdown-on-completion *contract* is decided and the helper landed in-tree (`channels::pump_bidi`, ADR-050); any alknet-core extraction is alknet-scope | -| OQ-24 (tracker; alknet OQ-68) | Add/strip API shape (built-in vs utility) | resolved | Whether the 8-byte header add/strip is built into the channels read/write path or exposed as a standalone utility. The *contract* is decided (ADR-035); the *function surface* is not | +| OQ-23 ("[tracker](open-questions.md)"; source: 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" | +| Dial seam (source: alknet OQ-55) | Client dial helper extraction | resolved (ADR-045); ownership → OQ-43 | `ChannelClient`'s API is decided (ADR-043): transport-agnostic `from_connection` primary; the dial lives in the composition layer's dial helper (`AlknetClient` today; post-alknet owner and shape — OQ-43) | +| Two-pump helper (source: alknet OQ-57) | Two-pump helper extraction to a shared core crate | dissolved | The shutdown-on-completion *contract* is decided and the helper is in-tree (`channels::pump_bidi`, ADR-050); the extraction target (a mono-repo core crate being replaced) no longer exists — alkcall owns its helpers | +| OQ-24 ("[tracker](open-questions.md)"; source: 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 shipped surface integrates the strip (see OQ-24 in [open-questions.md](open-questions.md)) | ## Key Design Principles @@ -136,6 +136,10 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. ## References +Source-repo research and history (alknet, provenance — these paths live +in the alknet repo, `/workspace/@alkdev/alknet/docs/research/...`, not +this one): + - `docs/research/alknet-channels/phase-0-findings.md` — Phase 0 research (vision, hub motivation, wire format, negotiation, internals, DPs, OQs) - `docs/research/alknet-channels/poc-summary.md` — the de-risk POC (28 @@ -146,8 +150,10 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. surfaced the pure-channel-multiplexing resolution (ADR-035) - `/workspace/alknet-channels-poc/` — the POC codebase - `docs/research/alknet-tty/phase-0-findings.md` — the TTY crate's chunk - format (the seed of the channels generalization) + format (the seed of the channels generalization; TTY is now the + sibling crate `alktty`) - `docs/research/alknet-ssh/phase-0-findings.md` — SSH's channel multiplexer (the prior art for N-channel multiplexing) -- `docs/architecture/crates/hub/README.md` — the hub crate (the primary - consumer; the relay implementation's home) \ No newline at end of file +- Source-repo hub crate (alknet, provenance — the composition layer's + hub wiring; the relay implementation itself is an alkcall export, + `ChannelRelay`, ADR-051) \ No newline at end of file diff --git a/docs/architecture/channels-adapter.md b/docs/architecture/channels-adapter.md index 640ec05..6562061 100644 --- a/docs/architecture/channels-adapter.md +++ b/docs/architecture/channels-adapter.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-07-18 +last_updated: 2026-10-13 (alkcall reframing) --- # channels-adapter.md — ChannelsAdapter and ChannelManager @@ -69,7 +69,7 @@ transport's stream count differs. ## `ChannelManager` ```rust -// In alknet-channels-core: +// In alkcall (channels subsystem): pub struct ChannelManager { channels: Mutex>, handlers: Arc, @@ -238,7 +238,7 @@ The runner's `select!` loop exits when all `MuxHandle` clones drop (the `new_pumps` sender closes) — the natural shutdown signal. This matches the dynamic `channel/open` model. -## The two-pump pattern (ADR-078 — documented here for handler authors) +## The two-pump pattern (ADR-050 — documented here for handler authors) Handlers with a two-pump shape (two `tokio::io::copy` pumps, one per direction — tunnel, SSH `direct-tcpip`) MUST shut down the opposite sink @@ -262,7 +262,8 @@ tokio::try_join!(c2t, t2c)?; The three-pump pattern (TTY's `pump_session`, coordinating via the `exit_code` future) does not have this deadlock — the `exit_code` future is -the third signal. The two-pump pattern is documented in ADR-078; the +the third signal. The two-pump pattern is pinned in-tree by ADR-050 +(`channels::pump_bidi`); the shutdown-on-completion contract is a handler-level concern, not a channels-layer one. @@ -308,7 +309,8 @@ All design decisions are documented as ADRs in [decisions/](decisions/). - ADR-039: ChannelsAdapter and ChannelManager (the decision) - ADR-035: channels pure channel multiplexing (the umbrella decision that - amends ADR-034/074/077) + amends ADR-034 and the source-repo connection/from_source + TTY-inside-channels decisions — + alknet ADR-074/077, provenance; the in-crate homes are ADR-007/008) - ADR-036: channel 0 pre-negotiated (the `preinstall_channel_0` step) - ADR-037: channel lifecycle operations (the ops registered on `call_ops`) - ADR-038: ChannelBidiStreamSource (what the manager constructs per @@ -318,7 +320,9 @@ All design decisions are documented as ADRs in [decisions/](decisions/). - ADR-041: per-identity channel cap (the `ChannelLifecyclePolicy` consulted by the `channel/open` handler; the relay consequence for hub-relayed channels) -- `docs/research/alknet-channels/poc-summary.md` §Issues Surfaced #4-#7 - (REQ-CH-01..04, the two-pump deadlock) -- `docs/research/stream-unification/findings.md` — the research that +- Source-repo POC research (alknet provenance): + `/workspace/@alkdev/alknet/docs/research/alknet-channels/poc-summary.md` + §Issues Surfaced #4-#7 + (REQ-CH-01..04, the two-pump deadlock); + `/workspace/@alkdev/alknet/docs/research/stream-unification/findings.md` — the research that surfaced the pure-multiplexing resolution \ No newline at end of file diff --git a/docs/architecture/channels-connection.md b/docs/architecture/channels-connection.md index 8acaa22..e75f02c 100644 --- a/docs/architecture/channels-connection.md +++ b/docs/architecture/channels-connection.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-07-18 +last_updated: 2026-10-13 (alkcall reframing) --- # channels-connection.md — ChannelBidiStreamSource and `BiStream` Access @@ -21,7 +21,7 @@ tunnel uses the `BiStream` as raw bytes, call length-prefixes JSON, SSH runs its own channel protocol. The `BiStream` is wrapped in a `ChannelBidiStreamSource` that implements -`alknet-core`'s `BidiStreamSource` trait (ADR-008), and a `Connection` is +alkcall's vendored-core `BidiStreamSource` trait (ADR-008), and a `Connection` is constructed from it via `Connection::from_source(source, alpn)`. The handler receives a `Connection`, calls `accept_bi()` once (yield-once per channel), gets a `BiStream`, and drives its session — identical to how it @@ -30,7 +30,7 @@ works on a top-level QUIC connection. ## `ChannelBidiStreamSource` ```rust -// In alknet-channels: +// In alkcall (channels subsystem): pub struct ChannelBidiStreamSource { // The reassembly buffer for this channel's payload bytes (one per @@ -86,7 +86,7 @@ async fn handle(&self, connection: Connection, _auth: &AuthContext) let (mut tcp_read, mut tcp_write) = tcp.into_split(); let (mut recv, mut send) = tokio::io::split(&mut bidi); - // Two-pump with shutdown-on-completion (ADR-078) + // Two-pump with shutdown-on-completion (ADR-050) let c2t = async { tokio::io::copy(&mut recv, &mut tcp_write).await?; tcp_write.shutdown().await.ok(); @@ -171,9 +171,11 @@ All design decisions are documented as ADRs in [decisions/](decisions/). - ADR-008: BidiStreamSource trait - ADR-009: `BiStream` as the handler leaf - ADR-007: `Connection::from_stream` (the yield-once path generalized) -- ADR-077: TTY inside channels (TTY always uses its 5-byte format, +- Provenance (source-repo, not ported): [alknet ADR-077](/workspace/@alkdev/alknet/docs/architecture/decisions/077-tty-inside-channels.md) — TTY inside channels (TTY always uses its 5-byte format, carried transparently in the channels payload) -- `docs/research/alknet-channels/poc-summary.md` §POC Target 2 (the - yield-once `Connection::from_stream` validation) -- `docs/research/stream-unification/findings.md` — the research that +- Source-repo POC research (alknet provenance): + `/workspace/@alkdev/alknet/docs/research/alknet-channels/poc-summary.md` + §POC Target 2 (the + yield-once `Connection::from_stream` validation); + `/workspace/@alkdev/alknet/docs/research/stream-unification/findings.md` — the research that surfaced the single-accessor resolution \ No newline at end of file diff --git a/docs/architecture/channels-overview.md b/docs/architecture/channels-overview.md index 0bdf6ab..6d7724b 100644 --- a/docs/architecture/channels-overview.md +++ b/docs/architecture/channels-overview.md @@ -1,13 +1,13 @@ --- status: draft -last_updated: 2026-07-18 +last_updated: 2026-10-13 (alkcall reframing) --- -# alknet-channels — Overview +# alkcall channels — Overview ## What -`alknet-channels` is a multiplexing proxy crate. It implements +The channels subsystem of alkcall is a multiplexing proxy. It implements `ProtocolHandler` for the `alk/channels` ALPN: it receives one bidirectional transport stream, reads 8-byte chunk headers, and routes each chunk's payload to the right logical channel. Each channel is reassembled @@ -28,7 +28,7 @@ multiplexing on the `BiStream` it receives. ### The problem: three multiplexing models that don't compose -Before channels, alknet had three multiplexing models: +Before channels, the alk stack had three multiplexing models: | Model | Where | Mechanism | |-------|-------|-----------| @@ -89,7 +89,8 @@ on the `BiStream` the channels layer gives them (ADR-035). inner layer's framing byte, carried transparently in the payload. - **The control channel is handler-internal.** TTY sub-demuxes control from its io `BiStream` using its 5-byte format (`STREAM_CTRL_IN` / - `STREAM_CTRL_OUT` — alknet ADR-052 amended by Phase 7). The channels layer + `STREAM_CTRL_OUT` — the TTY wire format, [alknet ADR-052](/workspace/@alkdev/alknet/docs/architecture/decisions/052-alknet-tty-wire-format-and-two-carriage.md) + amended by Phase 7; TTY is now the sibling crate `alktty`). The channels layer doesn't carry control. - **Recursive composition is literal.** A channel with ALPN `alk/channels` runs another channels demux on its `BiStream`. The @@ -110,8 +111,9 @@ The crate has two internal components (ADR-039): `channel/open` operation handler closes over. Each channel is presented to its handler as a `Connection` constructed via -`Connection::from_source(ChannelBidiStreamSource, alpn)` (ADR-008/074, as -amended by ADR-035). The handler calls `accept_bi()` once (yield-once per +`Connection::from_source(ChannelBidiStreamSource, alpn)` (ADR-008 for the +extension point, ADR-007 for the `Connection` constructors, as amended by +ADR-035). The handler calls `accept_bi()` once (yield-once per channel) and gets a `BiStream` — identical to how it works on a top-level QUIC connection. @@ -139,7 +141,8 @@ Downstream crates depend on alkcall as a single dependency: alktty / alktunnels / alktrader (protocol crates) └── alkcall (call + channels protocols, no networking) -alknet / alknode (networking + composition) +composition layer (dial/accept + hub — today the alknet glue, + being replaced; future owner and shape → OQ-43) ├── alkcall ├── alktty (optional — whichever protocol crates are wired in) ├── alktunnels (optional) @@ -147,7 +150,7 @@ alknet / alknode (networking + composition) ``` The hub and worker are **consumers** of alkcall, not sub-crates. The -downstream alknet crate IS the hub — it depends on alkcall and uses +composition layer IS the hub — it depends on alkcall and uses channels as its substrate, with the relay logic (ADR-042) living alongside its peer lifecycle and service discovery responsibilities. A worker is any crate that uses `ChannelClient` to dial. There are no @@ -168,7 +171,7 @@ stream: |-----------|-----| | QUIC bidi stream | `alk/channels` ALPN on a QUIC connection; one bidi stream carries all channels | | TCP+TLS | `alk/channels` ALPN on a TLS connection; the TCP stream carries all channels | -| WebTransport | `alk/channels` session (deferred per ADR-044; the browser path uses WebSocket carrying `alk/channels`) | +| WebTransport | `alk/channels` session (deferred per alknet ADR-044 — websocket-native session, provenance — the browser path uses WebSocket carrying `alk/channels`) | | SSH channel | channels connection riding inside an SSH `direct-tcpip` channel (channels-over-SSH) | | Another channels connection | recursive composition (channel type `alk/channels` inside `alk/channels`) | @@ -186,14 +189,14 @@ async shell (demux/mux) wraps this core with `read_exact`/`write_all` and `mpsc` routing. The `ChannelManager` is ALPN-blind, auth-blind, and transport-blind (ADR- -075) — pure byte routing with no platform or protocol dependencies. A WASM +039) — pure byte routing with no platform or protocol dependencies. A WASM build can read chunks from a WebTransport `BiStream`, reassemble them, and present `AsyncRead + AsyncWrite` handles to WASM-compatible handlers. The handlers themselves may or may not be WASM-compatible (russh's client is; `portable_pty` is not), but the channels layer is WASM-compatible by construction. -The async shell and `alknet-core` dep graph are not fully WASM-clean yet +The async shell dep graph is not fully WASM-clean yet (transitive `getrandom`/`rand` deps) — this is an implementation concern, not an architecture concern. The sync core's WASM compatibility is validated. @@ -225,9 +228,9 @@ A protocol crate can support two paths for its wire format: The protocol crate doesn't know which path it's on — it takes a `BiStream` (or `AsyncRead + AsyncWrite`) and drives the session. The two adapters are thin and live either in the protocol crate (behind -feature flags) or in the downstream alknet crate. +feature flags) or in the composition layer. -### alknet-call +### Channel 0 is the call protocol The call protocol runs on channel 0 exactly as on a top-level `alk/call` connection. The `Dispatcher` receives a `Connection` @@ -236,7 +239,7 @@ doesn't know it's inside channels. The call protocol's `EventEnvelope` framing (ADR-014) is the channels payload; the channels layer carries it transparently. -### alknet-hub / alknode +### Hub (composition layer) The hub is the primary consumer. With channels, the hub holds one channels connection per leg (browser↔hub, hub↔spoke) and relays @@ -261,7 +264,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). | [040](decisions/040-backpressure-channel-limits-id-reuse.md) | Backpressure, Limits, ID Reuse | Bounded-buffer (1 MiB), 256-channel cap, monotonic IDs | | [041](decisions/041-per-identity-channel-cap.md) | Per-Identity Channel Cap | 256 per `PeerId`, enforced via `ChannelLifecyclePolicy`; per-connection `max_channels` reframed as a memory bound | | [042](decisions/042-hub-relay-translate-not-forward.md) | Hub Relay | Translate channel 0, byte-forward data channels with ID rewrite | -| [043](decisions/043-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045, resolves OQ-55) | +| [043](decisions/043-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary; dial lives in the composition layer's dial helper (ADR-045) | | [044](decisions/044-channels-subcrate-decomposition.md) | Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers | | [047](decisions/047-openable-alpns-are-operations.md) | Openable ALPNs Are Operations | `channel/open` dissolves into per-ALPN ops; `ChannelCore` wrapper; connection-owner allocates `channel_id` | @@ -270,19 +273,22 @@ All design decisions are documented as ADRs in [decisions/](decisions/). Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this crate: -- **alknet OQ-55** (resolved by ADR-045): `AlknetClient` core **dial+TLS seam** - — extracted as `alknet-client` with three dial methods. - `ChannelClient`'s API is transport-agnostic (`from_connection`); the - dial is the shared seam, now extracted. See +- **Dial seam** (ADR-045; source: alknet OQ-55): the dial+TLS seam is + the composition layer's dial helper (`AlknetClient` today — future + owner and shape OQ-43). `ChannelClient`'s API is transport-agnostic + (`from_connection`); the dial is the shared seam. See [ADR-045](decisions/045-alknetclient-native-dial-seam.md). -- **OQ-23** (tracker; alknet OQ-56) (deferred(scope)): Full channel-level flow-control windowing — +- **OQ-23** ([tracker](open-questions.md); source: alknet OQ-56) + (deferred(scope)): Full channel-level flow-control windowing — bounded-buffer is decided (ADR-040); full windowing is an extension blocked on a real HOL-blocking deployment observation. -- **alknet OQ-57** (deferred(scope)): Two-pump helper extraction to alknet-core — - the *contract* is decided (the in-tree helper landed as - `channels::pump_bidi`, ADR-050); any extraction to alknet-core is - alknet-scope. -- **OQ-24** (tracker; alknet OQ-68) (resolved): Add/strip API shape — whether the 8-byte header +- **Two-pump helper** (source: alknet OQ-57) (dissolved): the + contract is decided (the in-tree helper landed as + `channels::pump_bidi`, ADR-050); the extraction target (a mono-repo + core crate) no longer exists — alkcall owns its helpers. +- **OQ-24** ([tracker](open-questions.md); source: alknet OQ-68) + (resolved): Add/strip API shape — whether the 8-byte header add/strip is built into the channels read/write path or exposed as a standalone utility. The *contract* (channels strips, handler parses - payload) is decided (ADR-035); the *function surface* is not. \ No newline at end of file + payload) is decided (ADR-035); the shipped surface integrates the + strip (see OQ-24 in [open-questions.md](open-questions.md)). \ No newline at end of file diff --git a/docs/architecture/channels-wire.md b/docs/architecture/channels-wire.md index a57ff5b..a0820ef 100644 --- a/docs/architecture/channels-wire.md +++ b/docs/architecture/channels-wire.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-07-18 +last_updated: 2026-10-13 (alkcall reframing) --- # channels-wire.md — The 8-Byte Chunk Format @@ -103,7 +103,8 @@ There is no channels-layer framing-disambiguation trick beyond the fixed 8-byte header. The channels layer does not interpret the payload — it doesn't know if the payload is TTY chunks, call frames, or tunnel bytes. Any framing disambiguation within the payload is the handler's concern -(see `tty-wire.md` §"Framing disambiguation" for TTY's first-byte trick, +(see the TTY wire format's "framing disambiguation" — alknet ADR-052, +provenance; TTY is now the sibling crate `alktty` — for TTY's first-byte trick, which is internal to TTY's 5-byte format). ## Zero-length sentinel = EOF @@ -141,13 +142,16 @@ experience. See ADR-034 §substrate modes (as amended by ADR-035), ADR-039. **Implementation status:** 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) is deferred to the downstream alknet crate (OQ-41). The wire +each) is deferred to the composition layer's dial/accept half (OQ-41; +its owner — OQ-43). The wire format and demux loop are correct for both substrates; only the outer `accept_bi()` loop is missing. ## Wire-level invariants (REQ-CH-01, 02, 04, 05) -The de-risk POC (`docs/research/alknet-channels/poc-summary.md` §Issues +The de-risk POC (source-repo research, alknet provenance: +`/workspace/@alkdev/alknet/docs/research/alknet-channels/poc-summary.md` +§Issues Surfaced) surfaced invariants that hang channels silently if underspecified. These are **contracts**, not implementation details — both sides must agree. @@ -262,14 +266,14 @@ for tooling. | Control (out-of-band) | `channel/control` call operation on channel 0 (deferred — OQ-39) | ADR-037 | | Close | `channel/close` call operation on channel 0; data chunks flushed before close | ADR-037, REQ-CH-06 | -### REQ-CH-06: exit-chunk-before-close ordering (generalizes ADR-055) +### REQ-CH-06: exit-chunk-before-close ordering (generalizes alknet ADR-055 — the TTY exit-chunk invariant, provenance) The channel's data chunks MUST be written and flushed before the `channel/close` operation is sent on channel 0. This is a wire-level invariant: the side closing must observe the data-channel pump complete before issuing the call operation. -For TTY this is the exit-chunk-is-last invariant (ADR-055) carried +For TTY this is the exit-chunk-is-last invariant (alknet ADR-055, provenance) carried forward: the exit control message (on TTY's `STREAM_CTRL_OUT` stream_type 4, inside TTY's 5-byte payload) is the last data before `channel/close`. For tunnels it is the last data byte before close. The channels layer's @@ -297,7 +301,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this doc: -- **OQ-24** (tracker; alknet OQ-68) (resolved): Add/strip API shape — +- **OQ-24** ([tracker](open-questions.md); source: alknet OQ-68) (resolved): Add/strip API shape — built into the read/write path (the demux loop strips headers; handlers never see them), with the low-level header primitives exposed in `channels::wire` for tooling. @@ -307,13 +311,15 @@ Key questions affecting this doc: - ADR-034: channels wire format (the decision, amended by ADR-035 — 8-byte header, no `stream_type`) - ADR-035: channels pure channel multiplexing (the umbrella decision that - amends ADR-034/074/077) + amends ADR-034 and the source-repo connection/from_source decision — + alknet ADR-074, provenance; the in-crate homes are ADR-007/008) - ADR-036: channel 0 pre-negotiated - ADR-037: channel lifecycle operations - ADR-040: backpressure, channel limits, ID reuse - ADR-047: openable ALPNs are operations (per-ALPN open ops dissolve the generic `channel/open`) -- `docs/research/alknet-channels/poc-summary.md` §POC Target 1, §Issues - Surfaced #4-#6 (REQ-CH-01, 02, 04) -- `docs/research/stream-unification/findings.md` — the research that - surfaced the 8-byte format decision \ No newline at end of file +- Source-repo POC research (alknet provenance): + `/workspace/@alkdev/alknet/docs/research/alknet-channels/poc-summary.md` + §POC Target 1, §Issues Surfaced #4-#6 (REQ-CH-01, 02, 04); + `/workspace/@alkdev/alknet/docs/research/stream-unification/findings.md` + — the research that surfaced the 8-byte format decision \ No newline at end of file diff --git a/docs/architecture/client-and-adapters.md b/docs/architecture/client-and-adapters.md index 073c351..8035a8e 100644 --- a/docs/architecture/client-and-adapters.md +++ b/docs/architecture/client-and-adapters.md @@ -1,9 +1,9 @@ --- status: draft -last_updated: 2026-07-17 +last_updated: 2026-10-13 (alkcall reframing) --- -# alknet-call — Client and Adapters +# alkcall — Client and Adapters The outbound half of the call protocol: opening connections, importing remote operations, and the adapter contract that ties import-style adapters together. @@ -11,14 +11,15 @@ This document covers what ADR-022 specced but the server-side implementation (`call-protocol.md`, `operation-registry.md`) did not include — the `CallClient` that *opens* a connection, the `from_call` adapter, and the `OperationAdapter` trait. (`from_jsonschema` was originally specced here -too, but ADR-027 moved it to `alknet-http` — see §"from_jsonschema" below.) +too, but ADR-027 moved it to the HTTP host crate — today `alkhttp` +(then named `alknet-http`) — see §"from_jsonschema" below.) The server-side `CallAdapter` and `CallConnection` dispatch loop are covered in `call-protocol.md`; this document covers the client-side connection-establishment half and the adapter surface. ## What -This document specifies three components, all in `alknet-call`: +This document specifies three components, all in alkcall: 1. **`CallClient`** — takes over an established transport `Connection` on ALPN `alk/call`, spawns the shared dispatch loop, and produces @@ -35,20 +36,21 @@ This document specifies three components, all in `alknet-call`: `from_openapi`, `from_mcp`, and `from_jsonschema` all implement. > **`from_jsonschema` moved.** ADR-027 moved `from_jsonschema` from -> `alknet-call` to `alknet-http` and gave it a real reqwest-backed +> the call protocol to the HTTP host crate and gave it a real reqwest-backed > forwarding handler (it was a broken schema-only placeholder before). > It is now an HTTP-backed single-endpoint adapter for non-standard / > non-OpenAPI / basic REST endpoints, functionally similar to -> `from_openapi` but one endpoint at a time. See -> [`crates/http/http-adapters.md`](/workspace/@alkdev/alknet/docs/architecture/crates/http/http-adapters.md) §"from_jsonschema". -> The `FromJsonSchema` provenance variant stays in `alknet-call` +> `from_openapi` but one endpoint at a time. It lives in `alkhttp`; its +> full spec is `alkhttp`'s `http-adapters.md` (original source-repo copy +> at `/workspace/@alkdev/alknet/docs/architecture/crates/http/http-adapters.md`, +> provenance). The `FromJsonSchema` provenance variant stays in alkcall > (`OperationProvenance`); only the adapter implementation moved. It also records two cross-cutting architectural mechanisms that the adapter surface rests on: -- The **adapter location map** — which adapters live in `alknet-call` vs - `alknet-http`, and why. +- The **adapter location map** — which adapters live in alkcall vs + `alkhttp`, and why. - The **no-env-vars invariant** — the architectural mechanism by which downstream consumers' `std::env::var` credential reads are made unreachable. @@ -65,16 +67,19 @@ downstream consumer: the runner pattern (a process that connects outward to a hub and exposes local ops), the container-service rewrite, the bilateral exchange, the NAPI projection, and the agent's cross-node tool dispatch all require a `CallClient`. `from_call` is the #2 gap; the `OperationAdapter` -trait is the enabling gap for `alknet-http`'s `from_openapi`/`from_mcp`. +trait is the enabling gap for `alkhttp`'s `from_openapi`/`from_mcp`. ADR-022 specced this surface. This document is the spec that operationally fills the gap ADR-022 left to implementation: the `CallClient` API, the `from_call` flow, the trait signature, the adapter location, the credential invariant, and the bilateral pattern. The gap -analysis (`docs/research/alknet-call-completion/gap-analysis.md`) identified +analysis (source-repo research, now at +`/workspace/@alkdev/alknet/docs/research/alknet-call-completion/gap-analysis.md`) +identified four decisions (DC-1..4) needed before implementation. DC-1 was initially resolved by ADR-023 (`remote_safe`/`trusted_peer`), but a subsequent research -pass (`docs/research/alknet-call-peer-routing/findings.md`) found that +pass (source-repo research, now at +`/workspace/@alkdev/alknet/docs/research/alknet-call-peer-routing/findings.md`) found that ADR-023's model was structurally broken for the head→N-workers pattern (the primary use case) and that its parallel `remote_safe`/`trusted_peer` authorization system duplicated the existing `AccessControl`/`Identity` @@ -101,7 +106,7 @@ ordered, reliable bidirectional stream — QUIC, TCP+TLS, WebTransport, SSH `direct-tcpip`, a WebSocket (ADR-007 `Connection::from_stream` / `from_bidi`). The primary constructor (`spawn_dispatch`) takes a pre-established `Connection` from any transport; the dial lives in -`AlknetClient` (`alknet-client`, ADR-045). This mirrors +the composition layer's dial helper, `AlknetClient` (ADR-045). This mirrors `ChannelClient::from_connection` (ADR-043) and is the client-side analogue of the server-side generalization ADR-007 made. @@ -176,14 +181,15 @@ authorization machinery that gates every other call. No `RemoteFilter`, no primary constructor — it takes a pre-established `Connection`, constructs a `CallConnection`, builds a `Dispatcher`, spawns the dispatch task, and returns the live `CallConnection`. The dial lives in -`AlknetClient` (`alknet-client`, ADR-045): keeping a QUIC convenience -constructor on `CallClient` would make `alknet-call` depend on -`alknet-client`, contradicting the dep graph (the protocol crates are +the composition layer's dial helper, `AlknetClient` (ADR-045): keeping a QUIC convenience +constructor on `CallClient` would make alkcall depend on +the dial crate, contradicting the dep graph (the protocol crates are parallel to the dial, not downstream of it). Callers compose `AlknetClient::dial_quic` + `spawn_dispatch` — two lines, the dial then the take-over. Tests use `spawn_dispatch` directly to wire mock/loopback connections. The one-way-door surface is `spawn_dispatch`; the dial -lives in `alknet-client`. +lives in the composition layer (`AlknetClient` today — future owner +OQ-43). This mirrors `ChannelClient::from_connection` (ADR-043) and is the client-side analogue of the server-side generalization ADR-007 made. @@ -232,7 +238,7 @@ authorization). See [ADR-024](decisions/024-peer-graph-routing-model.md) §6. The credential dimensions are split across two layers (ADR-012, amended 2026-07-17): -- **`ConnectionCredentials`** (in `alknet-core`, per ADR-012) — the +- **`ConnectionCredentials`** (vendored core types in alkcall, per ADR-012) — the **transport-level** credential bundle, consumed by the dial (`AlknetClient`). Carries the two transport-identity dimensions: `local_identity` (the local node's `TlsIdentity`) and `remote_identity` @@ -250,7 +256,7 @@ Credentials come from `Capabilities` (ADR-010), never from environment variables. The transport-identity dimensions (ADR-022 §7): ```rust -// Transport-level (alknet-core, consumed by the dial — ADR-012) +// Transport-level (vendored core in alkcall, consumed by the dial — ADR-012) pub struct ConnectionCredentials { pub local_identity: Option, // RFC 7250 raw key or X.509 pub remote_identity: Option, // expected fingerprint (None = CA path / fail-closed) @@ -262,14 +268,15 @@ pub struct ConnectionCredentials { // Dispatcher::resolve_identity reads payload.get("auth_token"). ``` -`RemoteIdentity` (ADR-022 §7, extended by ADR-034 §2) carries a +`RemoteIdentity` (ADR-022 §7, extended by alknet ADR-034 §2 — the +verifier-rule ADR, re-owned as ADR-054) carries a fingerprint string the assembly layer derives from `Capabilities` when the local node has a `PeerEntry` for the remote (the known-peer case → fingerprint pin). `remote_identity: None` is the **public X.509 endpoint** case: the local node has no `PeerEntry` for the remote, so there is no fingerprint to pin. Combined with an X.509 transport, `None` selects CA verification (`WebPkiServerVerifier`) per the -verifier-selection rule in ADR-034 §3. Combined with an Ed25519 +verifier-selection rule in ADR-054. Combined with an Ed25519 raw-key transport, `None` fails closed (raw-key remotes are always known peers — no CA to fall back to). The `Option` is load-bearing, not cosmetic: `Some(fingerprint)` means "pin this" (known peer), `None` @@ -282,25 +289,27 @@ pub struct RemoteIdentity { pub fingerprint: String } ``` There is no call-protocol credential bundle. The transport dimensions -(`local_identity`, `remote_identity`) are in `ConnectionCredentials` in -`alknet-core` per ADR-012. +(`local_identity`, `remote_identity`) are in `ConnectionCredentials` with +the vendored core types in alkcall, per ADR-012. - **TLS identity** — the local node's Ed25519 raw key (RFC 7250) or X.509 cert, - derived from the vault at startup (alknet ADR-020, ADR-026, ADR-027). + derived from the vault at startup (vault ADRs live in the source repo — + alknet ADR-020/026/027, provenance). - **Auth token** — an opaque call-protocol-level token, decrypted from the vault or derived from a shared secret. - **Remote identity verification** — the expected fingerprint/cert of the remote node, stored as a capability. `Some` → fingerprint pin (known peer with a `PeerEntry`); `None` → CA verification for X.509 remotes, - fail-closed for Ed25519 raw-key remotes (ADR-034 §2/§3). The `None` + fail-closed for Ed25519 raw-key remotes (ADR-054). The `None` case is the public-X.509-endpoint path, not a missing field. These are populated by the assembly layer at `CallClient` construction time from vault-derived `Capabilities`. The credential path is the no-env-vars invariant (below). The concrete shapes of `TlsIdentity`, `AuthToken`, and `RemoteIdentity` are implementation-detail two-way doors; the one-way -constraints are that they come from `Capabilities`, not env vars (ADR-010). - +constraints are that they come from `Capabilities`, not env vars (ADR-010), +and that `ConnectionCredentials` lives with the vendored core types in +alkcall (ADR-012). **TLS client-auth presentation** (OQ-14 #1, wired): the client presents its Ed25519 key as an RFC 7250 raw public key client cert — the client-side equivalent of the server's `RawKeyCertResolver`. This is **wired now**, not @@ -324,7 +333,8 @@ start as "accept any, extract fingerprint" and add fingerprint-pinning later. This is a two-way-door remainder; the one-way constraint (credentials from `Capabilities`, not env vars) is unaffected. -**Server cert verifier selection** (OQ-14 #2 + alknet ADR-034 §3): the client-side +**Server cert verifier selection** (OQ-14 #2 + ADR-054 §2, re-owned +from the source repo's ADR-034): the client-side `ServerCertVerifier` is selected by whether the local node has a `PeerEntry` for the remote, not by key type alone. A pure-client connection to a **public X.509 endpoint** (no `PeerEntry` on the local @@ -339,7 +349,7 @@ with mixed Ed25519 + X.509 fingerprints) uses fingerprint pinning on both cert paths and does enter the peer graph. An unknown Ed25519 raw-key remote fails closed (no CA to fall back to — raw-key remotes are always known peers). See -[ADR-034](/workspace/@alkdev/alknet/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md) +[ADR-054](decisions/054-outgoing-only-x509-three-remote-roles.md) for the verifier selection rule and the three-role naming. ### from_call @@ -419,7 +429,7 @@ 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 the composition layer's dial helper, `AlknetClient` — today the alknet glue; OQ-43). The overlay is per-connection (Layer 2, ADR-019), so a stale overlay dies with the connection; re-import on reconnect is naturally scoped to the new connection. A `CallConnection::refresh()` method for mid-connection @@ -456,35 +466,38 @@ want to disclose the originator. See [ADR-026](decisions/026-forwarded-for-ident ### from_jsonschema `from_jsonschema` was originally specified here (ADR-022 §5) as a -schema-only adapter in `alknet-call` — a placeholder handler returning +schema-only adapter in the call protocol — a placeholder handler returning `NOT_FOUND`. That was broken: an op in the registry needs a real handler, and the "schema-only, no handler" concept conflated schema validation (a planning activity that doesn't need a registry entry) with operation registration (which always needs a handler). [ADR-027](decisions/027-from-jsonschema-as-http-adapter.md) moved -`from_jsonschema` to `alknet-http` as an HTTP-backed single-endpoint +`from_jsonschema` to the HTTP host crate — today `alkhttp` +(ex-`alknet-http`) — as an HTTP-backed single-endpoint adapter: the caller supplies an `OperationSpec` + `HttpServiceConfig` + path template + method, and the adapter builds one `HandlerRegistration` with a real reqwest forwarding handler and `FromJsonSchema` provenance. It is functionally similar to `from_openapi` but one endpoint at a time, for non-standard / non-OpenAPI / basic REST -endpoints that don't have a full OpenAPI document. See -[`crates/http/http-adapters.md`](/workspace/@alkdev/alknet/docs/architecture/crates/http/http-adapters.md) §"from_jsonschema". +endpoints that don't have a full OpenAPI document. See `alkhttp`'s +`http-adapters.md` §"from_jsonschema" (its original source-repo copy at +`/workspace/@alkdev/alknet/docs/architecture/crates/http/http-adapters.md`, +provenance). The schema-validation-without-a-handler use case (the original stated purpose) is served by consuming `OperationSpec` directly — the spec already carries the input/output JSON Schemas. No adapter, no registry entry, no handler is needed for that. -The `FromJsonSchema` provenance variant stays in `alknet-call` +The `FromJsonSchema` provenance variant stays in alkcall (`OperationProvenance` in `registry/registration.rs`); only the adapter implementation moved. ### OperationAdapter trait The shared shape across import-style adapters. The trait lives in -`alknet-call` (where the types live); the implementations live where their +alkcall (where the types live); the implementations live where their transport dependencies live (see "Adapter Location Map" below). ```rust @@ -514,13 +527,13 @@ door, recorded here. Implementations: - `FromCall` — call-protocol-backed, transport-agnostic (in - `alknet-call`). `from_call` discovers ops over a `CallConnection`, + alkcall). `from_call` discovers ops over a `CallConnection`, which may be QUIC, TCP+TLS, or any transport `Connection::from_stream` supports (ADR-007). -- `FromOpenAPI` — HTTP-backed (in `alknet-http`). -- `FromJsonSchema` — HTTP-backed, single-endpoint (in `alknet-http` per - ADR-027; was a broken schema-only placeholder in `alknet-call`). -- `FromMCP` — MCP streamable-HTTP-backed (in `alknet-http`, feature-gated). +- `FromOpenAPI` — HTTP-backed (in `alkhttp`). +- `FromJsonSchema` — HTTP-backed, single-endpoint (in `alkhttp` per + ADR-027; was a broken schema-only placeholder in the call protocol). +- `FromMCP` — MCP streamable-HTTP-backed (in `alkhttp`, feature-gated). The `to_*` adapters (`to_openapi`, `to_mcp`) are outbound projections, not `OperationAdapter` implementations — they consume the registry, they don't @@ -529,18 +542,19 @@ produce entries for it (ADR-022 §5). ### Adapter Location Map The decomposition principle: **the adapter trait lives where the types live -(`alknet-call`); the adapter implementations live where their transport +(alkcall); the adapter implementations live where their transport dependencies live.** ``` -alknet-call (lean — no HTTP client, no HTTP server) +alkcall (lean — no HTTP client, no HTTP server) ├── OperationAdapter trait (the contract — async, per ADR-022 §5) ├── from_call (transport-agnostic — discovers remote ops via │ call protocol over any Connection) └── CallClient (outbound connection take-over — - spawn_dispatch, transport-agnostic; dial in AlknetClient) + spawn_dispatch, transport-agnostic; dial in + the composition layer's helper, AlknetClient) -alknet-http (owns HTTP server + HTTP client — separate crate, separate Phase 0) +alkhttp (owns HTTP server + HTTP client — sibling host crate) ├── ProtocolHandler for h2/http1.1/h3 (axum server — inbound HTTP) ├── from_openapi (parse OpenAPI doc + reqwest forwarding handler) ├── from_jsonschema (single-endpoint reqwest forwarding handler — ADR-027) @@ -550,28 +564,26 @@ alknet-http (owns HTTP server + HTTP client — separate crate, separate Phase 0 Not built: MCP stdio transport — stdio = spawn arbitrary executable = built-in RCE ("download untrusted MCP servers") - — streamable HTTP is the only supported MCP transport in alknet + — streamable HTTP is the only supported MCP transport in the alk stack — recorded as an explicit security position, not a feature gap ``` -`alknet-call` never sees the HTTP client. The `from_openapi`/`from_mcp` +alkcall never sees the HTTP client. The `from_openapi`/`from_mcp` forwarding handlers are opaque `Arc` from the registry's -perspective — constructed by `alknet_http::from_openapi()` at registration +perspective — constructed by `alkhttp::from_openapi()` at registration time, stored in `HandlerRegistration`, dispatched by the `CallAdapter` which -doesn't know reqwest is involved. `alknet-call` stays lean (no reqwest, no -axum); `alknet-http` owns both HTTP directions. +doesn't know reqwest is involved. alkcall stays lean (no reqwest, no +axum); `alkhttp` owns both HTTP directions. -**ADR-031 dependency note**: `alknet-http` implementing `from_openapi`/ -`from_mcp` means `alknet-http` depends on `alknet-call` (for `OperationSpec`, +**ADR-031 dependency note**: `alkhttp` implementing `from_openapi`/ +`from_mcp` means `alkhttp` depends on alkcall (for `OperationSpec`, `Handler`, `HandlerRegistration`, `OperationAdapter`). ADR-031's rule is "no -handler crate depends on another handler crate" — but `alknet-call` is both -a handler *and* the protocol foundation that `alknet-agent` and `alknet-napi` -already consume. `alknet-http` depending on `alknet-call` is "HTTP uses the -call protocol types," not "HTTP depends on SSH." This is within the spirit of -ADR-031 (`alknet-call` is protocol-foundation, not a peer handler). The -`alknet-http` spec should note this explicitly; a one-line amendment to -ADR-031 clarifying that `alknet-call` is a protocol-foundation crate is -deferred to the `alknet-http` Phase 0. +handler crate depends on another handler crate" — but alkcall is the +protocol foundation that every downstream consumer +(including the HTTP host and any agent crate) builds on. +`alkhttp` depending on alkcall is "HTTP uses the +call protocol types," not "HTTP depends on a sibling handler." This is within the spirit of +ADR-031 (alkcall is protocol-foundation, not a peer handler). ### No-Env-Vars Invariant @@ -595,7 +607,7 @@ vault (seed) → reqwest request goes out with vault-derived credential ``` -The `from_openapi`/`from_mcp` forwarding handlers (in `alknet-http`) are the +The `from_openapi`/`from_mcp` forwarding handlers (in `alkhttp`) are the credential injection point. They read from `context.capabilities`, not from `std::env::var`. The downstream consumers' `Default` impls reading env vars are simply never called — the assembly layer constructs providers with @@ -603,14 +615,14 @@ vault-derived credentials through the builder API, or the provider's HTTP calls are routed through `from_openapi` operations that carry the credential in `Capabilities`. -**This is a spec-level invariant in `alknet-call`, not a runtime convention.** +**This is a spec-level invariant in alkcall, not a runtime convention.** The dispatch path (`build_root_context` and `OperationEnv::invoke()` per ADR-018 §6) populates `OperationContext.capabilities` from the registration bundle. The invariant is: *no handler reads outbound credentials from any source other than `OperationContext.capabilities`.* This is already the architectural intent of ADR-010; this document records it as an explicit invariant that the `from_openapi`/`from_mcp` handler implementations (in -`alknet-http`) are verified against. +`alkhttp`) are verified against. ### Exchange-of-Operations Pattern (Runner / Container Service) @@ -649,20 +661,21 @@ Bilateral: the container service ALSO runs from_call against the hub, the other side's `External` ops. The #2 gap. 3. `OperationAdapter` trait — `from_call` implements it. The #3 gap (enabling, not blocking — `from_call` can be built as a free function before the trait - exists, but the trait is needed for `alknet-http`'s adapters). + exists, but the trait is needed for `alkhttp`'s adapters). -**Why the container service doesn't need alknet-ssh**: under the call +**Why the container service doesn't need SSH**: under the call protocol, the container service is a `CallClient` that dials the hub's `alk/call` ALPN (over QUIC, TCP+TLS, or any transport) — no SSH in the loop. SSH port forwarding becomes the *transitional* mechanism for targets that can't run a -call-protocol client (the `alknet-ssh` phase-0 findings document this +call-protocol client (the source repo's SSH phase-0 findings — alknet, +provenance — document this transition). Once the container service runs a `CallClient`, SSH is out of the path entirely. This is the "dev runner" pattern: a call-protocol client that connects back to a hub and exposes core dev tools (bash, fs, etc.) as operations. The agent -service (`alknet-agent`, downstream) is the consumer that orchestrates these +crate (downstream) is the consumer that orchestrates these via `env.invoke()`. ## Implementation Priority Order @@ -673,13 +686,13 @@ Based on the gap analysis and the downstream unblock chain: runner, no container service, no bilateral exchange. Reuses the existing `CallConnection` for the dispatch loop; adds only the connection-establishment + credential-handling half. The single - highest-value piece of work in the entire `alknet-call` completion. + highest-value piece of work in the entire call-completion effort. 2. **`from_call`** (critical, depends on `CallClient`) — consumes the already-implemented `services/list` + `services/schema` discovery API. 3. **`OperationAdapter` trait** (enabling) — the async trait. Small, - standalone, unblocks `alknet-http` Phase 1 (including `from_jsonschema` + standalone, unblocks `alkhttp`'s adapter work (including `from_jsonschema` per ADR-027). 4. **DC-1 resolution** (peer-graph routing model, ADR-024) — the @@ -692,23 +705,27 @@ Based on the gap analysis and the downstream unblock chain: ## What This Completion Unblocks -| Downstream crate | What it needs from alknet-call | Status without completion | +Historical planning table (source-repo era; crate names as they were). +The completion has since landed in this crate — `alknet-http` is now +`alkhttp`. + +| Downstream crate | What it needs from alkcall | Status without completion | |-------------------|-------------------------------|--------------------------| -| alknet-http | `OperationAdapter` trait (to implement `from_openapi`/`from_mcp`) | Blocked — can't define HTTP-backed adapters without the trait | -| alknet-ssh | Stable alknet-call types (no adapter dependency) | Not blocked — ssh depends on alknet-core, not alknet-call's adapters. Proceeds in parallel. | -| alknet-agent | `CallClient` (tool dispatch), `from_call` (remote tool import), `OperationAdapter` (provider adapters) | Blocked on `CallClient` + `from_call` | +| alknet-http (→ alkhttp) | `OperationAdapter` trait (to implement `from_openapi`/`from_mcp`) | Blocked — can't define HTTP-backed adapters without the trait | +| alknet-ssh (historical) | Stable alkcall types (no adapter dependency) | Not blocked — ssh depends on the vendored core types, not alkcall's adapters. Proceeds in parallel. | +| alknet-agent (historical) | `CallClient` (tool dispatch), `from_call` (remote tool import), `OperationAdapter` (provider adapters) | Blocked on `CallClient` + `from_call` | | Container service (dispatch rewrite) | `CallClient` + `from_call` | Blocked — this is the primary consumer | | Runner pattern (dev runner, opencode runner) | `CallClient` + `from_call` | Blocked — the runner IS a `CallClient` | -| alknet-napi | `CallClient` (Node.js calls remote ops) | Blocked — NAPI projects `CallClient` to JS | +| alknet-napi (historical) | `CallClient` (Node.js calls remote ops) | Blocked — NAPI projects `CallClient` to JS | ## Constraints -- **No HTTP in alknet-call.** `from_openapi`/`from_mcp`/`from_jsonschema`/ - `to_openapi`/`to_mcp` live in `alknet-http`. The `OperationAdapter` +- **No HTTP in alkcall.** `from_openapi`/`from_mcp`/`from_jsonschema`/ + `to_openapi`/`to_mcp` live in `alkhttp`. The `OperationAdapter` trait and the call-protocol-backed adapter (`from_call`, transport- - agnostic) live in `alknet-call`. `from_jsonschema` was originally - (mis)placed in `alknet-call` as a schema-only placeholder; ADR-027 - moved it to `alknet-http` as a real HTTP-backed adapter. See Adapter + agnostic) live in alkcall. `from_jsonschema` was originally + (mis)placed in the call protocol as a schema-only placeholder; ADR-027 + moved it to the HTTP host crate as a real HTTP-backed adapter. See Adapter Location Map. - **No secret material on the wire.** `ConnectionCredentials` carries vault-derived material for the *outbound* connection (TLS identity); `auth_token` is a @@ -729,7 +746,7 @@ Based on the gap analysis and the downstream unblock chain: not a singular connection overlay. `invoke_peer()` routes to the right peer via `PeerRef::Specific` / `PeerRef::Any` (ADR-024 §1-2). - **`from_call` is a manual free function.** The assembly layer calls it - after the dial (in `AlknetClient`). The overlay is per-connection so + after the dial (in the composition layer's dial helper, `AlknetClient` — today the alknet glue; OQ-43). The overlay is per-connection so re-import on reconnect is naturally scoped (DC-2, OQ-12). See [ADR-028](decisions/028-from-call-manual-free-function.md). - **`from_call` namespace collision is same-peer only.** Cross-peer collision @@ -739,7 +756,7 @@ Based on the gap analysis and the downstream unblock chain: - **`OperationAdapter::import()` returns `Result`.** Failures surface as `AdapterError` (DC-4, OQ-11). - **MCP stdio transport is not built.** Streamable HTTP is the only supported - MCP transport in alknet. stdio = spawn arbitrary executable = built-in RCE. + MCP transport in the alk stack. stdio = spawn arbitrary executable = built-in RCE. Recorded as an explicit security position, not a feature gap. - **Pure-client X.509 connections are not in the peer graph on the client side.** A `CallClient` connection to a public X.509 endpoint with no @@ -749,10 +766,10 @@ Based on the gap analysis and the downstream unblock chain: invoked through the `CallConnection` handle. The client-side `ServerCertVerifier` uses CA verification (`WebPkiServerVerifier`) for such remotes; known peers (hub with `PeerEntry`) use fingerprint - pinning. See [ADR-034](/workspace/@alkdev/alknet/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md). + pinning. See [ADR-054](decisions/054-outgoing-only-x509-three-remote-roles.md). - **`ConnectionCredentials.remote_identity: None` is load-bearing.** `None` means "no `PeerEntry` for this remote → use CA verification (X.509) - or fail closed (Ed25519 raw key)" per the ADR-034 §3 verifier rule. + or fail closed (Ed25519 raw key)" per the verifier rule (ADR-054). The implementation must not default `remote_identity` to a placeholder to satisfy the field, and must not treat `None` as "skip verification" — `None` + X.509 is CA verification, `None` + raw key is a hard @@ -763,11 +780,11 @@ Based on the gap analysis and the downstream unblock chain: | Decision | ADR | Summary | |----------|-----|---------| | Call protocol client and adapter contract | [ADR-022](decisions/022-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction; trait is async; adapters produce `HandlerRegistration` bundles | -| `from_jsonschema` as HTTP-backed single-endpoint adapter in alknet-http | [ADR-027](decisions/027-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf | +| `from_jsonschema` as HTTP-backed single-endpoint adapter | [ADR-027](decisions/027-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from the call protocol (broken schema-only placeholder) to the HTTP host crate as a real reqwest-backed single-endpoint adapter — today `alkhttp` (ex-`alknet-http`); `FromJsonSchema` provenance stays in alkcall as a leaf | | Peer-graph routing model (DC-1, supersedes ADR-023) | [ADR-024](decisions/024-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via existing `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` | | PeerEntry and Identity.id decoupling | [ADR-025](decisions/025-peerentry-and-identity-id-decoupling.md) | `PeerId` source changes from UUID to `Identity.id` (= `PeerEntry.peer_id`, stable across key rotation); `Identity.id` decoupled from crypto material on the fingerprint path | | Forwarded-for identity | [ADR-026](decisions/026-forwarded-for-identity.md) | `forwarded_for` field on `call.requested` and `OperationContext`; the `from_call` handler populates it; metadata only, never used by `AccessControl::check` | -| Storage boundary and repo/adapter pattern | [alknet ADR-033](/workspace/@alkdev/alknet/docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | +| Storage boundary and repo/adapter pattern | [alknet ADR-033](/workspace/@alkdev/alknet/docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md) (provenance) | Core defines repo traits + in-memory defaults; persistence adapters are separate crates (composition-layer work — OQ-43) | | Secret material flow and capability injection | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | The no-env-vars invariant's foundation; capabilities injected at assembly layer | | Handler registration, provenance, and composition authority | [ADR-018](decisions/018-handler-registration-provenance-and-composition-authority.md) | The registration bundle adapters produce; `composition_authority: None` for leaves | | Operation registry layering | [ADR-019](decisions/019-operation-registry-layering.md) | Layer 2 per-connection overlay where `from_call` imports land | @@ -776,11 +793,11 @@ Based on the gap analysis and the downstream unblock chain: | Operation error schemas | [ADR-016](decisions/016-operation-error-schemas.md) | `error_schemas` mirrored by `from_call` from remote op's spec | | Streaming handler for subscriptions | [ADR-021](decisions/021-streaming-handler-for-subscriptions.md) | `from_call` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`) that calls `CallConnection::subscribe()` and forwards the remote stream; `Query`/`Mutation` stay `HandlerKind::Once` | | TLS identity redesign | [alknet ADR-027](/workspace/@alkdev/alknet/docs/architecture/decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | RFC 7250 raw key / X.509 cert dimensions of the local `TlsIdentity` (now carried by `ConnectionCredentials.local_identity`) | -| Outgoing-only X.509 and three peer roles | [alknet ADR-034](/workspace/@alkdev/alknet/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Public X.509 endpoint is not a `PeerEntry` on the client side (no `PeerId`, not in peer graph); client-side verifier by `PeerEntry` presence (CA vs fingerprint pin); hub = mixed-fingerprint `PeerEntry` | -| HD derivation for encryption keys | [alknet ADR-020](/workspace/@alkdev/alknet/docs/architecture/decisions/020-hd-derivation-for-encryption-keys.md) | Vault-derived TLS identity material | -| Vault key model | [alknet ADR-026](/workspace/@alkdev/alknet/docs/architecture/decisions/026-vault-key-model-hd-derivation.md) | Vault-derived TLS identity material | -| Vault local-only dispatch | [alknet ADR-025](/workspace/@alkdev/alknet/docs/architecture/decisions/025-vault-local-only-dispatch.md) | Vault access at assembly layer only; the credential injection path's first hop | -| Crate decomposition | [ADR-031](decisions/031-crate-decomposition.md) | `alknet-http` depends on `alknet-call` (protocol-foundation exception, noted in Adapter Location Map) | +| Outgoing-only X.509 and three remote roles | [ADR-054](decisions/054-outgoing-only-x509-three-remote-roles.md) (re-owned; provenance: [alknet ADR-034](/workspace/@alkdev/alknet/docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md)) | Public X.509 endpoint is not a `PeerEntry` on the client side (no `PeerId`, not in peer graph); client-side verifier by `PeerEntry` presence (CA vs fingerprint pin); hub = mixed-fingerprint `PeerEntry` | +| HD derivation for encryption keys | [alknet ADR-020](/workspace/@alkdev/alknet/docs/architecture/decisions/020-hd-derivation-for-encryption-keys.md) (provenance — vault ADRs live in the source repo) | Vault-derived TLS identity material | +| Vault key model | [alknet ADR-026](/workspace/@alkdev/alknet/docs/architecture/decisions/026-vault-key-model-hd-derivation.md) (provenance) | Vault-derived TLS identity material | +| Vault local-only dispatch | [alknet ADR-025](/workspace/@alkdev/alknet/docs/architecture/decisions/025-vault-local-only-dispatch.md) (provenance) | Vault access at assembly layer only; the credential injection path's first hop | +| Crate decomposition | [ADR-031](decisions/031-crate-decomposition.md) | `alkhttp` depends on alkcall (protocol-foundation exception, noted in Adapter Location Map) | | One-way door decision framework | [ADR-032](decisions/032-one-way-door-decision-framework.md) | Door-type classification for DC-1..4 | ## Open Questions @@ -827,7 +844,7 @@ See [open-questions.md](open-questions.md) for full details. `PeerEntry` supports multiple credential paths (fingerprints + auth_token_hash), `ApiKeyEntry` is for tokens that ARE the identity. See OQ-20 in open-questions.md. -- **OQ-21** (resolved, alknet ADR-034 — not ported): X.509 outgoing-only +- **OQ-21** (resolved by ADR-054): X.509 outgoing-only case — three remote roles named (public X.509 endpoint, transport relay, hub). `PeerEntry` asymmetry is correct: a pure-client connection to a public @@ -837,10 +854,10 @@ See [open-questions.md](open-questions.md) for full details. land in the connection's Layer 2 overlay and are invoked through the connection handle. The client-side `ServerCertVerifier` is selected by `PeerEntry` presence: known peer → fingerprint pin; unknown X.509 - remote → CA verification (`WebPkiServerVerifier`). The deciding ADR - (alknet ADR-034, "Outgoing-Only X.509 and Three Peer Roles") was not - ported into alkcall; the surviving alkcall surface is ADR-025's - `PeerEntry` asymmetry. See OQ-21 in open-questions.md. + remote → CA verification (`WebPkiServerVerifier`). The rule is + re-owned in-crate by ADR-054 (provenance: alknet ADR-034, "Outgoing-Only + X.509 and Three Peer Roles", not ported); ADR-025's + `PeerEntry` asymmetry is the registry surface. See OQ-21 in open-questions.md. ## References @@ -852,11 +869,16 @@ See [open-questions.md](open-questions.md) for full details. model (the server-side complement to this document) - `operation-registry.md` — `HandlerRegistration`, provenance, capability injection, service discovery (the discovery API `from_call` consumes) -- `docs/research/alknet-call-completion/gap-analysis.md` — DC-1..4, the - implementation-state audit, the downstream unblock chain -- `docs/research/alknet-call-peer-routing/findings.md` — the peer-graph - routing research that identified ADR-023's structural gap and validated - the ADR-024 design via POC +- Source-repo research (alknet, provenance — these paths live in + `/workspace/@alkdev/alknet/docs/research/`, not this repo): + - `alknet-call-completion/gap-analysis.md` — DC-1..4, the + implementation-state audit, the downstream unblock chain + - `alknet-call-peer-routing/findings.md` — the peer-graph + routing research that identified ADR-023's structural gap and validated + the ADR-024 design via POC + - `alknet-ssh/phase-0-findings.md` — SSH Phase 0; confirms ssh depends on + the vendored core types, not alkcall's adapters, so it proceeds in + parallel with the completion - `/workspace/@alkdev/operations/` — TypeScript prior art (`from_openapi.ts`, `from_mcp.ts`, `from_schema.ts`, `scanner.ts`) - `/workspace/@alkdev/dispatch/` — concrete downstream consumer (container @@ -864,7 +886,4 @@ See [open-questions.md](open-questions.md) for full details. - `/workspace/aisdk/` — downstream consumer (Rust port of Vercel AI SDK); the no-env-vars invariant makes its `std::env::var` reads unreachable - `/workspace/rust-sdk/` — MCP Rust SDK (rmcp); streamable HTTP transport for - `alknet-http`'s `from_mcp`/`to_mcp` (separate crate, separate Phase 0) -- `docs/research/alknet-ssh/phase-0-findings.md` — alknet-ssh Phase 0; - confirms ssh depends on alknet-core not alknet-call's adapters, so it - proceeds in parallel with this completion \ No newline at end of file + `alkhttp`'s `from_mcp`/`to_mcp` \ No newline at end of file diff --git a/docs/architecture/decisions/009-bistream-as-the-handler-leaf.md b/docs/architecture/decisions/009-bistream-as-the-handler-leaf.md index 638a7a1..7b5d207 100644 --- a/docs/architecture/decisions/009-bistream-as-the-handler-leaf.md +++ b/docs/architecture/decisions/009-bistream-as-the-handler-leaf.md @@ -49,7 +49,7 @@ handler picks a joining strategy per-handler: | 1 | `BiStream` trait (`AsyncRead + AsyncWrite + Send + Unpin`) | `crates/alknet-core/src/types.rs:226` | **Vestigial in code.** Declared per ADR-005, named in ADR-008 as "a client-side / test convenience trait," but grep across the workspace finds **zero** consumers — no `impl BiStream`, no `dyn BiStream`, no `Box`. The trait is the ecosystem convention (`tokio::net::TcpStream`, `TlsStream`, `russh::Channel::into_stream()` all satisfy it natively) but it was never wired in. | | 2 | `Connection` (yields `(SendStream, RecvStream)` via `accept_bi`) | `crates/alknet-core/src/types.rs:507` | The handler-facing abstraction. Leaf is split. | | 3 | `SendStream` (AsyncWrite-only) + `RecvStream` (AsyncRead-only) | `crates/alknet-core/src/types.rs:228-294` | The actual leaves handlers receive. Each carries a quinn/iroh/generic enum (`SendStreamKind` / `RecvStreamKind`) and dispatches per-call. | -| 4 | `WsStream` trait (recv/send `axum::ws::Message`) | `crates/alknet-http/src/websocket/upgrade.rs:44` | Bypasses `Connection` entirely. The WS session runs its own dispatch loop directly over `axum::extract::ws::WebSocket`; `CallConnection::new_overlay_only` is used instead of `Connection::from_bidi`. ADR-044/048 already say "a WS message stream is another `BiStream`-satisfying transport" — the code does not. | +| 4 | `WsStream` trait (recv/send `axum::ws::Message`) | `crates/alknet-http/src/websocket/upgrade.rs:44` | Bypasses `Connection` entirely. The WS session runs its own dispatch loop directly over `axum::extract::ws::WebSocket`; `CallConnection::new_overlay_only` is used instead of `Connection::from_bidi`. alknet ADR-044/048 (alknet space, provenance) already say "a WS message stream is another `BiStream`-satisfying transport" — the code does not. | | 5 | `MpscSendStream` / `MpscRecvStream` (channels POC) | `/workspace/alknet-channels-poc/src/mpsc_stream.rs` | Split mpsc-backed halves fed to `Connection::from_stream`. The channels POC's `TunnelHandler` consumes them directly as two `tokio::io::copy` pumps — the split shape is right for the tunnel, wrong for HTTP. | The two Phase 6 issues are symptoms of one root: **the leaf type is @@ -108,21 +108,21 @@ accepts any of these directly, no per-handler wrapper. ### The two-pump shape is unaffected -The tunnel handler (ADR-078) and the SSH `direct-tcpip` handler +The tunnel handler (alknet ADR-078, provenance — in-crate: ADR-050) and the SSH `direct-tcpip` handler (future) use the split shape — two `tokio::io::copy` pumps, one per direction. With `BiStream` as the leaf, these handlers call `tokio::io::split(bidi)` to get `(ReadHalf, WriteHalf)` — the same stdlib idiom `tokio::io::split` already provides for `TcpStream` and `TlsStream`. The split is a stdlib call at the handler -boundary, not a per-handler trait wrapper. ADR-078's +boundary, not a per-handler trait wrapper. alknet ADR-078's (alknet space, provenance) shutdown-on-completion contract applies to the `ReadHalf`/`WriteHalf` unchanged. -### The TTY named-sub-streams case (ADR-038, ADR-077) +### The TTY named-sub-streams case (ADR-038; alknet ADR-077, provenance — TTY inside channels) ADR-038 specifies `into_sub_streams()` returning `Vec<(u8, SubStreamHandle)>` where `SubStreamHandle` is -`Send(SendStream) | Recv(RecvStream)`. ADR-077's TTY-inside-channels +`Send(SendStream) | Recv(RecvStream)`. alknet ADR-077's (alknet space, provenance) TTY-inside-channels mode destructures into five named handles (`stdin`, `stdout`, `stderr`, `ctrl_in`, `ctrl_out`). **Every stream_type is unidirectional** (ADR-034) — the typed-sub-stream leaves are @@ -306,13 +306,13 @@ The `WsStream` trait (`upgrade.rs:44-49`), the bespoke `drive_ws_session` loop, the `handle_inbound_envelope` / `dispatch_envelope_to_pending` helpers, and the `run_ws_session` glue are removed. The session's wire-level invariants (binary-only, protocol-level close on text, -`fail_all` pending on disconnect, ADR-048's `EventEnvelope` framing) +`fail_all` pending on disconnect, alknet ADR-048's `EventEnvelope` framing (alknet space, provenance)) move into `WsBidiStream`'s `AsyncRead` / `AsyncWrite` / `poll_shutdown` impls and the standard call-protocol dispatch path. `CallConnection::new_overlay_only` stays — it's the connection-local-overlay construction for non-peer clients -(ADR-034 §4, ADR-044 §5), orthogonal to the transport seam. What +(alknet ADR-034 §4, alknet ADR-044 §5 — alknet space, provenance), orthogonal to the transport seam. What changes is that the WS session feeds it through a `Connection` rather than a parallel `WsStream` trait. @@ -330,7 +330,7 @@ async fn accept_bi(&self) -> Result { `into_sub_streams()` is unchanged in shape — it still returns `Vec<(u8, SubStreamHandle)>` with `SubStreamHandle::Send(SendStream) | Recv(RecvStream)`, because TTY's named sub-streams are unidirectional -(ADR-034, ADR-077). The two paths (`accept_bi` for handlers that want +(ADR-034; alknet ADR-077, provenance). The two paths (`accept_bi` for handlers that want the joined pair, `into_sub_streams` for handlers that want the typed unidirectional sub-streams) are preserved per ADR-038. @@ -348,7 +348,7 @@ The `webtransport.md` spec describes the "VPN-like without being a VPN" path: a browser opens a WebTransport session to `/alk/ssh`, the h3 handler hands each bidi stream to `SshAdapter::handle` as a `Connection`, the browser's WASM SSH parser speaks SSH over the -stream. WebTransport is deferred per ADR-044. +stream. WebTransport is deferred per alknet ADR-044 (alknet space, provenance). With `BiStream` as the leaf, the same path exists over WebSocket in v1: a browser opens a WS connection, `WsBidiStream` presents it as a @@ -356,7 +356,7 @@ v1: a browser opens a WS connection, `WsBidiStream` presents it as a runs the channels demux over it, each channel's `accept_bi` yields a `BiStream` that `SshAdapter::handle` receives. The WASM SSH parser runs over a `BiStream`-over-WS-message adapter on the browser side -(the same `WsBidiStream` shape, browser-implemented). ADR-044/048's +(the same `WsBidiStream` shape, browser-implemented). alknet ADR-044/048's (alknet space, provenance) "WS message stream is another `BiStream`-satisfying transport" becomes literal — the code does what the spec said. @@ -379,7 +379,7 @@ ALPN-stream-proxy (ADR-040) is the channels-over-WebTransport shape, not an `alknet-http` shape. This is out of scope for this ADR (WebTransport is deferred per -ADR-044). It is recorded here because the unification is what makes +alknet ADR-044, alknet space — provenance). It is recorded here because the unification is what makes the future extraction clean: once `accept_bi` returns `BiStream`, the `h3` handler's "accept a WebTransport session, yield each stream as a `BiStream` to the ALPN handler" shape is the same code as @@ -405,7 +405,7 @@ redesign. unidirectional sub-streams are the case that justifies keeping `SendStream` / `RecvStream` (as thin newtypes, not quinn-welded enums). -- **The two-pump pattern** (ADR-078) — preserved. Tunnel/SSH handlers +- **The two-pump pattern** (alknet ADR-078, provenance — in-crate: ADR-050) — preserved. Tunnel/SSH handlers call `tokio::io::split(bidi)` for their two pump halves; the shutdown-on-completion contract applies to the `ReadHalf` / `WriteHalf` unchanged. @@ -436,7 +436,7 @@ redesign. lines of WS-specific dispatch glue are removed from `alknet-http/websocket/upgrade.rs`. The WS session runs through `Connection::from_bidi` + the call-protocol handler like any other - transport. ADR-044/048's "WS message stream is `BiStream`-satisfying" + transport. alknet ADR-044/048's (alknet space, provenance) "WS message stream is `BiStream`-satisfying" becomes literal. - One abstraction (`BiStream`) replaces five. The leaf type matches the ecosystem convention (`russh::Channel::into_stream()`, `TcpStream`, @@ -550,13 +550,13 @@ compilable: `Connection::from_bidi` + the call-protocol handler. This is the largest single change and can land after (1)-(3) — the WS path is independent of the handler call-site updates. -5. **Update ADR-007, ADR-008, ADR-038, ADR-077** to reflect the +5. **Update ADR-007, ADR-008, ADR-038, alknet ADR-077 (provenance)** to reflect the `BiStream` return shape. ADR-007's `from_stream` constructor is removed; `from_bidi` is the only public stream constructor (the rule: the split never crosses a crate boundary as part of a constructor). ADR-008's `accept_bi` return type is amended. ADR-038's `ChannelBidiStreamSource::accept_bi` return type is amended; - `into_sub_streams()` is unchanged. ADR-077's two-mode TTY design is + `into_sub_streams()` is unchanged. alknet ADR-077's (alknet space, provenance) two-mode TTY design is unchanged (the modes differ in *how* the adapter gets sub-streams, not in the leaf type). 6. **Update `findings.md` Phase 6.** The "deferred" status is @@ -602,12 +602,12 @@ fix (unify the leaf so the wrapper moves into core). `from_source` extension point are preserved) - ADR-038: `ChannelBidiStreamSource` (amended — `accept_bi` returns `BiStream`; `into_sub_streams()` is unchanged) -- ADR-077: TTY inside channels (unchanged — the two-mode design is +- alknet ADR-077 (provenance): TTY inside channels (unchanged — the two-mode design is preserved; the modes differ in how the adapter gets sub-streams, not in the leaf type) -- ADR-078: two-pump shutdown-on-completion (unchanged — the contract +- alknet ADR-078 (provenance — in-crate: ADR-050): two-pump shutdown-on-completion (unchanged — the contract applies to `tokio::io::split(bidi)` halves) -- ADR-044, ADR-048: WebSocket is the v1 browser bidirectional path +- alknet ADR-044/048 (alknet space, provenance): WebSocket is the v1 browser bidirectional path (this ADR makes the "WS message stream is `BiStream`-satisfying" claim literal) - `docs/research/alknet-crate-extraction/findings.md` Phase 6 — the diff --git a/docs/architecture/decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md b/docs/architecture/decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md index ee41125..1718d2b 100644 --- a/docs/architecture/decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md +++ b/docs/architecture/decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md @@ -2,7 +2,7 @@ ## Status -Accepted (resolves OQ-42; amends the `AccessControl::check` signature and +Accepted (resolves alknet OQ-42 — source-repo space, provenance; the question is retracked in-crate as OQ-30 — amends the `AccessControl::check` signature and adds `OperationSpec.resource_id_path`. Does not amend ADR-017 or ADR-018 — specific #4 confirms the composition authority stays static. Blocks lifted: the alknet-docker, alknet-tty, opencode-runner wrapper, and @@ -36,7 +36,7 @@ None of those crate specs can declare their `AccessControl` shapes until the core model answers: **how does `AccessControl::check` learn whether identity X owns runtime-spawned resource R?** -OQ-42 tracked this. The OQ resolved five sub-questions: +alknet OQ-42 (source-repo space, provenance; in-crate: OQ-30) tracked this. The OQ resolved five sub-questions: 1. Storage shape — reuse the repo/adapter pattern (ADR-033). 2. Integration point — `AccessControl::check` consults an ownership @@ -88,7 +88,7 @@ model has no path from that knowledge to `check`. ### The options considered -Three integration points were considered (see OQ-42 for the full +Three integration points were considered (see alknet OQ-42, in-crate OQ-30, for the full reasoning): - **Option 1 — augment `Identity.resources` with a per-request snapshot.** @@ -592,7 +592,7 @@ provider confirms it owns the container (dynamic). ## References -- OQ-42: Dynamic Resource Ownership for Runtime-Spawned Resources +- alknet OQ-42 (provenance; in-crate: OQ-30): Dynamic Resource Ownership for Runtime-Spawned Resources (resolved by this ADR — the five sub-questions this ADR writes into decision text) - ADR-003: Auth as Shared Core (`IdentityProvider` — the first instance diff --git a/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md b/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md index 5424e3d..482c709 100644 --- a/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md +++ b/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md @@ -617,7 +617,7 @@ existing code (which already has the right structure — The door-type classification is updated: `spawn_dispatch` is one-way (the handler-facing surface); ~~`connect` is two-way (additive convenience)~~ `connect` is **removed** (ADR-045 §5). The -`AlknetClient` extraction (OQ-55) is **resolved** by ADR-045 — the +`AlknetClient` extraction (alknet OQ-55) is **resolved** by ADR-045 — the shared dial seam is `alknet-client`; `spawn_dispatch` is the protocol-crate take-over that consumes the dial's `Connection`. diff --git a/docs/architecture/decisions/027-from-jsonschema-as-http-adapter.md b/docs/architecture/decisions/027-from-jsonschema-as-http-adapter.md index 11efcb0..2cc526c 100644 --- a/docs/architecture/decisions/027-from-jsonschema-as-http-adapter.md +++ b/docs/architecture/decisions/027-from-jsonschema-as-http-adapter.md @@ -177,4 +177,16 @@ endpoint. and no-env-vars invariant ([ADR-010](010-secret-material-flow-and-capability-injection.md)) established by `from_openapi`. - Reuses `HttpServiceConfig` and `SharedHttpClient` from - `from_openapi` (in `alknet-http`). \ No newline at end of file + `from_openapi` (in `alknet-http`). + +## Amendment 2026-10-13: name normalization (`alknet-http` → `alkhttp`) + +This ADR's decision text was authored when the HTTP host crate was +named `alknet-http` inside the alknet mono-repo. The HTTP host is now +the standalone sibling crate **`alkhttp`**, which wraps alkcall +(extraction context: ADR-031; the adapter-location rule is unchanged — +the trait lives in alkcall, HTTP-backed adapters live in `alkhttp`). +The title's "in alknet-http" and the body's crate-name references +should be read as "in `alkhttp`"; the References below link to the +source-repo originals (provenance). The `FromJsonSchema` provenance +variant remains in alkcall. \ No newline at end of file diff --git a/docs/architecture/decisions/031-crate-decomposition.md b/docs/architecture/decisions/031-crate-decomposition.md index f909b5f..6b800a0 100644 --- a/docs/architecture/decisions/031-crate-decomposition.md +++ b/docs/architecture/decisions/031-crate-decomposition.md @@ -140,4 +140,31 @@ The dead `irpc` / `irpc-derive` workspace deps and the `alknet-call` consumer dep were removed in commit `668d777`. See [ADR-014](014-irpc-never-integrated-hand-rolled-framing.md) for the full record (ADR-013, which accepted "irpc as the call protocol foundation," is -superseded). \ No newline at end of file +superseded). +### Amendment 4 (2026-10-13): post-extraction state — alkcall is the unified protocol crate; sibling names normalized + +The decomposition this ADR decided has been executed; this section +records where the named crates now live, so the decision text's +mono-repo framing reads as history: + +- **alkcall** (this crate) is the unified extraction of `alknet-call` + + `alknet-channels` + the vendored core types (the `alknet-core` + surfaces this crate's protocol depends on: `Connection`, + `BiStream`, `ProtocolHandler`, `AuthContext`, `IdentityProvider`, + `AuthToken`, `Capabilities`, `OwnershipProvider`, `HandlerError`, + `StreamError`). Two subsystems: `registry`/`protocol` (call) and the + channels subsystem (ADR-044's `channels-core`/`channels-call` split + landed as modules here). +- **Sibling host crates** (now standalone, all depending on alkcall): + `alknet-http` → `alkhttp`; `alknet-tls` → `alktls`; + `alknet-tty` → `alktty`; `alknet-tunnels` → `alktunnels`; + `alknet-trader` → `alktrader`. +- **The mono-repo (alknet) is being replaced** by thin glue over + alkcall and its siblings; its remaining roles (dial/accept helper + `AlknetClient`, endpoint layer, hub composition) move to the + composition layer — post-replacement owner and shape tracked as + OQ-43 in this crate's question tracker. +- The protocol-foundation rule (Amendment 1) carries over unchanged + with current names: `alkhttp` depends on alkcall for operation + registry / adapter / client types; peer-handler crates (e.g. + `alktty`, `alktunnels`, `alktrader`) depend on alkcall only. diff --git a/docs/architecture/decisions/034-channels-wire-format.md b/docs/architecture/decisions/034-channels-wire-format.md index 7da4abb..b4183dc 100644 --- a/docs/architecture/decisions/034-channels-wire-format.md +++ b/docs/architecture/decisions/034-channels-wire-format.md @@ -22,7 +22,7 @@ JSON, tunnel's raw bytes, SSH's channel protocol). The stream_type decomposition (unidirectional halves, mod 3 formula, 85 groups) is **removed from the channels layer**. The stream_type concept -survives in TTY's 5-byte format (ADR-052, amended by Phase 7), which the +survives in the TTY 5-byte format (alknet ADR-052, provenance — amended by Phase 7), which the channels layer carries transparently in its payload. The total header for a TTY chunk inside channels is 13 bytes (8 channels + 5 TTY), not 9; the two length fields are close but not identical @@ -47,7 +47,7 @@ Two prior formats inform this design: 1. **SSH's channel multiplexer (RFC 4254)** — `ChannelId(u32)` with string-named types negotiated per channel, all traffic interleaved on one encrypted transport stream. -2. **alknet-tty's chunk format (ADR-052)** — `[stream_type: u8][length: u32 be] +2. **alknet-tty's chunk format (alknet ADR-052, provenance)** — `[stream_type: u8][length: u32 be] [payload]`, a fixed set of four sub-streams (stdin/stdout/stderr/control) within one bidi stream. Validated by two POCs and in production code. @@ -100,18 +100,18 @@ client→server) and 4 (read, server→client), not one "bidirectional" 3. ``` 9 bytes of header. The `channel_id` is the addition over TTY's 5-byte -format; `stream_type` and `length` are identical to TTY's fields (ADR-052), +format; `stream_type` and `length` are identical to TTY's fields (alknet ADR-052, provenance), preserving the framing-disambiguation soundness property. | field | width | meaning | |-------|-------|---------| | `channel_id` | u32 BE | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open`. | | `stream_type` | u8 | The unidirectional sub-stream within the channel. See "Stream types" below. | -| `length` | u32 BE | The payload length in bytes. 0 = EOF sentinel (same convention as TTY — ADR-052 §Sentinels). | +| `length` | u32 BE | The payload length in bytes. 0 = EOF sentinel (same convention as TTY — alknet ADR-052 §Sentinels (provenance)). | ### `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, provenance). A chunk with `length > MAX_CHUNK_LEN` returns `ChunkTooLarge` and does not corrupt the stream — the demux drops the chunk and continues. The header is always exactly 9 bytes, so the demux can always resync by reading the next 9-byte @@ -214,7 +214,7 @@ the cost of uniformity. For the intended use cases (TTY, SSH, tunnels, call operations), this is noise. The high-throughput escape hatch is multi-connection, not stripping the header. -### Framing disambiguation (carried from ADR-052 §5) +### Framing disambiguation (carried from alknet ADR-052 §5, provenance) Channel 0 is just another channel — its chunks have `channel_id=0` in the header. Disambiguation between channel 0 (call protocol) and data channels @@ -225,7 +225,7 @@ first byte of a chunk payload from the server is unambiguous. ### Zero-length sentinel = EOF A zero-length chunk is delivered as an empty `Bytes`, which the reassembled -stream interprets as EOF (same convention as TTY — ADR-052 §Sentinels). This +stream interprets as EOF (same convention as TTY — alknet ADR-052 §Sentinels (provenance)). This is the clean-shutdown signal for a `(channel_id, stream_type)` pair. ### Sync core / async shell split @@ -251,7 +251,7 @@ tokio-dependent shell. - The hub relay correlates by `channel_id` in the header, uniformly across substrates — no stream-ID-to-`channel_id` mapping per leg. - WASM-compatible by construction — the pure core has no platform deps. -- The framing-disambiguation property from ADR-052 carries forward unchanged. +- The framing-disambiguation property from alknet ADR-052 (provenance) carries forward unchanged. **Negative:** - 9 bytes per chunk on all substrates, including QUIC where the transport @@ -288,7 +288,7 @@ within the one-way format. wire format is 8 bytes, not 9; `stream_type` removed from the channels header; the stream_type decomposition is removed from the channels layer; the handler owns its sub-stream multiplexing on the `BiStream`) -- ADR-052: alknet-tty wire format (the 5-byte format this generalizes; +- Source-repo provenance (not ported): alknet ADR-052 — alknet-tty wire format (the 5-byte format this generalizes; amended by ADR-077 — scoped to direct TTY; **re-amended by ADR-035 — TTY always uses its 5-byte format, carried transparently in the channels payload**) diff --git a/docs/architecture/decisions/035-channels-pure-channel-multiplexing.md b/docs/architecture/decisions/035-channels-pure-channel-multiplexing.md index 68b87c2..c13b9b3 100644 --- a/docs/architecture/decisions/035-channels-pure-channel-multiplexing.md +++ b/docs/architecture/decisions/035-channels-pure-channel-multiplexing.md @@ -76,7 +76,7 @@ on the `BiStream` the channels layer gives them. framing byte, carried transparently. - **The control channel is handler-internal.** TTY sub-demuxes control from its io `BiStream` using its 5-byte format (`STREAM_CTRL_IN = 3`, - `STREAM_CTRL_OUT = 4` — ADR-052 amended by Phase 7). The channels layer + `STREAM_CTRL_OUT = 4` — alknet ADR-052 (provenance), amended by Phase 7). The channels layer doesn't carry control. The "control isn't actually bidirectional" flaw is fixed at the TTY layer, not the channels layer. - **Recursive composition is literal.** A channel with ALPN @@ -165,7 +165,7 @@ Three things changed since ADR-034/074/077 were accepted: handler boundary — but there may be a generalized reason to expose it (recursive composition, test helpers, the hub relay's `channel_id` rewrite). The exact API shape is an implementation detail for the - channels crate, tracked as OQ-68. The *contract* — the channels layer + channels crate, tracked as alknet OQ-68 (source-repo space, provenance; in-crate: OQ-24). The *contract* — the channels layer strips its 8-byte header on read and the handler parses its own framing from the payload — is decided here; the *function surface* is not. - **TTY's `wire.rs` adaptation:** TTY's `ChunkReader` currently reads from @@ -174,10 +174,10 @@ Three things changed since ADR-034/074/077 were accepted: stream_type constants, length validation, control message parsing — is unchanged). This is an implementation concern for the channels + TTY integration, not an architecture decision. -- **Full channel-level flow-control windowing (OQ-56):** unchanged. The +- **Full channel-level flow-control windowing (alknet OQ-56, provenance; in-crate: OQ-23):** unchanged. The bounded-buffer backpressure (ADR-040) is the v1 mechanism; full windowing is an additive extension that doesn't change the wire - format. OQ-56 stays deferred(scope). + format. alknet OQ-56 stays deferred(scope) in the source repo; this crate tracks it as OQ-23. ## Decision @@ -194,7 +194,7 @@ handler's framing, carried transparently. | field | offset | width | meaning | |-------|--------|-------|---------| | `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). | -| `length` | 4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max `MAX_CHUNK_LEN` (16 MiB, matching TTY's cap — ADR-052 §5). | +| `length` | 4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max `MAX_CHUNK_LEN` (16 MiB, matching TTY's cap — alknet ADR-052 §5 (provenance)). | The `stream_type` byte is **removed** from the channels header. The channels layer has no `stream_type` concept — not in its header, not in @@ -206,7 +206,7 @@ tunnel's raw bytes, SSH's channel protocol). This amends ADR-034: the wire format is 8 bytes, not 9; the `stream_type` decomposition (mod 3, unidirectional halves, 85 groups) is removed from the channels layer. The stream_type concept survives in -TTY's 5-byte format (ADR-052, amended by Phase 7), which the channels +TTY's 5-byte format (alknet ADR-052, provenance — amended by Phase 7), which the channels layer carries transparently. ### 2. `into_sub_streams()` is removed; `accept_bi` is the only accessor @@ -430,7 +430,7 @@ inside-channels), re-introducing a separate inside-channels mode would be a rewrite of TTY's session driver. The trade is one unified session driver now vs. two-mode maintenance forever. -The add/strip API shape (OQ-68) is a **two-way door** — whether the +The add/strip API shape (alknet OQ-68, provenance; in-crate: OQ-24) is a **two-way door** — whether the header add/strip is built into the read/write path or exposed as a standalone utility is an implementation detail that can change without breaking the wire format or the handler contract. @@ -472,7 +472,7 @@ breaking the wire format or the handler contract. `open_channel` and `Channel`) - ADR-044: sub-crate decomposition (unchanged — 8-byte wire format in `channels-core`; call-protocol coupling in `channels-call`) -- ADR-052: alknet-tty wire format (the 5-byte format carried +- Source-repo provenance (not ported): alknet ADR-052 — alknet-tty wire format (the 5-byte format carried transparently in the channels payload; the control channel split from Phase 7 is TTY-internal) - `docs/research/stream-unification/findings.md` — the research that diff --git a/docs/architecture/decisions/038-channelconnection-bidistreamsource.md b/docs/architecture/decisions/038-channelconnection-bidistreamsource.md index 88ca5b7..e8b66cb 100644 --- a/docs/architecture/decisions/038-channelconnection-bidistreamsource.md +++ b/docs/architecture/decisions/038-channelconnection-bidistreamsource.md @@ -15,7 +15,7 @@ Every handler — TTY, tunnel, SSH, call — receives a `Connection`, calls `accept_bi()` once, gets a `BiStream`, and sub-multiplexes it however it wants. The "typed handler path" (this ADR's motivating case for TTY) is replaced by TTY sub-demuxing its `BiStream` via its own 5-byte format -(ADR-052) — the same code TTY runs in direct mode. The two-accessor +(alknet ADR-052, provenance) — the same code TTY runs in direct mode. The two-accessor design (`accept_bi` for generic handlers, `into_sub_streams` for typed handlers) collapses to one accessor. diff --git a/docs/architecture/decisions/040-backpressure-channel-limits-id-reuse.md b/docs/architecture/decisions/040-backpressure-channel-limits-id-reuse.md index cdca154..e8d81a8 100644 --- a/docs/architecture/decisions/040-backpressure-channel-limits-id-reuse.md +++ b/docs/architecture/decisions/040-backpressure-channel-limits-id-reuse.md @@ -109,7 +109,7 @@ per `ChannelManager` (`buffer_cap` field). This prevents memory exhaustion without the complexity of SSH's sliding-window protocol. Full channel-level windowing (SSH-style sliding-window per channel) is a -deferred extension, tracked as [OQ-56](/workspace/@alkdev/alknet/docs/architecture/questions/056-full-channel-level-flow-control-windowing.md) +deferred extension, tracked as [alknet OQ-56](/workspace/@alkdev/alknet/docs/architecture/questions/056-full-channel-level-flow-control-windowing.md) (deferred(scope)). It is blocked on a real deployment observing head-of- line blocking where the bounded-buffer mitigation is insufficient. The bounded-buffer decision is made; the extension is not. @@ -201,7 +201,7 @@ DoS defense summary. - Bounded-buffer backpressure does not eliminate head-of-line blocking — it bounds the memory cost. A slow consumer still stalls its own channel's demux reads. For the intended use cases (TTY, SSH, tunnels) this is - acceptable; full windowing is tracked as OQ-56 (deferred(scope)). + acceptable; full windowing is tracked as alknet OQ-56 (source-repo space, provenance; in-crate: OQ-23) (deferred(scope)). ## Door type diff --git a/docs/architecture/decisions/043-channelclient.md b/docs/architecture/decisions/043-channelclient.md index 4b8a319..8fbf932 100644 --- a/docs/architecture/decisions/043-channelclient.md +++ b/docs/architecture/decisions/043-channelclient.md @@ -62,7 +62,7 @@ surface: primary constructor and the one-way-door API. Takes a pre-established `Connection` (produced by any transport — TCP+TLS via `from_bidi`, WebTransport `BiStream`, SSH `direct-tcpip`, a quinn connection, a - WebSocket carrying `alk/channels` per ADR-044), installs channel 0, + WebSocket carrying `alk/channels` per alknet ADR-044 — the websocket-native-session ADR, provenance —), installs channel 0, spawns the demux/mux, returns the client. Mirrors the server-side `ChannelsAdapter::handle(Connection)` (substrate-agnostic) and the existing `CallClient::spawn_dispatch(Connection)` pattern. @@ -73,7 +73,7 @@ surface: The dial+TLS seam (the transport-specific work each dial helper does — verifier selection per ADR-034, handshake, produce a `Connection`) is the -correct scope of OQ-55's deferral. `AlknetClient` is the eventual shared +correct scope of alknet OQ-55's deferral. `AlknetClient` is the eventual shared *dial*; `from_connection` is the shared *channels-take-over*. Separating them now, before the one-way-door API is cast, is the point — not a deferral. The "QUIC-only initially" framing is removed; it was the @@ -81,7 +81,7 @@ anti-pattern this amendment corrects. The door-type classification is unchanged: `from_connection` is one-way (the handler-facing surface), `connect_quic` is two-way (additive -convenience). The `AlknetClient` extraction remains deferred (OQ-55) — but +convenience). The `AlknetClient` extraction remains deferred (alknet OQ-55) — but what is deferred is the shared *dial*, not a QUIC-welded client API. ## Context @@ -106,14 +106,14 @@ The phase-0 research (`docs/research/alknet-channels/phase-0-findings.md` seam — the transport-specific work (open socket, TLS handshake, ADR-034 verifier selection, produce a `Connection`) that each transport's dial helper rebuilds. Genuinely deferred — blocked on a second *transport's* - dial existing (OQ-55 tracks this correctly). A single QUIC dial + dial existing (alknet OQ-55 tracks this correctly). A single QUIC dial (`connect_quic`) does not give enough information to extract the transport-polymorphic dial seam; two different transport dials do. This ADR decides #1 — `ChannelClient`, with `from_connection` as the transport-agnostic primary constructor and `connect_quic` as a transport- specific dial helper. #2 (the shared `AlknetClient` dial+TLS seam) stays -deferred per OQ-55. +deferred per alknet OQ-55. ## Decision @@ -130,7 +130,7 @@ impl ChannelClient { /// Transport-agnostic primary constructor. Takes a pre-established /// `Connection` (any transport — TCP+TLS via `from_bidi`, /// WebTransport BiStream, SSH direct-tcpip, a quinn connection, a - /// WebSocket per ADR-044), installs channel 0 (alk/call), spawns + /// WebSocket per alknet ADR-044 — websocket-native session, provenance —), installs channel 0 (alk/call), spawns /// the demux/mux, and returns the client. Mirrors the server-side /// `ChannelsAdapter::handle(Connection)`. This is the one-way-door /// API surface — it must not be coupled to a transport (ADR-034, @@ -223,10 +223,10 @@ sense — it's one endpoint of a bidirectional channels connection. The name `ChannelClient` follows the `CallClient` convention (the side that dialed), not a request/response role. -### Relationship to `AlknetClient` (OQ-55 — deferred) +### Relationship to `AlknetClient` (alknet OQ-55 — deferred) `ChannelClient`'s *API* is transport-agnostic — `from_connection` takes -a pre-established `Connection`. What is deferred (OQ-55) is the shared +a pre-established `Connection`. What is deferred (alknet OQ-55) is the shared *dial+TLS* seam (`AlknetClient`): the transport-specific work each dial helper does — open a socket, run the TLS handshake, apply ADR-034's verifier-selection rule, produce a `Connection`. That dial is genuinely @@ -252,14 +252,14 @@ duplicated capability and not a QUIC-welded client API. matching the call protocol's `CallAdapter`/`CallClient` shape. - Bidirectionality is preserved — the client can both initiate and receive `channel/open`. -- The `AlknetClient` deferral (OQ-55) is not blocked by `ChannelClient` — +- The `AlknetClient` deferral (alknet OQ-55) is not blocked by `ChannelClient` — they are independent concerns. `ChannelClient` builds standalone; the core extraction happens later when the blocker clears. **Negative:** - Each transport-specific dial helper duplicates ~20 lines of verifier-selection boilerplate (from `connect_quic`). This is the known - cost of not extracting `AlknetClient` yet (OQ-55). Acceptable until the + cost of not extracting `AlknetClient` yet (alknet OQ-55). Acceptable until the second transport's dial exists, at which point `AlknetClient` extracts the shared dial+TLS seam. The `from_connection` API — the one-way-door surface — is unaffected; only the dial helpers carry the duplication. @@ -276,10 +276,10 @@ one-way-door primary constructor (transport-agnostic). `from_connection`. Adding, removing, or changing a dial helper is cheap and does not touch the one-way-door surface. -The `AlknetClient` extraction is a **deferred decision** (OQ-55, +The `AlknetClient` extraction is a **deferred decision** (alknet OQ-55, deferred(scope)), not a door-type attribute. Its door type is two-way (the extraction is a refactor, not a wire-format change), but it is not decided -in this ADR — see OQ-55 for the blocking condition. What is deferred is the +in this ADR — see alknet OQ-55 for the blocking condition. What is deferred is the shared *dial+TLS* seam; `from_connection`'s transport-agnostic contract is decided now. @@ -292,7 +292,7 @@ decided now. removed from `open_channel` and `Channel`; handler owns sub-stream multiplexing) - ADR-039: ChannelManager (the shared state `ChannelClient` holds) -- OQ-55: AlknetClient / client establishment extraction (the deferred core +- alknet OQ-55: AlknetClient / client establishment extraction (the deferred core concern this ADR does NOT block on) - `docs/research/alknet-channels/phase-0-findings.md` §OQ-CH-14 (the research-scope question this ADR carries forward) diff --git a/docs/architecture/decisions/044-channels-subcrate-decomposition.md b/docs/architecture/decisions/044-channels-subcrate-decomposition.md index 4a58220..4a14c7c 100644 --- a/docs/architecture/decisions/044-channels-subcrate-decomposition.md +++ b/docs/architecture/decisions/044-channels-subcrate-decomposition.md @@ -74,7 +74,7 @@ alknet-channels-core — the pure multiplexer (wire format, demux/mux, There are no `channels-hub` or `channels-worker` sub-crates. The hub and worker are **consumers** of channels, not sub-crates of it. The existing -`alknet-hub` crate (`docs/architecture/crates/hub/README.md`) IS the hub — +`alknet-hub` crate (source-repo path: docs/architecture/crates/hub/README.md — alknet, provenance) IS the hub — it depends on `channels-call` and uses the channels protocol as its substrate. A worker is just a worker — it depends on `channels-call` and uses `ChannelClient` (ADR-043) to dial. The hub relay logic (ADR-042) lives @@ -168,11 +168,11 @@ The dependency direction is: `alknet-hub` → `channels-call` → ### Relationship to `alknet-hub` -The existing `alknet-hub` crate (`docs/architecture/crates/hub/README.md`) +The existing `alknet-hub` crate (source-repo path: docs/architecture/crates/hub/README.md — alknet, provenance) is the hub pattern: peer lifecycle, aggregated env, service discovery. With channels as the substrate, `alknet-hub` gains a dependency on `channels-call` and incorporates the relay logic (ADR-042). The hub spec -(`crates/hub/README.md`) will be updated to reflect that the hub uses +(that source-repo doc) will be updated to reflect that the hub uses channels as its transport substrate — one channels connection per leg (browser↔hub, hub↔spoke), with the relay translating `channel/open` and byte-forwarding data channels. The hub's existing responsibilities (peer @@ -247,5 +247,15 @@ the dependency direction (hub/worker → channels, not channels → hub/worker). - ADR-043: ChannelClient (in `channels-call`; there is no `channels-worker` sub-crate — a worker is any crate that uses `ChannelClient` to dial) -- `docs/architecture/crates/hub/README.md` — the existing hub crate (the - relay's consumer) \ No newline at end of file +- alknet hub crate (source-repo path docs/architecture/crates/hub/README.md, provenance) — the existing hub crate (the + relay's consumer) +## Amendment 2026-10-13: the split landed inside alkcall, not as separate crates + +This ADR's decision text was authored when the channels protocol lived +in the mono-repo crate `alknet-channels` and framed the split as +sub-crates of it. The extraction (ADR-031, amendment 4) landed the +`channels-core` (pure multiplexer) / `channels-call` (call coupling: +channel 0 pre-negotiation, lifecycle ops, `ChannelCore`, `ChannelClient`) +decomposition as **modules inside alkcall** — downstream depends on +alkcall as a single crate. Hub and worker remain consumers (no +worker/hub sub-crate), as decided here. diff --git a/docs/architecture/decisions/045-alknetclient-native-dial-seam.md b/docs/architecture/decisions/045-alknetclient-native-dial-seam.md index a7b625d..b515ab2 100644 --- a/docs/architecture/decisions/045-alknetclient-native-dial-seam.md +++ b/docs/architecture/decisions/045-alknetclient-native-dial-seam.md @@ -1,8 +1,18 @@ # ADR-045: AlknetClient — the Native Client Dial Seam +> **Citation-space note (2026-10-13).** This ADR was authored in the +> alknet workspace before the extraction. Its `ADR-08x` references +> (endpoint types, `TlsClientConfig`, accept-loop runner, crypto +> provider) and its `OQ-55`/`OQ-58` references are **source-repo +> (alknet) space, not this crate's `decisions/`** — provenance; those +> ADRs were never ported. The verifier-selection rule formerly cited as +> "alknet ADR-034" is re-owned in-crate by ADR-054. This crate's own +> tracker/ADR numbers appear only where explicitly marked. + ## Status -Accepted (resolves OQ-55; §3 and §5 amended 2026-07-16 by ADR-012 — +Accepted (resolves alknet OQ-55 — source-repo space, provenance; +§3 and §5 amended 2026-07-16 by ADR-012 — the dial credential bundle is `ConnectionCredentials` (transport-level), not `CallCredentials` (call-protocol-level); all three dial signatures unify on `&ConnectionCredentials`; `dial_iroh`'s `node_id` parameter is @@ -81,7 +91,7 @@ contributing to the confusion that made `AlknetClient` hard to spec: (client). *Server-side accept vs. client-side dial.* The endpoint accepts connections and resolves identity from the incoming connection; the client dials and presents identity (client cert) - while verifying the remote (ADR-034). + while verifying the remote (alknet ADR-034, provenance). 3. **ALPN-level category** — endpoint ALPN / entry-point ALPN (ADR-086 §2). *Identity-gated vs. bootstrap, at the TLS layer.* @@ -143,7 +153,7 @@ The symmetry: |---------|---------------------------|-------------------------| | Transports | `with_quinn` / `with_iroh` / `with_tcp_tls` — pre-built by the assembly layer | `with_quinn` / `with_iroh` / `with_tcp_tls` — pre-built by the assembly layer | | Per-connection work | Accept → extract ALPN + fingerprint → `Connection` → `dispatch` | Dial → TLS handshake → `Connection` (ALPN + fingerprint carried) | -| Identity | Resolved *from* the incoming connection (fingerprint from client cert, or token on channel 0) | *Presented* (local `TlsIdentity` as client cert) + remote *verified* (ADR-034 — fingerprint pin or CA) | +| Identity | Resolved *from* the incoming connection (fingerprint from client cert, or token on channel 0) | *Presented* (local `TlsIdentity` as client cert) + remote *verified* (alknet ADR-034, provenance — fingerprint pin or CA) | | What it does NOT do | Run protocols — handlers do | Run protocols — `CallClient` / `ChannelClient` do | | Config | `TlsServerConfig` (per endpoint type, built by assembly) | `TlsClientConfig` (per-dial, built from `CallCredentials`) | @@ -168,7 +178,7 @@ pub struct AlknetClient { impl AlknetClient { /// QUIC dial. Builds a `TlsClientConfig` from `credentials` - /// (ADR-034 verifier selection + ADR-084 provider), dials `addr` + /// (alknet ADR-034 verifier selection, provenance + alknet ADR-084 provider), dials `addr` /// on `alpn`, returns a `Connection` via /// `Connection::from_quinn_with_alpn`. Feature-gated on `quinn`. #[cfg(feature = "quinn")] @@ -215,7 +225,7 @@ identity + auth token bundle, from `Capabilities`). The two rustls dials (QUIC, TCP+TLS) build a `TlsClientConfig` from the credentials; the iroh dial uses the raw `Ed25519SecretKey` directly. This mirrors the server side's "iroh shares the key, not the config" (ADR-082, ADR-087 -§3) — the consistency is in the rule (ADR-034 verifier selection), not +§3) — the consistency is in the rule (the verifier-selection rule — alknet ADR-034, provenance; re-owned in-crate as ADR-054), not in the type. > **Amendment 2026-07-16 (ADR-012):** The dial credential bundle is @@ -238,7 +248,7 @@ in the type. > is derived from `creds.remote_identity.fingerprint` > (`ed25519:` → `NodeId::from_bytes`), the same extraction pattern > the rustls dials use for the verifier. The consistency is now in both -> the rule (ADR-034) and the type. `CallCredentials` stays in +> the rule (alknet ADR-034, provenance) and the type. `CallCredentials` stays in > `alknet-call` as the call-protocol credential bundle; the > `auth_token` is a per-request field on `call.requested` payloads > (set by the caller or the `from_call` forwarding handler, resolved by @@ -492,7 +502,7 @@ several possible native clients sharing the same wire protocols. graph. - **The iroh dial is the exception.** It does not use `TlsClientConfig` — iroh has its own TLS. The dial helper applies - the same ADR-034 rule via iroh's API (NodeId match). The + the same verifier rule (alknet ADR-034, provenance) via iroh's API (NodeId match). The consistency is in the rule, not in the type. This is the same exception as the server side (ADR-082, ADR-087 §3) — unavoidable, and isolated to one dial method. @@ -554,4 +564,23 @@ dedicated ADR lands. - OQ-58 — worker registration flow (the HTTP path; `alk/register` is the native analogue) - `docs/architecture/crates/channels/channel-client.md` §"Relationship - to `AlknetClient`" — the deferral this ADR resolves \ No newline at end of file + to `AlknetClient`" — the deferral this ADR resolves + +## Amendment 2026-10-13: name normalization (`alknet-client` → the composition layer's dial helper) + +This ADR's decision text was authored when the dial helper lived in the +mono-repo crate `alknet-client`. The decision itself is name-independent +and unchanged: **the dial lives in the consumer/composition layer; the +protocol crates take over pre-established `Connection`s (`spawn_dispatch` +/ `from_connection`).** Current naming: + +- `AlknetClient` remains the dial helper's type name, but its host is + the **composition layer** (today the alknet glue, which is being + replaced). The post-replacement owner and shape of the composition + layer — including this dial seam — is tracked as OQ-43. +- `ConnectionCredentials` / `RemoteIdentity` (ADR-012) live with the + vendored core types in alkcall. +- The client-side verifier-selection rule this ADR's dial applies + (fingerprint pin vs CA vs fail-closed) is re-owned in-crate by + ADR-054; the alknet ADR-034 and ADR-084 references below are + provenance. \ No newline at end of file diff --git a/docs/architecture/decisions/054-outgoing-only-x509-three-remote-roles.md b/docs/architecture/decisions/054-outgoing-only-x509-three-remote-roles.md new file mode 100644 index 0000000..6eb1222 --- /dev/null +++ b/docs/architecture/decisions/054-outgoing-only-x509-three-remote-roles.md @@ -0,0 +1,125 @@ +# ADR-054: Outgoing-Only X.509 and the Three Remote Roles (Re-Owned In-Crate) + +## Status + +Accepted (re-owns the verifier-selection rule for alkcall's surface; +provenance: alknet ADR-034, not ported) + +## Context + +alkcall's connection surface depends on a verifier-selection rule and a +role taxonomy that were decided while this crate was a subdirectory of +the alknet mono-repo. The deciding ADR (alknet ADR-034, "Outgoing-Only +X.509 and Three Peer Roles") was never ported, so the rule's authority +lives in a cross-repo link — a dependency on a document outside this +crate for a constraint that binds this crate's public surface: + +- `ConnectionCredentials.remote_identity: None` semantics (ADR-022 §7, + (ADR-022 §7; alknet ADR-034 §2/§3, provenance): `None` is a load-bearing state — the + public-X.509-endpoint path, not a missing field. +- Client-side `ServerCertVerifier` selection is by `PeerEntry` + presence, not by key type alone (OQ-14 #2). +- `PeerEntry` asymmetry: a pure-client connection to a public X.509 + endpoint is not in the call-protocol peer graph on the client side + (no `PeerEntry`, no `PeerId`, no `PeerRef::Specific` routing). + +The alknet mono-repo is being rewritten as a thin glue layer over this +crate and its siblings; cross-repo ADR links will not survive that +rewrite as authority. The rule must be owned here. + +## Decision + +alkcall re-owns the rule in-crate. Three things are adopted: + +### 1. Three remote roles + +The single label "X.509 endpoint" conflated three structurally +different remote roles: + +- **Public X.509 endpoint** — a remote server reachable by domain + name, authenticated by a CA-issued X.509 cert. The local node is a + *client* of it (a third-party API, a public hub dialed over the + open internet, a peer that exposes domain + X.509 instead of an + Ed25519 raw key). The client authenticates to the server by bearer + token (`auth_token` payload field when riding the call protocol); + the server authenticates to the client by CA verification + (WebPKI), not fingerprint pinning. +- **Transport relay** — a connectivity-assistance node + (iroh's DERP-equivalent) that forwards encrypted datagrams between + peers who cannot directly connect. It is infrastructure, not an + application peer: no registered operations, no peer graph + participation, no `PeerEntry`. Its identity is the transport's + concern, not this crate's. +- **Hub / hosting node** — an application peer acting as a hub in a + hub-and-spoke topology. An ordinary `PeerEntry` holding mixed + fingerprints (Ed25519 raw key and/or X.509) — fingerprint pinning + applies on both cert paths. + +### 2. Verifier selection is by `PeerEntry` presence, not key type + +The client-side `ServerCertVerifier` is selected by whether the local +node holds a `PeerEntry` for the remote: + +| Local `PeerEntry` for remote? | Remote cert | Verifier | Peer graph? | +|-------------------------------|-------------|----------|-------------| +| yes | Ed25519 or X.509 | fingerprint pin | in (`PeerId` resolved from client-cert fingerprint — ADR-025 §5) | +| no | X.509 | `WebPkiServerVerifier` (CA) | **not in** (no `PeerId`) | +| no | Ed25519 raw key | fail closed (no CA to fall back to) | not in | + +### 3. `remote_identity: None` is load-bearing + +`Some(fingerprint)` means "pin this" (known peer). `None` means "no +`PeerEntry` for this remote" — CA verification for X.509 remotes, +hard failure for Ed25519 raw-key remotes. An implementation must not +default `remote_identity` to a placeholder to satisfy the field, and +must not treat `None` as "skip verification." + +### 4. Scope of the re-own + +This ADR is the deciding authority for alkcall's surface +(`ConnectionCredentials`, `RemoteIdentity`, the `ServerCertVerifier` +selection the dial-composer implements, `PeerEntry` asymmetry in the +peer graph). It does not re-own alknet ADR-034's other content (ACME +issuance, cert rotation, the alktls config surface) — those remain +properties of the transport/composition layer (see OQ-43, composition +layer). alknet ADR-034 is demoted to provenance: historical origin of +this rule, cited as such. + +## Consequences + +Positive: + +- The constraint binding alkcall's public surface is decided in this + crate's decision log; cross-repo link rot cannot strip the authority. +- OQ-21's "deciding ADR (alknet ADR-034, not ported)" caveat is + resolved — the surviving surfaces (`client-and-adapters.md` + §credentials, ADR-025's `PeerEntry` asymmetry) now cite an in-crate + ADR. +- The alknet rewrite can proceed without a documentation dependency + from this crate on the old repo's ADR numbering. + +Negative: + +- One more rule with a historical origin story in a different numbering + space; the provenance note (README §References) is the single place + that maps old to new. + +## References + +- [client-and-adapters.md](../client-and-adapters.md) — the working + spec carrying these semantics (§Credential sources for connections, + §Constraints "Pure-client X.509 connections", `remote_identity` + load-bearing rule) +- [ADR-022](022-call-protocol-client-and-adapter-contract.md) §7 — + transport-identity dimensions on the call protocol +- [ADR-025](025-peerentry-and-identity-id-decoupling.md) — + `PeerEntry` asymmetry, `PeerId` source, client-cert fingerprint → + `peer_id` resolution +- [ADR-012](012-connectioncredentials-decouple-dial-from-call.md) — + `ConnectionCredentials` shape (the dial consumes it; verifier + selection is applied there) +- OQ-21 (resolved by this ADR), OQ-43 (composition-layer ownership — + the transport/composition half mentioned in §4's scope carve-out) +- Provenance: alknet ADR-034, "Outgoing-Only X.509 and Three Peer + Roles" (`/workspace/@alkdev/alknet/docs/architecture/decisions/`), + retained as history, not authority \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index e73ba11..8e51458 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -1,6 +1,6 @@ --- status: current -last_updated: 2026-10-13 +last_updated: 2026-10-13 (composition-layer re-key) --- # Open Questions @@ -35,17 +35,14 @@ status, priority, and (when resolved) a resolution citing the ADR. | OQ-18 | PeerId — crypto identity vs stable logical id | resolved | high | ADR-025 — PeerId = Identity.id (stable) | | OQ-19 | Persistent peer registry | resolved | medium | ADR-025 — core trait + in-memory default; persistence adapters separate | | OQ-20 | ~~API key asymmetry~~ | dissolved | medium | PeerEntry supports multiple credential paths | -| OQ-21 | X.509 outgoing-only case | resolved | medium | Three remote roles; PeerEntry asymmetry correct | +| OQ-21 | X.509 outgoing-only case | resolved | medium | Three remote roles; PeerEntry asymmetry correct. Re-owned in-crate by ADR-054 — the verifier-selection rule that binds alkcall's surface (`RemoteIdentity: None` semantics, `ServerCertVerifier` by `PeerEntry` presence) is decided here, not by the un-ported source ADR (which is demoted to provenance). | ## Call Protocol — Pub/Sub | OQ | Title | Status | Priority | Resolution | |----|-------|--------|----------|------------| | 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. | +| OQ-42 | Hub message broker (topic registry, `Pub`↔`Sub` matching, N-consumer fan-out) | deferred(scope) | medium | Deferred downstream: the broker is a hub composition layering on 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. The hub itself lives in the composition layer (owner — OQ-43). | ### Pub/Sub Gap @@ -92,7 +89,7 @@ landed; what remains unbuilt is the downstream hub broker). | OQ-24 | Channels add/strip API shape | resolved | low | Resolved by the shipped surface: the strip is **integrated into the read/write path** — the demux loop consumes `read_header`/`write_chunk` so a handler's `BiStream` yields payload chunks and never sees the 8-byte header (ADR-035; matches ADR-037's payload-parse contract). The low-level primitives (`parse_header`/`write_header`/`read_header`/`write_chunk`/`write_eof` in `channels::wire`) remain exposed as the standalone utility for tooling/codegen, but no handler-facing add/strip wrapper exists or is needed. | | OQ-31 | `channel/open` ACL granularity | resolved | high | ADR-047 — `channel/open` dissolves into per-ALPN ops; each op has its own `access_control` | | OQ-32 | Quota lifecycle (opener vs closer, transport drop) | resolved | high | ADR-047 §7 — the per-connection opener ledger; decrement on every teardown path, keyed by opener | -| OQ-33 | Per-identity connection cap (endpoint layer) | deferred(scope) | low | Named as a separate layer (ADR-047 §"ALPN category reframe" references the findings); belongs at `alknet-endpoint`, not channels. Named to stop the re-tangle | +| OQ-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 the endpoint/connect-accept layer of the composition stack, not channels. Named to stop the re-tangle. The endpoint layer's home is the composition layer (owner — OQ-43; today it lives in the alknet glue). | | OQ-34 | `channel_open` marker wire format | resolved | medium | ADR-047 §2 — boolean `"channel_open": true` in `services/schema`; ALPN derivable from op name | | OQ-35 | `OperationEnv::channel_manager()` coupling | resolved | high | ADR-047 §4 — extension trait `ChannelOperationEnv` in `channels-call`; call crate stays free of channels types | | OQ-36 | `channel_id` allocation in Pub case | resolved | medium | ADR-047 §5 — "connection owner allocates" (the side that holds the `ChannelManager`); amends "responder allocates" | @@ -100,7 +97,7 @@ landed; what remains unbuilt is the downstream hub broker). | OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alk/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) | | OQ-39 | `channel/control` control-handle surface | resolved | medium | ADR-052 — the control-handle surface is implemented: per-channel control callbacks contributed by the ALPN establisher (`Establishment::with_control`), wrapper-installed and teardown-removed in the manager; the op routes the ALPN-specific `message` and answers `channel:no_control_handle` when the channel's ALPN registered none. The stub (`channel:control_not_implemented`) was removed with the single-publish fix (reviews 010 S-02 / 011). | | OQ-40 | `channel/resources/subscribe` live subscription | resolved | medium | ADR-053 — the live subscription is implemented: one `ResourceEnumerator` per openable ALPN (provided by the producing crate, snapshot-then-changes), aggregated by the handler over the scan-once snapshot; an empty registry emits one `{ "resources": [] }` seed and stays open. The one-shot error stub was removed with the single-publish fix (reviews 010 S-03 / 011). The alktunnels discovery-UI note (review 006 E-02) is served by the enumerator surface. | -| OQ-41 | QUIC-native multi-stream substrate | 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). | +| 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 the composition layer's QUIC accept loop. **Blocked on**: the composition layer's multi-stream substrate integration (owner — OQ-43; today the alknet glue, being replaced). **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 @@ -111,4 +108,20 @@ landed; what remains unbuilt is the downstream hub broker). | OQ-27 | ALPN string naming convention | resolved | medium | ADR-004 — alk/ prefix | | OQ-28 | Dynamic handler registration | resolved | low | ADR-019 — curated static, overlays dynamic | | OQ-29 | Handler-level auth resolution observability | resolved | medium | set_identity() on Connection for observability | -| OQ-30 | Dynamic resource ownership | resolved | high | ADR-011 — OwnershipProvider, resource_id_path | \ No newline at end of file +| OQ-30 | Dynamic resource ownership | resolved | high | ADR-011 — OwnershipProvider, resource_id_path | + +## Composition Layer + +The layer above alkcall that owns dial/accept, hub composition, and +endpoint concerns. As of 2026-10 that layer is the alknet glue (the +alknet mono-repo's crates: its client dial helper `AlknetClient`, its +endpoint crate, its hub wiring); the mono-repo itself is being replaced +by thin glue over alkcall and its sibling protocol/host crates +(alkhttp, alktty, alktunnels, alktrader, alktls, …). Several deferred +questions name "downstream" as their receiver — this OQ is the named +receiver for all of them, so the deferrals survive the replacement +rather than being repo-pinned to a codebase slated for deletion. + +| OQ | Title | Status | Priority | Resolution | +|----|-------|--------|----------|------------| +| OQ-43 | Composition-layer ownership and shape after the alknet replacement | deferred(scope) | medium | **Deferred scope**: who owns dial/accept/hub composition, the endpoint layer (OQ-33), the QUIC-native substrate (OQ-41), and persistence adapters (OQ-19) after the alknet mono-repo is replaced — and whether that composition is one thin glue workspace (an `alknode`-type crate consuming alkcall + the sibling crates) or several small glue binaries. **Blocked on**: the glue-workspace decision being committed (the stated direction is "a couple kloc of glue", but the shape — one workspace vs several binaries — and its ownership are not yet committed). **Impacts**: re-keys the deferrals OQ-33, OQ-41, OQ-42, OQ-19; determines the dial seam's future home (ADR-045's `AlknetClient`, today in the alknet glue); determines where hub-relay wiring templates are consumed (ADR-042, ADR-051). **Not blocking** any alkcall v0.9 API surface — alkcall is transport-blind and composition-blind by construction (ADR-031, ADR-035). **Cross-references**: OQ-33, OQ-41, OQ-42, OQ-19; ADR-045, ADR-031, ADR-042, ADR-051. | \ No newline at end of file diff --git a/docs/architecture/operation-registry.md b/docs/architecture/operation-registry.md index 0e883be..5f13b45 100644 --- a/docs/architecture/operation-registry.md +++ b/docs/architecture/operation-registry.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-07-09 +last_updated: 2026-10-13 (alkcall reframing) --- # Operation Registry @@ -21,7 +21,7 @@ The operation registry provides: - **Type safety**: JSON Schema for input and output enables validation and client code generation - **Composability**: Handlers can invoke other operations through `OperationEnv` (local dispatch — remote dispatch is a separate architectural concern, see Constraints) -The registry design is informed by the `@alkdev/operations` TypeScript package, which demonstrated the same capabilities in JavaScript runtimes. The Rust implementation in alknet-call is canonical — it preserves the behavioral contract (namespace + operation name → invoke with input, return output) while defining the adapter contract (from_*, to_*) in Rust (see ADR-033). +The registry design is informed by the `@alkdev/operations` TypeScript package, which demonstrated the same capabilities in JavaScript runtimes. The Rust implementation in alkcall is canonical — it preserves the behavioral contract (namespace + operation name → invoke with input, return output) while defining the adapter contract (from_*, to_*) in Rust (see ADR-033). ## Architecture @@ -90,7 +90,7 @@ Operation names use slash-based paths without a leading slash, aligned with URL The `namespace` field is derived from the name: for `fs/readFile` it's `fs`, for `agent/chat` it's `agent`. It's a convenience accessor for ACL matching and service grouping. -Visibility (ADR-017) controls whether an operation is callable from the wire. `External` operations are wire-facing — they appear in `services/list` and accept `call.requested` from clients. `Internal` operations are composition-only — they return `NOT_FOUND` (not `FORBIDDEN`) when called from the wire, and do not appear in `services/list`. The assembly layer declares visibility at registration. All import adapters (`from_openapi`, `from_mcp`, `from_jsonschema`, `from_call`) register operations as `Internal` by default (they're composition material, not directly callable); the handler that composes them is `External`. (`from_jsonschema` is now a real HTTP-backed adapter in `alknet-http` per ADR-027, not the schema-only placeholder it was.) +Visibility (ADR-017) controls whether an operation is callable from the wire. `External` operations are wire-facing — they appear in `services/list` and accept `call.requested` from clients. `Internal` operations are composition-only — they return `NOT_FOUND` (not `FORBIDDEN`) when called from the wire, and do not appear in `services/list`. The assembly layer declares visibility at registration. All import adapters (`from_openapi`, `from_mcp`, `from_jsonschema`, `from_call`) register operations as `Internal` by default (they're composition material, not directly callable); the handler that composes them is `External`. (`from_jsonschema` is now a real HTTP-backed adapter in `alkhttp` per ADR-027, not the schema-only placeholder it was.) ### AccessControl @@ -135,10 +135,13 @@ impl AccessControl { The `OwnershipProvider` trait (read side, sync — called on the dispatch hot path) and the `OwnershipStore` trait (write side, async — called by -handlers that manage resource lifecycles) are defined in `alknet-core` per -ADR-011's storage decision (fourth instance of the repo/adapter pattern, -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. +handlers that manage resource lifecycles) live with the vendored core +types in alkcall, per +ADR-011's storage decision (fourth instance of the source repo's +repo/adapter pattern — alknet ADR-033, provenance, not ported). The +trait shapes and the in-memory default adapter are in this crate's core +module; the parent repo's `auth.md` (alknet) is historical origin, not +part of this spec. **The ownership provider is carried on `OperationContext`** (or threaded by the dispatcher), populated by the dispatch path from the registry's @@ -440,7 +443,7 @@ pub enum OperationProvenance { | `Session` | Yes (within sandbox) | Yes — scopes set at sandbox creation | Internal always | > **`FromJsonSchema` provenance.** `from_jsonschema` is an HTTP-backed -> single-endpoint adapter in `alknet-http` (ADR-027): a real reqwest +> single-endpoint adapter in `alkhttp` (ADR-027): a real reqwest > forwarding handler, not a schema-only placeholder. `FromJsonSchema` > is a leaf, same trust model as `FromOpenAPI` (HTTP endpoint trusted; > handler is a forwarding stub). Schema validation without a handler is @@ -523,7 +526,7 @@ The `OperationEnv` trait is the universal composition mechanism. A handler calls /// through its `OperationContext.env` (which implements this trait). /// /// This must remain a trait, not a concrete type — session-scoped -/// registries (OQ-19) depend on wrapping the global env via trait +/// registries (OQ-04) depend on wrapping the global env via trait /// layering. Making `OperationEnv` concrete or hardcoding the global /// registry into the dispatch path would close the session-overlay /// pattern. @@ -604,7 +607,7 @@ The `parent` parameter propagates the calling context: the nested call gets `par The `invoke_peer` / `peer_contains` methods (ADR-024 §2) take a `PeerRef` selector and a `PeerId`. These types are defined alongside the -`PeerCompositeEnv` struct — see [client-and-adapters.md](client-and-adapters.md#peer-keyed-composition-env-adr-029) +`PeerCompositeEnv` struct — see [client-and-adapters.md](client-and-adapters.md#peer-keyed-composition-env-adr-024) and [ADR-024](decisions/024-peer-graph-routing-model.md) §2: ```rust @@ -805,7 +808,7 @@ Two things happen in `invoke()`: Future work may add remote call protocol dispatch as an additional backend. The handler-facing API stays the same. -**`OperationEnv` must remain a trait.** This is a constraint, not a suggestion. The trait-based design enables registry layering (ADR-019): the CallAdapter composes the root env per call from the curated base + active peer-keyed connection overlays + session overlay, and overlays wrap the base via trait layering. Session-scoped registries (OQ-19) and connection-scoped remote imports (ADR-022 `from_call`) are both overlays on the same base, using the same mechanism. The peer-keyed extension (`PeerCompositeEnv`, `invoke_peer`, ADR-024) composes on top of the same trait — it overrides the new peer-routing methods, not the base dispatch. Making `OperationEnv` concrete or hardcoding the global registry into the dispatch path would close both the session-overlay and connection-overlay patterns, and would prevent the peer-keyed routing model from composing. This is the same integration-point pattern as `IdentityProvider` (ADR-003). See OQ-19, ADR-019, and ADR-024. +**`OperationEnv` must remain a trait.** This is a constraint, not a suggestion. The trait-based design enables registry layering (ADR-019): the CallAdapter composes the root env per call from the curated base + active peer-keyed connection overlays + session overlay, and overlays wrap the base via trait layering. Session-scoped registries (OQ-04) and connection-scoped remote imports (ADR-022 `from_call`) are both overlays on the same base, using the same mechanism. The peer-keyed extension (`PeerCompositeEnv`, `invoke_peer`, ADR-024) composes on top of the same trait — it overrides the new peer-routing methods, not the base dispatch. Making `OperationEnv` concrete or hardcoding the global registry into the dispatch path would close both the session-overlay and connection-overlay patterns, and would prevent the peer-keyed routing model from composing. This is the same integration-point pattern as `IdentityProvider` (ADR-003). See OQ-04, ADR-019, and ADR-024. ### Service Discovery @@ -849,7 +852,7 @@ error handling. ### Operation Registry (hand-rolled, no irpc) -The operation registry is hand-rolled in alknet-call. ADR-013 accepted +The operation registry is hand-rolled in alkcall. ADR-013 accepted "irpc as the call protocol foundation," but no `.rs` file in the workspace ever imported irpc — the wire format (`wire.rs`), the operation registry, and the dispatch are all hand-rolled. ADR-014 supersedes ADR-013 and @@ -858,7 +861,7 @@ protocol (external, JSON)" with "irpc services (internal, postcard)" is moot — there is no irpc layer. If a handler internally uses a postcard/binary RPC for in-process calls, -that's a handler-internal choice, not an alknet-call integration. The +that's a handler-internal choice, not an alkcall integration. The operation registry's external interface is always JSON (the `EventEnvelope` wire format); the internal handler dispatch is a `Handler` / `StreamingHandler` trait object (ADR-021), not an irpc `Service`. @@ -968,19 +971,19 @@ The `Capabilities` type holds non-serializable, zeroized secret material. It doe |----------|-----|---------| | Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled framing, registry, dispatch; supersedes ADR-013 | | Call protocol stream model | [ADR-015](decisions/015-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | -| Static handler registration | [ADR-010](/workspace/@alkdev/alknet/docs/architecture/decisions/010-alpn-router-and-endpoint.md) | `HandlerRegistry` (ALPN-level) immutable after construction; `OperationRegistry` layered by ADR-019 (curated immutable, session/imported dynamic) | -| Vault integration via assembly layer | [ADR-008](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time | +| Static handler registration | [ADR-001](decisions/001-alpn-protocol-dispatch.md) | `HandlerRegistry` (ALPN-level) immutable after construction; `OperationRegistry` layered by ADR-019 (curated immutable, session/imported dynamic). Provenance: alknet ADR-010's endpoint/router layer is retired — alkcall has no endpoint layer (see OQ-43 for the endpoint layer's composition home) | +| Vault integration via assembly layer | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Vault is a capability source, accessed at assembly time (provenance: alknet ADR-008, superseded in-crate) | | Secret material flow and capability injection | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Capabilities carry outbound credentials; call protocol carries no secret material | | Privilege model and authority context | [ADR-017](decisions/017-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; composition authority + scoped env | | Handler registration, provenance, and composition authority | [ADR-018](decisions/018-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle | | Operation registry layering | [ADR-019](decisions/019-operation-registry-layering.md) | Curated (static, immutable) + session and connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) | | Operation error schemas | [ADR-016](decisions/016-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity for `from_openapi`/`to_openapi` | | Call protocol client and adapter contract | [ADR-022](decisions/022-call-protocol-client-and-adapter-contract.md) | `from_call`/`OperationAdapter` produce `HandlerRegistration` bundles; adapter-registered ops are `Internal` leaves. Surface specced in [client-and-adapters.md](client-and-adapters.md) | -| `from_jsonschema` as HTTP-backed single-endpoint adapter | [ADR-027](decisions/027-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from `alknet-call` (broken schema-only placeholder) to `alknet-http` as a real reqwest-backed single-endpoint adapter; `FromJsonSchema` provenance stays in `alknet-call` as a leaf (now handler-bearing, not "no handler") | +| `from_jsonschema` as HTTP-backed single-endpoint adapter | [ADR-027](decisions/027-from-jsonschema-as-http-adapter.md) | Moved `from_jsonschema` from the call protocol (broken schema-only placeholder) to the HTTP host crate as a real reqwest-backed single-endpoint adapter — today `alkhttp` (ex-`alknet-http`); `FromJsonSchema` provenance stays in alkcall as a leaf (now handler-bearing, not "no handler") | | Peer-graph routing model (supersedes ADR-023) | [ADR-024](decisions/024-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` (the field this doc's `HandlerRegistration` previously gained) | | Forwarded-for identity | [ADR-026](decisions/026-forwarded-for-identity.md) | `forwarded_for` field on `OperationContext` and `call.requested`; metadata only — `AccessControl::check` never reads it; the `from_call` handler populates it | | Streaming handler for subscriptions | [ADR-021](decisions/021-streaming-handler-for-subscriptions.md) | `StreamingHandler` type alongside `Handler`; `HandlerKind` enum on `HandlerRegistration` validated against `op_type`; `invoke_streaming()` on `OperationRegistry`; `invoke()` and `OperationEnv::invoke()` error with `INVALID_OPERATION_TYPE` on `Subscription` ops; composition stays request/response-only, stream composition is handler-level | -| Dynamic resource ownership for runtime-spawned resources | [ADR-011](decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md) | `AccessControl::check` consults an `OwnershipProvider` (sync read trait, alknet ADR-033 repo/adapter pattern); `OperationSpec` gains `resource_id_path` (JSON pointer into the input); proxy-only access pattern (spawner owns, proxy to share, teardown revokes); `list` = scope-gate + result-filter; teardown = automatic, handler-driven; composition = two orthogonal checks, ADR-017/022 unchanged | +| 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; the source repo's repo/adapter pattern — alknet ADR-033, provenance); `OperationSpec` gains `resource_id_path` (JSON pointer into the input); proxy-only access pattern (spawner owns, proxy to share, teardown revokes); `list` = scope-gate + result-filter; teardown = automatic, handler-driven; composition = two orthogonal checks, ADR-017/022 unchanged | | Publish operation type and HandlerKind::Sink | [ADR-046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | `OperationType::Pub` (producer→consumer streaming); `SinkHandler` + `HandlerKind::Sink` + `PublishStream`; `invoke_sink()` dispatch path; `call.published` wire event; `OperationSpec.publish_schema`; `Subscription` renamed to `Sub`; fan-out/broker out of scope for alkcall (OQ-22 resolved, OQ-42 downstream) | ## Open Questions @@ -1000,8 +1003,8 @@ See [open-questions.md](open-questions.md) for full details. `SamePeerCollision` (replaces flat `Conflict`). `#[non_exhaustive]`. See [client-and-adapters.md](client-and-adapters.md). - **OQ-12** (resolved): `from_call` re-import trigger — `from_call` is a - manual free function; the assembly layer calls it after the dial (in - `AlknetClient`). A `CallConnection::refresh()` method is a genuine + manual free function; the assembly layer calls it after the dial (in the composition layer's dial helper + `AlknetClient` — today the alknet glue; OQ-43). A `CallConnection::refresh()` method is a genuine feature addition — non-breaking, additive. See [ADR-028](decisions/028-from-call-manual-free-function.md). - **OQ-13** (resolved): `from_call` namespace collision — same-peer @@ -1017,9 +1020,11 @@ See [open-questions.md](open-questions.md) for full details. resolved; OQ-17 (multi-hop federation) deferred(scope) (feature extension); OQ-18 (`PeerId` source) resolved by ADR-025; OQ-19 (persistent peer registry) resolved by ADR-025; OQ-20 (API key - asymmetry) dissolved; OQ-21 (X.509 outgoing-only) resolved — the - deciding ADR (alknet ADR-034, not ported) named the three remote - roles; see OQ-21 in [open-questions.md](open-questions.md). + asymmetry) dissolved; OQ-21 (X.509 outgoing-only) resolved by + ADR-054 — three remote roles, verifier by `PeerEntry` presence (the + original deciding ADR, alknet ADR-034, was not ported; ADR-054 + re-owns the rule, provenance retained); see OQ-21 in + [open-questions.md](open-questions.md). - **OQ-30** (resolved by ADR-011): Dynamic resource ownership for runtime-spawned resources — `AccessControl::check` consults an `OwnershipProvider`; `OperationSpec` gains `resource_id_path`; proxy-only @@ -1030,8 +1035,8 @@ See [open-questions.md](open-questions.md) for full details. - [call-protocol.md](call-protocol.md) — CallAdapter, EventEnvelope, stream model, PendingRequestMap - ADR-014: Hand-rolled EventEnvelope framing (irpc never integrated; supersedes ADR-013) -- ADR-008: Vault integration point -- ADR-010: ALPN router and endpoint (static registration — applies to the `HandlerRegistry`, not the `OperationRegistry`; see ADR-019 for the distinction) +- ADR-010: Secret material flow and capability injection (the vault integration point) +- ADR-001: ALPN-based protocol dispatch (`HandlerRegistry` static registration) - ADR-015: Call protocol stream model - ADR-019: Operation registry layering (curated + session/connection overlays; `OperationEnv` as trait-object integration point) - ADR-024: Peer-graph routing model (peer-keyed overlays + `PeerRef` routing; `PeerCompositeEnv` supersedes the singular-connection `CompositeOperationEnv`) @@ -1039,4 +1044,4 @@ See [open-questions.md](open-questions.md) for full details. - ADR-026: Forwarded-for identity (`forwarded_for` on `OperationContext` and `call.requested`; metadata only) - ADR-021: Streaming handler for subscriptions (`StreamingHandler`, `HandlerKind`, `invoke_streaming()`, `INVALID_OPERATION_TYPE`) - ADR-011: Dynamic resource ownership for runtime-spawned resources (`OwnershipProvider` consulted by `AccessControl::check`; `OperationSpec.resource_id_path`; proxy-only access pattern; composition = two orthogonal checks, ADR-017/022 unchanged) -- Reference implementation: `/workspace/@alkdev/alknet-main/crates/alknet-core/src/call/` \ No newline at end of file +- Reference implementation: this crate (`src/`); historical origin: the alknet / alknet-main call-core sources (provenance) \ No newline at end of file