177 lines
7.0 KiB
Markdown
177 lines
7.0 KiB
Markdown
|
|
# DM Spec: Work Orders + Server-Side DM Logging + Box Visibility
|
|||
|
|
|
|||
|
|
## 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.
|