@staix/agent-hub 0.14.0 → 0.15.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.
- package/CHANGELOG.md +17 -0
- package/README.md +16 -6
- package/docs/agent-notes/README.md +4 -0
- package/docs/agent-notes/adapters.md +17 -3
- package/docs/agent-notes/budget.md +4 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +14 -9
- package/docs/agent-notes/tasks.md +9 -4
- package/docs/agent-notes/tests.md +3 -3
- package/docs/dogfood.md +9 -4
- package/docs/events.md +21 -7
- package/docs/operations.md +90 -18
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +9 -3
- package/docs/sdlc.md +17 -0
- package/docs/security.md +37 -10
- package/docs/specs/2026-09-19-agent-hub-design.md +74 -10
- package/docs/verification/2026-10-23-0.15.0.md +80 -0
- package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
- package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
- package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
- package/docs/verified.json +29 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +992 -312
- package/src/adapters/acp.ts +43 -7
- package/src/adapters/claude-channel.ts +26 -4
- package/src/adapters/claude-implementer.ts +243 -0
- package/src/adapters/claude-print-child.ts +90 -0
- package/src/adapters/claude-task-sessions.ts +80 -0
- package/src/adapters/compat.ts +4 -0
- package/src/adapters/conformance/enrollment.ts +2 -1
- package/src/adapters/conformance/fakes/acp-server.ts +3 -1
- package/src/adapters/definitions/builtin-metadata.ts +14 -2
- package/src/adapters/definitions/native-channel-state.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/kimi-wire-usage.ts +303 -0
- package/src/adapters/registry-types.ts +7 -3
- package/src/adapters/registry.ts +14 -2
- package/src/adapters/reviewer-print.ts +30 -53
- package/src/cli/console-config.ts +123 -0
- package/src/cli/console-state.ts +28 -1
- package/src/cli/console.ts +25 -1
- package/src/cli/facts-hook.ts +9 -5
- package/src/cli/help.ts +4 -3
- package/src/cli/launch.ts +10 -4
- package/src/cli/main.ts +47 -9
- package/src/cli/output.ts +28 -0
- package/src/cli/reset.ts +1 -1
- package/src/cli/settings-edit.ts +89 -0
- package/src/cli/terminal-recovery.ts +1 -6
- package/src/hub/action-enforce.ts +82 -0
- package/src/hub/action-policy.ts +22 -10
- package/src/hub/board.ts +26 -2
- package/src/hub/budget.ts +5 -0
- package/src/hub/bus.ts +5 -5
- package/src/hub/claude-launch-proof.ts +33 -0
- package/src/hub/config-apply.ts +5 -0
- package/src/hub/config-edit-host.ts +173 -0
- package/src/hub/config-edit.ts +282 -0
- package/src/hub/config-tier.ts +89 -0
- package/src/hub/control-client.ts +24 -3
- package/src/hub/daemon.ts +368 -59
- package/src/hub/deliverable-files.ts +131 -0
- package/src/hub/deliverables.ts +125 -0
- package/src/hub/delivery-journal.ts +18 -2
- package/src/hub/events.ts +3 -1
- package/src/hub/hub-tools.ts +19 -3
- package/src/hub/instances.ts +7 -4
- package/src/hub/kimi-usage.ts +144 -0
- package/src/hub/manager.ts +1 -1
- package/src/hub/peers.ts +7 -0
- package/src/hub/permission-payload.ts +15 -0
- package/src/hub/routing.ts +118 -3
- package/src/hub/settings.ts +1 -1
- package/src/hub/tasks.ts +202 -15
- package/src/hub/token-window.ts +33 -0
- package/src/hub/usage-rollup.ts +16 -6
- package/src/hub/workflow.ts +16 -0
- package/src/ui/index.html +115 -1
package/docs/events.md
CHANGED
|
@@ -40,7 +40,7 @@ marked `private: true`, and PII tasks `pii: true`.
|
|
|
40
40
|
| `turn_start` | `peer`, `turn` (`<peer>#<hub run>.<n>`, unique across restarts). A turn follows the adapter: pausing a busy peer does not end it |
|
|
41
41
|
| `turn_end` | `peer`, `turn`, `ms`, `tokens` (when the adapter reported any during the turn), `files` and `snapshotMs` (when snapshots are on: how many files the turn changed, and the time both snapshots took); optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`) frozen at turn start, `pii: true` for an attributed PII task |
|
|
42
42
|
| `tokens` | `peer`, `n` (tokens added since the previous report), optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`), `pii: true` for an attributed PII task |
|
|
43
|
-
| `usage` | `peer`, `source` (`omniroute`, `claude_transcript`, `codex_appserver`), opaque `id`, optional `measuredAt` (provider/source time), the `requestedModel`, `servedModel` and `provider` labels, and any provider-reported counters (`inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, `totalTokens`); optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`), `pii: true` for an attributed PII task. Missing counters stay unknown. |
|
|
43
|
+
| `usage` | `peer`, `source` (`omniroute`, `claude_transcript`, `codex_appserver`, `kimi_wire`), opaque `id`, optional `measuredAt` (provider/source time), the `requestedModel`, `servedModel` and `provider` labels, and any provider-reported counters (`inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, `totalTokens`); optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`), `pii: true` for an attributed PII task. Missing counters stay unknown. |
|
|
44
44
|
| `task` | `id`, `event` (the board history event, e.g. `proposed`, `assigned`, `done`, `check failed`, `blocked`, `ready`), `by`, `state`, `owner`, `reviewer`, `class`, `pii`, and `reason` when the board recorded a move reason (`manual`, `budget`, `offline`, `idle`, `rejections`, `declined`, `inference_failed`, `delivery_failed`) |
|
|
45
45
|
| `overlap` | `task`, `owner`, `others` (`task`, `owner`, `paths`, and `symbols` when plans name the same symbol; a name that matches a PII pattern is left out, so either list can be empty), the structured twin of the console notice |
|
|
46
46
|
| `permission_mode` | `peer`, `from`, `to` (`ask`, `ask-when-needed`, `never-ask`): a person set a peer's permission mode (`from` equals `to` when it was already in force) or confirmed a never-ask default (issue #242) |
|
|
@@ -56,12 +56,26 @@ marked `private: true`, and PII tasks `pii: true`.
|
|
|
56
56
|
|
|
57
57
|
Token usage by adapter:
|
|
58
58
|
|
|
59
|
-
- Kimi (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
- Kimi (#285 phase 2): consumption is the hub's own count of Kimi's session wire log, never an ACP total. The `{used, size}`
|
|
60
|
+
`usage_update` Kimi sends is context occupancy: it is shown as the peer's native context reading (source
|
|
61
|
+
`acp_usage_update`, phase 1) and writes no `tokens` event. The reader (`src/adapters/kimi-wire-usage.ts`, the only code that knows
|
|
62
|
+
the format) finds the session directory from `session_index.jsonl` (resolved with `realPath`, kept inside `<kimi home>/sessions`)
|
|
63
|
+
and reads `agents/*/wire.jsonl` incrementally from saved offsets, sub-agents included, at a turn end, when a `usage_update`
|
|
64
|
+
arrives and when the peer goes offline (a turn start only binds the session and fixes the history floor, it reads nothing): no
|
|
65
|
+
watcher, no timer. One call reads at most a fixed byte bound across all of the session's files (the rest waits for the next call),
|
|
66
|
+
and two agent entries that are one file count once. Each `usage.record` row becomes a `usage` event
|
|
67
|
+
(`source: kimi_wire`, `inputTokens` = Kimi's `inputOther`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `totalTokens`
|
|
68
|
+
including cache reads, `servedModel`, `measuredAt` = Kimi's row time) and a `tokens` event of the counted share. The id is an
|
|
69
|
+
opaque hash of session, agent and byte offset; no path, session id or text reaches an event. Only the four integers, the model
|
|
70
|
+
label and the time are copied from a row. The counted share is `inputOther + inputCacheCreation + output` (`kimiCountedTokens`,
|
|
71
|
+
one function): cache reads are recorded and shown, never counted against `budget.kimi_tokens_5h`. The rolling 5 h sum of the
|
|
72
|
+
counted share against that ceiling is the `tokens` window (`hub count, kimi wire log, of kimi_tokens_5h N`). Rows already in the
|
|
73
|
+
session's log when the hub first sees it (a resumed session) are history and are not counted. An unreadable index, a missing,
|
|
74
|
+
shrunk or replaced file (a replacement is told by a hash of the consumed head and tail kept with each offset, since Linux can reuse the inode), a truncated last line, an unknown row shape or an untested wire `protocol_version` (only 1.5 is known)
|
|
75
|
+
makes the count unknown: one notice (`kimi usage unknown (<reason>)`), the `tokens` reading is dropped, and no figure is reported
|
|
76
|
+
until the 5 h window the gap touched has passed (a transient read error or truncated line heals on the next read). Wire format
|
|
77
|
+
followed: protocol 1.5 as written by Kimi Code 2.3.0 (the issue read 2.1.1; the hub verifies 2.2.0 and 2.3.0 in `compat.ts`). The
|
|
78
|
+
live check against Kimi's own `/usage` (#285 AC11) is not done yet.
|
|
65
79
|
- Codex (app-server `thread/tokenUsage/updated`): recorded as the growth of the thread's running total, so
|
|
66
80
|
compaction estimates, usage-limit refreshes and the replay to a reattaching connection add nothing. A thread
|
|
67
81
|
started under the hub counts from zero; a resumed thread's first update is its history and only sets the
|
package/docs/operations.md
CHANGED
|
@@ -730,7 +730,7 @@ stay first. Stream: one aligned row per event (time, kind, sender, body in monos
|
|
|
730
730
|
the section head, above the scrolling list. Memory: result rows show title, kind and age, the selected row is marked with an accent
|
|
731
731
|
edge, and the graph and the detail pane use the colour tokens. Settings: each group is a section with a short header, a key that waits
|
|
732
732
|
for a restart carries a "restart pending" badge, and "Pending restart" shows a count; the text lines, Preview, Save and Undo are as
|
|
733
|
-
they were. Message uses the shared form styles.
|
|
733
|
+
they were. A Structure group edits structural configuration in a settings session (#689, see Structural configuration below). Message uses the shared form styles.
|
|
734
734
|
|
|
735
735
|
Icons replace the symbol glyphs: the star mark, the stream arrow and the Office "checking" mark are sprite icons (twelve Tabler icons
|
|
736
736
|
in all), and the Office draws each condition (idle, busy, offline, paused, waiting on approval) as a shape (ring, disc, cross, bars, raised hand)
|
|
@@ -1087,7 +1087,7 @@ approved; caught (changes were requested and the owner's redo was approved);
|
|
|
1087
1087
|
contradicted (within a week, work on the same file or symbol failed its check
|
|
1088
1088
|
or review); escalated (after the reviewer asked for changes). A record counts
|
|
1089
1089
|
tasks, not verdicts. `ahub task show <id>` lists a task's outcomes and `ahub
|
|
1090
|
-
route explain` shows each reviewer's record with the implementer. With
|
|
1090
|
+
route explain` shows each reviewer's record with the implementer. `ahub route explain <id>` also prints the planner walk (`planner candidate X: ...` lines, then `planner: X (rotation | named | kept)`): the rotation among the rostered planner holders by task id (#525-3), shown for information, since nothing records or enforces a per-task planner yet. A peer that accepted or finished a task never reviews it; when every eligible reviewer did, the person reviews and the trace says so. With
|
|
1091
1091
|
`"review": { "adaptive": true }` in `.agenthub/config.json`, reviewers with at
|
|
1092
1092
|
least `min_reviews` (5) reviews of that implementer in the class are ordered by
|
|
1093
1093
|
how their reviews held up, ahead of quota but after idle before busy; off by
|
|
@@ -1244,16 +1244,16 @@ ahub usage limits [--peer <id>] [--limit <n>] [--json] # current quota windows
|
|
|
1244
1244
|
ahub usage total [--json] # lifetime development tokens since the first event, per peer and overall
|
|
1245
1245
|
```
|
|
1246
1246
|
|
|
1247
|
-
The daemon folds `events.jsonl` into `usage_rollup`, `token_rollup` and `limit_events` from a stored byte offset, at most 8 MiB per pass
|
|
1247
|
+
The daemon folds `events.jsonl` into `usage_rollup`, `token_rollup` and `limit_events` from a stored byte offset, at most 8 MiB per pass, with a 20 ms scheduling slice checked between whole records (one record is indivisible),
|
|
1248
1248
|
only whole lines, and each pass commits its rows and the new offset together, so a crash repeats a pass and never half-applies one. A rebuild
|
|
1249
|
-
from byte 0 gives the same rows (tested). A log that was replaced by a shorter one is folded again from the start.
|
|
1249
|
+
from byte 0 gives the same rows (tested). A log that was replaced by a shorter one is folded again from the start. Before time slicing, on this project's 47 MB,
|
|
1250
1250
|
238k-line log the first fold took about 170 ms and a `usage` command about 95 ms, against about 150 ms for `ahub report --by task`, which
|
|
1251
1251
|
rescans the whole file on each call.
|
|
1252
1252
|
`issue`, `pr` and `release` are the board's current links of each task, read when you ask, so a task linked later is counted under its issue
|
|
1253
1253
|
for the history it already spent. A task with no link, and a record with no task, are `unattributed`.
|
|
1254
1254
|
|
|
1255
1255
|
Every number names its source: `native` (the provider's own counter), `hub count` (the hub's own `tokens` increments, which cover peers that
|
|
1256
|
-
report no usage record, such as Jcode
|
|
1256
|
+
report no usage record, such as Jcode), or `unknown`. Kimi's rows are `native`: the hub reads Kimi's own per-request counters from its session wire log and records them as `kimi_wire` usage records (#285 phase 2). Unknown is never shown as 0. `TOTAL` includes cache reads and `WITHOUT CACHE`
|
|
1257
1257
|
removes them; Claude's total is the sum of its four reported counters because its transcript carries none (`ahub report` still shows it as unknown), and a source whose input already
|
|
1258
1258
|
contains the cached part (Codex, the gateway) gets no derived total. A lifetime total that mixes a native and a hub-counted peer is labelled
|
|
1259
1259
|
`hub count`. When a peer is unknown the overall figure is `partial, at least`: a lower bound that leaves the named peers out, never a sum that counts them
|
|
@@ -1606,9 +1606,18 @@ stays the default until an evaluation says otherwise (`docs/cooperbench.md`).
|
|
|
1606
1606
|
|
|
1607
1607
|
## Always-ask action policy foundation (#636)
|
|
1608
1608
|
|
|
1609
|
-
The classifier, local configuration and session budget counter
|
|
1609
|
+
The classifier, local configuration and session budget counter decide; since slice 2 the Claude PreToolUse hook and ACP `session/request_permission` enforce them. Codex, Jcode, Pi and the local worker are not enforced yet (slice 3, #313): their `ahub status` rows still say `always-ask not enforceable: adapter integration pending`. A headless Claude implementer (#525-4) runs the same hook check (its facts hook returns `ask` for a classed call), but its status row also still says `adapter integration pending`. A default-on policy adds a pre-tool check only where the hub sees the call before it runs, and each peer's status row says whether it does.
|
|
1610
1610
|
|
|
1611
|
-
|
|
1611
|
+
What is enforced, and where it is not:
|
|
1612
|
+
|
|
1613
|
+
| Peer | Enforced | Not enforceable (the status row says why) |
|
|
1614
|
+
| --- | --- | --- |
|
|
1615
|
+
| Claude (`ahub claude`) | the managed PreToolUse hook returns `ask` for a classed call in `ask`, `ask-when-needed` and `never-ask` alike, and `ask` beats every allow the hook would add | a session with no hub permission hook, or one whose attached session has not yet run it (the launch record outlives the process, so it proves nothing alone: the status row says `no live Claude session has run the hub permission hook yet`); an `--unattended` launch (`--dangerously-skip-permissions` skips every permission check, hooks included); a hub that cannot answer the hook leaves Claude's own policy |
|
|
1616
|
+
| Kimi and other hub-owned ACP peers (headless) | `session/request_permission` for a classed call is never auto-approved (not even for a hub-owned tool title), carries `Action approval (<classes>): <command>`, withholds `allow_always`, and under `ahub --unattended` still waits for a person and expires as a denial | a peer in its native `ask-when-needed` (`yolo`) or `never-ask` (`auto`) mode: Kimi sends no request for a call that mode allows, so the hub cannot see it; the status row says `its native mode never-ask sends no request for a call it allows`. Switch the peer to `ask` to enforce. A request whose payload the agent did not report is not classed (unknown follows the mode) |
|
|
1617
|
+
|
|
1618
|
+
Class budgets: the approval of a classed ACP call offers one extra choice, `Allow <class> xN for this session`, when the class budget is above one. Only that choice (never an ordinary allow) spends the ledger: it allows this call and the next N-1 of the same classes in the same native session, then asks again; several requests open at once share it, so a budget of N never lets more than N approved calls through. A new native session, a changed policy or a stopped, crashed or restarted peer starts from zero, even when it resumes the same session id. A budget of one is the single call the ordinary allow already covers. Claude's own permission dialog cannot carry that choice, so every classed Claude call asks.
|
|
1619
|
+
|
|
1620
|
+
Only ignored, untracked `.agenthub/config.local.json` can hold `action_policy`. Its optional keys are `enabled` (default true), `always_ask` (a distinct subset of publish, external-write, destroy, install; default all), and `budgets` (per-class integers 0..1000; defaults 1, 5, 3, 2 respectively). Shared `config.json` cannot set this policy and gets no confirmation offer. A tracked or unknown-trust local file is ignored. Use `ahub settings set action_policy.enabled <true|false>` or `ahub settings set action_policy.budgets.<class> <n>` for registered settings; change `always_ask` in the local file and use the existing confirmed terminal reload. Policy publication invalidates cached grants and pending approval challenges. Startup still reads the untracked local policy without a retained confirmation, although the Claude and ACP gates now enforce it; this ceiling stays open (#312/#313).
|
|
1612
1621
|
|
|
1613
1622
|
The ledger asks first and after the approved class budget is spent. Adapter integrations must request that class allowance explicitly; an ordinary one-time answer does not seed the cache, and a cached budget never suppresses a prompt required by the native mode. One approval includes its own invocation; budget zero asks for every invocation. A fresh native session resets counters, and status polls do not bind sessions. Unknown command syntax retains native permission handling. This foundation never changes a sandbox or bypasses a denied permission; per-adapter enforcement tests land with their integrations.
|
|
1614
1623
|
|
|
@@ -1797,7 +1806,9 @@ if another write replaces that record, preview again. For busy roster removal,
|
|
|
1797
1806
|
the panel names `ahub roster remove <peer> --now`, which explicitly stops an owned
|
|
1798
1807
|
runtime or detaches a native session during its active turn; the panel does not force it.
|
|
1799
1808
|
Esc cancels, and pasted confirmation text cannot
|
|
1800
|
-
write. This panel uses the current authenticated daemon connection.
|
|
1809
|
+
write. This panel uses the current authenticated daemon connection. `c` edits
|
|
1810
|
+
structural configuration and `R` prefills `restart` while a restart is pending
|
|
1811
|
+
(see [Structural configuration](#structural-configuration-from-the-console-and-the-dashboard-issue-689)).
|
|
1801
1812
|
|
|
1802
1813
|
The dashboard's Settings section and `ahub settings` show and change the same
|
|
1803
1814
|
closed list of settings (issue #269). Run them from a plain terminal:
|
|
@@ -1885,12 +1896,46 @@ Pi relay model aliases (`pi.dgx_coding`, `pi.dgx_fast`) and `routing.pi` retain
|
|
|
1885
1896
|
waits for `y` in `ahub console` at each hub start. Tool approvals stay
|
|
1886
1897
|
deny-only for Pi and local in every session.
|
|
1887
1898
|
- Commands and binaries, credential files, gateway URLs and the local worker's
|
|
1888
|
-
reach are never editable here:
|
|
1899
|
+
reach are never editable here: change them with `ahub settings edit` in a
|
|
1900
|
+
terminal, each confirmed by typing its name (tier 3 below), or edit
|
|
1901
|
+
`.agenthub/config.local.json` yourself.
|
|
1889
1902
|
- Every change is announced in `hub.log` and open consoles with its source and
|
|
1890
1903
|
recorded as a `settings` event. `ahub settings` needs a running hub; an older
|
|
1891
1904
|
hub answers the request as unknown and the CLI advises upgrading. The unified
|
|
1892
1905
|
dashboard (`ahub ui --all`) acts as an ordinary session.
|
|
1893
1906
|
|
|
1907
|
+
### Structural configuration from the console and the dashboard (issue #689)
|
|
1908
|
+
|
|
1909
|
+
Instances, the review and approval blocks and the lifecycle can be edited without an editor. The hub reads the candidate with the loaders it starts with (`readInstanceConfig` and `validateInstances`, `loadConfig` with its effective-config bounds, `checkJcodeInstanceHomes`, `sdlcConfig`), shows a diff and writes only after a person confirms. Three tiers:
|
|
1910
|
+
|
|
1911
|
+
1. **Registry settings** (everything above) are unchanged.
|
|
1912
|
+
2. **Structural keys**, editable in the console and in a dashboard settings session:
|
|
1913
|
+
- `instances.<id>` with `kind`, `roles`, `start_mode`, `review_mode`, `readOnly`, `worktree` and `permission_mode`, in `config.local.json`;
|
|
1914
|
+
- `review.adaptive`, `review.min_reviews`, `review.pool`, `review.rerotate`, `snapshots.keep`, `approvals.timeout_s` and `context.gate`, in `config.local.json`;
|
|
1915
|
+
- `mode` and `required_checks` in `.agenthub/sdlc.toml`. Only those two lines change: comments and every other table stay as written, and a layout the editor cannot change faithfully (a key repeated, or one inside a multi-line string) is refused with its own reason.
|
|
1916
|
+
|
|
1917
|
+
`roster.*`, `tasks.wip_limits.*`, `permission_modes`, `peers` and the rest of the registry keep their own `ahub settings set` path.
|
|
1918
|
+
3. **Executable and secret-locating keys** are console only: the rest of `NEVER_EDITABLE` (`kimi_cmd`, `jcode_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx`, `omniroute`, `terminal`, `adapters`, `adapter_modules` and the like) and the Jcode instance fields `jcode_home`, `provider`, `model` and `provider_profile`. The console shows "this runs code with the daemon's rights, or chooses where secrets are read from" and writes only when the key's exact name is typed, one key at a time; `--yes` never confirms one. The dashboard lists each of them with the console command that changes it and nothing else: no value, no path, no input. A forged dashboard request for one is refused by the hub.
|
|
1919
|
+
|
|
1920
|
+
In the console (`ahub console`, Settings tab `6`): `c` asks what to edit (`1` an instance, `2` the lifecycle, `3` another key), asks each field, shows the hub's diff or its loader reason, asks for a reason, then `y`. Esc cancels at any step, and pasted text never confirms. `R` appears while a restart is pending and prefills `restart`; the command still asks its own `y`. In the dashboard, open `ahub ui --settings`: the Structure group has the same forms; outside a settings session it is read only.
|
|
1921
|
+
|
|
1922
|
+
```bash
|
|
1923
|
+
ahub settings edit config.local.json set instances.codex-review '{"kind":"codex","roles":["reviewer"],"start_mode":"headless","review_mode":"fresh"}' --reason "add the fresh reviewer"
|
|
1924
|
+
ahub settings edit sdlc.toml set mode '"enforce"' --preview
|
|
1925
|
+
ahub settings edit config.local.json remove review.pool --yes
|
|
1926
|
+
ahub settings backups # the newest backups of each file
|
|
1927
|
+
```
|
|
1928
|
+
|
|
1929
|
+
Rules:
|
|
1930
|
+
- A missing `start_mode` on the fresh `codex-review` reviewer shows the loader's reason (`fresh requires reviewer-only roles, headless mode ...`), names the field and writes nothing.
|
|
1931
|
+
- The edit carries the revision (a sha256, or `absent`) of the file it was read from. The hub checks it when it plans the edit and again immediately before it writes, so a file changed on disk since is refused with a hint to reload and review again.
|
|
1932
|
+
- The previous file is kept in `<state dir>/config-backups/` (the newest five of each file; `ahub settings backups` lists them) and the new one replaces it atomically. A `config.local.json` that git tracks is never written. Unknown fields and unknown keys are refused by name, never dropped.
|
|
1933
|
+
- A whole instance that holds a native home, account or model (`jcode_home`, `provider`, `model`, `provider_profile`) is tier 3 to remove or replace, so a dashboard can neither see nor delete those values; an existing instance changes leaf by leaf (`instances.<id>.<field>`), and `clear` removes one field. A preview or write shows a tier 3 value only as `(set)`, and a write changes exactly one tier 3 key, counting the keys the file adds (removing two Jcode instances is two writes, each confirmed by its own typed name). The audit record gives a tier 3 key no value hash at all, because a model or provider name is easy to recover from an unsalted hash; the file revisions bind the change instead. An instance already in `config.local.json` is changed leaf by leaf even before a restart has started it. If `.agenthub` or `config.local.json` is a symlink, the Settings view reports its revision as `unavailable` and no edit is planned. An edit of `sdlc.toml` keeps the file's own line endings.
|
|
1934
|
+
- Every write records a `config_edit` event: the file, each key path with a short hash of its value before and after (never a value), the file revisions before and after, the tier, the source (`dashboard` or `terminal`) and `by: person`. The notice and `hub.log` name key paths only.
|
|
1935
|
+
- A changed instance declaration is restart-bound: it is listed as `pending restart` (`instances`) and takes effect at the next hub start. A changed `sdlc.toml` is read once at start and is listed as `sdlc.mode` or `sdlc.required_checks` until the running hub holds the same value. `review`, `snapshots`, `approvals` and `context.gate` publish through the one reload path, one key at a time.
|
|
1936
|
+
- Person only: an agent shell or a hub tool is refused (`ahub settings` is a console command in the identity gate, and the three control requests answer only the console role). Recovery or shutdown holds every edit.
|
|
1937
|
+
- The hub must run this release: an older hub answers `unknown message` and the command says to upgrade and restart it.
|
|
1938
|
+
|
|
1894
1939
|
## Start modes: TUI by default, headless as opt-in
|
|
1895
1940
|
|
|
1896
1941
|
A peer that has a TUI the hub attaches to starts in it by default (issue #269).
|
|
@@ -3328,9 +3373,12 @@ ownership, bind servers or start agents, sidecars or models.
|
|
|
3328
3373
|
`ahub status`, `ahub tail` and the dashboard show native context occupancy,
|
|
3329
3374
|
source and measurement freshness. Claude readings come from the status-line
|
|
3330
3375
|
tee installed by `ahub claude`; Codex readings come from its current thread's
|
|
3331
|
-
native token-usage updates. Kimi
|
|
3332
|
-
(`{used, size}`, source `acp_usage_update`)
|
|
3333
|
-
|
|
3376
|
+
native token-usage updates. Kimi's context occupancy comes from its ACP `usage_update`
|
|
3377
|
+
(`{used, size}`, source `acp_usage_update`), not consumption, so no token total is counted from it. Its consumption is counted from its
|
|
3378
|
+
session wire log (`<kimi home>/sessions/.../agents/*/wire.jsonl`, #285 phase 2) and reported as the `tokens` window of
|
|
3379
|
+
`budget.kimi_tokens_5h` (source `hub count, kimi wire log, of kimi_tokens_5h N`; cache reads are not counted); an unreadable log shows
|
|
3380
|
+
unknown with one notice, never a short count. Until the OAuth probe or `ahub budget set kimi ...` feeds the plan quota, the provider's
|
|
3381
|
+
own 5 h and weekly ratios stay unknown. Not yet verified live against Kimi's `/usage` (#285 AC11); the adapter accepts the
|
|
3334
3382
|
update whenever it names the current session, because 2.1.1's source text
|
|
3335
3383
|
emits it after the turn settles (read from the source, not yet observed live
|
|
3336
3384
|
through the hub; docs/smoke.md), so the peer may already be idle.
|
|
@@ -3642,6 +3690,8 @@ A bundled adapter module is authorized by the person with `adapter_modules` (at
|
|
|
3642
3690
|
# Acceptance-criteria checklist (#481)
|
|
3643
3691
|
|
|
3644
3692
|
- The task reviewer/person records a head-bound `criteria` receipt with criterion ids matching the issue's AC ids, required/result fields and hashed evidence refs. This checklist belongs to the Compliance review pass. Use `ahub sdlc record <id> '<receipt JSON>'`; [schema and example](sdlc.md#acceptance-criteria-checklist).
|
|
3693
|
+
- Deliverables (#509a, docs/sdlc.md, Deliverable records): `hub_task_done` and `hub_sdlc_record` take `deliverables: [{path, format, version?}]`; the hub hashes the files itself (a claimed hash is refused), in a plain folder with no git or PR, under the owner's execution root (its own worktree for a declared worktree instance; an unproven root refuses and reads `unknown`), and a reviewer's approval is refused with `deliverable changed since it was delivered` when a file changed since delivery (the owner calls `hub_task_done` again, or the person approves the current bytes). `ahub task show <id>` shows each file's state (`unchanged`, `changed`, `missing`, `unknown`) and `changedSinceReview`; a PII task or `pii.paths` file shows id, hash and size only.
|
|
3694
|
+
- The review request lists the recorded criteria with their ids and results, and a `hub_review` unmet item that starts with a recorded id (`AC2: ...`) is recorded on the verdict as `unmetCriteria` (#507b); `ahub task show` lists those ids in the history. A task with no checklist, and a PII task, are unchanged.
|
|
3645
3695
|
- Advise shows incomplete/stale checklist blockers; enforce checks them at approval. The `criteria` gate does not need a workflow selector. Dependencies stay with `ahub task propose --after`; checklist task import is removed.
|
|
3646
3696
|
- `status <id>`, `explain <id>` and `export <id>` use the same current board/config projection. Export keeps sanitized state and source hashes, with no external text, extra authority or verification claim.
|
|
3647
3697
|
- Migration from the retired workflow and its compatibility window (until hub 0.13.0) are described in [Retired workflow compatibility](sdlc.md#retired-workflow-compatibility-252-481-s8). Stored checklist task lists are ignored with a visible note; they never change board dependencies.
|
|
@@ -3747,7 +3797,7 @@ are described below. Native planner-cycle evidence remains unverified.
|
|
|
3747
3797
|
|
|
3748
3798
|
## Planning and phased releases (#263)
|
|
3749
3799
|
|
|
3750
|
-
Use `ahub backlog list` to read public staged work, then `ahub backlog plan <id>
|
|
3800
|
+
Use `ahub backlog list` to read public staged work (a table by rank with the reserved owner, dependencies and cut titles, then a count by reserved owner; `--full` and piped output keep whole titles and `--json` prints the raw listing), then `ahub backlog plan <id>
|
|
3751
3801
|
<revision> <fields-json> <reason>` to set rank, priority (0-3), release id, planned
|
|
3752
3802
|
start/due UTC milliseconds or pin. `pin` accepts true/false; `undo` needs the
|
|
3753
3803
|
current revision and latest eligible edit. The dashboard provides local-time
|
|
@@ -4134,8 +4184,19 @@ An extra Claude instance is one you start yourself: declare it with `roles` (for
|
|
|
4134
4184
|
terminal. It keeps its own `claude-*.claude-review.json` state files. A planner or reviewer instance reads the
|
|
4135
4185
|
project checkout and refuses `worktree`. From #525-1 an `implementer` or `verifier` instance is accepted and
|
|
4136
4186
|
requires its own `worktree` (for example `"claude-impl": { "kind": "claude", "roles": ["implementer"],
|
|
4137
|
-
"worktree": ".worktrees/claude-impl" }`).
|
|
4138
|
-
|
|
4187
|
+
"worktree": ".worktrees/claude-impl" }`). `ahub claude --as claude-impl` validates that registered branch worktree and starts Claude there while keeping its canonical hub connection. Its private launch record stores the execution root. The channel reports its own canonical cwd (verified model-free with Claude 2.1.309); the daemon grants owner isolation only when that root, the current launch id, the recorded root and its own worktree validation all agree. Missing, legacy or mismatched evidence still permits a channel attachment, but write-owner routing is refused with `owner admission: declared write owner requires a verified isolated execution root`. Status shows the isolation refusal. A detach, replacement claim or launch-record change invalidates the proof. Facts and permission grants use the verified execution root; an unverified implementer gets no automatic grant. Recovery records retain that execution root, checked against the current declaration, separately from the Orca grouping root. This is same-user launch evidence, not kernel filesystem isolation. A Claude instance holding only planner or reviewer roles still uses the canonical checkout. A headless Claude reviewer retains its reviewer-only declaration. A headless implementer is available with the separate declaration below (#525-4).
|
|
4188
|
+
|
|
4189
|
+
A headless Claude implementer (#525-4) is declared separately:
|
|
4190
|
+
|
|
4191
|
+
```json
|
|
4192
|
+
"claude-impl": { "kind": "claude", "roles": ["implementer"], "start_mode": "headless", "worktree": ".worktrees/claude-impl", "permission_mode": "ask" }
|
|
4193
|
+
```
|
|
4194
|
+
|
|
4195
|
+
Create and register the attached-branch worktree first; the hub never creates it implicitly. `ahub claude --as claude-impl` asks the hub to start the instance and returns. Idle holds no native child. Task deliveries use a persisted UUID per task: first `--session-id`, later `--resume`. Unscoped chat reuses a session only when exactly one task is in progress for the instance; otherwise it is ephemeral. Positive missing-transcript evidence starts fresh with a notice; unknown read evidence retains the id. Reset includes the per-peer session map. A child inbox read refuses a queued batch attributed to another task and leaves it queued for that task's own session.
|
|
4196
|
+
|
|
4197
|
+
Both permission modes use native `default`, managed settings/hooks and the instance's strict tools-mode MCP binding. `ask` routes writes and shell calls to the person's approval. `ask-when-needed` may explicitly allow safe project file tools through the facts hook in the execution root; protected configuration, paths outside that worktree and shell/git writes still ask. There is no `acceptEdits` or `never-ask` bypass. A failed hook decides nothing and may be followed by a valid person allow. Prompt failure, timeout, disconnect and a stale child binding never grant permission. Approvals use the existing console and `approvals.timeout_s`; no answer returns deny. A stop holds uncertain delivery until the owned process group is confirmed gone; a failed cleanup keeps the peer busy.
|
|
4198
|
+
|
|
4199
|
+
The [native permission receipt](verification/2026-10-23-headless-claude-permissions.md) covers Claude Code 2.1.309 with a deterministic loopback fixture and no real account/model call. It is not sandbox proof or a runtime receipt gate. The real account/model/person interaction remains a separate live leg.
|
|
4139
4200
|
|
|
4140
4201
|
A headless Claude reviewer is run by the hub instead (#223): declare
|
|
4141
4202
|
`"claude-review": { "kind": "claude", "roles": ["reviewer"], "start_mode": "headless" }`
|
|
@@ -4236,8 +4297,19 @@ reattachment or TUI detachment clears it. The notice contains fixed guidance, wi
|
|
|
4236
4297
|
|
|
4237
4298
|
## Roles per SDLC stage (#525)
|
|
4238
4299
|
|
|
4239
|
-
The design for planner, implementer and reviewer instances per stage is in the spec section "Amendment: roles per SDLC stage (#525)". Slice 525-1 is available: role capability per kind and form, Claude channel instances as implementer or verifier in their own worktree, and `planner` refused on kinds that cannot plan (pi, local
|
|
4300
|
+
The design for planner, implementer and reviewer instances per stage is in the spec section "Amendment: roles per SDLC stage (#525)". Slice 525-1 is available: role capability per kind and form, Claude channel instances as implementer or verifier in their own worktree, and `planner` refused on builtin kinds that cannot plan (pi, local and agy): an instance declaring it is refused with `this kind cannot plan`, and a `roles` entry keeps its other roles with the notice `roles.<peer>: ignored roles outside the adapter ceiling`. A configured ACP agent (`adapters`) is different: it may hold `planner` in its `roles` or in a `roles` entry and then plans like any peer, but it can never be pinned with the `planner` setting or chosen as the automatic planner, because it has no host projection (#735). Slices 525-3, 525-4 and 525-5 are available too:
|
|
4301
|
+
|
|
4302
|
+
- 525-3: `assign()` walks the planner holders (rotation by task id over every holder, never renumbered by availability; a named planner exact; a recorded planner kept; none for a PII task) and `ahub route explain <id>` prints that walk. Nothing records the planner yet, so the walk is information only. A peer that accepted or finished a task never reviews it, a named reviewer and the declared reviewer pool included; when no other candidate is left the person reviews.
|
|
4303
|
+
- 525-4: a headless Claude implementer runs one owned child per turn in its declared worktree, with a separate persisted session per task (`start_mode: "headless"`, permission `ask` or `ask-when-needed`; `never-ask` refused).
|
|
4304
|
+
- 525-5: terminal Claude implementer instances own write-class tasks once the daemon has verified their execution root.
|
|
4305
|
+
|
|
4306
|
+
The operator steps for 525-1, 525-4 and 525-5 are in "Additional headless peer instances" above; each later slice (525-2, 525-6) adds its own here when it ships, and until then those instance rules are current. The planned changes an operator will see:
|
|
4240
4307
|
|
|
4241
|
-
- Code tasks routed to a terminal Claude implementer instance once its execution root is proven (525-5), and a headless Claude implementer with `start_mode: "headless"` (`permission_mode` `ask` or `ask-when-needed`; `never-ask` refused) (525-4).
|
|
4242
4308
|
- The `planner` setting accepts a list of peers or instance ids; each task records its own planner, and only that planner or the person runs planner tools on it.
|
|
4243
|
-
- A
|
|
4309
|
+
- A plan's author does not record its own plan review while another planner is available.
|
|
4310
|
+
|
|
4311
|
+
## Daemon responsiveness and launch connects (#719)
|
|
4312
|
+
|
|
4313
|
+
Periodic peer-roster and delivery-supervision reads select pending journal metadata/rows, excluding completed payload history. Queue revision reads do not construct a recovery snapshot. Explicit queue/history commands retain terminal history. The pending-state index supports these reads without sorting historical payloads.
|
|
4314
|
+
|
|
4315
|
+
Native launcher connections retry transport timeouts/unclean closes with bounded backoff for at most 15 seconds, printing `hub busy, retrying` once. The ordinary control-connect timeout remains 3 seconds. An immediately unreachable listener, authentication refusals, wire mismatch and project/instance mismatch are terminal. Every retry rereads the descriptor and preserves the invocation's delegated-audit instance pin. Usage queries retain their existing catch-up budgets and report `behind` until complete; time slicing never advances beyond the last atomically folded record.
|
package/docs/profiles.md
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
# Project profile overrides
|
|
51
51
|
|
|
52
|
+
- Acceptance criteria are profile-independent (#507b): a task's recorded criteria checklist (docs/sdlc.md, Acceptance-criteria checklist) is listed in every profile's review request, ahead of the profile's `review_checklist` lines, and an unmet item that starts with a criterion id names it; a profile only changes the wording of its checklist, never the criteria or their ids.
|
|
52
53
|
- `profile_overrides` in shared/local config selects project data over the chosen shipped profile. `classes`, `role_text` and `catalog_roles` maps merge by key across layers; `plan_keys`, `agents`, `example_checks` and `review_checklist` replace as units. A local `profile` selects the active base for both init and native contracts.
|
|
53
54
|
- Classes may add or remap profile aliases to existing base classes. Existing base names keep their meanings; class maps are bounded to 32 override entries. Task storage and route explanations preserve the alias, including a dev custom alias, while routing/budgets/checks use its base. Other tasks under custom dev data retain the selected profile semantics too, so document sections do not become whole-path overlaps. Unmodified dev rows retain their old shape.
|
|
54
55
|
- `plan_keys` selects distinct existing plan fields, not new board fields. Task proposals and their schemas use the selection. Accept schemas retain legacy fields for already existing tasks/profile changes.
|
package/docs/quickstart.md
CHANGED
|
@@ -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.
|
|
20
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.15.0
|
|
21
21
|
ahub setup
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -30,9 +30,13 @@ 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.15.0
|
|
34
|
+
|
|
35
|
+
Publish, external-write, destroy and install calls now ask a person (#636 slice 2): the Claude PreToolUse hook returns `ask` for such a call in every permission mode, `never-ask` included, and a hub-owned ACP peer such as Kimi in native `ask` mode never auto-approves one, not under `--unattended` either; it waits in the console and the dashboard as `Action approval (<classes>): <command>` and is denied when `approvals.timeout_s` passes. When a class budget is above one, the approval of a classed ACP call also offers `Allow <class> xN for this session`. Unattended work that publishes, installs or deletes now stops for you: answer it, or set `ahub settings set action_policy.enabled false` or `action_policy.budgets.<class> <n>` (the policy and its budgets live only in untracked `.agenthub/config.local.json`). `ahub status` names each peer where the hub cannot see the call before it runs: a Kimi in `yolo` or `auto`, a Claude session that has not run the hub's permission hook, an `--unattended` Claude launch, and Codex, Pi and the local worker until #313 (see [always-ask action policy](operations.md#always-ask-action-policy-foundation-636)). A terminal Claude instance declared with `implementer` or `verifier` and its own `worktree` now owns write-class tasks once the daemon verifies its launch record and channel cwd; start it again with `ahub claude --as <id>` after the upgrade, because a launch recorded without its execution root attaches without isolation and gets no write task (#525-5). A headless Claude implementer can be declared in `.agenthub/config.local.json` (`"start_mode": "headless"`, roles `["implementer"]`, its own registered `worktree`, `permission_mode` `ask` or `ask-when-needed`; `never-ask` is refused): create and register the worktree first, then `ahub claude --as <id>` asks the hub to start it and returns. It keeps one session per task, and its writes and shell calls reach you as allow-once or deny requests in the console; no answer is a deny (#525-4; see [additional headless peer instances](operations.md#additional-headless-peer-instances)). `hub_task_done` and `hub_sdlc_record` take `deliverables: [{path, format, version?}]`: the hub hashes the files itself, a reviewer's approval is refused when a file changed since delivery, and `ahub task show` reports each file as `unchanged`, `changed`, `missing` or `unknown` (#509; see [deliverable records](sdlc.md#deliverable-records-509a)). A review request lists the task's recorded acceptance criteria by id, and an unmet item that starts with one (`AC2: what is missing`) is recorded on the verdict (#507). A peer that accepted or finished a task no longer reviews it; when no other reviewer is left, you review it, and `ahub route explain <id>` also prints the planner walk (#525-3). Instances, `review`, `snapshots.keep`, `approvals.timeout_s`, `context.gate` and the `sdlc.toml` `mode` and `required_checks` can be edited with `ahub settings edit`, the console Settings key `c` or the Structure group of an `ahub ui --settings` session: the hub validates, shows a diff and keeps a backup; a changed instance or `sdlc.toml` waits for a hub restart (console `R`), and keys that run code or locate secrets stay console only with their name typed (#689; see [structural configuration](operations.md#structural-configuration-from-the-console-and-the-dashboard-issue-689)). Kimi's token use is now counted from its session wire log, so a `budget.kimi_tokens_5h` you set pauses Kimi when it is reached (#285). `ahub backlog list` prints a table by rank; a script that read its output uses `--json` (#399). A launcher that meets a busy hub retries for up to 15 s and prints `hub busy, retrying` once (#719). Upgrade running hubs with `ahub upgrade` before relying on deliverables: an older daemon drops them from a newer plugin's `hub_task_done` without a word. Restart Claude sessions so they load the 0.15.0 plugin, and if `ahub status` shows a fresh reviewer instance such as `codex-review` stopped after the upgrade, start it again with `ahub codex --as codex-review`. Coming from 0.13.0 or older, the notes below apply too.
|
|
36
|
+
|
|
33
37
|
## Upgrading to 0.14.0
|
|
34
38
|
|
|
35
|
-
At a terminal width `ahub board` now prints one line per task: the title is cut with `...`, dependencies move to an AFTER column, STAGE is the meter with a short next-step code (`intent: issue?`, `review: CI?`, `ready`) and a legend line explains them; `ahub board --full`, piped output without `COLUMNS` and `--json` keep the whole title and SDLC advice (#724). The dashboard's Agents view gains a Usage view under Usage budgets: lifetime totals, the last 14 UTC days of tokens per peer, the open pauses and recent limit events, and a task table in which a private task shows only as `#id [pii]` (#683 U4). A task sheet gains a Code group: the changed symbols marked `matching` or `unplanned` against the plan, the plan names nothing changed under as `untouched`, and a symbol opens the Memory tab on its node; a private task has no group (#684). `ahub sdlc metrics [--since <date>]` and the planner tool `hub_sdlc_metrics` (planner or person, read-only) report lead times, first-pass approval, rework rounds, stale-head re-reviews and stalls from board history, with `unknown` where the board cannot tell. An optional `[bands.<metric>]` table in `.agenthub/sdlc.toml` turns a breach into a hub notice, a read-only `triage` backlog task or a `plan` backlog card, once per episode, only while `mode` is `advise` or `enforce`, and never a GitHub issue (#481 S7; see [SDLC metrics and control bands](sdlc.md#sdlc-metrics-and-control-bands-481-s7)). In advise and enforce a task's issue identity is now linked when it is proposed or accepted, with no GitHub read; a title that names another repository (`agent-mem#13`) links only when `refs.repo` names it, and the propose result suggests setting it (#723). A fresh reviewer whose start fails now names a cause class (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout` or `unknown`) on the `reviewer` line of `ahub status` and in `hub.log` (#722). A Claude channel instance in `.agenthub/config.local.json` may declare `implementer` or `verifier` and then needs its own `worktree`, while a planner or reviewer instance refuses one; the hub routes no code tasks to such an instance until #525-5, and a headless Claude instance stays reviewer-only. `planner` is refused on a pi, local or agy instance declaration and dropped, with a notice, from a `roles` entry for those kinds (#525-1; see [Roles per SDLC stage](operations.md#roles-per-sdlc-stage-525
|
|
39
|
+
At a terminal width `ahub board` now prints one line per task: the title is cut with `...`, dependencies move to an AFTER column, STAGE is the meter with a short next-step code (`intent: issue?`, `review: CI?`, `ready`) and a legend line explains them; `ahub board --full`, piped output without `COLUMNS` and `--json` keep the whole title and SDLC advice (#724). The dashboard's Agents view gains a Usage view under Usage budgets: lifetime totals, the last 14 UTC days of tokens per peer, the open pauses and recent limit events, and a task table in which a private task shows only as `#id [pii]` (#683 U4). A task sheet gains a Code group: the changed symbols marked `matching` or `unplanned` against the plan, the plan names nothing changed under as `untouched`, and a symbol opens the Memory tab on its node; a private task has no group (#684). `ahub sdlc metrics [--since <date>]` and the planner tool `hub_sdlc_metrics` (planner or person, read-only) report lead times, first-pass approval, rework rounds, stale-head re-reviews and stalls from board history, with `unknown` where the board cannot tell. An optional `[bands.<metric>]` table in `.agenthub/sdlc.toml` turns a breach into a hub notice, a read-only `triage` backlog task or a `plan` backlog card, once per episode, only while `mode` is `advise` or `enforce`, and never a GitHub issue (#481 S7; see [SDLC metrics and control bands](sdlc.md#sdlc-metrics-and-control-bands-481-s7)). In advise and enforce a task's issue identity is now linked when it is proposed or accepted, with no GitHub read; a title that names another repository (`agent-mem#13`) links only when `refs.repo` names it, and the propose result suggests setting it (#723). A fresh reviewer whose start fails now names a cause class (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout` or `unknown`) on the `reviewer` line of `ahub status` and in `hub.log` (#722). A Claude channel instance in `.agenthub/config.local.json` may declare `implementer` or `verifier` and then needs its own `worktree`, while a planner or reviewer instance refuses one; the hub routes no code tasks to such an instance until #525-5, and a headless Claude instance stays reviewer-only. `planner` is refused on a pi, local or agy instance declaration and dropped, with a notice, from a `roles` entry for those kinds (#525-1; see [Roles per SDLC stage](operations.md#roles-per-sdlc-stage-525)). A planner pull of a reserved backlog task now assigns it to the reserved peer and offers it at once; when that peer is offline, off roster, paused, full or excluded, the pull is refused with the reason and no other peer takes it (#718). `ahub codex --as <id>` for a headless fresh reviewer instance starts it and returns (#720). Restart Claude sessions so they load the 0.14.0 plugin. Coming from 0.12.45 or older, the notes below apply too.
|
|
36
40
|
|
|
37
41
|
## Upgrading to 0.13.0
|
|
38
42
|
|
|
@@ -148,6 +152,7 @@ or set `OMNIROUTE_API_KEY`. The `.agenthub/routing.toml` that `ahub init` writes
|
|
|
148
152
|
| `ahub task cancel <id> --reason <text>` | close obsolete work without approving it or stopping a native turn |
|
|
149
153
|
| `ahub settings set planner <peer>` | pin the planner; status reports whether it came from settings, roles or the automatic choice |
|
|
150
154
|
| `ahub settings reload` | apply edited config files without a restart; guarded values wait for your `y` in this terminal, and restart-only keys stay pending |
|
|
155
|
+
| `ahub settings edit <file> set <path> <json>` | change an instance, the review block or the `sdlc.toml` lifecycle with a validated diff and a backup; `ahub settings backups` lists the backups |
|
|
151
156
|
| `ahub status` | peers, queues, pauses; `ahub budget` for quota windows |
|
|
152
157
|
| `ahub usage [--by peer\|task\|issue\|pr\|release\|day\|model]` | tokens per group, each number with its source; `ahub usage limits` for pauses and resets |
|
|
153
158
|
| `ahub agents` | which installed agent CLI versions the hub accepts; `ahub agents trust <id> <version>` accepts an untested one on this machine |
|
|
@@ -178,7 +183,8 @@ allow. They can be denied on the page. PII task details remain available only vi
|
|
|
178
183
|
`ahub task show <id>` in the terminal. The page also starts a peer in its start
|
|
179
184
|
mode (a TUI opens in a terminal when the hub has a terminal provider) and shows
|
|
180
185
|
the settings with a Reload configuration button; raising one needs `ahub ui --settings`
|
|
181
|
-
or `ahub settings` in a terminal
|
|
186
|
+
or `ahub settings` in a terminal (the Structure group edits instances and the
|
|
187
|
+
lifecycle only in such a session), and guarded values from an edited file need
|
|
182
188
|
`ahub settings reload` there. See the operations guide, "Settings from the dashboard and the
|
|
183
189
|
terminal" and "Start modes".
|
|
184
190
|
|
package/docs/sdlc.md
CHANGED
|
@@ -86,6 +86,7 @@ A person records ids and a claimed local receipt with `ahub sdlc record 12 '{"re
|
|
|
86
86
|
- A checklist names a bounded slug `id`, a full `head` and 1..32 criterion records. Each criterion has a bounded `id`, boolean `required`, `result` verified|failed|unverified and up to eight hashed refs. Accepted refs are `sha256:<64-hex>`, or `artifact|check|source:<ASCII-reference>@<64-hex>`. Whole receipt JSON is at most 32 KiB. The hub never fetches those refs or treats a model claim as a deterministic check. Verified outcomes are reviewer claims, separately labelled from fresh GitHub checks.
|
|
87
87
|
- Required failed/unverified criteria and missing/stale checklist heads keep readiness false. Advise/off preserve existing transition behavior. Enforce adds criteria blockers only at approval. Entering review remains possible with unverified criteria so the reviewer can inspect and record them. Criteria claims do not replace exact-head local/CI checks.
|
|
88
88
|
- Once recorded, a checklist adds its blockers in any enabled lifecycle; off ignores it, as #257 did. The `criteria` entry/exit gate uses the same checklist without a workflow selector, and a missing checklist is a blocker the reviewer clears by recording one. Task dependencies remain with `Tasks.after`, created through `ahub task propose --after`. Checklist task import is removed.
|
|
89
|
+
- A task's acceptance criteria are this checklist: there is no second criteria model (#507b). The review request lists the recorded criteria (`- <id>: <result>`, `(optional)` for a non-required one) ahead of its checklist, so the reviewer sees the ids the task is judged by; a PII task lists none. A `hub_review` unmet item that starts with a recorded id (`AC2: the overwrite case has no test`, or the bare id) names that criterion: the hub records the ids it matched on the verdict as `unmetCriteria` (history and the public view), read from the board's checklist at the verdict and never from the caller. An id the checklist does not hold, the wrong case, a longer word that merely starts with an id and an id in the middle of a sentence are plain text, and the note text is unchanged. These ids are what later review packing and learning refer to.
|
|
89
90
|
- `ahub sdlc status <id>`, `explain` and console/dashboard projections show the same criteria view. `ahub sdlc export <id>` keeps the #257 schema-1 bytes in plain off (`workflow: "none"`, `phase: null`); an enabled or retired-setting lifecycle emits schema 2, with sanitized state, criteria/hashes and `stateHash`/`sourceHash`: the exact projection hash and normalized effective lifecycle-configuration hash. These are derived hashes, not code/build attestations or import authority. Private exports contain a stub and no checklist refs.
|
|
90
91
|
|
|
91
92
|
Example reviewer receipt:
|
|
@@ -329,3 +330,19 @@ For every actual pushed `v*` destination, the hook requires an exact semantic ve
|
|
|
329
330
|
Before other release commands, agents run `ahub sdlc release-guard -- <literal argv...>`. Supported forms are one named-remote tag push, `gh release create v<version>` (an existing GitHub tag is read back; a new one requires `--target <full-sha>`), and `npm publish` from the approved checkout. Ambiguous selectors, alternate repositories, bulk pushes, workspaces and archive publishes refuse. The npm preflight refuses tracked worktree changes before and after rechecking the committed-doc verdict and HEAD, but does not attest the eventual tarball, npm configuration or lifecycle scripts. The command performs no release operation and its answer is not an execution grant. GitHub/npm enforcement remains instruction-based until adapter integration (#312/#313); rerun preflight after changing any target or candidate. Native install, live release and migration evidence remain separate obligations.
|
|
330
331
|
|
|
331
332
|
Tracked npm cleanliness compares the cached index to HEAD and raw tracked blob bytes without `git status`, clean/process filters or text converters. It refuses unknown evidence, gitlinks, transformed files and inventories above 4,096 entries, 4 MiB per file, 32 MiB total or 250 ms; untracked files remain outside this check and the eventual tarball is not attested.
|
|
333
|
+
|
|
334
|
+
## Deliverable records (#509a)
|
|
335
|
+
|
|
336
|
+
A report, translation or slide deck has no commit, so the artifact identity is the file's own content hash, recorded by the hub.
|
|
337
|
+
|
|
338
|
+
- `hub_task_done` and `hub_sdlc_record` (inside `receipt`) accept `deliverables: [{path, format, version?}]`, one to sixteen entries. `path` is project-relative with forward slashes; `format` is a lowercase label (`md`, `pdf`, `docx`, `csv`); `version` is an optional short label. There is no hash field: **a claimed `sha256`, `hash` or `bytes` is refused**, and so is any field the hub does not own.
|
|
339
|
+
- The hub opens and hashes each file itself. The path is resolved with `realPath` and checked with `guardPath` in read mode: outside the project, on the secrets denylist (`local.deny`), through a dangling link, inside `.git` or `.agenthub` (the hub's own state), not a regular file, missing, over 256 MiB each or 512 MiB together are refused, with the path as given and never a resolved one. The file is opened without following a final link and its device and inode are compared with what was checked, so a path swapped in between is refused; a file that changes while it is read is refused, never recorded half-read. Reading is chunked with an await between chunks, so a large file does not stall the daemon.
|
|
340
|
+
- A bad call is refused whole, before any board write: no partial record, and the task stays where it was. Each record stores `id` (an opaque id from path and format, stable across edits), `path`, `format`, `version`, the real `sha256` and the size.
|
|
341
|
+
- **Where the files are read.** Paths are relative to the owner's execution root: the project checkout for an ordinary owner, the instance's own verified worktree for a declared worktree instance (the place it writes). A root the host cannot prove refuses the call, and a read of such a task reports every file `unknown`, never `unchanged`; an approval is refused for the same reason.
|
|
342
|
+
- It works in a plain folder. Nothing needs git, a PR, a shadow repository, a snapshot, facts or a lifecycle receipt (a task with no `sdlc` receipt records and approves deliverables).
|
|
343
|
+
- **Approval binds the bytes.** When a reviewer approves, the hub reads the files again. Unchanged, the approval records the same hashes. Changed, missing or unreadable, the approval is refused before any write with `deliverable changed since it was delivered: <id> (<state>)`; the owner calls `hub_task_done` again so the current files are reviewed. `changes_requested` needs no binding. The person may approve files that moved: the current bytes are measured and bound, and the verdict note says the files had changed since delivery.
|
|
344
|
+
- **Later edits.** `hub_task_show` and `ahub task show` add `deliverables: { state: "delivered" | "approved", changedSinceReview, files: [{ id, sha256, state: "unchanged" | "changed" | "missing" | "unknown", ... }] }`, read from the files as they are now against what was approved (or, before approval, delivered). `changedSinceReview` is true only for a file measured to differ. A file the hub cannot read or vouch for (denied, outside, replaced by a link out, too large) is `unknown`, never `unchanged`.
|
|
345
|
+
- **Private records.** For a PII task, or a file under `pii.paths`, the record is private: every peer view, review request, status and log shows the id, hash and size only, `(path withheld)` in the request. The board keeps the path so the file can be found again, as it keeps the rest of a PII task's text. A path that matches a PII pattern is withheld the same way.
|
|
346
|
+
- `hub_sdlc_record` with `deliverables` records them on a `lifecycle` entry, through the same hashing and the same owner, reviewer or console authority; a receipt without them is the old call.
|
|
347
|
+
- Limit: a daemon from before this slice ignores `deliverables` on `hub_task_done` (the plugin is installed apart from the daemon), so restart the hub after an upgrade before relying on it; a done that names deliverables and shows no `Deliverables` block in its review request was recorded without them.
|
|
348
|
+
- Not in this slice: `ahub deliverables [--task <id>]` and the dashboard column (#509 AC3, the next slice), and a hash of a file the hub cannot read.
|
package/docs/security.md
CHANGED
|
@@ -6,9 +6,9 @@ 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. 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.
|
|
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 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`, `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
|
-
- **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.
|
|
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.
|
|
@@ -59,11 +59,31 @@ is refused before write/edit effects.
|
|
|
59
59
|
Kimi executes its own tools outside that sandbox; Claude and Codex retain their
|
|
60
60
|
vendor boundaries. The never-ask confirmation names this difference. No mode
|
|
61
61
|
turns off the sandbox or grants a session-wide allow-always option.
|
|
62
|
-
The always-ask action policy (#636)
|
|
63
|
-
|
|
64
|
-
`.agenthub/config.local.json
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
The always-ask action policy (#636) classes a call as publish, external-write,
|
|
63
|
+
destroy or install; `action_policy` is read only from an untracked
|
|
64
|
+
`.agenthub/config.local.json`. Since slice 2 two adapters enforce it. Claude's
|
|
65
|
+
managed PreToolUse hook returns `ask` for a classed call in every permission
|
|
66
|
+
mode, `never-ask` included, and that `ask` replaces any allow the mode would
|
|
67
|
+
add; the hook never denies, and a hub that is down, slow or refusing decides
|
|
68
|
+
nothing, which leaves Claude's own policy. A hub-owned ACP peer's
|
|
69
|
+
`session/request_permission` for a classed call is never auto-approved (a
|
|
70
|
+
hub-tool title, a mode grant and `--unattended` included), offers no
|
|
71
|
+
allow-always, shows `Action approval (<classes>): <command>` built from the
|
|
72
|
+
task's public view and the PII screen (withheld for a private turn or an open PII
|
|
73
|
+
task), and is denied when the approval timeout passes. A class budget above one
|
|
74
|
+
adds one person-only choice that allows at most N calls of those classes in the
|
|
75
|
+
same native session; a new session, a changed policy or a stopped, crashed or
|
|
76
|
+
restarted peer starts from zero, and an ordinary allow never spends it. The
|
|
77
|
+
limits are the point of the visibility rule: the hub enforces only a call it
|
|
78
|
+
sees before it runs. A Claude session without the hub hook or not yet proven to
|
|
79
|
+
have run it, an `--unattended` Claude launch (it skips hooks), an ACP agent in a
|
|
80
|
+
native mode that sends no request for a call it allows (Kimi's `yolo` and
|
|
81
|
+
`auto`), a call whose payload the agent did not report, and Codex, Pi and the
|
|
82
|
+
local worker until slice 3 (#313) are not enforced, and `ahub status` says
|
|
83
|
+
`always-ask not enforceable: <reason>` for each such peer rather than claiming
|
|
84
|
+
it. The classifier reads command text: unknown syntax keeps native handling, and
|
|
85
|
+
an agent with an unrestricted shell can still reach the same effect another way
|
|
86
|
+
(the hostile-agent boundary above).
|
|
67
87
|
|
|
68
88
|
Config defaults apply automatically only up to ask-when-needed. A never-ask
|
|
69
89
|
request from an applied untracked config is held as ask until the person
|
|
@@ -177,18 +197,19 @@ Please report vulnerabilities privately through GitHub's "Report a vulnerability
|
|
|
177
197
|
- Every request must have the exact listener Host. Every data or action request, including the ticket exchange and snapshot polling, must be POST with the exact listener Origin and JSON content type. Foreign and missing origins, missing or expired sessions, oversized bodies and unknown actions are rejected. There is no CORS support. The only unauthenticated GET is the fixed, data-free HTML shell. No files or paths are served dynamically.
|
|
178
198
|
- Responses are non-cacheable, cannot be framed, suppress referrers, and use a content security policy restricting scripts/styles to the shipped inline content and connections to this origin. Browser text is rendered with textContent, never interpreted as HTML.
|
|
179
199
|
- The browser receives a bounded redacted event stream, peer states and queue counts, public task views (with a task's screened changed-symbol observation, #684: ids, paths, names and kinds, never source or line spans, and its Code group, #684 phase 2c: those symbols marked against the plan's names, each name re-screened by the public metadata screen; a private task has no group and its plan names never enter the snapshot), budget windows, pending approvals, the settings rows (closed values and ids, the file each comes from) and each startable peer's start plan (issue #269), benchmark run summaries (issue #251): the newest runs in `~/.agenthub/bench/`, whichever project on this machine ran them, by suite name, arm label, state, the task id in progress, counts, tokens and times, never suite text, the Usage view (#683 U4), computed only while the Agents view is open: peer ids, UTC days and token counts with their source, budget pause, resume and reset events without their reason text, and per-task rows of the task id, a PII flag and token counts, never a title, and the office view's optional fields (issue #264): each peer's open turn (start time, attributed task id, attribution kind), a `checking` flag on tasks whose check is running, and warning `links` of overlapping, conflicting or cohort tasks (task and peer ids only, never paths, symbols or reasons; a PII task never joins a link, and a private or PII-attributed turn exposes no task id). Private envelope bodies and PII task text never cross this endpoint. Local-worker and Pi approval titles can contain PII, so their details remain terminal-only; the dashboard identifies the request and directs the operator to `ahub tail` before allowing it. Budget checkpoint summaries are also omitted.
|
|
180
|
-
- The closed action list includes permission response, peer pause/resume, console message, task proposal/assignment and the settings actions below (set, undo, preview, reload). Project-board actions add revision-fenced planning, Backlog pull/return/split/undo and release planning/state changes through Tasks; they do not tag, publish or write GitHub state. These use the existing daemon/task paths and budget pause rules. `start_peer` (issue #269) starts a peer from a closed list in the mode a person set, with a command the hub assembles: the request carries a peer id and nothing else. There is no generic control proxy, task-detail read, shell, file access, free-form configuration edit, budget override, launch of a caller-chosen command or daemon shutdown API.
|
|
200
|
+
- The closed action list includes permission response, peer pause/resume, console message, task proposal/assignment and the settings actions below (set, undo, preview, reload, and the structural edit preview and apply of a settings session). Project-board actions add revision-fenced planning, Backlog pull/return/split/undo and release planning/state changes through Tasks; they do not tag, publish or write GitHub state. These use the existing daemon/task paths and budget pause rules. `start_peer` (issue #269) starts a peer from a closed list in the mode a person set, with a command the hub assembles: the request carries a peer id and nothing else. There is no generic control proxy, task-detail read, shell, file access, free-form configuration edit, budget override, launch of a caller-chosen command or daemon shutdown API.
|
|
181
201
|
- The listener stops with the daemon. Expired sessions must be reopened with `ahub ui`; the page does not silently obtain new credentials.
|
|
182
202
|
|
|
183
203
|
## Dashboard settings and the settings session (issue #269)
|
|
184
204
|
|
|
185
|
-
- Settings are a closed registry (`src/hub/settings.ts`). The dashboard and `ahub settings` can write only its keys, each checked against its own closed values, and only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml`. A file git tracks is never written, so the shared `config.json` and `routing.toml` stay byte-identical; in a repository where git cannot answer, nothing is written either. Commands and binaries (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx`), credential files and gateway URLs (`omniroute`, `memory.worker_url`), the local worker's reach (`local.read_allow`, `local.bash_network`, `local.network_allow`), `bench.enabled`, `terminal` and `instances` are refused by name from every surface: they choose what the hub runs or where secrets and task text go.
|
|
205
|
+
- Settings are a closed registry (`src/hub/settings.ts`). The dashboard and `ahub settings` can write only its keys, each checked against its own closed values, and only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml`. A file git tracks is never written, so the shared `config.json` and `routing.toml` stay byte-identical; in a repository where git cannot answer, nothing is written either. Commands and binaries (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx`), credential files and gateway URLs (`omniroute`, `memory.worker_url`), the local worker's reach (`local.read_allow`, `local.bash_network`, `local.network_allow`), `bench.enabled`, `terminal` and `instances` are refused by name from every registry surface: they choose what the hub runs or where secrets and task text go. The structural editor below (#689) is the only other writer, and it keeps the executable and secret-locating keys console only.
|
|
186
206
|
- `routing.local.toml` holds `stay_switch` and, per class, `peers`, `escalate_to`, `route` (an id `routing.toml` defines) and `pi_backend`, and nothing else: targets, routes, signals and the PII constraint cannot be set through it. A write is read by the hub's own loader before it replaces the file (temp file and rename), and one previous version is kept for undo.
|
|
187
207
|
- An ordinary dashboard session (`ahub ui`) reads settings, lowers a mode to `ask`, turns `pi.auto_start`, `task_sweep.enabled` or `task_sweep.auto_reassign` off, sets a start mode back to `tui` or a WIP limit to 0, and changes the safe switches (`research.enabled`, `approvals.notify`, `snapshots.enabled`, `coordination`). Whatever can widen what agents do without asking needs a settings session: a non-ask mode now or at hub start, routing, starting Pi with the hub, removing a machine-local value, and undo. `ahub ui --settings` opens one: its ticket marks the session for 15 minutes, after which it is an ordinary session. `never-ask` also needs the peer's id typed as the confirmation, and a stored never-ask default still waits for the console's `y` at each hub start. An undo never puts never-ask back, and removing a machine-local value that uncovers a never-ask in the shared file needs the same confirmation. "Lowering to `ask`" means handing the decision back to the agent's own rules: for Codex that is the approval policy the person configured natively, which the hub does not judge.
|
|
188
208
|
- 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.
|
|
189
209
|
- 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.
|
|
190
210
|
- 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.
|
|
191
|
-
-
|
|
211
|
+
- 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.
|
|
212
|
+
- 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.
|
|
192
213
|
|
|
193
214
|
## Multi-project manager (protocol 7)
|
|
194
215
|
|
|
@@ -373,12 +394,18 @@ Stall causes are persisted and deduplicated by cause and replanning revision. Pl
|
|
|
373
394
|
|
|
374
395
|
A declared instance whose only role is reviewer gets review requests, task messages addressed to it and the person's messages; agent chat, digests and presence are discarded and journaled, and it gets no memory recall (#223). The headless Claude reviewer runs one new `claude -p` per review with `--setting-sources ""`, `--strict-mcp-config`, `--no-session-persistence`, tools limited to Read, Grep, Glob, `hub_review` and `hub_task_show`, and `dontAsk`. Its prompt holds hub facts and a bounded diff, never the builder's summary, and its `hub_task_show` drops the builder's notes. Changes touching `local.deny` or `pii.paths` are withheld and passed as `Read` deny rules, which Claude enforces, not the hub. Private tasks are never given to it. Its `hub_review` counts only while the child started for that task still runs; a stop or exit revokes it first, and surviving processes hold the delivery as `needs_review` and keep the reviewer busy.
|
|
375
396
|
|
|
397
|
+
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.
|
|
398
|
+
|
|
399
|
+
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.
|
|
400
|
+
|
|
376
401
|
The fresh Codex reviewer (#519) needs `review_mode: "fresh"` and a compat-admitted Codex (receipt-verified 0.146.0 and 0.163.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, 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.
|
|
377
402
|
|
|
378
403
|
The fresh Kimi reviewer (#533) needs `review_mode: "fresh"` and a compat-admitted Kimi Code (receipt-verified 2.2.0 and 2.3.0; `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 receipt](verification/2026-10-22-kimi-qwen-compat.md) is model-free and does 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.
|
|
379
404
|
|
|
380
405
|
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`.
|
|
381
406
|
|
|
407
|
+
Deliverable records (#509a). `hub_task_done` and `hub_sdlc_record` name files as `{path, format, version?}`; the hub hashes them itself and refuses a claimed `sha256`, `hash` or `bytes` and any field it does not own. Paths resolve under the owner's execution root with `realPath` and `guardPath` in read mode: outside the project, the secrets denylist, the hub's own state (`.git`, `.agenthub`), a dangling link, a non-regular file and more than 256 MiB each or 512 MiB together are refused, with the path as given and never a resolved one. The file is opened without following a final link and its device, inode, size and time are compared with what was checked, before and after the read, so a swapped or changing file is refused rather than recorded half-read; a bad call writes nothing. A declared worktree instance whose root is not proven refuses, and a later read of it reports `unknown`. No hash of hub state is ever taken, and a file the hub cannot read or vouch for is `unknown`, never `unchanged`. A reviewer's approval re-hashes the files and is refused when one changed, went missing or is unknown; only the person may approve the current bytes, which are then measured and bound. For a PII task, a file under `pii.paths` or a path matching a PII pattern, `publicDeliverables` is the one projection every peer view, review request, status and log uses: id, hash and size only. The board keeps the path, as it keeps a PII task's text.
|
|
408
|
+
|
|
382
409
|
Person release authorization (#481 S6). `ahub sdlc authorize-release <version>` is taken from the console role only and refused under `--live-test`. It binds the daemon checkout's commit, package version, push remote identity and a fresh-docs verdict, lives only in daemon memory (a restart revokes it) and is logged with its commit, version and docs digest. `ahub sdlc release-guard -- <argv>` is a read-only preflight that agents may run, except during a private or PII turn; it never executes the command and refuses ambient target selectors (`GIT_DIR`, `GH_REPO`, npm prefix and registry settings and the like). The pre-push hook that `ahub init --release-guard` installs refuses an unauthorized or unknown `v*` tag push, also with the daemon down, and passes other pushes through. It and the release block in `AGENTS.md` are a cooperative guard, not an operating-system boundary: `git push --no-verify`, a custom `core.hooksPath` or another Git invocation bypasses the hook, and `gh release create` and `npm publish` are checked only when the preflight is run (#312, #313).
|
|
383
410
|
|
|
384
411
|
SDLC metrics and control bands (#481 S7). `ahub sdlc metrics` and `hub_sdlc_metrics` are planner or person only, read-only, and refused during a private or PII turn. A private task is counted as withheld and read for nothing else; the report calls no model and makes no GitHub read. Band breaches are written only by the daemon's evaluator through Tasks, never while it is stopping, recovering or has a storage error. A `diagnose` or `propose` card is a backlog task (`triage` or `plan`) whose text is fixed words, numbers and task ids, never task text; it passes the PII signal check and the candidate-text screen before it is written, and no GitHub issue is created. A `log` breach is one hub notice, remembered in the board's `band_notices` table as a marker (`<metric>:#<task>:log`, checked against a fixed pattern) and a time, nothing else.
|