@staix/agent-hub 0.9.0 → 0.11.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 +18 -0
- package/README.md +2 -2
- package/docs/operations.md +115 -9
- package/docs/quickstart.md +1 -1
- package/docs/security.md +5 -2
- package/docs/smoke.md +63 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +111 -0
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +2 -2
- package/src/adapters/acp.ts +10 -2
- package/src/adapters/local-worker.ts +4 -3
- package/src/cli/main.ts +6 -4
- package/src/hub/ask.ts +7 -2
- package/src/hub/board.ts +29 -1
- package/src/hub/config-trust.ts +2 -0
- package/src/hub/crash.ts +109 -0
- package/src/hub/daemon.ts +164 -15
- package/src/hub/hub-tools.ts +1 -1
- package/src/hub/routing.ts +21 -3
- package/src/hub/tasks.ts +120 -16
- package/src/local/deny.ts +2 -2
- package/src/local/proxy.ts +142 -0
- package/src/local/sandbox.ts +104 -15
- package/src/local/tools.ts +4 -2
- package/templates/config.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
|
|
4
4
|
|
|
5
|
+
## 0.11.0
|
|
6
|
+
|
|
7
|
+
- Task operations settle model-written arguments before anything reaches the board: an `owner` or `peer` that is not a peer id, or a task id that is not a whole number, is refused with a clear message instead of leaving an ownerless task behind after a database error; `null` and an empty owner mean no owner (#70).
|
|
8
|
+
- Review outcomes are credited to the work they judged in more cases: task paths are stored in one spelling (`./src/a.ts` and `src/a.ts` are one file for blame, and `.` blames nobody), and handing a task to the owner it already has keeps an earlier catch; ownership events now record the owner in the task history (#67).
|
|
9
|
+
- After a crash, `pi.auto_start` brings Pi back on its recorded headless session, with or without `recovery.auto_resume_after_crash`, and on a fresh session (on the recorded backend and model) if that resume fails; before, a fresh Pi started and the recorded session could not be resumed while it ran (#66).
|
|
10
|
+
- Tests cover a refused `hub_send` from the local worker and from Pi, a Pi resumed after `kill -9` on the session file the dead run recorded, and the crash report for Codex and Claude (#68).
|
|
11
|
+
- Model-written text on an ordinary task is screened before it leaves the hub: a done summary, a review note, an unmet item or a budget handoff that matches a PII pattern is not saved to claude-mem, and every peer (the local worker included) gets a stub naming `ahub task show <id>`; the board keeps the text, and `ahub ask` shows such a note only on campus (#69).
|
|
12
|
+
- With `local.bash_network` on, the local worker's commands can read Python's own CA bundle (`certifi/cacert.pem`, also vendored by pip), so `pip install` and `requests` work over HTTPS; every other `.pem` stays denied (#64).
|
|
13
|
+
- Each command the local worker runs gets a temp dir of its own (`TMPDIR`), removed when it ends, a timeout or kill included; under deny-default the shared temp dirs (the user's and `/private/tmp`) are closed, so a command can no longer read what other tools left there (#63).
|
|
14
|
+
- Under deny-default, Apple's `/usr/bin` shims (python3 among them) work with a full Xcode selected: the sandbox opens the app's whole `Contents`, since its tools load `SharedFrameworks`; 0.10.0 opened only `Contents/Developer`, so they worked with the Command Line Tools only.
|
|
15
|
+
- With `local.bash_network: true` the local worker's and Pi's commands reach the network only through the hub's egress proxy: HTTPS to the hosts in `local.network_allow` (machine-local; by default the npm, PyPI, crates.io and Go module registries and GitHub), with names that resolve to internal addresses refused and each policy refusal logged by method and host; direct egress and loopback to claude-mem or the Codex app-server are denied. `"direct"` keeps the open network for one release (#65).
|
|
16
|
+
|
|
17
|
+
## 0.10.0
|
|
18
|
+
|
|
19
|
+
- Review checklists and review outcomes: review requests ask the reviewer to map signatures and call sites to the plan, read the check result and list unmet items (`hub_review {unmet}`); the hub records approved, caught, contradicted and escalated reviews per implementer, reviewer and class, shown by `ahub task show` and `ahub route explain`, and `review.adaptive` (off by default) orders reviewers by that record, after idle before busy and ahead of quota. A record counts tasks, a catch belongs to the owner whose work was caught, and only a failure on the same file or symbol contradicts an approval (#35).
|
|
20
|
+
- Recovery after an unplanned stop: the hub keeps each attached peer's session identity while it runs; a hub started after a crash reports what happened to each peer in `ahub status`, resumes Kimi (ACP `session/load`), a headless Pi and the local worker (on its recorded route) when `recovery.auto_resume_after_crash` is on, and gives each peer a notice of its deliveries left in `needs_review` with its next delivery. A stop someone asked for, even one past the shutdown deadline, is not taken for a crash (#37).
|
|
21
|
+
- The local worker's sandbox starts from deny default: commands may run and read the system, toolchain and project directories (and the selected Xcode or Command Line Tools dir) and write the project and temp, nothing else; `/Applications`, `/nix`, `/Volumes` and `/Users/Shared` now need `local.read_allow`; `local.sandbox: "allow-default"` (machine-local) keeps the profile of 0.9 and earlier for one release; with network on, the public CA bundles stay readable for TLS. Per-peer `capabilities` (`propose`, `assign`, `remember`, `important`) are enforced by the daemon, a malformed entry grants nothing and every refusal is logged, under deny-default the worker's commands can no longer reach the LaunchServices, CoreServices or SecurityServer brokers, and a test pins that a peer can never answer a permission request (#39).
|
|
22
|
+
|
|
5
23
|
## 0.9.0
|
|
6
24
|
|
|
7
25
|
- Early conflict detection: in a git work tree, a turn that changes a file another owner's open task changed earlier warns both owners (and the console, and `events.jsonl`), once per file and task, marked concurrent when another peer worked meanwhile. `ahub check-path` and the PreToolUse hook template `templates/claude-hooks.json` give Claude the same warning before an edit, without blocking it (#32).
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Cod
|
|
|
4
4
|
hub-owned local-LLM worker collaborate as peers in independent project directories, with
|
|
5
5
|
task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
|
|
6
6
|
|
|
7
|
-
Status: 0.
|
|
7
|
+
Status: 0.11.0, control protocol 10. Durable delivery records distinguish queued
|
|
8
8
|
work from uncertain execution. The [smoke checklist](docs/smoke.md) records
|
|
9
9
|
verified paths and remaining prerequisites.
|
|
10
10
|
|
|
@@ -28,7 +28,7 @@ cd <your project> && ahub init && ahub up && ahub tail
|
|
|
28
28
|
Or install the same version from GitHub:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
bun add -g github:STAIxBWLB/agent-hub#v0.
|
|
31
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.11.0 && ahub setup
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
The installed commands remain `ahub` and `agent-hub`.
|
package/docs/operations.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Operations guide
|
|
2
2
|
|
|
3
|
-
This guide describes ahub 0.
|
|
3
|
+
This guide describes ahub 0.11.0 and control protocol 10. Live verification
|
|
4
4
|
results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
|
|
5
5
|
|
|
6
6
|
## Install and start
|
|
@@ -27,11 +27,55 @@ what the hub runs, which files it sends as credentials, where task text goes,
|
|
|
27
27
|
or how far the local worker's sandbox reaches (`kimi_cmd`, `codex_bin`,
|
|
28
28
|
`pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`,
|
|
29
29
|
`omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`,
|
|
30
|
-
`local.read_allow`, `local.bash_network`) are machine-local:
|
|
31
|
-
from a file git confirms nobody committed. Put them in
|
|
30
|
+
`local.read_allow`, `local.bash_network`, `local.network_allow`, `local.sandbox`) are machine-local:
|
|
31
|
+
they apply only from a file git confirms nobody committed. Put them in
|
|
32
32
|
`.agenthub/config.local.json` (`ahub init` adds it to `.gitignore`), which is
|
|
33
33
|
read after `config.json`; outside a git repository they keep their defaults, and
|
|
34
34
|
an empty value always means the default.
|
|
35
|
+
|
|
36
|
+
The local worker's commands run under a sandbox that starts from deny default
|
|
37
|
+
(0.10): they may run and read the system, toolchain and project directories and
|
|
38
|
+
the selected Xcode or Command Line Tools dir (`xcode-select -p`; for an Xcode
|
|
39
|
+
app, its whole `Contents`, whose `SharedFrameworks` its tools load), write the
|
|
40
|
+
project and a temp dir of their own (`TMPDIR`, made for each command and
|
|
41
|
+
removed when it ends; one left by a hub crash, named `ahub-cmd-*`, goes with
|
|
42
|
+
the OS temp cleanup), and nothing else; the shared temp dirs are closed.
|
|
43
|
+
|
|
44
|
+
Network is off unless `local.bash_network` says otherwise. With `true` it goes
|
|
45
|
+
only through the hub's egress proxy on a loopback port (0.11): commands get
|
|
46
|
+
`HTTPS_PROXY` and the other proxy variables, the proxy opens HTTPS (`CONNECT`,
|
|
47
|
+
port 443 unless an entry names one) to the hosts in `local.network_allow`, and
|
|
48
|
+
the profile denies every other connection, direct egress and other loopback
|
|
49
|
+
ports (claude-mem's, the Codex app-server's) included. A listed name also
|
|
50
|
+
covers its subdomains; a name that resolves to a loopback or private address is
|
|
51
|
+
refused, and so is plain HTTP. Each such refusal is a `network: refused` line
|
|
52
|
+
in `hub.log`, by method and host. The default list holds the npm, PyPI, crates.io and Go module
|
|
53
|
+
registries and GitHub's code hosts; set `local.network_allow` in
|
|
54
|
+
`config.local.json` to replace it. `"direct"` keeps the open network of 0.10 and
|
|
55
|
+
earlier for one release. With network on, commands may also read the public CA
|
|
56
|
+
bundles and Python's `certifi/cacert.pem`, which the `*.pem` key deny would
|
|
57
|
+
otherwise hide.
|
|
58
|
+
`"local": { "sandbox": "allow-default" }` in `config.local.json` brings back the
|
|
59
|
+
profile of 0.9 and earlier for one release, should a toolchain need a path the
|
|
60
|
+
new one lacks; please report it. Newly closed outside home: `/Applications`
|
|
61
|
+
(an app's bundled CLI), `/nix`, `/Volumes` and `/Users/Shared`. A toolchain
|
|
62
|
+
there, or elsewhere in your home (a CI tool cache, a version manager the profile
|
|
63
|
+
does not list), needs its directory in `local.read_allow`, for example
|
|
64
|
+
`"/nix"` or `"~/.pixi"`.
|
|
65
|
+
|
|
66
|
+
`capabilities` in `.agenthub/config.json` narrows what a peer may do with the
|
|
67
|
+
hub's tools: list a peer and it keeps only the capabilities named, from
|
|
68
|
+
`propose` (`hub_task_propose`), `assign` (proposing with another peer as owner),
|
|
69
|
+
`remember` (the `hub_remember` tool; the notes the hub itself keeps of done
|
|
70
|
+
summaries and review verdicts are not gated) and `important` (`[IMPORTANT]`
|
|
71
|
+
messages). For example `"capabilities": { "local": ["propose", "remember"] }`.
|
|
72
|
+
A peer that is not listed keeps all of them, a listed peer whose value is not a
|
|
73
|
+
list gets none, and an unknown capability name grants nothing; `hub.log` says
|
|
74
|
+
so for each, and logs every refusal (`capabilities:`). A refused tool call says
|
|
75
|
+
which capability is missing; an `[IMPORTANT]` turn answer without the
|
|
76
|
+
capability goes out as status, with a note to the sender.
|
|
77
|
+
Approvals are never a capability: only the console (and the dashboard) answers a
|
|
78
|
+
permission request.
|
|
35
79
|
A committed value is ignored with a line in `hub.log`, a note from `ahub
|
|
36
80
|
codex` and `ahub models`, and a row in `ahub doctor`.
|
|
37
81
|
|
|
@@ -87,6 +131,21 @@ external side effect happened unless the task's refs and a live readback show
|
|
|
87
131
|
that effect. The hub can reassign after repeated review changes according to
|
|
88
132
|
the routing configuration.
|
|
89
133
|
|
|
134
|
+
A review request carries a checklist: map the changed signatures and call sites
|
|
135
|
+
to the task's plan (without one, to its detail, which the request then
|
|
136
|
+
includes), read the check result, and list what is
|
|
137
|
+
unmet (`hub_review` takes `unmet`, `ahub review ... --unmet <item>`). The hub
|
|
138
|
+
records how each review turned out, per implementer, reviewer and class:
|
|
139
|
+
approved; caught (changes were requested and the owner's redo was approved);
|
|
140
|
+
contradicted (within a week, work on the same file or symbol failed its check
|
|
141
|
+
or review); escalated (after the reviewer asked for changes). A record counts
|
|
142
|
+
tasks, not verdicts. `ahub task show <id>` lists a task's outcomes and `ahub
|
|
143
|
+
route explain` shows each reviewer's record with the implementer. With
|
|
144
|
+
`"review": { "adaptive": true }` in `.agenthub/config.json`, reviewers with at
|
|
145
|
+
least `min_reviews` (5) reviews of that implementer in the class are ordered by
|
|
146
|
+
how their reviews held up, ahead of quota but after idle before busy; off by
|
|
147
|
+
default, which leaves assignment as it was.
|
|
148
|
+
|
|
90
149
|
An agent can claim work nobody assigned it by proposing a task with itself as
|
|
91
150
|
owner; without a class, and with no model to name one, the claim is filed as
|
|
92
151
|
`implement`. A claim or an accept can carry a plan: the files, symbols and
|
|
@@ -383,8 +442,8 @@ source and carries every recovery fix released up to it. Protocol 8 and older
|
|
|
383
442
|
project directory, without replacing the global CLI first:
|
|
384
443
|
|
|
385
444
|
```bash
|
|
386
|
-
bunx --package @staix/agent-hub@0.
|
|
387
|
-
bunx --package @staix/agent-hub@0.
|
|
445
|
+
bunx --package @staix/agent-hub@0.11.0 ahub upgrade --to 0.11.0 --dry-run
|
|
446
|
+
bunx --package @staix/agent-hub@0.11.0 ahub upgrade --to 0.11.0 --yes
|
|
388
447
|
```
|
|
389
448
|
|
|
390
449
|
| Running now | Coordinator to use |
|
|
@@ -407,25 +466,28 @@ no blocker, and `--yes` stops at staging ("target protocol requires a newer
|
|
|
407
466
|
coordinator") with an operation left to clear by `ahub recovery abort <id>`. An
|
|
408
467
|
older 0.7.x CLI may lack recovery fixes released after it. The
|
|
409
468
|
[smoke ledger](smoke.md) records dry-runs from real 0.6.4 and 0.7.5 hubs (issue
|
|
410
|
-
#75)
|
|
469
|
+
#75) and these applied upgrades: one with the 0.7.0 coordinator, one with the
|
|
470
|
+
0.9.0 coordinator from a running 0.8.1 hub with tasks and a budget pause, and
|
|
471
|
+
one with the 0.10.0 coordinator from a 0.9.0 hub with one completion check
|
|
472
|
+
running and one queued.
|
|
411
473
|
|
|
412
474
|
The coordinator verifies and retains the exact target package, preserves its
|
|
413
475
|
own source, and promotes the global CLI only after restored projects pass
|
|
414
476
|
readback.
|
|
415
477
|
|
|
416
|
-
Once the installed CLI is 0.
|
|
478
|
+
Once the installed CLI is 0.11.0, review the current project or all registered
|
|
417
479
|
projects first:
|
|
418
480
|
|
|
419
481
|
```bash
|
|
420
482
|
ahub restart --dry-run
|
|
421
|
-
ahub upgrade --to 0.
|
|
483
|
+
ahub upgrade --to 0.11.0 --dry-run
|
|
422
484
|
```
|
|
423
485
|
|
|
424
486
|
Apply only after reviewing the plan:
|
|
425
487
|
|
|
426
488
|
```bash
|
|
427
489
|
ahub restart --yes
|
|
428
|
-
ahub upgrade --to 0.
|
|
490
|
+
ahub upgrade --to 0.11.0 --yes
|
|
429
491
|
ahub recovery status <operation-id>
|
|
430
492
|
ahub recovery resume <operation-id>
|
|
431
493
|
ahub recovery abort <operation-id>
|
|
@@ -448,6 +510,23 @@ block: `limits` (12 messages a minute per sender, 6 per recipient, 6
|
|
|
448
510
|
`[IMPORTANT]` an hour, 120 s repeats) and `budget.wait_max_min` (30). Set them
|
|
449
511
|
to 0 to opt out.
|
|
450
512
|
|
|
513
|
+
0.10.0 changes every project's local worker: its commands run under the
|
|
514
|
+
deny-default sandbox described above, and a toolchain outside the listed
|
|
515
|
+
directories needs `local.read_allow`; `"local": { "sandbox": "allow-default" }`
|
|
516
|
+
in `config.local.json` restores the old profile for one release. Off unless set:
|
|
517
|
+
`review.adaptive`, `recovery.auto_resume_after_crash` and `capabilities`. A hub
|
|
518
|
+
before 0.10.0 keeps no session record, so a crash of one is not reported as such
|
|
519
|
+
by the next start.
|
|
520
|
+
|
|
521
|
+
0.11.0 changes what `local.bash_network: true` means: the local worker's and
|
|
522
|
+
Pi's commands reach the network only through the hub's egress proxy, to the
|
|
523
|
+
hosts in `local.network_allow` (package registries and GitHub's code hosts by
|
|
524
|
+
default). `local.network_allow` is machine-local and replaces the default list:
|
|
525
|
+
a project that needs another host sets it in `config.local.json` with the
|
|
526
|
+
defaults it still needs. `"direct"` keeps the open network of 0.10.0 for one
|
|
527
|
+
release. Each command gets a temp dir of its own, and under deny-default the
|
|
528
|
+
shared temp dirs are closed.
|
|
529
|
+
|
|
451
530
|
The 0.7.0 transition stages the verified package and runs a retained
|
|
452
531
|
coordinator from the source tree. It accepts a verified protocol-9 source and
|
|
453
532
|
moves to a protocol-10 target. The source journal, queued envelopes, tasks,
|
|
@@ -469,6 +548,33 @@ Do not run an upgrade with an incompatible active protocol, an unverified
|
|
|
469
548
|
terminal binding, or an unresolved operation lock. Dry-run performs no package,
|
|
470
549
|
plugin, daemon, or terminal mutation.
|
|
471
550
|
|
|
551
|
+
### After an unplanned stop
|
|
552
|
+
|
|
553
|
+
While it runs, the hub keeps each attached peer's session identity in
|
|
554
|
+
`.agenthub/state/sessions.json` (ids and launch options, no message text); a stop
|
|
555
|
+
removes it as it begins. When a hub starts and finds the file, the previous run
|
|
556
|
+
died (`kill -9`, a crash, a lost machine), and `ahub status` and the console say
|
|
557
|
+
what happened to each peer. Hubs before 0.10.0 kept no such record, so a
|
|
558
|
+
crash of one is not reported this way.
|
|
559
|
+
|
|
560
|
+
- Deliveries that were in flight are in `needs_review` (`ahub queue list`), as
|
|
561
|
+
before. When a peer next attaches, its next delivery starts with a notice that
|
|
562
|
+
lists them by id, sender and task (`[pii]` for a PII task), never their text.
|
|
563
|
+
- Kimi, Pi and the local worker run inside the hub, so they died with it. With
|
|
564
|
+
`"recovery": { "auto_resume_after_crash": true }` in `.agenthub/config.json` the
|
|
565
|
+
hub starts them again: Kimi loads its recorded session (ACP `session/load`), Pi
|
|
566
|
+
resumes its session file, and the local worker starts without its history, on
|
|
567
|
+
its recorded route (or pinned model). Off by default: the report then says
|
|
568
|
+
what to start. With `pi.auto_start` on, Pi comes back on its recorded
|
|
569
|
+
headless session whether or not auto-resume is on (it runs on-prem, so this
|
|
570
|
+
spends no cloud quota), and on a fresh session if that fails, keeping the
|
|
571
|
+
recorded backend and model; the report says which. Malformed records in
|
|
572
|
+
`sessions.json` are skipped. A Pi that ran in a terminal (`--mode tui`) is never started on its
|
|
573
|
+
recorded session by the hub; the report gives the command, and with
|
|
574
|
+
`pi.auto_start` a fresh headless Pi starts instead.
|
|
575
|
+
- Codex's app-server died with the hub; run `ahub codex` again. Claude Code's
|
|
576
|
+
plugin reconnects by itself while that session is open.
|
|
577
|
+
|
|
472
578
|
## Evidence and limits
|
|
473
579
|
|
|
474
580
|
The 0.6.4 release has real measurements in [the smoke checklist](smoke.md):
|
package/docs/quickstart.md
CHANGED
package/docs/security.md
CHANGED
|
@@ -7,12 +7,15 @@ 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, a fixed header line plus 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.** `ahub claude` and `ahub codex` add nothing that weakens the agents' own prompts. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They touch no file and run no process, and every call passes the hub's own checks. The match relies on the agent putting the tool name in the request's `title`, as Kimi does; an ACP agent that titles calls with model-written text must not be configured as `kimi_cmd`. A payload longer than the console shows is marked as cut and never offers a session-wide grant. `--unattended` turns prompts off, says so loudly, and is never the default.
|
|
10
|
-
- **A committed config cannot choose launch commands, credential files, data endpoints or a wider sandbox.** The machine-local fields (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`) apply only from a config file git confirms nobody committed: `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust.
|
|
10
|
+
- **A committed config cannot choose launch commands, credential files, data endpoints or a wider sandbox.** The machine-local fields (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`) apply only from a config file git confirms nobody committed: `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust.
|
|
11
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. 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.
|
|
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
|
-
- **The
|
|
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
|
+
- **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) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval, and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git/hooks` or `.git/config` writes; no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses, so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, for one release). The allowlist is a deny-default guarantee: under allow-default, DNS and the system's network daemons stay reachable around the proxy. One channel stays open under deny-default too: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. `local.sandbox: "allow-default"`, machine-local, brings back the profile of 0.9 and earlier for one release. Under deny-default the shared temp dirs (the user's and `/private/tmp`) are closed; allow-default still reads them. Without the sandbox there is no `bash` tool.
|
|
16
|
+
- **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.
|
|
15
17
|
- **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.
|
|
18
|
+
- **Free text is screened too.** On an ordinary task, a done summary (with its check output: a match withholds the whole note), a review note, an unmet item or a budget handoff that matches a PII pattern (issue #69) is not saved to claude-mem, and every peer, `local` included, gets a stub naming `ahub task show <id>`; the board keeps the text, `hub.log` notes the withholding by task id only, and `ahub ask` shows such a note only when its model is reached on campus. `hub_remember` refuses a match; what a vendor agent writes to its own memory is not screened.
|
|
16
19
|
- **Secrets stay where they are read.** The gateway key and Cloudflare Access values are read inside the gateway client and go only into request headers: never into logs, errors, envelopes, tool output, memory, or the generated Switchyard config (the key travels by environment variable name). Subscription logins of Claude, Codex and Kimi are never proxied or pooled.
|
|
17
20
|
- **The hub's own model calls are fenced.** Digest condensation and task triage read agent-written text as data; their output is capped text framed as untrusted, or a value checked against a closed list. It is never used as a route, a peer id, a tool call or an instruction.
|
|
18
21
|
|
package/docs/smoke.md
CHANGED
|
@@ -901,3 +901,66 @@ peers attached.
|
|
|
901
901
|
- Run 1, before the hook was logged: Claude answered NONE. Whether the hook
|
|
902
902
|
did not fire or the model left the reminder out was not determined; that run
|
|
903
903
|
also had no stdin redirect (`< /dev/null`), which runs 2 and 3 had.
|
|
904
|
+
|
|
905
|
+
## Recovery after an unplanned stop (issue #37, 2026-10-01)
|
|
906
|
+
|
|
907
|
+
- AC1 baseline, released 0.7.11 in a scratch project, the fake ACP agent standing
|
|
908
|
+
in for the Kimi binary (`kimi_cmd`), `kill -9` of the daemon:
|
|
909
|
+
- the ACP child exited with the daemon;
|
|
910
|
+
- `ahub status` failed to reach the stale manifest's port;
|
|
911
|
+
- `ahub up` started a hub with no peers attached; nothing recorded Kimi's
|
|
912
|
+
session, so it could only start a new one.
|
|
913
|
+
- The same run with this branch and `auto_resume_after_crash` on: `ahub up`
|
|
914
|
+
reported the crash and `kimi resumed: ... session s1 ... (ACP session/load)`,
|
|
915
|
+
and Kimi was idle on its recorded session id.
|
|
916
|
+
- Pending, needs real accounts: Kimi 2.x (does it offer `loadSession`, and does
|
|
917
|
+
the resumed session keep its context), Pi with a real session file, and Codex
|
|
918
|
+
and Claude reattachment after `kill -9`.
|
|
919
|
+
|
|
920
|
+
## 0.8.1 to 0.9.0 attended upgrade (2026-10-01)
|
|
921
|
+
|
|
922
|
+
A scratch project (a git work tree whose `.agenthub/config.json` was written by
|
|
923
|
+
the 0.8.1 `ahub init`) ran a 0.8.1 hub with two proposed tasks, one with a path
|
|
924
|
+
and a detail, and an open budget pause: a peer had attached once and gone
|
|
925
|
+
offline, and `ahub budget set` fed it a 95% reading. No completion check was
|
|
926
|
+
configured or running, and no peer was attached at the upgrade.
|
|
927
|
+
|
|
928
|
+
- `bunx --package @staix/agent-hub@0.9.0 ahub upgrade --to 0.9.0 --dry-run`
|
|
929
|
+
exited 0: one project, source 0.8.1, protocol 10, no blockers.
|
|
930
|
+
- `--yes` scheduled the operation, which completed in about 10 s with the
|
|
931
|
+
project `verified`.
|
|
932
|
+
- After release the hub ran 0.9.0 under a new instance id. Both tasks were on
|
|
933
|
+
the board unchanged, now with `deps: []` (the column the target adds on open),
|
|
934
|
+
the budget pause kept its reset time, and the `outcomes` table was created.
|
|
935
|
+
- The coordinator promoted the global CLI to 0.9.0, and `ahub setup --yes`
|
|
936
|
+
installed the 0.9.0 plugin.
|
|
937
|
+
- For about two minutes after the publish step logged `+ @staix/agent-hub@0.9.0`,
|
|
938
|
+
`npm view` still showed `latest: 0.8.1`; the registry caught up without any
|
|
939
|
+
action.
|
|
940
|
+
|
|
941
|
+
## 0.9.0 to 0.10.0 attended upgrade (2026-10-01)
|
|
942
|
+
|
|
943
|
+
A scratch project (a git work tree initialized by the 0.9.0 `ahub init`) ran a
|
|
944
|
+
0.9.0 hub with tasks on its board and a completion check for the review class, set in
|
|
945
|
+
`.agenthub/config.local.json` (`"review": "sleep 40"`, timeout 120 s). No peer
|
|
946
|
+
was attached. Times are UTC.
|
|
947
|
+
|
|
948
|
+
- At 05:54:46 the console marked task #1 done, which started its 40 s check.
|
|
949
|
+
`bunx --package @staix/agent-hub@0.10.0 ahub upgrade --to 0.10.0 --yes` right
|
|
950
|
+
after, about 90 s after the publish, failed with "registry metadata
|
|
951
|
+
unavailable for @staix/agent-hub@0.10.0" and scheduled nothing; the 0.9.0 hub
|
|
952
|
+
kept running. This is the registry lag from the 0.9.0 entry, met here as an
|
|
953
|
+
error instead of a stale `npm view`.
|
|
954
|
+
- At 05:55:03 the console marked task #3 done, which queued its check behind
|
|
955
|
+
#1's (checks run one at a time), and `--yes` was applied in the same second.
|
|
956
|
+
Its plan listed one project, source 0.9.0, no blockers. The source did not
|
|
957
|
+
commit while a check was running or queued: #1's passed at 05:55:26 and #3's
|
|
958
|
+
at 05:56:07, the source committed and stopped at 05:56:07.1, and the 0.10.0
|
|
959
|
+
hub was up at 05:56:07.7. The operation, global install included, completed
|
|
960
|
+
at 05:56:15.
|
|
961
|
+
- After release the hub ran 0.10.0 under a new instance id. All three tasks
|
|
962
|
+
were on the board with their full history (#1 and #3 approved by their
|
|
963
|
+
checks, #2 still proposed), and no history entry recorded an interrupted
|
|
964
|
+
check.
|
|
965
|
+
- The coordinator promoted the global CLI to 0.10.0, and `ahub setup --yes`
|
|
966
|
+
installed the 0.10.0 plugin.
|
|
@@ -1016,3 +1016,114 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
1016
1016
|
suppression. Limits are off in `DEFAULT_CONFIG` and on with any project config
|
|
1017
1017
|
(12/min per sender, 6/min per recipient, 6 important an hour, 120 s repeats).
|
|
1018
1018
|
|
|
1019
|
+
## Amendment: review checklists and outcomes (issue #35)
|
|
1020
|
+
|
|
1021
|
+
- A review request carries a checklist: map the changed signatures and call
|
|
1022
|
+
sites to the task's plan, or without a plan to the task detail, which the
|
|
1023
|
+
request then includes; the result of the class's check, or that none ran; and
|
|
1024
|
+
`hub_review`'s `unmet`, one item each, which is appended to the verdict note.
|
|
1025
|
+
- Review outcomes live in a `reviews` table in hub.db: implementer, reviewer,
|
|
1026
|
+
class, kind, task, time. Kinds: `approved`; `caught` (each reviewer who asked
|
|
1027
|
+
for changes on the current owner's work, which was then approved);
|
|
1028
|
+
`contradicted` (an approval, within seven days, of a task on the same file or
|
|
1029
|
+
symbol as one whose check failed or whose review asked for changes, a
|
|
1030
|
+
directory or `.` not counting; once per approval; console approvals and PII
|
|
1031
|
+
tasks are not judged); `escalated` (the reviewer had asked for changes on the
|
|
1032
|
+
work that was escalated, so not an escalation of unreviewed work).
|
|
1033
|
+
- A reviewer's record with an implementer in a class counts tasks: n = the tasks
|
|
1034
|
+
it reviewed (approved, caught or escalated), held = (n - contradicted tasks) /
|
|
1035
|
+
n. `assign()` takes the records as input and always shows them in its trace;
|
|
1036
|
+
with `review.adaptive` it orders reviewer candidates that have at least
|
|
1037
|
+
`min_reviews` by held, others keep their place, and the implementer is never
|
|
1038
|
+
a candidate. The record is applied after quota, so the order is idle before
|
|
1039
|
+
busy, then the record, then quota.
|
|
1040
|
+
- Task paths are stored in one spelling (issue #67): no leading `./`, no repeated
|
|
1041
|
+
or trailing `/`, the root as `.`; older rows are compared in that spelling.
|
|
1042
|
+
Blame needs the same path, so a directory never blames the files under it,
|
|
1043
|
+
though two tasks that both claim the same directory still blame each other: a
|
|
1044
|
+
path does not say whether it is a directory. Existing rows are never
|
|
1045
|
+
rewritten.
|
|
1046
|
+
Ownership events (`assigned`, `escalated`, `reassigned`, `unassigned`) record
|
|
1047
|
+
the owner they leave, so a reassignment to the same owner does not end the
|
|
1048
|
+
window in which a catch counts; rows written before keep the old rule.
|
|
1049
|
+
|
|
1050
|
+
## Amendment: recovery after an unplanned stop (issue #37)
|
|
1051
|
+
|
|
1052
|
+
- The continuous record is `sessions.json` (instance id, time, and each attached
|
|
1053
|
+
peer's `recoveryMetadata()`), rewritten when a peer's state changes and
|
|
1054
|
+
removed when a stop of the run that wrote it begins (a stop that then runs past
|
|
1055
|
+
the shutdown deadline is still not a crash). Found at start, with no
|
|
1056
|
+
controlled-restart state in play, it means the previous run crashed; the new
|
|
1057
|
+
run takes the record over at once, so its own clean stop removes it even when
|
|
1058
|
+
no peer attaches. A controlled restart's target removes any record it finds.
|
|
1059
|
+
- Limits: a second crash before the peers attach loses their loss notices (the
|
|
1060
|
+
journal rows stay in `needs_review`, shown by `ahub queue list`), and the first
|
|
1061
|
+
attach rewrites the record with only the attached peers. A Pi in TUI mode is
|
|
1062
|
+
reported with its command, not resumed: the CLI runs its terminal.
|
|
1063
|
+
- `pi.auto_start` (issue #66) counts as consent to start Pi after a crash too:
|
|
1064
|
+
crash recovery, not the plain auto-start, starts it, on the recorded headless
|
|
1065
|
+
session first and with a `fresh` start (one that does not inherit the failed
|
|
1066
|
+
launch's pending session, keeps the recorded backend and model) if that fails
|
|
1067
|
+
or nothing headless was recorded. If crash recovery itself fails, the plain
|
|
1068
|
+
auto-start runs after all.
|
|
1069
|
+
- Resume goes through the same start path as `ahub kimi` / `ahub pi` / `ahub
|
|
1070
|
+
local`: Kimi with `sessionId` (ACP `session/load`, refused when the agent does
|
|
1071
|
+
not offer `loadSession`), Pi with its session file, the local worker afresh.
|
|
1072
|
+
Codex and Claude are reported, not resumed: the TUI and the Claude session live
|
|
1073
|
+
outside the hub. The issue's Codex `thread/resume` needs the TUI, so the report
|
|
1074
|
+
names the thread instead.
|
|
1075
|
+
- Loss notices use the bus preface, not an envelope: a peer's `needs_review`
|
|
1076
|
+
rows block its later deliveries, so a notice queued behind them would arrive
|
|
1077
|
+
only after they are resolved anyway; the preface leads that next delivery. Only
|
|
1078
|
+
rows still in `needs_review` when the peer attaches are listed.
|
|
1079
|
+
- `recovery.auto_resume_after_crash` is off by default, also with a project
|
|
1080
|
+
config: an automatic start spends quota the user did not ask for.
|
|
1081
|
+
|
|
1082
|
+
## Amendment: deny-default sandbox and capabilities (issue #39)
|
|
1083
|
+
|
|
1084
|
+
- The deny-default profile allows exec and reads of the system directories
|
|
1085
|
+
(`/usr`, `/bin`, `/sbin`, `/System`, `/Library`, `/opt`, `/private/etc`, the
|
|
1086
|
+
dyld and timezone databases), the toolchain directories in home, `read_allow`,
|
|
1087
|
+
the project, its external git dirs, the selected developer dir (`xcode-select
|
|
1088
|
+
-p`) and a temp dir made for each command (issue #63; the shared user temp
|
|
1089
|
+
dir and `/private/tmp` are not open); writes to the project, its git dirs and
|
|
1090
|
+
that temp dir; a short list of mach services (directory
|
|
1091
|
+
lookups, logging, notifications), plus name resolution and TLS trust when
|
|
1092
|
+
network is on, the trust being the public CA bundles allowed by exact path
|
|
1093
|
+
after the denies (the `*.pem` key deny matches them), and any path ending in
|
|
1094
|
+
`/certifi/cacert.pem` (Python's own bundle, issue #64); no brokers that act
|
|
1095
|
+
outside the sandbox (LaunchServices, SecurityServer). The denies at the end (credential
|
|
1096
|
+
stores, the denylist, `.agenthub`, git hooks and config) are shared by both
|
|
1097
|
+
bases.
|
|
1098
|
+
- The issue's allowlist proxy for `local.bash_network` is not built here: with
|
|
1099
|
+
the flag on, network is allowed as in 0.9 and earlier. Issue #65 builds it
|
|
1100
|
+
(amendment below).
|
|
1101
|
+
- `local.sandbox` ("deny-default" | "allow-default") is machine-local, since
|
|
1102
|
+
"allow-default" widens the sandbox.
|
|
1103
|
+
- Capabilities: `propose`, `assign` (a proposal naming another peer as owner),
|
|
1104
|
+
`remember`, `important`. A peer not listed in `capabilities` has all of them,
|
|
1105
|
+
which keeps today's behaviour, and a listed peer whose value is not a list has
|
|
1106
|
+
none; the console user and the hub are never limited. `remember` gates the
|
|
1107
|
+
`hub_remember` tool only, not the notes the hub keeps of done summaries and
|
|
1108
|
+
review verdicts. Every refusal, a malformed entry and an unknown capability
|
|
1109
|
+
name are logged.
|
|
1110
|
+
`important` is checked where messages are admitted, the others in the task
|
|
1111
|
+
operations, so in-process tools (the local worker, Pi) are covered too.
|
|
1112
|
+
|
|
1113
|
+
## Amendment: egress proxy for the local worker (issue #65)
|
|
1114
|
+
|
|
1115
|
+
- With `local.bash_network: true` the daemon runs an HTTP `CONNECT` proxy on a
|
|
1116
|
+
loopback port for the hub run (`src/local/proxy.ts`, built-ins only). The
|
|
1117
|
+
profile allows outbound network only to that port, looks up TLS trust but no
|
|
1118
|
+
DNS (the proxy resolves), and commands get the proxy variables.
|
|
1119
|
+
- The proxy opens a tunnel only to a host in `local.network_allow`
|
|
1120
|
+
(machine-local): a name also covers its subdomains, an address only itself,
|
|
1121
|
+
port 443 unless an entry names one (`host:port`). A name that resolves to a
|
|
1122
|
+
loopback, private, link-local or carrier-grade NAT address is refused; an
|
|
1123
|
+
address is reached only when listed. Plain HTTP is refused. No TLS
|
|
1124
|
+
interception. Each refusal is logged with the host only.
|
|
1125
|
+
- Decisions recorded in the issue: the default list holds the npm, Yarn, PyPI,
|
|
1126
|
+
crates.io and Go module registries and GitHub's code hosts, so a project that
|
|
1127
|
+
already had network on keeps installing packages; `"direct"` keeps the open
|
|
1128
|
+
network of 0.10 and earlier for one release.
|
|
1129
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Young Joon Lee",
|
|
@@ -15727,7 +15727,7 @@ class ControlClient {
|
|
|
15727
15727
|
// package.json
|
|
15728
15728
|
var package_default = {
|
|
15729
15729
|
name: "@staix/agent-hub",
|
|
15730
|
-
version: "0.
|
|
15730
|
+
version: "0.11.0",
|
|
15731
15731
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
15732
15732
|
license: "MIT",
|
|
15733
15733
|
type: "module",
|
|
@@ -15820,7 +15820,7 @@ var TASK_TOOLS = [
|
|
|
15820
15820
|
tool("hub_task_decline", "Pass on a task assigned to you; the hub offers it to the next peer.", { id, reason: str }, ["id"]),
|
|
15821
15821
|
tool("hub_task_done", "Mark your task finished. It goes to its reviewer with your summary and refs. When the project configures a check for its class, the hub runs it first and the result comes as a task message: a failed check keeps the task with you.", { id, summary: { type: "string", description: "what changed, why, and the check you ran with its result" }, refs }, ["id", "summary"]),
|
|
15822
15822
|
tool("hub_task_list", "The task board. PII tasks show as [pii].", { state: { type: "string", enum: ["proposed", "in_progress", "in_review", "approved", "changes_requested"] }, ready: { type: "boolean", description: "only proposed tasks with nothing left to wait for" } }),
|
|
15823
|
-
tool("hub_review", "Give your verdict on a task you were asked to review. Two changes_requested in a row move the task to another peer.", { id, verdict: { type: "string", enum: ["approved", "changes_requested"] }, note: str }, ["id", "verdict"]),
|
|
15823
|
+
tool("hub_review", "Give your verdict on a task you were asked to review: map the changed signatures and call sites to the task's plan or detail, read the check result, and list what is unmet. Two changes_requested in a row move the task to another peer.", { id, verdict: { type: "string", enum: ["approved", "changes_requested"] }, note: str, unmet: { type: "array", items: str, description: "each requirement of the plan or detail that the change does not meet" } }, ["id", "verdict"]),
|
|
15824
15824
|
tool("hub_checkpoint", "Answer a checkpoint request from the hub (your quota window is nearly used up): what you were doing, what is half done, what whoever continues must know. Write the same to .agenthub/checkpoint.md first if you can.", { summary: str }, ["summary"]),
|
|
15825
15825
|
tool("hub_remember", "Save a decision, finding, contract or fail to the memory all agents share (claude-mem); the other agents also get it with their next message. A fail is an approach you tried that does not work, and why: the most useful note, it stops the others spending their quota on it. Do not retry what a fail note rules out without new evidence. Conclusions worth recalling, not chatter.", { text: str, title: str, kind: { type: "string", enum: [...NOTE_KINDS] }, task: id }, ["text"])
|
|
15826
15826
|
];
|
package/src/adapters/acp.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface AcpOptions {
|
|
|
22
22
|
cmd: string[];
|
|
23
23
|
/** Coordinator-visible selected model only; contains no prompts or command arguments. */
|
|
24
24
|
launchModel?: string;
|
|
25
|
+
/** Load this earlier session (ACP `session/load`) instead of starting a new one: crash recovery, issue #37. */
|
|
26
|
+
resumeSessionId?: string;
|
|
25
27
|
cwd: string;
|
|
26
28
|
/** Optional launch environment; recovery authority is always removed before spawn. */
|
|
27
29
|
env?: NodeJS.ProcessEnv;
|
|
@@ -92,11 +94,17 @@ export class AcpPeer extends BasePeer {
|
|
|
92
94
|
createInterface({ input: proc.stdout }).on("line", (line) => this.onLine(line));
|
|
93
95
|
|
|
94
96
|
const handshake = async () => {
|
|
95
|
-
await this.request("initialize", {
|
|
97
|
+
const init = await this.request("initialize", {
|
|
96
98
|
protocolVersion: 1,
|
|
97
99
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
98
100
|
});
|
|
99
|
-
|
|
101
|
+
const resume = this.opts.resumeSessionId;
|
|
102
|
+
if (!resume) return this.request("session/new", { cwd: this.opts.cwd, mcpServers: this.opts.mcpServers ?? [] });
|
|
103
|
+
// The agent replays the session as updates while it loads; they arrive before the peer is idle, so none of
|
|
104
|
+
// them is taken for an answer.
|
|
105
|
+
if (!init?.agentCapabilities?.loadSession) throw new Error(`${this.id} cannot load an earlier session (the agent offers no loadSession)`);
|
|
106
|
+
await this.request("session/load", { sessionId: resume, cwd: this.opts.cwd, mcpServers: this.opts.mcpServers ?? [] });
|
|
107
|
+
return { sessionId: resume };
|
|
100
108
|
};
|
|
101
109
|
const timeout = new Promise<never>((_, reject) => {
|
|
102
110
|
setTimeout(() => reject(new Error(`${this.id} did not complete the ACP handshake within ${HANDSHAKE_MS / 1000} s`)), HANDSHAKE_MS).unref();
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { renderDigest, replyAudience, replyParent, STANDING_INSTRUCTION, USER, type Envelope, type EnvelopeOpts, type PeerId } from "../hub/envelope.ts";
|
|
3
3
|
import { TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
|
|
4
4
|
import { BasePeer } from "../hub/peers.ts";
|
|
5
|
-
import { profile } from "../local/sandbox.ts";
|
|
5
|
+
import { profile, proxyEnv, type SandboxNetwork } from "../local/sandbox.ts";
|
|
6
6
|
import { runTool, TOOL_SCHEMAS, touchedPaths, type ToolContext } from "../local/tools.ts";
|
|
7
7
|
import type { Capture } from "../memory/capture.ts";
|
|
8
8
|
import type { ChatMessage, ChatResult, OmniRoute } from "../omniroute/client.ts";
|
|
@@ -17,7 +17,7 @@ export interface LocalOptions {
|
|
|
17
17
|
route?: string;
|
|
18
18
|
/** Model id sent straight to OmniRoute when the sidecar is absent, unhealthy or fails a call. */
|
|
19
19
|
fixedModel: string;
|
|
20
|
-
tools: { deny: string[]; permit: ToolContext["permit"]; bashNetwork?:
|
|
20
|
+
tools: { deny: string[]; permit: ToolContext["permit"]; bashNetwork?: SandboxNetwork; readAllow?: string[]; sandbox?: "deny-default" | "allow-default" };
|
|
21
21
|
capture?: Capture;
|
|
22
22
|
/** Runs a hub task tool (hub_task_*, hub_review, hub_remember) as this peer. Absent = the tools are not offered. */
|
|
23
23
|
taskTool?: (name: string, args: Record<string, unknown>, turn: { pii: boolean }) => Promise<string>;
|
|
@@ -67,7 +67,7 @@ export class LocalPeer extends BasePeer {
|
|
|
67
67
|
private readonly opts: LocalOptions,
|
|
68
68
|
) {
|
|
69
69
|
super(id, opts.watchdogMs);
|
|
70
|
-
this.sandboxProfile = profile(opts.cwd, opts.tools.bashNetwork ?? false, opts.tools.readAllow, opts.tools.deny);
|
|
70
|
+
this.sandboxProfile = profile(opts.cwd, opts.tools.bashNetwork ?? false, opts.tools.readAllow, opts.tools.deny, opts.tools.sandbox === "allow-default" ? "allow" : "deny");
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
recoveryMetadata(): Record<string, unknown> {
|
|
@@ -165,6 +165,7 @@ export class LocalPeer extends BasePeer {
|
|
|
165
165
|
deny: this.opts.tools.deny,
|
|
166
166
|
permit: this.opts.tools.permit,
|
|
167
167
|
sandboxProfile: this.sandboxProfile,
|
|
168
|
+
sandboxEnv: proxyEnv(this.opts.tools.bashNetwork ?? false),
|
|
168
169
|
send: (text, to) => {
|
|
169
170
|
const refused = this.onMessage?.(text, policy?.pii ? reply : { inReplyTo: replyParent(envs), to: to?.length ? to : replyAudience(envs) });
|
|
170
171
|
if (typeof refused === "string") return `not sent: ${refused}`;
|
package/src/cli/main.ts
CHANGED
|
@@ -73,7 +73,7 @@ const USAGE = `agent-hub ${VERSION}: Claude Code, Codex and Kimi as peers in one
|
|
|
73
73
|
ahub task propose [--class <c> | <class>] <title...> [--owner <peer>] [--path <p>]... [--after <id>]... [--urgent] [--detail <text>]
|
|
74
74
|
ahub task show|escalate <id> full task with history (PII text included) / hand it to the next peer in escalate_to
|
|
75
75
|
ahub task assign <id> <peer> give a task to a peer yourself
|
|
76
|
-
ahub review <id> approved|changes_requested [note...]
|
|
76
|
+
ahub review <id> approved|changes_requested [note...] [--unmet <item>]...
|
|
77
77
|
ahub remember <text...> save a note to the memory all agents share
|
|
78
78
|
ahub ask [--remember] <question...> answer from the task board, shared memory and this run's log, with the ids it rests on
|
|
79
79
|
ahub route explain <id> why a task went where it went
|
|
@@ -693,9 +693,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
693
693
|
},
|
|
694
694
|
|
|
695
695
|
review: async () => {
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
696
|
+
const { many, rest } = takeFlags(args, [], ["--unmet"]);
|
|
697
|
+
const [id, verdict, ...note] = rest;
|
|
698
|
+
if (!id || !verdict) fail("usage: ahub review <id> approved|changes_requested [note...] [--unmet <item>]...");
|
|
699
|
+
console.log(await taskOp("hub_review", { id: Number(id), verdict, note: note.join(" "), ...(many["--unmet"]?.length ? { unmet: many["--unmet"] } : {}) }));
|
|
699
700
|
},
|
|
700
701
|
|
|
701
702
|
remember: async () => console.log(await taskOp("hub_remember", { text: freeText(args, "ahub remember <text>") })),
|
|
@@ -744,6 +745,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
744
745
|
if (args.includes("--json")) return console.log(JSON.stringify(status, null, 2));
|
|
745
746
|
console.log(`hub pid ${status.pid}, control 127.0.0.1:${status.controlPort}, ${status.cwd}`);
|
|
746
747
|
if (status.deliveryError) console.log(` delivery storage: ${status.deliveryError}; dispatch is stopped`);
|
|
748
|
+
for (const line of (status as { crash?: string[] }).crash ?? []) console.log(` crash recovery: ${line}`);
|
|
747
749
|
const peers = Object.entries(status.peers as Record<string, PeerRow>);
|
|
748
750
|
for (const [id, p] of peers) console.log(peerLine(id, p));
|
|
749
751
|
const models = (status as any).models?.backends as BackendRow[] | undefined;
|