Files
box/docs/BOX-API-DESIGN-THREADS.md

6.8 KiB

Box API: Per-Agent Thread Oversight — Contract Draft

Box is the main surface. All operator work goes through Box (box.muse-dev.online). The web UI, box CLI, and agents share the same API endpoints. No UI-only powers.

Status: draft 2026-10-04 — for the UI/UX agents. Not implemented, not deployed. Companion docs: BOX-API-DESIGN-DMS.md, BOX-UI-DESIGN.md, BOX-API-DESIGN-TIMERS.md.

Goal

A human (and any agent) can oversee every thread on a given agent: main chat, side chats, and DM threads — list them, open one, read messages. Same API serves the UI and agents. No UI-only capabilities: every view below is a thin render over a documented endpoint, and every page shows its curl equivalent.

Non-goals

  • No new write paths. Sending is already covered (POST /api/box/chat/send, POST /api/box/dms designs). This contract is reads only.
  • No cross-agent impersonation: an agent sees only its own threads.

Resource model

agent  = muse | pip | 646 | opm | operator-main        (node == agent == profile)
thread = { id, kind, title, participants[], last_message_at, message_count }
message = { id, from: { role, name }, text, ts }
  • kind: main | sidechat | dm
  • thread.id: the muse.ai thread UUID (chat:thread:<uuid>); main uses the literal id main.
  • title: side chats carry their auto/user title; DM threads are titled dm:<a>:<b>.
  • participants: agent names plus human where a human participates.

Endpoints

Base: https://box.muse-dev.online/api/box

1. GET /agents

List known agents with presence. (May alias existing node inventory; contract is what matters, not the table it reads.)

Response 200:

{
  "agents": [
    {"name": "muse", "status": "active", "threads": 4, "last_active": "2026-10-04T04:02:11Z"},
    {"name": "pip", "status": "active", "threads": 2, "last_active": "2026-10-04T03:58:44Z"}
  ]
}

2. GET /agents/{agent}/threads

Query: kind=main|sidechat|dm|all (default all), limit (default 50, max 200), offset (default 0).

Response 200:

{
  "agent": "pip",
  "threads": [
    {"id": "main", "kind": "main", "title": "Main chat",
     "participants": ["pip", "human"],
     "last_message_at": "2026-10-04T03:58:44Z", "message_count": 231},
    {"id": "5f18476d-8994-49e7-a9e0-4838732363fe", "kind": "sidechat",
     "title": "646 pip cross-operator coordination chat",
     "participants": ["pip", "human"],
     "last_message_at": "2026-10-04T02:11:03Z", "message_count": 17}
  ],
  "fetched_at": "2026-10-04T04:10:02Z"
}

fetched_at is honest staleness: the VM may serve a short-TTL cache (≤15s) so one page view doesn't fan out to every agent browser.

3. GET /agents/{agent}/threads/{thread_id}

Query: limit (default 50, max 200), before (message id cursor for paging back).

Response 200:

{
  "agent": "pip",
  "thread": {"id": "5f18476d-8994-49e7-a9e0-4838732363fe",
             "kind": "sidechat", "title": "646 pip cross-operator coordination chat",
             "participants": ["pip", "human"]},
  "messages": [
    {"id": "m_001", "from": {"role": "human", "name": "operator-main"},
     "text": "status check", "ts": "2026-10-04T02:10:01Z"},
    {"id": "m_002", "from": {"role": "agent", "name": "pip"},
     "text": "all green", "ts": "2026-10-04T02:11:03Z"}
  ],
  "next_before": "m_001",
  "fetched_at": "2026-10-04T04:10:05Z"
}

Messages are chronological ascending; next_before pages further back. null when at the start.

Auth

Same tier system as the rest of Box:

Caller /agents own threads other agent's threads DM threads
operator (PIN/bearer) yes yes yes yes
agent (box-signature) yes yes no (403) only threads they participate in
dev / anonymous no (403) no no no

Box-signatures bind to the full resource path (agents/pip/threads/<uuid>), so a signature for one thread can't be replayed for another. DM threads are additionally restricted: operator tier, or a participating agent.

Validation & errors

  • {agent} and {thread_id}: ^[a-z0-9-]{1,64}$, plus main literal and UUID form accepted for thread ids. Invalid → 400 INVALID_NAME.
  • Unknown agent → 404 AGENT_NOT_FOUND. Unknown thread → 404 THREAD_NOT_FOUND.
  • Bad before cursor → 400 BAD_CURSOR.
  • bl unreachable / helper missing → 502 BL_UNREACHABLE (honest, never silent).
  • Shape follows existing Box errors: {"ok": false, "code": "...", "detail": "..."}.

bl-side helper (read-only)

New allowlisted helper bin/box-chat.py on bl (sibling of box-ctl.py), invoked by the VM over the existing VM→bl SSH path. Read-only by construction: only two actions, no writes, no shell.

box-chat.py thread-list <agent> [--kind main|sidechat|dm|all] [--limit N]
box-chat.py thread-messages <agent> <thread-id> [--limit N] [--before MSGID]
  • <agent> validated against the known node list; anything else → error, no SSH.
  • <thread-id> validated ^[a-zA-Z0-9-]{1,64}$; main maps to the main chat.
  • Reads via the existing muse-chat-api.py in the agent's netns (CDP) — the same path operators already use; no new browser machinery.
  • Every invocation audit-logged on bl (box-chat.jsonl); VM audit-logs the API read to /srv/box/audit.jsonl as with other Box reads.

Rate limits, caching, audit

  • Reads: 60/min/actor → 429 + Retry-After (lighter than the 10/min mutation budget; reading is cheap, hammering browsers is not).
  • VM caches thread lists ≤15s, keyed by (agent, kind); fetched_at always returned so the UI can say "updated 12s ago" instead of faking live.
  • All reads audit-logged (actor, agent, thread id, ts). Message contents are not written to the audit log — ids and counts only.

UI mapping (for the UI agents)

Page Endpoint Notes
/box/agents GET /agents agent cards w/ status + thread counts
/box/agents/{agent} GET /agents/{agent}/threads thread table; filter by kind; shows fetched_at age
/box/agents/{agent}/threads/{id} GET /agents/{agent}/threads/{id} message view; "load older" uses before

Server-rendered pages + unified theme (per BOX-UI-DESIGN.md), ops-gated for humans. Every page embeds its curl equivalent, e.g.:

curl -s -H "Authorization: Bearer $BOX_TOKEN" \
  "https://box.muse-dev.online/api/box/agents/pip/threads?kind=sidechat"

Open questions (for review, not blockers)

  1. Unread counts: computable cheaply? If not, v1 ships without; "last message" ordering is the oversight signal.
  2. Should GET /agents include the human's own operator threads? Currently agent-scoped; operator DMs to agents appear under each agent's dm kind.
  3. Message id stability across reads (needed for before cursors) — confirm against muse-chat-api.py output during implementation.