| title | Coven local API contract (coven.daemon.v1) | ||
|---|---|---|---|
| summary | The versioned coven.daemon.v1 contract under /api/v1: health negotiation, capability discovery, error envelopes, and additive compatibility rules. | ||
| read_when |
|
||
| description | The versioned coven.daemon.v1 contract under /api/v1: health negotiation, capability discovery, error envelopes, and additive compatibility rules. |
The Coven daemon socket API is a public compatibility boundary for comux and external clients such as @opencoven/coven.
GET /api/v1/healthexposesapiVersion: "coven.daemon.v1",covenVersion, and a machine-readablecapabilitiesobject.- Clients should read
/api/v1/healthbefore assuming any response shape from other endpoints. - Legacy unversioned routes such as
GET /healthremain early-MVP aliases; new clients should use/api/v1. - Control-plane clients should discover capabilities before sending action ids.
- All API failures are returned as structured
{ "error": { "code", "message", "details" } }envelopes. - Events include a monotonic
seqcursor for incremental reads. - Event payloads are redacted by default before API display.
GET /api/v1/health returns daemon reachability, the named contract version, coven version, and machine-readable capabilities:
{
"ok": true,
"apiVersion": "coven.daemon.v1",
"covenVersion": "0.0.0",
"capabilities": {
"sessions": true,
"events": true,
"eventCursor": "sequence",
"structuredErrors": true
},
"daemon": {
"pid": 12345,
"startedAt": "2026-05-09T06:43:00Z",
"socket": "/Users/alice/.coven/coven.sock"
}
}If the daemon metadata is unavailable, daemon may be null.
| Field | Type | Description |
|---|---|---|
sessions |
boolean | Sessions API (/sessions, /sessions/:id) is available. |
events |
boolean | Events API (/events) is available. |
eventCursor |
string | Cursor type supported; "sequence" means afterSeq is stable. |
structuredErrors |
boolean | All errors use the { error: { code, message, details } } shape. |
flowchart TD
Req[Incoming request] --> Parse{Parse + version check}
Parse -- bad shape --> ErrInvalid["400 invalid_request"]
Parse -- unknown version --> ErrInvalid
Parse -- ok --> Route{Route exists?}
Route -- no --> ErrNotFound["404 not_found"]
Route -- yes --> Validate{Field validation}
Validate -- cwd outside root --> ErrInvalid
Validate -- unknown harness/action --> ErrInvalid
Validate -- ok --> Action{Resource lookup}
Action -- session missing --> ErrSession["404 session_not_found"]
Action -- session not live --> ErrLive["409 session_not_live"]
Action -- launch (PTY/pipe spawn, init write, harness startup) fails --> ErrLaunch["500 launch_failed"]
Action -- send_input fails --> ErrSend["500 send_input_failed"]
Action -- kill_session fails --> ErrKill["500 kill_failed"]
Action -- runtime down --> ErrRuntime["503 runtime_unavailable"]
Action -- internal panic --> ErrInternal["500 internal_error"]
Action -- ok --> Success[Documented success shape]
ErrInvalid & ErrNotFound & ErrSession & ErrLive & ErrLaunch & ErrSend & ErrKill & ErrRuntime & ErrInternal -->|"{ error: { code, message, details } }"| Client[Client branches on code]
All API errors use the following stable envelope. Clients must branch on error.code, not error.message:
{
"error": {
"code": "session_not_found",
"message": "Session was not found.",
"details": {
"sessionId": "abc-123"
}
}
}details is optional and included when extra context is useful.
| Code | HTTP status | Description |
|---|---|---|
not_found |
404 | Generic route not found. |
invalid_request |
400 or 404 | Malformed request, unknown harness id, missing required field, or unsupported API version. |
session_not_found |
404 | Session id does not exist. |
session_not_live |
409 | Session exists but is not running. |
project_root_violation |
400 | Reserved. Cwd-outside-root currently emits invalid_request with the violation message in the body; promoting to its own code would let clients branch without parsing prose. |
pty_spawn_failed |
500 | Reserved. PTY spawn failures currently emit launch_failed; promoting to its own code would let clients distinguish "the PTY couldn't open" (likely a host issue) from "the harness CLI errored at startup" (likely an auth/config issue). |
launch_failed |
500 | Daemon accepted the launch payload but the runtime (PTY/pipe spawn, initial-message write, harness CLI startup) failed. details.sessionId is the row that was inserted and marked failed. |
send_input_failed |
500 | Daemon accepted the input payload but the runtime write failed (closed pipe, killed process, IO error). details.sessionId is the affected session. |
kill_failed |
500 | Daemon accepted the kill request but the runtime signal/kill call failed (permission, missing process, IO error). details.sessionId is the affected session. |
runtime_unavailable |
503 | The session runtime is unavailable. |
internal_error |
500 | Unexpected internal error. |
raw_artifacts_disabled |
403 | Raw artifact retrieval was requested without explicit raw artifact persistence enabled. |
raw_artifact_requires_raw_flag |
400 | Raw artifact retrieval omitted the required raw=1 query flag. |
artifact_not_found |
404 | Sensitive artifact id does not exist for the session. |
GET /api/v1/capabilities returns the daemon/control-plane capability catalog. This is the intended OpenMeow handshake for deciding which actions to show or route through Coven.
{
"capabilities": [
{
"id": "coven.control.actions",
"label": "Coven control-plane action router",
"adapter": "coven-daemon",
"status": "available",
"policy": "allow",
"actions": ["coven.capabilities.refresh"]
},
{
"id": "desktop.automation",
"label": "Desktop automation adapters",
"adapter": "desktop-use",
"status": "planned",
"policy": "requiresApproval",
"actions": []
}
]
}Known enum values in v1:
status:available,plannedpolicy:allow,requiresApproval
Clients should ignore unknown future capability ids and action ids unless they explicitly support them.
POST /api/v1/actions accepts a policy-shaped action envelope. The daemon validates the action id before any adapter work is allowed.
{
"action": "coven.capabilities.refresh",
"origin": "open-meow",
"intentId": "intent-1",
"args": {}
}Immediately completed safe actions return 200:
{
"ok": true,
"accepted": true,
"action": "coven.capabilities.refresh",
"status": "completed",
"event": {
"kind": "capabilities.refreshed",
"action": "coven.capabilities.refresh",
"origin": "open-meow",
"intentId": "intent-1",
"payload": { "capabilities": 3 }
}
}Unknown action ids return 400 and fail closed:
{
"ok": false,
"accepted": false,
"action": "desktop.deleteEverything",
"status": "rejected",
"reason": "unknown action `desktop.deleteEverything`"
}In v1, session responses stay as raw JSON objects using the Rust daemon's snake_case field names.
Endpoints that return this shape:
GET /api/v1/sessions→SessionRecord[]POST /api/v1/sessions→SessionRecordGET /api/v1/sessions/:id→SessionRecord
{
"id": "session-1",
"project_root": "/repo",
"harness": "codex",
"title": "Fix the tests",
"status": "running",
"exit_code": null,
"archived_at": null,
"created_at": "2026-05-09T06:43:00Z",
"updated_at": "2026-05-09T06:43:05Z"
}GET /api/v1/events returns a paginated envelope with monotonic seq cursors. GET /api/v1/sessions/:id/events is the session-scoped alias with the same response shape and cursor query parameters except that sessionId comes from the path.
| Parameter | Required | Description |
|---|---|---|
sessionId |
Yes | Session to fetch events for. |
afterSeq |
No | Return only events with seq > afterSeq (preferred). |
afterEventId |
No | Compatibility cursor — resolves to a sequence position. |
limit |
No | Maximum number of events to return (daemon-enforced, max 1000). |
{
"events": [
{
"seq": 42,
"id": "event-uuid",
"session_id": "session-uuid",
"kind": "output",
"payload_json": "{\"data\":\"hello\"}",
"created_at": "2026-05-09T06:43:10Z"
}
],
"nextCursor": {
"afterSeq": 42
},
"hasMore": false
}nextCursor is null when there are no events. hasMore is true when a limit was applied and more events may exist.
payload_json is the redacted preview payload used by clients. Raw sensitive artifacts are never included in this envelope.
GET /api/v1/sessions/:id/log currently returns the full redacted log preview for the session as an unbounded array:
[
{
"ts": "2026-05-09T06:43:10Z",
"level": "info",
"message": "> hello"
}
]GET /api/v1/sessions/:id/artifacts/:artifactId?raw=1 is intentionally narrow. It is unavailable unless raw artifact persistence is explicitly enabled in local privacy settings. Disabled installs return:
{
"error": {
"code": "raw_artifacts_disabled",
"message": "Raw artifact persistence is not enabled.",
"details": {
"sessionId": "session-1",
"artifactId": "event-1"
}
}
}- Poll
GET /events?sessionId=<id>to get all events (with optionallimit). - Use
nextCursor.afterSeqin subsequent requests:GET /events?sessionId=<id>&afterSeq=<seq>. - Repeat until
hasMoreisfalse.
This gives clients stable incremental reads. Exactly-once delivery also requires client-side checkpointing and idempotency.
sequenceDiagram
participant Client
participant Daemon as /api/v1/events
Client->>Daemon: GET ?sessionId=S1
Daemon-->>Client: { events: [seq 1..50], nextCursor: { afterSeq: 50 }, hasMore: true }
Client->>Client: persist last seq = 50
Client->>Daemon: GET ?sessionId=S1&afterSeq=50
Daemon-->>Client: { events: [seq 51..78], nextCursor: { afterSeq: 78 }, hasMore: false }
Client->>Client: persist last seq = 78
note over Client,Daemon: Client crash + restart
Client->>Daemon: GET ?sessionId=S1&afterSeq=78
Daemon-->>Client: { events: [seq 79..82], nextCursor: { afterSeq: 82 }, hasMore: false }
Persisting afterSeq survives daemon restarts: events are append-only and seq numbers are monotonic, so a resumed poll always picks up where it stopped.
Both live-control endpoints return the same accepted response shape on success:
POST /api/v1/sessions/:id/inputPOST /api/v1/sessions/:id/kill
{
"ok": true,
"accepted": true
}Shared non-success responses use the structured error envelope:
404when the session does not exist:
{
"error": {
"code": "session_not_found",
"message": "Session was not found.",
"details": { "sessionId": "session-1" }
}
}409when the session exists but is not live:
{
"error": {
"code": "session_not_live",
"message": "Session is not live.",
"details": { "sessionId": "session-1" }
}
}- comux reads the
capabilitiesobject from/healthto decide which features to use. - The
@opencoven/covenOpenClaw bridge (packages/openclaw-coven) is updated in this repo alongside the daemon and usesapiVersion === "coven.daemon.v1"as its contract guard. - Client updates to use
afterSeqcursors and paginated event envelopes may happen independently of the daemon update; the daemon-enforced shape is the source of truth. - The
supportedApiVersionsfield has been removed from the health response incoven.daemon.v1; clients should checkapiVersiondirectly.
coven.daemon.v1clients may rely on the documented field names and top-level response shapes above.- Additive fields are backward compatible. Clients should ignore unknown fields when safe.
- Any incompatible change must ship under a new
apiVersionvalue exposed byGET /api/v1/healthor its successor route. - Before a client switches to a new major contract, the Coven repo should publish updated contract docs and a migration note that maps the old shape to the new one.
- Call
GET /api/v1/health. - Verify
apiVersion === "coven.daemon.v1"andcapabilities.structuredErrors === true. - Check
capabilities.eventCursor === "sequence"before usingafterSeqpagination. - Only then depend on the documented
v1sessions/events shapes.
The coven.daemon.v1 contract covers daemon health, capability discovery, action routing, sessions, events, live input, and live kill. Do not treat future orchestration, handoff, or task-routing route names as reserved API until they are implemented and documented in this file.