From 4b9f4889e32221a557a9fd3c4011afb883f78076 Mon Sep 17 00:00:00 2001 From: operator-main Date: Sun, 4 Oct 2026 04:16:15 +0000 Subject: [PATCH] =?UTF-8?q?docs/BOX-API-DESIGN-THREADS.md=20=E2=80=94=20pe?= =?UTF-8?q?r-agent=20thread=20oversight=20API=20contract=20(draft)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session: main --- docs/BOX-API-DESIGN-THREADS.md | 176 +++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 docs/BOX-API-DESIGN-THREADS.md diff --git a/docs/BOX-API-DESIGN-THREADS.md b/docs/BOX-API-DESIGN-THREADS.md new file mode 100644 index 0000000..eed267a --- /dev/null +++ b/docs/BOX-API-DESIGN-THREADS.md @@ -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:`); `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.