@staix/agent-hub 0.17.0 → 0.19.0

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 (109) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +4 -4
  3. package/docs/agent-notes/README.md +11 -0
  4. package/docs/agent-notes/adapters.md +10 -6
  5. package/docs/agent-notes/benchmarks.md +1 -0
  6. package/docs/agent-notes/daemon.md +6 -2
  7. package/docs/agent-notes/local-worker.md +3 -1
  8. package/docs/agent-notes/tasks.md +11 -2
  9. package/docs/agent-notes/tests.md +7 -3
  10. package/docs/dogfood.md +22 -2
  11. package/docs/memory.md +1 -1
  12. package/docs/operations.md +162 -35
  13. package/docs/profiles.md +36 -0
  14. package/docs/quickstart.md +15 -5
  15. package/docs/sdlc.md +29 -3
  16. package/docs/security.md +44 -13
  17. package/docs/smoke.md +3 -3
  18. package/docs/specs/2026-09-19-agent-hub-design.md +31 -10
  19. package/docs/verification/2026-10-25-0.18.0.md +76 -0
  20. package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
  21. package/docs/verification/2026-10-25-remote-docker-transport.md +56 -0
  22. package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
  23. package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
  24. package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
  25. package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
  26. package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
  27. package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
  28. package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
  29. package/docs/verification/2026-10-27-0.19.0.md +86 -0
  30. package/docs/verification/claude-permission-transport-native.py +187 -0
  31. package/docs/verification/claude-shell-wrapper-host.ts +20 -0
  32. package/docs/verification/claude-shell-wrapper-native.py +145 -0
  33. package/docs/verification/pi-140-model-free.ts +57 -0
  34. package/docs/verification/pi-140-native.mjs +21 -0
  35. package/docs/verification/profiles/YYYY-MM-DD-general.md +46 -0
  36. package/docs/verification/profiles/YYYY-MM-DD-research.md +61 -0
  37. package/docs/verification/profiles/YYYY-MM-DD-writing.md +46 -0
  38. package/docs/verified.json +24 -18
  39. package/package.json +2 -2
  40. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  41. package/plugins/agent-hub/server.js +364 -98
  42. package/src/adapters/agy/entry.ts +1 -1
  43. package/src/adapters/claude-channel.ts +7 -3
  44. package/src/adapters/claude-implementer.ts +5 -3
  45. package/src/adapters/compat.ts +32 -18
  46. package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
  47. package/src/adapters/definitions/builtin-metadata.ts +3 -3
  48. package/src/adapters/definitions/native-factories.ts +7 -2
  49. package/src/adapters/definitions/reviewer-variants.ts +2 -2
  50. package/src/adapters/local-worker.ts +5 -3
  51. package/src/adapters/pi.ts +39 -3
  52. package/src/adapters/qwen-dual.ts +25 -0
  53. package/src/adapters/registry-types.ts +6 -2
  54. package/src/adapters/registry.ts +1 -0
  55. package/src/cli/agents.ts +69 -7
  56. package/src/cli/claude-shell-wrapper.ts +54 -0
  57. package/src/cli/console-state.ts +9 -2
  58. package/src/cli/help.ts +29 -28
  59. package/src/cli/identity.ts +87 -6
  60. package/src/cli/main.ts +198 -71
  61. package/src/cli/output.ts +11 -2
  62. package/src/cli/sdlc-output.ts +242 -0
  63. package/src/cli/task-output.ts +212 -0
  64. package/src/cli/upgrade-output.ts +57 -0
  65. package/src/hub/agent-drift.ts +61 -0
  66. package/src/hub/board.ts +31 -2
  67. package/src/hub/check-runner.ts +385 -25
  68. package/src/hub/claude-shell-wrapper.ts +349 -0
  69. package/src/hub/conductor.ts +5 -0
  70. package/src/hub/config-apply.ts +16 -7
  71. package/src/hub/config-edit-host.ts +1 -1
  72. package/src/hub/config-edit.ts +2 -2
  73. package/src/hub/config-tier.ts +3 -3
  74. package/src/hub/daemon.ts +530 -87
  75. package/src/hub/dogfood-profile.ts +248 -0
  76. package/src/hub/dogfood-read.ts +77 -14
  77. package/src/hub/dogfood.ts +5 -1
  78. package/src/hub/events.ts +1 -1
  79. package/src/hub/host-executable.ts +21 -1
  80. package/src/hub/hub-tools.ts +2 -2
  81. package/src/hub/instances.ts +18 -2
  82. package/src/hub/lifecycle.ts +1 -1
  83. package/src/hub/operational-authority.ts +67 -0
  84. package/src/hub/operational-view.ts +137 -0
  85. package/src/hub/orphan-process.ts +69 -0
  86. package/src/hub/permission-mode.ts +1 -1
  87. package/src/hub/read-only-shell-proof.ts +81 -0
  88. package/src/hub/read-only-shell.ts +162 -0
  89. package/src/hub/review-scope.ts +13 -0
  90. package/src/hub/reviewer-pool-config.ts +3 -2
  91. package/src/hub/reviewer-pool.ts +3 -0
  92. package/src/hub/routing.ts +23 -6
  93. package/src/hub/sdlc-merge.ts +299 -0
  94. package/src/hub/sdlc-metrics.ts +6 -4
  95. package/src/hub/sdlc-mirror.ts +129 -0
  96. package/src/hub/sdlc.ts +58 -12
  97. package/src/hub/settings.ts +11 -5
  98. package/src/hub/snapshots.ts +60 -0
  99. package/src/hub/task-repository.ts +31 -5
  100. package/src/hub/task-shell-command.ts +85 -0
  101. package/src/hub/task-shell-default.ts +78 -0
  102. package/src/hub/task-shell-proof.ts +171 -0
  103. package/src/hub/task-shell-sandbox.ts +61 -0
  104. package/src/hub/tasks.ts +378 -27
  105. package/src/local/deny.ts +91 -11
  106. package/src/local/sandbox.ts +67 -28
  107. package/src/local/shell-environment.ts +10 -0
  108. package/src/local/tools.ts +17 -4
  109. package/src/ui/index.html +3 -1
@@ -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.17.0
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.19.0
21
21
  ahub setup
22
22
  ```
23
23
 
@@ -30,6 +30,16 @@ and upgrade or crash recovery, see the [operations guide](operations.md). To
30
30
  move running projects to a newer release later, run `ahub upgrade` in a
31
31
  terminal: it reviews the plan with you, applies it and follows it.
32
32
 
33
+ ## Upgrading to 0.19.0
34
+
35
+ Pi is now a gated CLI (#843): `ahub agents` lists it, 1.4.0 is the verified version, and any other version is refused at every launch path with the exact `ahub agents trust pi <version>` command. If your Pi is not 1.4.0, trust that version (person only) or install 1.4.0 before Pi work resumes. At hub start and once a day the hub reads each gated CLI's installed version without network, tells the planner once per id and version that is not verified, and `ahub status` shows an `agent CLI drift` line until it is (#843). Newly verified: agy 1.4.7, Jcode 0.99.1, headless Qwen 0.26.1, the Kimi fresh reviewer 2.3.2 and the headless Claude implementer 2.1.310. In `ask-when-needed`, a headless Claude implementer now runs proven read-only shell commands and a closed set of task commands (`scripts/check.sh --changed`, `bun run build`, `bun test <test files>`, `git add|commit`, `git push` and `gh pr view|checks|create` on the task's bound branch) through a host-owned wrapper with single-use tickets; check, test and build run in the local worker's sandbox, any other command and any call in an enabled always-ask class still ask, and an older plugin keeps asking (#845; see [additional headless peer instances](operations.md#additional-headless-peer-instances)). It may also name its model with `"model"` on its declaration in untracked `config.local.json`, applied at the next hub daemon restart (#844). The fresh Codex and Kimi reviewers now declare review scope `code`: they review code only, a `changes_requested` with no open structured finding is recorded as evidence-only and does not count toward escalation, `ahub status` shows `review scope` on each reviewer row, and `"review_scope"` on the instance in `config.local.json` overrides it (#852). Planners get one `Planner work ready:` notice when a write frees WIP capacity or makes backlog work ready; nothing is pulled for them (#785). The remote check runner installs dependencies once per lockfile key into a read-only volume and refuses a commit whose `package.json` declares an install lifecycle script (#793). A conductor operation whose hub connection or authorization handshake is unavailable now says so instead of reporting a role refusal (#812). The hub sandbox also protects a linked worktree's submodule git dirs and `config.worktree`, and refuses writes under a modules root outside a discovered git dir (#518, #865). A Qwen TUI background-task notice no longer ends a hub turn as uncertain (#517). `@staix/agent-mem` is pinned at 0.10.0. Upgrade running hubs with `ahub upgrade` and restart Claude sessions so they load the 0.19.0 plugin. Coming from 0.17.0 or older, the notes below apply too.
36
+
37
+ ## Upgrading to 0.18.0
38
+
39
+ The `ahub sdlc` read commands (`status`, `explain`, `queue`, `release`, `metrics`, `costs`, `authorize-release`) now print readable output by default, no longer a JSON document (#814). A script that parses their output must pass `--json`: it prints exactly what the command printed before. `sdlc export` stays JSON. `restart` and `upgrade` off a terminal (a pipe or `--dry-run`), `recovery status` and its results, `models setup|start`, `queue resolve` and `budget execution configure|disable` follow (#814): pass `--json` for the old document. `task show|cancel`, `backlog groom|return|plan|pin|split|undo|pull-ready` and `release list|show|new|plan|activate|freeze|ship|drop` follow (#814): pass `--json` for the hub tool's text. More commands follow in later releases.
40
+
41
+ Agents can now run the read-only `ahub` forms themselves (`doctor`, `projects`, `models status`, `recovery status`, `queue list`, `roster`, `settings` reads, `sdlc status|explain|queue|release`, `release show` and the `--dry-run` previews of `upgrade` and `restart`), and the configured conductor can run `stop`, `queue show|resolve|discard`, `task cancel`, `roster add|remove`, `logs`, `export`, `doctor --orphans --kill` and `settings set` on twelve live keys (#812). Nothing changes for a person's terminal. `sdlc.toml` gains `auto_merge` (`off` by default): set `dry-run` to see what the guarded merge and the one-per-verdict GitHub COMMENT review would do before setting `on` (#786). `review.same_family = "never"` keeps a same-family reviewer from being assigned (#820). In `ask-when-needed`, Pi and the local worker run proven bounded read-only shell commands without asking (#816). The isolated remote Docker check runner is opt-in through `checks.runner = "remote"` in the person's untracked `config.local.json` (#793). `@staix/agent-mem` is pinned at 0.8.0.
42
+
33
43
  ## Upgrading to 0.17.0
34
44
 
35
45
  `ahub qwen --mode tui` runs Qwen Code's own TUI in your terminal, driven by the hub through its dual-output files (#286). It is opt-in: the hub never opens it by itself, `roster.qwen` stays off by default, and headless Qwen works as before. It needs Qwen Code 0.26.1 (a new `qwen-dual` compat entry; the headless entry is not widened), refuses an extra instance (`--as`) and refuses while a headless Qwen runs (`ahub stop qwen` first). It has two limits: a hub deny ends the turn without an answer (the delivery completes, so later messages still reach Qwen), and a stalled turn is never cancelled; the hub reports it and you press Esc in the Qwen terminal. `ahub stop qwen` does not end the TUI, and `ahub upgrade` and recovery do not restore it: end it in its terminal before you upgrade, then run `ahub qwen --mode tui` again, which resumes its session. The `codex-review` reviewer now approves exactly `hub_review` and `hub_task_show`, per tool: on Codex 0.164.0 `exec` refused every MCP tool call, so its reviews ended without a verdict (#783). A reviewer turn that still ends without a verdict leaves one screened `hub.log` line (exit code and the hub tools it called) and is offered on, or to you when you named that reviewer (#453). A stale review request that the hub refuses at admission no longer marks the reviewer unavailable: a request whose binding changed is skipped, and one whose policy changed is rerouted as no verdict (#781). An unavailable reviewer slot no longer stops the agy TUI from reading its own task (#794). An `ahub recovery dispose --stop-and-archive` audit entry now keeps the error that blocked the upgrade as `cause` (#776). A task counts toward a recurring finding only where that finding was recorded open or accepted, so a fix-only record no longer files a "Review policy amendment" card (#328), and a missing generation sync remote no longer logs a failure every sync interval (#401). Upgrade running hubs with `ahub upgrade` and restart Claude sessions so they load the 0.17.0 plugin. Coming from 0.15.0 or older, the notes below apply too.
@@ -94,9 +104,9 @@ Upgrading from 0.12.37 or earlier, the 0.12.38 notes below also apply.
94
104
 
95
105
  The 0.12.38 plugin reports a reply stopped by the hop cap as not delivered instead of as sent (#546); the restart above loads it.
96
106
 
97
- Antigravity (`agy`) can join as an optional pull-only implementer (#485). It is off roster by default: install the Antigravity CLI 1.4.x (1.4.2 verified; other 1.4.x patches launch with a warning; on 1.4.3 the hub tools appear from the second TUI turn), run `ahub roster add agy`, then run `ahub agy` in a terminal. Its native permissions are unmanaged and it has no hub sandbox. See [Antigravity operations](operations.md#optional-antigravity-agy-peer).
107
+ Antigravity (`agy`) can join as an optional pull-only implementer (#485). It is off roster by default: install the Antigravity CLI 1.4.x (1.4.2 and 1.4.7 verified; other 1.4.x patches launch with a warning; the hub tools appear from the second TUI turn), run `ahub roster add agy`, then run `ahub agy` in a terminal. Its native permissions are unmanaged and it has no hub sandbox. See [Antigravity operations](operations.md#optional-antigravity-agy-peer).
98
108
 
99
- Reviews can rotate across fresh Claude, Codex and Kimi reviewer sessions (#519, #533). Declare headless reviewer-only instances in `.agenthub/config.local.json`; Codex and Kimi join only with `review_mode: "fresh"`. Codex also needs a compat-admitted Codex (receipt-verified 0.146.0 and 0.163.0; select it with `codex_bin` when needed), and Kimi needs a compat-admitted Kimi Code on PATH (receipt-verified 2.2.0, 2.3.0 and 2.3.1), signed in with its own OAuth login. Run `ahub agents` for the current version policy; every fresh reviewer still needs a passing runtime preparation. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) receipts are model-free and do not establish real login or model readiness. A headless reviewer instance without `review_mode` keeps its native behavior and stays out of the pool. agy is not a reviewer: naming it in `review.pool` shows an unavailable skip in `ahub route explain`, never a review. Once a train is active or frozen, or `review.pool` is set or fresh reviewer instances are declared, `ahub upgrade` refuses a target release without that support, so an older release cannot take the project over. See [fresh reviewer pool](operations.md#fresh-reviewer-pool-519).
109
+ Reviews can rotate across fresh Claude, Codex and Kimi reviewer sessions (#519, #533). Declare headless reviewer-only instances in `.agenthub/config.local.json`; Codex and Kimi join only with `review_mode: "fresh"`. Codex also needs a compat-admitted Codex (receipt-verified 0.146.0 and 0.163.0; select it with `codex_bin` when needed), and Kimi needs a compat-admitted Kimi Code on PATH (receipt-verified 2.2.0, 2.3.0, 2.3.1 and 2.3.2), signed in with its own OAuth login. Run `ahub agents` for the current version policy; every fresh reviewer still needs a passing runtime preparation. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md), [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) and [2.3.2](verification/2026-10-26-kimi-232-reviewer-compat.md) receipts are model-free and do not establish real login or model readiness. A headless reviewer instance without `review_mode` keeps its native behavior and stays out of the pool. agy is not a reviewer: naming it in `review.pool` shows an unavailable skip in `ahub route explain`, never a review. Once a train is active or frozen, or `review.pool` is set or fresh reviewer instances are declared, `ahub upgrade` refuses a target release without that support, so an older release cannot take the project over. See [fresh reviewer pool](operations.md#fresh-reviewer-pool-519).
100
110
 
101
111
  A guarded config edit that the hub keeps at its running value now shows as `config retained: <keys>; run ahub settings reload` in status, the console and the dashboard's Settings, and starting a peer whose launch keys are retained says it uses the running value (#542). See [settings](operations.md#settings-from-the-dashboard-and-the-terminal).
102
112
 
@@ -106,7 +116,7 @@ The restarted Claude planner gets the release-train tools (`hub_release_new`, `h
106
116
 
