docs(arch): resolve call-crate OQs, promote OQ-29 to load-bearing on ADR-030

Resolve the call-crate open questions where the decision is made —
OQ-27 (auto-re-import), OQ-28 (same-peer collision = error), OQ-30
(PeerRef::Any insertion-order first-match), OQ-31 (services/list-peers
opt-in). These were previously marked 'open' with 'v1' hedging language
despite having a decided default. What remains (refresh(), richer routing,
services/list-peers the op) is genuine feature addition, not unmade
architecture.

Reframe OQ-32 (multi-hop) as a feature extension rather than a 'v1'
deferral — the one-hop model is the architectural commitment; extending
to multi-hop doesn't break downstream.

Promote OQ-29 (CallClient TLS client-auth) from medium to high priority
and surface its real interaction with ADR-030. Previously framed as
'additive — two-way-door remainder,' but ADR-030's PeerEntry fingerprint
→ peer_id resolution requires the client to present a TLS client cert.
With with_no_client_auth(), no fingerprint is extracted, the PeerEntry
path is dormant, and PeerCompositeEnv keys on None or the API-key prefix
instead of the stable peer_id. This is the activation path for ADR-030's
primary use case, not an additive feature. Three options laid out: (a)
wire client-auth with the ADR-029 migration, (b) ship token-only and
switch later (the 'compounds into a mess' path), (c) extend PeerEntry
to cover auth_token-based identity. Requires a decision before the
migration lands.

Clarify OQ-36 (concrete adapter shapes): the trait shapes and in-memory
adapters ship with core — the deferral is only for the persistence
adapters (SQLite, etc.). The in-memory adapters are real implementations
of a full repo pattern, not stubs.

Update call_client.rs source comment to reference OQ-29 instead of the
'v1' / 'two-way-door remainder' framing.

Workspace green: 326 tests pass, build clean.
This commit is contained in:
2026-06-28 05:35:52 +00:00
parent f224ea998c
commit 1d94aaea51
5 changed files with 224 additions and 151 deletions

View File

@@ -207,17 +207,21 @@ fn build_quinn_client_config(
_credentials: &CallCredentials, _credentials: &CallCredentials,
alpn: &[u8], alpn: &[u8],
) -> Result<quinn::ClientConfig, String> { ) -> Result<quinn::ClientConfig, String> {
// v1 connects without client-auth TLS identity: the server-side // TODO(OQ-29): connects without client-auth TLS identity. The server-side
// `AcceptAnyCertVerifier` (in alknet-core::endpoint) does not require or // `AcceptAnyCertVerifier` (in alknet-core::endpoint) requests but does not
// verify client certs, so a client cert is not needed to establish a // verify client certs, so a client cert is not needed to establish a
// connection. Wiring the local node's RawKey/X509 identity as a quinn // connection. However, without a client cert, the server cannot extract a
// client-auth cert (for servers that *do* verify client identity) is a // fingerprint, so `IdentityProvider::resolve_from_fingerprint` returns
// two-way-door remainder — the `credentials.tls_identity` field is // None and the peer gets no stable `PeerEntry.peer_id` (ADR-030). This is
// carried through `CallCredentials` so the assembly layer can populate // load-bearing on ADR-030's peer-identity model — see OQ-29 for the
// it, and a future task plugs it into the rustls client config. The // decision needed before the ADR-029 migration lands.
// one-way constraint (credentials from Capabilities, not env vars, //
// ADR-014) is unaffected: the auth_token dimension flows through the // The `credentials.tls_identity` field is carried through `CallCredentials`
// call-protocol `auth_token` payload field, not TLS. // so the assembly layer can populate it; wiring it into the rustls client
// config is the missing piece. The one-way constraint (credentials from
// `Capabilities`, not env vars, ADR-014) is unaffected: the `auth_token`
// dimension flows through the call-protocol `auth_token` payload field,
// not TLS.
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider()); let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let mut config = rustls::ClientConfig::builder_with_provider(provider) let mut config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions() .with_safe_default_protocol_versions()

View File

