# Box API: Per-Agent Thread Oversight — Contract Draft 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:`); `main` uses the literal id `main`. - `title`: side chats carry their auto/user title; DM threads are titled `dm::`. - `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`: ```json { "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`: ```json { "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`: ```json { "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/`), 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 [--kind main|sidechat|dm|all] [--limit N] box-chat.py thread-messages [--limit N] [--before MSGID] ``` - `` validated against the known node list; anything else → error, no SSH. - `` 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.: ```bash 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.