@staix/agent-hub 0.12.20 → 0.12.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +17 -9
  3. package/docs/agent-notes/adapters.md +13 -3
  4. package/docs/agent-notes/benchmarks.md +1 -1
  5. package/docs/agent-notes/bus.md +7 -3
  6. package/docs/agent-notes/daemon.md +12 -2
  7. package/docs/agent-notes/local-worker.md +5 -1
  8. package/docs/agent-notes/tasks.md +4 -2
  9. package/docs/agent-notes/tests.md +5 -2
  10. package/docs/bench.md +143 -0
  11. package/docs/events.md +15 -5
  12. package/docs/operations.md +510 -54
  13. package/docs/quickstart.md +16 -11
  14. package/docs/research.md +92 -0
  15. package/docs/security.md +49 -11
  16. package/docs/smoke.md +138 -0
  17. package/docs/specs/2026-09-19-agent-hub-design.md +227 -20
  18. package/docs/specs/2026-09-20-upgrade-recovery-design.md +152 -18
  19. package/docs/verified.json +34 -17
  20. package/package.json +1 -1
  21. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  22. package/plugins/agent-hub/server.js +3 -2
  23. package/src/adapters/acp.ts +77 -10
  24. package/src/adapters/codex-appserver.ts +72 -2
  25. package/src/adapters/local-worker.ts +46 -11
  26. package/src/adapters/pi.ts +215 -22
  27. package/src/cli/bench.ts +287 -0
  28. package/src/cli/console-state.ts +226 -28
  29. package/src/cli/console.ts +16 -4
  30. package/src/cli/facts-hook.ts +50 -8
  31. package/src/cli/help.ts +16 -5
  32. package/src/cli/identity.ts +10 -4
  33. package/src/cli/launch.ts +272 -18
  34. package/src/cli/main.ts +290 -30
  35. package/src/cli/preview.ts +13 -3
  36. package/src/cli/recovery-package.ts +17 -0
  37. package/src/cli/status-lines.ts +2 -1
  38. package/src/cli/terminal-recovery.ts +83 -31
  39. package/src/cli/upgrade-interactive.ts +297 -0
  40. package/src/cli/upgrade-runtime.ts +123 -20
  41. package/src/cli/upgrade.ts +108 -19
  42. package/src/hub/bench.ts +211 -0
  43. package/src/hub/bus.ts +24 -7
  44. package/src/hub/conductor.ts +22 -21
  45. package/src/hub/config-trust.ts +1 -0
  46. package/src/hub/control-client.ts +4 -1
  47. package/src/hub/daemon.ts +477 -86
  48. package/src/hub/delivery-journal.ts +6 -6
  49. package/src/hub/events.ts +4 -2
  50. package/src/hub/lifecycle.ts +8 -13
  51. package/src/hub/manager.ts +16 -14
  52. package/src/hub/peers.ts +2 -2
  53. package/src/hub/permission-mode.ts +44 -0
  54. package/src/hub/recovery-store.ts +59 -9
  55. package/src/hub/registry.ts +36 -22
  56. package/src/hub/research.ts +255 -0
  57. package/src/hub/tasks.ts +99 -55
  58. package/src/hub/tool-refusal.ts +7 -0
  59. package/src/hub/ui.ts +4 -1
  60. package/src/local/deny.ts +48 -1
  61. package/src/local/sandbox.ts +6 -3
  62. package/src/local/tools.ts +34 -11
  63. package/src/pi/extension.ts +43 -20
  64. package/src/pi/process-signature.ts +23 -0
  65. package/src/pi/tool-receipts.ts +24 -2
  66. package/src/ui/index.html +98 -17
  67. package/src/ui/task-progress.ts +33 -0
@@ -17,7 +17,7 @@ ahub setup # installs the Claude Code channel plu
17
17
  Or use the matching GitHub release:
18
18
 
19
19
  ```bash
20
- bun add -g github:STAIxBWLB/agent-hub#v0.12.20
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.12.22
21
21
  ahub setup
22
22
  ```
23
23
 
@@ -26,7 +26,9 @@ From a clone instead: `git clone`, `bun install`, `bun link`.
26
26
  `ahub doctor` tells you what is installed, running and configured. Rows you do not need can stay red: the hub works with any subset of the peers.
27
27
 
28
28
  For managed launchers, two-agent task/review work, approvals, graceful shutdown,
29
- and upgrade or crash recovery, see the [operations guide](operations.md).
29
+ and upgrade or crash recovery, see the [operations guide](operations.md). To
30
+ move running projects to a newer release later, run `ahub upgrade` in a
31
+ terminal: it reviews the plan with you, applies it and follows it.
30
32
 
31
33
  ## First session
32
34
 
@@ -50,13 +52,14 @@ ahub say @kimi "run the tests and report" # one peer
50
52
 
51
53
  ## The local worker
52
54
 
53
- `local` needs an OpenAI-compatible gateway. Put yours in `.agenthub/config.json`:
55
+ `local` needs an OpenAI-compatible gateway. Put yours in `.agenthub/config.local.json`
56
+ (machine-local, kept out of git by `ahub init`):
54
57
 
55
58
  ```json
56
59
  { "omniroute": { "urls": ["http://your-gateway:20128/v1"], "api_key_file": "/path/to/key" } }
57
60
  ```
58
61
 
59
- or set `OMNIROUTE_API_KEY`. Name the model in `.agenthub/routing.toml` (`[local] fixed_model`). Then `ahub local`. It works only inside the project, asks before it writes or runs anything (`ahub permit <id> allow`), and everything it executes is sandboxed.
62
+ or set `OMNIROUTE_API_KEY`. Name the model in `.agenthub/routing.toml` (`[local] fixed_model`). Then `ahub local`. It works only inside the project, asks before it writes or runs anything (`ahub permit <id> allow`) unless you chose a permission mode for it (`ahub permission`), and everything it executes is sandboxed. Keep `ahub console` open while it works: a request nobody answers expires, and after two in a row its turn ends.
60
63
 
