Session: main
6.6 KiB
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/dmsdesigns). 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|dmthread.id: the muse.ai thread UUID (chat:thread:<uuid>);mainuses the literal idmain.title: side chats carry their auto/user title; DM threads are titleddm:<a>:<b>.participants: agent names plushumanwhere 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}$, plusmainliteral and UUID form accepted for thread ids. Invalid →400 INVALID_NAME.- Unknown agent →
404 AGENT_NOT_FOUND. Unknown thread →404 THREAD_NOT_FOUND. - Bad
beforecursor →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}$;mainmaps to the main chat.- Reads via the existing
muse-chat-api.pyin 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.jsonlas 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_atalways 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)
- Unread counts: computable cheaply? If not, v1 ships without; "last message" ordering is the oversight signal.
- Should
GET /agentsinclude the human's own operator threads? Currently agent-scoped; operator DMs to agents appear under each agent'sdmkind. - Message
idstability across reads (needed forbeforecursors) — confirm againstmuse-chat-api.pyoutput during implementation.