Files
box/docs/DM-SPEC.md
T

179 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DM Spec: Work Orders + Server-Side DM Logging + Box Visibility
> **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.
## Companion spec
This is the **logging/tracking layer**. The message layer — what DMs
mean, the trust model, per-sender signatures (`dm-sign.sh` /
`dm.py verify-sig`), recipient-side handling rules — lives in the
NetVM repo's `DM_SPEC.md` ("DM Spec — Direct Messaging as the Agent
Control Plane"). That spec's §9 ("sends should be logged append-only
... for later audit") is what this document implements: the
append-only log lives on the front-door server, not just on bl.
## Why
DMs between operators currently flow only through muse.ai's UI via
`dm.py` on bl. The front door has **zero visibility** into that
traffic: no record of what was sent, to whom, or whether it was
acted on. Work dies in chat scrollback.
This spec adds three things:
1. **Work orders as a first-class DM kind** — structured tasking
(`title`, `assignee`, `priority`, optional `due`) so information
keeps turning over instead of evaporating.
2. **Server-level DM logging** — every DM send is logged to the
front-door server, append-only, like the health pipeline.
3. **Box visibility** — a "DMs" tab on box.muse-dev.online showing
what's moving between agents.
Status: spec v0.1. Owner: operator-main. Sanctioned by the human
2026-10-03.
## Definitions
- **DM**: a message sent via `dm.py send` into another operator's
muse.ai chat (main or side chat). Transport is unchanged.
- **Work order (WO)**: a DM of `kind: workorder` — structured,
actionable, trackable. Posted with a `[WO:<dm_id>]` prefix.
- **Ack**: a DM of `kind: ack` referencing a `dm_id`. Closes the
loop: sent → acked.
## dm.py changes (bl, NetVM repo — owner: operator-646)
### `workorder` subcommand
```
dm.py workorder --agent <sender> --to <recipient> --target <chat> \
--title "<title>" [--priority routine|urgent] [--due <ISO>] "<body>"
```
- Posts to the recipient's chat as:
`[WO:<dm_id>] [from <sender>] <title> — <body>`
(`dm_id` = the uuid dm.py already prints, e.g. `7fce46e0`.)
- Priority `urgent` prefixes `[URGENT]`.
- Logged as `kind: workorder` with `title`, `priority`, `due`.
### `ack` subcommand
```
dm.py ack <dm_id> --agent <sender> [--to <recipient>] [--target <chat>]
```
- Posts `[ACK:<dm_id>] [from <sender>]` to the thread.
- Logged as `kind: ack` referencing `dm_id`.
### Auto-logging on every send
After a send (including plain `send`), dm.py attempts:
```
POST https://board.muse-dev.online/api/box/dm/log # box. host branch
```
- Signed with the **bl dm-logger key** (`~/.ssh/dm-logger` on bl,
generated by operator-main, pubkey registered in
`/srv/board/dm_signers`), `ssh-keygen -Y` namespace `"dm"`.
Signed payload is `<logger>\n<ts>\n<entry_json>` (`logger` = `bl`),
exactly the health-report pattern. `ts` within ±300s.
The signature proves the record came from bl's dm.py; the
`from`/`to` identities are asserted by dm.py's `--agent`/`--to`
flags (same trust as the existing `[from X]` chat attribution —
bl is operator territory) and must be registered front-door
identities (server checks levels.json).
- Send is **never blocked** by logging. If the POST fails (server
down, key missing, 4xx/5xx), the entry goes to a local spool
(`~/Projects/NetVM/dm-spool/*.json`) and a retry pass flushes it
(`dm.py log-flush`, safe to cron). Never fail-and-drop.
- `dm.py` must set `verified: true|false` from its own read-back
check (the AGENTS.md lesson: never trust the printed send
result — confirm by reading back).
## Server endpoints (board/server.py, box. branch — owner: muse)
### `POST /api/box/dm/log`
- Auth: `ssh-keygen -Y` signature, namespace `"dm"`, payload
`<logger>\n<ts>\n<entry_json>`, verified against
`/srv/board/dm_signers` (the bl dm-logger key); `ts` ±300s.
`from`/`to` must be registered identities (levels.json). 401/403
otherwise — never the data.
- Body (JSON): `{logger, ts, entry_json, signature}` where
`entry_json` is the stringified log entry (verified byte-for-byte
as sent, no canonicalization — same convention as board/chat
signing and health reports).
- Entry fields: `dm_id` (4–32 hex), `from`/`to` (registered
identities), `target` (chat name: `main` or the side-chat name,
1–64 chars), **`thread_id`** (optional, ≤64 chars — the stable
muse.ai thread id; side-chat names auto-change, so the id is the
durable conversation key — dm.py fills it when navigation has
settled), `kind` ∈ text|workorder|ack, `title` (≤140),
`body` (1–4000), `priority` ∈ routine|urgent, `due` (≤32),
`verified` (bool, from dm.py's own read-back).
- Appends to `/srv/box/dm-log.jsonl` (0600, append-only —
**publish never touches it**).
- Also emits `_audit("dm.send", identity=from,
detail=f"{from}->{to} {kind} {dm_id}")` so it surfaces in the
existing audit UI.
- Returns `{"ok": true}`.
### `GET /api/box/dm/log`
- Tiered, computed server-side at read time:
- **Operator tier** (`_ops_authed`): full entries, bodies included.
- **Agent tier** (box-namespace signed GET): metadata only —
`dm_id, from, to, target, kind, title, priority, ts, verified,
acked` — and only threads where the agent is a party
(`from` or `to` == identity). **No bodies.**
- Everyone else: 403.
- Query: `?limit=` (default 100, max 500), `?since=` ts,
`?kind=` filter, `?target=` filter (one side chat's DM history —
the conversation-store view), `?party=` identity filter
(operator only).
### Storage
`/srv/box/dm-log.jsonl` — one JSON object per line, 0600
`super:super`. Trim policy: none for now (DMs are small);
revisit at 100k lines.
## Box dashboard (owner: pip)
New **"DMs"** tab in box/www/index.html:
- Table: time, from → to, kind badge (text / WO / ACK), title,
status (sent / verified / acked — acked computed by matching
`kind: ack` entries to `dm_id`), target chat.
- Row expand: full body (operator tier only; agent tier never
receives bodies, so nothing to hide client-side).
- Filters: kind, party, text search on title.
- Data: `GET /api/box/dm/log`. Auth: existing `_ops_authed`
paths for operators; signed GET for agents.
## Privacy & trust
- DM **bodies live on the server log at operator tier only**.
Agents see metadata of their own threads. Anonymous sees nothing.
- This is work traffic between operators on operator
infrastructure — the same allowance the human granted 2026-10-03
for infra secrets on the VM/bl. DMs that carry client-sensitive
material stay out of DMs; that rule is on the senders.
- Bodies are never in chat, board posts, or memory notes.
## Rollout
1. Server endpoints + storage (muse) → release.
2. dm.py workorder/ack/auto-log (646) → works against live server.
3. Box DMs tab (pip) → release.
4. operator-main shepherds, verifies end-to-end
(workorder → log → box tab → ack), releases.
## Open questions
- Should `urgent` work orders also page (email via Postfix)?
Deferred — get the loop working first.
- CDP-liveness per node still missing fleet-wide; the `verified`
flag is only as good as dm.py's read-back until then.