@@ -114,16 +114,18 @@ See [open-questions.md](open-questions.md) for the full tracker.
- **OQ-34**: ~~Persistent peer registry~~**resolved by ADR-030 + ADR-031 + ADR-033** (storage boundary: core defines repo traits + in-memory defaults; persistence adapters are separate crates) - **OQ-34**: ~~Persistent peer registry~~**resolved by ADR-030 + ADR-031 + ADR-033** (storage boundary: core defines repo traits + in-memory defaults; persistence adapters are separate crates)
- **OQ-35**: API key identity vs peer identity — resolved (recorded by ADR-030; the asymmetry between fingerprint and API-key paths is deliberate) - **OQ-35**: API key identity vs peer identity — resolved (recorded by ADR-030; the asymmetry between fingerprint and API-key paths is deliberate)
**Open (two-way-door remainders from alknet-call completion + peer-graph routing):** **Resolved by the call-completion / ADR-029 work:**
- **OQ-25**: ~~Remote-safe marking shape~~**dissolved by ADR-029** (no marking; peer authorization is `AccessControl::check(peer_identity)`) - **OQ-27**: ~~`from_call` re-import trigger~~**resolved** (auto-re-import on connection establishment; `refresh()` is a feature addition)
- **OQ-26**: ~~`OperationAdapter` error type~~**resolved** (`AdapterError` variants: `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`; `#[non_exhaustive]`) - **OQ-28**: ~~`from_call` namespace collision~~**resolved** (same-peer collision = error; cross-peer dissolved by ADR-029)
- **OQ-27**: `from_call` re-import trigger — v1 default auto-on-reconnect; explicit `refresh()` additive - **OQ-30**: ~~`PeerRef::Any` routing policy~~**resolved** (insertion-order first-match; richer routing is a feature extension)
- **OQ-28**: `from_call` namespace collision — cross-peer **dissolved by ADR-029** (separate sub-overlays); same-peer stays error - **OQ-31**: ~~`services/list-peers` re-export semantics~~ **resolved** (opt-in `services/list-peers`; `services/list` is "own ops only")
- **OQ-29**: `CallClient` TLS client-auth — v1 `with_no_client_auth()` + `AcceptAnyServerCertVerifier`; wiring RawKey client-auth is additive
- **OQ-30**: `PeerRef::Any` routing policy — v1 insertion-order first-match; round-robin/least-loaded is future (ADR-029) **Open (requires decision before ADR-029 migration lands):**
- **OQ-31**: `services/list-peers` re-export semantics — v1 "own ops only"; `services/list-peers` is opt-in (ADR-029) - **OQ-29**: `CallClient` TLS client-auth — **promoted to high priority, load-bearing on ADR-030**. Not "additive" as previously framed — it's the activation path for the `PeerEntry` fingerprint → `peer_id` resolution. Without it, `PeerCompositeEnv` keys on `None` or the API-key prefix, not the stable `peer_id`. See OQ-29 for the three options (wire client-auth with the migration / ship token-only / extend PeerEntry to cover auth_token).
- **OQ-32**: Multi-hop federation — v1 one-hop; peer-keyed model extends without redesign; petgraph candidate (ADR-029)
- **OQ-36**: Concrete adapter shapes — open (deferred for exploration; the repo/adapter pattern is committed by ADR-033, the concrete adapter shapes are not) **Open (feature extensions, not blocking):**
- **OQ-32**: Multi-hop federation — the one-hop model is the architectural commitment; multi-hop is a feature extension that doesn't break downstream
- **OQ-36**: Concrete adapter shapes — the repo/adapter pattern is committed (ADR-033); concrete adapter shapes are deferred for exploration. Note: the trait shapes and in-memory adapters must ship with core (per the project's clarification) — the deferral is for the persistence adapters (SQLite, etc.), not the core traits
**Deferred (not active):** **Deferred (not active):**
- **OQ-09**: WASM target boundaries — design constraint, not deliverable - **OQ-09**: WASM target boundaries — design constraint, not deliverable

View File

@@ -1,7 +1,7 @@
--- ---
status: draft status: draft
last_updated: 2026-06-26 last_updated: 2026-06-27
review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/014/015/016/017/022/023/024), security, and pattern-consistency checks all conformant; 159 unit/integration tests green; `cargo build`, `cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo test` clean. Call-completion gap (ADR-017 client/adapter surface) addressed 2026-06-26 ADR-028 + client-and-adapters.md added; implementation pending. review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/014/015/016/017/022/023/024), security, and pattern-consistency checks all conformant; 159 unit/integration tests green; `cargo build`, `cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo test` clean. Call-completion gap (ADR-017 client/adapter surface) addressed 2026-06-26; ADR-029 migration pending.
--- ---
# alknet-call # alknet-call
@@ -40,6 +40,9 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
| [024](../../decisions/024-operation-registry-layering.md) | Operation Registry Layering | Curated (static) + session/connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) | | [024](../../decisions/024-operation-registry-layering.md) | Operation Registry Layering | Curated (static) + session/connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) |
| [028](../../decisions/028-callclient-peer-scoped-registry-filtering.md) | ~~Peer-Scoped Registry Filtering~~ | ~~Accepted~~**Superseded** by ADR-029 (flat-namespace single-peer model couldn't express head→N-workers; parallel auth system duplicated `AccessControl`) | | [028](../../decisions/028-callclient-peer-scoped-registry-filtering.md) | ~~Peer-Scoped Registry Filtering~~ | ~~Accepted~~**Superseded** by ADR-029 (flat-namespace single-peer model couldn't express head→N-workers; parallel auth system duplicated `AccessControl`) |
| [029](../../decisions/029-peer-graph-routing-model.md) | Peer-Graph Routing Model | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` | | [029](../../decisions/029-peer-graph-routing-model.md) | Peer-Graph Routing Model | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` |
| [030](../../decisions/030-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | `PeerId` source = `Identity.id` = `PeerEntry.peer_id` (stable); supersedes ADR-029's UUID source |
| [032](../../decisions/032-forwarded-for-identity.md) | Forwarded-For Identity | `forwarded_for` on `OperationContext` and `call.requested`; metadata only, never used by `AccessControl::check` |
| [033](../../decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Storage Boundary and Repo/Adapter Pattern | Core defines repo traits + in-memory defaults; persistence adapters are separate crates |
## Relevant Open Questions ## Relevant Open Questions
@@ -52,14 +55,14 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
| OQ-19 | Session-scoped operation registries | resolved | Agent-written operations overlaid on curated registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. Generalized by ADR-024 to cover connection-scoped overlays. | | OQ-19 | Session-scoped operation registries | resolved | Agent-written operations overlaid on curated registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. Generalized by ADR-024 to cover connection-scoped overlays. |
| OQ-25 | ~~Remote-safe marking shape~~ | **dissolved** (ADR-029) | `remote_safe`/`trusted_peer` retired; peer authorization is `AccessControl::check(peer_identity)` | | OQ-25 | ~~Remote-safe marking shape~~ | **dissolved** (ADR-029) | `remote_safe`/`trusted_peer` retired; peer authorization is `AccessControl::check(peer_identity)` |
| OQ-26 | OperationAdapter error type (AdapterError variants) | **resolved** | `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`; `#[non_exhaustive]` | | OQ-26 | OperationAdapter error type (AdapterError variants) | **resolved** | `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`; `#[non_exhaustive]` |
| OQ-27 | from_call re-import trigger | open (two-way) | v1 default: auto-on-reconnect; explicit `refresh()` additive | | OQ-27 | from_call re-import trigger | **resolved** | Auto-re-import on connection establishment; `refresh()` is a feature addition |
| OQ-28 | from_call namespace collision | cross-peer **dissolved** (ADR-029) / same-peer stays | Cross-peer: separate sub-overlays, no collision. Same-peer: error. `namespace_prefix` is local-naming sugar | | OQ-28 | from_call namespace collision | **resolved** | Same-peer collision = error; cross-peer dissolved by ADR-029 (separate sub-overlays) |
| OQ-29 | CallClient TLS client-auth and remote-identity verification | open (two-way) | v1 `with_no_client_auth()` + `AcceptAnyServerCertVerifier`; wiring RawKey client-auth is additive (orthogonal to ADR-029) | | OQ-29 | CallClient TLS client-auth | **open (high, load-bearing on ADR-030)** | NOT "additive" — activates the `PeerEntry` fingerprint → `peer_id` path. Requires decision before ADR-029 migration. |
| OQ-30 | `PeerRef::Any` routing policy | open (two-way) | v1 insertion-order first-match; round-robin/least-loaded is future (ADR-029) | | OQ-30 | `PeerRef::Any` routing policy | **resolved** | Insertion-order first-match; richer routing is a feature extension |
| OQ-31 | `services/list-peers` re-export semantics | open (two-way) | v1 "own ops only"; `services/list-peers` is opt-in (ADR-029) | | OQ-31 | `services/list-peers` re-export semantics | **resolved** | Opt-in `services/list-peers`; `services/list` is "own ops only" |
| OQ-32 | Multi-hop federation | open | v1 one-hop; peer-keyed model extends without redesign; petgraph candidate (ADR-029) | | OQ-32 | Multi-hop federation | open (feature extension) | One-hop model is the commitment; multi-hop is a feature extension, not a deferral |
| OQ-33 | PeerId — crypto identity vs stable logical id | **resolved** | Logical id (UUID v1), not `Identity.id`; decoupled from crypto for key-rotation-safe ACLs | | OQ-33 | PeerId — crypto identity vs stable logical id | **resolved** (ADR-030) | `PeerId = Identity.id = PeerEntry.peer_id` (stable across key rotation) |
| OQ-34 | Persistent peer registry (cross-node state storage) | open | Not a v1 blocker (UUID works); the no-DB posture's limit, tracked for deliberate future decision | | OQ-34 | Persistent peer registry | **resolved** (ADR-030+033) | Core trait + in-memory default; persistence adapters are separate crates |
## Key Design Principles ## Key Design Principles
@@ -74,6 +77,6 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
9. **Internal calls switch authority context, not skip ACL**: The `internal` flag marks composition-originated calls. ACL runs against the handler's composition authority, not the caller's and not as a blanket skip. Operations have External/Internal visibility. Scoped composition env bounds reachability. See ADR-015, ADR-022. 9. **Internal calls switch authority context, not skip ACL**: The `internal` flag marks composition-originated calls. ACL runs against the handler's composition authority, not the caller's and not as a blanket skip. Operations have External/Internal visibility. Scoped composition env bounds reachability. See ADR-015, ADR-022.
10. **Provenance determines composition capability**: Only `Local` and `Session` ops can compose. Leaves (`FromOpenAPI`, `FromMCP`, `FromCall`) are forwarding stubs — they don't get composition authority or a scoped env. The assembly layer is the sole grantor of composition authority. See ADR-022. 10. **Provenance determines composition capability**: Only `Local` and `Session` ops can compose. Leaves (`FromOpenAPI`, `FromMCP`, `FromCall`) are forwarding stubs — they don't get composition authority or a scoped env. The assembly layer is the sole grantor of composition authority. See ADR-022.
11. **Connection direction is independent of call direction**: Who opens the QUIC connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The `CallAdapter` accepts connections; the `CallClient` opens them; both produce the same `CallConnection` and dispatch through the same loop. See ADR-017, [client-and-adapters.md](client-and-adapters.md). 11. **Connection direction is independent of call direction**: Who opens the QUIC connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The `CallAdapter` accepts connections; the `CallClient` opens them; both produce the same `CallConnection` and dispatch through the same loop. See ADR-017, [client-and-adapters.md](client-and-adapters.md).
12. **CallClient registry is default-deny**: A `CallClient` exposes no operations to the remote peer unless explicitly marked remote-safe. Sharing the global registry is an explicit trusted-peer opt-in, never the default. This prevents a remote peer's call from triggering dispatch that populates `OperationContext.capabilities` from the local node's registration bundle. See ADR-028. 12. **Peer authorization via `AccessControl`**: A remote peer's call is authorized by `AccessControl::check(peer_identity)` against the op's `AccessControl` — the same mechanism that gates every other call. No `remote_safe` flag, no `trusted_peer` bypass. An op with `AccessControl::default()` is callable by any peer; an op with `required_scopes` is callable only by peers whose `Identity.scopes` satisfy them; an op with `Visibility::Internal` is never callable from the wire. See ADR-029.
13. **Adapter trait lives with the types; implementations live with their transport**: `OperationAdapter` is in `alknet-call`; `from_call`/`from_jsonschema` are in `alknet-call` (QUIC / pure parse); `from_openapi`/`from_mcp`/`to_openapi`/`to_mcp` are in `alknet-http` (reqwest / axum). `alknet-call` stays lean — no HTTP client, no HTTP server. See [client-and-adapters.md](client-and-adapters.md). 13. **Adapter trait lives with the types; implementations live with their transport**: `OperationAdapter` is in `alknet-call`; `from_call`/`from_jsonschema` are in `alknet-call` (QUIC / pure parse); `from_openapi`/`from_mcp`/`to_openapi`/`to_mcp` are in `alknet-http` (reqwest / axum). `alknet-call` stays lean — no HTTP client, no HTTP server. See [client-and-adapters.md](client-and-adapters.md).
14. **No handler reads outbound credentials from any source other than `OperationContext.capabilities`** (no-env-vars invariant): the credential injection path is vault → assembly layer → `Capabilities``HandlerRegistration.capabilities``OperationContext.capabilities` → handler. Downstream consumers' `std::env::var` reads are unreachable because the assembly layer never calls `Default::default()`. See ADR-014, [client-and-adapters.md](client-and-adapters.md). 14. **No handler reads outbound credentials from any source other than `OperationContext.capabilities`** (no-env-vars invariant): the credential injection path is vault → assembly layer → `Capabilities``HandlerRegistration.capabilities``OperationContext.capabilities` → handler. Downstream consumers' `std::env::var` reads are unreachable because the assembly layer never calls `Default::default()`. See ADR-014, [client-and-adapters.md](client-and-adapters.md).

View File

@@ -625,26 +625,30 @@ See [open-questions.md](../../open-questions.md) for full details.
- **OQ-26** (resolved): `AdapterError` variants — `DiscoveryFailed`, - **OQ-26** (resolved): `AdapterError` variants — `DiscoveryFailed`,
`SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision` `SchemaParse`, `Transport`, `Unauthorized`, `SamePeerCollision`
(replaces flat `Conflict`). `#[non_exhaustive]`. (replaces flat `Conflict`). `#[non_exhaustive]`.
- **OQ-27** (open, two-way): `from_call` re-import trigger — auto-on-reconnect - **OQ-27** (resolved): `from_call` re-import trigger — auto-re-import on
(v1 default, recorded here) vs explicit `CallConnection::refresh()`. v1 is connection establishment. `CallConnection::refresh()` is a feature
auto-on-reconnect; the explicit path is additive. The overlay is now addition, not an unmade decision.
peer-scoped (drops with the connection), so re-import is naturally scoped. - **OQ-28** (resolved): `from_call` namespace collision — same-peer
- **OQ-28** (cross-peer dissolved by ADR-029 / same-peer stays): Cross-peer collision = error; cross-peer dissolved by ADR-029 (separate sub-overlays).
collision dissolves — same name on different peers is fine (separate `namespace_prefix` is optional local-naming sugar.
sub-overlays). Same-peer collision stays an error. `namespace_prefix` is - **OQ-29** (open, **high priority, load-bearing on ADR-030**): `CallClient`
optional local-naming sugar, not the disambiguation mechanism. TLS client-auth — NOT "additive" as previously framed. ADR-030's
- **OQ-29** (open, two-way): `CallClient` TLS client-auth + remote-identity `PeerEntry` fingerprint → `peer_id` resolution requires the client to
verification — v1 connects with `with_no_client_auth()` and present a TLS client cert; `with_no_client_auth()` means no fingerprint,
`AcceptAnyServerCertVerifier`. Wiring RawKey client-auth is additive. no `PeerEntry` resolution, no stable `peer_id`. The `auth_token` path
Orthogonal to the routing model (ADR-029); `auth_token` flows through the resolves to `Identity.id = ApiKeyEntry.prefix`, not `peer_id`. See OQ-29
call-protocol payload, not TLS, so the no-env-vars invariant is unaffected. for the three options (wire client-auth with the migration / ship
- **OQ-30** (open, two-way): `PeerRef::Any` routing policy — v1 insertion-order token-only / extend PeerEntry to cover auth_token). Requires a decision
first-match; round-robin/least-loaded is the future extension (ADR-029 §2). before the ADR-029 migration lands.
- **OQ-31** (open, two-way): `services/list-peers` re-export semanticsv1 - **OQ-30** (resolved): `PeerRef::Any` routing policyinsertion-order
defaults to "own ops only"; `services/list-peers` is the opt-in (ADR-029 §6). first-match. A richer `RoutingPolicy` is a feature extension.
- **OQ-32** (open): Multi-hop federation — v1 is one-hop; the peer-keyed - **OQ-31** (resolved): `services/list-peers` — opt-in; `services/list`
overlay model extends to multi-hop without redesign; petgraph is the is "own ops only."
candidate if path-finding becomes real (ADR-029 §3.7). - **OQ-32** (open, feature extension): Multi-hop federation — the one-hop
model is the architectural commitment; multi-hop is a feature extension
that doesn't break downstream. The peer-keyed model extends to multi-hop
without redesign; petgraph is the candidate if path-finding becomes real
(ADR-029 §3.7).
- **OQ-33** (resolved by ADR-030): `PeerId` is a logical id. Source is - **OQ-33** (resolved by ADR-030): `PeerId` is a logical id. Source is
`Identity.id` from `IdentityProvider` resolution (= `PeerEntry.peer_id`, `Identity.id` from `IdentityProvider` resolution (= `PeerEntry.peer_id`,
stable across key rotation), not a connection-assigned UUID. The UUID stable across key rotation), not a connection-assigned UUID. The UUID
@@ -657,11 +661,10 @@ See [open-questions.md](../../open-questions.md) for full details.
asymmetry between the fingerprint path (gets `PeerEntry` id-decoupling) asymmetry between the fingerprint path (gets `PeerEntry` id-decoupling)
and the API-key path (doesn't) is deliberate. See OQ-35 in and the API-key path (doesn't) is deliberate. See OQ-35 in
open-questions.md. open-questions.md.
- **OQ-36** (tracked by ADR-033): Concrete adapter shapes — the repo/adapter - **OQ-36** (open, deferred for exploration): Concrete persistence adapter
pattern is committed (core trait + in-memory default; persistence adapters shapes — the repo/adapter pattern is committed (ADR-033); the in-memory
are separate crates); the concrete adapter shapes (table schemas, backend adapters ship with core; the persistence adapter shapes (SQLite, etc.)
choice, indexing) are deferred for exploration. See OQ-36 in are deferred for exploration. See OQ-36 in open-questions.md.
open-questions.md.
## References ## References

View File

@@ -323,9 +323,10 @@ These open questions are the remainders from the call-completion gap analysis
(`docs/research/alknet-call-completion/gap-analysis.md`, DC-1..4) and the (`docs/research/alknet-call-completion/gap-analysis.md`, DC-1..4) and the
peer-graph routing research (`docs/research/alknet-call-peer-routing/findings.md`). peer-graph routing research (`docs/research/alknet-call-peer-routing/findings.md`).
ADR-029 supersedes ADR-028 and dissolves OQ-25 and the cross-peer half of ADR-029 supersedes ADR-028 and dissolves OQ-25 and the cross-peer half of
OQ-28; the remaining two-way-door shape/defaults are recorded in OQ-28. Most of the remaining OQs are now resolved (decisions made, defaults
[client-and-adapters.md](crates/call/client-and-adapters.md) and may be recorded). OQ-29 is the exception — it's load-bearing on ADR-030 and
revisited during implementation without a new ADR. requires a decision before the ADR-029 migration lands. OQ-32 (multi-hop)
is a feature extension, not an unmade architecture decision.
### OQ-25: ~~Remote-Safe Marking Shape for CallClient Peer-Scoped Filtering~~ (Dissolved by ADR-029) ### OQ-25: ~~Remote-Safe Marking Shape for CallClient Peer-Scoped Filtering~~ (Dissolved by ADR-029)
@@ -373,122 +374,170 @@ revisited during implementation without a new ADR.
### OQ-27: from_call Re-Import Trigger ### OQ-27: from_call Re-Import Trigger
- **Origin**: [client-and-adapters.md](crates/call/client-and-adapters.md), ADR-017 Assumption 4 - **Origin**: [client-and-adapters.md](crates/call/client-and-adapters.md), ADR-017 Assumption 4
- **Status**: open - **Status**: **resolved** (2026-06-27)
- **Door type**: Two-way - **Door type**: Two-way
- **Priority**: low - **Priority**: low
- **Resolution**: ADR-017 Assumption 4 noted re-import "happens on - **Resolution**: The decision is **auto-re-import on connection
reconnection or is triggered explicitly." The v1 default is establishment**. The overlay is per-connection (Layer 2, ADR-024), so a
**auto-re-import on connection establishment**. The overlay is stale overlay dies with the connection; re-import on reconnect is
per-connection (Layer 2, ADR-024), so a stale overlay dies with the naturally scoped to the new connection. This is the right default for the
connection; re-import on reconnect is naturally scoped to the new runner pattern (a worker reconnects → the hub re-discovers the worker's
connection. This is the right default for the runner pattern (a worker ops automatically). An explicit `CallConnection::refresh()` method is a
reconnects → the hub re-discovers the worker's ops automatically). genuine feature addition — non-breaking, additive — if a deployment
Explicit re-import via a future `CallConnection::refresh()` method is needs manual control.
additive and can be added if a deployment needs manual control. Reversal
is cheap; no ADR needed.
- **Cross-references**: ADR-017, ADR-024, [client-and-adapters.md](crates/call/client-and-adapters.md) - **Cross-references**: ADR-017, ADR-024, [client-and-adapters.md](crates/call/client-and-adapters.md)
### OQ-28: from_call Namespace Collision Behavior ### OQ-28: from_call Namespace Collision Behavior
- **Origin**: [client-and-adapters.md](crates/call/client-and-adapters.md), ADR-017 §3 - **Origin**: [client-and-adapters.md](crates/call/client-and-adapters.md), ADR-017 §3
- **Status**: open - **Status**: **resolved** (2026-06-27)
- **Door type**: Two-way - **Door type**: Two-way
- **Priority**: low - **Priority**: low
- **Resolution**: ADR-017 §3's `FromCallConfig` namespace prefix is - **Resolution**: ADR-017 §3's `FromCallConfig` namespace prefix is
**optional, default no prefix, collision = error**. A node importing from **optional, default no prefix, same-peer collision = error**. A node
two remotes that both expose `/container/exec` without prefixes should fail importing from a peer that exposes two ops with the same name should fail
loudly rather than silently overwrite. The operator adds prefixes when they loudly rather than silently overwrite. This matches the default-deny,
know they're importing from multiple sources. This matches the explicit-allow posture (ADR-015). The alternative (last-wins) would
default-deny, explicit-allow posture (ADR-015, ADR-028). Reversal is cheap; silently mask one op behind another, which is the kind of surprise the
no ADR needed. The alternative (last-wins) would silently mask one
remote's op behind another's, which is the kind of surprise the
default-deny posture exists to avoid. default-deny posture exists to avoid.
**Cross-peer collision dissolved by ADR-029.** Under the peer-keyed overlay **Cross-peer collision dissolved by ADR-029.** Under the peer-keyed
model, same name on different peers is fine — they live in separate overlay model, same name on different peers is fine — they live in
peer sub-overlays, no collision, no prefix needed. The collision rule now separate peer sub-overlays, no collision, no prefix needed.
stays only *within* a peer (same name on the same peer is still an error — `FromCallConfig::namespace_prefix` is optional local-naming sugar for
a peer shouldn't expose two ops with the same name). `FromCallConfig::namespace_prefix` when the importing node wants to expose a peer's ops under a different
becomes optional local-naming sugar, not the disambiguation mechanism. See name *locally* — a local-naming concern, not a disambiguation concern.
ADR-029 §5. See ADR-029 §5.
- **Cross-references**: ADR-015, ADR-017, ~~ADR-028~~ (superseded), ADR-029, - **Cross-references**: ADR-015, ADR-017, ~~ADR-028~~ (superseded), ADR-029,
[client-and-adapters.md](crates/call/client-and-adapters.md) [client-and-adapters.md](crates/call/client-and-adapters.md)
### OQ-29: CallClient TLS Client-Auth and Remote-Identity Verification ### OQ-29: CallClient TLS Client-Auth and Remote-Identity Verification
- **Origin**: [client-and-adapters.md](crates/call/client-and-adapters.md), ADR-017 §7 - **Origin**: [client-and-adapters.md](crates/call/client-and-adapters.md), ADR-017 §7
- **Status**: open - **Status**: **open — load-bearing on ADR-030** (not "additive" as previously framed)
- **Door type**: Two-way - **Door type**: One-way (identity model interaction), two-way (mechanism)
- **Priority**: medium - **Priority**: **high** (was medium; promoted — this is the activation path
- **Resolution**: v1 `CallClient::connect()` builds the quinn client config for ADR-030's `PeerEntry` fingerprint → `peer_id` resolution)
with `with_no_client_auth()` and an `AcceptAnyServerCertVerifier` — the - **Resolution**: **Previously framed as "additive — two-way-door
client does not present its TLS identity (`credentials.tls_identity`) as a remainder." That framing is incorrect.** ADR-030 makes `PeerId =
client cert, and does not pin the remote's expected identity from Identity.id = PeerEntry.peer_id` on the fingerprint path. But the
`credentials.remote_identity`. The server-side fingerprint path requires the client to present a TLS client cert, and
`AcceptAnyCertVerifier` (in alknet-core's endpoint) does not require or the current `CallClient::connect()` uses `with_no_client_auth()` — no
verify client certs, so a client cert is not needed to establish a client cert is presented, no fingerprint is extracted by the server's
connection in v1. Wiring the local node's RawKey/X509 identity as a rustls `AcceptAnyCertVerifier`, and `IdentityProvider::resolve_from_fingerprint`
client-auth cert (for servers that *do* verify client identity) and returns `None`. The peer gets no `PeerId` from the fingerprint path.
plugging `credentials.remote_identity` into a real `ServerCertVerifier` is
additive — a two-way-door remainder surfaced during implementation. The `auth_token` path (`resolve_from_token`) still works, but it
**The one-way constraint (credentials from `Capabilities`, not env vars, resolves to `Identity.id = ApiKeyEntry.prefix` (the API-key identity
ADR-014) is unaffected**: the `auth_token` dimension flows through the path), **not** to `PeerEntry.peer_id`. So with TLS client-auth unwired,
call-protocol `auth_token` payload field, not TLS, so the no-env-vars a calling peer's `PeerId` is either `None` (no client cert) or an
invariant holds independently of this gap. Decided during a future task that API-key prefix (if an `auth_token` is used) — neither is the stable
wires RawKey client-auth; recorded here, not in a full ADR. `PeerEntry.peer_id` that ADR-030 commits. The PeerEntry path is dormant
- **Cross-references**: ADR-014, ADR-017, ADR-027, [client-and-adapters.md](crates/call/client-and-adapters.md), [endpoint.md](crates/core/endpoint.md) until client-auth is wired.
This is not a "two-way-door remainder" — it's the activation path for
ADR-030's primary use case (stable `peer_id` across key rotation for
peer-keyed overlays). The decision to make is:
- **(a)** Wire TLS client-auth as part of the ADR-029 migration, so the
fingerprint → `PeerEntry``peer_id` path is live from day one. The
server's `AcceptAnyCertVerifier` already requests (but doesn't verify)
client certs; the client's `with_no_client_auth()` is the gap. Wiring
the local node's `RawKey`/`X509` identity as a rustls client-auth cert
is the missing piece. Remote-identity verification (plugging
`credentials.remote_identity` into a real `ServerCertVerifier`) is
genuinely additive — the server-side fingerprint extraction is what
matters for `PeerId`, not the client-side verification of the server.
- **(b)** Ship the ADR-029 migration with `auth_token`-only peer identity
and treat TLS client-auth as a follow-up. This means `PeerCompositeEnv`
keys on `Identity.id = ApiKeyEntry.prefix` (the token prefix) until
client-auth is wired, then switches to `PeerEntry.peer_id` when it is.
The switch is a behavior change for any deployment that built on the
token-prefix identity — the `PeerId` changes from the prefix to the
`peer_id`. This is the "compounds into a mess" path.
- **(c)** Extend `PeerEntry` to also cover `auth_token`-based peer
identity — a peer entry keyed by token prefix (or a `PeerEntry.token`
field) instead of (or alongside) fingerprint. This unifies the two
identity paths under `PeerEntry`, so the `PeerId` is stable regardless
of which credential path the peer used. This is a design change to
ADR-030, not just an implementation choice.
**The X.509 / raw-key wrinkle:** the vast majority of end users will use
Ed25519 raw keys (RFC 7250) — the same key type as SSH keys, native to
iroh's `NodeId` model. The fingerprint format for raw keys is
`ed25519:<hex>`. For X.509 (public-facing endpoints like
`api.alk.dev`, relays), the fingerprint is `SHA256:<hex of DER>` — a
different format, a different key type, but the same `PeerEntry.fingerprint`
field. The `IdentityProvider::resolve_from_fingerprint` path is
format-agnostic (it's a string match against `PeerEntry.fingerprint`),
so both key types work once client-auth is wired. The wrinkle is on the
client side: presenting an Ed25519 raw key as a TLS client cert uses a
different rustls path than presenting an X.509 cert. Both are supported
by rustls; the `CallCredentials.tls_identity` field already carries the
`TlsIdentity` enum (RawKey / X509). The wiring is per-variant.
**Not decided yet.** This OQ is promoted to high priority and requires a
decision before the ADR-029 migration lands. The previous "additive,
two-way-door remainder" framing is struck.
- **Cross-references**: ADR-014, ADR-017, ADR-027, ADR-029, ADR-030,
[client-and-adapters.md](crates/call/client-and-adapters.md),
[endpoint.md](crates/core/endpoint.md), [auth.md](crates/core/auth.md)
### OQ-30: PeerRef::Any Routing Policy ### OQ-30: PeerRef::Any Routing Policy
- **Origin**: [ADR-029](decisions/029-peer-graph-routing-model.md) §2, [client-and-adapters.md](crates/call/client-and-adapters.md), `docs/research/alknet-call-peer-routing/findings.md` §3.2 - **Origin**: [ADR-029](decisions/029-peer-graph-routing-model.md) §2, [client-and-adapters.md](crates/call/client-and-adapters.md), `docs/research/alknet-call-peer-routing/findings.md` §3.2
- **Status**: open - **Status**: **resolved** (2026-06-27)
- **Door type**: Two-way - **Door type**: Two-way
- **Priority**: low - **Priority**: low
- **Resolution**: v1 `PeerRef::Any` uses insertion-order first-match — - **Resolution**: `PeerRef::Any` uses **insertion-order first-match**
deterministic but order-dependent (worker A connects before worker B → `Any` deterministic but order-dependent (worker A connects before worker B →
routes to A until A disconnects). This is the simplest routing policy and is `Any` routes to A until A disconnects). This is the simplest routing
correct for the immediate use case (the head picks the first worker that policy and is correct for the immediate use case (the head picks the
serves the op). A richer `RoutingPolicy` (round-robin, least-loaded, first worker that serves the op). A richer `RoutingPolicy` (round-robin,
affinity) is the two-way-door remainder; the `PeerRef` enum is designed to least-loaded, affinity) is a feature extension — the `PeerRef` enum is
compose with a `Route { selector, policy }` struct without breaking the designed to compose with a `Route { selector, policy }` struct without
`invoke_peer` signature. Decided during implementation when a fan-out use breaking the `invoke_peer` signature. Adding a routing policy is
case needs it; recorded here, not in a full ADR. non-breaking; it's a feature addition when a fan-out use case needs it,
not an unmade architectural decision.
- **Cross-references**: ADR-029, [client-and-adapters.md](crates/call/client-and-adapters.md) - **Cross-references**: ADR-029, [client-and-adapters.md](crates/call/client-and-adapters.md)
### OQ-31: services/list-peers Re-Export Semantics ### OQ-31: services/list-peers Re-Export Semantics
- **Origin**: [ADR-029](decisions/029-peer-graph-routing-model.md) §6, `docs/research/alknet-call-peer-routing/findings.md` §3.5 - **Origin**: [ADR-029](decisions/029-peer-graph-routing-model.md) §6, `docs/research/alknet-call-peer-routing/findings.md` §3.5
- **Status**: open - **Status**: **resolved** (2026-06-27)
- **Door type**: Two-way - **Door type**: Two-way
- **Priority**: low - **Priority**: low
- **Resolution**: v1 defaults to "own ops only" — `services/list` shows the - **Resolution**: `services/list` defaults to **"own ops only"**it shows
head's own Layer 0 `External` ops, filtered by `AccessControl::check(calling_peer)`, the head's own Layer 0 `External` ops, filtered by
unchanged from today (minus the `remote_safe` filter). A `services/list-peers` `AccessControl::check(calling_peer)`, unchanged from today (minus the
opt-in (new built-in operation) lists the peer overlays with attribution: retired `remote_safe` filter). A `services/list-peers` opt-in (new
each peer's sub-overlay listed as `{ peer: Option<PeerId>, operations: [...] }`, built-in operation) lists the peer overlays with attribution: each
filtered by the calling peer's authorization. Whether re-exported peer ops peer's sub-overlay listed as `{ peer: Option<PeerId>, operations: [...] }`,
are listed by default, opt-in, or per-peer-policy is the two-way-door filtered by the calling peer's authorization. The re-export policy is an
remainder; v1 is opt-in (`services/list-peers`). The re-export policy is an `AccessControl` decision on the listing op. Whether `services/list-peers`
`AccessControl` decision on the listing op. Decided during implementation is built now or as a feature addition is a scheduling question — the
when a consumer needs peer-attributed discovery; recorded here, not in a decision (opt-in, `AccessControl`-filtered) is made.
full ADR.
- **Cross-references**: ADR-029, [client-and-adapters.md](crates/call/client-and-adapters.md) - **Cross-references**: ADR-029, [client-and-adapters.md](crates/call/client-and-adapters.md)
### OQ-32: Multi-Hop Federation ### OQ-32: Multi-Hop Federation
- **Origin**: [ADR-029](decisions/029-peer-graph-routing-model.md) §3.7, `docs/research/alknet-call-peer-routing/findings.md` §3.7 - **Origin**: [ADR-029](decisions/029-peer-graph-routing-model.md) §3.7, `docs/research/alknet-call-peer-routing/findings.md` §3.7
- **Status**: open - **Status**: open (feature extension, not an unmade architecture decision)
- **Door type**: One-way (federation model), two-way (mechanism) - **Door type**: One-way (federation model), two-way (mechanism)
- **Priority**: low - **Priority**: low
- **Resolution**: v1 is one-hop — worker A does not transitively see worker - **Resolution**: The model is **one-hop** — worker A does not transitively
B's ops through the head unless the head explicitly re-exports them. The see worker B's ops through the head unless the head explicitly re-exports
peer-keyed overlay model extends to multi-hop without redesign (a chain of them. The peer-keyed overlay model extends to multi-hop without redesign
`PeerRef::Specific` routing decisions), but path-finding (which peer reaches (a chain of `PeerRef::Specific` routing decisions), but path-finding
which op transitively) is where a graph library (petgraph) would pay off. (which peer reaches which op transitively) is where a graph library
For v1 (one hop, shallow), a nested `HashMap<PeerId, HashMap<String, ...>>` (petgraph) would pay off. For one-hop (shallow), a nested
suffices. Whether multi-hop federation becomes a real use case is a future `HashMap<PeerId, HashMap<String, ...>>` suffices. Multi-hop federation is
decision; the peer-keyed model does not foreclose it. Not designed; tracked a feature extension the one-hop model is the architectural commitment;
here so the v1 model's extendability is recorded. extending to multi-hop doesn't break downstream crates. Whether multi-hop
becomes a real use case is a future decision; the peer-keyed model does
not foreclose it.
- **Cross-references**: ADR-029, [client-and-adapters.md](crates/call/client-and-adapters.md) - **Cross-references**: ADR-029, [client-and-adapters.md](crates/call/client-and-adapters.md)
### OQ-33: PeerId — Cryptographic Identity vs Stable Logical Identifier ### OQ-33: PeerId — Cryptographic Identity vs Stable Logical Identifier
@@ -591,7 +640,7 @@ revisited during implementation without a new ADR.
- **Cross-references**: ADR-030, [auth.md](crates/core/auth.md), - **Cross-references**: ADR-030, [auth.md](crates/core/auth.md),
[config.md](crates/core/config.md) [config.md](crates/core/config.md)
### OQ-36: Concrete Adapter Shapes (Deferred for Exploration) ### OQ-36: Concrete Persistence Adapter Shapes (Deferred for Exploration)
- **Origin**: ADR-033 §"What this does NOT do" (concrete adapter shapes not - **Origin**: ADR-033 §"What this does NOT do" (concrete adapter shapes not
specified), the project's note that the repo pattern is a tool to reach specified), the project's note that the repo pattern is a tool to reach
@@ -599,23 +648,35 @@ revisited during implementation without a new ADR.
- **Status**: open (deferred for exploration) - **Status**: open (deferred for exploration)
- **Door type**: Two-way (adapter shapes are implementation details; - **Door type**: Two-way (adapter shapes are implementation details;
the trait shapes are the one-way doors, already committed by ADR-030/031/033) the trait shapes are the one-way doors, already committed by ADR-030/031/033)
- **Priority**: low (becomes real when a persistence use case forces a - **Priority**: medium (must be addressed before the next round of
concrete adapter build) implementation; not blocking the current OQ-29 decision)
- **Resolution**: The repo/adapter pattern is committed (ADR-033): core - **Resolution**: The repo/adapter pattern is committed (ADR-033): core
defines repo traits + in-memory default adapters; persistence adapters defines repo traits + in-memory default adapters; persistence adapters
are separate crates; the assembly layer wires the adapter. The are separate crates; the assembly layer wires the adapter.
**concrete adapter shapes** — table schemas, backend choice (SQLite +
honker vs. a key-value store vs. a remote service), indexing, caching,
connection management — are deferred for exploration.
The project is iterating on adapter simplification. The trait shapes **What ships with core** (not deferred): the repo traits
(`IdentityProvider`, `CredentialStore`) are the commitment; the adapter (`IdentityProvider`, `CredentialStore`) and their in-memory default
shapes are not. When a concrete use case (peer identity persistence adapters (`ConfigIdentityProvider`, `InMemoryCredentialStore`). These are
across restarts, credential persistence across restarts, ACL delegation the one-way-door commitments — they ship with the core crate, not as
graph) forces a persistence adapter build, the adapter shape gets separate adapters. The in-memory adapters are real implementations, not
reasoned through then, not speculatively now. stubs — a full repo pattern (the same trait surface a persistence
adapter would implement), just backed by config / `HashMap` instead of
a database.
**What's deferred**: the concrete *persistence* adapter shapes — table
schemas, backend choice (SQLite + honker vs. a key-value store vs. a
remote service), indexing, caching, connection management. These are the
separate-crate adapters (e.g., `alknet-peer-store-sqlite`,
`alknet-credential-store-sqlite`) that implement the core traits against
a specific backend. The project is iterating on adapter simplification;
the trait shapes are the commitment, the persistence adapter shapes are
not. When a concrete use case (peer identity persistence across
restarts, credential persistence across restarts, ACL delegation graph)
forces a persistence adapter build, the adapter shape gets reasoned
through then.
This OQ exists so the deferral is deliberate, not accidental — the This OQ exists so the deferral is deliberate, not accidental — the
pattern is committed, the adapters are not, and the gap is tracked. pattern is committed, the in-memory adapters ship with core, and the
persistence adapter shapes are the open exploration.
- **Cross-references**: ADR-030, ADR-031, ADR-033, OQ-34, - **Cross-references**: ADR-030, ADR-031, ADR-033, OQ-34,
[auth.md](crates/core/auth.md), [config.md](crates/core/config.md) [auth.md](crates/core/auth.md), [config.md](crates/core/config.md)