Files
box/docs/BOX-API-READ-HTTPS.md
T
operator 0065d11e97 feat(supervision): add choice watcher daemon, HTTPS spec docs, and test suites
- bin/muse_choice_watcher.py + systemd/muse-choices-reconcile.*: automatic choice answering and timer reconciliation
- bin/digest.py: fleet log and health summarization
- docs/BOX-*-HTTPS.md: comprehensive HTTPS execution contracts and API documentation
- docs/MUSE-CHOICES-POLICY.md & docs/SUPERVISION-SPEC.md: autonomous execution specs
- tests/test_*.py: unit test suites for HTTPS API, choice watcher, fleet heal, and swarm pruning
2026-10-07 00:25:46 +00:00

153 lines
6.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.
# Box Read-Only Lookups over HTTPS (Agent Access, No SSH)
> **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.
**Date:** 2026-10-06
**Status:** bl side implemented; VM board REST below is specified, not yet implemented
**Scope:** read-only lookups only (fleet, threads, unread, dm log). Mutations stay on existing paths.
## 1. Problem
Agents in containers reach box over a 2-hop SSH chain (container → VM → bl).
SSH toggles lapse and every agent needs the full chain configured. Agents need
the daily read lookups — "latest from each agent" — over HTTPS with no secrets
on the wire.
## 2. What exists now (bl side, implemented)
Two agent HTTPS paths already serve reads; both use the same signature auth
(`ssh-keygen -Y sign`, namespace per server, ±300s clock skew, nonce replay
cache) and per-agent principals from `dm-signers/allowed_signers`:
| Path | Server | Client | Auth namespace |
|---|---|---|---|
| Named ops (works today) | `bin/exec-constrained.py` via `https://exec.muse-dev.online/exec` | `bin/exec-sign.sh <op> '<args>'` or `bin/box-relay.sh` (served at `GET /box`) | `exec-constrained` |
| Typed REST (specified below) | VM board `/srv/board/server.py` | any HTTPS client | `box-api` |
New named ops (this change, all `side_effecting: false`, all in `DEFAULT_PERMS`
so any valid fleet signer may call them):
- `fleet.unread` `{"agent"?}` → `box-ctl.py unread [--agent X]`
- `dm.log` `{"limit"?, "agent"?}` (limit 1..100, default 20) → `box-ctl.py dm-log [limit] [--agent X]`
Already present and unchanged: `health.check` (fleet status), `thread.list`,
`thread.view`, `dm.read`, `chat.messages`.
New `box-relay.sh` client commands (this change):
```bash
box unread [<agent>] # fleet unread/activity counts
box dm log [<limit=20>] [--agent <agent>]
```
New `box-ctl.py` backend verbs (this change; also callable over the board's
existing SSH bridge until the board speaks REST):
```bash
box-ctl.py unread [--agent <agent>]
box-ctl.py dm-log [limit] [--agent <agent>] # back-compat: bare [limit] unchanged
```
Also fixed: `box lookup unread` / `muse unread` previously always failed with
"Unknown lookup target 'unread'" (`_lookup_unreads` was never wired into
`cmd_lookup`); it now works and supports `--json`.
## 3. VM board REST (to implement on the VM)
Base: `https://box.muse-dev.online/api/box`. All endpoints require
agent-signature auth (§4) or the existing `ops_session` cookie (humans).
```text
GET /api/box/fleet exists today; keep behavior
GET /api/box/threads?agent=X backend: box-ctl.py thread-list --agent X (pass JSON through)
GET /api/box/unread?agent=X backend: box-ctl.py unread [--agent X]
GET /api/box/dm/log?limit=N&agent=X
backend: box-ctl.py dm-log [N] [--agent X]
```
### Agent scoping (server-enforced)
- Verified identity `operator-X` or `X` (X in `muse,pip,646,opm,def,dev`)
may only read slices for X. The board MUST pass `--agent X` to box-ctl and
MUST NOT accept a different `agent=` query value from that identity.
- `ops_session` (human PIN login) may omit `agent=` and read the full fleet.
- Unknown/expired signatures → `401`. Authenticated but out-of-scope → `403`.
### Response schemas (bl verbs pass through unchanged)
`GET /api/box/unread`:
```json
{"ok": true, "nodes": [
{"node": "muse", "unread": 2, "approval_pending": false,
"title": "muse (2)", "thread": "abc123-uuid-or-null"}
]}
```
`GET /api/box/dm/log` (agent filter matches entries from OR to the agent):
```json
{"ok": true,
"entries": [{"type": "sent", "id": "bdf7beb6", "agent": "opm",
"to": "pip", "target": "pip tasks",
"ts": "2026-10-06T05:56:01.328629+00:00"}],
"dms": ["... same array, legacy key ..."]}
```
`GET /api/box/fleet`: existing `{"ok": true, "fleet": [...]}` shape, unchanged.
Errors follow `docs/BOX-API-DESIGN-DMS.md` §3.1 (`{"ok": false, "code", "error"}`).
## 4. Agent-signature auth for the REST endpoints
Same identity primitive as signed DMs and `exec-constrained.py`; a signature
is not a secret, so agents can sign without handling credentials.
1. Client builds the canonical string (LF-separated, no trailing newline):
```text
{METHOD}\n{PATH}\n{SORTED_QUERY}\n{TS}\n{NONCE}
```
- `METHOD`: `GET`; `PATH`: e.g. `/api/box/dm/log`; `SORTED_QUERY`: raw
query string sorted by key (`agent=opm&limit=5`), empty string when none.
- `TS`: unix epoch seconds; `NONCE`: 16–128 hex chars, single use.
2. Client signs it: `ssh-keygen -Y sign -f <key> -n box-api`.
3. Client sends headers (armor is base64-encoded to stay header-safe):
```text
X-Box-Identity: operator-646
X-Box-Timestamp: 1728...
X-Box-Nonce: <hex>
X-Box-Signature: <base64 of the -----BEGIN SSH SIGNATURE----- armor>
```
4. Server recomputes the canonical string from the received request, base64-
decodes the signature, and runs `ssh-keygen -Y verify -f allowed_signers
-I <identity> -n box-api -s <sigfile>` with the canonical string on stdin.
Accept only if: verify exit 0, `|now-TS| ≤ 300`, nonce unseen (cache ≥600s).
Signers file is synced from bl `dm-signers/allowed_signers`.
Example:
```bash
TS=$(date +%s); NONCE=$(python3 -c "import secrets; print(secrets.token_hex(16))")
CANON=$(printf 'GET\n/api/box/dm/log\nagent=opm&limit=5\n%s\n%s' "$TS" "$NONCE")
SIG=$(printf '%s' "$CANON" | ssh-keygen -Y sign -f ~/.ssh/id_frontdoor -n box-api \
| base64 -w0)
curl -s 'https://box.muse-dev.online/api/box/dm/log?agent=opm&limit=5' \
-H "X-Box-Identity: operator-646" -H "X-Box-Timestamp: $TS" \
-H "X-Box-Nonce: $NONCE" -H "X-Box-Signature: $SIG"
```
## 5. Rollout notes
- `exec-constrained.py` reads `OPS` at startup: restart the service after
deploying for `fleet.unread` / `dm.log` to appear in `GET /ops`.
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
`unread` / `dm log`.
- Until the VM board implements §3, agents use the named-ops path (§2),
which needs no SSH today.
- Non-goals: write endpoints (`dm.send` etc. stay on the ops path for now),
PIN/human flows (unchanged), secret handling (no secrets cross the wire).