Files
wraith/docs/architecture/decisions/024-bidirectional-call-protocol.md
glm-5.1 af7f4d0006 docs: add auth, call protocol architecture specs and ADRs 023-025
Unified authentication (ADR-023): SSH and WebTransport auth share the same
Ed25519 key material. Token auth uses signed timestamps verified against the
same authorized_keys set. IdentityProvider trait decouples core from identity
storage.

Bidirectional call protocol (ADR-024): Generalizes control channel (ADR-018)
to support hub→spoke and spoke→hub calls. Operation paths use /{spoke}/{service}/{op}
format for three-level routing. EventEnvelope wire format, five call events,
PendingRequestMap for correlation.

Handler/spec separation (ADR-025): Downstream consumers register operations
without modifying core. OperationRegistry maps paths to specs + handlers.
Service discovery via /services/list and /services/schema.

Resolves OQ-17 (transport-aware auth), OQ-21 (spoke routing), OQ-CFG-04 and
OQ-CFG-06 (WebTransport auth and transport-aware auth layer). Adds OQ-18
through OQ-22 for remaining open questions.
2026-06-05 08:19:41 +00:00

2.9 KiB

ADR-024: Bidirectional Call Protocol

Status

Accepted

Context

The wraith control channel (ADR-018) routes from client → server's event bus. This is unidirectional: clients can send events to the server, but the server cannot call operations on the client. In the hub/spoke model, spokes (dev env containers) connect to a hub and expose operations (fs, bash, search) that the hub invokes. The hub needs to call spoke operations.

Additionally, the current control channel provides no request/response semantics. Every consumer that needs call/response reinvents the pending-request correlation.

Decision

The call protocol is bidirectional. Both sides can send call.requested and receive call.responded. The protocol uses EventEnvelope wire format (4-byte BE length prefix + JSON) — the same as @alkdev/pubsub.

Five event types: call.requested, call.responded, call.completed, call.aborted, call.error.

A call is a subscribe that resolves after one event. Both use call.requested with correlated requestId. PendingRequestMap in core provides correlation.

Operation names use slash-based paths: /{spoke}/{service}/{op}. The first path segment routes the call to the correct connected node. The hub's registry maps spoke prefixes to connections. This mirrors iroh's ALPN dispatch: the first segment is the routing key, remaining path dispatches within the node.

Core-provided operations use short paths without a spoke prefix (/services/list, /services/schema). Spoke operations are prefixed (/dev1/fs/readFile).

This generalizes ADR-018's control channel: the wraith-* destination becomes a transport for EventEnvelope frames with call protocol semantics, instead of raw pubsub dispatch.

Consequences

  • Positive: Hub can invoke operations on spokes. Dev env containers expose fs, bash, search — the hub calls them as needed.
  • Positive: Browser clients can expose custom UDFs. Any connected participant can both call and serve operations.
  • Positive: Built-in request/response correlation. One PendingRequestMap in core serves all consumers.
  • Positive: Slash-based paths align with URL routing, OpenAPI, MCP, and iroh's ALPN dispatch. First segment = routing key.
  • Positive: Multiple spokes exposing the same service (two dev envs both exposing /fs/*) are naturally differentiated by the spoke prefix.
  • Negative: The PendingRequestMap adds in-memory state. Entries must be cleaned up on timeout or connection close.
  • Negative: The hub must maintain a routing table mapping spoke identities to connections, with registration on connect and cleanup on disconnect.

References