docs: re-key architecture docs off the alknet mono-repo — speak as alkcall
The ported docs still framed alknet siblings (alknet-core/-call/-channels/
-http/-tls/-client) as live load-bearing context of this crate. With the
extraction complete and alknet headed for glue-level replacement, the docs
now speak as alkcall and cite the real sibling crates (alkhttp, alktls,
alktty, alktunnels, alktrader).
- ADR-054 (new): re-own the verifier-selection rule in-crate — three
remote roles (public X.509 endpoint / transport relay / hub),
ServerCertVerifier by PeerEntry presence, RemoteIdentity None
load-bearing. Provenance: alknet ADR-034 (never ported), demoted to
footnote. Resolves OQ-21's cross-repo authority gap.
- OQ-43 (new, 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, ADR-045 dial
seam). README layering diagram redrawn with the composition layer as
TBD-ownership instead of "alknet / alknode" in concrete.
- Spec docs reframed: titles, self-references, dial/TLS/from_jsonschema
homes, malformed legacy cite rows removed; dead relative research
paths qualified as source-repo paths.
- ADR amendments (append-only): 027 (alknet-http -> alkhttp), 031 (post-
extraction state), 044 (split landed as modules in alkcall), 045
(name normalization; citation-space note).
- Citation hygiene: unprefixed/colliding source-repo cites prefixed
("alknet ADR-NNN (provenance)") across 009/011/022/034/035/038/040/
043/044/045; stale OQ cites corrected (session registries OQ-04, batch
semantics OQ-03, verifier rule ADR-054); stale anchors fixed.
- README provenance note consolidated; citation conventions documented.
Verification: markdown link check (all local links resolve, all in-crate
ADR cites point at existing decisions/ files); table integrity check
(uniform row widths across open-questions.md and edited tables); residual
alknet-mention sweep (only provenance/historical/intentional framing
remains); architecture review subagent pass with zero remaining criticals.
Docs-only change — no code paths touched.
This commit is contained in:
1 parent
7b8a3e7558
commit
5f019c02f1
26 files changed
+661
-374
No files matched your search
+51
-33
@@ -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 + │
|
||||
@@ -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).
|
||||
> 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)).
|
||||
@@ -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).
|
||||
@@ -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<OperationRegistry>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
/// 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<Arc<dyn SessionOverlaySource + Send + Sync>>,
|
||||
/// 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<dyn SessionOverlaySource + Send + Sync>) -> 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/`
|
||||
- Reference implementation: this crate (`src/`); historical origin: the alknet / alknet-main call-core sources (provenance)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
- Source-repo hub crate (alknet, provenance — the composition layer's
|
||||
hub wiring; the relay implementation itself is an alkcall export,
|
||||
`ChannelRelay`, ADR-051)
|
||||
@@ -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<HashMap<u32, ChannelState>>,
|
||||
handlers: Arc<HandlerRegistry>,
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
payload) is decided (ADR-035); the shipped surface integrates the
|
||||
strip (see OQ-24 in [open-questions.md](open-questions.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
|
||||
- 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
|
||||
@@ -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<TlsIdentity>, // RFC 7250 raw key or X.509
|
||||
pub remote_identity: Option<RemoteIdentity>, // 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<dyn Handler>` 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
|
||||
- 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
|
||||
- `docs/research/alknet-call-peer-routing/findings.md` — the peer-graph
|
||||
- `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
|
||||
`alkhttp`'s `from_mcp`/`to_mcp`
|
||||
@@ -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<dyn BiStream>`. The trait is the ecosystem convention (`tokio::net::TcpStream`, `TlsStream<TcpStream>`, `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<TcpStream>`. 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<BiStream, StreamError> {
|
||||
`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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -178,3 +178,15 @@ endpoint.
|
||||
established by `from_openapi`.
|
||||
- Reuses `HttpServiceConfig` and `SharedHttpClient` from
|
||||
`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.
|
||||
@@ -141,3 +141,30 @@ 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).
|
||||
### 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.
|
||||
@@ -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**)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
- 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.
|
||||
@@ -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:<hex>` → `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.
|
||||
@@ -555,3 +565,22 @@ dedicated ADR lands.
|
||||
is the native analogue)
|
||||
- `docs/architecture/crates/channels/channel-client.md` §"Relationship
|
||||
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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -112,3 +109,19 @@ landed; what remains unbuilt is the downstream hub broker).
|
||||
| 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 |
|
||||
|
||||
## 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. |
|
||||
@@ -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/`
|
||||
- Reference implementation: this crate (`src/`); historical origin: the alknet / alknet-main call-core sources (provenance)
|
||||
Reference in new issue
Block a user