@staix/agent-hub 0.7.6 → 0.7.7
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 +7 -0
- package/README.md +2 -2
- package/docs/operations.md +28 -9
- package/docs/quickstart.md +1 -1
- package/docs/security.md +1 -1
- package/docs/smoke.md +114 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +24 -0
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1 -1
- package/src/adapters/acp.ts +34 -6
- package/src/cli/main.ts +2 -0
- package/src/hub/daemon.ts +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.7
|
|
6
|
+
|
|
7
|
+
- `ahub setup` without a terminal and without `--yes` prints the step it would take and exits non-zero instead of waiting forever at its prompt, as `restart` and `upgrade` already did. A piped answer (`yes | ahub setup`) is no longer read: use `--yes` (#73).
|
|
8
|
+
- The smoke ledger no longer names the internal gateway's address, and the package check fails when any published file contains a private IPv4 address, naming only the file and line (#74).
|
|
9
|
+
- Kimi's requests for the hub's own tools (`hub_send`, the task tools) are approved once without a console prompt, as Codex's already were, so a Kimi nobody watches can claim work and record notes. Kimi 2.1.1 approval prompts for other tools show the command again: the payload falls back to the streamed argument JSON when the request carries none. A payload too long to show whole is marked as cut and no longer offers a session-wide grant, for every ACP agent (#72).
|
|
10
|
+
- The operations guide's upgrade section names each supported source range (0.6.x, 0.7.x before the target) and the unsupported one (0.5.x and earlier): run the target release's coordinator through `bunx`. Dry-runs from running 0.6.4 and 0.7.5 hubs are recorded (#75).
|
|
11
|
+
|
|
5
12
|
## 0.7.6
|
|
6
13
|
|
|
7
14
|
- Shared notes reach the agents working now: `hub_remember` has a `fail` kind for approaches that do not work, and every saved note rides on the other peers' next delivery (newest 10, one line each) instead of waiting for their next session. A note that matches a PII pattern is refused. A preface returned by a failed delivery no longer overwrites one created meanwhile (#68).
|
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.
|
|
7
|
+
Status: 0.7.7, 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.7.
|
|
31
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.7.7 && 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.7.
|
|
3
|
+
This guide describes ahub 0.7.7 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
|
|
@@ -178,31 +178,50 @@ Rows without a live process are stale registrations; forget them with
|
|
|
178
178
|
|
|
179
179
|
## Upgrade and crash recovery
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
Upgrade running projects with the target release's own coordinator. It accepts
|
|
182
|
+
a running source on control protocol 9 (0.6.x) or 10 (0.7.0 and later) and only
|
|
183
|
+
a target on its own protocol, so the target's coordinator fits every supported
|
|
184
|
+
source and carries every recovery fix released up to it. Protocol 8 and older
|
|
185
|
+
(0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
|
|
186
|
+
project directory, without replacing the global CLI first:
|
|
183
187
|
|
|
184
188
|
```bash
|
|
185
|
-
bunx --package @staix/agent-hub@0.7.
|
|
186
|
-
bunx --package @staix/agent-hub@0.7.
|
|
189
|
+
bunx --package @staix/agent-hub@0.7.7 ahub upgrade --to 0.7.7 --dry-run
|
|
190
|
+
bunx --package @staix/agent-hub@0.7.7 ahub upgrade --to 0.7.7 --yes
|
|
187
191
|
```
|
|
188
192
|
|
|
193
|
+
| Running now | Coordinator to use |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| 0.6.x (protocol 9) | the target's, through `bunx` as above |
|
|
196
|
+
| 0.7.0 up to the release before the target (protocol 10) | the target's, through `bunx` as above |
|
|
197
|
+
| any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
|
|
198
|
+
| 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
|
|
199
|
+
|
|
200
|
+
Do not use an older installed CLI as the coordinator. A 0.6.x CLI cannot target
|
|
201
|
+
protocol 10: its plan does not check the target's protocol, so the dry-run shows
|
|
202
|
+
no blocker, and `--yes` stops at staging ("target protocol requires a newer
|
|
203
|
+
coordinator") with an operation left to clear by `ahub recovery abort <id>`. An
|
|
204
|
+
older 0.7.x CLI may lack recovery fixes released after it. The
|
|
205
|
+
[smoke ledger](smoke.md) records dry-runs from real 0.6.4 and 0.7.5 hubs (issue
|
|
206
|
+
#75); an applied upgrade was last proven with the 0.7.0 coordinator.
|
|
207
|
+
|
|
189
208
|
The coordinator verifies and retains the exact target package, preserves its
|
|
190
209
|
own source, and promotes the global CLI only after restored projects pass
|
|
191
|
-
readback.
|
|
210
|
+
readback.
|
|
192
211
|
|
|
193
|
-
Once the installed CLI is 0.7.
|
|
212
|
+
Once the installed CLI is 0.7.7, review the current project or all registered
|
|
194
213
|
projects first:
|
|
195
214
|
|
|
196
215
|
```bash
|
|
197
216
|
ahub restart --dry-run
|
|
198
|
-
ahub upgrade --to 0.7.
|
|
217
|
+
ahub upgrade --to 0.7.7 --dry-run
|
|
199
218
|
```
|
|
200
219
|
|
|
201
220
|
Apply only after reviewing the plan:
|
|
202
221
|
|
|
203
222
|
```bash
|
|
204
223
|
ahub restart --yes
|
|
205
|
-
ahub upgrade --to 0.7.
|
|
224
|
+
ahub upgrade --to 0.7.7 --yes
|
|
206
225
|
ahub recovery status <operation-id>
|
|
207
226
|
ahub recovery resume <operation-id>
|
|
208
227
|
ahub recovery abort <operation-id>
|
package/docs/quickstart.md
CHANGED
package/docs/security.md
CHANGED
|
@@ -6,7 +6,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
6
6
|
|
|
7
7
|
- **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input (a channel tag with `meta.source` for Claude, 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
|
-
- **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 120 s of silence. `--unattended` turns prompts off, says so loudly, and is never the default.
|
|
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 120 s of silence. 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
10
|
- **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: no writes outside the project, its git dir and temp; the home directory is unreadable except toolchains; no `.git/hooks` or `.git/config` writes; no network, loopback included. Without the sandbox there is no `bash` tool.
|
|
11
11
|
- **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.
|
|
12
12
|
- **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.
|
package/docs/smoke.md
CHANGED
|
@@ -57,7 +57,7 @@ and the local worker:
|
|
|
57
57
|
- Task #1 reached `approved` with its file written. The local worker served
|
|
58
58
|
`sy/coding` through the switchyard on 127.0.0.1:4813
|
|
59
59
|
(`vllm/deepseek-ai/DeepSeek-V4-Flash-0731`), and Pi's `dgx/coding` traffic
|
|
60
|
-
went through the gateway
|
|
60
|
+
went through the internal gateway as before.
|
|
61
61
|
- Kimi stayed healthy through the run (KIMI-IDLE-AGAIN, KIMI-FINAL-MARKER
|
|
62
62
|
markers, zero queued at the end). The disposable hub and manager were
|
|
63
63
|
stopped after recording.
|
|
@@ -693,3 +693,116 @@ cutover; it also exposed three recovery defects that became 0.7.3 and 0.7.4.
|
|
|
693
693
|
|
|
694
694
|
Issue [#12](https://github.com/STAIxBWLB/agent-hub/issues/12) remains open
|
|
695
695
|
for off-campus Access credentials and a natural near-limit budget pause.
|
|
696
|
+
|
|
697
|
+
## Shared notes and claims live run (issue #68, 0.7.6, 2026-09-30)
|
|
698
|
+
|
|
699
|
+
Measured against the released 0.7.6 package (global CLI and Claude plugin both
|
|
700
|
+
0.7.6 after `ahub setup`) in a disposable git project, with real Kimi 2.1.1 under
|
|
701
|
+
ACP and real Codex (codex-cli 0.156.1) behind the hub's app-server proxy. A script
|
|
702
|
+
stood in for the Codex TUI (the `scripts/smoke-codex.ts` pattern, attached to the
|
|
703
|
+
live daemon's proxy). No Claude peer was attached. No console message named a tool
|
|
704
|
+
or a note kind; the role text and the AGENTS.md block in the peers' prompts do, by
|
|
705
|
+
design. Times are hub.log timestamps (UTC).
|
|
706
|
+
|
|
707
|
+
- Fail note, kind chosen by the model: asked at 03:07:24.886Z to "record this for
|
|
708
|
+
the other agents so nobody retries it" (a WeakMap cannot replace this
|
|
709
|
+
string-keyed Map), Codex called `hub_remember` with `kind: "fail"` on its own,
|
|
710
|
+
with a title and "Do not retry this replacement without a different key model
|
|
711
|
+
or new evidence". The log showed `note from codex [fail]: ...` at 03:07:32.331Z
|
|
712
|
+
(7.4 s); Codex closed with `[FYI]`, so nobody's turn was spent on the answer.
|
|
713
|
+
- No turn of its own: Kimi was idle with nothing queued and made no `busy`
|
|
714
|
+
transition between the note and the next console message.
|
|
715
|
+
- Ride-along, once: the next console question to Kimi at 03:07:48.827Z ("Should
|
|
716
|
+
src/cache.ts switch its Map to a WeakMap so entries can be garbage
|
|
717
|
+
collected? ...", without mentioning the note) was delivered with the note in the
|
|
718
|
+
same prompt. Kimi's session transcript has the note in the first of its three
|
|
719
|
+
turn prompts only. Kimi answered "No" at 03:08:06.398Z (17.6 s) and said the
|
|
720
|
+
answer "rests on the code itself plus codex's earlier recorded fail note". The
|
|
721
|
+
same first prompt also carried 1042 chars of claude-mem recall from an unrelated
|
|
722
|
+
earlier project: claude-mem keys a project by its directory name, and this one
|
|
723
|
+
was `proj`. Later runs should use a unique directory name.
|
|
724
|
+
- Self-claim: told to claim unassigned work without editing, Kimi picked
|
|
725
|
+
`hub_task_propose` with `owner: kimi` and `refs: src/cache.ts` itself. Task #1
|
|
726
|
+
went straight to `in_progress` (`proposed` at 03:11:40.342Z, `claimed by kimi`
|
|
727
|
+
at 03:11:40.343Z, reviewer codex), and no task envelope came back to Kimi.
|
|
728
|
+
- Overlap: Codex then claimed "everything under src/" (`owner: codex`,
|
|
729
|
+
`refs.paths: ["src/"]`). Its tool result carried `Overlaps #1 (owner kimi) on
|
|
730
|
+
src/. Settle it with that owner via hub_send before editing those paths.`, the
|
|
731
|
+
console printed `... Overlaps #1 (owner kimi) on src/. codex is told to settle
|
|
732
|
+
it.` at 03:12:01.494Z, and Kimi received nothing. Codex acknowledged the overlap
|
|
733
|
+
in its `[FYI]` answer (in Korean, translated: coordination is needed before
|
|
734
|
+
editing) and did not message Kimi in the claim turn: it had been told not to
|
|
735
|
+
edit yet, and the guidance asks for settlement before editing.
|
|
736
|
+
- Environmental limit: Kimi 2.1.1 asked approval for both of its
|
|
737
|
+
`hub_task_propose` calls (its `Read` call asked for none), and sent no
|
|
738
|
+
`rawInput` before the approval, so unlike 2.0.1 the hub had nothing to show:
|
|
739
|
+
the console read `mcp__agent-hub__hub_task_propose (payload not reported by
|
|
740
|
+
the agent)` with `approve_once` / `reject` only (the always option is withheld,
|
|
741
|
+
as designed). Kimi's own transcript records the approval before its tool-call
|
|
742
|
+
event, so the arguments seem to arrive after the request (inferred, not
|
|
743
|
+
captured on the ACP stream). The first request (03:08:48.088Z) went unanswered,
|
|
744
|
+
was cancelled at the 120 s permission timeout, and the claim was lost; Kimi
|
|
745
|
+
reported that accurately at 03:10:58.585Z. Codex called the same tools without
|
|
746
|
+
an approval. A Kimi that should claim or record notes unattended needs a
|
|
747
|
+
tool-approval policy of its own for the agent-hub server; the hub does not
|
|
748
|
+
change it.
|
|
749
|
+
- The disposable hub was stopped and its registration removed after recording.
|
|
750
|
+
|
|
751
|
+
## Kimi approvals for the hub's own tools (issue #72, 2026-09-30)
|
|
752
|
+
|
|
753
|
+
Kimi 2.1.1 under ACP. The capture used a small ACP client of its own with the hub's
|
|
754
|
+
tools server; the live run used a disposable project and a hub built from the PR
|
|
755
|
+
branch. Times are hub.log timestamps (UTC).
|
|
756
|
+
|
|
757
|
+
- ACP stream, captured for `mcp__agent-hub__hub_task_list` (saved) and observed in a
|
|
758
|
+
second capture for `Bash` (`echo probe-72`; output not kept, and the live run
|
|
759
|
+
below shows the same command on the console): `tool_call` (`pending`) names the
|
|
760
|
+
tool and has no `rawInput`;
|
|
761
|
+
`tool_call_update` (`in_progress`) streams the argument JSON cumulatively as
|
|
762
|
+
`content` text (`{"command":"` ... `{"command":"echo probe-72"}`); then
|
|
763
|
+
`session/request_permission` arrives with no `rawInput` and Kimi's own line
|
|
764
|
+
(`Requesting approval to Running: echo probe-72`); `rawInput` appears only on the
|
|
765
|
+
update after the answer. Options: `approve_once`, `approve_always` ("Approve for
|
|
766
|
+
this session"), `reject`. `session/new` offers modes `default`, `plan`, `auto`,
|
|
767
|
+
`yolo` and nothing per server or tool.
|
|
768
|
+
- Unattended claim and note: with no console tail open, one message at
|
|
769
|
+
05:40:04.837Z asked Kimi to claim unassigned work and record why a WeakMap cannot
|
|
770
|
+
replace the string-keyed Map. hub.log shows
|
|
771
|
+
`permission auto-approved for kimi: mcp__agent-hub__hub_task_propose`
|
|
772
|
+
(05:40:17.083Z), `... hub_remember` (05:40:17.104Z), the note
|
|
773
|
+
`note from kimi [fail]: ...` (05:40:17.126Z, kind chosen by Kimi), and a second
|
|
774
|
+
auto-approved `hub_task_propose` (05:40:22.741Z) that claimed task #1
|
|
775
|
+
(`claimed by kimi` at 05:40:22.749Z). Kimi's first propose had no `class`; this
|
|
776
|
+
hub had no triage model, so it answered `class is required` and Kimi retried with
|
|
777
|
+
`implement`. Kimi closed with `[FYI]` at 05:40:26.318Z. Nobody answered anything.
|
|
778
|
+
- Other tools still ask, now with the arguments: asked to run `echo probe-72`, Kimi's
|
|
779
|
+
request reached the console at 05:40:58.696Z as `kimi asks permission: Bash:
|
|
780
|
+
{"command":"echo probe-72"}`, with `approve_always` offered again because the
|
|
781
|
+
payload was complete. It was rejected; Kimi did not run it and said so at
|
|
782
|
+
05:41:04.397Z.
|
|
783
|
+
- The disposable hub was stopped and its registration removed after recording.
|
|
784
|
+
|
|
785
|
+
## Upgrade sources for the operations guide (issue #75, 2026-09-30)
|
|
786
|
+
|
|
787
|
+
Two disposable projects, each the only running registered project at the time,
|
|
788
|
+
ran a released hub through `bunx --package @staix/agent-hub@<version> ahub up`:
|
|
789
|
+
0.7.5 (hub.log `ahub up` at 05:43:12.894Z) and 0.6.4 (06:13:57.001Z).
|
|
790
|
+
|
|
791
|
+
- From 0.7.5 (protocol 10): `bunx --package @staix/agent-hub@0.7.6 ahub upgrade
|
|
792
|
+
--to 0.7.6 --dry-run` exited 0 with plan `kind: upgrade`, `version: 0.7.6`, no
|
|
793
|
+
plan blockers; the project read as `running`, source 0.7.5, protocol 10, no
|
|
794
|
+
project blockers. The installed 0.7.6 CLI's dry-run showed the same project,
|
|
795
|
+
source and blockers (its `sourceRoot`, and so its fingerprint, differ).
|
|
796
|
+
- From 0.6.4 (protocol 9): the same 0.7.6 dry-run exited 0 with no plan or
|
|
797
|
+
project blockers; the project read as `running`, source 0.6.4, protocol 9.
|
|
798
|
+
- Nothing changed: each hub kept running, and its hub.log gained no line until it
|
|
799
|
+
was stopped for cleanup.
|
|
800
|
+
- Code basis: `RECOVERY_SOURCE_PROTOCOLS = [9, PROTOCOL]`
|
|
801
|
+
(`src/hub/control-client.ts`) admits a running protocol-9 or protocol-10 source
|
|
802
|
+
(`makeUpgradePlan` in `src/cli/upgrade-runtime.ts`); an older one is refused as
|
|
803
|
+
`manual-bootstrap-required`; the staged target must report the coordinator's own
|
|
804
|
+
`PROTOCOL` (`stage` in the same file).
|
|
805
|
+
- Not measured here: an applied (`--yes`) upgrade from either source (last proven
|
|
806
|
+
with the 0.7.0 coordinator, see above), and session or terminal planning: no peer
|
|
807
|
+
was attached, so `peers` and `terminals` were empty.
|
|
808
|
+
- Both hubs were stopped and their registrations removed after recording.
|
|
@@ -727,3 +727,27 @@ that need no new service and cost no peer an extra turn.
|
|
|
727
727
|
envelope's closing line ask for what changed, why, and the check that was run with
|
|
728
728
|
its result. The verifier role already asks for what passed and what did not, so it
|
|
729
729
|
stays as it was.
|
|
730
|
+
|
|
731
|
+
## Amendment: Kimi approvals for the hub's own tools (issue #72)
|
|
732
|
+
|
|
733
|
+
Measured on Kimi 2.1.1 (ACP stream captured on 2026-09-30): a permission request
|
|
734
|
+
carries no `rawInput`; the argument JSON streams before it as `content` text on
|
|
735
|
+
`tool_call_update`, and `rawInput` arrives only after the answer. Kimi offers
|
|
736
|
+
session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool.
|
|
737
|
+
|
|
738
|
+
- Kimi's requests for exactly `mcp__agent-hub__<name>`, where `<name>` is
|
|
739
|
+
`hub_send` or a task tool, are answered with their `allow_once` option by the
|
|
740
|
+
hub, without a console prompt, and logged by tool name only. This matches the
|
|
741
|
+
`approval_mode = "approve"` Codex already gets for the same tools. Every other
|
|
742
|
+
request keeps the console path; `allow_always` is never chosen automatically.
|
|
743
|
+
- For the other tools, the console payload falls back to the streamed argument
|
|
744
|
+
text once it is a complete JSON object. A partial stream stays unresolved and
|
|
745
|
+
keeps `allow_always` withheld.
|
|
746
|
+
- For every ACP agent, `rawInput` included: a payload longer than the 600
|
|
747
|
+
characters the console shows is marked `[cut, N chars]` and withholds
|
|
748
|
+
`allow_always`, because the hidden tail can change what runs.
|
|
749
|
+
- Name collisions (inferred from reading Kimi 2.1.1's bundled code, not tested
|
|
750
|
+
live): tool names are qualified as `mcp__<server>__<tool>` after replacing
|
|
751
|
+
anything outside `[a-zA-Z0-9_-]`, and a server passed in `session/new` shadows a
|
|
752
|
+
configured server of the same name, so only the hub's `agent-hub` server can
|
|
753
|
+
produce the approved names.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
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.7.
|
|
15730
|
+
version: "0.7.7",
|
|
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",
|
package/src/adapters/acp.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface AcpOptions {
|
|
|
33
33
|
/** Resolve with an optionId, or undefined to cancel. Absent = every request is cancelled.
|
|
34
34
|
* A request whose payload could not be resolved is titled as such and carries no session-wide allow option. */
|
|
35
35
|
onPermission?: (req: PermissionRequest) => Promise<string | undefined>;
|
|
36
|
+
/** Tool titles approved once without asking: the hub's own tools, as Codex gets them (issue #72). Exact names only. */
|
|
37
|
+
autoApprove?: (toolTitle: string) => boolean;
|
|
36
38
|
log?: (line: string) => void;
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -42,6 +44,16 @@ const TOOL_INPUT_CAP = 64;
|
|
|
42
44
|
|
|
43
45
|
type Pending = { resolve: (v: any) => void; reject: (e: Error) => void };
|
|
44
46
|
|
|
47
|
+
/** Streamed argument text as a payload: a complete JSON object, or nothing (a partial stream is not what will run). */
|
|
48
|
+
function jsonObject(text: string | undefined): object | undefined {
|
|
49
|
+
try {
|
|
50
|
+
const v = text === undefined ? undefined : JSON.parse(text);
|
|
51
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
45
57
|
/** ACP client (JSON-RPC 2.0, newline-delimited, over the child's stdio). One session, one prompt in flight. */
|
|
46
58
|
export class AcpPeer extends BasePeer {
|
|
47
59
|
private proc: ChildProcessWithoutNullStreams | undefined;
|
|
@@ -50,6 +62,7 @@ export class AcpPeer extends BasePeer {
|
|
|
50
62
|
private readonly pending = new Map<number, Pending>();
|
|
51
63
|
private chunks: string[] = [];
|
|
52
64
|
private readonly toolInputs = new Map<string, unknown>();
|
|
65
|
+
private readonly toolText = new Map<string, string>(); // streamed argument text, per call, until it finishes
|
|
53
66
|
private primed = false;
|
|
54
67
|
private turn = 0; // generation: a prompt cancelled by the watchdog must not touch the turn that followed it
|
|
55
68
|
private activeDeliveryId: string | undefined;
|
|
@@ -183,10 +196,15 @@ export class AcpPeer extends BasePeer {
|
|
|
183
196
|
if (u?.sessionUpdate === "agent_message_chunk" && u.content?.type === "text") this.chunks.push(u.content.text);
|
|
184
197
|
// The permission request that follows may carry no `rawInput` (Kimi 2.0.1 does not), and then the
|
|
185
198
|
// console would be asked to approve a bare tool name. Keep what the call said it would run (issue #31).
|
|
199
|
+
// Kimi 2.1.1 sends no rawInput before the answer either: the argument JSON streams as content text (issue #72).
|
|
186
200
|
else if ((u?.sessionUpdate === "tool_call" || u?.sessionUpdate === "tool_call_update") && typeof u.toolCallId === "string") {
|
|
201
|
+
// A new call starts clean, so a reused id can never show the arguments of the call before it.
|
|
202
|
+
if (u.sessionUpdate === "tool_call") (this.toolInputs.delete(u.toolCallId), this.toolText.delete(u.toolCallId));
|
|
187
203
|
if (u.rawInput !== undefined) this.toolInputs.set(u.toolCallId, u.rawInput);
|
|
188
|
-
|
|
189
|
-
|
|
204
|
+
const text = Array.isArray(u.content) ? u.content.map((c: any) => (c?.type === "content" && c.content?.type === "text" ? String(c.content.text) : "")).join("") : "";
|
|
205
|
+
if (text) this.toolText.set(u.toolCallId, text);
|
|
206
|
+
if (u.status === "completed" || u.status === "failed") (this.toolInputs.delete(u.toolCallId), this.toolText.delete(u.toolCallId));
|
|
207
|
+
for (const map of [this.toolInputs, this.toolText]) while (map.size > TOOL_INPUT_CAP) map.delete(map.keys().next().value as string);
|
|
190
208
|
}
|
|
191
209
|
else if (u?.sessionUpdate === "usage_update" && this.opts.onTokens) {
|
|
192
210
|
const f = { ...u, ...(typeof u.usage === "object" ? u.usage : {}) } as Record<string, unknown>;
|
|
@@ -208,13 +226,23 @@ export class AcpPeer extends BasePeer {
|
|
|
208
226
|
|
|
209
227
|
private async answerPermission(msg: any): Promise<void> {
|
|
210
228
|
const call = msg.params?.toolCall ?? {};
|
|
229
|
+
const once = (msg.params?.options ?? []).find((o: PermissionOption) => o.kind === "allow_once");
|
|
230
|
+
if (once && typeof call.title === "string" && this.opts.autoApprove?.(call.title)) {
|
|
231
|
+
this.opts.log?.(`permission auto-approved for ${this.id}: ${call.title}`); // the name only: arguments may quote a PII turn
|
|
232
|
+
return this.send({ jsonrpc: "2.0", id: msg.id, result: { outcome: { outcome: "selected", optionId: once.optionId } } });
|
|
233
|
+
}
|
|
211
234
|
// The approver has to see what runs, not only the tool's name ("Bash"). The request itself carries it
|
|
212
|
-
// for some agents; for the rest it was on the `tool_call` update that announced the call
|
|
213
|
-
|
|
214
|
-
const
|
|
235
|
+
// for some agents; for the rest it was on the `tool_call` update that announced the call, as rawInput or,
|
|
236
|
+
// failing that, as streamed argument text that only counts once it is a complete JSON object.
|
|
237
|
+
const id = typeof call.toolCallId === "string" ? call.toolCallId : undefined;
|
|
238
|
+
const raw = call.rawInput ?? (id === undefined ? undefined : this.toolInputs.get(id) ?? jsonObject(this.toolText.get(id)));
|
|
239
|
+
const full = raw === undefined ? "" : typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
240
|
+
// A cut payload hides its tail, and a tail can change what runs: it is marked and buys no session-wide grant.
|
|
241
|
+
const cut = full.length > 600;
|
|
242
|
+
const input = raw === undefined ? "" : `: ${cut ? `${full.slice(0, 600)} [cut, ${full.length} chars]` : full}`;
|
|
215
243
|
// An unknown payload is never dressed up as a description, and it must not buy a blanket grant.
|
|
216
244
|
const title: string = raw === undefined ? `${call.title ?? "tool call"} (payload not reported by the agent)` : `${call.title ?? "tool call"}${input}`;
|
|
217
|
-
const options: PermissionOption[] = (msg.params?.options ?? []).filter((o: PermissionOption) => raw !== undefined || o.kind !== "allow_always");
|
|
245
|
+
const options: PermissionOption[] = (msg.params?.options ?? []).filter((o: PermissionOption) => (raw !== undefined && !cut) || o.kind !== "allow_always");
|
|
218
246
|
const picked = await this.opts.onPermission?.({ peer: this.id, title, options }).catch(() => undefined);
|
|
219
247
|
const valid = options.some((o) => o.optionId === picked);
|
|
220
248
|
this.send({
|
package/src/cli/main.ts
CHANGED
|
@@ -404,6 +404,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
404
404
|
else {
|
|
405
405
|
console.log(`ahub setup changes your Claude Code plugin configuration, one step at a time, re-checking after each. First step:\n ${step.argv.join(" ")}\n ${step.why}`);
|
|
406
406
|
if (!args.includes("--yes")) {
|
|
407
|
+
// Nobody can answer without a terminal: waiting would hang a script forever (issue #73).
|
|
408
|
+
if (!process.stdin.isTTY) fail("not a terminal: nothing was changed; rerun with --yes to apply");
|
|
407
409
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
408
410
|
const answer = (await rl.question("Proceed (this and the steps that follow from it)? [y/N] ")).trim().toLowerCase();
|
|
409
411
|
rl.close();
|
package/src/hub/daemon.ts
CHANGED
|
@@ -409,6 +409,10 @@ export async function startDaemon(opts: DaemonOptions) {
|
|
|
409
409
|
return { ...e, env: { ...env, ...(task ? { refs: { task } } : {}), body: `[private${task ? `: task #${task}, see ahub task show ${task}` : ""}]` } };
|
|
410
410
|
};
|
|
411
411
|
const SERVER_JS = join(import.meta.dir, "..", "..", "plugins", "agent-hub", "server.js");
|
|
412
|
+
// What the hub's MCP server offers a native peer. Codex approves them in config, Kimi's requests for exactly these
|
|
413
|
+
// names are answered by the hub: no file or process is touched, and every call passes the hub's own checks.
|
|
414
|
+
const HUB_TOOLS = ["hub_send", ...TASK_TOOLS.map((t) => t.name)];
|
|
415
|
+
const HUB_TOOL_TITLES = new Set(HUB_TOOLS.map((name) => `mcp__agent-hub__${name}`));
|
|
412
416
|
const toolEnv = (peer: PeerId) => ({ AGENTHUB_MODE: "tools", AGENTHUB_PEER_ID: peer, AGENTHUB_STATE_DIR: opts.stateDir, AGENTHUB_PROJECT_DIR: opts.cwd });
|
|
413
417
|
|
|
414
418
|
/** One entry point for the task tools, whoever calls them: MCP clients, the local worker, the console. */
|
|
@@ -691,6 +695,10 @@ export async function startDaemon(opts: DaemonOptions) {
|
|
|
691
695
|
cwd: opts.cwd,
|
|
692
696
|
watchdogMs: config.watchdog_ms,
|
|
693
697
|
onPermission,
|
|
698
|
+
// The hub's own tools pass without a console prompt, as Codex's do (approval_mode below; issue #72). This
|
|
699
|
+
// relies on the agent putting the tool name in `title`, as Kimi does; an ACP agent that titles calls with
|
|
700
|
+
// model-written text must not be configured as kimi_cmd. A stopping hub approves nothing.
|
|
701
|
+
autoApprove: (title) => !stopping && HUB_TOOL_TITLES.has(title),
|
|
694
702
|
log,
|
|
695
703
|
onTokens: onKimiTokens,
|
|
696
704
|
mcpServers: [{ name: "agent-hub", command: "bun", args: ["run", SERVER_JS], env: Object.entries(toolEnv("kimi")).map(([name, value]) => ({ name, value })) }],
|
|
@@ -713,7 +721,7 @@ export async function startDaemon(opts: DaemonOptions) {
|
|
|
713
721
|
["command", '"bun"'],
|
|
714
722
|
["args", JSON.stringify(["run", SERVER_JS])],
|
|
715
723
|
["env", `{${Object.entries(toolEnv("codex")).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ")}}`],
|
|
716
|
-
...
|
|
724
|
+
...HUB_TOOLS.map((name) => [`tools.${name}.approval_mode`, '"approve"']),
|
|
717
725
|
].flatMap(([k, v]) => ["-c", `mcp_servers.agent-hub.${k}=${v}`]),
|
|
718
726
|
preamble: roleContract("codex", config.roles),
|
|
719
727
|
onUsage: (rateLimits, hard) => budget.report("codex", codexWindows(rateLimits), hard),
|