7 Commits

Author SHA1 Message Date
9c81129f24 feat(call): introduce StreamingHandler, HandlerKind, ResponseStream + INVALID_OPERATION_TYPE (ADR-049)
Add the foundational types for ADR-049 streaming handlers:
- StreamingHandler, ResponseStream type aliases and HandlerKind enum
  (Once | Stream) in registration.rs, with make_streaming_handler() helper
- CallError::invalid_operation_type() in wire.rs (sixth protocol code,
  retryable: false)
- HandlerRegistration.handler flipped from Handler to HandlerKind;
  HandlerRegistration::new() now takes HandlerKind
- OperationRegistryBuilder absorbs wrapping: with_local/with_leaf/
  with_leaf_provenance wrap raw Handler in HandlerKind::Once for
  Query/Mutation; new with_local_streaming/with_leaf_streaming take a
  StreamingHandler and wrap in HandlerKind::Stream for Subscription.
  Builder validates kind matches spec.op_type (mismatch = startup error)
- OperationRegistry::register() returns Result<(), String> with a clear
  mismatch message; all call sites updated to handle the Result
- invoke() matches on HandlerKind: Once -> existing path; Stream ->
  INVALID_OPERATION_TYPE error envelope (guards against silent
  truncation; invoke_streaming() added in a downstream task)
- OverlayOperationEnv::invoke_with_policy matches on HandlerKind:
  Once -> dispatch; Stream -> INVALID_OPERATION_TYPE (composition is
  request/response-only)
- Migrated every HandlerRegistration::new() construction site (~95)
  to wrap raw Handler in HandlerKind::Once(handler); the builder sites
  are handled by the builder-absorbs-wrapping change
- Updated two websocket subscription tests that relied on Subscription
  ops dispatching via invoke() to expect INVALID_OPERATION_TYPE
- Added unit tests for invoke/register validation and
  make_streaming_handler
2026-07-02 09:28:05 +00:00
bfd1621b9b feat(call): add ScopedPeerEnv peer-pinned reachability (ADR-029 §4, call/scoped-peer-env) 2026-06-30 11:07:41 +00:00
c106f4a37b feat(call): wire CallClient TLS client-auth and server cert verifier selection (call/call-client-verifier-selection)
Replace AcceptAnyServerCertVerifier (a security hole for X.509) with
verifier selection by PeerEntry presence (ADR-034 §3, OQ-29):

- build_client_auth presents the Ed25519 key as an RFC 7250 raw public
  key client cert (replaces with_no_client_auth), activating the
  PeerEntry fingerprint -> peer_id resolution path on quinn.
- select_server_verifier: Some(fingerprint) -> FingerprintPinVerifier
  (fingerprint match for known peers); None -> WebPkiServerVerifier
  (CA verification for public X.509 endpoints). None + Ed25519 raw key
  fails closed at handshake (no CA to fall back to).
- FingerprintPinVerifier matches ed25519:<hex> (raw key extraction) and
  SHA256:<hex> (DER hash); verifies handshake signatures via
  verify_tls13_signature_with_raw_key / verify_tls12/13_signature.
- Extract shared fingerprint logic into alknet_core::fingerprint (pub
  module) reused by endpoint (server-side) and call_client (client-side).
- remote_identity: None is load-bearing (not defaulted to placeholder).
- Integration tests updated to pin the self-signed server cert
  fingerprint (the known-peer path).
2026-06-28 22:24:09 +00:00
5d6a943ad4 feat(call): add forwarded_for field to OperationContext (call/operation-context-forwarded-for) 2026-06-28 22:08:35 +00:00
4490bc251f feat(call): retire remote_safe/trusted_peer/RemoteFilter (call/retire-remote-safe) 2026-06-28 21:52:57 +00:00
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
2026-06-26 13:25:13 +00:00
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
2026-06-26 13:19:15 +00:00