@staix/agent-hub 0.12.16 → 0.12.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +4 -4
  3. package/docs/agent-notes/adapters.md +1 -0
  4. package/docs/agent-notes/bus.md +2 -0
  5. package/docs/agent-notes/tasks.md +3 -1
  6. package/docs/agent-notes/tests.md +1 -0
  7. package/docs/operations.md +106 -9
  8. package/docs/quickstart.md +3 -3
  9. package/docs/security.md +24 -0
  10. package/docs/smoke.md +93 -0
  11. package/docs/specs/2026-09-19-agent-hub-design.md +120 -1
  12. package/docs/verification/2026-10-09-agent-shell-t0.md +123 -0
  13. package/docs/verified.json +26 -17
  14. package/package.json +1 -1
  15. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  16. package/plugins/agent-hub/server.js +17 -5
  17. package/src/adapters/acp.ts +2 -2
  18. package/src/adapters/claude-channel.ts +3 -2
  19. package/src/adapters/codex-appserver.ts +3 -2
  20. package/src/adapters/local-worker.ts +5 -5
  21. package/src/cli/console-state.ts +213 -0
  22. package/src/cli/console.ts +201 -0
  23. package/src/cli/facts-hook.ts +8 -1
  24. package/src/cli/identity-audit.ts +66 -0
  25. package/src/cli/identity.ts +54 -0
  26. package/src/cli/launch.ts +15 -2
  27. package/src/cli/main.ts +62 -29
  28. package/src/cli/tail-render.ts +17 -0
  29. package/src/cli/upgrade-runtime.ts +1 -1
  30. package/src/hub/board.ts +6 -2
  31. package/src/hub/bus.ts +82 -10
  32. package/src/hub/child-process.ts +11 -0
  33. package/src/hub/conductor.ts +196 -0
  34. package/src/hub/control-client.ts +3 -3
  35. package/src/hub/daemon.ts +367 -46
  36. package/src/hub/envelope.ts +1 -1
  37. package/src/hub/events.ts +6 -1
  38. package/src/hub/hub-tools.ts +13 -0
  39. package/src/hub/report.ts +53 -4
  40. package/src/hub/supervision.ts +152 -0
  41. package/src/hub/tasks.ts +30 -15
  42. package/src/hub/usage.ts +37 -2
  43. package/src/pi/launch.ts +2 -1
