docs: spec-level sync with the single-publish fix + port-debt repair
The spec docs had not caught up with the stub-sweep fix commits (4eae18f..3317ea0), and the ported docs still carried alknet numbering that now collides with alkcall's own ADR range. Spec sync with the landed code: - call-protocol.md: register_imported/register_imported_all return Result and refuse marked specs; marked-spec registration refusal contract spelled out (ADR-047 am. 4 §4.8, ADR-022 am. 2026-10-13) - client-and-adapters.md: from_call flow gains the Pub sink-forwarding branch and the marked-op refusal (resolves OQ-37 — no from_call relay wrapper; sanctioned compositions are ChannelRelay::register_relay_openable / ChannelClient) - ADR-051: "channel/control is not implemented" stale sentence replaced with the ADR-052 per-leg control contract - OQ-37 resolved; OQ-22 split (primitive resolved; broker placement out of scope) with new OQ-42 for the downstream hub broker (deferred(scope), concretely blocked); OQ-24 resolved by the shipped add/strip surface; OQ-41 → deferred(scope) with blocking condition - README: OQ bullets renumbered to tracker IDs; alknet-collision note rewritten (range 001..053, alknet prefix convention); new principle 11 (implement-or-stop-advertising); AGENTS.md ADR range 001..053 Port-debt repair: - 149 broken decision links across 17 docs — ported with alknet numbers/paths, most now remapped to alkcall files by slug; the rest link the alknet repo per the README convention - stale OQ numbering rewritten to tracker IDs in call-protocol.md, call-README.md, operation-registry.md, client-and-adapters.md, ADR-025, channels-overview/README/wire, channel-client - unambiguous "alknet ADR-NNN" labels where an alknet number collides with an alkcall ADR (052 TTY vs channel/control, 033 storage vs rust-canonical, 025/026/027+020 vault vs PeerEntry/auth ADRs) Verified: cargo test (697 + guard), fmt, clippy --all-targets -D warnings; full markdown link scan clean.
This commit is contained in:
1 parent
3317ea0963
commit
7b8a3e7558
26 files changed
+432
-332
No files matched your search
@@ -129,18 +129,16 @@ pub struct CallConnection {
|
||||
impl CallConnection {
|
||||
/// Register an imported operation into this connection's overlay
|
||||
/// (Layer 2, ADR-019). Called by `from_call` after discovery.
|
||||
pub fn register_imported(&self, registration: HandlerRegistration) {
|
||||
let name = registration.spec.name.clone();
|
||||
self.imported_operations.write().insert(name, registration);
|
||||
}
|
||||
/// Refuses marked (`channel_open`) specs — they are not
|
||||
/// overlay-importable (ADR-047 amendment 4 §4.8).
|
||||
pub fn register_imported(&self, registration: HandlerRegistration)
|
||||
-> Result<(), String>;
|
||||
|
||||
/// Register multiple imported operations (bulk variant for `from_call`).
|
||||
pub fn register_imported_all(&self, registrations: Vec<HandlerRegistration>) {
|
||||
let mut overlay = self.imported_operations.write();
|
||||
for reg in registrations {
|
||||
overlay.insert(reg.spec.name.clone(), reg);
|
||||
}
|
||||
}
|
||||
/// Same marked-spec refusal; on refusal the entries inserted before the
|
||||
/// marked one stay in the overlay and the error is returned.
|
||||
pub fn register_imported_all(&self, registrations: Vec<HandlerRegistration>)
|
||||
-> Result<(), String>;
|
||||
|
||||
/// Build an `OperationEnv` impl for this connection's overlay. Used by
|
||||
/// the `CallAdapter` when composing the root `OperationContext.env`.
|
||||
@@ -167,6 +165,24 @@ Layer-0-only; runtime overlay registration uses `CallConnection` methods.
|
||||
When the connection drops, the overlay (and all imported ops) is dropped —
|
||||
no explicit deregistration needed.
|
||||
|
||||
**Marked-spec registration refusal** (ADR-047 amendment 4 §4.8; ADR-022
|
||||
amendment 2026-10-13): every generic registration path —
|
||||
`OperationRegistry::register`, `CallConnection::register_imported`,
|
||||
`register_imported_all`, and the `op/register` announcement path — refuses
|
||||
a marked (`channel_open`) spec loudly at registration time. Marked ops are
|
||||
channel-machinery ops (the data plane is the allocated channel's
|
||||
`BiStream`); a marked spec can never land as an advertised-but-unusable
|
||||
overlay body. The sanctioned marked-op imports are the channel-wrapper
|
||||
family: `register_openable*`, `register_pub_openable`,
|
||||
`register_relay_openable`. `op/register` rejects a marked announcement
|
||||
with the typed error `MARKED_SPEC_UNSUPPORTED` (ADR-016 error schema),
|
||||
naming `ChannelRelay::register_relay_openable` (relaying hub) and
|
||||
`ChannelClient` (direct consumption) as the sanctioned compositions.
|
||||
Unmarked `Sub`/`Pub` announcements register the real forwarding handler
|
||||
shapes (ADR-022 amendment 2026-10-13). This closes the stub class: an
|
||||
advertised op is always a capable one — implement-or-stop-advertising is
|
||||
release-blocking (`tests/stub_guard.rs`).
|
||||
|
||||
The adapter:
|
||||
1. Accepts bidirectional streams on the connection
|
||||
2. Reads length-prefixed JSON `EventEnvelope` frames from each stream
|
||||
@@ -185,7 +201,7 @@ differs (accept vs dial). Peer authorization flows through the existing
|
||||
(ADR-024 §3). The composition env is peer-keyed (`PeerCompositeEnv`,
|
||||
ADR-024 §1) to handle head→N-workers routing. See
|
||||
[client-and-adapters.md](client-and-adapters.md) for the `Dispatcher` mechanism
|
||||
and [ADR-024](decisions/029-peer-graph-routing-model.md) for the
|
||||
and [ADR-024](decisions/024-peer-graph-routing-model.md) for the
|
||||
peer-graph routing model.
|
||||
|
||||
### Stream Model
|
||||
@@ -265,7 +281,7 @@ The `payload` of a `call.requested` event has this shape:
|
||||
- `operationId` — the operation to invoke, **with a leading slash** on the wire (e.g., `/fs/readFile`, `/agent/chat`, `/services/list`). This is the display form of the operation name. The registry stores names without the leading slash (`fs/readFile` — see [operation-registry.md](operation-registry.md#operationspec)); the wire format adds it. The `CallAdapter` strips the leading slash before registry lookup.
|
||||
- `input` — the operation input, matching the operation's `input_schema` (JSON Schema). Always a `serde_json::Value`.
|
||||
- `auth_token` — optional. If present, the `CallAdapter` resolves it via `IdentityProvider::resolve_from_token()` and the resulting `Identity` takes precedence over the connection-level identity for this request. See [Identity Resolution](#authcontext-and-identity-resolution) below.
|
||||
- `forwarded_for` — optional (ADR-026). Present when a `from_call` forwarding handler propagates the originator's identity to a spoke. Carries a serialized `Identity` (id, scopes, resources) — the end user the hub authenticated. **Metadata only** — `AccessControl::check` never reads it; the spoke authorizes the hub (its direct caller), not the end user. The hub may set `forwarded_for: None` if it doesn't want to disclose the originator. See [ADR-026](decisions/032-forwarded-for-identity.md).
|
||||
- `forwarded_for` — optional (ADR-026). Present when a `from_call` forwarding handler propagates the originator's identity to a spoke. Carries a serialized `Identity` (id, scopes, resources) — the end user the hub authenticated. **Metadata only** — `AccessControl::check` never reads it; the spoke authorizes the hub (its direct caller), not the end user. The hub may set `forwarded_for: None` if it doesn't want to disclose the originator. See [ADR-026](decisions/026-forwarded-for-identity.md).
|
||||
|
||||
The `call.requested` payload does **not** carry an abort policy field. The abort policy (`abort-dependents` vs `continue-running`, ADR-020) is set on `OperationContext` and propagated through `OperationEnv::invoke()` — the composing handler decides the child's policy, not the wire caller. See [Abort Cascade and Nested Calls](#abort-cascade-and-nested-calls) below.
|
||||
|
||||
@@ -558,7 +574,7 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is
|
||||
- The call protocol does not depend on any database. `PendingRequestMap` is in-memory. Durable session storage is a consumer concern.
|
||||
- Operation specs use JSON Schema. The envelope is always JSON. Binary payloads may be base64-encoded in the `payload` field.
|
||||
- Batch is not a protocol primitive — multiple `call.requested` events with correlated IDs provide equivalent semantics. See OQ-14.
|
||||
- The call protocol is transport-agnostic at the envelope level. The `EventEnvelope` framing can run over QUIC streams, WebSocket frames, or Worker `postMessage`. The `CallAdapter` is the `ProtocolHandler` implementation that receives a `Connection` (any transport — ADR-007) and dispatches `EventEnvelope` frames. **The `EventEnvelope` shape (`{ type, id, payload }`) was derived from the `@alkdev/pubsub` `EventEnvelope` (`/workspace/@alkdev/pubsub/src/types.ts`), which already has a working WebSocket client/server implementation (`event-target-websocket-client.ts` / `event-target-websocket-server.ts`) and a generalized "event target" abstraction. The call protocol refined the envelope with typed event names (`call.requested`, `call.responded`, etc.) and structured payloads; the delta is small and well-defined, making a browser (and Node) WebSocket client straightforward to derive from the pubsub prior art. See ADR-044, [ADR-048](decisions/048-websocket-native-session-not-gateway.md), and [websocket.md](../http/websocket.md).
|
||||
- The call protocol is transport-agnostic at the envelope level. The `EventEnvelope` framing can run over QUIC streams, WebSocket frames, or Worker `postMessage`. The `CallAdapter` is the `ProtocolHandler` implementation that receives a `Connection` (any transport — ADR-007) and dispatches `EventEnvelope` frames. **The `EventEnvelope` shape (`{ type, id, payload }`) was derived from the `@alkdev/pubsub` `EventEnvelope` (`/workspace/@alkdev/pubsub/src/types.ts`), which already has a working WebSocket client/server implementation (`event-target-websocket-client.ts` / `event-target-websocket-server.ts`) and a generalized "event target" abstraction. The call protocol refined the envelope with typed event names (`call.requested`, `call.responded`, etc.) and structured payloads; the delta is small and well-defined, making a browser (and Node) WebSocket client straightforward to derive from the pubsub prior art. See alknet ADR-044, [ADR-048](/workspace/@alkdev/alknet/docs/architecture/decisions/048-websocket-native-session-not-gateway.md), and [websocket.md](/workspace/@alkdev/alknet/docs/architecture/crates/http/websocket.md).
|
||||
- `OperationEnv::invoke()` dispatches through the local registry. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer. See ADR-014 and OQ-13.
|
||||
- **The call protocol carries no secret material.** Secret material (private keys, API keys, mnemonics, decrypted credentials, raw tokens) must not appear in `call.requested` payloads, `call.responded` payloads, or `OperationContext.metadata`. The wire format carries `serde_json::Value` and cannot enforce this at the type level — the constraint is architectural, enforced by the operation registry and by convention. Operations that need to share public key material use a dedicated operation that returns only the public component. See ADR-010.
|
||||
- **Abort cascades to descendants.** `call.aborted` for a parent request cascades to all non-terminal descendants in the call tree. Default policy is `abort-dependents`; `continue-running` is an opt-in. See ADR-020.
|
||||
@@ -567,19 +583,19 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is
|
||||
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled length-prefixed JSON framing, operation registry, dispatch; supersedes ADR-013 (irpc was never imported) |
|
||||
| Call protocol stream model | [ADR-015](decisions/012-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation |
|
||||
| ALPN per connection | [ADR-004](decisions/006-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN |
|
||||
| ProtocolHandler receives Connection | [ADR-005](decisions/007-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams |
|
||||
| Vault integration point | [ADR-008](decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time |
|
||||
| Secret material flow | [ADR-010](decisions/014-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer |
|
||||
| Privilege model and authority context | [ADR-017](decisions/015-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env |
|
||||
| Abort cascade for nested calls | [ADR-020](decisions/016-abort-cascade-for-nested-calls.md) | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in |
|
||||
| Call protocol client and adapter contract | [ADR-022](decisions/017-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction. Client/adapter surface specced in [client-and-adapters.md](client-and-adapters.md) |
|
||||
| Handler registration, provenance, and composition authority | [ADR-018](decisions/022-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle |
|
||||
| Peer-graph routing model (supersedes ADR-023) | [ADR-024](decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` |
|
||||
| Forwarded-for identity | [ADR-026](decisions/032-forwarded-for-identity.md) | `forwarded_for` field on `call.requested` and `OperationContext`; metadata only — `AccessControl::check` never reads it; the `from_call` handler populates it |
|
||||
| Operation error schemas | [ADR-016](decisions/023-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details` |
|
||||
| Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/014-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled length-prefixed JSON framing, operation registry, dispatch; supersedes ADR-013 (irpc was never imported) |
|
||||
| Call protocol stream model | [ADR-015](decisions/015-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation |
|
||||
| ALPN per connection | [ADR-004](decisions/004-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN |
|
||||
| ProtocolHandler receives Connection | [ADR-005](decisions/005-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams |
|
||||
| Vault integration point | [ADR-008](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time |
|
||||
| Secret material flow | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer |
|
||||
| Privilege model and authority context | [ADR-017](decisions/017-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env |
|
||||
| Abort cascade for nested calls | [ADR-020](decisions/020-abort-cascade-for-nested-calls.md) | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in |
|
||||
| Call protocol client and adapter contract | [ADR-022](decisions/022-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction. Client/adapter surface specced in [client-and-adapters.md](client-and-adapters.md) |
|
||||
| Handler registration, provenance, and composition authority | [ADR-018](decisions/018-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle |
|
||||
| Peer-graph routing model (supersedes ADR-023) | [ADR-024](decisions/024-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` |
|
||||
| Forwarded-for identity | [ADR-026](decisions/026-forwarded-for-identity.md) | `forwarded_for` field on `call.requested` and `OperationContext`; metadata only — `AccessControl::check` never reads it; the `from_call` handler populates it |
|
||||
| Operation error schemas | [ADR-016](decisions/016-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details` |
|
||||
| Streaming handler for subscriptions | [ADR-021](decisions/021-streaming-handler-for-subscriptions.md) | `StreamingHandler` type, `invoke_streaming()` dispatch path, `INVALID_OPERATION_TYPE` protocol code; the server-side streaming branch in `handle_stream` |
|
||||
| Publish operation type and HandlerKind::Sink | [ADR-046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | `OperationType::Pub` (producer→consumer streaming); `SinkHandler` + `HandlerKind::Sink`; `call.published` wire event; `invoke_sink()` dispatch; `Subscription` renamed to `Sub` |
|
||||
|
||||
@@ -587,45 +603,48 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is
|
||||
|
||||
See [open-questions.md](open-questions.md) for full details.
|
||||
|
||||
- **OQ-13** (resolved): Operation path format is `/{service}/{op}`. Remote dispatch is a separate mechanism, not a path prefix.
|
||||
- **OQ-14** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive.
|
||||
- **OQ-16** (resolved by ADR-010): No vault operations are exposed over the call protocol for now.
|
||||
- **OQ-19** (resolved): Session-scoped operation registries — agent-written operations overlaid on global registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait.
|
||||
- **OQ-25** (dissolved by ADR-024): `remote_safe` marking shape — moot;
|
||||
- **OQ-02** (resolved): Operation path format is `/{service}/{op}`. Remote dispatch is a separate mechanism, not a path prefix.
|
||||
- **OQ-03** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive.
|
||||
- **OQ-09** (resolved by ADR-010): No vault operations are exposed over the call protocol for now.
|
||||
- **OQ-04** (resolved): Session-scoped operation registries — agent-written operations overlaid on global registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait.
|
||||
- **OQ-10** (dissolved by ADR-024): `remote_safe` marking shape — moot;
|
||||
`remote_safe`/`trusted_peer` retired; peer authorization is
|
||||
`AccessControl::check(peer_identity)`.
|
||||
- **OQ-26** (resolved): `OperationAdapter` error type — `AdapterError`
|
||||
- **OQ-11** (resolved): `OperationAdapter` error type — `AdapterError`
|
||||
variants (`DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`,
|
||||
`SamePeerCollision`); `#[non_exhaustive]`. See
|
||||
[client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-27** (resolved): `from_call` re-import trigger — `from_call` is a
|
||||
- **OQ-12** (resolved): `from_call` re-import trigger — `from_call` is a
|
||||
manual free function; the assembly layer calls it after the dial (in
|
||||
`AlknetClient`). See
|
||||
[ADR-028](decisions/069-from-call-manual-free-function.md).
|
||||
- **OQ-28** (resolved): `from_call` namespace collision — same-peer collision
|
||||
[ADR-028](decisions/028-from-call-manual-free-function.md).
|
||||
- **OQ-13** (resolved): `from_call` namespace collision — same-peer collision
|
||||
= error; cross-peer dissolved by ADR-024 (separate sub-overlays). See
|
||||
[client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-29** (resolved): `CallClient` TLS client-auth — wire quinn client-auth
|
||||
- **OQ-14** (resolved): `CallClient` TLS client-auth — wire quinn client-auth
|
||||
(present Ed25519 key as raw public key client cert); key-type-aware server
|
||||
cert verification; fingerprint normalization. See
|
||||
[client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-30** (resolved): `PeerRef::Any` routing policy — insertion-order
|
||||
- **OQ-15** (resolved): `PeerRef::Any` routing policy — insertion-order
|
||||
first-match. See [client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-31** (resolved): `services/list-peers` re-export semantics — opt-in;
|
||||
- **OQ-16** (resolved): `services/list-peers` re-export semantics — opt-in;
|
||||
`services/list` is "own ops only." See [client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-32** (open, feature extension): Multi-hop federation — the one-hop
|
||||
- **OQ-17** (deferred(scope), feature extension): Multi-hop federation — the one-hop
|
||||
model is the architectural commitment; multi-hop is a feature extension
|
||||
that doesn't break downstream. See [client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-33** (resolved by ADR-025): `PeerId` source — `Identity.id` from
|
||||
- **OQ-18** (resolved by ADR-025): `PeerId` source — `Identity.id` from
|
||||
`IdentityProvider` resolution (= `PeerEntry.peer_id`, stable across key
|
||||
rotation), not a connection-assigned UUID.
|
||||
- **OQ-34** (resolved by ADR-025 + ADR-033): Persistent peer registry —
|
||||
- **OQ-19** (resolved by ADR-025): Persistent peer registry —
|
||||
the storage boundary is `core trait + in-memory default`; persistence
|
||||
adapters are separate crates.
|
||||
- **OQ-37** (resolved by ADR-034): X.509 outgoing-only case — three remote
|
||||
adapters are separate crates (the concrete adapter shapes were named in
|
||||
alknet research, not re-specced here).
|
||||
- **OQ-21** (resolved): X.509 outgoing-only case — three remote
|
||||
roles named (public X.509 endpoint, transport relay, hub); pure-client
|
||||
X.509 connections are not in the peer graph on the client side. See
|
||||
[client-and-adapters.md](client-and-adapters.md).
|
||||
X.509 connections are not in the peer graph on the client side. The
|
||||
deciding ADR (alknet ADR-034, "Outgoing-Only X.509 and Three Peer
|
||||
Roles") was not ported into alkcall; the surviving alkcall surface is
|
||||
ADR-025's `PeerEntry` asymmetry. See [client-and-adapters.md](client-and-adapters.md).
|
||||
|
||||
## References
|
||||
|
||||
|
||||
Reference in new issue
Block a user