d04cb9c1254e432c6f9f562963dddd333b43bfe9
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| a3825f57cf |
feat(call): from_call adapter — discover + register remote ops (ADR-017 §3)
The #2 gap in alknet-call: discovers the remote peer's External operations via services/list + services/schema and registers them in the connection's Layer 2 overlay as FromCall-provenance leaves with forwarding handlers. The discovery mechanism was already implemented in registry/discovery.rs; from_call is the client-side consumer of that API. src/client/from_call.rs: - from_call(connection, FromCallConfig) -> Result<Vec<HandlerRegistration>, AdapterError>. Calls services/list then services/schema for each op, rebuilds OperationSpec from the schema JSON (parsing op_type, visibility, error_schemas, access_control), constructs a forwarding handler that calls the remote op via CallConnection::call(), and returns FromCall-provenance bundles (composition_authority: None, scoped_env: None, empty capabilities, remote_safe: false per ADR-028 §4). - FromCallConfig { namespace_prefix: Option<String>, operation_filter: Option<HashSet<String>> } with builder methods. - v1 defaults (two-way doors recorded in client-and-adapters.md): - error-on-collision (DC-3/OQ-28): applying the (possibly empty) prefix produces a name already seen -> AdapterError::Conflict, not silent overwrite. - auto-on-reconnect (DC-2/OQ-27): the overlay is per-connection (Layer 2, ADR-024), so re-import on reconnect is naturally scoped; the assembly layer calls from_call immediately after connect(). - Forwarding handler captures an Arc<CallConnection> and, on invocation, calls the remote op and returns its ResponseEnvelope. The parent_request_id participates in the cross-node abort cascade (ADR-016 §6) — if the parent is aborted, the cascade reaches this handler which sends call.aborted to the remote node; cross-node abort is transparent. - Trust is transitive (recorded in spec): a from_call-imported op executes the remote node's code; scoped_env bounds which ops are reachable, not what they do. OperationContext.internal is now pub (was pub(crate)) so downstream consumers (assembly layer, integration tests) can construct contexts for overlay-env dispatch. Tests (207 lib + 2 integration): - Unit: rebuild_spec name/prefix/op_type/visibility/error_schemas/acl; unknown op_type -> SchemaParse; missing op_type -> SchemaParse; FromCallConfig builder; from_call against a mock connection returns DiscoveryFailed (no transport); FromCall provenance + leaf fields + remote_safe false. - Integration (tests/two_node_call.rs): from_call over a real QUIC loopback — CallClient connects, from_call discovers server/echo, registers the bundle in the overlay, and the forwarding handler round-trips an input through the overlay env to the remote op and back. clippy + fmt + test all green. Refs: tasks/call/client/from-call.md Refs: docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md §3, §6 Refs: docs/architecture/crates/call/client-and-adapters.md §from_call |
|||
| 4bf897f5ab |
feat(call): CallClient + shared dispatch loop + peer-scoped default-deny (ADR-017, ADR-028)
The #1 gap in alknet-call: the outbound connection opener. Every downstream consumer (runner, container service, bilateral exchange, NAPI, agent cross-node dispatch) is blocked on it. Shared dispatch loop (ADR-017 §1 — the architectural commitment that keeps CallClient from becoming a parallel protocol implementation): - Extracts the accept-path dispatch (sweeper, accept_bi loop, handle_stream, dispatch_requested, build_root_context, compose_root_env, fail_all on close) out of CallAdapter into a new protocol/dispatch.rs Dispatcher struct. Both CallAdapter::handle and CallClient::connect produce a CallConnection and hand it to Dispatcher::run_loop — the loop is genuinely shared (refactored, not duplicated). - CallAdapter keeps its public API and test-facing wrappers (pub(crate), #[cfg(test)]-gated) that delegate to the Dispatcher. Peer-scoped default-deny (ADR-028 — the one-way-door security dimension): - RemoteFilter { trusted_peer: bool } on the Dispatcher. In default-deny mode (CallClient::new), an incoming call to an op with remote_safe: false returns NOT_FOUND *before* any capability material reaches the handler — a remote peer's call must not populate OperationContext.capabilities from the local registration bundle unless the op is explicitly remote-safe (ADR-028 Context). Trusted-peer mode (CallClient::trusted_peer, explicit opt-in) bypasses the filter. - The accept path (CallAdapter) uses RemoteFilter::trusted() by convention: a direct QUIC client is not a filtered CallClient peer in the ADR-028 sense. - OperationRegistry::list_operations_peer_scoped(trusted_peer) + services_list_handler_peer_scoped for the CallClient's services/list serving path (ADR-028 Assumption 2: a peer should not see ops it cannot call, so discovery and dispatch filters agree). CallClient (src/client/call_client.rs): - CallClient { registry, identity_provider, trusted_peer: bool }. - new() default-deny; trusted_peer() explicit opt-in (ADR-028 §3). - connect(addr, CallCredentials) dials QUIC on ALPN alknet/call (quinn feature), spawns Dispatcher::run_loop, returns a live CallConnection. - spawn_dispatch(connection) shared path for connect + tests. - CallCredentials { tls_identity, auth_token, remote_identity } — all from Capabilities (ADR-014), never env vars (no-env-vars invariant). v1 connects without client-auth TLS identity (server uses AcceptAnyCertVerifier); RawKey client-auth is a two-way-door remainder. - RemoteIdentity { fingerprint } — concrete shape is a two-way door (OQ-25 remainder); the one-way constraint is it comes from Capabilities. - ClientError { Transport, TlsSetup, ConnectionClosed }. - CallConnection is now Clone (shares the inner Arcs) so connect can hand the caller a live clone while the dispatcher task keeps its clone. Tests (199 lib + 1 integration): - Unit: default-deny NOT_FOUND for non-remote-safe; remote_safe dispatches; trusted-peer dispatches all External; default-deny does NOT populate capabilities (the load-bearing security assertion — verified by a handler that inspects context.capabilities and the fact that the handler is never reached for non-remote-safe ops); remote_safe op populates capabilities; services/list peer-scoped hide/trusted variants; CallClient constructors; CallCredentials builder; Send+Sync. - Integration (tests/two_node_call.rs): real QUIC loopback — CallAdapter server (self-signed cert via rcgen) accepts, CallClient connects, client.call() round-trips to server/echo. Proves the connect path + shared dispatch loop work end-to-end. clippy + fmt + test all green. Refs: tasks/call/client/call-client.md Refs: docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md §1, §2, §7 Refs: docs/architecture/decisions/028-callclient-peer-scoped-registry-filtering.md Refs: docs/architecture/crates/call/client-and-adapters.md |
|||
| 1e5f94b06b |
feat(call): OperationAdapter trait + AdapterError + from_jsonschema (ADR-017 §5)
- client module: defines the async OperationAdapter trait (import() -> Result<Vec<HandlerRegistration>, AdapterError>) and the #[non_exhaustive] AdapterError enum (string-message payloads: DiscoveryFailed, SchemaParse, Transport, Unauthorized, Conflict). The trait lives in alknet-call where the types live; implementations live with their transport deps. - from_jsonschema: schema-only registration producing a FromJsonSchema-provenance HandlerRegistration with no real handler (placeholder errors if invoked), None authority/scoped_env, empty capabilities, remote_safe false (ADR-028 §4). Implements OperationAdapter; malformed (non-object) schema returns AdapterError::SchemaParse. No network I/O. - Re-exported from lib.rs. - Tests: trait compiles for Ok and Err adapters; from_jsonschema bundle shape; placeholder handler errors; OperationAdapter import Ok + SchemaParse paths. All 178+N tests pass, clippy + fmt clean. Unblocks alknet-http Phase 1 (from_openapi/from_mcp adapter implementations). Refs: tasks/call/client/operation-adapter-trait.md, tasks/call/client/from-jsonschema.md Refs: docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md §5 Refs: docs/architecture/crates/call/client-and-adapters.md |
|||
| 2649e068e5 |
docs(arch): call-completion — ADR-028 peer-scoped filtering + client-and-adapters spec + tasks
Resolves the four gap-analysis decisions (DC-1..4) blocking the alknet-call client/adapter surface specced in ADR-017: - ADR-028 (new): locks the one-way door for DC-1 — CallClient registry is default-deny (remote_safe: bool on HandlerRegistration, default false across all provenance); share-global is an explicit trusted-peer opt-in; filtering is a dispatch-time read over the single Layer-0 registry, not a copy. - client-and-adapters.md (new spec): operationally fills the gap ADR-017 left to implementation — CallClient, from_call, from_jsonschema, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern. Keeps call-protocol.md and operation-registry.md under the 700-line split threshold. - ADR-017 amended: records DC-2/3/4 v1 defaults (auto-on-reconnect, error-on-collision, Result error type) and points DC-1 at ADR-028. - OQ-25..28 (new): two-way-door remainders (remote_safe shape, AdapterError variants, re-import trigger, namespace collision) with v1 defaults recorded. - Index/cross-ref updates across READMEs and the two existing call specs. Tasks: 6 task files under tasks/call/ decomposing the completion work along the gap-analysis priority order — remote-safe-marking (one-way door, first) → call-client (phase-risk) → from-call → operation-adapter-trait → from-jsonschema (parallel with call-client) → review-completion. Graph validated with taskgraph; parallelism designed in (from-jsonschema runs concurrent with call-client/from-call once the trait lands). |