@staix/agent-hub 0.12.22 → 0.12.23
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 +19 -0
- package/README.md +16 -8
- package/docs/agent-notes/adapters.md +10 -5
- package/docs/agent-notes/benchmarks.md +3 -0
- package/docs/agent-notes/budget.md +4 -0
- package/docs/agent-notes/daemon.md +8 -1
- package/docs/agent-notes/local-worker.md +3 -2
- package/docs/agent-notes/tasks.md +2 -1
- package/docs/agent-notes/tests.md +2 -0
- package/docs/events.md +7 -3
- package/docs/operations.md +232 -26
- package/docs/quickstart.md +9 -4
- package/docs/security.md +15 -6
- package/docs/smoke.md +72 -5
- package/docs/specs/2026-09-19-agent-hub-design.md +77 -10
- package/docs/verification/2026-10-10-cli-output-phase1.md +309 -0
- package/docs/verification/2026-10-10-cli-output-phase2.md +672 -0
- package/docs/verification/2026-10-11-0.12.23.md +90 -0
- package/docs/verified.json +25 -16
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +14 -3
- package/src/adapters/acp-quota.ts +13 -0
- package/src/adapters/acp.ts +95 -13
- package/src/adapters/codex-appserver.ts +43 -13
- package/src/cli/bench.ts +2 -1
- package/src/cli/console-state.ts +28 -22
- package/src/cli/console.ts +20 -10
- package/src/cli/help.ts +19 -14
- package/src/cli/identity.ts +2 -2
- package/src/cli/init.ts +1 -1
- package/src/cli/main.ts +272 -159
- package/src/cli/output.ts +363 -0
- package/src/cli/preview.ts +2 -1
- package/src/cli/reset.ts +3 -0
- package/src/cli/status-lines.ts +0 -28
- package/src/cli/terminal-recovery.ts +27 -1
- package/src/cli/upgrade-runtime.ts +24 -7
- package/src/cli/upgrade.ts +23 -5
- package/src/hub/board.ts +2 -0
- package/src/hub/budget.ts +39 -7
- package/src/hub/bus.ts +10 -11
- package/src/hub/child-process.ts +11 -0
- package/src/hub/conductor.ts +23 -11
- package/src/hub/config-trust.ts +16 -5
- package/src/hub/conflicts.ts +2 -0
- package/src/hub/context-window.ts +8 -1
- package/src/hub/crash.ts +2 -1
- package/src/hub/daemon.ts +338 -23
- package/src/hub/db-timeout.ts +3 -0
- package/src/hub/delivery-journal.ts +2 -1
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +2 -0
- package/src/hub/execution-budget.ts +2 -0
- package/src/hub/facts.ts +5 -4
- package/src/hub/git.ts +32 -0
- package/src/hub/hub-tools.ts +1 -1
- package/src/hub/lifecycle.ts +10 -4
- package/src/hub/manager.ts +18 -5
- package/src/hub/peers.ts +5 -3
- package/src/hub/permission-mode.ts +14 -0
- package/src/hub/project.ts +2 -1
- package/src/hub/recovery-error.ts +31 -0
- package/src/hub/recovery-store.ts +6 -5
- package/src/hub/registry.ts +2 -1
- package/src/hub/routing.ts +114 -13
- package/src/hub/settings.ts +392 -0
- package/src/hub/snapshots.ts +4 -2
- package/src/hub/start-mode.ts +48 -0
- package/src/hub/tasks.ts +129 -31
- package/src/hub/terminal-open.ts +64 -0
- package/src/hub/ui.ts +25 -13
- package/src/local/deny.ts +29 -7
- package/src/local/sandbox.ts +44 -6
- package/src/memory/recall.ts +2 -2
- package/src/models/mlx.ts +2 -1
- package/src/pi/tool-receipts.ts +2 -1
- package/src/ui/index.html +157 -10
package/docs/operations.md
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
# Operations guide
|
|
2
2
|
|
|
3
|
-
This guide describes ahub 0.12.
|
|
3
|
+
This guide describes ahub 0.12.23 and control protocol 16. Live verification
|
|
4
4
|
results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
|
|
5
5
|
|
|
6
|
+
## Command output
|
|
7
|
+
|
|
8
|
+
`ahub status`, `board`, `budget`, `doctor`, `projects`, `queue list`, `turns` and `report` print shared console tables and labelled details.
|
|
9
|
+
`status --all` uses the projects table. `queue show`, `models status` and `budget execution status` show labelled fields; add `--json` for the raw fetched document.
|
|
10
|
+
`report --by task` keeps the attribution and coverage sentences with section headings and aligned counters.
|
|
11
|
+
Use `--json` for scripts; it returns the fetched data without color, and `status --json` keeps its existing document.
|
|
12
|
+
In `projects --json` and `status --all --json`, an unavailable hub that explicitly refuses an inspection for SQLite contention also carries `failure: { "code": "database_busy" }`; it is additive metadata alongside the neutral `error` sentence.
|
|
13
|
+
Text columns are a human interface, not a scripting contract. An identifier accepted as a command argument is whole wherever its available column can hold its own length:
|
|
14
|
+
turn ids, queue ids, project ids, execution budget ids and ids in labelled views. Only status's informational settlement and hold ids are shortened;
|
|
15
|
+
`status --full` keeps those whole too. Identifiers in suggested commands are always whole.
|
|
16
|
+
|
|
17
|
+
`--color=auto|always|never` uses the console palette. Auto colors only a TTY with `TERM` other than `dumb`
|
|
18
|
+
and empty `NO_COLOR`; pipes and redirects stay plain. Color never replaces a state or level word.
|
|
19
|
+
TTY width is its columns (at least 80). A pipe uses a positive integer `COLUMNS` when supplied, otherwise rows
|
|
20
|
+
are complete and unwrapped. Long fields wrap within their column; when whole metadata words and the readable flexible-column minimum do not fit, table bands retain every field. Each band carries the row id; when it cannot share a line with the metadata, a per-row id banner links them. Labelled two-column views keep each label beside its value, wrapping the value instead of separating labels and values into bands. In `turns`, the time and state cells wrap at word boundaries only when the file list needs the room and the listing gets shorter for it. When `projects` bands, the task list stays with the state columns in the first band and the second band is always PROJECT and ROOT. Console children receive the console's width
|
|
21
|
+
minus its four-column indent and remain uncolored.
|
|
22
|
+
|
|
6
23
|
## Command help
|
|
7
24
|
|
|
8
25
|
`ahub help`, `ahub --help` and `ahub -h` print the commands grouped by section;
|
|
@@ -46,6 +63,15 @@ a three-line footer, separated by ASCII `-` rules; the stream has one rule above
|
|
|
46
63
|
its footer. The active tab is bracketed, as in `[2 Approvals]`, so it reads
|
|
47
64
|
without color. The Approvals tab counts pending requests and the Queue tab
|
|
48
65
|
counts `needs_review` deliveries, in the attention and failure tones.
|
|
66
|
+
Tabs change with the digits 1 to 5 or with Left and Right (wrapping at both
|
|
67
|
+
ends), never while a command is being edited or a choice is open; under a
|
|
68
|
+
confirmation an arrow dismisses it like any key but y, without moving a tab. A pasted
|
|
69
|
+
chunk can only edit the command line, but a read that holds nothing but arrow
|
|
70
|
+
sequences is that many key presses, so a held key repeats its tab move. In the Events
|
|
71
|
+
tab a row's header line is tokenized and coloured exactly as the same event in
|
|
72
|
+
the stream; only the header is tab-flattened before tokenizing. Body lines retain
|
|
73
|
+
the previous rendering and stay plain, as does command output; colour never changes
|
|
74
|
+
what the panel's text says.
|
|
49
75
|
|
|
50
76
|
Peers, Approvals, Tasks and Queue are tables with a header row. A column is as
|
|
51
77
|
wide as its widest cell, up to a third of the terminal; the last column takes
|
|
@@ -78,7 +104,7 @@ prompts, are quoted unless they are a single word starting with a letter, so
|
|
|
78
104
|
punctuation, a newline or a bare number in them cannot imitate the console's own
|
|
79
105
|
structure. Under the `?` key table only `?`, Escape and `q` act.
|
|
80
106
|
|
|
81
|
-
|
|
107
|
+
Up/Down arrows or j/k move, Enter opens a detail of labeled fields (relative times,
|
|
82
108
|
never JSON), Escape returns, and `?` shows the keys grouped by panel; in the
|
|
83
109
|
stream, `?` prints that table and `v` prints the selected request as its push
|
|
84
110
|
showed it. The footer's last line lists only the keys that act in the current
|
|
@@ -199,10 +225,12 @@ Launch that peer with `ahub claude` for channel pushes, or select `codex` in
|
|
|
199
225
|
`roles` and launch `ahub codex`. The conductor splits work into owned tasks,
|
|
200
226
|
observes `hub_status`, moves stalled work and obtains review before reporting
|
|
201
227
|
results and open decisions. It does not implement the tasks it handed out.
|
|
202
|
-
`hub_peer_start` starts
|
|
203
|
-
|
|
204
|
-
the
|
|
205
|
-
|
|
228
|
+
`hub_peer_start` starts a peer in the start mode a person set for it (see
|
|
229
|
+
"Start modes"): local, Kimi and a headless Pi or Codex start at once; for a
|
|
230
|
+
TUI peer the hub opens a terminal running the fixed `ahub <peer>` command,
|
|
231
|
+
after validating it through the shared launcher planner, and when it cannot
|
|
232
|
+
open one the call fails with that command for the person to run. The
|
|
233
|
+
conductor cannot choose the mode. `hub_peer_hold` and `hub_peer_release`
|
|
206
234
|
manage only holds placed by that conductor. Assignment also requires `assign`
|
|
207
235
|
when the conductor has an explicit capabilities list.
|
|
208
236
|
|
|
@@ -351,7 +379,7 @@ Managed launchers attach native peers to the project daemon:
|
|
|
351
379
|
ahub claude
|
|
352
380
|
ahub codex
|
|
353
381
|
ahub kimi
|
|
354
|
-
ahub pi
|
|
382
|
+
ahub pi # its TUI; --headless for the background peer
|
|
355
383
|
ahub local
|
|
356
384
|
ahub local --model <served-model-id>
|
|
357
385
|
```
|
|
@@ -380,7 +408,19 @@ A delivery in `needs_review` holds later deliveries to that peer. The console an
|
|
|
380
408
|
the hold once, and status, task assignment and route explanations name its delivery id.
|
|
381
409
|
Inspect it with `ahub queue show <id>` and explicitly choose `completed`, `retry` or
|
|
382
410
|
`discard` through `ahub queue resolve`. Reassignment or approval of its task never
|
|
383
|
-
resolves an uncertain delivery automatically.
|
|
411
|
+
resolves an uncertain delivery automatically. The same rule holds on every path that
|
|
412
|
+
assigns a task (#297): once the move is saved on the board, a failed delivery prints
|
|
413
|
+
exactly one console notice ("the move is saved... delivery is not confirmed"), printed
|
|
414
|
+
where the error is thrown, and the caller (a peer in its turn, or the CLI) still gets
|
|
415
|
+
its error. Two exceptions, by construction: a move superseded while the assignment was being prepared, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close; the hub's quiet bookkeeping such as a late check result or a reviewer change does not supersede), is
|
|
416
|
+
the caller's error as before, with no notice naming either owner; and a claim is its own hand-over, so its
|
|
417
|
+
accepted write and line stay outside the delivery rule (its overlap and routing announcements to earlier
|
|
418
|
+
owners are deliveries and share it). The repair is always
|
|
419
|
+
to look for a queued or needs_review delivery with
|
|
420
|
+
`ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in
|
|
421
|
+
`ahub task show <id>` if the journal itself is down) and then send the assignment again
|
|
422
|
+
with `ahub task assign <id> <owner>`. Nothing resends automatically: a blind resend can
|
|
423
|
+
duplicate a delivery that actually left.
|
|
384
424
|
|
|
385
425
|
Reviewer candidates follow `[classes.review].peers`, then attached peers with a
|
|
386
426
|
`reviewer` role in `roles`. The owner cannot review its own task. If none is available,
|
|
@@ -644,7 +684,7 @@ ahub undo <turn> --yes --context # Codex's latest turn: also drop it from Codex
|
|
|
644
684
|
has not removed yet included, and a PII file left in the project is
|
|
645
685
|
snapshotted by later turns like any other file. Keep PII files in an ignored
|
|
646
686
|
directory.
|
|
647
|
-
- A turn the hub stopped in the middle of shows
|
|
687
|
+
- A turn the hub stopped in the middle of shows the state `no end snapshot` and
|
|
648
688
|
cannot be undone.
|
|
649
689
|
- Claude's turns are not recorded: its channel shows the hub no turn boundary.
|
|
650
690
|
Claude Code's own checkpoints cover its edits, though not its shell commands.
|
|
@@ -956,6 +996,130 @@ permission_mode (peer/from/to) with no tool arguments under events schema 1.
|
|
|
956
996
|
The optional control requests preserve PROTOCOL; an older hub answers unknown
|
|
957
997
|
and the CLI advises upgrading.
|
|
958
998
|
|
|
999
|
+
## Settings from the dashboard and the terminal
|
|
1000
|
+
|
|
1001
|
+
The dashboard's Settings section and `ahub settings` show and change the same
|
|
1002
|
+
closed list of settings (issue #269). Run them from a plain terminal:
|
|
1003
|
+
|
|
1004
|
+
```bash
|
|
1005
|
+
ahub settings # every setting: value, source file, when it applies
|
|
1006
|
+
ahub settings get permission.kimi
|
|
1007
|
+
ahub settings set routing.classes.implement.peers codex,kimi,pi --preview
|
|
1008
|
+
ahub settings set routing.classes.implement.peers codex,kimi,pi
|
|
1009
|
+
ahub settings set permission_modes.kimi ask-when-needed
|
|
1010
|
+
ahub settings set research.enabled inherit # remove the machine-local value
|
|
1011
|
+
ahub settings undo # put back the file the last write changed
|
|
1012
|
+
ahub ui --settings # dashboard with a 15-minute settings session
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
| Group | Keys | Stored in | Applies |
|
|
1016
|
+
| --- | --- | --- | --- |
|
|
1017
|
+
| Permissions | `permission.<peer>` (the mode now) | the running hub only | at once |
|
|
1018
|
+
| Permissions | `permission_modes.<peer>` (the mode at hub start) | `config.local.json` | next hub start |
|
|
1019
|
+
| Start | `peers.pi.start_mode`, `peers.codex.start_mode` (`tui` or `headless`) | `config.local.json` | that peer's next start |
|
|
1020
|
+
| Start | `pi.auto_start` | `config.local.json` | next hub start |
|
|
1021
|
+
| Routing | `routing.stay_switch`, `routing.classes.<class>.peers`, `.escalate_to`, `.route`, `.pi_backend` | `routing.local.toml` | at once |
|
|
1022
|
+
| Switches | `research.enabled`, `approvals.notify`, `snapshots.enabled`, `coordination` | `config.local.json` | next hub start |
|
|
1023
|
+
|
|
1024
|
+
- Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml`,
|
|
1025
|
+
the machine-local files (`ahub init` ignores both in git). The shared
|
|
1026
|
+
`config.json` and the hand-written `routing.toml` are never rewritten, and a
|
|
1027
|
+
machine-local file that is committed to git is refused. Each write is read by
|
|
1028
|
+
the hub's own loader first and replaces the file atomically.
|
|
1029
|
+
- `routing.local.toml` overrides `routing.toml` key by key and may hold only the
|
|
1030
|
+
routing keys in the table. `ahub route explain` and the Settings rows name the
|
|
1031
|
+
file each value comes from. `--preview`, and the dashboard's Preview button,
|
|
1032
|
+
show `ahub route explain` for the open tasks before and after, or what a
|
|
1033
|
+
permission mode grants, and save nothing.
|
|
1034
|
+
- A row shows the value in force, its source file, when it applies, and for a
|
|
1035
|
+
value read at hub start what the next start will read (`Pending restart`).
|
|
1036
|
+
- `ahub settings undo` (the dashboard's "Undo last write") puts back the one
|
|
1037
|
+
previous version of the file the last write changed. It refuses when that
|
|
1038
|
+
file was edited since, and when it would put a stored never-ask back (set it
|
|
1039
|
+
again with its confirmation instead). A mode changed for the running hub is
|
|
1040
|
+
not a file: set it again instead. Setting a value it already has, or
|
|
1041
|
+
removing one that is not set, writes nothing. A project with no
|
|
1042
|
+
`.agenthub/config.json` gets no first config file from a settings write
|
|
1043
|
+
(any config file turns the project defaults on): run `ahub init` first.
|
|
1044
|
+
- A project initialised before 0.12.23 has no `.gitignore` line for
|
|
1045
|
+
`.agenthub/routing.local.toml`: run `ahub init` again, or add it. A
|
|
1046
|
+
`permission_modes` value is read only from a file git confirms nobody
|
|
1047
|
+
committed (config trust); where the hub will not read what was written, the
|
|
1048
|
+
reply and the Settings row say so.
|
|
1049
|
+
- Authority: a dashboard opened with `ahub ui` reads settings, lowers a mode to
|
|
1050
|
+
`ask`, turns `pi.auto_start` off, sets a start mode back to `tui` and changes
|
|
1051
|
+
the Switches group. Everything
|
|
1052
|
+
else needs `ahub ui --settings`, whose session may raise for 15 minutes, or
|
|
1053
|
+
`ahub settings` in a terminal. `never-ask` needs the peer id typed in the
|
|
1054
|
+
dashboard, or `--yes` in the terminal, and a stored never-ask default still
|
|
1055
|
+
waits for `y` in `ahub console` at each hub start. Tool approvals stay
|
|
1056
|
+
deny-only for Pi and local in every session.
|
|
1057
|
+
- Commands and binaries, credential files, gateway URLs and the local worker's
|
|
1058
|
+
reach are never editable here: edit `.agenthub/config.local.json` yourself.
|
|
1059
|
+
- Every change is announced in `hub.log` and open consoles with its source and
|
|
1060
|
+
recorded as a `settings` event. `ahub settings` needs a running hub; an older
|
|
1061
|
+
hub answers the request as unknown and the CLI advises upgrading. The unified
|
|
1062
|
+
dashboard (`ahub ui --all`) acts as an ordinary session.
|
|
1063
|
+
|
|
1064
|
+
## Start modes: TUI by default, headless as opt-in
|
|
1065
|
+
|
|
1066
|
+
A peer that has a TUI the hub attaches to starts in it by default (issue #269).
|
|
1067
|
+
|
|
1068
|
+
| Peer | Default | Headless |
|
|
1069
|
+
| --- | --- | --- |
|
|
1070
|
+
| pi | `tui` | `ahub pi --headless` (or `--mode headless`), or `peers.pi.start_mode: headless` |
|
|
1071
|
+
| codex | `tui` (`ahub codex`) | `peers.codex.start_mode: headless`: a hub-made start runs the app-server only |
|
|
1072
|
+
| claude | `tui` (`ahub claude`) | none |
|
|
1073
|
+
| kimi | headless (ACP) | it has no TUI the hub attaches to |
|
|
1074
|
+
| local | headless (hub-native) | not applicable |
|
|
1075
|
+
|
|
1076
|
+
- **You type the command:** `ahub pi` opens Pi's TUI in that terminal.
|
|
1077
|
+
`--headless` and the start mode setting are the two ways to ask for the
|
|
1078
|
+
background peer. `ahub settings set peers.pi.start_mode headless` writes the
|
|
1079
|
+
setting; it applies at Pi's next start.
|
|
1080
|
+
- **The hub starts the peer** (the dashboard's Start control, `pi` typed in
|
|
1081
|
+
`ahub console`, the conductor's `hub_peer_start`, `pi.auto_start`): it
|
|
1082
|
+
follows the peer's start mode. For `tui` it opens a terminal running the
|
|
1083
|
+
fixed command `ahub <peer>`, through a terminal provider:
|
|
1084
|
+
1. `terminal.open` in `.agenthub/config.local.json`, an argv in which one
|
|
1085
|
+
element holds `{command}` (the quoted command line to run); `{title}` and
|
|
1086
|
+
`{cwd}` are replaced too. Examples:
|
|
1087
|
+
`["tmux", "new-window", "-n", "{title}", "{command}"]`,
|
|
1088
|
+
`["wezterm", "start", "--cwd", "{cwd}", "--", "sh", "-c", "{command}"]`.
|
|
1089
|
+
Only a config file nobody committed may set it, and it is not a setting
|
|
1090
|
+
the dashboard or `ahub settings` can write. It is read at hub start.
|
|
1091
|
+
`{command}` is quoted for a POSIX shell; `{title}` and `{cwd}` are raw,
|
|
1092
|
+
so give each an argv element of its own.
|
|
1093
|
+
2. Orca, when the hub runs in an Orca worktree or an earlier `ahub <peer>`
|
|
1094
|
+
launch of this project recorded one (the path recovery uses).
|
|
1095
|
+
- **No provider:** the start is refused with the command to run by hand. It is
|
|
1096
|
+
never downgraded to headless. `ahub settings` lists each start mode, and the
|
|
1097
|
+
dashboard's Start control says which mode a start will use and where its
|
|
1098
|
+
terminal comes from.
|
|
1099
|
+
- **Dashboard Start:** the control is locked while its request is in flight. A confirmed answer unlocks it immediately, so a retry reaches the hub's own 30-second terminal reopen guard and shows any known refusal. The unified dashboard waits 35 seconds, covering a terminal provider's 30-second cap plus a 5-second response margin; a slower headless start can still end unconfirmed while the hub continues. Check the Peers panel before retrying. An unconfirmed unified-dashboard start holds that peer's control for the shared 30-second reopen bound, or until its `starts` snapshot confirms attachment, across redraws and project switches within the same hub instance; the row states the retry bound. The project-local dashboard keeps its usual confirmed-answer behavior. Other forwarded actions keep their 10-second wait; the manager opens no settings session.
|
|
1100
|
+
- The conductor cannot choose the mode: `hub_peer_start` takes the peer's own.
|
|
1101
|
+
- Recovery restores each peer in the mode its recorded session had. After an
|
|
1102
|
+
unplanned stop, a Pi that ran in a terminal is reported with its command and
|
|
1103
|
+
is not replaced by a headless one unless headless is its start mode.
|
|
1104
|
+
|
|
1105
|
+
### Migration from 0.12.22 and earlier
|
|
1106
|
+
|
|
1107
|
+
Pi's default changes from headless to its TUI.
|
|
1108
|
+
|
|
1109
|
+
- `ahub pi` in a terminal now opens the TUI. For the background peer run
|
|
1110
|
+
`ahub pi --headless`.
|
|
1111
|
+
- A project with `"pi": { "auto_start": true }` used to get a headless Pi at
|
|
1112
|
+
every hub start. To keep that, set the start mode once:
|
|
1113
|
+
`ahub settings set peers.pi.start_mode headless` (or add
|
|
1114
|
+
`"peers": { "pi": { "start_mode": "headless" } }` to
|
|
1115
|
+
`.agenthub/config.local.json`). Without it the hub opens Pi's TUI in a
|
|
1116
|
+
terminal at start, or, with no terminal provider, says
|
|
1117
|
+
`pi.auto_start did not start Pi` with the command to run.
|
|
1118
|
+
- Scripts and the console's `pi` command: without a terminal, `ahub pi` asks
|
|
1119
|
+
the hub to open one. Add `--headless` where a headless Pi is meant.
|
|
1120
|
+
- A conductor's `hub_peer_start` no longer takes a mode. A call that passes one
|
|
1121
|
+
is refused unless it repeats the peer's start mode.
|
|
1122
|
+
|
|
959
1123
|
## Approvals and pauses
|
|
960
1124
|
|
|
961
1125
|
Inspect permission requests in the terminal:
|
|
@@ -995,6 +1159,13 @@ A budget pause remains authoritative until the budget command explicitly
|
|
|
995
1159
|
overrides it or the window resets. Check `ahub status` and `ahub board` after
|
|
996
1160
|
a pause or handoff.
|
|
997
1161
|
|
|
1162
|
+
Kimi Code CLI 2.1.1's observed 5-hour usage-limit refusal creates a hard budget reading (`kimi ACP usage-limit refusal`). `ahub status` shows a budget pause and `ahub budget` shows its source and reset estimate. The refusal gives no reset timestamp, so the hub estimates the reset at five hours after it was observed and resumes on the next budget tick after the existing one-minute grace. With `budget.wait_max_min` below that estimate, the existing budget relay automatically hands open tasks to an attached eligible peer; routing skips Kimi while paused. No checkpoint is requested from an exhausted agent.
|
|
1163
|
+
`ahub budget resume kimi` overrides the reading so the provider can be tried early. A new provider refusal ends that override and pauses it again; a pending soft checkpoint cannot postpone this hard pause.
|
|
1164
|
+
During one hub run, an early override followed by the same window's refusal keeps its earlier future reset bound; a provider's explicit reset remains authoritative. A restored pause without a per-window reading cannot identify that earlier window, so its next refusal starts a new estimate.
|
|
1165
|
+
Unread quota resume notices are coalesced without losing their public task moves; the next notice lists moves accumulated across those pauses. An older notice without structured move metadata is kept as public text inside that notice.
|
|
1166
|
+
|
|
1167
|
+
A refused prompt with no acceptance or activity is requeued without a review hold. At the estimated reset it is tried again; another quota refusal pauses it again without consuming the poison-envelope retry count. Work that already started remains held for review even while its open tasks move to other peers. After the quota reset its queue stays held: inspect `ahub queue show <delivery-id>` and the associated task, then choose `ahub queue resolve <delivery-id> --action completed|retry|discard --reason "<text>"`. Do not retry work that another owner already took over without inspecting it. If the hub cannot confirm the pause, the refusal is also held instead of spending retries. Other errors keep their ordinary review behavior. Only the observed Kimi 5-hour text is recognised; weekly/monthly limits and other ACP vendors, including Qwen, remain unverified. The recognised text and its provenance live in `src/adapters/acp-quota.ts`.
|
|
1168
|
+
|
|
998
1169
|
Quota also shapes routing and handoffs:
|
|
999
1170
|
|
|
1000
1171
|
- Among peers a task could go to, those with quota readings are ordered by
|
|
@@ -1052,8 +1223,8 @@ Only `ahub claude` starts Claude Code with the development-channel flag. A
|
|
|
1052
1223
|
Claude session started another way (plain `claude`, an IDE) keeps the hub tools
|
|
1053
1224
|
but cannot show pushes, so it attaches tools-only (protocol 16, issue #205):
|
|
1054
1225
|
|
|
1055
|
-
- Its messages stay queued. `ahub status` shows
|
|
1056
|
-
messages wait for hub_inbox; for pushes restart Claude with ahub claude`; the
|
|
1226
|
+
- Its messages stay queued. `ahub status` shows the queue count in `Q` and a `tools-only` detail:
|
|
1227
|
+
`messages wait for hub_inbox; for pushes restart Claude with ahub claude`; the
|
|
1057
1228
|
console footer shows `tools-only: ahub claude` and the dashboard shows the
|
|
1058
1229
|
status line.
|
|
1059
1230
|
- The session reads them with `hub_inbox`, at most ten messages at a time plus
|
|
@@ -1230,21 +1401,21 @@ section is what they run.
|
|
|
1230
1401
|
|
|
1231
1402
|
Upgrade running projects with the target release's own coordinator. It accepts
|
|
1232
1403
|
a running source on control protocol 9 (0.6.x), 10 (0.7.0 through 0.12.0),
|
|
1233
|
-
11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.
|
|
1404
|
+
11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.23), and only
|
|
1234
1405
|
a target on its own protocol, so the target's coordinator fits every supported
|
|
1235
1406
|
source and carries every recovery fix released up to it. Protocol 8 and older
|
|
1236
1407
|
(0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
|
|
1237
1408
|
project directory, without replacing the global CLI first:
|
|
1238
1409
|
|
|
1239
1410
|
```bash
|
|
1240
|
-
bunx --package @staix/agent-hub@0.12.
|
|
1241
|
-
bunx --package @staix/agent-hub@0.12.
|
|
1411
|
+
bunx --package @staix/agent-hub@0.12.23 ahub upgrade --to 0.12.23 --dry-run
|
|
1412
|
+
bunx --package @staix/agent-hub@0.12.23 ahub upgrade --to 0.12.23 --yes
|
|
1242
1413
|
```
|
|
1243
1414
|
|
|
1244
1415
|
| Running now | Coordinator to use |
|
|
1245
1416
|
| --- | --- |
|
|
1246
1417
|
| 0.6.x (protocol 9) | the target's, through `bunx` as above |
|
|
1247
|
-
| 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.
|
|
1418
|
+
| 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.23 (protocol 16) | the target's, through `bunx` as above |
|
|
1248
1419
|
| any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
|
|
1249
1420
|
| 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
|
|
1250
1421
|
|
|
@@ -1276,14 +1447,14 @@ projects first:
|
|
|
1276
1447
|
|
|
1277
1448
|
```bash
|
|
1278
1449
|
ahub restart --dry-run
|
|
1279
|
-
ahub upgrade --to 0.12.
|
|
1450
|
+
ahub upgrade --to 0.12.23 --dry-run
|
|
1280
1451
|
```
|
|
1281
1452
|
|
|
1282
1453
|
Apply only after reviewing the plan:
|
|
1283
1454
|
|
|
1284
1455
|
```bash
|
|
1285
1456
|
ahub restart --yes
|
|
1286
|
-
ahub upgrade --to 0.12.
|
|
1457
|
+
ahub upgrade --to 0.12.23 --yes
|
|
1287
1458
|
ahub recovery status [<operation-id>]
|
|
1288
1459
|
ahub recovery resume [<operation-id>]
|
|
1289
1460
|
ahub recovery abort [<operation-id>]
|
|
@@ -1510,6 +1681,28 @@ next actions").
|
|
|
1510
1681
|
restart snapshot.
|
|
1511
1682
|
A second `resume` while its own hold still stands checks the peers again
|
|
1512
1683
|
before it closes anything.
|
|
1684
|
+
- If a restart stops with `database_busy`, the receipt stays `blocked` and
|
|
1685
|
+
carries `failure: { code: "database_busy", retryable: true, site: [...] }`.
|
|
1686
|
+
Let the other connection settle, inspect `ahub recovery status <id>` and
|
|
1687
|
+
use its offered `ahub recovery resume <id>`. Resume rechecks the source and
|
|
1688
|
+
reconciles a commit that may already have been requested. A busy read while
|
|
1689
|
+
that committed source is stopping stays inside the existing shutdown wait.
|
|
1690
|
+
A stop-and-archive that meets contention instead says to retry that
|
|
1691
|
+
disposition; resume remains refused while abandonment is partway. Successful
|
|
1692
|
+
abort, fresh-session choice and disposal clear the earlier failure annotation.
|
|
1693
|
+
Do not delete database files or start a second operation to bypass the lock.
|
|
1694
|
+
A failed installed-layout CI restart retains bounded, redacted fixture logs,
|
|
1695
|
+
receipt fields (including `commitSent` and failure frames) and file-mode
|
|
1696
|
+
metadata as a per-OS artifact.
|
|
1697
|
+
|
|
1698
|
+
The historical CI receipt (38061057253, attempt 1) kept no throw site or
|
|
1699
|
+
hub log. At its main, status/inspect RPC failures were swallowed and the
|
|
1700
|
+
synchronous catch sent a fixed sentence, so its raw SQLite text could only
|
|
1701
|
+
have come from the commit rejection or the runner's own `registry.db` read.
|
|
1702
|
+
"Resume | wait until the hub settles", without stop-and-archive, means its
|
|
1703
|
+
next inspection saw neither running nor stopped/missing. Which of the two
|
|
1704
|
+
paths, which statement and which holder remain unknown. The controlled
|
|
1705
|
+
rollback-copy reproduction is separate evidence, not that historical cause.
|
|
1513
1706
|
- When a target hub dies after it started (a crash or a reboot), `resume`
|
|
1514
1707
|
starts it again from the operation's snapshot while the project's state
|
|
1515
1708
|
directory still holds it: it records the restart (status shows `restarts`),
|
|
@@ -1648,8 +1841,9 @@ crash of one is not reported this way.
|
|
|
1648
1841
|
spends no cloud quota), and on a fresh session if that fails, keeping the
|
|
1649
1842
|
recorded backend and model; the report says which. Malformed records in
|
|
1650
1843
|
`sessions.json` are skipped. A Pi that ran in a terminal (`--mode tui`) is never started on its
|
|
1651
|
-
recorded session by the hub; the report gives the command
|
|
1652
|
-
`pi.auto_start` a fresh headless Pi starts instead
|
|
1844
|
+
recorded session by the hub; the report gives the command. With
|
|
1845
|
+
`pi.auto_start` a fresh headless Pi starts instead only when Pi's start mode
|
|
1846
|
+
is headless; with the default (tui) nothing is started in its place.
|
|
1653
1847
|
- Codex's app-server died with the hub; run `ahub codex` again. Claude Code's
|
|
1654
1848
|
plugin reconnects by itself while that session is open.
|
|
1655
1849
|
|
|
@@ -1776,7 +1970,7 @@ whose MLX slot is still busy after that wait goes to `dgx/fast` first (`source:
|
|
|
1776
1970
|
so does an enforced tool loop pinned to it (`source: "pin"`); without it nothing moves for load. `dgx/fast`
|
|
1777
1971
|
never goes first while it is cooling down, or while its own last dispatch failed in any way,
|
|
1778
1972
|
an error status or a failed stream included, until it succeeds or 30 s pass (`ahub status`:
|
|
1779
|
-
`
|
|
1973
|
+
a `dispatch held` detail with a relative time and `no load moves until then`). No load move or pin happens while the request
|
|
1780
1974
|
runs under an execution budget, and enforced, a load move happens only at a user turn. A moved
|
|
1781
1975
|
attempt gets 15 s to its response headers after the gateway lookup (OmniRoute's own probe,
|
|
1782
1976
|
up to two 4 s rounds when no gateway is cached) and is then abandoned for MLX, which serves
|
|
@@ -1790,7 +1984,7 @@ consecutive transport or startup failures outside a cooldown (a failure more tha
|
|
|
1790
1984
|
the last counted one starts the count over), a relay alias cools down for 30 s, doubling up to
|
|
1791
1985
|
5 min. A busy MLX slot and timeouts cut short by an execution budget never count; any HTTP
|
|
1792
1986
|
answer, a success or an error status, proves the transport works and ends the streak and the
|
|
1793
|
-
cooldown. `ahub status` shows `cooling
|
|
1987
|
+
cooldown. `ahub status` shows a `cooling` detail with a relative time and failure count and `events.jsonl`
|
|
1794
1988
|
records `cooldown` events.
|
|
1795
1989
|
Progress judgements suggest reassignment; they never change task ownership.
|
|
1796
1990
|
|
|
@@ -1930,7 +2124,8 @@ sequential seeded-guard job. Main and release jobs reuse only a successful full
|
|
|
1930
2124
|
PR or push check with the identical Git tree and all three successful jobs. An absent or
|
|
1931
2125
|
unreadable result runs the main gate again and refuses release. A manual
|
|
1932
2126
|
`prepare_bundle` dispatch builds reviewable plugin assets without publishing;
|
|
1933
|
-
it is never accepted as full-gate evidence.
|
|
2127
|
+
it is never accepted as full-gate evidence. A `timing_runs=20` dispatch runs only the daemon, PII screen and ACP
|
|
2128
|
+
test files twenty times on macOS, in its own concurrency group; it does not replace the full gate.
|
|
1934
2129
|
|
|
1935
2130
|
## Preview initialization and native launch
|
|
1936
2131
|
|
|
@@ -1959,9 +2154,19 @@ ownership, bind servers or start agents, sidecars or models.
|
|
|
1959
2154
|
`ahub status`, `ahub tail` and the dashboard show native context occupancy,
|
|
1960
2155
|
source and measurement freshness. Claude readings come from the status-line
|
|
1961
2156
|
tee installed by `ahub claude`; Codex readings come from its current thread's
|
|
1962
|
-
native token-usage updates.
|
|
2157
|
+
native token-usage updates. Kimi readings come from its ACP `usage_update`
|
|
2158
|
+
(`{used, size}`, source `acp_usage_update`): context occupancy, not
|
|
2159
|
+
consumption, so no token total is counted from it; the adapter accepts the
|
|
2160
|
+
update whenever it names the current session, because 2.1.1's source text
|
|
2161
|
+
emits it after the turn settles (read from the source, not yet observed live
|
|
2162
|
+
through the hub; docs/smoke.md), so the peer may already be idle.
|
|
2163
|
+
Pi and other unsupported surfaces show unknown.
|
|
1963
2164
|
A stale or disconnected reading is unknown, not 0%. Codex's accumulated session
|
|
1964
|
-
usage is never used as context occupancy.
|
|
2165
|
+
usage is never used as context occupancy. In the tail (`ahub tail` and the
|
|
2166
|
+
console's stream) a reading prints once and repeats only when the line changes
|
|
2167
|
+
apart from its time: the printed percentage, the freshness, the source, or known
|
|
2168
|
+
against unknown. Status, the Peers panel and the dashboard always read the
|
|
2169
|
+
latest reading with its time.
|
|
1965
2170
|
|
|
1966
2171
|
Context-triggered checkpoints are disabled by default. To enable them, add
|
|
1967
2172
|
this to `.agenthub/config.json` and restart the daemon deliberately:
|
|
@@ -1973,8 +2178,9 @@ this to `.agenthub/config.json` and restart the daemon deliberately:
|
|
|
1973
2178
|
`gate` is a fraction between 0 and 1; 0 disables checkpoint requests.
|
|
1974
2179
|
`stale_min` must be positive. A reading at or above the threshold records a metadata-only
|
|
1975
2180
|
event and console notice, then asks an attached Claude/Codex with active work
|
|
1976
|
-
for a checkpoint when no checkpoint request is already outstanding
|
|
1977
|
-
|
|
2181
|
+
for a checkpoint when no checkpoint request is already outstanding (an ACP peer
|
|
2182
|
+
such as Kimi gets the event and notice only; no checkpoint request is sent to it
|
|
2183
|
+
yet). The request supplies a `request_id`; include that id with
|
|
1978
2184
|
`hub_checkpoint {summary, request_id}`. Repeated high readings do not repeat
|
|
1979
2185
|
it until a fresh below-threshold reading or new session rearms the crossing.
|
|
1980
2186
|
|
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.12.
|
|
20
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.12.23
|
|
21
21
|
ahub setup
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -86,7 +86,11 @@ Use the page to pause/resume a peer, send a console message, propose/assign task
|
|
|
86
86
|
and allow/deny agent permission requests. Local-worker and Pi requests show no tool
|
|
87
87
|
contents: inspect them with `ahub tail`, then use `ahub permit <id> <option>` to
|
|
88
88
|
allow. They can be denied on the page. PII task details remain available only via
|
|
89
|
-
`ahub task show <id>` in the terminal.
|
|
89
|
+
`ahub task show <id>` in the terminal. The page also starts a peer in its start
|
|
90
|
+
mode (a TUI opens in a terminal when the hub has a terminal provider) and shows
|
|
91
|
+
the settings; raising one needs `ahub ui --settings` or `ahub settings` in a
|
|
92
|
+
terminal. See the operations guide, "Settings from the dashboard and the
|
|
93
|
+
terminal" and "Start modes".
|
|
90
94
|
|
|
91
95
|
`ahub ui --no-open` prints a one-time link instead of launching a browser. Open it
|
|
92
96
|
within 60 seconds. A session expires after one hour; run `ahub ui` again. Restarting
|
|
@@ -108,8 +112,9 @@ commands use the nearest project inside that Git working-tree boundary.
|
|
|
108
112
|
|
|
109
113
|
Open `ahub ui --all` from any directory to manage all registered hubs. Use Start
|
|
110
114
|
to launch an initialized project, Use to view a running project, and Stop to stop
|
|
111
|
-
the selected hub. Stop confirmation names its root.
|
|
112
|
-
still `ahub --project <path> claude` or `ahub --project <path> codex
|
|
115
|
+
the selected hub. Stop confirmation names its root. Without a terminal provider,
|
|
116
|
+
agent launch is still `ahub --project <path> claude` or `ahub --project <path> codex`
|
|
117
|
+
in a terminal.
|
|
113
118
|
|
|
114
119
|
The manager is independent: `ahub ui --all --stop` closes its browser sessions
|
|
115
120
|
without stopping project hubs. One-time links and session expiry work the same
|
package/docs/security.md
CHANGED
|
@@ -7,13 +7,13 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
7
7
|
- **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input: a channel tag with `meta.source` for Claude started with `ahub claude`; otherwise a fixed header line (`frame()`, `[agent-hub message from ...]`) with an instruction that says it is untrusted, the plugin's server instructions for a tools-only Claude reading `hub_inbox` and a standing instruction for the others. A message body cannot forge the hub's own headers: such lines are quoted (`sanitize`). Replies inherit a hop count capped at 3, so agents cannot ping-pong forever; neither a digest nor a steer can reset it.
|
|
8
8
|
- **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. External clients cannot claim the console user's id or a hub-managed peer's id.
|
|
9
9
|
- **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits; Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default.
|
|
10
|
-
- **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`) apply only from a config file git confirms nobody committed: `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust.
|
|
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`) apply only from a config file git confirms nobody committed: `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, 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 go through the one helper in `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 and the credential and askpass helpers are not (every call pipes stdout, and no hub command authenticates), and their settings are belt and suspenders. 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 has a 10 s cap). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
|
|
11
11
|
- **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts, and the task id (with a PII flag) that each usage and token record is attributed to. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. It stays on the machine; `ahub export` only prints it. Two opt-in stores copy numbers from it into your home directory, shared by every project on the machine (directories mode 700, files 600): research records (issue #247, `research.enabled`) in `~/.agenthub/research/` hold per approved task the ids, states, counts, tokens, times and PII flag the events hold, a person's label from a closed list, and a hash in place of the project id; benchmark runs (issue #251) in `~/.agenthub/bench/` hold suite name and hash, arm label, the attached peers' ids, models and permission modes, task ids, outcomes, exit codes, counts, tokens and times, never suite text or a command's output.
|
|
12
12
|
- **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
|
|
13
13
|
- **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
|
|
14
14
|
- **The session record holds identities only.** `.agenthub/state/sessions.json` (issue #37, mode 600) keeps each attached peer's recovery metadata: launch options, session and thread ids, Pi's session file path. No message or task text; loss notices name deliveries by id, sender and public task title.
|
|
15
15
|
- **Context checkpoints have a separate text record.** Native occupancy readings carry numeric metadata only. An optional context checkpoint persists its summary in `.agenthub/state/context-checkpoint-<peer>.json` with mode 0600 and attempts shared-memory storage when enabled. Completion requires the outstanding request id, current peer/native session and transport generation; new connection claims invalidate old requests. Private turns, every open PII task associated with the peer as owner or reviewer (including pending review), and PII-pattern summaries are refused before persistence or cloud memory. Its text is never broadcast, and context pressure does not pause, reassign or replace the session.
|
|
16
|
-
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state and the state archives `ahub reset --all` leaves) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval unless the operator's permission mode grants them (writes and edits inside the grant filter in `ask-when-needed`, all four in `never-ask`; see below), and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git` name at any depth, closing the creation, rename, symlink and gitfile routes to a planted repository
|
|
16
|
+
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state and the state archives `ahub reset --all` leaves) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval unless the operator's permission mode grants them (writes and edits inside the grant filter in `ask-when-needed`, all four in `never-ask`; see below), and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git` name at any depth, closing the creation, rename, symlink and gitfile routes to a planted repository, no `config` or `hooks` directly under one nor below the discovered modules root of the project's own `.git` (a submodule's git directory, nested submodules included; every term is anchored below that root, so a directory named `refs`, `config` or `hooks` above the project neither disables the rule nor trips it; a `/refs/` or `/logs/` segment below it keeps refs named `config` or `hooks` writable, and leaves a submodule whose own path contains such a segment uncovered; a submodule whose path has `hooks` as a whole component (`tools/hooks` included) fares worse: the rule matches its whole git directory, so every write there is refused; the discovered git directories, the modules root and every component in between are also refused as targets, closing the rename, replace and symlink routes to the same files), no `commondir` at any depth below one, with every such name matched folded against the code points HFS+ ignores (the fold covers the security-relevant names; on a volume that folds names, folded spellings of the components between `.git` and a submodule's git directory are not matched — the 1024-byte SBPL literal cannot hold a folded path — the same residual the external git dir rules carry since #270), and no `.agenthub` anywhere in the project (in an external git dir the `config`/`hooks` rules cover only its own children and the hooks directory's contents, so refs named `config` or `hooks` still work); a nested repository in bare layout stays open — the directory is the git dir, with no `.git` name to anchor a rule on, and the hub never runs git there, whatever the project root is: its calls are all `-C <project root>` or `--no-index` on temp files, and none descends into a nested directory's git layout, so a planted hook fires only if a person or another tool runs git in that directory themselves; the rules are probed on HFS+, which folds the 16 code points the patterns tolerate, and on a case-sensitive APFS image, which folds nothing (a folded spelling is an inert new file there and the real names stay refused); the cost is that `git init`, `git clone` and `git worktree add` do not run inside the sandbox (add, commit, stash, checkout and gc still do), `git submodule update --init` does not either (its git directory's `config` write is refused), and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` in a project with a nested repository, or `rm -rf` of a directory that holds a `.git` (refusing the unlink is what stops the rename-out route); no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses (behind NAT64 with the well-known prefix, by the IPv4 address the answer carries; a network-specific prefix is not recognised), so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, until 0.13.0). One channel stays open: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. The allow-default profile of 0.9 and earlier was removed in 0.12.0 (issue #83); a `local.sandbox` setting is ignored with a note. The shared temp dirs (the user's and `/private/tmp`) are closed. Without the sandbox there is no `bash` tool.
|
|
17
17
|
- **Capabilities are enforced, not suggested.** `capabilities` (issue #39) is checked by the daemon where task operations and messages arrive, so a peer cannot get round it by phrasing. A peer can never answer a permission request: the control link takes `permit` from the console role only, and a message that quotes a permit command is just text. Both bind the hub's own tool paths: a vendor agent with its own shell in the project (Codex, Claude, Kimi) can read `.agenthub/state/control-token` and connect as the console, which only the local worker's and Pi's sandbox prevents.
|
|
18
18
|
- **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it is sent to claude-mem, whose observer is a cloud model.
|
|
19
19
|
- **A model screen can add to the patterns (issue #198).** With `signals.pii_screen = "local"` in `routing.toml` (default `off`; it applies while `constraints.pii = "local_only"`), a newly proposed task is read by a model before the hub does anything else with it: until the verdict it is on no board, in no envelope, before no triage model, and the hub makes no brief or note call for it. The local worker's claude-mem capture leaves its hub tool calls out (issue #230), so a proposal it makes in an ordinary turn cannot reach claude-mem ahead of the verdict that way. The screen runs on the model on this machine (MLX or Ollama, loopback only) or, without one, on the gateway only while `onCampus()` holds, and the gateway call is refused once more right before transport if the gateway is behind Access; task text never goes off campus to be screened. When the on-device generation slot is taken (Pi generating, another screen), the screen goes to the campus gateway instead of waiting, and on campus a device that fails or is still loading after 60% of the deadline hands over to the gateway too; off campus it waits for the slot and the device within the deadline. With the legacy MLX provider the screen may start an `mlx_lm.server`, as Pi's relay does, and it stays running until `ahub models stop`. Its answer is `pii` or `clear` plus a category from a closed list (name, student id, phone, address, grade, health, other). Anything else, a timeout (8 s, one call and one generation slot per item), a missing or off-campus model, or text longer than 6000 UTF-8 bytes (about 2000 Hangul syllables) is `unknown`, and an unknown task is a PII task: this is the hub's one model call that fails closed, so with the screener down every new task waits for `local`. An unknown verdict has the costs of any PII task, even when a second look clears it later: opening it lifts every silent turn-free cohort for good and turns facts off while it is open, and off campus a proposal without a class is refused (triage is not asked for a PII task there), with an error that says so. Off campus with the on-device slot taken, a task stays `unknown` until it is screened again: while it is still proposed and no peer ever owned it, the hub's release timer (every 60 s, one task at a time, the least tried first, at most 10 times per task in a hub run; never text too long to read) asks again, and `clear` lifts its `pii` signal and routes it through the class peers (a named owner is not remembered; a reserved one is offered first, as always). A task `local` took keeps its PII path, also once it is declined back to nobody. A pattern match is PII without a call. The task's history records the source of its `pii` signal (`regex`, `screen` with the category, or `unknown` with the reason), an unknown verdict is a console line naming the task id only, and `events.jsonl` gets a `pii_screen` record with the item kind, label, source, category, reason and latency, never the text. A routing explanation of a draft does not call the screen and says so.
|
|
@@ -92,8 +92,9 @@ no role: a task's current owner and reviewer may read its public view with
|
|
|
92
92
|
`hub_task_assign` while it is proposed, nobody ever accepted it and its latest
|
|
93
93
|
move other than the hub's own was not made from the console (to another peer
|
|
94
94
|
only with `assign` when its capabilities are listed).
|
|
95
|
-
A conductor may inspect public state, assign or escalate work, start
|
|
96
|
-
|
|
95
|
+
A conductor may inspect public state, assign or escalate work, start a peer in
|
|
96
|
+
the start mode a person set (it cannot choose the mode) and place its own
|
|
97
|
+
delivery holds. It cannot answer approvals,
|
|
97
98
|
resolve durable deliveries, override budget pauses, release a human hold or stop
|
|
98
99
|
a peer: `peer_stop` is taken from the console role only, and no hub tool sends it.
|
|
99
100
|
Pending approval summaries exclude titles; PII tasks remain public stubs.
|
|
@@ -111,10 +112,18 @@ Please report vulnerabilities privately through GitHub's "Report a vulnerability
|
|
|
111
112
|
- The exchange creates a separate random session, valid for one hour without renewal, in an `HttpOnly; SameSite=Strict; Path=/` cookie named for this listener's port. Sessions and tickets live only in memory and die with the daemon. The page itself keeps one more cookie, `agent-hub-theme` (issue #246): script-set, `SameSite=Strict`, kept for a year for the whole `127.0.0.1` host, and read back only as one of `system`, `light` or `dark`. HTTP is loopback-only; the cookie is not a substitute for the origin checks.
|
|
112
113
|
- 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.
|
|
113
114
|
- 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.
|
|
114
|
-
- The browser receives a bounded redacted event stream, peer states and queue counts, public task views, budget windows, pending approvals and benchmark run summaries (issue #251): the newest runs in `~/.agenthub/bench/`, whichever project on this machine ran them, by suite name, arm label, state, the task id in progress, counts, tokens and times, never suite text. Private envelope bodies and PII task text never cross this endpoint. Local-worker and Pi approval titles can contain PII, so their details remain terminal-only; the dashboard identifies the request and directs the operator to `ahub tail` before allowing it. Budget checkpoint summaries are also omitted.
|
|
115
|
-
- The closed action list is permission response, peer pause/resume, console message, task proposal and
|
|
115
|
+
- The browser receives a bounded redacted event stream, peer states and queue counts, public task views, 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), and benchmark run summaries (issue #251): the newest runs in `~/.agenthub/bench/`, whichever project on this machine ran them, by suite name, arm label, state, the task id in progress, counts, tokens and times, never suite text. Private envelope bodies and PII task text never cross this endpoint. Local-worker and Pi approval titles can contain PII, so their details remain terminal-only; the dashboard identifies the request and directs the operator to `ahub tail` before allowing it. Budget checkpoint summaries are also omitted.
|
|
116
|
+
- The closed action list is permission response, peer pause/resume, console message, task proposal, task assignment and, since issue #269, the settings actions below (set, undo, preview). 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.
|
|
116
117
|
- The listener stops with the daemon. Expired sessions must be reopened with `ahub ui`; the page does not silently obtain new credentials.
|
|
117
118
|
|
|
119
|
+
## Dashboard settings and the settings session (issue #269)
|
|
120
|
+
|
|
121
|
+
- 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` and `terminal` are refused by name from every surface: they choose what the hub runs or where secrets and task text go.
|
|
122
|
+
- `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.
|
|
123
|
+
- An ordinary dashboard session (`ahub ui`) reads settings, lowers a mode to `ask`, turns `pi.auto_start` off 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.
|
|
124
|
+
- 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.
|
|
125
|
+
- 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.
|
|
126
|
+
- 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, 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). For a TUI the hub opens a terminal running `<runtime> <its own entry point> --project <this project> <peer>` (and `--mode tui` for Pi): 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. A conductor can start a peer but cannot choose its mode.
|
|
118
127
|
|
|
119
128
|
## Multi-project manager (protocol 7)
|
|
120
129
|
|