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
+670
-383
No files matched your search
@@ -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
|
||||
implementation-state audit, the downstream unblock chain
|
||||
- `docs/research/alknet-call-peer-routing/findings.md` — the peer-graph
|
||||
routing research that identified ADR-023's structural gap and validated
|
||||
the ADR-024 design via POC
|
||||
- Source-repo research (alknet, provenance — these paths live in
|
||||
`/workspace/@alkdev/alknet/docs/research/`, not this repo):
|
||||
- `alknet-call-completion/gap-analysis.md` — DC-1..4, the
|
||||
implementation-state audit, the downstream unblock chain
|
||||
- `alknet-call-peer-routing/findings.md` — the peer-graph
|
||||
routing research that identified ADR-023's structural gap and validated
|
||||
the ADR-024 design via POC
|
||||
- `alknet-ssh/phase-0-findings.md` — SSH Phase 0; confirms ssh depends on
|
||||
the vendored core types, not alkcall's adapters, so it proceeds in
|
||||
parallel with the completion
|
||||
- `/workspace/@alkdev/operations/` — TypeScript prior art (`from_openapi.ts`,
|
||||
`from_mcp.ts`, `from_schema.ts`, `scanner.ts`)
|
||||
- `/workspace/@alkdev/dispatch/` — concrete downstream consumer (container
|
||||
@@ -864,7 +886,4 @@ See [open-questions.md](open-questions.md) for full details.
|
||||
- `/workspace/aisdk/` — downstream consumer (Rust port of Vercel AI SDK); the
|
||||
no-env-vars invariant makes its `std::env::var` reads unreachable
|
||||
- `/workspace/rust-sdk/` — MCP Rust SDK (rmcp); streamable HTTP transport for
|
||||
`alknet-http`'s `from_mcp`/`to_mcp` (separate crate, separate Phase 0)
|
||||
- `docs/research/alknet-ssh/phase-0-findings.md` — alknet-ssh Phase 0;
|
||||
confirms ssh depends on alknet-core not alknet-call's adapters, so it
|
||||
proceeds in parallel with this completion
|
||||
`alkhttp`'s `from_mcp`/`to_mcp`
|
||||
Reference in new issue
Block a user