107
117
  Jcode must be 0.98.0 or later, below 1.0.0; 0.94.0 is no longer accepted, so `ahub doctor` fails its Jcode row for an older install and the peer does not start (#529). Every owned jcode process runs with `JCODE_NO_TELEMETRY=1`.
108
118
 
109
- Since 0.12.37 the hub opens a split memory home through its shards (#534); the current exact `@staix/agent-mem` pin is 0.7.0. Before anyone runs `amem migrate --shards` on a memory home, every hub using that home must run 0.12.37 or later: an older hub does not see the split and keeps writing the old layout. `ahub memory profile-check` is split-aware and compares each hub with its own project's shard (#536). See [memory profiles and rollout](memory.md#selected-memory-profile).
119
+ Since 0.12.37 the hub opens a split memory home through its shards (#534); the current exact `@staix/agent-mem` pin is 0.10.0. Before anyone runs `amem migrate --shards` on a memory home, every hub using that home must run 0.12.37 or later: an older hub does not see the split and keeps writing the old layout. `ahub memory profile-check` is split-aware and compares each hub with its own project's shard (#536). See [memory profiles and rollout](memory.md#selected-memory-profile).
110
120
 
111
121
  From 0.12.30 or earlier, existing Jcode project MCP entries need one refresh before conductor or dashboard starts. Once per project, the person runs:
112
122
 
@@ -173,7 +183,7 @@ More: `ahub help`. How it is built and why: [`docs/specs/2026-09-19-agent-hub-de
173
183
 
174
184
  ## Shared memory
175
185
 
176
- The hub exact-pins the first-party `@staix/agent-mem` 0.7.0 core, including deterministic capture; no claude-mem worker is needed. Existing shared-v1 memory remains the default; upgrading does not migrate it. Managed agents use screened `hub_memory_search`, `hub_memory_show` and `hub_memory_graph` tools, or propose a correction with `hub_memory_correct`; only a person accepts candidates through the supported `amem review` path. Since agent-mem 0.7.0, `amem review` also accepts candidates on a selected generation.
186
+ The hub exact-pins the first-party `@staix/agent-mem` 0.10.0 core, including deterministic capture; no claude-mem worker is needed. Existing shared-v1 memory remains the default; upgrading does not migrate it. Managed agents use screened `hub_memory_search`, `hub_memory_show` and `hub_memory_graph` tools, or propose a correction with `hub_memory_correct`; only a person accepts candidates through the supported `amem review` path. Since agent-mem 0.7.0, `amem review` also accepts candidates on a selected generation.
177
187
 
178
188
  Before any real generation migration, upgrade every hub sharing that memory home to 0.12.30 or later and check its authenticated binding with `ahub memory profile-check` in each custom registry. Ready bindings prove compatibility, not an atomic live cutover: quiesce consumers and close their handles before migrating, then reopen through the supported session-preserving lifecycle. Invalid or changed profiles disable native memory without falling back to legacy writes; the bus and board continue. See [memory operations](operations.md#hub-memory) for the remaining rollout gates.
179
189
 
package/docs/sdlc.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Authority and evidence
4
4
 
5
- The issue remains the specification, Tasks remains the sole task mutation boundary, and GitHub remains authoritative for PR heads, required checks, merge commits and releases. The hub stores artifact identities and bounded structured readbacks, never issue/PR bodies. Off is the default. Advise exposes blockers without changing task transitions. Enforce blocks only the review/approval transitions listed below; it never merges, releases, edits GitHub or gives a peer additional capabilities.
5
+ The issue remains the specification, Tasks remains the sole task mutation boundary, and GitHub remains authoritative for PR heads, required checks, merge commits and releases. The hub stores artifact identities and bounded structured readbacks, never issue/PR bodies. Off is the default. Advise exposes blockers without changing task transitions. Enforce blocks only the review/approval transitions listed below; it never releases, edits GitHub or gives a peer additional capabilities. The one GitHub write is the guarded merge of an approved task (#786, below), off by default.
6
6
 
7
7
  A reviewed head is a full lowercase 40-character SHA observed through a successful readback, not an implementer-supplied statement. A check is evidence only for that same head. Unknown/missing required checks, unavailable GitHub, pending reviews or unresolved threads remain blockers in enforce and visible unknowns in advise. A newer head invalidates the derived review and approval readiness. Historical task approval is retained as history, not presented as approval of the new commit. Revalidate after asynchronous readbacks immediately before any board write.
8
8
 
@@ -18,7 +18,7 @@ The built-in lifecycle follows the [AI-Native SDLC Playbook](https://claude.com/
18
18
  | build | Build | owner | PR identity/current head and local check on that head | entering review, when enabled | agent-executable |
19
19
  | test | Test | verifier/reviewer | required checks and local gate on the reviewed head | approval, when enabled; bug test integrity below, evals pending S5 | agent-executable |
20
20
  | review | Deploy | assigned reviewer | current head-bound verdict, no pending change request/open thread; AC checklist under Compliance | approval, when enabled; open Important findings block in enforce | agent-executable |
21
- | merge | Deploy | person | current-head readiness, ordered touching PRs | shown; no mutation | agent-executable where the project's AGENTS.md delegates it to the approving reviewer (this repository does, #784); the hub still shows the person as actor |
21
+ | merge | Deploy | person | current-head readiness, ordered touching PRs | shown; the guarded merge below runs only when `auto_merge` is `on` (default off) | agent-executable where the project's AGENTS.md delegates it to the approving reviewer (this repository does, #784); the hub still shows the person as actor |
22
22
  | release | Deploy | person | merged commit, version/tag, docs freshness/live receipt checklist | person authorization and optional tag hook below | agent-evidence / person-verdict (consent) |
23
23
  | maintain | Maintain | person/planner | follow-up intent and operational evidence | shown; metrics and control bands from S7 (`ahub sdlc metrics`) | agent-executable |
24
24
 
@@ -54,7 +54,7 @@ A reviewer records the head it actually reviewed. If GitHub no longer reports th
54
54
 
55
55
  ## Readbacks and presentation
56
56
 
57
- Use structured gh JSON/GraphQL read-only calls with validated repository and numeric artifact identities. No shell interpolation, PR/issue body fetching, token logging or unbounded comment storage. Failure yields unknown. Required checks come from repository rules or explicitly configured names; an empty/unknown set is not silently green. Review-thread status is counted, not copied. Cancellation and task-owner generation fencing prevent a late read from approving reassigned work.
57
+ Use structured gh JSON/GraphQL read-only calls (the guarded merge below is the one exception) with validated repository and numeric artifact identities. No shell interpolation, PR/issue body fetching, token logging or unbounded comment storage. Failure yields unknown. Required checks come from repository rules or explicitly configured names; an empty/unknown set is not silently green. Review-thread status is counted, not copied. Cancellation and task-owner generation fencing prevent a late read from approving reassigned work.
58
58
 
59
59
  ahub sdlc status and explain expose mode, stage, gate, observed/reviewed/local-check heads, next actor and next action. Console/dashboard derive the same result. Merge ordering compares exact PR path sets, reports intersections and never guesses an automatic landing order where dependencies are unknown. Release readiness lists merged work since the tag, stale/nonancestor documentation stamps and open native/live receipts; each unknown remains visible.
60
60
 
@@ -164,6 +164,30 @@ The package-owned scripts/check-docs.mjs accepts --json with the same normal/--r
164
164
 
165
165
  The release reader additionally requests --committed: manifest, README and package metadata come from the captured HEAD, and dirty documentation/evidence files block freshness. A working-tree restamp cannot certify a commit that still contains stale stamps. The ordinary checker continues to support working-tree review before those stamps are committed.
166
166
 
167
+ # Guarded merge (#786)
168
+
169
+ `.agenthub/sdlc.toml` takes `auto_merge = "off" | "dry-run" | "on"` (default `off`) and optional `[repos."owner/name"]` tables with `required_checks` and `auto_merge`. A table's mode overrides the top-level one for that repository; its `required_checks` come first, then the top-level list, so a repository without branch protection is gated by what this file names (an empty set refuses: `required checks unknown`). Unknown keys are refused with a field path. The file is read once at start.
170
+
171
+ Two triggers, neither blocking: an approving `hub_review` at the PR's current head (the check runs after the verdict is saved and reports through a notice; a failing or throwing check never changes the verdict), and `ahub sdlc merge <task> [--dry-run]` (the person or the one configured conductor; a daemon control op that always replies). Right before acting the hub re-reads the PR (`readSdlcPr`) and the queue (`readMergeQueue`) and refuses, naming every failing gate: task not approved, head changed since the approved head, a required check failing, pending or missing, a pending change request, pending or unknown review requests, an unresolved or unknown thread, an open Important finding, a draft, a PR not first among its touching PRs or a dependency that is not approved, a release PR (`release PR: person consent required`), a private task, or unreadable evidence. Unknown is never a pass. The decision judges every gate on three readings (the PR, the queue's own read, then the PR once more, last) and refuses what any of them fails, so a check, review request, thread or draft that changes between reads at the same head is caught; `--match-head-commit` pins only the head. The board-side inputs are read again, synchronously, in the same stretch as the `gh pr merge` call (nothing is awaited between the read and the call) and compared with what the decision judged: the task's state, repo, PR and reviewed head, its privacy, every dependency's state and the dependency edges, open Important findings on the head, the release stage, the merge mode and required checks in force, and the asker's authority and the hub's availability. Any difference refuses and runs nothing. The complete local queue inputs (all task links, dependency edges, receipts, plans, findings, privacy and SDLC configuration) must also match their pre-read fingerprint in a synchronous check after the fresh gates and argv validation, immediately before dispatch. The caller must still be the person, the hub approval trigger or the currently configured conductor at that final check. Any changed projection refuses, conservatively including an unrelated task projection change; a new invocation reads the queue again.
172
+
173
+ `on` runs exactly `gh pr merge <pr> --repo <repo> --squash --match-head-commit <head>` (a closed argv shape checked right before the call) and reads the PR back: only GitHub reporting `MERGED` is a merge, whatever the exit code. Never `--admin`, never a ref update used to perform the merge, and no `--auto` while the repository does not allow auto-merge (a person's repository setting). `dry-run` reports the same decision and the argv it would run and runs nothing; `off` reads nothing. Logs and notices name the task number and gates only. The verdict mirror is described in the next section.
174
+
175
+ ## Merge result and cleanup (#786 T2)
176
+
177
+ Post-run reads make at most three attempts within a shared six-second budget. Only a GitHub MERGED observation with its merge commit permits cleanup; the CLI exit is recorded separately, including a nonzero exit when GitHub confirms the merge. Non-merged, missing or unreadable evidence records the bounded result and leaves branches alone. Off and dry-run create no merge record or cleanup effects.
178
+
179
+ Tasks stores host-only `sdlc.merge` evidence bound to the repository, PR and head. Lifecycle tools cannot submit this field, and artifact changes invalidate it. A change during an awaited operation leaves the previous attempt in history without overwriting the replacement artifact. Durable cleanup and owner-notice claims precede those effects. Merged or uncertain attempts are not automatically repeated after restart; a positively observed not-merged result can be retried by an explicit command. A crash before the first post-run record remains an untracked attempt requiring inspection.
180
+
181
+ Remote cleanup reads the merged PR head, repository default, legacy protection and active rulesets. It skips missing, fork, default, protected, moved or unsupported refs. Its only ref write is a single GraphQL compare-and-swap deletion: `beforeOid` is the merged PR head and `afterOid` is zero. A moved head refuses deletion; unknown outcomes stay explicit. The hub sends the owner a public-view notice to remove only its task worktree and local branch when safe. The hub runs no local branch or worktree deletion.
182
+
183
+ ## Verdict mirror (#786)
184
+
185
+ Each `hub_review` verdict is mirrored as ONE `COMMENT` review on the head it was given for, so a person reading the PR sees the hub's verdict. One GitHub account authors and merges every PR and GitHub refuses `APPROVE` and `REQUEST_CHANGES` on your own PR, so a comment is the only review the hub can leave; it is a record, never a gate, and nothing reads it back as approval. The call is exactly `gh api repos/<repo>/pulls/<pr>/reviews -f event=COMMENT -f commit_id=<head> -f body=<text>`, a closed shape checked right before it runs; the hub makes no other review write. The first line of the body is `agent-hub verdict: approved|changes_requested (task #N, reviewer <peer>)`.
186
+
187
+ It follows the same `auto_merge` setting as the guarded merge, per repository: `off` posts and reads nothing, `dry-run` reports the call it would make in a notice, `on` posts. It runs after the verdict is saved, is never awaited by the verdict and never fails it (a failure is a notice naming the task number). A verdict at a head the PR is no longer at posts nothing, a closed PR gets nothing (the head and state are read once more after the review list, last, so a push or a close during that read is caught; GitHub offers no compare-and-set on a review, so a push landing between that read and the post is not, and the comment still names its commit), and unknown evidence (an unreadable PR or review list, or a full page of 100 reviews) posts nothing. At most one mirror exists per (task, head, verdict): the hub reads the PR's reviews for its own headline at that head before posting, so a retried verdict or a restart does not post twice. For an approval the comment is posted before the guarded merge starts reading.
188
+
189
+ The body after the headline is the note's public view: a private task, a note the review screen withheld, a note given in a private turn and a note matching a private pattern are mirrored as the headline alone. The note is bounded, control characters are stripped, and a line that looks like a verdict headline or an agent-hub message header is neutralised so a note cannot forge a second mirror.
190
+
167
191
  # Read-only merge queue contract (#257)
168
192
 
169
193
  Queue reads fetch PR numbers and bounded head/check/review/thread/file-count metadata, never remote titles or bodies. Up to16 open PRs and8 external dependency reads share a20second budget. Every linked local receipt must satisfy the existing approval floor on that current head; draft/unknown status, missing file coverage and unlinked/private work remain blocked. Same-target path intersections return counts without names. Task after edges provide the only dependency order; open predecessor PRs block current readiness, unresolved touching order asks the person, cycles/truncation remain incomplete. A board approval never substitutes for a merged-commit receipt. No queue operation changes dependencies, owns work, merges or writes GitHub.
@@ -251,6 +275,8 @@ The planner can provide scope with `hub_plan_review`; the person can run `ahub s
251
275
 
252
276
  A review request is premature if its task head does not match the PR head, configured required checks are not green, or a pending bot thread is unanswered. A resolved thread is settled; an unresolved one needs a user reply after its last bot comment. The hub reads identity, timestamps, authorship type and resolution only, never comment bodies. Without explicit required checks the native floor is both OS checks plus `seeded guards`. Advise reports the gap; enforce refuses before the requesting done/cohort writes. If only quiet task-history events arrive during an advise readback, the evidence is discarded and review continues with unknown readiness, without another external read. A reviewer change is allowed because the reviewer is re-derived before the write; it is not an authority or readiness receipt. Mode, owner, privacy, receipt, authority and other history drift still refuse; enforce remains unchanged. A private turn may finish its own PII task in advise with a generic unknown-readiness notice and no remote metadata read; the private-turn refusal for ordinary work remains. Delayed checks revalidate before completing; a refused completion sends a private-aware notice to call `hub_task_done` again only while the task is still in progress with the owner that started the check. Otherwise only the console notice is recorded. Background roster/quota role changes remain fact, but enforce holds a new review envelope without an admission readback; refresh it through a manual reviewer assignment after CI and replies are ready. The brief and verdict coverage belong to #520, separate from this admission.
253
277
 
278
+ **Evidence in the review request (#824).** A fresh reviewer cannot run tests or read `gh`, so the request carries the hub's own reading of the admission it already made, for every reviewer: `Hub-verified at <head>: required checks <names> passed at this exact head; no bot review thread is waiting for a reply` (plus `via identical-tree run <n>` when the hub reused one), or in advise mode `Hub-verified at <head>: not ready: <blockers>`. No admission (lifecycle off) and a PII task get no block, and no new `gh` call is made for it: it renders what `reviewRequestAdmission` read. `ReviewReadiness.checks` names the required checks that were judged. When the admission is ready and the hub ran no check of its own, the checklist's check line says CI passed at this exact head and that test execution or CI evidence is not an unmet item (name a specific missing test or behaviour instead); otherwise the old wording stays. The text comes only from the hub's reads and recorded `check passed` events, never from the builder's summary or a `hub_sdlc_record` claim, and a review-only reviewer still gets no builder summary (#223 R2).
279
+
254
280
  CI controllers are serialized per pull request (`check-pr-<number>`) and for main pushes and ordinary dispatches (`check-main`), for the entire workflow and without canceling an admitted controller (#609); macOS timing dispatches use their own macos-timing group. Within a PR `queue: single` lets a newer pending run replace the older pending one; main keeps `queue: max`, which preserves up to 100 waiting controllers ([GitHub concurrency documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency)). Different PRs run side by side: a repository-wide lock serialized every PR (17-18 min alone, 40-54 min with 4-5 PRs in flight on 2026-10-20). Two groups admitting the same tree at the same instant can run it twice; that duplicate costs one run and never a false green. Required job names and read-only token permissions are unchanged. Before PR or push gates, `scripts/ci-reuse.mjs` reuses successful full proof for the identical Git tree, including its workflow. On reuse the two OS matrix jobs still expand and report their exact required names, while the seeded aggregate reports its exact name. They run only a `Reuse verified identical-tree full gate` marker step naming the original proof; full setup and gates are skipped. Skipped jobs and successful reuse-reporting jobs never count as fresh full proof. A running identical-tree gate refuses duplicate admission; unknown active coverage refuses safely. Only a 404, a timeout on the individual read limit, or malformed successful proof is dropped: a failed successful-run list drops that proof phase, while a failed commit or jobs read drops only that candidate and continues the bounded search. Other failures (5xx, 403, 429, response size bounds, authentication and unclassified errors) remain unknown. Exhausting the overall deadline or request budget stays unknown even when a request reports a timeout. Without verified proof, a complete empty active scan requires a fresh gate. An unreadable, truncated or unidentifiable active inventory, or exhaustion of the shared request budget or deadline, stays unknown and refuses admission. The release reader (`--require`) still refuses without verified identical-tree full proof. Metadata reads are bounded to 100 recent successes/active runs and 240 calls within 60 seconds. Pending serialized controllers are not admitted gates. Run focused tests and `scripts/check.sh --changed` locally; CI carries the full-suite gate; a known flake warrants only one rerun of its failed job with recorded evidence.
255
281
 
256
282
  `hub_sdlc_costs` and the person's `ahub sdlc costs [owner/repo]` report linked merged PRs without changing the board. A review round is a board done-to-verdict handoff, not each reviewer in a multi-reviewer round. An observed handoff still counts when its declared scope was none. The report counts distinct check-workflow run ids, extra attempts (`run_attempt - 1`), full re-reviews, delta rounds and summed review intervals, with GitHub review counts alongside them. Unspecified scope and unavailable evidence stay unknown/incomplete. Only current public links are projected; a private counterpart withholds the whole PR. Up to 16 linked PR reads share 20 seconds; each PR covers at most 100 commits, reviews and recent check-workflow runs, and the pure aggregator bounds 500 tasks, 200 PRs and 10,000 history entries. Truncation is explicit. S10 exposes pure band-breach proposal data; S7 owns rolling baselines, band configuration, automatic scheduling and board-card creation.
package/docs/security.md CHANGED
@@ -6,14 +6,14 @@ 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`. The bearer header a Codex TUI sends to the proxy carries only the terminal launch id (#586): it binds terminal ownership and is not a credential. External clients cannot claim the console user's id or a hub-managed peer's id.
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. A call the always-ask policy classes (below) is never approved this way, whatever its title. 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. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. 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; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under 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, and a headless Claude implementer's proven shell commands through the host wrapper (#845, below); 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. A call the always-ask policy classes (below) is never approved this way, whatever its title. 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. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. 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; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under it.
10
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` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `terminal.open`, `instances`, `action_policy`) apply only from a config file git confirms nobody committed (`instances` and `action_policy` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are ignored even when untracked): `.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, the command that opens a terminal, 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. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
11
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. Budget pauses, resumes and quota resets carry the peer, window, times and the hub's own reason text, and a person's release authorization its commit, version and docs digest. The usage roll-up (#683) folds the usage, token and limit records into `hub.db` and joins issue, PR and release from the board when read; `ahub usage` and the console's Usage panel answer only the console and a configured planner, and a planner not during a private turn. Kimi's token records (#285) are counted from its own session wire log under its home: only the four token integers, the model label and the row time are copied, and the record id is an opaque hash, never a path, session id or text. The log 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. The Pi continuity bookmark that recovery keeps across stop and archive (`.agenthub/state/recovery-continuity.json`, mode 600, at most 16 KiB) holds identities only too: operation and project ids, the project root, and Pi's session id, session file, backend and model name; a bookmark that is not a private regular file, is larger than that or names another project or operation reads as invalid, which is kept distinct from no bookmark.
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, when memory is enabled, proposes it as a machine-local memory candidate that only a person can accept. 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 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 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, no `config` or `hooks` directly under one nor below the discovered modules root of the project's own `.git` (a submodule's git directory, nested submodules included; every term is anchored below that root, so a directory named `refs`, `config` or `hooks` above the project neither disables the rule nor trips it; a `/refs/` or `/logs/` segment below it keeps refs named `config` or `hooks` writable, and leaves a submodule whose own path contains such a segment uncovered; a submodule whose path has `hooks` as a whole component (`tools/hooks` included) fares worse: the rule matches its whole git directory, so every write there is refused; the discovered git directories, the modules root and every component in between are also refused as targets, closing the rename, replace and symlink routes to the same files), no `commondir` at any depth below one, with every such name matched folded against the code points HFS+ ignores (the fold covers the security-relevant names; on a volume that folds names, folded spellings of the components between `.git` and a submodule's git directory are not matched — the 1024-byte SBPL literal cannot hold a folded path — the same residual the external git dir rules carry since #270), and no `.agenthub` anywhere in the project (in an external git dir the `config`/`hooks` rules cover only its own children and the hooks directory's contents, so refs named `config` or `hooks` still work); a nested repository in bare layout stays open — the directory is the git dir, with no `.git` name to anchor a rule on, and the hub never runs git there, whatever the project root is: its calls are all `-C <project root>` or `--no-index` on temp files, and none descends into a nested directory's git layout, so a planted hook fires only if a person or another tool runs git in that directory themselves; the rules are probed on HFS+, which folds the 16 code points the patterns tolerate, and on a case-sensitive APFS image, which folds nothing (a folded spelling is an inert new file there and the real names stay refused); 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), `git submodule update --init` does not either (its git directory's `config` write is refused), 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.
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, no `config`, `config.worktree` or `hooks` directly under one, nor in any submodule git directory (every modules tree the project can reach is covered: the common dir's, each linked worktree's own under `<common>/worktrees/<wt>/modules/`, matched by pattern so a tree or worktree that does not exist yet is covered too, and each discovered submodule git directory's own `modules/`, nested submodules and a submodule's own linked worktrees included; under a modules tree every write that is not inside a discovered submodule git directory is refused, whatever its name, so a fake git directory cannot be planted for a later unsandboxed `git submodule update --init` to reuse; inside each discovered one its own `config`, `config.worktree` and `hooks` and its linked worktrees' `config.worktree` are refused, while refs, logs, objects, the index and `HEAD` stay writable, so commit, branch, tag, gc and rebase work; a linked worktree's git directory under `<common>/worktrees/` gets the same `config`, `config.worktree` and `hooks` refusal; every term is anchored at the discovered path, `hooks` by depth, so a directory named `config` or `hooks` above the project neither disables the rule nor trips it; the discovered git directories, the modules roots, the worktrees container, each worktree git directory and every component in between are also refused as targets, closing the rename, replace and symlink routes to the same files), no `commondir` at any depth below one, with every such name matched folded against the code points HFS+ ignores (the fold covers the security-relevant names; on a volume that folds names, folded spellings of the components between `.git` and a submodule's git directory are not matched — the 1024-byte SBPL literal cannot hold a folded path — the same residual the external git dir rules carry since #270), and no `.agenthub` anywhere in the project (in an external git dir the `config`/`config.worktree`/`hooks` rules cover only its own children and the hooks directory's contents, so refs named `config` or `hooks` still work); a nested repository in bare layout stays open — the directory is the git dir, with no `.git` name to anchor a rule on, and the hub never runs git there, whatever the project root is: its calls are all `-C <project root>` or `--no-index` on temp files, and none descends into a nested directory's git layout, so a planted hook fires only if a person or another tool runs git in that directory themselves; the rules are probed on HFS+, which folds the 16 code points the patterns tolerate, and on a case-sensitive APFS image, which folds nothing (a folded spelling is an inert new file there and the real names stay refused); 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), `git submodule update --init` does not either (its git directory's `config` write is refused), 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 enters hub-managed memory; local storage does not relax this privacy fence.
19
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 amem capture leaves its hub tool calls out (issue #230), so a proposal it makes in an ordinary turn cannot reach shared memory 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.
@@ -23,7 +23,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
23
23
 
24
24
  ## Native memory authority and profile selection
25
25
 
26
- The portable core is the exact-pinned first-party `@staix/agent-mem` 0.7.0 package; it does not replace the hub's caller identity, PII policy or task-write authority. Deny, hardened git, real paths and process identity are single-sourced in its Node-safe platform entry. Current project privacy is applied by the hub before promotion and again before model-facing reads. Standalone person reads are unscreened, and the standalone `--policy-free` acknowledgement grants no hub or peer authority.
26
+ The portable core is the exact-pinned first-party `@staix/agent-mem` 0.10.0 package; it does not replace the hub's caller identity, PII policy or task-write authority. Deny, hardened git, real paths and process identity are single-sourced in its Node-safe platform entry. Current project privacy is applied by the hub before promotion and again before model-facing reads. Standalone person reads are unscreened, and the standalone `--policy-free` acknowledgement grants no hub or peer authority.
27
27
 
28
28
  `hub_memory_search`, `hub_memory_show` and `hub_memory_graph` take the project from the daemon, require `memory_read`, refuse private turns and validate UTF-8 input before logs or storage work. Complete records and graph metadata are screened before counts and truncation. Search is at most 20 lines/4 KiB (default 8); show accepts at most 8 ids and returns at most 8 KiB; graph is at most 120 nodes/240 edges/6 KiB (default 60 nodes), with no edge lacking an endpoint. Unknown, ambiguous and withheld ids share one neutral refusal. Arguments and results are excluded from hub-managed capture; this does not govern a vendor's independent transcript or memory hooks.
29
29
 
@@ -235,9 +235,26 @@ move other than the hub's own was not made from the console (to another peer
235
235
  only with `assign` when its capabilities are listed).
236
236
  A conductor may inspect public state, assign or escalate work, start a peer in
237
237
  the start mode a person set (it cannot choose the mode) and place its own
238
- delivery holds. It cannot answer approvals,
239
- resolve durable deliveries, override budget pauses, release a human hold or stop
240
- a peer: `peer_stop` is taken from the console role only, and no hub tool sends it.
238
+ delivery holds. Through the `ahub` CLI under its own peer identity it may also
239
+ run the closed operational family of #812: `roster add|remove`, `stop <peer>`,
240
+ `queue show|resolve|discard`, `task cancel`, `doctor --orphans --kill`, `logs`,
241
+ `export` and `settings set` on twelve named live keys. Each is admitted by the
242
+ daemon after a recheck of the conductor role, and bounded; the effectful ones and
243
+ the history reads are audited as a closed label with no argument text (`queue show`
244
+ and a settings preview recheck the role without an audit record), a refusal of
245
+ that authority is audited as refused, and an ordinary operation failure keeps its
246
+ own cause rather than being reported as missing authority (#839): logs and export are public projections of the last
247
+ 100 lines and 1000 events, orphan cleanup signals only a positively identified
248
+ hub-owned process of the selected project, and a stop keeps the existing
249
+ ownership and recovery guards (`peer_stop` from a non-console connection is
250
+ admitted only through this conductor check; no hub tool sends it). It still cannot answer
251
+ approvals, override budget pauses, release a human hold, trust an agent version,
252
+ authorize a release or run recovery resume, abort and reattach: those stay with
253
+ the person. Other agents may run only the read-only forms of #812 (`doctor`,
254
+ `projects`, `models status`, `recovery status`, `queue list`, `roster`,
255
+ `settings` reads, `sdlc status|explain|queue|release`, `release show` and the
256
+ `--dry-run` upgrade and restart previews) through the public views, never the
257
+ console identity.
241
258
  Pending approval summaries exclude titles; PII tasks remain public stubs.
242
259
  Conduct events contain ids only. Supervision feeds use structured reasons and
243
260
  never carry check output or approval bodies.
@@ -265,7 +282,7 @@ Please report vulnerabilities privately through GitHub's "Report a vulnerability
265
282
  - A settings session is as strong as the console for these settings and no stronger elsewhere. A mode change is the console's own request with the same refusals; tool approvals stay deny-only for Pi and local; there is still no shell, file, task-detail or launch-command API. `ahub ui --settings` and `ahub settings` pass the same identity gate as `ahub permission`, which is an operating policy, not authentication. The hostile-agent boundary above applies unchanged: an agent with an unrestricted shell as the same user can impersonate the console, and so can open a settings session.
266
283
  - Every change writes a `settings` event (the key, the value before and after, the source `dashboard` or `terminal`) and a line in `hub.log` and open consoles; opening a settings session is logged too. The unified dashboard (`ahub ui --all`) forwards settings actions as an ordinary session and opens no settings session.
267
284
  - Configuration reload (issue #439). The daemon watches `config.json`, `config.local.json`, `routing.toml` and `routing.local.toml` and re-reads the complete candidate after a change, as the dashboard's Reload configuration and `ahub settings reload` do; an invalid candidate applies nothing. Each changed key is then authorized on its own against its file's actual git provenance. A privacy tightening (local-only PII, the model screen on, added PII patterns, narrower roles or capability lists, unless it moves the planner or conductor) and a safe registered setting apply from any file. Every other change, machine-local fields included, keeps its running value when its file is tracked or git cannot answer. From an untracked file such a guarded change is adopted only by `ahub settings reload` in an interactive terminal, after it lists each guarded key with its full before and after values and the person types `y` within 30 seconds; the confirmation is bound to the file contents and accepted state it was shown for. File watches and dashboard reloads, a settings session's included, never confirm, and no reload changes the roster: membership moves only through the roster setting. Keys whose consumers are bound at start (memory, the gateway and MLX, checks, the privacy and sandbox policy of existing sessions, the terminal provider and others) stay pending until a restart.
268
- - Structural configuration (issue #689). A second, wider list than the registry can be edited, in three tiers (`src/hub/config-tier.ts` is the one rule). Tier 2 (instance fields that choose no executable or home, the review, snapshots, approvals and `context.gate` keys, and `sdlc.toml` `mode` and `required_checks`) is editable from the console and from a settings session. Tier 3 is everything that chooses what the hub executes or where secrets go (the rest of `NEVER_EDITABLE`, and an instance's `jcode_home`, `provider`, `model` and `provider_profile`): console only, confirmed by typing the key's name, one key per write, never by `--yes`, and the dashboard shows it as text with the console command. A browser page must never be able to set what the hub runs, so the refusal is in the hub, not in the page: `config_edit_preview` and `config_edit_apply` from a dashboard session refuse a tier 3 path whatever else the request carries, and an ordinary session is refused before anything about the project is read. Reload authority admits a structural key from a settings session only while the daemon's own validated edit holds that key's scope (`structural` in `authorizeConfigLeaf`) and `structuralLeaf` says tier 2; a file edit made by hand still needs the terminal's confirmed reload. The candidate is read by the same loaders the daemon starts with before anything is written, the file revision is checked twice (planning and writing), the previous file is kept as one of the newest five backups, a symlinked, oversized or git-tracked file is never written, and unknown request fields and keys are refused. One write changes exactly one tier 3 key, counting the keys the plan adds from what the file holds (`plan.tier3Keys`), not only the ones the request names, and the audit record carries no hash of a tier 3 value. A whole instance that holds a tier 3 field is tier 3 to remove or replace (decided from what the file holds, not from the request), a preview or write reply masks every tier 3 value as `(set)`, and `.agenthub` itself must be a real directory. The `config_edit` event and the notice carry key paths and hashes, never a value, so a tier 3 key's home or model never reaches the event log, `hub.log` or the dashboard. Agent shells and hub tools cannot reach any of it: the CLI is `console` in `classifyPeerCommand` and the control requests answer the console role only.
285
+ - Structural configuration (issue #689). A second, wider list than the registry can be edited, in three tiers (`src/hub/config-tier.ts` is the one rule). Tier 2 (instance fields that choose no executable or home, the review, snapshots, approvals and `context.gate` keys, and `sdlc.toml` `mode` and `required_checks`) is editable from the console and from a settings session. Tier 3 is everything that chooses what the hub executes or where secrets go (the rest of `NEVER_EDITABLE`, and an instance's `jcode_home`, `provider`, `model` and `provider_profile`), plus a fresh reviewer's `review_scope`, which decides whether its approval covers tests and CI (#852): console only, confirmed by typing the key's name, one key per write, never by `--yes`, and the dashboard shows it as text with the console command. A browser page must never be able to set what the hub runs, so the refusal is in the hub, not in the page: `config_edit_preview` and `config_edit_apply` from a dashboard session refuse a tier 3 path whatever else the request carries, and an ordinary session is refused before anything about the project is read. Reload authority admits a structural key from a settings session only while the daemon's own validated edit holds that key's scope (`structural` in `authorizeConfigLeaf`) and `structuralLeaf` says tier 2; a file edit made by hand still needs the terminal's confirmed reload. The candidate is read by the same loaders the daemon starts with before anything is written, the file revision is checked twice (planning and writing), the previous file is kept as one of the newest five backups, a symlinked, oversized or git-tracked file is never written, and unknown request fields and keys are refused. One write changes exactly one tier 3 key, counting the keys the plan adds from what the file holds (`plan.tier3Keys`), not only the ones the request names, and the audit record carries no hash of a tier 3 value. A whole instance that holds a tier 3 field is tier 3 to remove or replace (decided from what the file holds, not from the request), a preview or write reply masks every tier 3 value as `(set)`, and `.agenthub` itself must be a real directory. The `config_edit` event and the notice carry key paths and hashes, never a value, so a tier 3 key's home or model never reaches the event log, `hub.log` or the dashboard. Agent shells and hub tools cannot reach any of it: the CLI is `console` in `classifyPeerCommand` and the control requests answer the console role only.
269
286
  - Start modes. A start the hub makes itself (the dashboard's Start control, `peer_start` from a console child, the conductor's `hub_peer_start`, `pi.auto_start`) takes the peer's start mode. Through settings only a settings session or the terminal can set it to headless; a config file can also set `peers.<id>.start_mode`, the shared one included (it is not a machine-local field: headless was the only form for Pi, Kimi and the local worker before this change, and for Jcode before 0.12.39, a hub-made headless Codex start is the app-server the hub already ran for it, and a start mode grants nothing a permission mode or a capability controls). A declared extra instance accepts only `headless` from any surface; a Claude channel instance accepts no start mode (one that implements or verifies needs its own worktree, and a planner or reviewer one refuses a worktree, #525-1), and a headless Claude instance is either a reviewer (roles exactly `["reviewer"]`, no worktree) or an implementer with its own registered worktree and permission mode `ask` or `ask-when-needed` (#525-4). For a TUI the hub opens a terminal running `<runtime> <its own entry point> --project <this project> <peer>` (with `--mode tui` for Pi, Kimi and Jcode): every word is the hub's own, the command line is those words each quoted for a POSIX shell, and no request field, tool argument or agent message is part of it. In a `terminal.open` template `{command}` is that quoted line and `{title}` and `{cwd}` are raw text for argv elements of their own; each placeholder is substituted once, so a project path that itself contains a placeholder, a quote or `$` stays one word. A provider's own error text goes to `hub.log`, not to the caller. The terminal comes from `terminal.open`, a machine-local command template that config trust refuses from a committed file and that no settings surface can write, or from Orca. The opened terminal's environment carries no agent marker, so `ahub` there has the console's identity. Without a provider the start is refused with the command to run by hand; it is never downgraded to headless. After a successful headless start of a watch-capable peer (Kimi, Qwen) the hub also opens `<runtime> <its own entry point> --project <this project> watch <peer>` through the same provider under the same fixed-words rule, at most once per peer per 30 s and never for an already attached peer or a declared instance; the pane has the console's identity, and a missing or failing provider leaves the start standing and reports the command (#286). A conductor can start a peer but cannot choose its mode.
270
287
 
271
288
  ## Multi-project manager (protocol 7)
@@ -331,18 +348,18 @@ live after a settings write, undo or reload (turning it off from an edited file
331
348
  be set through an agent, tools or conductor surface. Claude's daemon response
332
349
  withholds the scoped mode on these writes so an older hook grants nothing too.
333
350
 
334
- Agent CLI versions (#691). One policy gates the Codex and Kimi fresh reviewers, Jcode, agy and Qwen: a receipt-verified version passes, Jcode and agy also start an untested version inside their compatible line with a note, and any other version is refused unless the person trusts that exact version on this machine with `ahub agents trust <id> <version>`. The record is `agent-compat.json` in the hub home (`~/.agenthub`), written atomically with mode 600 and at most 64 entries; an unreadable, linked, oversized or invalid record trusts nothing. Agent shells and `--live-test` may read `ahub agents` but cannot trust or revoke. Trust lifts only the version pin: the fresh reviewers' runtime proofs still run before every child, and an untested version's native contract stays unverified.
351
+ Agent CLI versions (#691). One policy gates the Codex and Kimi fresh reviewers, the headless Claude implementer, Jcode, agy, Qwen (ACP and the dual-output TUI) and Pi (#843): a receipt-verified version passes, Jcode and agy also start an untested version inside their compatible line with a note, and any other version is refused unless the person trusts that exact version on this machine with `ahub agents trust <id> <version>`. The record is `agent-compat.json` in the hub home (`~/.agenthub`), written atomically with mode 600 and at most 64 entries; an unreadable, linked, oversized or invalid record trusts nothing. Agent shells and `--live-test` may read `ahub agents` but cannot trust or revoke. Trust lifts only the version pin: the fresh reviewers' runtime proofs still run before every child, and an untested version's native contract stays unverified.
335
352
 
336
- Qwen Code's optional production ACP peer requires the qwen-code native identity, a compat-admitted version (receipt-verified 0.25.0 and 0.26.0)
353
+ Qwen Code's optional production ACP peer requires the qwen-code native identity, a compat-admitted version (receipt-verified 0.25.0, 0.26.0 and 0.26.1)
337
354
  and advertised mode ids before setting a policy or starting work. Its own tools
338
355
  have no hub sandbox; auto-edit approves file edits, yolo every tool. Configured
339
356
  never-ask still requires the person's confirmation. qwen_cmd is machine-local and
340
357
  never editable through settings. Legacy benchmark adapters remain unmanaged by
341
358
  this production table. Startup diagnostics withhold native host/payload details.
342
359
 
343
- Jcode is optional, source/version-fenced and runs native tools without a hub sandbox or hub permission-mode mapping. .jcode joins the scoped-grant exclusion, including lexical/resolved aliases. Only a person writes its unique project MCP entry; foreign/override/symlink entries refuse. The hub owns a private daemon/socket and ACP group, never the global daemon, and copies no credentials. It accepts a plain jcode release from 0.98.0 up to, not including, 1.0.0 (#529; only 0.98.1 and 0.99.0 are verified, and another version in that line starts with a note), or an exact version the person trusts (#691), and runs every owned jcode process (probe, serve, ACP and TUI) with `JCODE_NO_TELEMETRY=1`, because jcode's telemetry can report the provider account's email. Native diagnostics withhold payloads/hosts. Hub tools are bound to an owned launch (#368): the shared project entry names no peer, each owned Jcode process carries its own peer id and launch id, the daemon admits a hub-tools connection for a Jcode peer only with the id of that peer's current owned launch, and a tools-mode server started without a peer id refuses every tool. Extra Jcode instances (#685, at most 8) are declared only through `instances` in an untracked `.agenthub/config.local.json`, start headless, and each needs its own `jcode_home`, which may not be the base home or another instance's, nor nested in or around one, however it is spelled. Unproved conversation loads refuse; no fresh replacement is called a restore. A finished task's advisory about the project root checkout names the branch only after the hub's whole public-text screen (`Tasks.publicMetadataText`: a PII pattern, a secret shape and a project-private path) passes it and it is a plain ref name; otherwise a generic sentence carries no part of it (#572). Native TUI launch exposes its private socket/session tuple only to the authenticated console, bound to the same live owned socket and session after read-only native persistence proof. Tools/status do not receive it. The TUI has no single-writer guard: person input may interleave with ACP. Native policy remains unmanaged and closing the frontend does not stop ACP. An `important` message reaches a busy Jcode as a soft interrupt written to the hub's own private server socket for the owned session (#572); the hub never cancels a turn to deliver, and a steer that was sent but not acknowledged, or whose turn ended first, is held as `needs_review`, never replayed. Jcode has no switch to choose its MCP servers, so the owned serve and ACP processes load every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, besides the hub's own; `ahub doctor` lists their names only. Point `jcode_home` at a separate home, or remove a server from those files, to keep it out.
360
+ Jcode is optional, source/version-fenced and runs native tools without a hub sandbox or hub permission-mode mapping. .jcode joins the scoped-grant exclusion, including lexical/resolved aliases. Only a person writes its unique project MCP entry; foreign/override/symlink entries refuse. The hub owns a private daemon/socket and ACP group, never the global daemon, and copies no credentials. It accepts a plain jcode release from 0.98.0 up to, not including, 1.0.0 (#529; only 0.98.1, 0.99.0 and 0.99.1 are verified, and another version in that line starts with a note), or an exact version the person trusts (#691), and runs every owned jcode process (probe, serve, ACP and TUI) with `JCODE_NO_TELEMETRY=1`, because jcode's telemetry can report the provider account's email. Native diagnostics withhold payloads/hosts. Hub tools are bound to an owned launch (#368): the shared project entry names no peer, each owned Jcode process carries its own peer id and launch id, the daemon admits a hub-tools connection for a Jcode peer only with the id of that peer's current owned launch, and a tools-mode server started without a peer id refuses every tool. Extra Jcode instances (#685, at most 8) are declared only through `instances` in an untracked `.agenthub/config.local.json`, start headless, and each needs its own `jcode_home`, which may not be the base home or another instance's, nor nested in or around one, however it is spelled. Unproved conversation loads refuse; no fresh replacement is called a restore. A finished task's advisory about the project root checkout names the branch only after the hub's whole public-text screen (`Tasks.publicMetadataText`: a PII pattern, a secret shape and a project-private path) passes it and it is a plain ref name; otherwise a generic sentence carries no part of it (#572). Native TUI launch exposes its private socket/session tuple only to the authenticated console, bound to the same live owned socket and session after read-only native persistence proof. Tools/status do not receive it. The TUI has no single-writer guard: person input may interleave with ACP. Native policy remains unmanaged and closing the frontend does not stop ACP. An `important` message reaches a busy Jcode as a soft interrupt written to the hub's own private server socket for the owned session (#572); the hub never cancels a turn to deliver, and a steer that was sent but not acknowledged, or whose turn ended first, is held as `needs_review`, never replayed. Jcode has no switch to choose its MCP servers, so the owned serve and ACP processes load every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, besides the hub's own; `ahub doctor` lists their names only. Point `jcode_home` at a separate home, or remove a server from those files, to keep it out.
344
361
 
345
- Antigravity (`agy`, #485) is optional, cloud and off roster by default. The launcher accepts the Antigravity CLI 1.4.x line; only 1.4.2 has a native receipt, so another 1.4.x patch launches with a warning that it is unverified, and every other version is refused before a hub connection or plugin write (#589) unless the person trusts that exact version (#691). It runs native tools under its own unmanaged permissions, without a hub sandbox or hub permission-mode mapping; headless, resume, unattended and native permission flags are refused. Its roles are capped at implementer and verifier, so it never plans, reviews or receives PII work. Only the person's `ahub agy` launcher writes its three plugin files in `.agents/plugins/agent-hub/` (`plugin.json`, `mcp_config.json`, `hooks.json`): linked or foreign entries refuse before any write, cleanup removes only that launch's matching files, and it never writes project-level `.agents/hooks.json` or the person's `~/.gemini` settings. Conductor starts are refused, and a declared agy extra instance is refused at config load (#625). Hook authority (`native_hook_launch`) comes only from that launcher, belongs to the console socket that began it and ends when that socket closes; hook input over 1 MiB is refused, and observations are bound to the daemon, launch, native session and project.
362
+ Antigravity (`agy`, #485) is optional, cloud and off roster by default. The launcher accepts the Antigravity CLI 1.4.x line; only 1.4.2 and 1.4.7 have a native receipt ([1.4.7](verification/2026-10-26-agy-1.4.7.md)), so another 1.4.x patch launches with a warning that it is unverified, and every other version is refused before a hub connection or plugin write (#589) unless the person trusts that exact version (#691). It runs native tools under its own unmanaged permissions, without a hub sandbox or hub permission-mode mapping; headless, resume, unattended and native permission flags are refused. Its roles are capped at implementer and verifier, so it never plans, reviews or receives PII work. Only the person's `ahub agy` launcher writes its three plugin files in `.agents/plugins/agent-hub/` (`plugin.json`, `mcp_config.json`, `hooks.json`): linked or foreign entries refuse before any write, cleanup removes only that launch's matching files, and it never writes project-level `.agents/hooks.json` or the person's `~/.gemini` settings. Conductor starts are refused, and a declared agy extra instance is refused at config load (#625). Hook authority (`native_hook_launch`) comes only from that launcher, belongs to the console socket that began it and ends when that socket closes; hook input over 1 MiB is refused, and observations are bound to the daemon, launch, native session and project.
346
363
 
347
364
  Configured ACP adapters (#451 A3) are declared only by the person, in a Git-confirmed untracked `.agenthub/config.local.json`. The whole declaration (command, argv, identity, roles and mode maps) is machine-local authority: a shared or tracked declaration is ignored with a notice, settings never edit it, and changes need a hub restart. The hub runs only the declared argv, never a module or callback. Argv that selects a leader, attach, connect or remote mode is refused, and a Grok declaration needs `--no-leader`. Each start requires the pinned native identity, an exact `session/load` echo on resume, and positive ownership of the spawned process and its original process group; unknown ownership refuses, and stop signals only that verified group. An agent that hands its work to a process outside that group is outside this boundary. Without a mode map native policy is unmanaged, and there is no hub sandbox either way. Configured kinds are cloud peers (never PII), headless only and off roster by default, and the hub makes no quota, context or controlled-recovery claims for them. A read-only instance needs the native read-only mode acknowledged before a prompt and is limited to read and review hub tools, both in the published MCP list and at the host; its native non-read tool requests are rejected, and no setting or runtime change can raise it. These agents' stderr and error text are withheld from logs, and previews redact the executable, argv and session id.
348
365
 
@@ -463,11 +480,17 @@ A declared instance whose only role is reviewer gets review requests, task messa
463
480
 
464
481
  A terminal Claude implementer or verifier instance (#525-5) owns write-class tasks only after the daemon proves its execution root: the current launch id, a private single-link launch record naming the root, the cwd the channel's MCP server reports (verified model-free on Claude Code 2.1.309) and the daemon's own validation of the declared non-canonical branch worktree must all agree, and the proof belongs to the attached claim generation and socket. A detach, a replacement claim or a changed launch record revokes it. Missing, legacy or mismatched evidence still attaches the channel, but write-owner routing is refused and facts grants give nothing, never a fall-back to the canonical tree; terminal recovery revalidates the recorded root against the current declaration. This is same-user launch evidence, not filesystem isolation: Claude can still write wherever its own permissions allow.
465
482
 
466
- The headless Claude implementer (#525-4) runs one owned `claude -p` child per turn in its declared worktree, with `--setting-sources ""`, `--strict-mcp-config`, native permission mode `default`, the hub's managed PreToolUse hook and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `--bare`, `acceptEdits`, `dontAsk` or `bypassPermissions`, and `never-ask` is refused. Each task keeps its own persisted session; unscoped chat reuses one only while exactly one task is in progress, and a `hub_inbox` read refuses a batch attributed to another task. The approval path fails closed: `hub_permission_request` is accepted only from the current child's launch and generation, goes to the console's existing approval queue and `approvals.timeout_s`, and offers allow-once and deny only, with no allow-always or session grant. A timeout, a prompt failure, a disconnect, a hub that is down or refusing, or a stale child binding answers deny; a failed hook decides nothing, so the call still waits for a person. The approval title is the tool name and a JSON rendering of the arguments passed through `sanitize()` and the public-text screen (any PII, secret or private-path match withholds the whole value as `[withheld]`, before any cut), never task text, cut at 600 characters with a `[cut, N chars]` marker; the original arguments stay with the child and are not logged. The MCP wire cannot tell a native permission dispatch from a call the model makes itself; either way it needs a fresh allow-once for its own arguments and grants nothing later. A stop holds an uncertain delivery until the owned process group is confirmed gone. The [native permission receipt](verification/2026-10-23-headless-claude-permissions.md) is model-free on Claude Code 2.1.309; it is not sandbox proof, and a real account, model and person interaction remains a live leg.
483
+ The headless Claude implementer (#525-4) runs one owned `claude -p` child per turn in its declared worktree, with `--setting-sources ""`, `--strict-mcp-config`, native permission mode `default`, the hub's managed PreToolUse hook and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `--bare`, `acceptEdits`, `dontAsk` or `bypassPermissions`, and `never-ask` is refused. Each task keeps its own persisted session; unscoped chat reuses one only while exactly one task is in progress, and a `hub_inbox` read refuses a batch attributed to another task. The approval path fails closed: `hub_permission_request` is accepted only from the current child's launch and generation, goes to the console's existing approval queue and `approvals.timeout_s`, and offers allow-once and deny only, with no allow-always or session grant. A timeout, a prompt failure, a disconnect, a hub that is down or refusing, or a stale child binding answers deny; a failed hook decides nothing, so the call still waits for a person. The approval title is the tool name and a JSON rendering of the arguments passed through `sanitize()` and the public-text screen (any PII, secret or private-path match withholds the whole value as `[withheld]`, before any cut), never task text, cut at 600 characters with a `[cut, N chars]` marker; the original arguments stay with the child and are not logged. The MCP wire cannot tell a native permission dispatch from a call the model makes itself; a person allow still refers only to its original arguments. Automatic Bash authorization uses the separate single-use host wrapper contract below, never a caller-supplied tool id as native provenance. A stop holds an uncertain delivery until the owned process group is confirmed gone. The [native permission receipt](verification/2026-10-23-headless-claude-permissions.md) is model-free on Claude Code 2.1.309, [repeated on 2.1.310](verification/2026-10-26-claude-implementer-2.1.310.md); it is not sandbox proof, and a real account, model and person interaction remains a live leg.
484
+
485
+ The #845 host shell wrapper is the only automatic Bash path for a headless implementer. In `ask-when-needed`, a complete public input must pass the existing #816 read-only proof or the closed task-command proof, with an active owned task and registered worktree. Enabled action-policy classes still ask, and `ask`, PII handling and the `never-ask` refusal are unchanged. The native request and authenticated host response both name wrapper protocol 1, so an older plugin cannot drop the rewrite and execute an original command automatically.
486
+
487
+ A grant uses a random, expiring single-use nonce and an in-memory record bound to task, child generation, root, branch, exact argv, executable and host environment. The fixed wrapper has no command-selection arguments. Redemption consumes the ticket before rechecking current task/child/root/branch and proof; unknown, expired, reused or mismatched tickets return a fixed nonzero refusal and execute nothing. Restart revokes tickets. The host service streams stdout/stderr and exit status from an owned command process, using the proven cwd, absolute executable and the #816 fixed environment plus proxy/peer identity and private command TMPDIR. Native `GIT_DIR`, `RIPGREP_CONFIG_PATH`, PATH and other inherited native values are not execution authority. Host audit logs include neither nonce nor command arguments. The requesting MCP call and wrapper argv necessarily see the nonce; a direct model-selected permission call receives no broader authority and can record the same branch binding after the same host proofs. Read-only and executable code grants (check/test/build) execute through the existing local-worker Seatbelt profile and own-temp rules. Code may write the worktree and owned temp, but all writes to the project's Git common/current metadata are denied, including loose and packed objects, refs, HEAD, index, logs, other worktrees and shared/private submodule metadata. It cannot poison objects that a later checkout would materialize; protected metadata directory entries cannot be renamed or replaced. Code cannot read credentials or the live hub state, even with a canonical-checkout read allowance; network follows local.bash_network, with the proxy for ordinary enabled mode and direct network access when the person explicitly selects "direct". The default branch is captured once at startup from a bounded remote symref probe against the captured GitHub repository, with private HOME/cwd and isolated config; local origin/HEAD is never authority. Unknown or unauthenticated private-remote evidence asks. Sandbox protections are rebuilt and compared at ticket redemption and immediately before spawn; a changed submodule Git-directory inventory invalidates the ticket. Without sandbox-exec these commands ask. Only closed host Git add/commit/push and gh operations run outside the profile. Filter/external-diff/textconv drivers (including global Git LFS) make only add/commit ask; push/gh do not execute those drivers and keep their grants. This deliberately preserves LFS conversion semantics on the person-approved path instead of neutralizing filters. Config includes and partial clones still refuse privileged Git/gh; host Git disables signing and automatic maintenance in addition to core hooks/fsmonitor/credential hardening. A deliberately setsid-detached target descendant can outlive owned-group cleanup and retain its confined worktree writes. This confines automatic task code, not the entire native Claude process.
488
+
489
+ Task branch authority uses recorded `refs.branch`, otherwise a host-observed non-default attached branch on the first eligible task-command request, excluding `main`, `master`, unknown/detached HEAD and another open task's branch. The binding is a normal `Tasks` write with history and an opaque canonical-root hash; a changed branch or worktree asks and never rebinds. Push grants accept only `git push origin <bound>` or `git push -u origin <bound>`; force, refspec, deletion and other remotes ask. Every origin push URL is checked against the captured project repository and PR argv pins that repository. Execution uses an explicit same-branch refspec; configured remote.origin.push/mirror refuses. Project/worktree PATH directories and aliases are removed, and gh child Git inherits core SAFE_CONFIG through fixed environment configuration. PR body-file content is bounded, screened and snapshotted into executed --body bytes. See [the design contract](specs/2026-09-19-agent-hub-design.md#headless-implementer-host-shell-execution-845) for the exact closed forms and [the native receipt](verification/2026-10-26-headless-claude-shell-context.md) for verification limits.
467
490
 
468
491
  The fresh Codex reviewer (#519) needs `review_mode: "fresh"` and a compat-admitted Codex (receipt-verified 0.146.0, 0.163.0 and 0.164.0; `ahub agents` shows the current policy); an unverified/untrusted version is unavailable. Before each review it requires a model-free isolation receipt: binary and config unchanged, only the two hub tools in the configured MCP inventory (each approved by its own name, never by a server-wide default, #783), plugins, hooks and Apps disabled (Apps by a separate feature readback), and a named filesystem profile whose fixtures prove public reads, private-path denial (exact, glob and symlink), standard host credential denial and write denial. Credential and tool-home roots are denied even with an empty `local.deny`; that list does not cover every personal file. A dangling or unreadable credential root refuses the reviewer, and the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never the path or its contents. A failed per-review start names one class from a closed list (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`) with a fixed sentence in the unavailable reason, `hub.log` and the `reviewer` line of `ahub status`; it never carries a path, native CLI text or a token (#722). Its shell is sandboxed read-only, not Claude's Read/Grep/Glob whitelist. Network and web search are configured disabled but not observed: managed requirements can still select cached web search, and no run has shown the review selecting the profile through `default_permissions`. A failed receipt or start never falls back to a continuing native session, and a child that ends without a saved verdict is never approval. The agy reviewer slot is an unavailable placeholder and never runs; it binds nothing, so agy's own `hub_task_show` reads are not fenced as a review (#792).
469
492
 
470
- The fresh Kimi reviewer (#533) needs `review_mode: "fresh"` and a compat-admitted Kimi Code (receipt-verified 2.2.0, 2.3.0 and 2.3.1; `ahub agents` shows the current policy) with its native OAuth login; an unverified/untrusted version, or a failed preparation, is unavailable. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) receipts are model-free and do not prove real login or a model review. It is a native tool-authority boundary, not a sandbox: no Seatbelt wraps it, and same-user native code is not isolated from the filesystem or network. Each review starts a new child in a private `KIMI_CODE_HOME` with managed-provider-only config, a clean temporary working directory and an empty skills directory. Its agent may use only `hub_review` and `hub_task_show`, served by a loopback MCP session bound to the task, head and review revision that never carries the hub's control token; it has no native read, file or shell tool and sees only the screened review brief and host facts. Its `credentials/` and `oauth/` directories and `device_id` link to the person's native ones, so refresh keeps Kimi's own lock and atomic rename; no token is copied and `KIMI_DISABLE_OAUTH_LOCK` is never set. A stop revokes its verdict authority before termination; killing it mid-refresh may require a native re-login. Private tasks are never given to it, and a child that ends without a saved verdict is never approval.
493
+ The fresh Kimi reviewer (#533) needs `review_mode: "fresh"` and a compat-admitted Kimi Code (receipt-verified 2.2.0, 2.3.0, 2.3.1 and 2.3.2; `ahub agents` shows the current policy) with its native OAuth login; an unverified/untrusted version, or a failed preparation, is unavailable. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) receipts are model-free and do not prove real login or a model review. It is a native tool-authority boundary, not a sandbox: no Seatbelt wraps it, and same-user native code is not isolated from the filesystem or network. Each review starts a new child in a private `KIMI_CODE_HOME` with managed-provider-only config, a clean temporary working directory and an empty skills directory. Its agent may use only `hub_review` and `hub_task_show`, served by a loopback MCP session bound to the task, head and review revision that never carries the hub's control token; it has no native read, file or shell tool and sees only the screened review brief and host facts. Its `credentials/` and `oauth/` directories and `device_id` link to the person's native ones, so refresh keeps Kimi's own lock and atomic rename; no token is copied and `KIMI_DISABLE_OAUTH_LOCK` is never set. A stop revokes its verdict authority before termination; killing it mid-refresh may require a native re-login. Private tasks are never given to it, and a child that ends without a saved verdict is never approval.
471
494
 
472
495
  The review brief (#520) withholds private changes whole (both sides of a move) and counts them; a rule file that fails the private-data screen is named, not shown. Attributes and rules are read at the merge base, so a change cannot loosen its own review. Optional changed symbols (#684) come from ripwire 0.6.7 on the daemon's host PATH, run as an owned process on a disposable snapshot of the task's commit; each path, name and kind passes the hub's public metadata screen before the brief, the console's task detail or the dashboard shows it, a private task stores and shows none, and a failed or missing read is `unknown`, never a gate. Spec acceptance, plan review, plan conformance, premature-review, cost, test-integrity (#481 S4), issue and PR identity linking (#694) and dogfood scorecard (#687) reads fetch GitHub metadata only (edit times, check states, thread authorship and resolution, changed file names, open PR heads, workflow run conclusions), never issue bodies, comments or patches. Issue identity linking at propose and accept (#723) parses the task's title and refs locally, reads nothing from GitHub and skips PII tasks. The CI workflow keeps `contents: read` and `actions: read`.
473
496
 
@@ -485,6 +508,14 @@ SDLC metrics and control bands (#481 S7). `ahub sdlc metrics` and `hub_sdlc_metr
485
508
 
486
509
  The runner uses Apple `container`, pinned Bun 1.4.2 and a verified `hostOnly` internal network with DNS disabled, two CPUs and 4 GiB. It starts a named, labeled VM with a bounded idle process, copies a hub-owned snapshot into the running VM, then invokes a fixed `container exec` check. This run/copy/exec sequence is required because Apple `container copy` rejects a stopped container. No host directory, home, credential directory, hub state or socket is mounted. Source blobs are read from the exact Git commit; symlinks and gitlinks anywhere in the source refuse the run before exclusions. Directory prefixes and filenames are checked for NFC/Unicode case-fold collisions before staging writes, including denied entries; folding must reach a fixed point within a bounded number of passes or the run refuses. This is a conservative collision screen, not a claim of exact parity with every host filesystem. Denied and PII paths are excluded and counted. Dependency files require an equal lockfile, bounded regular files/directories and internal relative symlinks. No dependency installation or Internet access occurs in the VM.
487
510
 
511
+ The optional remote backend (#793) selects `checks.runner = "remote"` and a strict `checks.remote` object containing `host`, digest-pinned `image` and absolute `root`. Both the grant and destination must come from an untracked `config.local.json`; a tracked or shared-file grant is refused. The destination is host-owned configuration, never a peer argument. OpenSSH uses BatchMode, disables TTY allocation (`-T`), agent/X11 forwarding (`-a`, `-x`), configured forwardings and local SSH commands, and retains only the hub-owned agent socket for local SSH authentication. It uses fixed Docker operations, and every remote argument is shell-quoted because OpenSSH joins the command before the remote shell interprets it. SSH diagnostics are discarded. Only actual check execution merges remote command stderr into screened output; readiness JSON keeps stdout separate from withheld diagnostics. Exit 255 discards stdout too; retained container stdout is screened and destination/root strings are replaced before receipt/output persistence. Public image identity is digest-only. Platform comes from inspected Linux image metadata and the actual fixed Bun probe architecture, with only a numeric Docker version prefix. No host, account or remote root is included in those projections.
512
+
513
+ Remote checks reuse the same request validation, exact Git snapshot, queue, authority fences, repository heavy lease, receipt retention and cleanup lifecycle. They stream a trusted host tar producer into `docker exec -i --user 65532:65532 ... tar -xf - -C /work`; no working-tree sync or Mac `node_modules` copy occurs. Darwin pins the system /usr/bin/tar regardless of PATH; Linux keeps the hub-owned executable resolver. The archive excludes extended attributes, macOS metadata and ACLs, and normalizes owner/group identifiers and names instead of carrying host identity. The staging parent remains mode 0700; snapshot directories are explicitly 0755 and files 0644 or Git-executable 0755 even under umask 077, so non-root execution can traverse and read the copy. Extraction does not restore archive ownership: the copied files belong to the container uid and may be changed only in its throwaway work tmpfs. No privileged chown or root extraction is used. Only the throwaway work tmpfs and home/temp tmpfs can be writable, never the host source. The tar producer is cancelled and reaped on failure or cancellation.
514
+
515
+ Docker checks use a non-root numeric uid, `--network none`, read-only root filesystem, home/temp/work tmpfs, all capabilities dropped, no-new-privileges and fixed pids, CPU (4) and memory (4 GiB) caps. No host path, socket, home or credential mount is allowed. A uniquely named/labeled run uses only a locally present image (`--pull never`). Removal is confirmed through project/name-filtered Docker inventory before the existing heavy lease is released. Inventory projects individual hub label fields instead of parsing Docker's comma-joined label display, and does not enumerate unrelated projects' containers. Readiness checks Docker reachability/version, the exact image digest and architecture, and a fixed hardened network-none probe that uses the same archive transfer and non-root readback as a run; any failed proof is `remote runner unavailable`, with no local fallback. Probe cleanup remains bounded and runs even after its admission signal is cancelled. Exact project/probe-UUID/name rows without a task run id are left to their fixed 600-second expiry and excluded from task-run orphan warnings. Malformed probe rows and unknown task-run owners still produce warnings; labels alone never authorize killing a probe.
516
+
517
+ Remote dependencies are prepared in a separate, admitted step inside the same run, never in the check (#793b). A named Docker volume holds `node_modules`; its name is `agenthub-check-deps-<project key>-<32 hex>`, the hash of the commit's `bun.lock` SHA-256, a digest of every other install input in the staged source (each `package.json`, `bunfig.toml`, `.npmrc` and `patches/` file, by path and content), the Bun version and architecture the readiness probe observed in the pinned image, and the image digest, so a different lockfile, Bun, architecture or image never shares a volume. Because a root or workspace lifecycle script (`preinstall`, `install`, `postinstall`, `prepublish`, `preprepare`, `prepare`, `postprepare`) can build `node_modules` from tracked source the key does not cover, a commit whose `package.json` declares one, or whose manifest is not valid JSON, is refused before any volume is created; the install stays exactly `bun install --frozen-lockfile` and is never weakened with `--ignore-scripts`. The volume carries `agenthub.deps.project` and `agenthub.deps.key` labels, and an existing volume is used only when both match exactly: a same-named volume with other labels is refused and never mounted or removed. A missing volume is prepared: a root, capability-dropped, network-less container only changes the new volume's ownership to uid 65532, then one container, identical to a check container apart from a writable volume mount and a `bridge` network, runs exactly `bun install --frozen-lockfile` on the commit's own source (a source-only snapshot with no Mac `node_modules`) and writes a marker holding the key. That container is the only one in the whole backend with a network, and the only one that mounts the volume writable. Every prepared volume, new or existing, is then proven by a separate network-less container that mounts it read-only, reads the marker and confirms a write fails; a volume that does not verify is removed and prepared again, never repaired in place, and a failed preparation removes its candidate volume. The check container mounts only that volume, read-only, with `--network none`, and never installs. The preparation, ownership and verification containers reuse the run's name and labels, are removed and confirmed absent through the existing inventory before the next step, and are covered by the same cancellation, deadline (the ten-minute budget includes preparation), cleanup and orphan-sweep paths and the heavy lease. A missing or unprovable volume refuses the run with `remote dependencies unavailable`, never a writable shared cache and never a local fallback. After a verified volume, the project's own volumes beyond the newest three (the current one always kept) are removed, but only those whose name matches the exact pattern, whose project label is this project and whose key label is a 64-hex key that reproduces the name; a volume with an absent, malformed or inconsistent key label, another project's volume and any other name are neither counted nor touched. Install output is not retained or shown. The backend's `root` stays reserved, validated metadata, and no Docker command uses a host path. Fake transport and argv tests establish the contract, and a local Docker run of the production transport showed one network-attached install on the first run and none on the second (see the [receipt](verification/2026-10-25-remote-docker-transport.md#dependency-volume-793b)). Actual machine opt-in, remote SSH and pi's live tools remain unverified (AC6).
518
+
488
519
  Dependency copies detect observed inode, size and timestamp changes and refuse final-component symlinks using `O_NOFOLLOW`. A same-user writer can still exchange and restore a parent directory between observations; this is not an atomic immutable-dependency guarantee. Source commits remain immutable. Do not treat a dependency copy as protection against a hostile process running as the same host user.
489
520
 
490
521
  Checks serialize through the repository's existing `ahub-heavy.lock`, with one active and at most two queued runs, a ten-minute execution ceiling after heavy-slot admission and a bounded heavy-slot wait. Container startup has a 120-second cap for cold VM boot, and copy has a 180-second cap for the bounded 512 MiB source plus 1 GiB dependency corpus. Each stage is clamped to the remaining ten-minute budget and uses the same cancellation signal; exec receives only the remaining budget. Network/probe commands retain a 15-second cap, also clamped and cancellable during an admitted run. Named-container cleanup has its separate bounded teardown commands and must still be confirmed before releasing the heavy slot. Queued cancellation settles its receipt and releases queue capacity immediately; active cancellation, timeout and shutdown stop only the named check container. Cleanup is confirmed through inventory before releasing the heavy slot; unknown cleanup keeps the slot and reports an error. Startup removes only this project's labeled containers whose signed recorded hub owner is confirmed gone. Unknown owners are reported for the person, never killed.
package/docs/smoke.md CHANGED
@@ -2001,13 +2001,13 @@ Complete these legs in a disposable project with authorized accounts after sourc
2001
2001
  - Request a delta review and confirm the same eligible independent reviewing family receives a new session with prior findings. After an earlier same-family fallback, a newly available independent family still takes precedence. Exercise configured re-rotation separately. Stop or exhaust one reviewer and confirm the pure routing walk chooses the next eligible candidate; no failed start or child ending without a saved verdict becomes approval. Check the next candidate even when the missing verdict followed a vendor quota error; no text-based quota classifier is assumed.
2002
2002
  - Make all pool candidates unavailable and confirm a blocked review awaiting planner/person action. Then test a different fresh session in same-family fallback only when no other family can run. Verify stale-child, changed-head and private-review verdict refusals.
2003
2003
  - Verify compatibility separately: a Codex/Kimi headless reviewer-role declaration without `review_mode` keeps its native app-server/ACP behavior and existing worktree/permission options, and stays outside the fresh pool. An explicit fresh declaration must refuse an unsupported backend without downgrading.
2004
- - The default logical pool is Claude, Codex, Kimi. Kimi is runnable for a compat-admitted version when runtime preparation passes (receipt-verified 2.2.0, 2.3.0 and 2.3.1; inspect `ahub agents`); an unverified/untrusted version or failed preparation is unavailable with its reason. Seed modulo includes only kinds with a declared candidate, even when blocked; undeclared kinds remain trailing trace entries. Verify seeds 0..5 with only Claude/Codex declared alternate 3/3, and an offline declared kind still retains its slot. Agy stays unavailable as a reviewer with exact reason `agy is an implementer peer (#485)`, including explicit pool requests. #234 adds no agy reviewer launch or live leg.
2004
+ - The default logical pool is Claude, Codex, Kimi. Kimi is runnable for a compat-admitted version when runtime preparation passes (receipt-verified 2.2.0, 2.3.0, 2.3.1 and 2.3.2; inspect `ahub agents`); an unverified/untrusted version or failed preparation is unavailable with its reason. Seed modulo includes only kinds with a declared candidate, even when blocked; undeclared kinds remain trailing trace entries. Verify seeds 0..5 with only Claude/Codex declared alternate 3/3, and an offline declared kind still retains its slot. Agy stays unavailable as a reviewer with exact reason `agy is an implementer peer (#485)`, including explicit pool requests. #234 adds no agy reviewer launch or live leg.
2005
2005
 
2006
2006
  Record source head, exact executable version/hash, isolation receipt, task/head identities, route walk, distinct native session evidence and persisted verdict separately. Do not record credentials or raw transport environments. Codex sandboxed shell must not be described as Claude's Read/Grep/Glob-only whitelist. Network policy is configured disabled; no network-denial probe or native LLM isolation result has been recorded by this investigation.
2007
2007
 
2008
2008
  ## Kimi reviewer and task-bound MCP (#533)
2009
2009
 
2010
- Kimi receipt-verified 2.2.0, 2.3.0 and 2.3.1 are available when runtime preparation passes; a failed or unsupported preparation remains unavailable. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) receipts are model-free; the following authenticated evidence belongs to 2.2.0. Completed evidence includes account-free two-tool/native OAuth race checks, host/fake lifecycle refusals and authenticated fresh `changes_requested` verdicts. The complete-collector live run observed project parity/canary absence, authenticated exact-two hub inventory/calls, zero decoy requests, cleanup and unchanged real config hashes. Its original aggregate receipt remains failed because of an unattributed extra observer error; the later account-free GET reproduction/correction is not a new live pass. These separate outcomes are preserved in the R0/live report. The remaining actual interactive-TUI/coexistence and usage-observation checks are **UNVERIFIED**; they do not turn completed live/fake evidence into an unrun test. Person ruling 2026-10-19 (8281ec35) limits #234 to Kimi and task-bound MCP and supersedes all earlier agy reviewer implementation/admission/live-leg rulings. Agy is an implementer under #485; its retained reviewer slot remains unavailable with exact reason `agy is an implementer peer (#485)`. Earlier agy probes and unresolved meta-tool/resource findings remain historical in the [R0 report](verification/2026-10-19-kimi-agy-reviewer-isolation.md), not active smoke requirements here. Kimi's native-permission/shared-account ruling supersedes its earlier R0-only hold and Seatbelt requirement. The [spec amendment](specs/2026-09-19-agent-hub-design.md#kimi-reviewer-and-task-bound-mcp-amendment-533-hub-task-234) defines the active contract and residuals.
2010
+ Kimi receipt-verified 2.2.0, 2.3.0, 2.3.1 and 2.3.2 are available when runtime preparation passes; a failed or unsupported preparation remains unavailable. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) and [2.3.2](verification/2026-10-26-kimi-232-reviewer-compat.md) receipts are model-free; the following authenticated evidence belongs to 2.2.0. Completed evidence includes account-free two-tool/native OAuth race checks, host/fake lifecycle refusals and authenticated fresh `changes_requested` verdicts. The complete-collector live run observed project parity/canary absence, authenticated exact-two hub inventory/calls, zero decoy requests, cleanup and unchanged real config hashes. Its original aggregate receipt remains failed because of an unattributed extra observer error; the later account-free GET reproduction/correction is not a new live pass. These separate outcomes are preserved in the R0/live report. The remaining actual interactive-TUI/coexistence and usage-observation checks are **UNVERIFIED**; they do not turn completed live/fake evidence into an unrun test. Person ruling 2026-10-19 (8281ec35) limits #234 to Kimi and task-bound MCP and supersedes all earlier agy reviewer implementation/admission/live-leg rulings. Agy is an implementer under #485; its retained reviewer slot remains unavailable with exact reason `agy is an implementer peer (#485)`. Earlier agy probes and unresolved meta-tool/resource findings remain historical in the [R0 report](verification/2026-10-19-kimi-agy-reviewer-isolation.md), not active smoke requirements here. Kimi's native-permission/shared-account ruling supersedes its earlier R0-only hold and Seatbelt requirement. The [spec amendment](specs/2026-09-19-agent-hub-design.md#kimi-reviewer-and-task-bound-mcp-amendment-533-hub-task-234) defines the active contract and residuals.
2011
2011
 
2012
2012
  - For Kimi regression smoke, confirm a fresh child per review, no resume, source evidence supplied only by screened `hub_task_show`/review brief and host facts, and no native Read/file/shell tool. Confirm the in-process host MCP exposes only the task-bound `hub_review` and `hub_task_show` capability, without the full hub control token. Exercise wrong task/head/review revision, stale/replaced child, private review, malformed arguments, unknown tool and stopped-child refusals; verify no saved approval on any failure.
2013
2013
  - For each compat-admitted Kimi version, confirm no Seatbelt wrapper, a private `KIMI_CODE_HOME` with managed-provider-only config, clean temporary cwd and explicit empty skills directory. Verify exactly the two `mcp__agent-hub__...` tools in model disclosure and invocation; reject native read/write/command and extra MCP calls. Confirm `credentials/` and `oauth/` are directory symlinks to the native locations, `device_id` links the native identity, no token copy/extraction/static provider is used, and `KIMI_DISABLE_OAUTH_LOCK` is absent. The concurrent account-free OAuth print stand-in proved two expired generations serialize refresh through shared directory links, but it was not an attached interactive TUI. Actual TUI refresh/atomic rename/lock coordination and shared-account quota/pause fanout remain separate live legs. Existing project MCP/config byte-parity observations remain recorded in the R0/live report. Record identities/metadata without token values.
@@ -2019,7 +2019,7 @@ Kimi usage ceiling: 2.2.0 print stream-json exposes no token counts. Keep unobse
2019
2019
 
2020
2020
  ## Antigravity P1 (#485)
2021
2021
 
2022
- - Run agy on the 1.4.x line (1.4.2 verified; other patches warn, #589); verify its executable/version and keep the actual TUI/IDE leg separate from the account-free [T0 transport receipt](verification/2026-10-19-agy-p1-t0.md).
2022
+ - Run agy on the 1.4.x line (1.4.2 and 1.4.7 verified; other patches warn, #589); verify its executable/version and keep the actual TUI/IDE leg separate from the account-free T0 transport receipts ([1.4.2](verification/2026-10-19-agy-p1-t0.md), [1.4.7](verification/2026-10-26-agy-1.4.7.md)).
2023
2023
  - With a reviewed compatible daemon running and agy explicitly on roster, the person runs `ahub agy` in a terminal. Confirm peer `agy` is pull-only, native permissions unmanaged and no hub sandbox/budget source is claimed.
2024
2024
  - In the native TUI call `hub_inbox`, `hub_send`, and a permitted board read/proposal within a non-PII task. Confirm the tool audience and actor remain `agy`, the native model consumes the tool results, and no unsolicited push or PII task text reaches it.
2025
2025
  - End the TUI normally. Confirm its group/socket detaches and only its matching managed three-file plugin is cleaned up; unrelated project/global settings remain unchanged.