Skip to main content

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 MiB

The 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 limit arguments 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 optional protocol_info probe. The daemon answers protocol probes before authentication and then continues waiting forauthenticate. After authentication, the resolved identity is bound to the connection.
  • Compatibility. protocol_info is intentionally minimal and treated as stable across protocol versions 1 and 2: the response shape did not change at the v2 bump, only max_supported advanced. 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.