docs/BOX-API-DESIGN-THREADS.md — per-agent thread oversight API contract (draft)
Session: main
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# 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:<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.
|
||||
Reference in New Issue
Block a user