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