2026-10-04 04:16:15 +00:00
# Box API: Per-Agent Thread Oversight — Contract Draft
2026-10-05 15:58:37 +00:00
> **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.
2026-10-04 04:16:15 +00:00
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` :
``` 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/<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.:
``` 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.