# ADR-017: Call Protocol Client and Adapter Contract *Ported from alknet ADR-017 (Call Protocol Client and Adapter Contract); re-targeted to alkhttp.* ## Status Accepted (amended 2026-06-26, 2026-07-13, and 2026-07-16 — see "Amendments" below; the 2026-07-16 amendment per alknet ADR-089 §5 removes `CallClient::connect`) ## Context The call protocol spec (alknet ADR-012, now alkcall ADR-015) defined the stream model as bidirectional — "both sides can initiate calls." But the spec only described the accept side: `CallAdapter` implements `ProtocolHandler`, accepts incoming connections, and dispatches to the operation registry. The connect side — who opens the connection, how calls are sent, how remote operations are discovered and imported — was left as alknet OQ-15. The need for the connect side is concrete and immediate: - **Head/worker dispatch**: a head node manages worker nodes (Vast.ai, RunPod, local Docker). The head needs to call operations on workers (exec, sync, status) and workers need to call back (report status, request work). The POC at `/workspace/@alkdev/dispatch` demonstrated this over SSH+axum; under the call protocol, it's cross-node composition. - **NAPI/Python adapters**: Node.js and Python consumers need to call operations on an alk node. They speak the EventEnvelope wire format over a connection. - **Agent tool dispatch**: an agent handler needs to call operations on remote nodes (tools, services) the same way it calls local operations — through `OperationEnv::invoke()`. The `from_call` adapter makes remote operations appear in the local registry. - **Cross-protocol interop**: external systems (HTTP APIs, MCP servers) are imported via `from_openapi` and `from_mcp`. The reverse direction — exposing local operations to external systems — needs `to_openapi` and `to_mcp`. The `@alkdev/operations` TypeScript package demonstrated the adapter patterns (`from_openapi`, `from_mcp`) and the `buildEnv` composition mechanism. The Rust implementation defines the canonical traits (alknet ADR-013, now alkcall ADR-033). alknet OQ-15 was constrained by [ADR-014](014-secret-material-flow-and-capability-injection.md) (adapters take credential sources, not static tokens) and [ADR-015](015-privilege-model-and-authority-context.md) (adapter-registered operations are `Internal` by default). This ADR locks the remaining one-way door: the client/adapter contract architecture. The decision record now lives in the alkcall crate (alkcall ADR-022); this port exists because alkhttp implements the adapter contract — the `OperationAdapter` trait consumes this crate's HTTP-backed adapter implementations, and the gateway `/search` and `/schema` endpoints are the discovery surface `from_call` mirrors. ## Decision ### 1. `CallClient` opens connections and shares the dispatch loop `CallClient` opens a connection to a remote node with ALPN `alk/call`. Once connected, the connection is symmetric — both sides can send and receive `call.requested`. The `CallClient` is not just a caller; it is also a callee. It has its own operation registry to dispatch incoming calls from the remote side. ```rust pub struct CallClient { registry: Arc, identity_provider: Arc, } impl CallClient { pub async fn connect(&self, addr: SocketAddr, credentials: CallCredentials) -> Result; } ``` The dispatch loop is shared between `CallAdapter` and `CallClient`. Once a connection is established (whether accepted by the adapter or opened by the client), the same logic applies: read `EventEnvelope` frames, dispatch to the operation registry, write responses, and send outgoing `call.requested` events for calls initiated on this side. The only difference is who opened the connection. `CallConnection` provides: - `call(operation_id, input) -> ResponseEnvelope` — send `call.requested`, await `call.responded` (one result) - `subscribe(operation_id, input) -> Stream` — send `call.requested`, yield each `call.responded` until `call.completed` or `call.aborted` - `abort(request_id)` — send `call.aborted`, cascade to descendants (alkcall ADR-020) - `services_list() -> Vec` — call `services/list` - `services_schema(name) -> OperationSpec` — call `services/schema` ### 2. Connection direction is independent of call direction Who opens the connection (who has the public IP, who uses a relay, who connects out reverse-runner style) is a connection-layer concern, not a protocol-layer concern. Once connected, both sides can call each other. | Topology | Who advertises | Who opens connection | Who can call whom | |----------|---------------|----------------------|-------------------| | Public service | Producer (public IP/domain) | Consumer | Both directions | | P2P (iroh relay) | Both (relay-assisted) | Either | Both directions | | Reverse (runner pattern) | Head (public IP) | Worker connects out | Both directions | | Reverse (dispatch pattern) | Worker (public SSH port) | Head connects out | Both directions | The protocol does not distinguish producer and consumer after connection establishment. The `CallAdapter` accepts connections; the `CallClient` opens connections. Both dispatch incoming and outgoing calls through the same mechanism. ### 3. `from_call` adapter imports remote operations `from_call` does for call protocol endpoints what `from_openapi` does for HTTP APIs: discovers operations and registers them in the local registry with forwarding handlers. ```rust pub async fn from_call( connection: &CallConnection, config: FromCallConfig, ) -> Vec ``` The adapter: 1. Calls `services/list` on the remote node → gets the list of `External` operations 2. Calls `services/schema` for each → gets the input/output JSON Schemas and declared error_schemas ([ADR-023](023-operation-error-schemas.md)) 3. For each discovered operation, constructs a `HandlerRegistration` bundle: - The spec mirrors the remote operation's name, namespace, type, schemas (input, output, and error_schemas — ADR-023), and access control - The handler sends `call.requested` through the `CallConnection` and awaits `call.responded` (or streams for `Sub` operations) - `provenance: FromCall`, `composition_authority: None`, `scoped_env: None` (leaves — [ADR-022](022-handler-registration-provenance-and-composition-authority.md)) 4. The caller registers these bundles in their local registry (into the connection's overlay — alkcall ADR-024/ADR-019) `from_call`-registered operations are `Internal` by default ([ADR-015](015-privilege-model-and-authority-context.md)) — they are composition material, not directly callable from the wire. The handler that composes them is `External`. The `FromCallConfig` includes: - The credential source for the outbound connection ([ADR-014](014-secret-material-flow-and-capability-injection.md)) — TLS identity, auth token, or capability-provided credentials - An optional namespace prefix (to avoid collisions when importing from multiple remote nodes) - An optional operation filter (to import only specific operations) ### 4. `to_openapi` and `to_mcp` adapters export local operations The reverse direction — exposing local operations to external systems: - **`to_openapi`**: generates an OpenAPI spec from the local registry's `External` operations. External systems (HTTP clients, API gateways) can discover and call alk operations through a standard HTTP interface. This is the gateway pattern — the five fixed gateway endpoints (`/search`/`/schema`/`/call`/`/batch`/`/subscribe`), not one path per operation (ADR-042) — implemented in alkhttp. - **`to_mcp`**: exposes local operations as MCP tools. MCP clients (editors, AI tools) can discover and call alk operations through the MCP protocol — the four fixed gateway tools (ADR-041), implemented in alkhttp. These adapters are outbound bridges — they translate the call protocol's operation model into external protocol formats. They do not modify the local registry; they project it. ### 5. The adapter contract trait The adapter patterns share a common shape: they produce `HandlerRegistration` bundles that register in the local registry. The trait: ```rust #[async_trait] pub trait OperationAdapter: Send + Sync { async fn import(&self) -> Vec; } ``` The return type is `Vec` (not `(OperationSpec, Handler)` pairs) — [ADR-022](022-handler-registration-provenance-and-composition-authority.md) changed the registration API to the bundle shape, and adapters must produce bundles. Adapter convenience methods construct bundles with `composition_authority: None` and `scoped_env: None` for the leaf ops they produce. The trait is **async** because `from_call` requires async discovery (`services/list` + `services/schema` over a connection). A synchronous trait cannot accommodate `from_call` without a separate async pre-step that populates a cache. The sync adapters (`from_openapi`, `from_mcp` reading a static spec) trivially satisfy an async trait — their `import()` bodies contain no `.await` points. The async/sync question is decided: the trait is async. Implementations: - `FromOpenAPI` — imports from an OpenAPI spec (HTTP-backed handlers; implemented in alkhttp) - `FromMCP` — imports from an MCP server (MCP-backed handlers; implemented in alkhttp) - `FromCall` — imports from a remote call protocol endpoint (call-protocol-backed handlers; implemented in alkcall) - ~~`FromJsonSchema` — imports from a JSON Schema definition (schema-only, no handler — used for validation or client generation)~~ — **superseded by [ADR-066](066-from-jsonschema-as-http-adapter.md)**: `from_jsonschema` is now an HTTP-backed single-endpoint adapter in alkhttp (reqwest forwarding handler, not a schema-only placeholder); `FromJsonSchema` provenance stays in alkcall as a handler-bearing leaf. The `to_*` adapters are outbound projections, not `OperationAdapter` implementations — they consume the registry, they don't produce entries for it. The specific trait signatures (error types, configuration parameters) are two-way doors for implementation. The one-way doors are the architectural commitments: adapters produce `HandlerRegistration` bundles ([ADR-022](022-handler-registration-provenance-and-composition-authority.md)), the trait is async (required by `from_call`), and the adapter *trait* lives in alkcall while adapter *implementations* live with their transport (HTTP-backed adapters in alkhttp per ADR-066; connection-backed `from_call` in alkcall). See alkcall's `client-and-adapters.md` §"Adapter Location Map." ### 6. Cross-node call tree and abort cascade When a `from_call` handler sends `call.requested` to a remote node, the call participates in the local call tree via `parent_request_id`. If the parent is aborted, the cascade (alkcall ADR-020) reaches the `from_call` handler, which sends `call.aborted` to the remote node. The remote node cascades to its own descendants. The abort crosses the node boundary transparently. ``` Head node Worker node r1: /dispatch/run_training r1-a: worker/exec (from_call handler) → call.requested { id: r1-a } ────────→ receives, dispatches to exec r1-a-1: exec spawns child user aborts r1 cascade to r1-a from_call handler sends: call.aborted { id: r1-a } ───────────→ receives, cascades to r1-a-1 aborts exec and children ``` ### 7. Credential sources for connections The `CallClient` needs credentials to authenticate to the remote node. These come from capabilities ([ADR-014](014-secret-material-flow-and-capability-injection.md)), not environment variables. The credential types: - **TLS identity**: the local node's Ed25519 key (RFC 7250 raw key) or X.509 cert, derived from the vault at startup - **Auth token**: an opaque token for call-protocol-level authentication, decrypted from the vault or derived from a shared secret - **Remote identity verification**: the expected fingerprint or cert of the remote node, stored as a capability (not an env var or config file) The `from_call` adapter receives these credentials at registration time, same as `from_openapi` receives HTTP credentials. ## Consequences **Positive:** - Cross-node composition works the same as local composition. A handler calls `env.invoke("worker", "exec", ...)` and doesn't know (or care) whether `worker/exec` is a local operation or a `from_call`-imported remote operation. The composition is transparent. - The head/worker pattern (dispatch, runners) is a connection topology, not a protocol feature. Workers can connect to heads (runner pattern) or heads can connect to workers (dispatch pattern) — the protocol handles both. - `from_call` is the same pattern as `from_openapi` and `from_mcp`: discover, register, forward. The adapter contract is unified. - `to_openapi` and `to_mcp` enable interop with non-alk systems without those systems needing to speak EventEnvelope. - The abort cascade (alkcall ADR-020) crosses node boundaries transparently. No consumer needs to implement cross-node abort propagation. - The NAPI and Python adapters can use `CallClient` directly to call remote operations — they don't need a separate client implementation. **Negative:** - `CallClient` has its own operation registry (for dispatching incoming calls from the remote side). This is a second registry instance, not the global one — it needs to be populated with the operations this node wants to expose to that specific remote peer. The specific mechanism (sharing the global registry, a peer-scoped subset, or a separate registry) is a two-way door. - `from_call`-registered operations have a latency cost: each invocation sends a `call.requested` and awaits a `call.responded`. This is inherent to remote calls and not specific to the adapter pattern. Caching or batching strategies are consumer concerns. - The `to_*` adapters need to translate the call protocol's operation model (JSON Schema, EventEnvelope, subscribe/stream) into external formats (OpenAPI paths, MCP tools). Some semantics don't map cleanly (e.g., `Sub` streaming in OpenAPI, bidirectional calls in MCP). The adapters handle these with best-effort mappings and document the gaps. - **Published `to_*` specs are compatibility contracts.** The "best-effort" mapping label is internal framing. Once a generated spec is published and external clients build against it, the mapping semantics (e.g., `Sub` streaming → SSE long-poll) become a de facto contract. Changing the mapping later breaks every client. `to_*` mapping choices are two-way *before* first publication but one-way *after*. Version the generated specs (e.g., OpenAPI spec version tied to the registry's External operation set version) and emit a spec version marker so consumers can detect mapping changes. This is the "published artifact is a contract" blind spot in alknet ADR-009's framework: it classifies doors by reversal cost in the codebase, not by compatibility cost for external consumers. (The versioning was subsequently specced — alknet ADR-045, ported here as [ADR-045](045-to-openapi-gateway-spec-versioning.md).) - **Sharing the global registry with a `CallClient` exposes local capabilities to the remote peer.** Each `HandlerRegistration` carries `Capabilities` with secret material. If the `CallClient` shares the global registry, a remote peer calling an External operation triggers dispatch that populates `OperationContext.capabilities` from the local registration bundle — meaning the local node's API keys and signing keys are used for the remote peer's call. A peer-scoped subset must filter by capability remote-safety (is this operation's capability safe to expose to this peer?), not just operation name. The registry-mechanism choice (share global vs subset vs separate) is two-way mechanically but has a security dimension post-[ADR-022](022-handler-registration-provenance-and-composition-authority.md): the "share global" option is a capability-exposure decision, not just a dispatch decision. - The `CallConnection` abstraction adds a layer between the handler and the raw transport stream. This is necessary for the `from_call` handler to be transparent — it shouldn't know about the underlying transport streams, only about call/request semantics. ## Assumptions 1. **The connection is symmetric after establishment.** Both sides can send and receive `call.requested`. If a future use case requires one-directional connections (e.g., a fire-and-forget notification where the receiver can't call back), the model needs extension. The assumption is that bidirectional is the correct default. 2. **`services/list` and `services/schema` are the discovery mechanism for `from_call`.** The remote node exposes its `External` operations through these built-in operations. If a remote node doesn't support service discovery (e.g., a minimal worker that only accepts specific calls), `from_call` needs an alternative discovery mechanism (static config, manual spec). The assumption is that nodes participating in cross-node composition support service discovery. 3. **The `from_call` handler is transparent to composition.** A handler that calls `env.invoke("worker", "exec", ...)` doesn't know it's a remote call. If the remote node is unreachable or the connection drops, the handler gets a `call.error` (same as a local handler error). The assumption is that remote call failures are handled the same as local handler failures. 4. **`from_call`-registered operations mirror the remote spec.** The imported `OperationSpec` has the same name, namespace, type, schemas (input, output, and error_schemas per [ADR-023](023-operation-error-schemas.md)), and access control as the remote operation. If the remote operation changes (new schema, renamed), the imported spec is stale until re-import. The assumption is that re-import happens on reconnection or is triggered explicitly. Hot-swapping imported specs is a two-way door. 5. **The `to_*` adapters are projections, not live bridges.** `to_openapi` generates a spec; it doesn't proxy HTTP requests. An external HTTP client calling the generated OpenAPI endpoints needs an HTTP host (alkhttp) that translates HTTP requests into call protocol operations — the gateway dispatch. The assumption is that `to_*` generates specs/tools, and a separate HTTP/MCP handler bridges the actual traffic. ## References - alknet ADR-005: irpc as call protocol foundation (superseded — see alkcall ADR-014, irpc was never integrated; framing is hand-rolled) - alkcall ADR-015: Call Protocol Stream Model (alknet ADR-012; bidirectional streams) - alkcall ADR-033: Rust as canonical implementation language (alknet ADR-013; adapter traits in Rust) - [ADR-014](014-secret-material-flow-and-capability-injection.md): Secret material flow (credential sources, not static tokens) - [ADR-015](015-privilege-model-and-authority-context.md): Privilege model (adapter ops are Internal by default) - alkcall ADR-020: Abort cascade for nested calls (alknet ADR-016; cross-node abort propagation) - alkcall ADR-023: Peer-Scoped Registry Filtering for CallClient Inbound Dispatch (alknet ADR-028; resolves the §1 Consequences security dimension flagged as a two-way door) - alkcall ADR-024: Peer-Graph Routing Model (alknet ADR-029; supersedes alkcall ADR-023's `remote_safe`/`trusted_peer` gate) - alknet OQ-15: Call protocol client and adapter contract (resolved by this ADR — the decision record is alkcall ADR-022) - alknet OQ-25..28: Two-way-door remainders from the call-completion gap analysis (DC-1 shape, DC-4 error type, DC-2 re-import trigger, DC-3 namespace collision — see alknet mono-repo `open-questions.md`; the OQ numbers are alknet-record citations) - The alkcall crate's `call-protocol.md` — the wire protocol spec (alknet mono-repo: `crates/call/call-protocol.md`) - The alkcall crate's `operation-registry.md` — the registry spec (alknet mono-repo: `crates/call/operation-registry.md`) - The alkcall crate's `client-and-adapters.md` — the spec that operationally fills the gap this ADR left to implementation (alknet mono-repo: `crates/call/client-and-adapters.md`) - alknet mono-repo `docs/research/alknet-call-completion/gap-analysis.md` — DC-1..4, the decisions that needed resolution before implementation - TypeScript `@alkdev/operations` — `from_openapi`, `from_mcp`, `buildEnv` prior art - POC at `/workspace/@alkdev/dispatch` — head/worker dispatch over SSH+axum ## Amendments (2026-06-26) This ADR left four decisions as two-way doors (§1 Consequences flagged DC-1's security dimension; §5 noted trait signatures are two-way doors; Assumption 4 noted re-import hot-swap is a two-way door; §3 mentioned the namespace prefix). The call-completion gap analysis (alknet mono-repo `docs/research/alknet-call-completion/gap-analysis.md` DC-1..4) resolved them. The resolutions: ### DC-1 — CallClient registry scope: resolved by alkcall ADR-023, superseded by alkcall ADR-024 The §1 Consequences security dimension was originally resolved by alknet ADR-028 (default-deny `remote_safe: bool` + `trusted_peer` opt-in; now alkcall ADR-023). **alknet ADR-028 is now superseded by alknet ADR-029** (2026-06-27; now alkcall ADR-024): the flat-namespace single-peer model alknet ADR-028 built on cannot express the head→N-workers pattern, and the `remote_safe`/`trusted_peer` gate duplicates the existing `AccessControl`/`Identity` machinery while reintroducing the blanket-bypass anti-pattern [ADR-015](015-privilege-model-and-authority-context.md) killed. alkcall ADR-024 replaces the flat overlay with peer-keyed overlays + `PeerRef` routing, and retires `remote_safe`/ `trusted_peer` in favor of `AccessControl::check(peer_identity)` — the existing authorization path that was already in the dispatch path. The peer- scoping question this section flagged is now answered structurally (peer-keyed overlays), not by a parallel boolean gate. ### DC-4 — OperationAdapter trait error type: resolved §5 showed `async fn import(&self) -> Vec` with no error type. The trait returns `Result, AdapterError>` where `AdapterError` is a crate-level enum. The *presence* of the error type is recorded in the alkcall crate's `client-and-adapters.md`; the exact variants are the two-way-door remainder, tracked as alknet OQ-26. ### DC-2 — from_call re-import on reconnection: manual free function Assumption 4 noted re-import "happens on reconnection or is triggered explicitly." The decision is **manual**: `from_call` is a free function; the assembly layer calls it after establishing the connection. The overlay is per-connection (Layer 2, alkcall ADR-024/ADR-019), so re-import on reconnect is naturally scoped; a stale overlay dies with the connection. A `CallConnection::refresh()` method for mid-connection re-discovery is a genuine feature addition — non-breaking, additive — if a deployment needs manual re-discovery without drop-and-reconnect. Two-way door; recorded in the alkcall crate's `client-and-adapters.md`; tracked as alknet OQ-27. See alknet ADR-069 (from_call is a manual free function; alkcall ADR-028) for the full rationale. ### DC-3 — from_call namespace collision: default set §3's `FromCallConfig` namespace prefix is **optional, default no prefix, collision = error**. A node importing from two remotes that both expose the same unprefixed op name should fail loudly. The operator adds prefixes when importing from multiple sources. Two-way door; recorded in the alkcall crate's `client-and-adapters.md`; tracked as alknet OQ-28. ### Operational spec The gap this ADR left to implementation — the `CallClient` API, the `from_call` flow, the trait signature, the adapter location map, the no-env-vars invariant, and the exchange-of-operations pattern — is specified in the alkcall crate's `client-and-adapters.md` (alknet mono-repo: `crates/call/client-and-adapters.md`). That document is the operational complement to this ADR; this ADR remains the architectural authority. ## Amendments (2026-07-09) ### `from_jsonschema` clause superseded by ADR-066 The §5 `FromJsonSchema` implementation listing ("schema-only, no handler") is **superseded by [ADR-066](066-from-jsonschema-as-http-adapter.md)**. `from_jsonschema` is now an HTTP-backed single-endpoint adapter in alkhttp (reqwest forwarding handler, same shape as `from_openapi`), not a schema-only placeholder in alkcall. The `FromJsonSchema` provenance variant stays in alkcall (`OperationProvenance`) but is now a handler-bearing leaf, not a "no handler" entry. The "schema-only, no handler" concept is removed — schema validation without a handler is served by consuming `OperationSpec` directly. The §5 "adapters live in alkcall" one-way-door statement is corrected above to "the adapter trait lives in alkcall; implementations live with their transport." See [ADR-066](066-from-jsonschema-as-http-adapter.md) and the alkcall crate's `client-and-adapters.md` §"from_jsonschema". ## Amendments (2026-07-13) ### `CallClient` transport-agnostic API (mirrors alknet ADR-080's amendment) The §1 Decision framed `CallClient::connect(addr: SocketAddr, credentials)` as the primary constructor and described it as "opens a QUIC connection." The operational spec (the alkcall crate's `client-and-adapters.md`) framed `spawn_dispatch(connection)` as the "lower-level API" that `connect()` uses after the dial. That framing welded the client-side one-way-door API to QUIC — the same welding alknet ADR-065 unwound on the accept side (alkcall ADR-007) and alknet ADR-080 corrected for `ChannelClient` (alkcall ADR-043). The call protocol is transport-agnostic (alkcall ADR-015 EventEnvelope framing; alknet ADR-065 `Connection::from_stream`/`from_bidi` — alkcall ADR-007 — accept any `AsyncRead + AsyncWrite`). The connect side is half of that protocol and must not be coupled to a transport. This amendment reframes the existing code (which already has the right structure — `spawn_dispatch` is not feature-gated, `connect` is `#[cfg(feature = "quinn")]`): - **`CallClient::spawn_dispatch(connection: Connection)`** — the transport-agnostic primary constructor and the one-way-door API. Takes a pre-established `Connection` (any transport), spawns the shared dispatch loop, returns a live `CallConnection`. Mirrors the accept-side `CallAdapter::handle(Connection)` and `ChannelClient::from_connection` (alkcall ADR-043). - **`CallClient::connect(addr, credentials)`** — ~~a QUIC convenience constructor~~ **REMOVED per alknet ADR-089 §5 (2026-07-16; alkcall ADR-045)**. The dial is centralized in `AlknetClient` (the native client dial seam, alkcall ADR-045); `connect` is deleted, not retained as a two-way-door convenience, to avoid alkcall depending on the client-dial crate and to let alkcall shed its TLS/transport deps. Callers compose `AlknetClient::dial_quic(...).await?` + `CallClient::new(...).spawn_dispatch(conn)`. The door-type classification is updated: `spawn_dispatch` is one-way (the handler-facing surface); ~~`connect` is two-way (additive convenience)~~ `connect` is **removed** (alknet ADR-089 §5). The `AlknetClient` extraction (alknet OQ-55) is **resolved** by alknet ADR-089 — the shared dial seam is the client-dial crate; `spawn_dispatch` is the protocol-crate take-over that consumes the dial's `Connection`. See the alkcall crate's `client-and-adapters.md` §"CallClient" for the reframed operational spec. ## Port notes - Renames: "alknet-http" → alkhttp; "alknet-core"/"alknet-call" → alkcall; "alknet node"/"non-alknet systems" → "alk node"/"non-alk systems". - ALPN correction: `alknet/call` → `alk/call` (alkcall ADR-004 renamed the ALPN convention to the `alk/` namespace). - Producer/consumer terminology: "server side"/"client side" (§Context, §1, §6) → "accept side"/"connect side" for the connection-establishment half; "who advertises"/"who opens connection" table retained (it was already direction-of-establishment language); §2's closing sentence now says the protocol does not distinguish producer and consumer after connection establishment. "Node.js and Python clients" → "consumers". HTTP/MCP inherent directionality is untouched ("HTTP clients", "MCP servers", "MCP clients" keep their names — those are protocol roles of the external systems, not call-protocol roles). - `Subscription` → `Sub` (alkcall renamed `OperationType::Subscription` to `Sub`; alkcall ADR-046 added `OperationType::Pub` — producer→consumer streaming via `call.published`, `HandlerKind::Sink`). §1's `subscribe()` description, §3's "or streams for subscriptions", and the Consequences "subscriptions → SSE long-poll" example now say `Sub`. - §4 corrected to name the concrete alkhttp realizations: `to_openapi` is the OpenAPI gateway pattern (ADR-042 — five fixed gateway endpoints, not one path per operation) and `to_mcp` is the MCP tool-gateway pattern (ADR-041 — four fixed gateway tools). The original predates those ADRs; the decision content (outbound projections of `External` ops) is unchanged. - Assumption 5 correction: "a separate HTTP handler (alknet-http)" → "an HTTP host (alkhttp) ... the gateway dispatch" — the direct-call per-operation HTTP surface was removed by ADR-047; the gateway is the sole invoke path. - Amendment remappings (verified alknet→alkcall ADR mapping): alknet ADR-028 → alkcall ADR-023; alknet ADR-029 → alkcall ADR-024; alknet ADR-016 (abort cascade) → alkcall ADR-020; alknet ADR-024 (registry layering) → alkcall ADR-019; alknet ADR-065 (`Connection::from_stream`) → alkcall ADR-007; alknet ADR-080 (`ChannelClient`) → alkcall ADR-043; alknet ADR-089 (dial seam) → alkcall ADR-045; alknet ADR-012 (stream model) → alkcall ADR-015; alknet ADR-069 (from_call manual free function) → alkcall ADR-028; alknet ADR-013 (Rust canonical) → alkcall ADR-033; alknet ADR-009 (one-way door framework) → alkcall ADR-032. The 2026-07-13 amendment title's "mirrors ADR-080's amendment" cites the alknet number (the amendment mirrored the alknet-record ADR-080). - All `client-and-adapters.md` relative links converted to textual "the alkcall crate's `client-and-adapters.md`" references (the document lives in the alkcall crate's docs/architecture/). - alknet OQ numbers (OQ-15, OQ-25..28, OQ-55) are alknet-record citations, annotated as such — alkcall's OQ numbering differs (e.g., alkcall OQ-25 is BiStream type definition, not the DC remainders). - The `QuicCallCredentials`-shaped `connect(addr, credentials)` signature in §1 is retained as decision history (the 2026-07-16 amendment removes `connect`); the QUIC-specific wording "opens a QUIC connection" is softened to "opens a connection" in the §1 prose per the transport-agnostic amendment, with the amendment block recording the original framing. - Status line: "per ADR-089 §5" annotated as alknet ADR-089 (alkcall ADR-045) — the amendment citation refers to the alknet-record number. - No decision content changed — the shared dispatch loop, the connection-direction independence, the adapter contract trait, the cross-node abort cascade, the credential-source rule, and all three amendment blocks are verbatim from the alknet ADR modulo the mechanical corrections logged above.