61
64
  ## Commands of a working day
62
65
 
@@ -67,6 +70,7 @@ or set `OMNIROUTE_API_KEY`. Name the model in `.agenthub/routing.toml` (`[local]
67
70
  | `ahub board` | who has what; `ahub task show <id>` for one task |
68
71
  | `ahub status` | peers, queues, pauses; `ahub budget` for quota windows |
69
72
  | `ahub ask <question>` | what the board, shared memory and the log say, with the ids the answer rests on |
73
+ | `ahub stop <peer>` | stop one headless peer (Kimi, headless Pi, `local`); the hub and the other peers keep running |
70
74
  | `ahub kill` | stop the daemon and everything it started |
71
75
 
72
76
  More: `ahub help`. How it is built and why: [`docs/specs/2026-09-19-agent-hub-design.md`](specs/2026-09-19-agent-hub-design.md). What it protects and what it does not: [`docs/security.md`](security.md).
@@ -74,11 +78,12 @@ More: `ahub help`. How it is built and why: [`docs/specs/2026-09-19-agent-hub-de
74
78
  ## Browser dashboard
75
79
 
76
80
  After `ahub up`, run `ahub ui` to open the local dashboard. It refreshes the
77
- conversation stream, peer queues, task board, budget windows and approvals every
78
- second. No build step, web server command or browser extension is needed.
81
+ conversation stream, peer queues, task board, budget windows, approvals and
82
+ benchmark runs every second. No build step, web server command or browser
83
+ extension is needed.
79
84
 
80
85
  Use the page to pause/resume a peer, send a console message, propose/assign tasks,
81
- and allow/deny agent permission requests. Local-worker requests show no tool
86
+ and allow/deny agent permission requests. Local-worker and Pi requests show no tool
82
87
  contents: inspect them with `ahub tail`, then use `ahub permit <id> <option>` to
83
88
  allow. They can be denied on the page. PII task details remain available only via
84
89
  `ahub task show <id>` in the terminal.
@@ -88,10 +93,10 @@ within 60 seconds. A session expires after one hour; run `ahub ui` again. Restar
88
93
  the daemon invalidates all links and sessions. The UI listener is started only on
89
94
  request, binds loopback and stops with the daemon. Remote access is not supported.
90
95
 
91
- The stream retains the latest 200 events since the dashboard listener was started;
92
- it does not load historical logs. An unopened dashboard has no listener or event
93
- buffer. A paused budget window cannot be overridden on the page; the CLI's
94
- `ahub budget resume <peer>` remains the explicit override.
96
+ The stream retains the latest 200 events since the daemon started; it does not
97
+ load historical logs. An unopened dashboard has no listener. A paused budget
98
+ window cannot be overridden on the page; the CLI's `ahub budget resume <peer>`
99
+ remains the explicit override.
95
100
 
96
101
 
97
102
  ## Concurrent project development (0.4.0)
@@ -0,0 +1,92 @@
1
+ # Research records
2
+
3
+ Opt-in records of how each task went, kept across projects for management and research (#247). They are built only
4
+ from a project's `events.jsonl`, so they hold ids, states, counts, tokens and times, never a title, detail, plan, note,
5
+ path, message body or check output.
6
+
7
+ ## Turning them on
8
+
9
+ In a project's `.agenthub/config.json`:
10
+
11
+ ```json
12
+ { "research": { "enabled": true } }
13
+ ```
14
+
15
+ Off by default; anything but `true` is off. While it is off the hub writes nothing and creates no file. The setting is
16
+ read when the hub starts.
17
+
18
+ ## Where they go
19
+
20
+ `~/.agenthub/research/<project key>.jsonl` (the directory 0700, the file 0600), where the key is the first 16 hex
21
+ characters of the SHA-256 of the project's registry id. Every project on the machine that opts in writes to the same
22
+ directory, so `ahub research --all` can compare them. `ahub reset --all` does not touch it.
23
+
24
+ ## When a record is written
25
+
26
+ When a task is approved (a review approves it, or its owner reports a task without a reviewer done), the hub waits
27
+ until no open turn is attributed to the task (the turn that approved it ends after the approval, and its usage arrives
28
+ later still), checking every 10 s for at most 30 minutes, then appends one task record built from that project's
29
+ events. A hub that stops first writes it once its peers have stopped. Approval is final on the board, so each task has
30
+ one record, and the first record written for a task is final. A failing write is one `hub.log` line per hub run and
31
+ never changes the task flow.
32
+
33
+ `ahub research backfill` builds the same records from a project's existing `events.jsonl` (marked
34
+ `writer.source: "backfill"`), reading every event attributed to a task, and appends those the store does not hold yet.
35
+ It is for tasks approved before research was on: a record it writes is final, so running it while a live record still
36
+ waits for its turns freezes that record without them.
37
+ A task is identified by project, id and proposal time (`createdAt`), because `ahub reset --all` starts the ids again
38
+ while the store keeps the older records.
39
+
40
+ ## Task record (`agent-hub.research/v1`, `kind: "task"`)
41
+
42
+ | Field | Meaning |
43
+ | --- | --- |
44
+ | `project` | the project key above |
45
+ | `task` | the board's task id |
46
+ | `class`, `pii` | the task's class; whether it was a PII task (its record holds counts only, like any other) |
47
+ | `outcome` | `approved` |
48
+ | `createdAt`, `startedAt`, `approvedAt` | proposal, first `in_progress`, approval (ISO) |
49
+ | `wallMs` | from `startedAt` to `approvedAt`, null when the start is not in the events |
50
+ | `activeMs`, `turns`, `filesChanged` | sums over the turns attributed to the task (`turn_end`), the ones ending after the approval included |
51
+ | `owners`, `reviewer` | the owners in the order they held the task (an owner who gets it back appears again; reports by owner count the task once per owner); the last reviewer |
52
+ | `reviewRounds`, `changesRequested`, `dones` | times it went to review, was sent back, was reported done |
53
+ | `checkPassed`, `checkFailed` | check results; a check that fails produces no done, so dones without a check are `dones - checkPassed` |
54
+ | `reassignments`, `reassignedBy` | owner changes after the first owner, and the same by the board's move reason (`declined`, `idle`, `offline`, `budget`, `rejections`, `manual`, ...; `none` when the board recorded no reason) |
55
+ | `firstPass` | approved on its first review with no failed check and no changes requested |
56
+ | `stuck`, `overlaps`, `conflicts` | escalation verdicts, overlap warnings and file conflicts about the task |
57
+ | `tests` | route outcomes that ran tests: `pass`, `fail` counts |
58
+ | `tokens` | attributed tokens: `total`, `byPeer`, and `byAttribution` (`delivery`, `single_open`, the #200 rules); unattributed tokens are in no record |
59
+ | `usage` | provider usage per peer: `input`, `output`, `cacheRead`, `total` (null when never reported) |
60
+ | `usageByModel` | provider-reported total tokens per served (else requested) model |
61
+ | `models` | model routes and served models of the attributed requests |
62
+ | `writer` | the release and `live` or `backfill` |
63
+
64
+ ## Label record (`kind: "label"`)
65
+
66
+ `ahub task label <id> ok|regressed|reverted|incomplete|wrong|abandoned [--created <time>]` appends
67
+ `{ project, task, createdAt, label, at }`: a person's later verdict (a revert next week, a regression found later, work
68
+ given up). It labels the current board's task with that id (its proposal in `events.jsonl` pins `createdAt`), so a
69
+ later task with the same id after a reset does not inherit it. A task from before `ahub reset --all` is not on the board
70
+ any more: it is the store's only record with that id, or, when there are several or the id is in use again, the one
71
+ `--created` names (the `createdAt` that `ahub research export` shows; a record without one takes no label). A task with no record yet (not approved, its record still waiting for
72
+ the turns that approved it, or approved while research was off) is refused; labels mark approved tasks only. Only a
73
+ person can run it (agent shells are refused), and only while research is on. The latest label wins.
74
+
75
+ ## Measures
76
+
77
+ `ahub research [--since 30d] [--all] [--json]` reports, overall and by class, owner, model and project:
78
+
79
+ - success rate: approved tasks whose latest label is none or `ok`, over approved tasks;
80
+ - first-pass rate: `firstPass` over approved tasks;
81
+ - rework: changes requested per approved task;
82
+ - check-failure rate: failed checks over checks run (`checkFailed / (checkFailed + checkPassed)`);
83
+ - tokens and wall time per approved task, median and p90 (linear interpolation between ranks).
84
+
85
+ They are computed each time from the records; nothing derived is stored. `ahub research export [--format jsonl|csv]
86
+ [--since] [--all]` writes the records for outside analysis; the CSV columns are, in order: project, task, class, pii,
87
+ outcome, createdAt, startedAt, approvedAt, wallMs, activeMs, owners, reviewer, reviewRounds, changesRequested, checkPassed,
88
+ checkFailed, dones, reassignments, firstPass, stuck, overlaps, conflicts, testsPass, testsFail, tokens, turns,
89
+ filesChanged, models, label, writerVersion, writerSource. The CSV is a flat subset; JSONL carries every field.
90
+ Another project's measures: `ahub --project <dir> research`.
91
+
92
+ Agents may read the measures and export (ids and counts only); `backfill` and `task label` are a person's.
package/docs/security.md CHANGED
@@ -6,18 +6,18 @@ agent-hub connects agents that can each run commands. This page says what the hu
6
6
 
7
7
  - **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input: a channel tag with `meta.source` for Claude started with `ahub claude`; otherwise a fixed header line (`frame()`, `[agent-hub message from ...]`) with an instruction that says it is untrusted, the plugin's server instructions for a tools-only Claude reading `hub_inbox` and a standing instruction for the others. A message body cannot forge the hub's own headers: such lines are quoted (`sanitize`). Replies inherit a hop count capped at 3, so agents cannot ping-pong forever; neither a digest nor a steer can reset it.
8
8
  - **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. External clients cannot claim the console user's id or a hub-managed peer's id.
9
- - **Permission prompts stay on.** `ahub claude` and `ahub codex` add nothing that weakens the agents' own prompts. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. `--unattended` turns prompts off, says so loudly, and is never the default.
10
- - **A committed config cannot choose launch commands, credential files, data endpoints or a wider sandbox.** The machine-local fields (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`) apply only from a config file git confirms nobody committed: `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust.
11
- - **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts, and the task id (with a PII flag) that each usage and token record is attributed to. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. It stays on the machine; `ahub export` only prints it.
9
+ - **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits; Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default.
10
+ - **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`) apply only from a config file git confirms nobody committed: `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust.
11
+ - **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts, and the task id (with a PII flag) that each usage and token record is attributed to. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. It stays on the machine; `ahub export` only prints it. Two opt-in stores copy numbers from it into your home directory, shared by every project on the machine (directories mode 700, files 600): research records (issue #247, `research.enabled`) in `~/.agenthub/research/` hold per approved task the ids, states, counts, tokens, times and PII flag the events hold, a person's label from a closed list, and a hash in place of the project id; benchmark runs (issue #251) in `~/.agenthub/bench/` hold suite name and hash, arm label, the attached peers' ids, models and permission modes, task ids, outcomes, exit codes, counts, tokens and times, never suite text or a command's output.
12
12
  - **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
13
13
  - **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
14
14
  - **The session record holds identities only.** `.agenthub/state/sessions.json` (issue #37, mode 600) keeps each attached peer's recovery metadata: launch options, session and thread ids, Pi's session file path. No message or task text; loss notices name deliveries by id, sender and public task title.
15
15
  - **Context checkpoints have a separate text record.** Native occupancy readings carry numeric metadata only. An optional context checkpoint persists its summary in `.agenthub/state/context-checkpoint-<peer>.json` with mode 0600 and attempts shared-memory storage when enabled. Completion requires the outstanding request id, current peer/native session and transport generation; new connection claims invalidate old requests. Private turns, every open PII task associated with the peer as owner or reviewer (including pending review), and PII-pattern summaries are refused before persistence or cloud memory. Its text is never broadcast, and context pressure does not pause, reassign or replace the session.
16
- - **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state and the state archives `ahub reset --all` leaves) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval, and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git/hooks` or `.git/config` writes; no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses (behind NAT64 with the well-known prefix, by the IPv4 address the answer carries; a network-specific prefix is not recognised), so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, until 0.13.0). One channel stays open: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. The allow-default profile of 0.9 and earlier was removed in 0.12.0 (issue #83); a `local.sandbox` setting is ignored with a note. The shared temp dirs (the user's and `/private/tmp`) are closed. Without the sandbox there is no `bash` tool.
16
+ - **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state and the state archives `ahub reset --all` leaves) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval unless the operator's permission mode grants them (writes and edits inside the grant filter in `ask-when-needed`, all four in `never-ask`; see below), and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git` name at any depth, closing the creation, rename, symlink and gitfile routes to a planted repository (others stay open; issue #281), no `config` or `hooks` under one (in an external git dir, only its own children and the hooks directory's contents, so refs named `config` or `hooks` still work), no `commondir` at any depth below one, with every such name matched folded against the code points HFS+ ignores, and no `.agenthub` anywhere in the project; the cost is that `git init`, `git clone` and `git worktree add` do not run inside the sandbox (add, commit, stash, checkout and gc still do), and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` in a project with a nested repository, or `rm -rf` of a directory that holds a `.git` (refusing the unlink is what stops the rename-out route); no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses (behind NAT64 with the well-known prefix, by the IPv4 address the answer carries; a network-specific prefix is not recognised), so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, until 0.13.0). One channel stays open: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. The allow-default profile of 0.9 and earlier was removed in 0.12.0 (issue #83); a `local.sandbox` setting is ignored with a note. The shared temp dirs (the user's and `/private/tmp`) are closed. Without the sandbox there is no `bash` tool.
17
17
  - **Capabilities are enforced, not suggested.** `capabilities` (issue #39) is checked by the daemon where task operations and messages arrive, so a peer cannot get round it by phrasing. A peer can never answer a permission request: the control link takes `permit` from the console role only, and a message that quotes a permit command is just text. Both bind the hub's own tool paths: a vendor agent with its own shell in the project (Codex, Claude, Kimi) can read `.agenthub/state/control-token` and connect as the console, which only the local worker's and Pi's sandbox prevents.
18
18
  - **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it is sent to claude-mem, whose observer is a cloud model.
19
- - **A model screen can add to the patterns (issue #198).** With `signals.pii_screen = "local"` in `routing.toml` (default `off`; it applies while `constraints.pii = "local_only"`), a newly proposed task is read by a model before the hub does anything else with it: until the verdict it is on no board, in no envelope, before no triage model, and the hub makes no brief or note call for it. The proposer's own channel is not covered: when the local worker proposes it in an ordinary turn, claude-mem capture of that worker's tool calls (below) sends the arguments whatever the verdict. The screen runs on the model on this machine (MLX or Ollama, loopback only) or, without one, on the gateway only while `onCampus()` holds, and the gateway call is refused once more right before transport if the gateway is behind Access; task text never goes off campus to be screened. When the on-device generation slot is taken (Pi generating, another screen), the screen goes to the campus gateway instead of waiting, and on campus a device that fails or is still loading after 60% of the deadline hands over to the gateway too; off campus it waits for the slot and the device within the deadline. With the legacy MLX provider the screen may start an `mlx_lm.server`, as Pi's relay does, and it stays running until `ahub models stop`. Its answer is `pii` or `clear` plus a category from a closed list (name, student id, phone, address, grade, health, other). Anything else, a timeout (8 s, one call and one generation slot per item), a missing or off-campus model, or text longer than 6000 UTF-8 bytes (about 2000 Hangul syllables) is `unknown`, and an unknown task is a PII task: this is the hub's one model call that fails closed, so with the screener down every new task waits for `local`. An unknown verdict has the costs of any PII task, even when a second look clears it later: opening it lifts every silent turn-free cohort for good and turns facts off while it is open, and off campus a proposal without a class is refused (triage is not asked for a PII task there), with an error that says so. Off campus with the on-device slot taken, a task stays `unknown` until it is screened again: while it is still proposed and no peer ever owned it, the hub's release timer (every 60 s, one task at a time, the least tried first, at most 10 times per task in a hub run; never text too long to read) asks again, and `clear` lifts its `pii` signal and routes it through the class peers (a named owner is not remembered; a reserved one is offered first, as always). A task `local` took keeps its PII path, also once it is declined back to nobody. A pattern match is PII without a call. The task's history records the source of its `pii` signal (`regex`, `screen` with the category, or `unknown` with the reason), an unknown verdict is a console line naming the task id only, and `events.jsonl` gets a `pii_screen` record with the item kind, label, source, category, reason and latency, never the text. A routing explanation of a draft does not call the screen and says so.
20
- - **Free text is screened too.** On an ordinary task, a done summary (with its check output: a match withholds the whole note), a review note, an unmet item or a budget handoff that matches a PII pattern (issue #69) is not saved to claude-mem, and every peer, `local` included, gets a stub naming `ahub task show <id>`; the board keeps the text, `hub.log` notes the withholding by task id only, and `ahub ask` shows such a note only when its model is reached on campus. `hub_remember` refuses a match; what a vendor agent writes to its own memory is not screened. With the model screen on, the same items (and `hub_remember` notes) go through it as well, one call each and only after the call's own checks (owner, state, verdict, a running check, memory enabled) pass: a `pii` or `unknown` verdict withholds the item exactly like a pattern match, and the board marks the history entry (`withheld`) so later views (task lists, completed-change notices, `ahub ask`, a reviewer replaced after a restart) keep withholding it. A budget hand-off is screened once per hand-off, whatever the number of tasks it moves; an escalation's reason is the hub's own text and is not screened. Pattern-only, without the model: the check output tail in a done note (only the summary is screened), a plan given with `hub_task_accept`, a decline reason, a task's refs (paths, branch), and context checkpoint summaries. Not screened at all (an existing gap, not new with the screen): the local worker's claude-mem capture records its tool calls in ordinary turns with their raw arguments, including the hub tools, so a title, detail, done summary, review note or `hub_remember` text it writes reaches claude-mem without a pattern or model check.
19
+ - **A model screen can add to the patterns (issue #198).** With `signals.pii_screen = "local"` in `routing.toml` (default `off`; it applies while `constraints.pii = "local_only"`), a newly proposed task is read by a model before the hub does anything else with it: until the verdict it is on no board, in no envelope, before no triage model, and the hub makes no brief or note call for it. The local worker's claude-mem capture leaves its hub tool calls out (issue #230), so a proposal it makes in an ordinary turn cannot reach claude-mem ahead of the verdict that way. The screen runs on the model on this machine (MLX or Ollama, loopback only) or, without one, on the gateway only while `onCampus()` holds, and the gateway call is refused once more right before transport if the gateway is behind Access; task text never goes off campus to be screened. When the on-device generation slot is taken (Pi generating, another screen), the screen goes to the campus gateway instead of waiting, and on campus a device that fails or is still loading after 60% of the deadline hands over to the gateway too; off campus it waits for the slot and the device within the deadline. With the legacy MLX provider the screen may start an `mlx_lm.server`, as Pi's relay does, and it stays running until `ahub models stop`. Its answer is `pii` or `clear` plus a category from a closed list (name, student id, phone, address, grade, health, other). Anything else, a timeout (8 s, one call and one generation slot per item), a missing or off-campus model, or text longer than 6000 UTF-8 bytes (about 2000 Hangul syllables) is `unknown`, and an unknown task is a PII task: this is the hub's one model call that fails closed, so with the screener down every new task waits for `local`. An unknown verdict has the costs of any PII task, even when a second look clears it later: opening it lifts every silent turn-free cohort for good and turns facts off while it is open, and off campus a proposal without a class is refused (triage is not asked for a PII task there), with an error that says so. Off campus with the on-device slot taken, a task stays `unknown` until it is screened again: while it is still proposed and no peer ever owned it, the hub's release timer (every 60 s, one task at a time, the least tried first, at most 10 times per task in a hub run; never text too long to read) asks again, and `clear` lifts its `pii` signal and routes it through the class peers (a named owner is not remembered; a reserved one is offered first, as always). A task `local` took keeps its PII path, also once it is declined back to nobody. A pattern match is PII without a call. The task's history records the source of its `pii` signal (`regex`, `screen` with the category, or `unknown` with the reason), an unknown verdict is a console line naming the task id only, and `events.jsonl` gets a `pii_screen` record with the item kind, label, source, category, reason and latency, never the text. A routing explanation of a draft does not call the screen and says so.
20
+ - **Free text is screened too.** On an ordinary task, a done summary (with its check output: a match withholds the whole note), a review note, an unmet item or a budget handoff that matches a PII pattern (issue #69) is not saved to claude-mem, and every peer, `local` included, gets a stub naming `ahub task show <id>`; the board keeps the text, `hub.log` notes the withholding by task id only, and `ahub ask` shows such a note only when its model is reached on campus. `hub_remember` refuses a match; what a vendor agent writes to its own memory is not screened. With the model screen on, the same items (and `hub_remember` notes) go through it as well, one call each and only after the call's own checks (owner, state, verdict, a running check, memory enabled) pass: a `pii` or `unknown` verdict withholds the item exactly like a pattern match, and the board marks the history entry (`withheld`) so later views (task lists, completed-change notices, `ahub ask`, a reviewer replaced after a restart) keep withholding it. A budget hand-off is screened once per hand-off, whatever the number of tasks it moves; an escalation's reason is the hub's own text and is not screened. Pattern-only, without the model: the check output tail in a done note (only the summary is screened), a plan given with `hub_task_accept`, a decline reason, a task's refs (paths, branch), and context checkpoint summaries. Not screened at all (an existing gap, not new with the screen): the local worker's claude-mem capture records its file, shell and git tool calls in ordinary turns with their raw arguments and output, and the closing answer of a turn that used tools, without a pattern or model check. Its hub tool calls (`hub_send`, the task tools, the conductor tools) are not captured (issue #230), so a title, detail, done summary, review note or `hub_remember` text it writes reaches claude-mem only through the screened paths above.
21
21
  - **Secrets stay where they are read.** The gateway key and Cloudflare Access values are read inside the gateway client and go only into request headers: never into logs, errors, envelopes, tool output, memory, or the generated Switchyard config (the key travels by environment variable name). Subscription logins of Claude, Codex and Kimi are never proxied or pooled.
22
22
  - **The hub's own model calls are fenced.** Digest condensation, task triage and the PII screen read agent-written text as data; their output is capped text framed as untrusted, or a value checked against a closed list. It is never used as a route, a peer id, a tool call or an instruction.
23
23
 
@@ -29,6 +29,41 @@ agent-hub connects agents that can each run commands. This page says what the hu
29
29
  - **Network-level proof for PII.** The hub proves at its own boundaries (tests search every output) that PII text does not leave; packet-level verification of your gateway path is an operations check.
30
30
  - **Other operating systems' sandboxes.** Only macOS seatbelt is implemented.
31
31
 
32
+ ## Permission modes and the hostile-agent boundary
33
+
34
+ Permission modes control vendor prompts; the CLI identity gate is an operating
35
+ policy, not a security boundary against a hostile agent. Kimi or another agent
36
+ whose mode permits unasked shells can clear shell markers, invoke the CLI with
37
+ `--yes`, or read the control token and connect as the console to raise any
38
+ peer's mode. A role check on the ordinary hub tool or conductor path does not
39
+ contain an unrestricted shell running as the same user.
40
+
41
+ Pi and local retain the hub's sandbox, path guard and denylist in every mode.
42
+ Their ask-when-needed write/edit grants also exclude canonical native agent
43
+ configuration segments in any case, using the same list as Claude's hook. A changed canonical target after approval
44
+ is refused before write/edit effects.
45
+ Kimi executes its own tools outside that sandbox; Claude and Codex retain their
46
+ vendor boundaries. The never-ask confirmation names this difference. No mode
47
+ turns off the sandbox or grants a session-wide allow-always option.
48
+
49
+ Config defaults apply automatically only up to ask-when-needed. A never-ask
50
+ request from an applied untracked config is held as ask until the person
51
+ confirms its source file in the console at hub start. Git-tracked mode defaults
52
+ are filtered by config trust and never offered for confirmation. This confirmation reduces accidental startup
53
+ relaxation; an agent with unrestricted shell/token access can still impersonate
54
+ the console. Merged and generated Claude settings live in 0600 state files and
55
+ are passed by path, keeping caller settings out of process argv.
56
+
57
+ Claude ask-when-needed grants only resolving project file targets outside
58
+ .agenthub, .git, .claude, .codex, .qwen, .kimi, .pi and .mcp.json. Symlink escapes, protected aliases, traversal and
59
+ unresolved wildcard targets receive no decision. Names are compared folded (letter case, compatibility forms and
60
+ the code points HFS+ ignores; case-insensitive APFS folds case but ignored none in the probe; so a look-alike such
61
+ as a long s or a name with a zero-width joiner is the same name), the path is judged inside the project, and
62
+ a file with a second hard link receives no decision,
63
+ for Claude and for the Pi/local write and edit grants alike. The local worker's and Pi's write guard refuses `.git`
64
+ and `.agenthub` by the same folded comparison in every mode, so a new `.GIT/config` cannot be planted for git to run. Native rules then apply;
65
+ this is a grant filter, not a filesystem sandbox.
66
+
32
67
  ## Agent CLI identity and conductor authority
33
68
 
34
69
  Inside an agent session, `ahub` identifies the caller from `AGENTHUB_PEER_ID`,
@@ -36,8 +71,10 @@ or the native Claude/Codex shell marker when the hub marker is absent. It connec
36
71
  as that peer in tools mode. Messages and task changes retain that actor;
37
72
  `say` defaults to status priority and important messages still require the peer's
38
73
  capability. Conflicting or malformed markers fail closed. There is no `--as-user`.
39
- Human-only operations are refused before connecting, including permission answers,
40
- queue resolution, budget overrides, lifecycle and recovery operations, and `ask`.
74
+ Human-only operations are refused before connecting, including permission answers
75
+ and permission modes, queue resolution, budget overrides, benchmark runs, research
76
+ backfill and task labels, stopping a peer (`ahub stop`), lifecycle and recovery
77
+ operations, and `ask`.
41
78
  The operator uses a plain terminal or `ahub console`. A Claude `!` command that
42
79
  inherits the agent markers follows the same rule. The console strips control
43
80
  sequences from agent and daemon text before it quotes forged headers, and its
@@ -57,7 +94,8 @@ move other than the hub's own was not made from the console (to another peer
57
94
  only with `assign` when its capabilities are listed).
58
95
  A conductor may inspect public state, assign or escalate work, start supported
59
96
  headless peers and place its own delivery holds. It cannot answer approvals,
60
- resolve durable deliveries, override budget pauses or release a human hold.
97
+ resolve durable deliveries, override budget pauses, release a human hold or stop
98
+ a peer: `peer_stop` is taken from the console role only, and no hub tool sends it.
61
99
  Pending approval summaries exclude titles; PII tasks remain public stubs.
62
100
  Conduct events contain ids only. Supervision feeds use structured reasons and
63
101
  never carry check output or approval bodies.
@@ -70,10 +108,10 @@ Please report vulnerabilities privately through GitHub's "Report a vulnerability
70
108
 
71
109
  - `ahub ui` uses the authenticated console control connection to start an ephemeral HTTP listener on `127.0.0.1`. No UI listener exists before that command. The control and Codex proxy ports continue to refuse every `Origin` header.
72
110
  - The daemon issues a cryptographically random, single-use bootstrap ticket valid for 60 seconds. The CLI opens the dashboard with that ticket in the URL fragment, never a query string or the control token. The static page removes the fragment immediately and exchanges it with a same-origin POST. Tickets are consumed once; expired tickets cannot create sessions.
73
- - The exchange creates a separate random session, valid for one hour without renewal, in an `HttpOnly; SameSite=Strict; Path=/` cookie named for this listener's port. Sessions and tickets live only in memory and die with the daemon. HTTP is loopback-only; the cookie is not a substitute for the origin checks.
111
+ - The exchange creates a separate random session, valid for one hour without renewal, in an `HttpOnly; SameSite=Strict; Path=/` cookie named for this listener's port. Sessions and tickets live only in memory and die with the daemon. The page itself keeps one more cookie, `agent-hub-theme` (issue #246): script-set, `SameSite=Strict`, kept for a year for the whole `127.0.0.1` host, and read back only as one of `system`, `light` or `dark`. HTTP is loopback-only; the cookie is not a substitute for the origin checks.
74
112
  - Every request must have the exact listener Host. Every data or action request, including the ticket exchange and snapshot polling, must be POST with the exact listener Origin and JSON content type. Foreign and missing origins, missing or expired sessions, oversized bodies and unknown actions are rejected. There is no CORS support. The only unauthenticated GET is the fixed, data-free HTML shell. No files or paths are served dynamically.
75
113
  - Responses are non-cacheable, cannot be framed, suppress referrers, and use a content security policy restricting scripts/styles to the shipped inline content and connections to this origin. Browser text is rendered with textContent, never interpreted as HTML.
76
- - The browser receives a bounded redacted event stream, peer states and queue counts, public task views, budget windows and pending approvals. Private envelope bodies and PII task text never cross this endpoint. Local-worker approval titles can contain PII, so their details remain terminal-only; the dashboard identifies the request and directs the operator to `ahub tail` before allowing it. Budget checkpoint summaries are also omitted.
114
+ - The browser receives a bounded redacted event stream, peer states and queue counts, public task views, budget windows, pending approvals and benchmark run summaries (issue #251): the newest runs in `~/.agenthub/bench/`, whichever project on this machine ran them, by suite name, arm label, state, the task id in progress, counts, tokens and times, never suite text. Private envelope bodies and PII task text never cross this endpoint. Local-worker and Pi approval titles can contain PII, so their details remain terminal-only; the dashboard identifies the request and directs the operator to `ahub tail` before allowing it. Budget checkpoint summaries are also omitted.
77
115
  - The closed action list is permission response, peer pause/resume, console message, task proposal and task assignment. These use the existing daemon/task paths and budget pause rules. There is no generic control proxy, task-detail read, shell, file access, configuration edit, budget override, peer launch or daemon shutdown API.
78
116
  - The listener stops with the daemon. Expired sessions must be reopened with `ahub ui`; the page does not silently obtain new credentials.
79
117
 
package/docs/smoke.md CHANGED
@@ -2,6 +2,62 @@
2
2
 
3
3
  `scripts/check.sh` covers everything against fakes. The legs below need real accounts and an interactive terminal, so they are run by hand and recorded here.
4
4
 
5
+ ## Codex resumed approval policy (#270)
6
+
7
+ 2026-10-10: installed version read back as `codex-cli 0.162.1`. The live
8
+ stickiness leg is unverified: a hand-run `codex --remote` resume and a human-only
9
+ `ahub permission codex ask` require a person's interactive terminal. The approved
10
+ fallback is implemented: keep restoration owed per thread and send the captured
11
+ native policy once on that thread's next accepted turn, whether or not the native
12
+ override is sticky across resume. Fake app-server coverage verifies both overlays,
13
+ other-thread visits, either resume/ask order, rejected restoration and restoration
14
+ only once. The proxy protects the one restoring turn; a repeated policy on the
15
+ next TUI turn is indistinguishable from a person's explicit native choice.
16
+
17
+ Manual leg in a disposable project:
18
+
19
+ 1. Start `ahub codex`, record its thread ID and native approval policy, then run
20
+ `ahub permission codex never-ask --yes` from a human shell and complete a benign turn.
21
+ 2. Close the TUI without stopping its hub proxy. Run `ahub permission codex ask`.
22
+ 3. Hand-run `codex --remote <the existing proxy URL>` and resume that same thread.
23
+ Record what the TUI sends as `approvalPolicy` on its first turn after a resume,
24
+ including any echo of the resume response. Observe the policy before and after
25
+ that benign turn; record whether the override persisted across resume and
26
+ whether the original policy was restored.
27
+ 4. Send the TUI's second turn after that resume; record its outgoing policy and
28
+ the effective native policy. A repeat of the reported override is passed through
29
+ after the one restoring turn, just like an explicit choice.
30
+ 5. Repeat with `ask-when-needed` and with resume before switching to ask. Record
31
+ the Codex version, thread ID, policies and outcomes. This checklist is not
32
+ evidence that any live leg has passed.
33
+
34
+ ## Interactive upgrade (#272)
35
+
36
+ Not run live yet. Covered by tests with a scripted terminal and, for `restart`,
37
+ by a real detached operation driven through the plan screen
38
+ (`test/upgrade-cli.test.ts`, with standard input and output marked as a terminal).
39
+ The live leg, at a real terminal with Orca-launched peers:
40
+
41
+ 1. With a hub on the previous release, run
42
+ `bunx --package @staix/agent-hub@<version> ahub upgrade`. Expect the plan
43
+ screen: the project, each peer's fate, `in progress:` when a peer is busy, no
44
+ blocker.
45
+ 2. Press `a`. Expect the steps in order, `waiting for:` while a peer is busy, and
46
+ `upgrade to <version> completed`; `ahub --version` and `ahub status` report it.
47
+ 3. On a scratch project, start `ahub restart` while a peer is busy, press `a`,
48
+ then Enter during the wait and `c` on the operation screen. Expect `operation
49
+ <id> is cancelled; the recovery lock is free`, the reset question, and the hub
50
+ still running.
51
+ 4. With an installed CLI one release behind, run `ahub upgrade`, confirm the
52
+ hand-over, press `a`, then Ctrl+C during the wait. Expect the `left:` line, a
53
+ clean shell prompt, and `ahub recovery` showing the runner still working.
54
+
55
+ 5. On the plan screen press `k`, then `t`, and confirm. Expect each TUI agent's
56
+ terminal closed and the next plan showing them offline; with a hub of 0.12.22
57
+ or newer, `h` stops the headless agents the same way.
58
+
59
+ Record the versions, the screens' text and what differed.
60
+
5
61
  ## ahub reset on a scratch project (#214)
6
62
 
7
63
  Not yet run. A person runs it in a terminal, on a scratch project only, never
@@ -1343,3 +1399,85 @@ Not run yet. Record the terminal, its theme and the result per step.
1343
1399
  - Press `?`, Enter on an approval and Tab back to the stream; check that the key
1344
1400
  table, the labeled detail and the stream's rule above the footer read in both
1345
1401
  themes.
1402
+
1403
+ ## `ahub bench` live run (#251)
1404
+
1405
+ Not run yet (unverified). In a scratch git repository kept for benchmarks (`"bench": { "enabled": true }`, `.agenthub/`
1406
+ untracked), with at least two real peers attached:
1407
+
1408
+ - Run a two-task suite with `--repeat 2` under one arm; check each attempt's outcome against its `verify` command and
1409
+ that the tree is reset between attempts.
1410
+ - Let one task run past its `timeout_s`; check the run stops, names the open task and reads `stopped`.
1411
+ - Press Ctrl-C during a `verify`; check the attempt reads `interrupted` and nothing it started keeps running.
1412
+ - Run a second arm, then `ahub bench compare <arm> <arm>`; check the dashboard's Benchmarks section shows both.
1413
+
1414
+ ## Permission modes (#240, #242), 2026-10-10
1415
+
1416
+ Installed native versions: Kimi 2.1.1, Codex 0.162.0, Claude Code 2.1.296,
1417
+ Pi 1.0.1. Checks use disposable scratch fixtures outside this repository, one
1418
+ short file-edit plus harmless `printf` shell prompt per non-ask mode. Native
1419
+ runs go through `dot admit`; no production permission command or sandbox
1420
+ change is used. Two admission attempts were deferred before any prompt by
1421
+ unrelated rsync jobs; a later attempt admitted the sequential fixture run.
1422
+
1423
+ | Agent | Mode | File edit | Harmless shell | Permission evidence |
1424
+ | --- | --- | --- | --- | --- |
1425
+ | Kimi | ask-when-needed (`yolo`) | `file-edit-ok` read back | `shell-ok` read back | 0 ACP permission requests |
1426
+ | Kimi | never-ask (`auto`) | `file-edit-ok` read back | `shell-ok` read back | 0 ACP permission requests |
1427
+ | Codex | ask-when-needed | Unverified, no file produced | Unverified, no file produced | Proxy turn emitted userMessage but no tool/completion within 180 s; 0 approval requests |
1428
+ | Codex | never-ask | Not run, unverified | Not run, unverified | Omitted after the first bounded failure, preserving prompt quota |
1429
+ | Claude | ask-when-needed | edit.txt changed to after | shell.txt read back shell-ok | Historical earlier policy: bound native hook identity; 0 denials. Current scoped policy unverified |
1430
+ | Claude | never-ask | edit.txt changed to after | shell.txt read back shell-ok | Historical earlier policy: bound native hook identity; 0 denials. Current scoped policy unverified |
1431
+ | Pi | ask-when-needed | Unverified, no tool ran | Unverified, no tool ran | Fixture had no model gateway configured; RPC prompt accepted but no model/tool execution |
1432
+ | Pi | never-ask | Unverified, no tool ran | Unverified, no tool ran | Same missing fixture gateway; no successful model/tool execution |
1433
+
1434
+ The Kimi trace records set_mode replies before each prompt. session/new lists
1435
+ `default`, `plan`, `auto`, `yolo` and currentModeId default; loading the same
1436
+ session after never-ask reports default, with no extra prompt. Fake regressions
1437
+ also cover a resumed session retaining auto/yolo, which is reset to default
1438
+ before prompts under the project's ask policy.
1439
+
1440
+ Kimi's CLI calls `yolo` Ask When Needed and says routine edits and commands run
1441
+ automatically. The observed harmless shells agree. These two benign prompts do
1442
+ not prove risky-action policy or distinguish the two modes on risky actions;
1443
+ the contradictory ACP mode descriptions do not justify reversing the CLI/UI
1444
+ mapping. Codex similarly uses its native on-request policy rather than the
1445
+ Claude/Pi read/edit allowlist.
1446
+
1447
+ Claude Code completed both isolated headless native runs with exit 0. Its
1448
+ ask-when-needed hook allows file tools and returns no override for Bash;
1449
+ the harmless shell was permitted by the existing native policy. A zero
1450
+ permission_denials array is not evidence that an interactive dialog appeared.
1451
+ The hook's matching instance/launch metadata confirms its transport ran.
1452
+
1453
+ No additional native prompts were sent for the subsequent launcher metadata
1454
+ and permission-only idle fixes; their behavior is pinned by focused daemon/launcher
1455
+ regressions. Pi's missing gateway was a fixture configuration limitation, not
1456
+ evidence of an account outage or a permission-mode failure. Codex's timeout
1457
+ cause remains undiagnosed. These legs remain unverified rather than being
1458
+ replaced with fake evidence. Fixture process groups were stopped afterward.
1459
+
1460
+ The historical Claude benign checks above used the earlier hook policy. The current scoped-path policy, expanded native configuration exclusions, startup reconciliation, and private settings retirement are covered by fixtures but remain native unverified. No additional account prompts were used for the second review corrections.
1461
+
1462
+ Unverified natively (Codex 0.162.0): whether a thread that got a `never` or `on-request` override keeps it when the
1463
+ same thread is resumed through the hub's proxy by a hand-run `codex --remote` after `ahub permission codex ask` was
1464
+ given while no TUI was attached. The status says ask; the current proxy keeps restoration owed per thread and
1465
+ sends the captured native policy once on the resumed thread's next accepted turn, including when the TUI echoes
1466
+ the resume response's sticky override. This protects only the restoring turn; a repeat on the second TUI turn
1467
+ cannot be distinguished from an explicit choice and passes through. Both turns' TUI traffic remain natively unverified. `ahub codex` always starts
1468
+ a new proxy, which is not affected. See the current #270 manual checklist above.
1469
+
1470
+ ## Whole-board task progress and dashboard themes (#246)
1471
+
1472
+ Live console and dashboard light/dark inspection: **unverified**. No native
1473
+ sessions or model prompts were started for this change. The review pass admitted
1474
+ a real dependency install and focused tests, recorded separately from native
1475
+ visual/theme qualification.
1476
+
1477
+ When admitted, inspect an empty board and a mixed board (approved, waiting,
1478
+ working, review and changes requested) at 80x24, 120x40 and 200x60. Confirm the
1479
+ stream count is in the footer and the scroll region starts at row 1 and the Tasks summary/row meters agree
1480
+ with the dashboard. In the dashboard select Light, Dark and System, reload each
1481
+ choice, change OS theme on System, and check text, bars, stage labels and controls.
1482
+ Repeat with cookies blocked and across two dashboard ports; verify unchanged
1483
+ progress snapshots do not repeatedly announce the live region. Record the exact head and verdict here.