Skip to main content

Module server

Module server 

Source
Expand description

§2 module 31 server (COMPOSABLE-HARNESS-DESIGN.md, D7 “full programmatic RPC/HTTP server”, D8 “remote attach”, D10 “daemon”; §1.9 Obligation 9’s out-of-process half — the in-process SDK already meets the core commitment via crate::EventSink).

The embedding ladder this module builds:

  1. --output-format stream-json (the CLI’s existing rung, UX-23) — already ships a JSONL crate::AgentEvent stream over stdout. This unit completes it: crate::AgentEvent::to_json is now the single canonical projection both that sink AND this module’s notifications share, and it covers the FULL event set (previously crate::AgentEvent::BackgroundOutput fell into a generic “unknown” catch-all).
  2. JSONL-RPC over stdio (run_stdio) — the SDK-out-of-process surface: a parent process drives this agent’s loop over stdin/stdout with {"id","method","params"} request lines, getting back {"id","result"|"error"} responses interleaved with {"event":...} notifications. Parent-process-trusted (same trust model as crate::mcp::serve_stdio) — no auth token.
  3. The same RPC surface over HTTP (run_http, D8 “remote attach”) — POST /rpc for request/response, GET /events for the event stream (SSE-shaped: data: <json>\n\n per line). Unlike stdio, a network client is UNTRUSTED by default, so every request must carry the bearer token (check_auth).

Security posture (this is a listener — the highest-risk module class):

  • [capabilities.server] is project-forbidden (D-10) — see crates/cli/src/userconfig.rs’s PROJECT_FORBIDDEN_CAPABILITY_TABLES and this crate’s configfile::PROJECT_FORBIDDEN_CAPABILITY_TABLES (both already listed "server" before this unit landed; this module is what makes the listener the strip was already guarding against real).
  • Default-off: nothing in this module is ever reached unless a caller explicitly invokes run_stdio/run_http AND the CLI’s own gate (capabilities.server.enabled == Some(true), checked before either is called) passed.
  • Loopback-only HTTP bind by default — enforced by the CALLER (the CLI’s serve command defaults bind to 127.0.0.1:0 and only binds elsewhere on an explicit bind/--bind override, with a printed exposure warning); run_http itself binds whatever address it’s given, since the loopback POLICY decision belongs to the config/CLI layer, not the transport.
  • No permission/sandbox bypass. RpcEngine::new takes an already fully-constructed crate::Agent — the SAME Agent a local run/chat session would build (same Config, same permission rules, same sandbox). This module installs NO approval handler of its own and provides no channel for a remote/RPC caller to answer an approval prompt; combined with Agent’s existing fail-closed rule (“absent handler denies” — crates/harness/src/agent.rs’s prepare_tool_call), any tool call that would need interactive approval is DENIED, never silently approved, when driven through this module. See crates/harness/tests/server_engine.rs for a fail-on-revert proof.
  • Bounded buffering throughout (SERVER_MAX_LINE_BYTES, SERVER_EVENT_CHANNEL_CAPACITY) — same 16MiB-class discipline P5-2 established for crate::mcp’s SSE reader, reused here rather than re-derived.
  • Graceful shutdown: the shutdown RPC method stops the stdio loop and the HTTP accept loop alike (both select on the same RpcEngine::wait_for_shutdown) — no orphaned listener/accept task survives a shutdown call, mirroring P5-3/P5-6’s drop-abort discipline for background work.

Structs§

FrontendRequestBridge
Pre-runtime bridge for MCP clients that must receive their elicitation handler before they are consumed into agent tool registration.
FrontendWebSocketServer
Lifetime handle for an authenticated frontend.v2 WebSocket listener. Dropping the handle detaches the listener without closing its SDK runtime.
RpcEngine
The out-of-process RPC driver: wraps one already-constructed crate::Agent with the submit/interrupt/status/shutdown method set (§ module doc). Shared by both transports (run_stdio, run_http) so the method semantics — including the fail-closed permission behavior — can never drift between them.
RpcRequest
One JSONL-RPC request line a client sends: {"id", "method", "params"}. params defaults to null when omitted (a method that takes no arguments, e.g. status/shutdown, never requires callers to spell out "params": null} explicitly).
RuntimeHttpCredential
One bearer credential and its exact SDK authorization grant.
RuntimeStatus
Protocol-neutral snapshot of one SDK-owned agent runtime.

Enums§

RuntimeSubmitError
Typed turn failure shared by local, HTTP, ACP, CLI, and language adapters.

Constants§

SERVER_EVENT_CHANNEL_CAPACITY
Bounded broadcast capacity for the event-notification channel — mirrors crate::mcp::MCP_SSE_CHANNEL_CAPACITY’s bounded-buffering discipline (P5-2): a slow/absent subscriber can never make the sender block or grow memory unboundedly; a lagging receiver just misses old events (broadcast::error::RecvError::Lagged) rather than stalling the agent loop or accumulating unbounded backlog.
SERVER_MAX_LINE_BYTES
Maximum accepted line/body length (bytes) for both the stdio JSONL-RPC reader and the HTTP transport’s request line/headers/body — the same 16MiB-class cap P5-2 established for crate::mcp’s SSE frame reader (MCP_MAX_SSE_FRAME_BYTES), reused here so an adversarial or simply broken client can never make either transport buffer an unbounded amount of data in memory.

Functions§

generate_token
Mint a random per-session bearer token (32 bytes, hex-encoded) for the HTTP transport, when the operator hasn’t configured a fixed capabilities.server.token. Uses getrandom (already resolved transitively via reqwest’s rustls/ring stack; promoted to a direct dependency here so this crate can call it directly, rather than rolling a hand-written PRNG for a value that must actually be unguessable).
run_frontend_websocket
Publish the language-neutral facade over authenticated WebSocket RPC. The endpoint accepts only /frontend/v2, reuses the SDK coordinator, and emits canonical events as frontend.v2.event notifications.
run_http
Bind bind (host:port; :0 for an OS-assigned ephemeral port) and serve the HTTP transport (D8 “remote attach”) in a background task until engine signals shutdown. Returns the actually-bound address (so a caller that asked for port 0 can learn the real port). Every connection is authenticated per-request via token — see check_auth. The LOOPBACK-BY-DEFAULT policy decision is the caller’s (see the module doc) — this fn binds whatever address it’s given.
run_http_authorized
Bind an SDK HTTP runtime with multiple independently scoped bearer credentials. The token bytes remain server-private; each successful authentication produces the exact authorization grant projected by the shared runtime coordinator.
run_http_authorized_with_lease_ttl
Test/embedder variant of run_http_authorized with an explicit controller lease duration.
run_stdio
Drive the JSONL-RPC protocol over reader/writer (the stdio rung — parent-process-trusted, no auth token; see the module doc). Each request line is dispatched on its OWN spawned task so a submit in-flight never blocks the reader from picking up a subsequent interrupt/status line — every outgoing line (a response OR an event notification) is funneled through one mpsc channel into a single writer task, so two concurrent handlers can never interleave a line’s bytes. Returns once reader hits EOF or a shutdown request lands.