@@ -0,0 +1,123 @@
1
+ # Agent shell identity T0, issues #193 and #194
2
+
3
+ ## Evidence collected
4
+
5
+ - Installed CLI reports `codex-cli 0.146.0` (`codex --version`, 2026-10-09). `codex app-server --help` supports configuration overrides and WebSocket listeners; its example names `shell_environment_policy.inherit=all`.
6
+ - The [version-pinned native environment implementation](https://github.com/openai/codex/blob/rust-v0.146.0/codex-rs/protocol/src/shell_environment.rs) applies inheritance, default exclusions, custom exclusions, explicit values, and `include_only` in that order. It then injects `CODEX_THREAD_ID`. `AGENTHUB_PEER_ID` is an ordinary inherited variable and can be removed by filtering. The native thread marker is injected after filtering, which supplies a fallback without widening the user's environment policy.
7
+ - The [version-pinned core wrapper](https://github.com/openai/codex/blob/rust-v0.146.0/codex-rs/core/src/exec_env.rs) explicitly documents thread-marker injection even with `include_only`.
8
+ - The [current configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference) describes inheritance and filtering, and now describes a `filters` map alongside legacy `exclude` and `include_only`. Current documentation is not evidence that every new option exists in installed 0.146.0.
9
+ - The hub's Codex adapter passes a child environment even without optional launch values. It now sets its own marker and removes stale vendor markers inherited from the caller. Recovery authority remains scrubbed by `childEnv`.
10
+
11
+ ## Native Codex observations (2026-10-09)
12
+
13
+ - Real installed `codex exec --json --model gpt-5.5 --sandbox workspace-write`
14
+ completed a single fixed Python shell probe in an isolated git fixture.
15
+ With explicit `inherit=all`, the hub marker, native thread marker and harmless
16
+ canary were present; a Claude marker was absent. The disposable loopback
17
+ responder was reachable by the harness, while the native shell request failed
18
+ with `URLError`, errno 1. No sandbox or network exception was requested.
19
+ - A second real run used `inherit=none`, an explicit PATH and
20
+ `include_only=["PATH"]`. The hub marker, native marker and canary still appeared
21
+ in the observed shell. This does not establish that filtering removed them;
22
+ the difference from the pinned environment construction is unresolved in this
23
+ installed host environment. The fallback is supported by source and marker
24
+ presence, without widening the caller's configured policy.
25
+ - A real native MCP call under `workspace-write` completed `agent-hub.hub_status`
26
+ against the candidate bundle and an isolated conductor daemon. Native JSONL
27
+ recorded the MCP call completing, and the daemon independently recorded a
28
+ codex `conduct/status` event. Only this exact hub tool was approved through the
29
+ existing per-tool `approval_mode=approve` configuration; its daemon role check
30
+ still applied. This proves MCP access separately from the blocked shell path.
31
+ - The first run with the user's configured `gpt-6.1-sol` failed before tools:
32
+ that slug was rejected by the installed ChatGPT-account endpoint. The probe's
33
+ explicit model selection did not change the user's configuration.
34
+
35
+ ## Native Claude observations (2026-10-09)
36
+
37
+ - The final observer proof used source `20ad5e6`. Its read-only feed-off
38
+ continuation verified one completed native turn, 263,413 cached-inclusive
39
+ tokens, a matching opaque daemon Stop receipt, native idle and no pending
40
+ deliveries. The fresh feed-own run verified two actual owner completions and
41
+ independent file reviews, nine native finals matched by nine daemon Stops,
42
+ 27 unique usage records and 1,888,242 tokens. Eight completed turns contained
43
+ supervision, totalling 1,292,922 tokens. The receipt matched the final native
44
+ message, session, launcher and instance; earlier incomplete captures below
45
+ were not retroactively marked complete.
46
+
47
+ - In the original captures, Claude Code 2.1.295, Opus 5.5, ran the candidate MCP tools in real PTYs
48
+ after its five-hour quota reset. Both feed-off and feed-own fixtures started
49
+ local and headless Pi, proposed two tasks, reassigned Beta to Pi, held and
50
+ released both peers, and independently read the exact ALPHA/BETA files before
51
+ approving their tasks. The console independently audited four allow-once
52
+ answers in each fixture, covering the two writes and bounded file checks.
53
+ - The real feed-off TUI displayed inbound review requests; the feed-own TUI
54
+ displayed actual supervision and review pushes. MCP success and channel
55
+ delivery were observed separately. A transient startup warning about the
56
+ server name did not prevent the later observed channel deliveries.
57
+ - In the feed-own native TUI, the operator submitted the fixed Python probe
58
+ through `!`. Its actual command output was `AGENTHUB_PEER_ID=true`,
59
+ `CLAUDECODE=true`, `CLAUDE_CODE_SESSION_ID=true`, `CODEX_THREAD_ID=false`.
60
+ This is shell output captured before Claude's interpretation of it.
61
+ - A subsequent feed-off run pinned to `da2ebbc` executed the same fixed probe
62
+ through the model's actual native Bash tool and separately through `!`.
63
+ Both actual command outputs showed the same three Claude/hub markers present
64
+ and `CODEX_THREAD_ID` absent. This closes the separate model-shell observation;
65
+ neither command printed environment values.
66
+ - The original harness accepted hub idle too early and stopped both last
67
+ review turns before a native final answer. Native transcripts independently
68
+ show two completed turns in feed-off and five in feed-own, with the last
69
+ assistant message still `tool_use` in each case. The board approvals are real;
70
+ complete final-turn verification and production supervision accounting were
71
+ incomplete for those captures. The harness now requires the current daemon instance, its session,
72
+ the fixture-specific transcript, a final `end_turn` and `turn_duration` after
73
+ the last actual review. Missing measurements remain unknown.
74
+ - The `da2ebbc` run verified the final native message UUID, message id,
75
+ `end_turn`, subsequent `turn_duration`, daemon instance, session and launcher.
76
+ Its complete transcript contains five completed turns and 21 unique usage
77
+ records, totalling 1,451,782 tokens including cached input. The daemon certified
78
+ only one native Stop, however. Its log refused the other completions as an
79
+ unavailable completed transcript message or completion predating the current
80
+ turn; the final refusal occurred before cleanup. A normal review receipt
81
+ remained queued. Actual task/file/final-answer evidence is valid, while full
82
+ runtime accounting and delivery settlement remain incomplete.
83
+ - The `6034dd9` read-only continuation preserved both approved tasks and exact
84
+ files. It completed one native turn with four unique usage records and 261,483
85
+ cached-inclusive tokens, but the matching daemon Stop remained absent. After
86
+ a bounded retry, the Stop handler logged unavailable completion at
87
+ 01:39:04.087 UTC; the native stop summary/duration appeared at 01:39:04.095 UTC.
88
+ With no overlapping operator prompt, this strongly supports a transcript
89
+ visibility barrier while the native hook waits. The strict harness timed out
90
+ incomplete and stopped its owned processes. The final message's thinking/text
91
+ rows carried identical usage counters in these observed transcripts; this
92
+ observation does not replace the final text/duration requirement.
93
+
94
+ ## Plain Claude and Kimi observations (2026-10-09)
95
+
96
+ - Plain Claude 2.1.295, launched with the candidate MCP configuration but no
97
+ development-channel flag, completed one native `hub_status` call with an
98
+ independent daemon Claude status audit. Its only native tool calls were
99
+ ToolSearch and that status call. A unique operator push was bridge-accepted,
100
+ but no native pushed user row or response appeared in the observed
101
+ 20.075-second window. This bounded negative observation is not a universal
102
+ claim about channel support. Actual channel-enabled review/supervision pushes
103
+ were observed separately in the conductor cases. The plain probe and cleanup
104
+ took 56.724 seconds and made no model file, shell, task or settings changes.
105
+ - Real Kimi Code CLI 2.1.1 answered ACP `initialize` and created native session
106
+ `session_17eeb9dc-38d3-4685-b1aa-3db6a89b465c`. Its single prompt failed with
107
+ protocol error `-32000`, HTTP 403, for the managed account's weekly usage
108
+ limit before any requested tool event. No task-list/status result or ordinary
109
+ peer's conductor refusal was obtained. The owning CLI's safe provider list
110
+ showed only `managed:kimi-code`, type `kimi`, four models, OAuth, default
111
+ `kimi-code/k3`. No distinct configured provider was found. The reset time
112
+ remains unknown; no purchase, credential/configuration change or model retry
113
+ was attempted. Owned native processes and the fixture daemon were stopped.
114
+
115
+ ## Release disposition and remaining bounded live probe
116
+
117
+ - On 2026-10-09 the user explicitly approved release 0.12.17 with only the Kimi native new-tool and ordinary-role refusal T0 evidence deferred. Kimi coverage remains unverified. This decision defers no other source/native gate and authorizes no purchase, credentials, provider/configuration change, account retry or authority relaxation.
118
+ - Kimi ACP MCP: after native provider access is available under an authorized account, request the same read-only task list/status and ordinary-role refusal probes in isolated sessions. Verify actual native tool events/results and the daemon reply before marking this prerequisite verified. The existing quota failure proves neither new-tool access nor refusal behavior.
119
+
120
+ ## Verification limits
121
+
122
+ - Codex shell and MCP probes used the root agent's serial resource slot. Claude `!`, model shell, enabled-channel pushes and the bounded plain-session comparison were observed in subsequent serial native cases. Kimi new-tool access remains unverified because its real account rejected the prompt before tools.
123
+ - New unit and fake-adapter tests are prepared but have not been run by this worker. The root agent owns the final immutable-head checks.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "documents": {
4
4
  "README.md": {
5
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
5
+ "verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
6
6
  "paths": [
7
7
  "package.json",
8
8
  "src/",
@@ -11,14 +11,14 @@
11
11
  ]
12
12
  },
13
13
  "docs/security.md": {
14
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
14
+ "verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
15
15
  "paths": [
16
16
  "src/",
17
17
  "templates/"
18
18
  ]
19
19
  },
20
20
  "docs/operations.md": {
21
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
21
+ "verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
22
22
  "paths": [
23
23
  "package.json",
24
24
  "src/",
@@ -30,11 +30,12 @@
30
30
  ".github/workflows/check.yml",
31
31
  "scripts/ci-gates.mjs",
32
32
  "scripts/ci-reuse.mjs",
33
- ".github/workflows/release.yml"
33
+ ".github/workflows/release.yml",
34
+ "scripts/smoke-conductor.ts"
34
35
  ]
35
36
  },
36
37
  "docs/quickstart.md": {
37
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
38
+ "verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
38
39
  "paths": [
39
40
  "package.json",
40
41
  "src/",
@@ -42,7 +43,7 @@
42
43
  ]
43
44
  },
44
45
  "docs/agent-notes/adapters.md": {
45
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
46
+ "verifiedAgainst": "20ad5e68fd1264fa43dd7426f60cb0c8211d6555",
46
47
  "paths": [
47
48
  "src/adapters/",
48
49
  "src/pi/",
@@ -55,7 +56,7 @@
55
56
  ]
56
57
  },
57
58
  "docs/agent-notes/benchmarks.md": {
58
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
59
+ "verifiedAgainst": "8b7d7254f1bb5602ec5d658e7814e236f8fb43e7",
59
60
  "paths": [
60
61
  "scripts/benchmarks/",
61
62
  "test/benchmarks/",
@@ -67,7 +68,7 @@
67
68
  ]
68
69
  },
69
70
  "docs/agent-notes/budget.md": {
70
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
71
+ "verifiedAgainst": "20ad5e68fd1264fa43dd7426f60cb0c8211d6555",
71
72
  "paths": [
72
73
  "src/hub/budget.ts",
73
74
  "src/cli/statusline-tee.ts",
@@ -79,18 +80,19 @@
79
80
  ]
80
81
  },
81
82
  "docs/agent-notes/bus.md": {
82
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
83
+ "verifiedAgainst": "20ad5e68fd1264fa43dd7426f60cb0c8211d6555",
83
84
  "paths": [
84
85
  "src/hub/bus.ts",
85
86
  "src/hub/envelope.ts",
86
87
  "src/hub/limits.ts",
87
88
  "src/hub/delivery-journal.ts",
88
89
  "src/hub/inference.ts",
89
- "src/hub/daemon.ts"
90
+ "src/hub/daemon.ts",
91
+ "src/hub/supervision.ts"
90
92
  ]
91
93
  },
92
94
  "docs/agent-notes/daemon.md": {
93
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
95
+ "verifiedAgainst": "20ad5e68fd1264fa43dd7426f60cb0c8211d6555",
94
96
  "paths": [
95
97
  "src/hub/daemon.ts",
96
98
  "src/hub/control-client.ts",
@@ -107,11 +109,16 @@
107
109
  "src/cli/recovery-package.ts",
108
110
  "src/cli/facts-hook.ts",
109
111
  "src/pi/process-signature.ts",
110
- "src/hub/context-window.ts"
112
+ "src/hub/context-window.ts",
113
+ "src/hub/conductor.ts",
114
+ "src/cli/console.ts",
115
+ "src/cli/console-state.ts",
116
+ "src/cli/identity.ts",
117
+ "src/cli/identity-audit.ts"
111
118
  ]
112
119
  },
113
120
  "docs/agent-notes/local-worker.md": {
114
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
121
+ "verifiedAgainst": "8b7d7254f1bb5602ec5d658e7814e236f8fb43e7",
115
122
  "paths": [
116
123
  "src/adapters/local-worker.ts",
117
124
  "src/local/",
@@ -120,7 +127,7 @@
120
127
  ]
121
128
  },
122
129
  "docs/agent-notes/models.md": {
123
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
130
+ "verifiedAgainst": "8b7d7254f1bb5602ec5d658e7814e236f8fb43e7",
124
131
  "paths": [
125
132
  "src/models/",
126
133
  "src/hub/inference.ts",
@@ -129,7 +136,7 @@
129
136
  ]
130
137
  },
131
138
  "docs/agent-notes/tasks.md": {
132
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
139
+ "verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
133
140
  "paths": [
134
141
  "src/hub/tasks.ts",
135
142
  "src/hub/board.ts",
@@ -138,11 +145,13 @@
138
145
  "src/hub/task-sweep.ts",
139
146
  "src/cli/launch.ts",
140
147
  "src/cli/main.ts",
141
- "src/cli/preview.ts"
148
+ "src/cli/preview.ts",
149
+ "src/hub/conductor.ts",
150
+ "src/hub/supervision.ts"
142
151
  ]
143
152
  },
144
153
  "docs/agent-notes/tests.md": {
145
- "verifiedAgainst": "2f6274d007d47aa3db0976c4989021520c03a3ef",
154
+ "verifiedAgainst": "2a274289e280252b264716da3a1112f67b0c278e",
146
155
  "paths": [
147
156
  "test/",
148
157
  "scripts/check.sh",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.12.16",
3
+ "version": "0.12.17",
4
4
  "description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.12.16",
3
+ "version": "0.12.17",
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",
@@ -15291,8 +15291,8 @@ function projectContext(cwd, env = process.env) {
15291
15291
  function stateDirFor(cwd) {
15292
15292
  return projectContext(cwd).stateDir;
15293
15293
  }
15294
- var PROTOCOL = 14;
15295
- var RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, PROTOCOL];
15294
+ var PROTOCOL = 15;
15295
+ var RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, PROTOCOL];
15296
15296
  function readControl(stateDir) {
15297
15297
  try {
15298
15298
  const status = JSON.parse(readFileSync2(join2(stateDir, "status.json"), "utf8"));
@@ -15403,7 +15403,7 @@ class ControlClient {
15403
15403
  // package.json
15404
15404
  var package_default = {
15405
15405
  name: "@staix/agent-hub",
15406
- version: "0.12.16",
15406
+ version: "0.12.17",
15407
15407
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
15408
15408
  license: "MIT",
15409
15409
  type: "module",
@@ -15503,7 +15503,18 @@ var TASK_TOOLS = [
15503
15503
  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"])
15504
15504
  ];
15505
15505
  var TASK_TOOL_NAMES = new Set(TASK_TOOLS.map((t) => t.name));
15506
+ var CONDUCTOR_TOOLS = [
15507
+ tool("hub_status", "Inspect public team state, holds, quota windows, task counts and pending approval peer/tool/age. Only the configured conductor may use this tool.", {}),
15508
+ tool("hub_task_show", "Read a task's public view and history. PII tasks remain stubs. Only the conductor may use this tool.", { id }, ["id"]),
15509
+ tool("hub_task_assign", "Move a task to another peer, as the conductor. Requires assign capability when the conductor has an explicit capabilities list.", { id, peer: str }, ["id", "peer"]),
15510
+ tool("hub_task_escalate", "Escalate a task through the normal task flow, as the conductor. Requires assign capability when explicitly listed.", { id }, ["id"]),
15511
+ tool("hub_peer_start", "Start local, kimi or headless pi. Claude, Codex and Pi TUI requests return a command for the person to run, without launching a terminal.", { peer: { type: "string", enum: ["local", "kimi", "pi", "claude", "codex"] }, mode: { type: "string", enum: ["headless", "tui"] } }, ["peer"]),
15512
+ tool("hub_peer_hold", "Hold a peer's deliveries as the conductor. This hold is separate from the person's hold and budget pauses.", { peer: str }, ["peer"]),
15513
+ tool("hub_peer_release", "Release only the conductor hold you placed. Never lifts a person's hold or a budget pause.", { peer: str }, ["peer"])
15514
+ ];
15515
+ var CONDUCTOR_TOOL_NAMES = new Set(CONDUCTOR_TOOLS.map((t) => t.name));
15506
15516
  var ROLE_TEXT = {
15517
+ conductor: "conductor: plan and split work into tasks with owners, watch the team with hub_status, move stalled work, and ensure every task is reviewed (review it yourself only if you also hold reviewer). Report results and open decisions to the person. Do not implement tasks you handed out. Never ask a peer to answer an approval; ask the person for human-only actions. You may release only holds you placed, never human holds or budget pauses.",
15507
15518
  planner: "planner: break work into tasks with hub_task_propose (one outcome each, the right class, paths in refs, and after: [ids] for work that must wait for other tasks) instead of doing everything yourself.",
15508
15519
  implementer: "implementer: accept tasks assigned to you, do them, and finish with hub_task_done (summary: what changed, why, and the check you ran with its result; refs). Decline what you cannot do. Before starting work nobody assigned you, claim it with hub_task_propose naming yourself as owner, with the paths in refs. With a claim or an accept, give a plan: the files, symbols and signatures you will change and where new code goes.",
15509
15520
  verifier: "verifier: run the checks a task names and report what passed and what did not in hub_task_done.",
@@ -15690,7 +15701,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
15690
15701
  inputSchema: { type: "object", properties: { delivery_id: { type: "string" }, delivery_generation: { type: "string" } }, required: ["delivery_id", "delivery_generation"], additionalProperties: false }
15691
15702
  }
15692
15703
  ],
15693
- ...TASK_TOOLS
15704
+ ...TASK_TOOLS,
15705
+ ...CONDUCTOR_TOOLS
15694
15706
  ]
15695
15707
  }));
15696
15708
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -15720,7 +15732,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
15720
15732
  const sent = `sent to: ${res.targets.join(", ") || "(no other peers attached)"}`;
15721
15733
  return text(typeof res.notice === "string" ? `${sent}; ${res.notice}` : sent);
15722
15734
  }
15723
- if (TASK_TOOL_NAMES.has(name)) {
15735
+ if (TASK_TOOL_NAMES.has(name) || CONDUCTOR_TOOL_NAMES.has(name)) {
15724
15736
  if (!hub)
15725
15737
  return text(offline());
15726
15738
  const res = await hub.request({ t: "task", op: name, args: args ?? {} });
@@ -2,7 +2,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
  import { renderDigest, replyAudience, replyParent, type Envelope, type PeerId } from "../hub/envelope.ts";
4
4
  import { BasePeer } from "../hub/peers.ts";
5
- import { childEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
5
+ import { peerChildEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
6
6
 
7
7
  export interface PermissionOption {
8
8
  optionId: string;
@@ -135,7 +135,7 @@ export class AcpPeer extends BasePeer {
135
135
  async start(): Promise<void> {
136
136
  const [bin, ...args] = this.opts.cmd;
137
137
  // Its own process group, stopped as a whole (#115, as Codex's in #113): an agent CLI may be a launcher with a native child.
138
- const proc = spawn(bin!, args, { cwd: this.opts.cwd, env: childEnv({ ...process.env, ...(this.opts.env ?? {}) }), stdio: ["pipe", "pipe", "pipe"], detached: true });
138
+ const proc = spawn(bin!, args, { cwd: this.opts.cwd, env: peerChildEnv(this.id, { ...process.env, ...(this.opts.env ?? {}) }), stdio: ["pipe", "pipe", "pipe"], detached: true });
139
139
  this.proc = proc;
140
140
  trackGroup(proc);
141
141
  proc.on("error", (e) => this.down(`spawn failed: ${e.message}`));
@@ -8,7 +8,7 @@ import { readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { ControlClient, stateDirFor } from "../hub/control-client.ts";
10
10
  import { VERSION } from "../version.ts";
11
- import { DEFAULT_ROLES, roleContract, TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
11
+ import { CONDUCTOR_TOOLS, CONDUCTOR_TOOL_NAMES, DEFAULT_ROLES, roleContract, TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
12
12
  import { frame, replyParent, sanitize, HUB_MESSAGE_INSTRUCTION, type Envelope } from "../hub/envelope.ts";
13
13
 
14
14
  // A native session is pinned at launch. Unlike a new CLI invocation after `cd`,
@@ -201,6 +201,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
201
201
  },
202
202
  ]),
203
203
  ...TASK_TOOLS,
204
+ ...CONDUCTOR_TOOLS,
204
205
  ],
205
206
  }));
206
207
 
@@ -225,7 +226,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
225
226
  const sent = `sent to: ${res.targets.join(", ") || "(no other peers attached)"}`;
226
227
  return text(typeof res.notice === "string" ? `${sent}; ${res.notice}` : sent);
227
228
  }
228
- if (TASK_TOOL_NAMES.has(name)) {
229
+ if (TASK_TOOL_NAMES.has(name) || CONDUCTOR_TOOL_NAMES.has(name)) {
229
230
  if (!hub) return text(offline());
230
231
  const res = await hub.request({ t: "task", op: name, args: args ?? {} });
231
232
  return text(res.ok ? res.text : `error: ${res.error}`);
@@ -3,7 +3,7 @@ import type { Server, ServerWebSocket } from "bun";
3
3
  import { renderDigest, replyAudience, replyParent, type Envelope, type PeerId } from "../hub/envelope.ts";
4
4
  import { BasePeer } from "../hub/peers.ts";
5
5
  import { codexContext, type ContextReading } from "../hub/context-window.ts";
6
- import { childEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
6
+ import { peerChildEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
7
7
 
8
8
  export interface CodexOptions {
9
9
  /** Port the TUI attaches to: `codex --enable tui_app_server --remote ws://127.0.0.1:<proxyPort>`. */
@@ -305,9 +305,10 @@ export class CodexPeer extends BasePeer {
305
305
  throw new Error(`port ${port} already answers /healthz: an app-server the hub does not own is running (orphan from a crashed hub?)`);
306
306
  }
307
307
  let gone = "";
308
+ const env = peerChildEnv("codex", { ...process.env, ...(this.opts.env ?? {}) });
308
309
  this.proc = spawn(this.opts.bin ?? "codex", ["app-server", "--listen", `ws://127.0.0.1:${port}`, ...(this.opts.extraArgs ?? [])], {
309
310
  cwd: this.opts.cwd,
310
- env: childEnv({ ...process.env, ...(this.opts.env ?? {}) }),
311
+ env,
311
312
  stdio: ["ignore", "ignore", "pipe"],
312
313
  detached: true, // its own process group, stopped as a whole (#113): `codex` is a launcher with a native child
313
314
  });
@@ -4,7 +4,7 @@ import type { RouteLabelEvent, RouteTurnOutcome } from "../models/route/labels.t
4
4
  import type { ToolObservation } from "../models/route/signals.ts";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { renderDigest, replyAudience, replyParent, STANDING_INSTRUCTION, USER, type Envelope, type EnvelopeOpts, type PeerId } from "../hub/envelope.ts";
7
- import { TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
7
+ import { CONDUCTOR_TOOL_NAMES, CONDUCTOR_TOOLS, TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
8
8
  import { BasePeer } from "../hub/peers.ts";
9
9
  import { profile, proxyEnv, type SandboxNetwork } from "../local/sandbox.ts";
10
10
  import { runTool, toolResultFailed, TOOL_SCHEMAS, touchedPaths, type ToolContext } from "../local/tools.ts";
@@ -98,7 +98,7 @@ export class LocalPeer extends BasePeer {
98
98
  await this.requireBudget(envs, "model_calls");
99
99
  if (generation !== this.turn) throw new Error("route belongs to an ended turn");
100
100
  if (signal.aborted) throw new Error("turn cancelled before model request");
101
- const tools = judge ? undefined : [...TOOL_SCHEMAS, ...(this.opts.taskTool ? TASK_TOOLS.map(asFunction) : [])];
101
+ const tools = judge ? undefined : [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...TASK_TOOLS, ...CONDUCTOR_TOOLS].map(asFunction) : [])];
102
102
  const result = await this.opts.omni.chat({ model, messages, ...(tools ? { tools } : {}), ...(judge ? { max_tokens: maxTokens ?? 2048 } : {}) }, { signal, ...(pii ? { onCampusOnly: true } : {}) });
103
103
  this.recordUsage(result, model);
104
104
  if (generation !== this.turn) throw new Error("route belongs to an ended turn");
@@ -250,7 +250,7 @@ export class LocalPeer extends BasePeer {
250
250
  await this.requireBudget(envs, "tool_calls");
251
251
  if (turnSignal.aborted) throw new ExecutionBudgetStop(this.budgetStopReason || "turn cancelled before tool execution");
252
252
  } catch (error) { clearInterval(alive); throw error; }
253
- const running = TASK_TOOL_NAMES.has(name) && this.opts.taskTool ? this.opts.taskTool(name, safeParse(call.function.arguments), { pii: !!policy?.pii }).catch((e: Error) => `error: ${e.message}`) : runTool(name, call.function.arguments, ctx);
253
+ const running = (TASK_TOOL_NAMES.has(name) || CONDUCTOR_TOOL_NAMES.has(name)) && this.opts.taskTool ? this.opts.taskTool(name, safeParse(call.function.arguments), { pii: !!policy?.pii }).catch((e: Error) => `error: ${e.message}`) : runTool(name, call.function.arguments, ctx);
254
254
  const output = await running.finally(() => clearInterval(alive));
255
255
  if (turn !== this.turn) return "";
256
256
  this.touch();
@@ -281,7 +281,7 @@ export class LocalPeer extends BasePeer {
281
281
  });
282
282
  },
283
283
  sandboxProfile: this.sandboxProfile,
284
- sandboxEnv: proxyEnv(this.opts.tools.bashNetwork ?? false),
284
+ sandboxEnv: { ...proxyEnv(this.opts.tools.bashNetwork ?? false), AGENTHUB_PEER_ID: this.id },
285
285
  signal: turnSignal,
286
286
  send: (text, to) => {
287
287
  const refused = this.onMessage?.(text, pii ? reply : { inReplyTo: replyParent(envs), to: to?.length ? to : replyAudience(envs) });
@@ -334,7 +334,7 @@ export class LocalPeer extends BasePeer {
334
334
  // A task turn asks for its class's route; a route needs the sidecar, which exists only when the worker was started with one.
335
335
  const route = policy?.route ?? this.opts.route;
336
336
  const fixedModel = policy?.fixedModel ?? this.opts.fixedModel;
337
- const tools = [...TOOL_SCHEMAS, ...(this.opts.taskTool ? TASK_TOOLS.map(asFunction) : [])];
337
+ const tools = [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...TASK_TOOLS, ...CONDUCTOR_TOOLS].map(asFunction) : [])];
338
338
  const signal = this.abort!.signal;
339
339
  const messages: ChatMessage[] = [{ role: "system", content: system(this.opts.cwd, this.opts.preamble) }, ...this.history, ...turnMsgs];
340
340
  const routed = await this.hubCall(route, messages, policy, signal);