docs(arch): ADR-029 peer-graph routing model — supersedes ADR-028
ADR-028's remote_safe/trusted_peer was a parallel, weaker authorization system
that duplicated the existing AccessControl/Identity machinery and couldn't
express the head→N-workers pattern (the primary use case). The flat-namespace
single-peer overlay model (one connection layer in CompositeOperationEnv)
structurally breaks the moment a head has two workers both exposing
/container/exec.
ADR-029 replaces it with:
- Peer-keyed overlays: PeerCompositeEnv { connections: HashMap<PeerId, ...> }
replaces CompositeOperationEnv's singular connection layer. A head node
routes invoke_peer() to the right peer via PeerRef::Specific / PeerRef::Any.
- AccessControl-based peer authorization: the existing AccessControl::check
(peer_identity) gates peer calls — the same mechanism that gates every other
call. remote_safe/trusted_peer/RemoteFilter/list_operations_peer_scoped/
services_list_handler_peer_scoped are retired. The op's AccessControl IS the
peer-authorization policy; no parallel system.
- ScopedPeerEnv: peer-qualified reachability (peer-pinned allowlist) replaces
from_call's namespace_prefix as the disambiguation mechanism. Cross-peer
collision dissolves (separate sub-overlays); same-peer collision stays error.
- services/list-peers opt-in for peer-attributed re-export listing.
POC-validated against real types (scratch module written, type-checked,
removed; build clean, 207 tests pass). Petgraph not needed for v1 (one-hop,
shallow); nested HashMap suffices; extends to multi-hop without redesign (OQ-32).
OQ impact: OQ-25 dissolved (no marking); OQ-28 cross-peer dissolved / same-peer
stays; OQ-26/27/29 stay; new OQ-30 (Any routing policy), OQ-31 (list-peers
semantics), OQ-32 (multi-hop federation).
Research: docs/research/alknet-call-peer-routing/findings.md (POC shapes,
prior art — Ray.io actors, Dapr service invocation, full ADR draft).
ADR-028 marked Superseded; ADR-017 DC-1 amendment updated to point at ADR-029.
This commit is contained in:
@@ -38,7 +38,8 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
|
||||
| [022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Registration bundle carries provenance, composition authority, scoped env, capabilities |
|
||||
| [023](../../decisions/023-operation-error-schemas.md) | Operation Error Schemas | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity |
|
||||
| [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 for CallClient Inbound Dispatch | Default-deny peer-scoped registry view; `remote_safe` marking on `HandlerRegistration`; trusted-peer opt-in; locks the ADR-017 §1 security-dimension one-way door |
|
||||
| [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` |
|
||||
|
||||
## Relevant Open Questions
|
||||
|
||||
@@ -49,11 +50,14 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
|
||||
| OQ-14 | Batch operation semantics | resolved | Correlated `call.requested` events is the correct protocol design |
|
||||
| OQ-16 | Safe vault operations for call protocol exposure | resolved (ADR-014) | None exposed for now |
|
||||
| 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 for CallClient peer-scoped filtering | open (two-way) | Existence of default-deny filtering locked by ADR-028; shape (`remote_safe: bool` v1 vs per-peer allowlist) is the two-way-door remainder |
|
||||
| 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) | open (two-way) | `import()` returns `Result<_, AdapterError>`; variants decided in implementation |
|
||||
| OQ-27 | from_call re-import trigger | open (two-way) | v1 default: auto-on-reconnect; explicit `refresh()` is additive |
|
||||
| OQ-28 | from_call namespace collision behavior | open (two-way) | v1 default: error on collision (no prefix by default) |
|
||||
| OQ-29 | CallClient TLS client-auth and remote-identity verification | open (two-way) | v1 connects with `with_no_client_auth()` + `AcceptAnyServerCertVerifier`; wiring RawKey client-auth and a real `ServerCertVerifier` is additive (no-env-vars invariant unaffected — `auth_token` flows via call-protocol payload, not TLS) |
|
||||
| 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) |
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
|
||||
@@ -168,10 +168,13 @@ The dispatch loop is **shared** with `CallClient` (ADR-017 §1): both
|
||||
`CallAdapter::handle` (accept path) and `CallClient::connect` (connect path)
|
||||
construct a `Dispatcher` (`protocol/dispatch.rs`) and call `run_loop` — the
|
||||
dispatch half is one implementation, the connection-establishment half differs
|
||||
(accept vs dial). The `Dispatcher` carries a `RemoteFilter` (ADR-028) that
|
||||
gates dispatch by `remote_safe`; the accept path uses `RemoteFilter::trusted()`
|
||||
by convention. See [client-and-adapters.md](client-and-adapters.md) for the
|
||||
`Dispatcher`/`RemoteFilter` mechanism.
|
||||
(accept vs dial). Peer authorization flows through the existing
|
||||
`AccessControl::check(peer_identity)` — no `RemoteFilter`/`remote_safe` gate
|
||||
(ADR-029 §3). The composition env is peer-keyed (`PeerCompositeEnv`,
|
||||
ADR-029 §1) to handle head→N-workers routing. See
|
||||
[client-and-adapters.md](client-and-adapters.md) for the `Dispatcher` mechanism
|
||||
and [ADR-029](../../decisions/029-peer-graph-routing-model.md) for the
|
||||
peer-graph routing model.
|
||||
|
||||
### Stream Model
|
||||
|
||||
@@ -535,7 +538,7 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is
|
||||
| Abort cascade for nested calls | [ADR-016](../../decisions/016-abort-cascade-for-nested-calls.md) | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in |
|
||||
| Call protocol client and adapter contract | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction. Client/adapter surface specced in [client-and-adapters.md](client-and-adapters.md) |
|
||||
| Handler registration, provenance, and composition authority | [ADR-022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | Registration bundle carries provenance, composition authority, scoped env, capabilities; dispatch path reads from bundle |
|
||||
| Peer-scoped registry filtering for CallClient | [ADR-028](../../decisions/028-callclient-peer-scoped-registry-filtering.md) | Default-deny `CallClient` registry view; `remote_safe` marking; trusted-peer opt-in |
|
||||
| Peer-graph routing model (supersedes ADR-028) | [ADR-029](../../decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization; retires `remote_safe`/`trusted_peer` |
|
||||
| Operation error schemas | [ADR-023](../../decisions/023-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details` |
|
||||
|
||||
## Open Questions
|
||||
@@ -546,8 +549,15 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
- **OQ-14** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive.
|
||||
- **OQ-16** (resolved by ADR-014): No vault operations are exposed over the call protocol for now.
|
||||
- **OQ-19** (resolved): Session-scoped operation registries — agent-written operations overlaid on global registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait.
|
||||
- **OQ-25..28** (open, two-way): Call-completion remainders — `CallClient` remote-safe marking shape, `OperationAdapter` error type, `from_call` re-import trigger, `from_call` namespace collision. The `CallClient`/adapter surface itself is specced in [client-and-adapters.md](client-and-adapters.md); the one-way door among these (existence of default-deny filtering) is resolved by ADR-028.
|
||||
- **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 and a real `ServerCertVerifier` is additive. See [client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-25** (dissolved by ADR-029): `remote_safe` marking shape — moot;
|
||||
`remote_safe`/`trusted_peer` retired; peer authorization is
|
||||
`AccessControl::check(peer_identity)`.
|
||||
- **OQ-26..29** (OQ-26/27/29 open two-way; OQ-28 cross-peer dissolved / same-peer stays):
|
||||
`OperationAdapter` error type, `from_call` re-import trigger, `from_call`
|
||||
namespace collision, `CallClient` TLS client-auth. See
|
||||
[client-and-adapters.md](client-and-adapters.md) and ADR-029.
|
||||
- **OQ-30..32** (open): `PeerRef::Any` routing policy, `services/list-peers`
|
||||
re-export semantics, multi-hop federation. See ADR-029.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-26
|
||||
last_updated: 2026-06-27
|
||||
---
|
||||
|
||||
# alknet-call — Client and Adapters
|
||||
@@ -61,9 +61,16 @@ fills the gap ADR-017 left to implementation: the `CallClient` API, the
|
||||
`from_call`/`from_jsonschema` flows, 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
|
||||
four decisions (DC-1..4) needed before implementation; DC-1 is resolved by
|
||||
ADR-028, and DC-2/3/4 are two-way-door defaults recorded here and tracked as
|
||||
OQs (DC-2→OQ-27, DC-3→OQ-28, DC-4→OQ-26).
|
||||
four decisions (DC-1..4) needed before implementation. DC-1 was initially
|
||||
resolved by ADR-028 (`remote_safe`/`trusted_peer`), but a subsequent research
|
||||
pass (`docs/research/alknet-call-peer-routing/findings.md`) found that
|
||||
ADR-028'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`
|
||||
machinery. **ADR-029 supersedes ADR-028**: peer-keyed overlays + `PeerRef`
|
||||
routing, and peer authorization through the existing `AccessControl::check(peer_identity)`.
|
||||
DC-2/3/4 are two-way-door defaults recorded here (DC-2→OQ-27, DC-3→OQ-28
|
||||
cross-peer dissolved / same-peer stays, DC-4→OQ-26).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -79,31 +86,13 @@ accept path is the producer on the inbound side. Both produce the same
|
||||
|
||||
```rust
|
||||
pub struct CallClient {
|
||||
/// The operation registry. The peer-scoped view is a dispatch-time read
|
||||
/// over this registry, not a copy (ADR-028 §5).
|
||||
registry: Arc<OperationRegistry>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
/// Trusted-peer mode (ADR-028 §3): when true, the dispatch path exposes
|
||||
/// all External ops to the remote peer and `services/list` lists all
|
||||
/// External ops, ignoring the `remote_safe` marking. When false
|
||||
/// (default), only registrations with `remote_safe: true` dispatch, and
|
||||
/// `services/list` hides non-remote-safe ops (ADR-028 Assumption 2).
|
||||
trusted_peer: bool,
|
||||
}
|
||||
|
||||
impl CallClient {
|
||||
/// Default-deny mode: only `remote_safe: true` ops dispatch/list to the
|
||||
/// remote peer (ADR-028).
|
||||
pub fn new(registry: Arc<OperationRegistry>, idp: Arc<dyn IdentityProvider>) -> Self;
|
||||
|
||||
/// Trusted-peer mode: construct a CallClient that exposes all External
|
||||
/// ops from `registry` to the remote peer, ignoring the remote-safe
|
||||
/// marking. Explicit opt-in per ADR-028 §3.
|
||||
pub fn trusted_peer(
|
||||
registry: Arc<OperationRegistry>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
) -> Self;
|
||||
|
||||
/// Open a QUIC connection to `addr` on ALPN `alknet/call`, perform
|
||||
/// credential handshake, and return a CallConnection running the shared
|
||||
/// dispatch loop. Credentials come from capabilities (ADR-014), not env
|
||||
@@ -118,20 +107,25 @@ impl CallClient {
|
||||
}
|
||||
```
|
||||
|
||||
The v1 mechanism is the `trusted_peer: bool` flag plus the `remote_safe: bool`
|
||||
field on each `HandlerRegistration` (default `false` across all provenance,
|
||||
ADR-028 §4). A richer per-peer filtering mechanism (per-peer allowlist,
|
||||
capability-class tag) is the two-way-door remainder tracked as OQ-25; v1's
|
||||
boolean limits exposure control to "remote-safe for any peer" vs "not," which
|
||||
is acceptable for the runner/dispatch pattern (one remote peer per
|
||||
`CallClient`).
|
||||
Peer authorization flows through the existing `AccessControl::check` against
|
||||
the peer's resolved `Identity` (ADR-029 §3) — there is no `trusted_peer` flag
|
||||
and no `remote_safe` marking. When a remote peer calls an op, the dispatch
|
||||
path resolves the peer's `Identity` (from the connection's TLS fingerprint or
|
||||
the `auth_token` payload, via the existing `IdentityProvider`) and runs
|
||||
`AccessControl::check(peer_identity)` against the op's `AccessControl`. If
|
||||
the op's required scopes/resources are satisfied, the call dispatches; if not,
|
||||
`FORBIDDEN` before the handler runs (capabilities never populated — the
|
||||
security property). An op that should never be callable from the wire uses
|
||||
`Visibility::Internal` (existing mechanism, `NOT_FOUND` before ACL). See
|
||||
[ADR-029](../../decisions/029-peer-graph-routing-model.md) §3 for the full
|
||||
mapping of the three `remote_safe` cases to `AccessControl`/`Visibility`.
|
||||
|
||||
The connection is symmetric after establishment (ADR-017 §2): both sides can
|
||||
send and receive `call.requested`. Connection direction (who opened it) is
|
||||
independent of call direction (who calls whom). The `CallClient` is therefore
|
||||
both a caller and a callee — it dispatches incoming calls from the remote
|
||||
peer against its peer-scoped registry view, and it initiates outgoing calls
|
||||
through the `CallConnection::call()` / `subscribe()` / `abort()` API.
|
||||
peer through the same `AccessControl`-gated path, and it initiates outgoing
|
||||
calls through the `CallConnection::call()` / `subscribe()` / `abort()` API.
|
||||
|
||||
#### Shared Dispatcher
|
||||
|
||||
@@ -143,13 +137,6 @@ accept path and `CallClient`'s connect path construct a `Dispatcher` and call
|
||||
connection-establishment half differs (accept vs dial).
|
||||
|
||||
```rust
|
||||
/// Peer-scoped registry filter state (ADR-028). `trusted_peer: false`
|
||||
/// (default-deny for a CallClient) hides ops whose
|
||||
/// `HandlerRegistration.remote_safe` is false from both dispatch and
|
||||
/// `services/list`. `trusted_peer: true` (explicit opt-in, also used by the
|
||||
/// CallAdapter's local accept path) bypasses the filter.
|
||||
pub struct RemoteFilter { pub trusted_peer: bool }
|
||||
|
||||
/// Shared dispatcher for an established CallConnection. Constructed by both
|
||||
/// CallAdapter (accept path) and CallClient (connect path). Holds no
|
||||
/// per-connection state; the CallConnection is passed into run_loop.
|
||||
@@ -158,37 +145,54 @@ pub struct Dispatcher {
|
||||
pub identity_provider: Arc<dyn IdentityProvider>,
|
||||
pub session_source: Option<Arc<dyn SessionOverlaySource + Send + Sync>>,
|
||||
pub default_timeout: Duration,
|
||||
pub remote_filter: RemoteFilter,
|
||||
}
|
||||
```
|
||||
|
||||
The `remote_filter` is the dispatch-time gate that enforces ADR-028's
|
||||
default-deny: `dispatch_requested` checks `remote_filter.allows(registration.remote_safe)`
|
||||
**before** building the context or invoking the handler — a non-remote-safe op
|
||||
returns `NOT_FOUND` before any capability material reaches the handler (the
|
||||
security argument for default-deny, ADR-028 Context). The accept path
|
||||
(`CallAdapter`) uses `RemoteFilter::trusted()` by convention — a direct QUIC
|
||||
client is not a filtered `CallClient` peer in the ADR-028 sense.
|
||||
The dispatch path resolves the peer's `Identity`, runs `AccessControl::check`
|
||||
against the op's `AccessControl`, and dispatches if allowed — the same
|
||||
authorization machinery that gates every other call. No `RemoteFilter`, no
|
||||
`remote_safe` gate (ADR-029 §3 retires these).
|
||||
|
||||
`CallClient::spawn_dispatch(connection)` is the lower-level API that takes a
|
||||
pre-established `Connection`, constructs a `CallConnection`, builds a
|
||||
`Dispatcher` with the appropriate `RemoteFilter`, spawns the dispatch task,
|
||||
and returns the live `CallConnection`. `connect()` uses it after the QUIC dial
|
||||
completes; tests use it to wire mock/loopback connections directly.
|
||||
`Dispatcher`, spawns the dispatch task, and returns the live `CallConnection`.
|
||||
`connect()` uses it after the QUIC dial completes; tests use it to wire
|
||||
mock/loopback connections directly.
|
||||
|
||||
#### services/list peer-scoped serving
|
||||
#### Peer-keyed composition env (ADR-029)
|
||||
|
||||
The `services/list` hide behavior (ADR-028 Assumption 2) is wired via a
|
||||
separate handler factory: `services_list_handler_peer_scoped(registry,
|
||||
trusted_peer)` in `registry/discovery.rs`, backed by
|
||||
`OperationRegistry::list_operations_peer_scoped(trusted_peer)`. The assembly
|
||||
layer constructs the `CallClient`'s registry with this peer-scoped handler
|
||||
(not the plain `services_list_handler` used by the `CallAdapter`'s local
|
||||
accept path) so that when the remote peer calls `services/list` on the
|
||||
`CallClient`, the response hides non-remote-safe ops in default-deny mode.
|
||||
The dispatch-path `RemoteFilter` (above) and the `services/list`-handler
|
||||
filter are the two halves of the same default-deny posture — discovery and
|
||||
dispatch filters agree.
|
||||
The composition env that aggregates multiple connections is **peer-keyed**
|
||||
(ADR-029 §1). `CompositeOperationEnv`'s singular
|
||||
`connection: Option<Arc<dyn OperationEnv>>` is replaced by `PeerCompositeEnv`
|
||||
with peer-keyed connections:
|
||||
|
||||
```rust
|
||||
pub struct PeerCompositeEnv {
|
||||
pub base: Arc<dyn OperationEnv + Send + Sync>, // Layer 0 curated
|
||||
pub session: Option<Arc<dyn OperationEnv + Send + Sync>>, // Layer 1
|
||||
pub connections: HashMap<PeerId, Arc<dyn OperationEnv + Send + Sync>>, // Layer 2, peer-keyed
|
||||
connection_order: Vec<PeerId>, // insertion order for PeerRef::Any first-match
|
||||
}
|
||||
pub type PeerId = String; // = Identity.id
|
||||
```
|
||||
|
||||
`OperationEnv` gains a peer-routing method with a `PeerRef` selector
|
||||
(`Specific(PeerId)` / `Any`), default-impl for back-compat. See
|
||||
[ADR-029](../../decisions/029-peer-graph-routing-model.md) §2 for the full
|
||||
`invoke_peer` signature and `ScopedPeerEnv` peer-qualified reachability. The
|
||||
per-`CallConnection` overlay stays flat (one connection = one peer); the
|
||||
peer-keying is at the aggregation layer (the head node's composition env).
|
||||
|
||||
#### services/list
|
||||
|
||||
`services/list` filters by `AccessControl::check(calling_peer_identity)` —
|
||||
the calling peer sees only ops it is authorized to call. The
|
||||
`services_list_handler` / `services_list_handler_peer_scoped` split collapses
|
||||
to a single `AccessControl`-filtered handler (the `peer_scoped` variant and
|
||||
the `remote_safe` filter are removed). `services/list-peers` is the opt-in for
|
||||
peer-attributed re-export listing (each peer's sub-overlay listed with
|
||||
attribution, filtered by the calling peer's authorization). See
|
||||
[ADR-029](../../decisions/029-peer-graph-routing-model.md) §6.
|
||||
|
||||
### Credential sources for connections
|
||||
|
||||
@@ -287,10 +291,14 @@ a stale overlay dies with the connection; re-import on reconnect is naturally
|
||||
scoped to the new connection. This is the v1 default; explicit re-import via a
|
||||
future `CallConnection::refresh()` is additive.
|
||||
|
||||
**Namespace collision** (DC-3, OQ-28): optional prefix, default no prefix,
|
||||
collision = error. A node importing from two remotes that both expose
|
||||
`/container/exec` without prefixes should fail loudly. The operator adds
|
||||
prefixes when they know they're importing from multiple sources.
|
||||
**Namespace collision** (DC-3, OQ-28): under the peer-graph model (ADR-029),
|
||||
cross-peer collision dissolves — same name on different peers is fine (they
|
||||
live in separate peer sub-overlays, no prefix needed). Same-peer collision
|
||||
stays an error (a peer shouldn't expose two ops with the same name).
|
||||
`FromCallConfig::namespace_prefix` is optional local-naming sugar for when
|
||||
the importing node wants to expose a peer's ops under a different name
|
||||
*locally* — a local-naming concern, not a disambiguation concern. It defaults
|
||||
to `None`.
|
||||
|
||||
**Trust is transitive** (recorded in `operation-registry.md`): a
|
||||
`from_call`-imported operation executes the remote node's code, not yours.
|
||||
@@ -520,10 +528,13 @@ Based on the gap analysis and the downstream unblock chain:
|
||||
4. **`from_jsonschema`** (medium, standalone) — schema-only registration, no
|
||||
handler. Small.
|
||||
|
||||
5. **DC-1 resolution** (peer-scoped registry filtering, ADR-028) — the
|
||||
security dimension of `CallClient`'s registry. Addressed in parallel with
|
||||
#1 — it's a filtering layer on the registry the `CallClient` exposes, not
|
||||
a blocker for the connection-establishment work.
|
||||
5. **DC-1 resolution** (peer-graph routing model, ADR-029) — the
|
||||
peer-keyed overlay + `AccessControl`-based peer authorization model that
|
||||
replaces ADR-028's `remote_safe`/`trusted_peer`. This is a structural
|
||||
change to `CompositeOperationEnv` (→ `PeerCompositeEnv`), the dispatch
|
||||
path (retire `RemoteFilter`), and `OperationEnv` (gain `invoke_peer`).
|
||||
See ADR-029 for the migration; the POC shapes in the research doc are the
|
||||
reference.
|
||||
|
||||
## What This Completion Unblocks
|
||||
|
||||
@@ -547,13 +558,23 @@ Based on the gap analysis and the downstream unblock chain:
|
||||
call protocol's wire format carries no private keys, API keys, or decrypted
|
||||
credentials (ADR-014). The no-env-vars invariant (above) is the dispatch-side
|
||||
corollary.
|
||||
- **Peer-scoped registry is default-deny.** A `CallClient` exposes no
|
||||
operations to the remote peer unless marked remote-safe. Trusted-peer
|
||||
opt-in is explicit (ADR-028).
|
||||
- **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 (ADR-029 §3). 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.
|
||||
- **Composition env is peer-keyed.** A head node with N worker connections
|
||||
holds a `PeerCompositeEnv` with `connections: HashMap<PeerId, Arc<dyn OperationEnv>>`,
|
||||
not a singular connection overlay. `invoke_peer()` routes to the right peer
|
||||
via `PeerRef::Specific` / `PeerRef::Any` (ADR-029 §1-2).
|
||||
- **`from_call` re-import is auto-on-reconnect.** v1 default; the overlay is
|
||||
per-connection so re-import is naturally scoped (DC-2, OQ-27).
|
||||
- **`from_call` namespace collision is an error.** Default no prefix; the
|
||||
operator adds prefixes when importing from multiple sources (DC-3, OQ-28).
|
||||
- **`from_call` namespace collision is same-peer only.** Cross-peer collision
|
||||
dissolves (same name on different peers is fine — separate sub-overlays,
|
||||
ADR-029 §5). Same-peer collision stays an error. `namespace_prefix` is
|
||||
optional local-naming sugar, not the disambiguation mechanism (DC-3, OQ-28).
|
||||
- **`OperationAdapter::import()` returns `Result`.** Failures surface as
|
||||
`AdapterError` (DC-4, OQ-26).
|
||||
- **MCP stdio transport is not built.** Streamable HTTP is the only supported
|
||||
@@ -565,7 +586,8 @@ Based on the gap analysis and the downstream unblock chain:
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| Call protocol client and adapter contract | [ADR-017](../../decisions/017-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 |
|
||||
| Peer-scoped registry filtering (DC-1) | [ADR-028](../../decisions/028-callclient-peer-scoped-registry-filtering.md) | Default-deny; `remote_safe: bool` on `HandlerRegistration`; trusted-peer opt-in; one-way door on the security dimension |
|
||||
| Peer-graph routing model (DC-1, supersedes ADR-028) | [ADR-029](../../decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via existing `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` |
|
||||
| ~~Peer-scoped registry filtering~~ (superseded) | ~~[ADR-028](../../decisions/028-callclient-peer-scoped-registry-filtering.md)~~ | ~~Default-deny; `remote_safe: bool`; trusted-peer opt-in~~ — superseded by ADR-029 (flat-namespace single-peer model couldn't express head→N-workers; parallel auth system duplicated existing `AccessControl`) |
|
||||
| Secret material flow and capability injection | [ADR-014](../../decisions/014-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-022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | The registration bundle adapters produce; `composition_authority: None` for leaves |
|
||||
| Operation registry layering | [ADR-024](../../decisions/024-operation-registry-layering.md) | Layer 2 per-connection overlay where `from_call` imports land |
|
||||
@@ -583,38 +605,50 @@ Based on the gap analysis and the downstream unblock chain:
|
||||
|
||||
See [open-questions.md](../../open-questions.md) for full details.
|
||||
|
||||
- **OQ-25** (open, two-way): Remote-safe marking shape — `remote_safe: bool`
|
||||
v1 vs per-peer allowlist vs capability-class tag. The *existence* of
|
||||
filtering is locked by ADR-028; the shape is the two-way-door remainder.
|
||||
- **OQ-25** (dissolved by ADR-029): `remote_safe` marking shape — moot.
|
||||
`remote_safe`/`trusted_peer` are retired; peer authorization is
|
||||
`AccessControl::check(peer_identity)`. No marking to shape.
|
||||
- **OQ-26** (open, two-way): `AdapterError` enum variants (DC-4). The
|
||||
*presence* of an error type is recorded here; the variants are
|
||||
implementation-detail.
|
||||
implementation-detail. A `SamePeerCollision` variant may replace the flat
|
||||
`Conflict` variant (ADR-029 §5).
|
||||
- **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.
|
||||
- **OQ-28** (open, two-way): `from_call` namespace collision behavior — error
|
||||
on collision (v1 default, recorded here) vs last-wins.
|
||||
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` (does not present a client cert, does not pin
|
||||
the remote's expected identity from `credentials.remote_identity`). Wiring
|
||||
the local node's RawKey/X509 identity as a rustls client-auth cert and
|
||||
plugging `remote_identity` into a real `ServerCertVerifier` is additive.
|
||||
The one-way constraint (credentials from `Capabilities`, ADR-014) is
|
||||
unaffected — `auth_token` flows through the call-protocol payload, not TLS.
|
||||
`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).
|
||||
|
||||
## References
|
||||
|
||||
- ADR-017: Call Protocol Client and Adapter Contract (the spec this document
|
||||
operationally fills)
|
||||
- ADR-028: Peer-Scoped Registry Filtering for CallClient Inbound Dispatch
|
||||
(resolves DC-1)
|
||||
- ADR-029: Peer-Graph Routing Model (supersedes ADR-028; resolves DC-1 with
|
||||
peer-keyed overlays + `AccessControl`-based peer authorization)
|
||||
- ~~ADR-028~~: Peer-Scoped Registry Filtering (superseded by ADR-029)
|
||||
- `call-protocol.md` — `CallAdapter`, `CallConnection`, dispatch loop, stream
|
||||
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-028's structural gap and validated
|
||||
the ADR-029 design via POC
|
||||
- `/workspace/@alkdev/operations/` — TypeScript prior art (`from_openapi.ts`,
|
||||
`from_mcp.ts`, `from_schema.ts`, `scanner.ts`)
|
||||
- `/workspace/@alkdev/dispatch/` — concrete downstream consumer (container
|
||||
|
||||
@@ -232,8 +232,9 @@ pub struct HandlerRegistration {
|
||||
pub composition_authority: Option<CompositionAuthority>, // None for leaves
|
||||
pub scoped_env: Option<ScopedOperationEnv>, // None for leaves
|
||||
pub capabilities: Capabilities,
|
||||
pub remote_safe: bool, // default false; ADR-028 — exposes this op to
|
||||
// CallClient peers (trusted-peer mode bypasses)
|
||||
// NOTE: ADR-028 added `remote_safe: bool` here; ADR-029 supersedes it and
|
||||
// removes the field. Peer authorization is `AccessControl::check(peer_identity)`,
|
||||
// not a per-op boolean. See ADR-029 §3.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -664,7 +665,8 @@ The `Capabilities` type holds non-serializable, zeroized secret material. It doe
|
||||
| Operation registry layering | [ADR-024](../../decisions/024-operation-registry-layering.md) | Curated (static, immutable) + session and connection overlays (dynamic); `OperationEnv` as trait-object integration point; `OperationContext.env` split into `scoped_env` (data) and `env` (dispatch trait) |
|
||||
| Operation error schemas | [ADR-023](../../decisions/023-operation-error-schemas.md) | Operations declare domain errors; `call.error` carries typed `details`; adapter fidelity for `from_openapi`/`to_openapi` |
|
||||
| Call protocol client and adapter contract | [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) | `from_call`/`from_jsonschema`/`OperationAdapter` produce `HandlerRegistration` bundles; adapter-registered ops are `Internal` leaves. Surface specced in [client-and-adapters.md](client-and-adapters.md) |
|
||||
| Peer-scoped registry filtering for CallClient | [ADR-028](../../decisions/028-callclient-peer-scoped-registry-filtering.md) | Default-deny `CallClient` registry view; adds `remote_safe` marking to `HandlerRegistration` (the bundle this doc defines) |
|
||||
| Peer-graph routing model (supersedes ADR-028) | [ADR-029](../../decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays + `PeerRef` routing; peer authorization via `AccessControl::check(peer_identity)`; retires `remote_safe`/`trusted_peer` (the field this doc's `HandlerRegistration` previously gained) |
|
||||
| ~~Peer-scoped registry filtering~~ (superseded) | ~~[ADR-028](../../decisions/028-callclient-peer-scoped-registry-filtering.md)~~ | ~~`remote_safe` marking on `HandlerRegistration`~~ — superseded by ADR-029 |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -674,8 +676,14 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
- **OQ-14** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive.
|
||||
- **OQ-16** (resolved by ADR-014): No vault operations are exposed over the call protocol for now.
|
||||
- **OQ-19** (resolved): Session-scoped operation registries — agent-written operations overlaid on the curated registry via `OperationEnv` trait layering. Protocol doesn't need changes; `OperationEnv` must remain a trait. Session ops are `Session` provenance (ADR-022) — always `Internal`, compose under restricted authority scoped down at sandbox creation. Generalized by ADR-024 to cover connection-scoped overlays as well.
|
||||
- **OQ-25** (open, two-way): Remote-safe marking shape — existence of default-deny `CallClient` filtering locked by ADR-028; the shape (the `remote_safe: bool` field this doc's `HandlerRegistration` gains vs a richer per-peer mechanism) is the two-way-door remainder. See [client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-26..28** (open, two-way): `OperationAdapter` error type, `from_call` re-import trigger, `from_call` namespace collision. v1 defaults recorded in [client-and-adapters.md](client-and-adapters.md).
|
||||
- **OQ-25** (dissolved by ADR-029): `remote_safe` marking shape — moot.
|
||||
`remote_safe`/`trusted_peer` are retired; peer authorization is
|
||||
`AccessControl::check(peer_identity)`, the existing mechanism. See
|
||||
[client-and-adapters.md](client-and-adapters.md) and ADR-029 §3.
|
||||
- **OQ-26..28** (OQ-26/27 stay two-way; OQ-28 cross-peer dissolved by ADR-029 /
|
||||
same-peer stays): `OperationAdapter` error type, `from_call` re-import
|
||||
trigger, `from_call` namespace collision. v1 defaults recorded in
|
||||
[client-and-adapters.md](client-and-adapters.md).
|
||||
|
||||
## References
|
||||
|
||||
|
||||
Reference in New Issue
Block a user