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
This commit is contained in:
operator
2026-10-07 00:25:46 +00:00
parent 04339bad14
commit 0065d11e97
29 changed files with 6419 additions and 7 deletions
+152
View File
@@ -0,0 +1,152 @@
# 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).
+92
View File
@@ -0,0 +1,92 @@
# Box Approvals over HTTPS (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:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
`box-ctl.py` verbs pre-existed, plus fast node validation and
`quality-validate` branches; `approvals.py` untouched)
**Scope:** approval visibility (check) + governed decisions (deny, auto,
one-shot allow). Persistent/forced allow (`--always`/`--force`) stays
SSH-only.
## 1. Why
Agents blocked on browser approvals needed SSH to see fleet approval
state, deny a bad prompt, auto-resolve trusted prompts, or allow a
known-good one. All of this now rides the agent HTTPS path
(`https://exec.muse-dev.online/exec`, signature or Bearer [REDACTED], named-op
allowlist, audit log).
## 2. New ops
| Op | Args | Backend | Access |
|---|---|---|---|
| `approval.check` | `{node?}` (default fleet) | `box-ctl.py approval-check` | read-only, in `DEFAULT_PERMS` |
| `approval.deny` | `{node!, message!, allow_main_chat?}` | `box-ctl.py approval-deny` | known-identities-only |
| `approval.auto` | `{node?}` (default fleet) | `box-ctl.py approval-auto` | known-identities-only |
| `approval.allow` | `{node!, message!, allow_main_chat?}` | `box-ctl.py approval-allow` (one-shot) | known-identities-only |
New `box-relay.sh` client commands:
```bash
box approvals check [node]
box approvals allow <node> --message <text> [--allow-main-chat]
box approvals deny <node> --message <text> [--allow-main-chat]
box approvals auto [node]
```
Raw op calls (signature auth, no token):
```bash
exec-sign.sh approval.check '{}'
exec-sign.sh approval.check '{"node": "646"}'
exec-sign.sh approval.allow '{"node": "646", "message": "trusted deploy script"}'
exec-sign.sh approval.auto '{"node": "opm"}'
```
## 3. What the decisions do
- `approval.allow` clicks Allow **once** on the node's active prompt.
There is deliberately no remote `--always` (persistent site allow)
or `--force`.
- `approval.deny` clicks Deny on the node's active prompt.
- `approval.auto` scans (fleet or one node) and allows only TRUSTED
non-key prompts. Key/passkey approvals are never auto-approved;
they need an explicit allow/deny, which notifies the waiting agent.
- All three are audited with identity + op + node.
## 4. Safety notes (same posture as existing ops)
- **Attribution is mandatory.** The allow/deny flows DM the waiting
agent, historically with an `[operator]` prefix. A remote caller is
an agent, not the operator — so the exec layer **requires** a
non-empty `message` (≤2000 chars, no controls) on both allow and
deny. (`box-ctl.py` still permits omitting `--message` for SSH
callers; the HTTPS layer is the narrower gate.)
- **One-shot only.** The allow argv never carries `--always` or
`--force`; the validators reject those keys. Persistence stays an
SSH-side decision.
- **Sidechat-first.** `allow_main_chat` defaults to false; Main Chat
delivery needs the explicit flag, same as `notify`/`dm.ack`.
- **Fixed argv, validated values.** Nodes must be fleet members (fast
`BAD_NODE` before any CDP probe — `approval-check`/`approval-auto`
gained the same node check allow/deny already had); unknown arg
keys rejected.
- **Timeouts.** Check 180s (fleet CDP scan), auto 300s (scan plus one
click per trusted prompt), allow/deny 120s.
- **Retry-safe reads.** `approval-check` / `approval-list` joined
`IDEMPOTENT_ACTIONS`; all six approval verbs have
`quality-validate` dry-run branches.
## 5. Rollout notes
- Restart `exec-constrained.py` after deploy for the 4 new ops to
appear in `GET /ops` (repo total becomes 83).
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to
get the `approvals` group.
- While fleet browsers crash-loop, decision ops fail honestly
(`APPROVAL_FAILED` / CDP errors) instead of hanging; check still
reports per-node state including `UNREACHABLE`.
- Non-goals: deletes, `main-loop` enable/disable, policy writes,
swarm kill/prune — future expansions, same pattern.
+80
View File
@@ -0,0 +1,80 @@
# Box Dev + Comms over HTTPS (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:** implemented on bl (`exec-constrained.py` + `box-ctl.py` + `box-relay.sh`)
**Scope:** git visibility, test runs, notify, work-order acks. Read-only lookups
live in `docs/BOX-API-READ-HTTPS.md`.
## 1. Why
Agents developing the box must inspect the tree, run the suite, nudge peers,
and acknowledge work orders without SSH. All of this now rides the existing
agent HTTPS path (`https://exec.muse-dev.online/exec`, signature or bearer
auth, named-op allowlist, audit log) — no new trust model.
## 2. New ops
| Op | Args | Backend | Write? | Who |
|---|---|---|---|---|
| `git.status` | `{}` | `box-ctl.py git-status` | no | any valid signer |
| `git.diff` | `{path?, stat?}` | `box-ctl.py git-diff [--stat] [--path p]` | no | any valid signer |
| `git.log` | `{limit?, path?}` (1..50, default 10) | `box-ctl.py git-log` | no | any valid signer |
| `tests.run` | `{test?, filter?}` (`tests.<module>` or full suite; `filter` is unittest `-k`) | `box-ctl.py tests-run [module] [--filter p]` | yes (executes) | known identities only |
| `notify.send` | `{agent, message≤1000, sidechat?, sender?}` | `box-ctl.py notify ...` | yes (sends DM) | known identities only |
| `dm.ack` | `{id, to, sender!, sidechat?, allow_main_chat?}` | `box-ctl.py ack ...` | yes (sends DM) | known identities only |
New `box-ctl.py` verbs: `git-status`, `git-diff`, `git-log`, `tests-run`,
`ack` (all in `USAGE`, `quality-validate`, and — for the git reads —
`IDEMPOTENT_ACTIONS`).
New `box-relay.sh` client commands:
```bash
box git status
box git diff [--stat] [--path <path>] # path also accepted positionally
box git log [<limit=10>] [--path <path>]
box tests run [tests.<module>] [--filter <pattern>] # full suite (~2-3 min) when omitted; filter is -k
box notify <agent> [--sidechat <n>] [--sender <a>] <message...>
box dm ack <id> --to <agent> --sender <agent> [--sidechat <name>]
```
Raw op call (signature auth, no token):
```bash
exec-sign.sh git.log '{"limit": 5, "path": "bin/dm.py"}'
exec-sign.sh tests.run '{"test": "tests.test_box_dev_https"}'
exec-sign.sh dm.ack '{"id": "bdf7beb6", "to": "pip", "sender": "opm"}'
```
## 3. Safety notes (same posture as existing ops)
- **Fixed argv, validated values.** Clients influence only whitelisted argument
values. Git paths must be repo-relative without `..` (plus symlink-escape
check in box-ctl); test modules must match `^tests\.[a-z0-9_]+$` and exist;
ack ids must be 6–64 hex; notify messages ≤1000 chars.
- **Caps.** `git diff` output capped at 64KB, status at 200 entries, test
output at 32KB tail; every capped response carries `truncated: true`.
- **Sidechat-first.** `notify.send` and `dm.ack` default to the recipient's
sidechat and never touch Main Chat unless `allow_main_chat` is set —
mirroring `box-ctl.py notify` and the WO dispatcher.
- **Attribution.** `sender` is caller-asserted (validated ∈ fleet agents),
same as the existing `dm.send` op; the HTTPS identity is recorded
separately in the exec audit log. `dm.ack` requires an explicit sender —
no silent default.
- **tests.run executes repo code** (whatever is in `tests/`), so it is
`side_effecting`, excluded from the read-only default permission subset,
and capped at a 600s timeout. Test failures report as
`{"ok": false, "returncode", "output"}` — the op itself succeeded.
- New `box-ctl.py` fail codes `GIT_ERROR` / `TESTS_ERROR` are registered in
`KNOWN_ERROR_CODES`, so `quality-check` stays at its baseline.
## 4. Rollout notes
- Restart `exec-constrained.py` after deploy for the new ops to appear in
`GET /ops`.
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
`git` / `tests` / `notify` / `dm ack`.
- Non-goals: git commit/push, service restarts for box itself, live
streaming tails — future expansions, same pattern.
+88
View File
@@ -0,0 +1,88 @@
# Box Job Lifecycle over HTTPS (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:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
`box-ctl.py` verbs pre-existed)
**Scope:** safe job-lifecycle mutations. Deletes are deliberately NOT
exposed (`job-delete`, `timer-delete` stay SSH/operator-only).
## 1. Why
Agents own scheduled automation but could only run jobs (`cron.run`) or read
them (`box.exec`). Creating, updating, triggering, chaining, previewing, and
pausing jobs needed SSH. All of this now rides the agent HTTPS path
(`https://exec.muse-dev.online/exec`, signature or bearer auth, named-op
allowlist, audit log).
## 2. New ops
| Op | Args | Backend | Write? | Who |
|---|---|---|---|---|
| `job.put` | `{name, definition}` | `box-ctl.py job-put` (definition on stdin) | yes (writes + commits) | known identities only |
| `job.trigger` | `{name}` | `box-ctl.py job-trigger` | yes (dispatches now) | known identities only |
| `job.chain` | `{from, to, on_failure?}` | `box-ctl.py job-chain` | yes (writes + commits) | known identities only |
| `job.next` | `{job_id, success?}` | `box-ctl.py job-next` | no (dry-run) | any valid signer |
| `cron.timer_stop` | `{name}` | `box-ctl.py timer-stop` | yes (systemd) | known identities only |
| `cron.timer_disable` | `{name}` | `box-ctl.py timer-disable` | yes (systemd) | known identities only |
New `box-relay.sh` client commands:
```bash
box cron put <name> '<json-definition>' # create or update (see §3)
box cron trigger <name> # dispatch now (audited JSON)
box cron chain <from> <to> [--on-failure] # wire chain_next
box cron next <job-id> [--success|--fail] # dry-run: what dispatches next
box timer stop <name> # pause schedule (keeps unit)
box timer disable <name> # pause schedule (disables unit)
```
Raw op call (signature auth, no token):
```bash
exec-sign.sh job.next '{"job_id": "heartbeat-20200101-000000-deadbeef"}'
exec-sign.sh job.chain '{"from": "ops-audit-step2", "to": "ops-audit-step3"}'
```
Notes:
- `job.trigger` vs existing `job.run`: `job.run` shells straight to
`job-dispatch.py` and relays raw output; `job.trigger` goes through
`box-ctl.py` (existence check, 300s bound, audit trail, JSON contract).
Prefer `job.trigger` for agent-driven dispatches.
- `job.next` job ids look like `<name>-YYYYMMDD-HHMMSS-<8hex>`; with no
`success` flag the box infers it from the last recorded result.
## 3. Job definition shape (`job.put`)
The full schema is enforced by `box-ctl.py validate_job` (single copy);
required fields: `name` (must match the argv name), `schedule` (`manual`
or convertible cron), `agent` (fleet member), `prompt_template` (1–4000
chars, no protocol literals, known `{placeholders}` only). `timeout`
60–3600s, `on_failure` policy, optional `chain_next` (must exist) and
`sidechat` / `dm_target` routing. `box-ctl.py` writes `jobs/<name>.json`
and commits (`Add/Update job <name> via box-ctl`).
## 4. Safety notes (same posture as existing ops)
- **Fixed argv, validated values.** Job names match
`^[a-z0-9][a-z0-9-]{0,63}$`; trigger/chain/timer ops require the job
file to exist; chain rejects self-links and cycles; timer ops require
the unit to exist. All checks run before any side effect.
- **Stdin plumbing.** `job.put` is the first op to pipe a request body to
`box-ctl.py` stdin (the raw definition, not the envelope); the routing
lives in one helper (`_stdin_body`) covered by unit tests.
- **No deletes, no kills.** `job-delete` / `timer-delete` are reachable
only over the operator SSH path, by explicit scope decision.
- **Audited.** Every execution records identity + op + args hash;
`box-ctl.py` additionally audits each mutation with its target.
## 5. Rollout notes
- Restart `exec-constrained.py` after deploy for the new ops to appear in
`GET /ops`.
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
the `cron put|trigger|chain|next` and `timer stop|disable` commands.
- Non-goals: deletes, `job-result` ingestion, `strat`/`loop` writes,
approvals — future expansions, same pattern.
+85
View File
@@ -0,0 +1,85 @@
# Box Loop + Strategy Writes over HTTPS (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:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
`box-ctl.py` verbs pre-existed, plus a vars-name validator fix)
**Scope:** safe loop/strategy/variable mutations. Reads (`loop-status`,
`loop-health`, `loop-breaks`, `strat-get`, `vars-get`, ...) already ride
`box.exec` or typed read ops and are unchanged here.
## 1. Why
Agents watching loop health could see breaks but needed SSH to remediate
them, resolve stale followups, tune strategy overrides, or undo variable
changes. All of this now rides the agent HTTPS path
(`https://exec.muse-dev.online/exec`, signature or bearer auth, named-op
allowlist, audit log).
## 2. New ops (all known-identities-only, none in the read-only subset)
| Op | Args | Backend |
|---|---|---|
| `loop.remediate` | `{dry_run?}` (default false) | `box-ctl.py loop-remediate [--dry-run]` |
| `loop.resolve` | `{dm_id, note?}` | `box-ctl.py loop-resolve` |
| `strat.set` | `{type!, subtype?, agent?, track?, priority?, timeout_s?, nudges?, escalate?}` | `box-ctl.py strat-set` (payload as argv JSON) |
| `strat.reset` | `{type!, subtype?, agent?}` | `box-ctl.py strat-reset` |
| `vars.reset` | `{name}` | `box-ctl.py vars-reset` (restore default) |
| `vars.rollback` | `{name, revision?}` (int step or timestamp) | `box-ctl.py vars-rollback` |
New `box-relay.sh` client commands:
```bash
box loop remediate [--dry-run]
box loop resolve <dm_id> [note...]
box strat set <type> [--subtype S] [--agent A] [--track true|false] [--priority p] [--timeout N] [--nudges N] [--escalate E]
box strat reset <type> [subtype] [--agent <agent>]
box vars reset <name>
box vars rollback <name> [revision]
```
Raw op call (signature auth, no token):
```bash
exec-sign.sh loop.remediate '{"dry_run": true}'
exec-sign.sh strat.set '{"type": "job", "priority": "important", "nudges": 3}'
```
## 3. What remediate does (non-dry)
`loop.remediate` delegates to `gravity.remediate_breaks`: resolves
followups already answered in logs, re-arms expired followups with nudges
remaining (runs the followup sweeper once), auto-allows TRUSTED (non-key)
browser approvals, and on hard breaks appends a `hard_break_alert` to
job-log plus one DM to opm. Use `{"dry_run": true}` first to preview the
`remediated` / `escalated` lists with zero side effects.
## 4. Safety notes (same posture as existing ops)
- **Fixed argv, validated values.** Strategy types are a strict enum
(`wake|job|siphon|manual|health|heartbeat`) — the backend silently maps
typos to MANUAL, so the op rejects them instead. Priorities are a
strict enum; timeouts/nudges must be integers (backend clamps);
dm ids must be 6–64 hex; variable names mirror the engine identifier
rule. All checks run before any side effect.
- **Stdin-free.** Unlike `job.put`, `strat.set` passes its JSON payload
as an argv token (`strat-set <type> [JSON]`), so no new stdin plumbing
was needed.
- **Vars-name validator fix.** `quality-validate` for all five vars
verbs used the job-name regex (`^[a-z0-9-]{1,64}$`), rejecting every
real variable name (`max_nudge_count`, ...). They now share
`qv_var_name` (`^[A-Za-z0-9_.-]{1,64}$`), mirroring the
`exec-constrained.py` rule.
- **Audited.** Every execution records identity + op + args hash;
`box-ctl.py` additionally audits each mutation with its target.
## 5. Rollout notes
- Restart `exec-constrained.py` after deploy for the new ops to appear in
`GET /ops`.
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
the `loop` / `strat` groups and `vars reset|rollback`.
- Non-goals: approvals, deletes, `main-loop` enable/disable — future
expansions, same pattern. (md drive files shipped separately; see
BOX-MD-HTTPS.md.)
+109
View File
@@ -0,0 +1,109 @@
# Box Md Drive Files over HTTPS (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:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
`box-ctl.py` verbs pre-existed, plus traversal hardening, output caps,
`--stdin` content plumbing, and hyphenated amend/append/pull aliases)
**Scope:** md reads (audit/list/read/diff) + governed writes
(amend/append/pull/inject-drive/sync-all). Raw container writes
(`md-write`) stay SSH-only by design.
## 1. Why
Agents shaping fleet behavior could see drive scores but needed SSH to
read an agent's `SOUL.md`, diff it against the canonical template, or
push updated operator files. All of this now rides the agent HTTPS path
(`https://exec.muse-dev.online/exec`, signature or Bearer [REDACTED], named-op
allowlist, audit log).
## 2. New ops
Reads (all `side_effecting: false`, all in `DEFAULT_PERMS`):
| Op | Args | Backend |
|---|---|---|
| `md.audit` | `{accounts?}` (default all) | `box-ctl.py md-audit [accounts...]` |
| `md.list` | `{account!, path?}` | `box-ctl.py md-list` (capped, see §4) |
| `md.read` | `{account!, filename!}` | `box-ctl.py md-read` (capped, see §4) |
| `md.diff` | `{account!, filename!}` (shared template only) | `box-ctl.py md-diff` (capped, see §4) |
Governed writes (all known-identities-only, none in the read-only subset):
| Op | Args | Backend |
|---|---|---|
| `md.pull` | `{account!, filename!}` (shared template only) | `box-ctl.py md-pull` |
| `md.inject_drive` | `{account!}` | `box-ctl.py md-inject-drive` |
| `md.sync_all` | `{}` | `box-ctl.py md-sync-all` |
| `md.amend` | `{filename!, content!, author?, reason?}` | `box-ctl.py md-amend --stdin` (content on stdin) |
| `md.append` | `{filename!, text!, author?, section?}` | `box-ctl.py md-append --stdin` (text on stdin) |
New `box-relay.sh` client commands:
```bash
box md audit [accounts...]
box md list <account> [path]
box md read <account> <filename>
box md diff <account> <filename>
box md pull <account> <filename>
box md inject-drive <account>
box md sync-all
box md amend <filename> (--content <text>|--file <path>) [--author <name>] [--reason <why>]
box md append <filename> (--content <text>|--file <path>) [--author <name>] [--section <header>]
```
Raw op calls (signature auth, no token):
```bash
exec-sign.sh md.audit '{"accounts": ["646", "opm"]}'
exec-sign.sh md.read '{"account": "646", "filename": "SOUL.md"}'
exec-sign.sh md.diff '{"account": "pip", "filename": "HEARTBEAT.md"}'
exec-sign.sh md.append '{"filename": "AGENTS.md", "text": "lesson ...", "author": "646"}'
```
## 3. What the governed writes do
- `md.amend` rewrites a `shared/operators/` template after the
drive-safety checks (HEARTBEAT checklist not gutted,
PROACTIVE_PREFERENCES not blanked, SOUL not reverted to stock),
then git-commits it. Full-file content rides stdin (up to 256KB).
- `md.append` appends a timestamped, attributed note (optional section)
via the same validated + committed path (up to 64KB).
- `md.pull` / `md.inject_drive` / `md.sync_all` push canonical
templates *out* to containers; no agent-supplied content crosses.
Injection always overwrites (AGENTS.md preserves remote `## Lessons`).
## 4. Safety notes (same posture as existing ops)
- **Traversal hardening (single-copy in `agent_md.py`).** Account,
filename, and list-path validation now lives in `agent_md.py`
(`MDValidationError`, raised before any gateway call or write);
`box-ctl.py` maps it to `BAD_NAME`, and exec ops + quality-validate
mirror the same shapes. Previously `md-read 646 ../x` reached the
gateway and `md amend ../../x` could escape `shared/operators/`.
Template flows (diff/amend/append/pull) additionally require one of
the 8 known template names.
- **Fixed argv, validated values.** Unknown arg keys rejected; author /
reason / section are control-char-free with length caps; amend
content must be non-empty.
- **Caps with `truncated` flags.** Reads cap at 64KB, diffs at 64KB,
listings at 200 entries — same convention as git/tests verbs.
- **No raw `md-write` op.** Arbitrary content-to-container stays
SSH-only; remote writes go through the validated template flows.
- **Timeouts.** Audit 300s, sync-all 600s, single-file ops 120s.
- **Audited.** Every execution records identity + op; `box-ctl.py`
additionally audits each verb with its target.
- **Retry-safe reads.** `md-audit` / `md-list` / `md-read` / `md-diff`
joined `IDEMPOTENT_ACTIONS`; all ten md verbs have
`quality-validate` dry-run branches.
## 5. Rollout notes
- Restart `exec-constrained.py` after deploy for the 9 new ops to
appear in `GET /ops` (repo total becomes 79).
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to
get the `md` group.
- Non-goals: deletes, `main-loop` enable/disable, policy writes,
swarm kill/prune — future expansions, same pattern. (Approvals
shipped separately; see BOX-APPROVALS-HTTPS.md.)
+103
View File
@@ -0,0 +1,103 @@
# Muse-Choices Deny/Escalate Policy — DECISION RECORD (Final)
Topic: add deny/escalate decisions to the `muse-choices` auto-approve daemon
(`bin/muse_choice_watcher.py`), which today only approves (top choice per
prompt kind). Interviewed 2026-10-06/07 per grill contract; accepted
verbatim below, which flipped this record from Draft to Final.
## Standing constraints (settled by user)
- All prompts must resolve: no stuck states are acceptable in any outcome.
- The full-auto top-choice flow must always exist as a path.
- Model review of choices is a FUTURE layer. Deferred out of this interview.
## Settled decisions
- D0 (helper form): a checked-in repo rules file informs decisions.
Source: user's structured answerquared 2026-10-06 ("Repo rules file
(Recommended)" for "which helper should inform approve/deny/hold
decisions"). Rationale recorded at selection time: deterministic,
versioned, sub-second, unit-testable; no agent round-trip latency.
## Settled during interview
- D0b (approve path needs no helper): straightforward prompts resolve
locally with top-choice keys (the five matcher kinds, already
implemented and live). Helpers (D0 rules file) govern deny/hold
judgments only. Source: user direction 2026-10-06 ("the watcher
itself should be able to input 1"; "always have the flow for full
auto just top choice").
- D1 (rule match dimensions): command pattern first, plus kind and
text pattern. Source: user selected option 1, 2026-10-06.
Rationale: the observed risk lives in the `$ command` of approval
dialogs; kind/text add precision around it.
- D2 (deny mechanics): deny exists only for permission kinds --
`muse-approval` dialogs receive `2` + Enter, `y/n` prompts receive
`n` + Enter. Question kinds (interview, letter, numbered) always
resolve top-choice and are never denied. Source: user selected
option 1, 2026-10-06. Rationale: deny is only meaningful where a
permission is refused; questions stay total.
- D3 (hold mechanics): hold leaves the dialog untouched, suppresses
auto-answer, raises a HELD entry in `box muse-choices status` plus
an audit record; the operator resolves via a box command, otherwise
a SHORT window expires back to top-choice approve. Source: user
selected option 1 with "short window", 2026-10-06. Exact duration
proposed below (2 minutes, tunable); accepted or amended with the
scope text in D5.
- D4 (unmatched default): approve top-choice, exactly today's
behavior. Source: user selected option 1, 2026-10-06. Rationale:
follows from the standing constraints; rules carve out only
deny/hold exceptions, so an empty rules file changes nothing.
## Open questions (unresolved)
None. All interview questions resolved and the scope accepted.
## Scope contract (ACCEPTED)
Artifact boundary, IN:
- `docs/MUSE-CHOICES-POLICY.md`: this record (Draft -> Final on acceptance).
- New `muse-choices-rules.json` at repo root (beside
`keepalive-config.json`): the checked-in deny/hold rules.
- `bin/muse_choice_watcher.py`: rule evaluation, deny/hold paths, HELD
state with short-window expiry, resolve plumbing.
- `bin/super-cli.py`: `box muse-choices resolve` command + HELD display
in status.
- `tests/test_muse_choice_watcher.py`: rule eval, per-kind deny keys,
hold/suppress/expiry, resolve flow.
Artifact boundary, OUT (rejected or deferred, each needs its own
interview to re-enter):
- Model review of choices (deferred future stage).
- Peer-agent consultation (rejected in D0).
- New matcher shapes (matcher suite's lane).
- `box runtime` work (adjacent lane, untouched).
- Timer cadence / daemon supervision changes.
Done means (all observable):
- [ ] This record marked Final with the acceptance quoted.
- [ ] Rules file loads; empty rules == today's behavior exactly.
- [ ] Deny sends `2`+Enter / `n`+Enter per D2: unit tests + one live
scratch proof per permission kind.
- [ ] Hold suppresses + shows HELD + resolves via box + expires to
approve: unit tests + one live scratch proof of hold and one of
expiry.
- [ ] Audit records for deny/hold/resolve/expire in `box-ctl.jsonl`.
- [ ] Full suite green; fleet reloaded; desired state left as found.
Acceptance (quoted verbatim, chat, 2026-10-07T00:19:57Z): "ACCEPT".
Accepted as written, including the 2-minute tunable hold window. Per the
grill scope contract, later work outside the IN boundary needs explicit
owner approval or its own follow-up interview; "go" authorizes only this
boundary. No owning issue exists in this workflow, so this record is the
lane-coordination evidence.
## Non-goals (accepted with the scope)
- Model-based review of choices (deferred future layer).
- Peer-agent consultation over sidechat (rejected in favor of D0).
- Changes to approval matching shapes (covered by the matcher test suite).
+81
View File
@@ -0,0 +1,81 @@
# Supervision Scope Contract
Status: **Draft** — decisions below are unsettled until marked otherwise.
Only explicit user acceptance moves this document (or any decision) to Final.
## Goal
Every fleet node stays alive and truthfully reported: browsers supervised,
relays supervised, dead nodes recovered or loudly paged, and `box` status
honest from any shell (including PID/net-blind sandboxed shells).
## Non-goals (proposed)
- Agent lifecycle/onboarding stages (provision, auth, OTP, invite redeem).
- Work completion (job dispatch, followups, harvester, completion auditor).
- Loop-health scoring and drive repair.
## Supervisors (observed, all installed 2026-10-06)
| Supervisor | Cadence | Coverage | Decides |
|---|---|---|---|
| chromebox-watchdog@\<node\>.timer ×6 | 2 min | all registry nodes (def/dev timers installed 18:32Z) | browser+egress per node; tunnel restart, chrome relaunch |
| cdp-relay-watchdog.timer | 5 min | registry-driven (`watched_nodes()`) | relay veth IP + connectivity; relay restart |
| agent-health.timer (user) | 5 min | registry-driven | API check per node; kill+restart with 2-strike rule + circuit breaker (3 futile → open 30 min) |
| ensure-node-supervision.sh | on node-up / `--all` | new + drifted nodes | NODES.md row + chromebox timer install |
| host_evidence fallback | on `box` read | registry (relay) + installed timers (browser) | effective status when live probes are blind |
## Decisions
(D1..D7 below — all UNRESOLVED unless marked.)
### D1. Contract boundary: which supervisors are in scope — SETTLED (recommended accepted)
IN: chromebox-watchdog ×6, cdp-relay-watchdog, agent-health + circuit
breaker, ensure-node-supervision feed, host_evidence fallback.
OUT: onboarding pipeline lifecycle, completion auditor, loop-health
(each keeps its own owner and interviews separately).
### D2. Kill-path precedence (chromebox-watchdog vs agent-health) — UNRESOLVED
Both can kill a browser today; only time guards (<2 min) de-conflict them.
### D3. Egress-down fall-through (relaunch chrome after failed tunnel restart?) — UNRESOLVED
Observed 19:12Z: tunnel restart failed, watchdog relaunched chrome 3×
anyway (one FAILED page). Browser was never the problem.
### D4. Circuit-breaker thresholds (3 futile / 30 min cooldown) — UNRESOLVED
Current values unvalidated against real recurrence intervals.
### D5. Coverage source of truth — UNRESOLVED
Registry-only vs registry+installed-timers for browser verdicts.
### D6. Concurrent-edit protocol for shared supervision files — UNRESOLVED
Two agents editing super-cli.py / watchdogs / runbook; one clobber
(18:03Z) and one unattributed commit (c9143a5) already occurred.
### D7. Done means — UNRESOLVED
Proposed checklist: timers on all 6 firing silent; relay/agent-health
loops registry-driven with tests; ensure hook live; this doc Final.
## Risks
- Egress-down pages read as browser failures (D3).
- Uncommitted supervisor work can be clobbered by a concurrent editor (D6).
- c9143a5 contains unattributed peer hunks (host_evidence `_covered_nodes`,
fleet-status test updates) — needs an amend-or-leave decision.
## Validation
- `box fleet status` truthful from blind shells (live-verified 6/6 ACTIVE).
- Focused suites green (supervision, fleet, agent-health, watchdog coverage).
- Timer firing proven via journal, not config presence.
## Unresolved items
D2–D7 unresolved. D1 settled.