Local IPC
The daemon's canonical wire protocol. Clients on the same host (the CLI, the TUI, operator UIs, third-party tooling) communicate with the daemon over length-prefixed JSON on a Unix socket at $COVENANT_HOME/sock. The HTTP gateway is a thin adapter over the same surface.
Frame format
Each frame is a 4-byte big-endian unsigned integer length prefix followed by exactly that many bytes of UTF-8 JSON. Frames over 8 MiB are rejected at the read boundary.
+---------+---------+---------+---------+---------- … ----------+
| len[31..24] | len[23..16] | len[15..8] | len[7..0] | JSON payload |
+-------------+-------------+------------+-----------+--------------+
4-byte big-endian length up to 8 MiBThe framing applies in both directions: each request frame is followed by exactly one response frame, and a long-lived connection can carry many request/response pairs in sequence. Connections are not pooled by the daemon; clients may reuse a single connection or open one per request. The IPC v2 streaming opt-in (see Streaming responses (IPC v2) below) replaces the single response frame with a sequence of StreamEnvelope frames; the per-frame framing rules are unchanged.
Request shapes
A request is a JSON object tagged with kind. Core request kinds; the exhaustive enum lives in the covenant-ipc Rust crate.
{ "kind": "ping" }
{ "kind": "protocol_info" }
{ "kind": "authenticate",
"token_b58": "…" }
{ "kind": "submit_intent",
"text": "…",
"prefer_stream": null | true | false }
{ "kind": "recent_memory",
"tier": "working" | "episodic" | "longterm" | null,
"limit": 10,
"prefer_stream": null | true | false }
{ "kind": "search_memory",
"query": "…",
"tier": "working" | "episodic" | "longterm" | null,
"limit": 10,
"min_relevance": null | 0.6 }
{ "kind": "purge_memory",
"tier": "working" | "episodic" | "longterm" | null,
"before_ms": 1714938000000 }
{ "kind": "purge_audit", "before_ms": 1714938000000 }
{ "kind": "purge_capabilities", "before_ms": 1714938000000 }
{ "kind": "purge_peers", "before_ms": 1714938000000 }
{ "kind": "recent_receipts",
"limit": 10,
"since_ms": null | 1714938000000 }
{ "kind": "recent_capabilities",
"limit": 10 }
{ "kind": "grant_capability",
"action": "tool.web_search",
"scope": null | { ... },
"expires_at": null | 1714938000000 }
{ "kind": "revoke_capability",
"signature_b58": "…" }
{ "kind": "verify",
"window": 100 }
{ "kind": "ignore_check",
"text": "…" }
{ "kind": "list_tools" }
{ "kind": "call_tool",
"name": "echo",
"arguments": { ... } }
{ "kind": "recent_audit",
"limit": 20,
"since_ms": null | 1714938000000,
"prefer_stream": null | true | false }
{ "kind": "verify_audit_integrity" }
{ "kind": "send_a2_a_task", "task": { ... } }
{ "kind": "try_recv_a2_a_task" }
{ "kind": "a2_a_queue",
"limit": 20,
"min_lease_age_ms": null | 5000,
"deadline_within_ms": null | 60000,
"state_filter": null | "queued" | "in_flight" }
{ "kind": "post_a2_a_result", "result": { ... } }
{ "kind": "try_recv_a2_a_result" }Response shapes
Responses are also kind-tagged. One canonical response shape per request, plus a generic error for any handler-level failure.
{ "kind": "pong" }
{ "kind": "protocol_info",
"info": {
"protocol": "covenant.ipc",
"version": 1,
"min_supported": 1,
"max_supported": 2
} }
{ "kind": "authenticated",
"display": "user@local" }
{ "kind": "authentication_failed",
"reason": "…" }
{ "kind": "intent_result",
"intent_id": "uuid",
"status": "ok" | "ignored",
"text": "…",
"sources": ["…"],
"settlement": null | { ... } }
{ "kind": "memories", "records": [ ... ] }
{ "kind": "memory_purged", "purged": 42 }
{ "kind": "audit_purged", "purged": 42 }
{ "kind": "capabilities_purged", "purged": 42 }
{ "kind": "peers_purged", "purged": 42 }
{ "kind": "receipts", "receipts": [ ... ] }
{ "kind": "capabilities", "capabilities": [ ... ] }
{ "kind": "capability_granted",
"signature_b58": "…",
"subject_display": "user@local",
"action": "tool.web_search" }
{ "kind": "capability_revoked",
"signature_b58": "…",
"removed": true }
{ "kind": "verify_report",
"window": 100,
"checks": [ { "name": "…", "passed": true, "message": "…" } ],
"drift": [ { "kind": "…", "id": "…", "message": "…", "repair": "…" } ],
"orphans_total": 0 }
{ "kind": "ignore_report",
"ignored": false,
"matched_pattern": null,
"rules_loaded": 3 }
{ "kind": "tool_list", "tools": [ ... ] }
{ "kind": "tool_result", "content": [ ... ], "is_error": false }
{ "kind": "audit_events", "events": [ ... ] }
{ "kind": "audit_integrity", "report": {
"events": 42,
"anchors": 42,
"valid": true,
"root_hash_hex": "…",
"failures": []
} }
{ "kind": "a2_a_task_queued", "task_id": "uuid" }
{ "kind": "a2_a_task_opt", "task": null | { ... } }
{ "kind": "a2_a_result_posted", "task_id": "uuid" }
{ "kind": "a2_a_result_opt", "result": null | { ... } }
{ "kind": "a2_a_queue", "tasks": [ ... ], "results": [ ... ] }
{ "kind": "error", "message": "…" }Streaming responses (IPC v2)
Three request kinds (recent_memory, recent_audit, and submit_intent) accept an optional prefer_stream boolean. When the client sets prefer_stream: true, the daemon replaces the single canonical response frame with a sequence of StreamEnvelope frames terminated by stream_end (or stream_error on a mid-flight failure). Omitted, null, or false keeps the v1 single-Response shape byte-identically; v1 clients are not affected.
Each StreamEnvelope is one length-prefixed JSON frame using the same framing rules as v1 (4-byte big-endian length prefix, 8 MiB cap, UTF-8 JSON payload). The envelope is tagged with a kind discriminator:
{ "kind": "stream_begin",
"stream_id": "uuid",
"response_kind": "memories" | "audit_events" | "intent_result" }
{ "kind": "stream_chunk",
"stream_id": "uuid",
"sequence": 0,
"chunk": { … per-verb chunk payload … } }
{ "kind": "stream_end",
"stream_id": "uuid",
"summary": null | { … per-verb summary … } }
{ "kind": "stream_error",
"stream_id": "uuid",
"message": "…" }stream_begin opens the stream with a fresh stream_id and announces the buffered Response variant the stream materializes via response_kind: memories for recent_memory, audit_events for recent_audit, and intent_result for submit_intent.
stream_chunk frames carry a sequence starting at 0 and incrementing by 1, plus a per-verb chunk: a memory record for recent_memory, an audit event for recent_audit, and an AgentResult-shaped object ({ text, sources, runtime_events }) for submit_intent. An empty result set still emits a stream_begin / stream_end pair so a dead daemon is never confused with an empty stream.
stream_end closes the stream. For submit_intent the summary field carries intent_id, status, and settlement: IntentResult bookkeeping that does not fit in an AgentResult chunk. recent_memory and recent_audit omit summary.
stream_error is reserved for streams that opened successfully and then failed mid-flight. Streaming refused (capability gate failure, ignore-rule match, budget exhaustion) is signaled by the daemon returning the v1 single-Response frame (typically { "kind": "error", "message": "…" }) instead of opening a stream; the consumer disambiguates the two cases by reading the first frame's kind discriminator.
The HTTP gateway exposes this same v2 surface as Server-Sent Events: see Streaming responses on the HTTP API page for the per-frame SSE encoding.
Implementation notes
- Backpressure. The daemon reads one frame at a time per connection; long-running operations hold the connection open until completion, so a slow handler delays the next request on that connection.
- Frame size. The 8 MiB cap applies in both directions. A memory record set exceeding the cap is rare under normal operation, but a verification window over millions of records can reach it. Use the available
limitarguments to bound response sizes. - Timeouts. The daemon does not impose a per-request timeout. Clients are responsible for their own timeouts.
- Authentication. The first frame must be
authenticate, except for an optionalprotocol_infoprobe. The daemon answers protocol probes before authentication and then continues waiting forauthenticate. After authentication, the resolved identity is bound to the connection. - Compatibility.
protocol_infois intentionally minimal and treated as stable across protocol versions 1 and 2: the response shape did not change at the v2 bump, onlymax_supportedadvanced. Clients should ignore unknown fields; adding new required fields implies a protocol version bump.
Reference implementation
The covenant-ipc Rust crate provides read_frame and write_frame helpers alongside the Request and Response enums. Refer to the CLI for an end-to-end example.
Related
- HTTP API: the same surface for clients that prefer JSON over HTTP.
- Security model: what the socket-as-credential design costs you.