@staix/agent-hub 0.20.0 → 0.21.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 +59 -3
- package/README.md +2 -2
- package/docs/agent-notes/README.md +11 -0
- package/docs/agent-notes/adapters.md +7 -3
- package/docs/agent-notes/benchmarks.md +27 -0
- package/docs/agent-notes/budget.md +2 -1
- package/docs/agent-notes/bus.md +2 -2
- package/docs/agent-notes/daemon.md +15 -6
- package/docs/agent-notes/local-worker.md +4 -0
- package/docs/agent-notes/models.md +2 -0
- package/docs/agent-notes/tasks.md +17 -9
- package/docs/agent-notes/tests.md +9 -2
- package/docs/bench.md +1 -1
- package/docs/data-policy.md +2 -0
- package/docs/dogfood.md +37 -4
- package/docs/events.md +2 -1
- package/docs/memory.md +9 -3
- package/docs/operations.md +565 -62
- package/docs/profiles.md +13 -1
- package/docs/quickstart.md +14 -9
- package/docs/research.md +7 -4
- package/docs/sdlc.md +54 -10
- package/docs/security.md +396 -34
- package/docs/smoke.md +127 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +1070 -22
- package/docs/verification/2026-10-27-0.19.0.md +5 -5
- package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-bash-permission.json +635 -0
- package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-permission-transport.json +403 -0
- package/docs/verification/2026-10-28-claude-implementer-permission-triage.md +123 -0
- package/docs/verification/2026-10-28-jcode-0.99.1-usage.md +41 -0
- package/docs/verification/2026-10-28-realpath-name-audit.md +222 -0
- package/docs/verification/2026-10-28-routing-f0bb8ad0.md +1000 -0
- package/docs/verification/2026-10-29-0.21.0.md +61 -0
- package/docs/verification/2026-10-29-model-effort-capabilities.md +123 -0
- package/docs/verification/2026-10-29-modelrouting-b152ddb9.md +953 -0
- package/docs/verification/claude-implementer-permission-triage.ts +160 -0
- package/docs/verification/claude-permission-transport-native.py +71 -14
- package/docs/verification/profiles/YYYY-MM-DD-research.md +1 -1
- package/docs/verified.json +15 -15
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1016 -280
- package/src/adapters/acp.ts +79 -3
- package/src/adapters/channel-peer.ts +4 -0
- package/src/adapters/claude-channel.ts +13 -6
- package/src/adapters/claude-implementer.ts +60 -7
- package/src/adapters/codex-appserver.ts +77 -4
- package/src/adapters/conformance/fakes/jcode-agent.ts +6 -2
- package/src/adapters/conformance/kit.ts +3 -9
- package/src/adapters/definitions/builtin-metadata.ts +3 -114
- package/src/adapters/definitions/native-config-defaults.ts +1 -1
- package/src/adapters/definitions/native-config-policy.ts +3 -2
- package/src/adapters/definitions/native-context.ts +12 -0
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-modules.ts +63 -3
- package/src/adapters/jcode.ts +6 -2
- package/src/adapters/local-worker.ts +40 -25
- package/src/adapters/native-proof-ports.ts +10 -0
- package/src/adapters/registry-types.ts +4 -0
- package/src/broker/protocol.ts +203 -0
- package/src/broker/server.ts +314 -0
- package/src/broker/store.ts +548 -0
- package/src/cli/bench.ts +3 -2
- package/src/cli/broker.ts +58 -0
- package/src/cli/conductor-recovery.ts +142 -0
- package/src/cli/configured-acp-entry.ts +26 -6
- package/src/cli/console-state.ts +59 -17
- package/src/cli/console.ts +17 -2
- package/src/cli/facts-hook.ts +3 -1
- package/src/cli/help.ts +28 -16
- package/src/cli/identity.ts +23 -10
- package/src/cli/init.ts +34 -7
- package/src/cli/intake-output.ts +33 -0
- package/src/cli/main.ts +314 -71
- package/src/cli/output.ts +41 -1
- package/src/cli/recovery-package.ts +18 -1
- package/src/cli/role-holders.ts +35 -0
- package/src/cli/room.ts +304 -0
- package/src/cli/say-input.ts +16 -0
- package/src/cli/schedule-output.ts +49 -0
- package/src/cli/sdlc-output.ts +44 -6
- package/src/cli/smoke-pty-bridge.ts +33 -0
- package/src/cli/smoke-pty.ts +145 -0
- package/src/cli/smoke-run.ts +139 -0
- package/src/cli/smoke-target.ts +130 -0
- package/src/cli/source-output.ts +29 -0
- package/src/cli/task-output.ts +3 -2
- package/src/cli/upgrade-runtime.ts +98 -16
- package/src/cli/upgrade.ts +66 -7
- package/src/hub/ask.ts +76 -12
- package/src/hub/board.ts +132 -22
- package/src/hub/bus.ts +399 -56
- package/src/hub/child-process.ts +2 -0
- package/src/hub/ci-watch.ts +42 -20
- package/src/hub/claude-failure-stage.ts +85 -0
- package/src/hub/claude-shell-wrapper.ts +11 -5
- package/src/hub/conductor.ts +17 -4
- package/src/hub/config-apply.ts +32 -5
- package/src/hub/config-reload.ts +7 -5
- package/src/hub/config-trust.ts +7 -1
- package/src/hub/context-window.ts +33 -3
- package/src/hub/control-client.ts +14 -2
- package/src/hub/corpus-config.ts +34 -0
- package/src/hub/corpus-evidence.ts +184 -0
- package/src/hub/corpus.ts +373 -0
- package/src/hub/cost-governance.ts +26 -0
- package/src/hub/daemon.ts +1150 -171
- package/src/hub/deliverable-files.ts +8 -2
- package/src/hub/delivery-journal.ts +123 -17
- package/src/hub/dogfood-profile.ts +32 -5
- package/src/hub/dogfood-read.ts +6 -2
- package/src/hub/envelope.ts +11 -0
- package/src/hub/eval-gate.ts +131 -0
- package/src/hub/events.ts +1 -1
- package/src/hub/external-instance.ts +69 -0
- package/src/hub/facts.ts +15 -5
- package/src/hub/folder-trigger.ts +223 -0
- package/src/hub/hub-tools.ts +2 -1
- package/src/hub/inference.ts +20 -5
- package/src/hub/instance-config.ts +69 -3
- package/src/hub/instance-execution.ts +6 -2
- package/src/hub/instances.ts +32 -4
- package/src/hub/intake.ts +125 -0
- package/src/hub/learning.ts +85 -0
- package/src/hub/lifecycle.ts +7 -3
- package/src/hub/limits.ts +12 -5
- package/src/hub/native-permission-request.ts +77 -0
- package/src/hub/operational-authority.ts +63 -5
- package/src/hub/operational-settings.ts +76 -0
- package/src/hub/operational-view.ts +1 -1
- package/src/hub/packs.ts +132 -0
- package/src/hub/path-policy.ts +7 -0
- package/src/hub/peer-doing.ts +130 -0
- package/src/hub/peers.ts +12 -0
- package/src/hub/queue-backpressure.ts +58 -0
- package/src/hub/read-tools.ts +2 -0
- package/src/hub/recovery-authority.ts +151 -0
- package/src/hub/recovery-policy.ts +220 -0
- package/src/hub/research.ts +24 -14
- package/src/hub/restart.ts +7 -0
- package/src/hub/routing.ts +54 -11
- package/src/hub/schedules.ts +266 -0
- package/src/hub/sdlc-merge.ts +38 -14
- package/src/hub/sdlc-metrics.ts +32 -10
- package/src/hub/sdlc-queue.ts +7 -1
- package/src/hub/sdlc-release.ts +1 -1
- package/src/hub/sdlc.ts +37 -7
- package/src/hub/security-baseline.ts +38 -0
- package/src/hub/settings.ts +6 -1
- package/src/hub/shipped-packs.ts +8 -0
- package/src/hub/snapshots.ts +72 -6
- package/src/hub/source-read.ts +160 -0
- package/src/hub/sources.ts +311 -0
- package/src/hub/tasks.ts +910 -110
- package/src/hub/workflow.ts +1 -1
- package/src/local/doc-read.ts +69 -0
- package/src/local/proxy.ts +4 -13
- package/src/local/tools.ts +37 -3
- package/src/local/web-fetch-config.ts +43 -0
- package/src/local/web-fetch.ts +183 -0
- package/src/memory/brief.ts +85 -13
- package/src/memory/capture.ts +1 -1
- package/src/rooms/application.ts +114 -0
- package/src/rooms/boundary.ts +103 -0
- package/src/rooms/commands.ts +294 -0
- package/src/rooms/crypto.ts +371 -0
- package/src/rooms/link.ts +584 -0
- package/src/rooms/policy.ts +82 -0
- package/src/rooms/remote-peer.ts +80 -0
- package/src/rooms/service.ts +426 -0
- package/src/rooms/status.ts +146 -0
- package/src/rooms/store.ts +1210 -0
- package/src/rooms/wire.ts +465 -0
- package/src/ui/index.html +91 -4
- package/templates/packs/example.pack.json +13 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** #544 opt-in diagnostic producer. It characterizes deliberate permission-push omission outside the regression suite. No installed Claude binary, account or model provider is used. */
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
|
+
import { COMPAT, assessVersion, trustFile, writeTrust } from "../../src/adapters/compat.ts";
|
|
9
|
+
import { startDaemon, loadConfig } from "../../src/hub/daemon.ts";
|
|
10
|
+
import { ControlClient } from "../../src/hub/control-client.ts";
|
|
11
|
+
import { hubGitEnv, hubGitSync } from "../../src/hub/git.ts";
|
|
12
|
+
import { hostExecutable } from "../../src/hub/host-executable.ts";
|
|
13
|
+
import { realPath } from "../../src/hub/project.ts";
|
|
14
|
+
import { shellQuote } from "../../src/cli/shell-quote.ts";
|
|
15
|
+
import { taskShellCommand } from "../../src/hub/task-shell-command.ts";
|
|
16
|
+
import { sandboxAvailable } from "../../src/local/sandbox.ts";
|
|
17
|
+
import { runConsole, type ConsoleTerminal } from "../../src/cli/console.ts";
|
|
18
|
+
|
|
19
|
+
// Keep testing the trusted-but-unverified path if a later native receipt verifies 2.1.312.
|
|
20
|
+
const VERSION = COMPAT["claude-implementer"].verified.includes("2.1.312") ? "9999.0.0" : "2.1.312";
|
|
21
|
+
const REPO_ROOT = resolve(import.meta.dir, "../..");
|
|
22
|
+
const COMMAND = "bun test test/ci-watch.test.ts";
|
|
23
|
+
async function until(check: () => boolean, label: string): Promise<void> {
|
|
24
|
+
for (let n = 0; n < 400 && !check(); n++) await new Promise<void>(done => setImmediate(done));
|
|
25
|
+
expect(check(), label).toBe(true);
|
|
26
|
+
}
|
|
27
|
+
async function withDeadline<T>(promise: Promise<T>, label: string): Promise<T> {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const timer = setTimeout(() => reject(new Error(`triage barrier timed out: ${label}`)), 5000);
|
|
30
|
+
promise.then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function deferred<T>() { let resolve!: (value: T) => void; const promise = new Promise<T>(done => { resolve = done; }); return { promise, resolve }; }
|
|
34
|
+
async function fixture() {
|
|
35
|
+
const root = realPath(mkdtempSync(join(tmpdir(), "ahub-544-"))), hostBin = realPath(mkdtempSync(join(tmpdir(), "ahub-544-host-bin-")));
|
|
36
|
+
const peer = "claude-triage", tree = join(root, ".worktrees", peer), home = join(root, "home"), bin = join(root, "bin"), stateDir = join(root, ".agenthub", "state"), callFile = join(root, "fake-call.json");
|
|
37
|
+
const keys = ["HOME", "CLAUDE_CONFIG_DIR", "AGENTHUB_HOME", "AMEM_HOME", "PATH"] as const;
|
|
38
|
+
const previous = Object.fromEntries(keys.map(key => [key, process.env[key]]));
|
|
39
|
+
const clients: ControlClient[] = []; let daemon: Awaited<ReturnType<typeof startDaemon>> | undefined;
|
|
40
|
+
const close = async () => {
|
|
41
|
+
for (const client of clients) client.close();
|
|
42
|
+
await daemon?.stop();
|
|
43
|
+
for (const key of keys) { if (previous[key] === undefined) delete process.env[key]; else process.env[key] = previous[key]; }
|
|
44
|
+
rmSync(root, { recursive: true, force: true }); rmSync(hostBin, { recursive: true, force: true });
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(home); mkdirSync(bin); mkdirSync(stateDir, { recursive: true });
|
|
48
|
+
const git = hostExecutable("git", root, previous.PATH ?? "/usr/bin:/bin");
|
|
49
|
+
writeFileSync(join(hostBin, "git"), `#!/bin/sh\nexport GIT_CONFIG_SYSTEM=/dev/null\nexport GIT_CONFIG_GLOBAL=/dev/null\nexec ${shellQuote(git)} "$@"\n`, { mode: 0o700 });
|
|
50
|
+
const env = hubGitEnv({ ...process.env, HOME: home, PATH: `${hostBin}:${previous.PATH ?? "/usr/bin:/bin"}` }, { repositoryIsolation: true });
|
|
51
|
+
for (const args of [["init", "-q"], ["-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "--allow-empty", "-qm", "triage fixture"], ["worktree", "add", "-qb", "triage/worker", tree], ["remote", "add", "origin", "https://github.com/fixture/triage.git"]]) expect(hubGitSync(["-C", root, ...args], { encoding: "utf8", env }).status).toBe(0);
|
|
52
|
+
mkdirSync(join(tree, "test")); writeFileSync(join(tree, "test", "ci-watch.test.ts"), 'throw new Error("fixture command must not execute");\n');
|
|
53
|
+
writeFileSync(join(bin, "claude"), `#!${process.execPath}\nimport{writeFileSync,renameSync}from"node:fs";\nif(process.argv.includes("--version")){console.log(${JSON.stringify(VERSION + " (Claude Code)")});process.exit(0)}\nawait new Response(Bun.stdin.stream()).text();\nwriteFileSync(${JSON.stringify(callFile + ".tmp")},JSON.stringify({args:process.argv.slice(2),launchId:process.env.AGENTHUB_LAUNCH_ID,peer:process.env.AGENTHUB_PEER_ID}));renameSync(${JSON.stringify(callFile + ".tmp")},${JSON.stringify(callFile)});\nsetInterval(()=>{},1000);\n`, { mode: 0o700 });
|
|
54
|
+
Object.assign(process.env, { HOME: home, CLAUDE_CONFIG_DIR: join(home, "claude"), AGENTHUB_HOME: join(home, "hub"), AMEM_HOME: join(home, "memory"), PATH: `${bin}:${hostBin}:${previous.PATH ?? "/usr/bin:/bin"}` });
|
|
55
|
+
writeTrust("claude-implementer", VERSION, false, trustFile(join(home, "hub")));
|
|
56
|
+
writeFileSync(join(root, ".agenthub", "config.local.json"), JSON.stringify({ instances: { [peer]: { kind: "claude", roles: ["implementer"], start_mode: "headless", worktree: `.worktrees/${peer}`, permission_mode: "ask-when-needed" } }, roles: { [peer]: ["implementer"] }, roster: { [peer]: true }, memory: { enabled: false, native: false }, inference: { enabled: false }, mlx: { enabled: false }, snapshots: { enabled: false }, pi: { enabled: false, auto_start: false }, task_sweep: { enabled: false }, batch_ms: 0 }));
|
|
57
|
+
daemon = await startDaemon({ cwd: root, stateDir, config: loadConfig(root), controlPort: 0, codexAppPort: 0, codexProxyPort: 0, permissionTimeoutMs: 4000, agentDrift: false, shellDefaultBranchProbe: async () => "main" });
|
|
58
|
+
const person = await ControlClient.connect(stateDir, { role: "console", projectRoot: root }); clients.push(person);
|
|
59
|
+
const instanceId = (await person.request({ t: "status" })).status.instanceId;
|
|
60
|
+
expect(await person.request({ t: "start", peer })).toMatchObject({ ok: true, executionCwd: tree });
|
|
61
|
+
const proposed = await person.request({ t: "task", op: "hub_task_propose", args: { title: "isolated permission triage", class: "test", owner: peer } });
|
|
62
|
+
expect(proposed.ok).toBe(true); const task = Number(String(proposed.text).match(/task #(\d+)/)?.[1]); expect(task).toBeGreaterThan(0);
|
|
63
|
+
// The fake records before idling. This waits for its consumed stdin, never an installed native process.
|
|
64
|
+
for (let n = 0; n < 300 && !existsSync(callFile); n++) await Bun.sleep(5);
|
|
65
|
+
expect(existsSync(callFile)).toBe(true);
|
|
66
|
+
const call = JSON.parse(readFileSync(callFile, "utf8")) as { args: string[]; launchId: string; peer: string };
|
|
67
|
+
const tools = await ControlClient.connect(stateDir, { role: "tools", peer, launchId: call.launchId, projectRoot: root }); clients.push(tools);
|
|
68
|
+
expect(await tools.request({ t: "task", op: "hub_task_accept", args: { id: task } })).toMatchObject({ ok: true });
|
|
69
|
+
const connectPerson = async () => { const client = await ControlClient.connect(stateDir, { role: "console", projectRoot: root }); clients.push(client); return client; };
|
|
70
|
+
return { root, home, peer, tree, stateDir, person, tools, call, instanceId, close, connectPerson };
|
|
71
|
+
} catch (error) { await close(); throw error; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const permissionPush = (client: ControlClient) => {
|
|
75
|
+
const pending = deferred<any>(), closed = deferred<any>(), pushes: any[] = [];
|
|
76
|
+
client.onPush = message => { pushes.push(message); if (message.t === "permission") pending.resolve(message); if (message.t === "permission_closed") closed.resolve(message); };
|
|
77
|
+
client.send({ t: "tail" });
|
|
78
|
+
return { pending: () => withDeadline(pending.promise, "permission push"), closed: () => withDeadline(closed.promise, "permission closure"), pushes };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// This proves host policy using a fake native and never redeems the issued shell ticket.
|
|
82
|
+
test("#544 trusted unverified admission does not itself disable the closed host-wrapper permission path", async () => {
|
|
83
|
+
expect(assessVersion("claude-implementer", VERSION, [])).toMatchObject({ ok: false, state: "refused" });
|
|
84
|
+
expect(assessVersion("claude-implementer", VERSION, [{ id: "claude-implementer", version: VERSION, at: 1 }])).toMatchObject({ ok: true, state: "trusted" });
|
|
85
|
+
expect(taskShellCommand(COMMAND, { branch: "triage/worker", defaultBranch: "main" })?.kind).toBe("test");
|
|
86
|
+
const f = await fixture();
|
|
87
|
+
try {
|
|
88
|
+
expect(f.call.args[f.call.args.indexOf("--permission-mode") + 1]).toBe("default");
|
|
89
|
+
expect(f.call.args[f.call.args.indexOf("--permission-prompt-tool") + 1]).toBe("mcp__agent-hub__hub_permission_request");
|
|
90
|
+
expect(f.call.args).not.toContain("bypassPermissions"); expect(f.call.args).not.toContain("dontAsk");
|
|
91
|
+
const answer = await f.tools.request({ t: "permission_request", tool: "Bash", input: { command: COMMAND }, wrapperProtocol: 1 });
|
|
92
|
+
if (sandboxAvailable()) {
|
|
93
|
+
expect(answer).toMatchObject({ ok: true, behavior: "allow", wrapperProtocol: 1 });
|
|
94
|
+
expect(answer.updatedInput.command).not.toBe(COMMAND);
|
|
95
|
+
} else {
|
|
96
|
+
expect(answer).toMatchObject({ ok: true, behavior: "deny", message: "no person answered" });
|
|
97
|
+
expect(answer.updatedInput).toBeUndefined();
|
|
98
|
+
}
|
|
99
|
+
} finally { await f.close(); }
|
|
100
|
+
}, 20_000);
|
|
101
|
+
|
|
102
|
+
test("#544 real daemon pending approvals are pushed, replayed to a fresh console, then withdrawn on timeout", async () => {
|
|
103
|
+
const f = await fixture();
|
|
104
|
+
try {
|
|
105
|
+
const live = permissionPush(f.person);
|
|
106
|
+
const answer = f.tools.request({ t: "permission_request", tool: "Bash", input: { command: "pwd && pwd" }, wrapperProtocol: 1 }); void answer.catch(() => {});
|
|
107
|
+
const request = await live.pending();
|
|
108
|
+
expect(request).toMatchObject({ peer: f.peer, options: [{ optionId: "allow", name: "Allow once", kind: "allow_once" }, { optionId: "deny", name: "Deny", kind: "reject_once" }] });
|
|
109
|
+
expect((await f.person.request({ t: "ui_snapshot" })).permissions).toContainEqual(expect.objectContaining({ id: request.id, peer: f.peer }));
|
|
110
|
+
expect((await f.person.request({ t: "status" })).status.attentionItems).toContainEqual(expect.objectContaining({ kind: "approval", id: request.id, peer: f.peer, level: "act" }));
|
|
111
|
+
const late = await f.connectPerson(), replay = permissionPush(late);
|
|
112
|
+
expect((await replay.pending()).id).toBe(request.id);
|
|
113
|
+
expect(await answer).toMatchObject({ ok: true, behavior: "deny", message: "no person answered" });
|
|
114
|
+
expect(await live.closed()).toMatchObject({ id: request.id, outcome: "cancelled", reason: "expired" });
|
|
115
|
+
expect((await f.person.request({ t: "ui_snapshot" })).permissions).toEqual([]);
|
|
116
|
+
expect((await f.person.request({ t: "status" })).status.attentionItems.filter((item: { kind: string }) => item.kind === "approval")).toEqual([]);
|
|
117
|
+
} finally { await f.close(); }
|
|
118
|
+
}, 20_000);
|
|
119
|
+
|
|
120
|
+
test("#544 missing native tool_use_id denies in the production MCP channel before any daemon approval", async () => {
|
|
121
|
+
const f = await fixture(), observed = permissionPush(f.person); let client: Client | undefined;
|
|
122
|
+
try {
|
|
123
|
+
await f.person.request({ t: "status" }); // Same socket barrier: tail is registered before the test calls MCP.
|
|
124
|
+
const env = Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined && !entry[0].startsWith("AGENTHUB_") && !["CLAUDECODE", "CLAUDE_CODE_SESSION_ID"].includes(entry[0])));
|
|
125
|
+
Object.assign(env, { HOME: f.home, CLAUDE_CONFIG_DIR: join(f.home, "claude"), AGENTHUB_HOME: join(f.home, "hub"), AMEM_HOME: join(f.home, "memory"), AGENTHUB_MODE: "tools", AGENTHUB_PERMISSION_TOOL: "1", AGENTHUB_PEER_ID: f.peer, AGENTHUB_PROJECT_DIR: f.root, AGENTHUB_STATE_DIR: f.stateDir, AGENTHUB_INSTANCE_ID: f.instanceId, AGENTHUB_LAUNCH_ID: f.call.launchId });
|
|
126
|
+
const transport = new StdioClientTransport({ command: process.execPath, args: [join(REPO_ROOT, "src", "adapters", "claude-channel.ts")], cwd: f.tree, env, stderr: "pipe" });
|
|
127
|
+
let diagnostics = ""; transport.stderr?.on("data", chunk => { diagnostics = (diagnostics + String(chunk)).slice(-16384); });
|
|
128
|
+
client = new Client({ name: "permission-triage-fixture", version: "1" }); await client.connect(transport);
|
|
129
|
+
for (let n = 0; n < 300 && !diagnostics.includes(`connected to hub as "${f.peer}"`); n++) await Bun.sleep(5);
|
|
130
|
+
expect(diagnostics).toContain(`connected to hub as "${f.peer}"`);
|
|
131
|
+
const schema = (await client.listTools()).tools.find(tool => tool.name === "hub_permission_request")!.inputSchema;
|
|
132
|
+
expect(schema.required).toEqual(["tool_name", "input", "tool_use_id"]);
|
|
133
|
+
const result = await client.callTool({ name: "hub_permission_request", arguments: { tool_name: "Bash", input: { command: COMMAND } } });
|
|
134
|
+
const content = result.content as { type: string; text?: string }[];
|
|
135
|
+
expect(JSON.parse(content.filter(row => row.type === "text").map(row => row.text).join("\n"))).toEqual({ behavior: "deny", message: "invalid native permission request" });
|
|
136
|
+
expect(observed.pushes.filter(message => message.t === "permission")).toEqual([]);
|
|
137
|
+
expect((await f.person.request({ t: "ui_snapshot" })).permissions).toEqual([]);
|
|
138
|
+
expect((await f.person.request({ t: "status" })).status.attentionItems.filter((item: { kind: string }) => item.kind === "approval")).toEqual([]);
|
|
139
|
+
} finally { await client?.close(); await f.close(); }
|
|
140
|
+
}, 20_000);
|
|
141
|
+
|
|
142
|
+
// Deliberately dropped push: this proves the UI's current information-source boundary,
|
|
143
|
+
// not that a real WebSocket lost a message in the reported incident.
|
|
144
|
+
test("#544 console status refresh alone cannot reconstruct a missed permission push, while a later push renders", async () => {
|
|
145
|
+
const root = mkdtempSync(join(tmpdir(), "ahub-544-console-")), output: string[] = [];
|
|
146
|
+
const id = "triage-approval", expiresAt = Date.now() + 60_000;
|
|
147
|
+
let input = (_text: string) => {}, signal = () => {}, statusReads = 0;
|
|
148
|
+
const terminal: ConsoleTerminal = { columns: 120, rows: 30, isTTY: true, write: text => { output.push(text); }, raw: () => {}, onData: listener => { input = listener; return () => {}; }, onResize: () => () => {}, onSignal: listener => { signal = listener; return () => {}; } };
|
|
149
|
+
const client = { onPush: (_message: any) => {}, onClose: (_code: number, _reason: string) => {}, send: (_message: any) => {}, close: () => {}, request: async (message: any) => {
|
|
150
|
+
if (message.t === "status") statusReads++;
|
|
151
|
+
return { ok: true, status: { peers: {}, attentionItems: [{ kind: "approval", level: "act", id, peer: "claude-triage", until: expiresAt }] }, budget: {}, text: "[]", deliveries: [] };
|
|
152
|
+
} };
|
|
153
|
+
const running = runConsole({ client, cwd: root, stateDir: root, terminal, panels: true, noPet: true, color: false });
|
|
154
|
+
try {
|
|
155
|
+
await until(() => statusReads >= 1, "initial positive status"); input("2"); await until(() => statusReads >= 2, "refresh after choosing approvals");
|
|
156
|
+
expect(output.join("")).toContain("0 approvals"); expect(output.join("")).not.toContain("TRIAGE-PENDING-BASH");
|
|
157
|
+
client.onPush({ t: "permission", id, peer: "claude-triage", title: "TRIAGE-PENDING-BASH", expiresAt, options: [{ optionId: "allow", name: "Allow once", kind: "allow_once" }, { optionId: "deny", name: "Deny", kind: "reject_once" }] });
|
|
158
|
+
expect(output.join("")).toContain("1 approval"); expect(output.join("")).toContain("TRIAGE-PENDING-BASH");
|
|
159
|
+
} finally { signal(); await running; rmSync(root, { recursive: true, force: true }); }
|
|
160
|
+
});
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
# Model-free native receipt of the headless Claude permission transport (#843 AC5, task #507).
|
|
2
2
|
# Repeats the case table of 2026-10-23-headless-claude-permissions.md on each installed binary given.
|
|
3
3
|
# Usage: python3 claude-permission-transport-native.py OUTPUT --claude /absolute/claude [--claude ...]
|
|
4
|
-
#
|
|
5
|
-
|
|
4
|
+
# Default: nine Write cases. --tool Bash selects a harmless fixture writer; --rewrite-bash narrows Bash to two replacement cases.
|
|
5
|
+
# No account, no provider, no model: a loopback server supplies the fixed tool call, then a completion.
|
|
6
|
+
import argparse, hashlib, http.server, json, os, pathlib, re, shlex, signal, subprocess, threading, time
|
|
7
|
+
|
|
8
|
+
def mcp_name(value):
|
|
9
|
+
if not re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,63}', value):
|
|
10
|
+
raise argparse.ArgumentTypeError('MCP names must be 1..64 ASCII letters, digits, underscores or hyphens, starting with a letter')
|
|
11
|
+
return value
|
|
12
|
+
|
|
6
13
|
|
|
7
14
|
parser = argparse.ArgumentParser()
|
|
8
15
|
parser.add_argument('output')
|
|
9
16
|
parser.add_argument('--claude', action='append', required=True)
|
|
17
|
+
parser.add_argument('--tool', choices=('Write', 'Bash'), default='Write')
|
|
18
|
+
parser.add_argument('--rewrite-bash', action='store_true', help='Bash only: two allow/deny cases proving native updatedInput command replacement')
|
|
19
|
+
parser.add_argument('--server-name', type=mcp_name, default='fixture')
|
|
20
|
+
parser.add_argument('--permission-tool', type=mcp_name, default='approve')
|
|
21
|
+
parser.add_argument('--require-tool-use-id', action='store_true', help='Advertise the three required production permission fields and reject extra properties')
|
|
10
22
|
args = parser.parse_args()
|
|
23
|
+
if args.rewrite_bash and args.tool != 'Bash': parser.error('--rewrite-bash requires --tool Bash')
|
|
11
24
|
ROOT = pathlib.Path(args.output).resolve()
|
|
12
25
|
ROOT.mkdir(mode=0o700, parents=False, exist_ok=False)
|
|
13
26
|
|
|
@@ -20,13 +33,22 @@ for line in sys.stdin:
|
|
|
20
33
|
method, rid = q.get('method'), q.get('id')
|
|
21
34
|
if rid is None: continue
|
|
22
35
|
if method == 'initialize':
|
|
23
|
-
r = {'protocolVersion': '2024-11-05', 'capabilities': {'tools': {}}, 'serverInfo': {'name': 'fixture', 'version': '1'}}
|
|
36
|
+
r = {'protocolVersion': '2024-11-05', 'capabilities': {'tools': {}}, 'serverInfo': {'name': os.environ.get('SERVER_NAME', 'fixture'), 'version': '1'}}
|
|
24
37
|
elif method == 'tools/list':
|
|
25
|
-
|
|
38
|
+
schema = {'type': 'object', 'properties': {'tool_name': {'type': 'string'}, 'input': {'type': 'object'}, 'tool_use_id': {'type': 'string'}}, 'required': ['tool_name', 'input']}
|
|
39
|
+
if os.environ.get('REQUIRE_TOOL_USE_ID') == '1':
|
|
40
|
+
schema['required'].append('tool_use_id'); schema['additionalProperties'] = False
|
|
41
|
+
schema['properties']['tool_name']['maxLength'] = 128; schema['properties']['tool_use_id']['maxLength'] = 256
|
|
42
|
+
tools = [] if case == 'missing-tool' else [{'name': os.environ.get('PERMISSION_TOOL', 'approve'), 'description': 'Person approval fixture', 'inputSchema': schema}]
|
|
26
43
|
r = {'tools': tools}
|
|
27
44
|
elif method == 'tools/call':
|
|
28
45
|
args = q.get('params', {}).get('arguments', {})
|
|
29
|
-
|
|
46
|
+
record = {'arguments': args}
|
|
47
|
+
if os.environ.get('PERMISSION_TOOL') or os.environ.get('REQUIRE_TOOL_USE_ID') == '1': record['tool'] = q.get('params', {}).get('name')
|
|
48
|
+
rewrite = os.environ.get('REWRITE_COMMAND')
|
|
49
|
+
if rewrite:
|
|
50
|
+
record['response'] = {'behavior': 'allow', 'updatedInput': {**args.get('input', {}), 'command': rewrite}}
|
|
51
|
+
with open(os.environ['RECEIPT'], 'a') as f: print(json.dumps(record), file=f)
|
|
30
52
|
if case == 'pending': time.sleep(120)
|
|
31
53
|
if case == 'disconnect': sys.exit(1)
|
|
32
54
|
if case == 'mcp-error':
|
|
@@ -35,7 +57,7 @@ for line in sys.stdin:
|
|
|
35
57
|
if case in ('deny', 'hook-deny', 'deadline'):
|
|
36
58
|
value = {'behavior': 'deny', 'message': 'no person answered' if case == 'deadline' else 'fixture denied'}
|
|
37
59
|
else:
|
|
38
|
-
value = {'behavior': 'allow', 'updatedInput': args.get('input', {})}
|
|
60
|
+
value = record.get('response', {'behavior': 'allow', 'updatedInput': args.get('input', {})})
|
|
39
61
|
r = {'content': [{'type': 'text', 'text': json.dumps(value)}]}
|
|
40
62
|
else:
|
|
41
63
|
r = {}
|
|
@@ -58,7 +80,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
58
80
|
msgs = q.get('messages', [])
|
|
59
81
|
results = sum(1 for m in msgs if isinstance(m.get('content'), list) for x in m['content'] if isinstance(x, dict) and x.get('type') == 'tool_result')
|
|
60
82
|
done = results >= 1
|
|
61
|
-
content = [{'type': 'text', 'text': 'Fixture finished.'}] if done else [{'type': 'tool_use', 'id': 'toolu_probe_0', 'name': '
|
|
83
|
+
content = [{'type': 'text', 'text': 'Fixture finished.'}] if done else [{'type': 'tool_use', 'id': 'toolu_probe_0', 'name': state['tool'], 'input': state['input']}]
|
|
62
84
|
result = {'id': 'msg_probe_%d' % results, 'type': 'message', 'role': 'assistant', 'model': 'claude-sonnet-4-5', 'content': content, 'stop_reason': 'end_turn' if done else 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 100, 'output_tokens': 10}}
|
|
63
85
|
if not q.get('stream'):
|
|
64
86
|
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(json.dumps(result).encode()); return
|
|
@@ -101,7 +123,7 @@ def rows(file):
|
|
|
101
123
|
return [json.loads(line) for line in file.read_text().splitlines() if line] if file.exists() else []
|
|
102
124
|
|
|
103
125
|
|
|
104
|
-
HOOK_FAIL = {'hooks': {'PreToolUse': [{'matcher':
|
|
126
|
+
HOOK_FAIL = {'hooks': {'PreToolUse': [{'matcher': args.tool, 'hooks': [{'type': 'command', 'command': 'exit 1'}]}]}}
|
|
105
127
|
# case -> (settings, expected native exit, mcp calls, file written, tool_result is_error, external stop expected)
|
|
106
128
|
CASES = {
|
|
107
129
|
'allow': ({}, 0, 1, True, False, False),
|
|
@@ -115,6 +137,7 @@ CASES = {
|
|
|
115
137
|
'deadline': ({}, 0, 1, False, True, False),
|
|
116
138
|
}
|
|
117
139
|
|
|
140
|
+
selected_cases = {case: value for case, value in CASES.items() if not args.rewrite_bash or case in ('allow', 'deny')}
|
|
118
141
|
results = []
|
|
119
142
|
for index, raw in enumerate(args.claude):
|
|
120
143
|
binary = pathlib.Path(raw).resolve()
|
|
@@ -124,20 +147,42 @@ for index, raw in enumerate(args.claude):
|
|
|
124
147
|
v = subprocess.run([str(binary), '--version'], env={'HOME': str(ident), 'CLAUDE_CONFIG_DIR': str(ident / 'config'), 'PATH': '/usr/bin:/bin'}, capture_output=True, text=True, timeout=10)
|
|
125
148
|
assert v.returncode == 0, v
|
|
126
149
|
version = v.stdout.strip().split()[0]
|
|
127
|
-
for case, (settings, want_exit, want_calls, want_file, want_error, want_stop) in
|
|
150
|
+
for case, (settings, want_exit, want_calls, want_file, want_error, want_stop) in selected_cases.items():
|
|
128
151
|
d = ROOT / ('%d-%s-%s' % (index, version, case)); d.mkdir()
|
|
129
152
|
home = d / 'home'; home.mkdir(); work = d / 'work'; work.mkdir(); work = work.resolve()
|
|
130
153
|
target = work / 'probe.txt'
|
|
131
154
|
out = {'version': version, 'binarySha256': digest, 'case': case}
|
|
155
|
+
if args.tool != 'Write': out.update(tool=args.tool, rewriteBash=args.rewrite_bash)
|
|
156
|
+
if args.server_name != 'fixture' or args.permission_tool != 'approve' or args.require_tool_use_id: out.update(serverName=args.server_name, permissionTool=args.permission_tool, requireToolUseId=args.require_tool_use_id)
|
|
132
157
|
srv = native = None
|
|
133
158
|
try:
|
|
159
|
+
harmless = 'printf %s probe > ' + shlex.quote(str(target))
|
|
160
|
+
tool_input = {'file_path': str(target), 'content': 'probe'} if args.tool == 'Write' else {'command': 'bun test test/ci-watch.test.ts' if args.rewrite_bash else harmless}
|
|
161
|
+
settings = dict(settings)
|
|
162
|
+
if args.tool == 'Bash': settings['permissions'] = {'ask': ['Bash']}
|
|
163
|
+
fixture_path = '/usr/bin:/bin'
|
|
164
|
+
original_marker = work / 'original-command-ran'
|
|
165
|
+
if args.rewrite_bash:
|
|
166
|
+
(work / 'test').mkdir()
|
|
167
|
+
(work / 'test' / 'ci-watch.test.ts').write_text('throw new Error("original command must not execute");\n')
|
|
168
|
+
(work / 'test' / 'ci-watch.test.ts').chmod(0o600)
|
|
169
|
+
fixture_bin = d / 'bin'; fixture_bin.mkdir(mode=0o700)
|
|
170
|
+
fake_bun = fixture_bin / 'bun'
|
|
171
|
+
fake_bun.write_text('#!/bin/sh\nprintf %s original > ' + shlex.quote(str(original_marker)) + '\nexit 97\n')
|
|
172
|
+
fake_bun.chmod(0o700)
|
|
173
|
+
fixture_path = str(fixture_bin.resolve()) + ':' + fixture_path
|
|
174
|
+
mcp_env = {'CASE': case, 'RECEIPT': str(d / 'mcp.jsonl')}
|
|
175
|
+
if args.server_name != 'fixture': mcp_env['SERVER_NAME'] = args.server_name
|
|
176
|
+
if args.permission_tool != 'approve': mcp_env['PERMISSION_TOOL'] = args.permission_tool
|
|
177
|
+
if args.require_tool_use_id: mcp_env['REQUIRE_TOOL_USE_ID'] = '1'
|
|
178
|
+
if args.rewrite_bash and case == 'allow': mcp_env['REWRITE_COMMAND'] = harmless
|
|
134
179
|
srv = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
|
135
|
-
srv.state = {'requests': 0, 'target': str(target), 'tools_file': str(d / 'tools.json')}
|
|
180
|
+
srv.state = {'requests': 0, 'target': str(target), 'tools_file': str(d / 'tools.json'), 'tool': args.tool, 'input': tool_input}
|
|
136
181
|
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
|
137
|
-
(d / 'mcp.json').write_text(json.dumps({'mcpServers': {
|
|
182
|
+
(d / 'mcp.json').write_text(json.dumps({'mcpServers': {args.server_name: {'command': '/usr/bin/python3', 'args': [str(ROOT / 'mcp.py')], 'env': mcp_env}}}))
|
|
138
183
|
(d / 'settings.json').write_text(json.dumps(settings))
|
|
139
|
-
env = {'HOME': str(home), 'CLAUDE_CONFIG_DIR': str(home / 'config'), 'PATH':
|
|
140
|
-
argv = [str(binary), '-p', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', str(d / 'mcp.json'), '--setting-sources', '', '--settings', str(d / 'settings.json'), '--permission-mode', 'default', '--permission-prompts', 'host', '--permission-prompt-tool', '
|
|
184
|
+
env = {'HOME': str(home), 'CLAUDE_CONFIG_DIR': str(home / 'config'), 'PATH': fixture_path, 'ANTHROPIC_BASE_URL': 'http://127.0.0.1:%d' % srv.server_port, 'ANTHROPIC_API_KEY': 'synthetic-probe-not-a-credential', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC': '1', 'DISABLE_AUTOUPDATER': '1'}
|
|
185
|
+
argv = [str(binary), '-p', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', str(d / 'mcp.json'), '--setting-sources', '', '--settings', str(d / 'settings.json'), '--permission-mode', 'default', '--permission-prompts', 'host', '--permission-prompt-tool', 'mcp__%s__%s' % (args.server_name, args.permission_tool), '--model', 'claude-sonnet-4-5', 'Run the fixture write.' if args.tool == 'Write' else 'Run the fixture Bash command.']
|
|
141
186
|
started = time.monotonic()
|
|
142
187
|
native = subprocess.Popen(argv, cwd=work, env=env, stdout=open(d / 'stdout', 'w'), stderr=open(d / 'stderr', 'w'), start_new_session=True)
|
|
143
188
|
stopped_externally = False
|
|
@@ -158,7 +203,14 @@ for index, raw in enumerate(args.claude):
|
|
|
158
203
|
assert len(calls) == want_calls, ('mcp calls', calls)
|
|
159
204
|
if want_calls:
|
|
160
205
|
call = calls[0]['arguments']
|
|
161
|
-
assert call['tool_name'] ==
|
|
206
|
+
assert call['tool_name'] == args.tool and call['input'] == tool_input and call['tool_use_id'] == 'toolu_probe_0', call
|
|
207
|
+
if args.permission_tool != 'approve' or args.require_tool_use_id: assert calls[0]['tool'] == args.permission_tool, calls[0]
|
|
208
|
+
if args.rewrite_bash and case == 'allow':
|
|
209
|
+
assert calls[0]['response'] == {'behavior': 'allow', 'updatedInput': {'command': harmless}}, calls[0]
|
|
210
|
+
if args.rewrite_bash:
|
|
211
|
+
out['originalCommandExecuted'] = original_marker.exists()
|
|
212
|
+
assert not original_marker.exists(), 'original bun command executed instead of the rewritten fixture writer'
|
|
213
|
+
assert tool_input == {'command': 'bun test test/ci-watch.test.ts'}, tool_input
|
|
162
214
|
assert written == want_file, ('file written', written)
|
|
163
215
|
# A successful native tool_result carries no is_error key at all; only a failed one sets it to true.
|
|
164
216
|
if want_error is not None: assert [b.get('is_error') is True for b in tool_results] == [want_error], tool_results
|
|
@@ -177,11 +229,16 @@ for index, raw in enumerate(args.claude):
|
|
|
177
229
|
out['nativeGroupEmpty'] = empty_group(native.pid)
|
|
178
230
|
if not out['nativeGroupEmpty']: out.update(passed=False, cleanup='native group not established empty')
|
|
179
231
|
if srv: srv.shutdown()
|
|
232
|
+
if args.rewrite_bash:
|
|
233
|
+
out['originalCommandExecuted'] = original_marker.exists()
|
|
234
|
+
if out['originalCommandExecuted']: out.update(passed=False, error='original bun command executed instead of the rewritten fixture writer')
|
|
180
235
|
out['binaryUnchanged'] = sha(binary) == digest
|
|
181
236
|
if not out['binaryUnchanged']: out.update(passed=False, error='native binary changed during probe')
|
|
182
237
|
results.append(out)
|
|
183
238
|
print(json.dumps(out), flush=True)
|
|
184
239
|
receipt = {'cases': results, 'certified': all(r['passed'] for r in results), 'account': False, 'inference': False}
|
|
240
|
+
if args.tool != 'Write': receipt.update(tool=args.tool, rewriteBash=args.rewrite_bash)
|
|
241
|
+
if args.server_name != 'fixture' or args.permission_tool != 'approve' or args.require_tool_use_id: receipt.update(serverName=args.server_name, permissionTool=args.permission_tool, requireToolUseId=args.require_tool_use_id)
|
|
185
242
|
(ROOT / 'receipt.json').write_text(json.dumps(receipt, indent=2))
|
|
186
243
|
print(json.dumps({'receipt': str(ROOT / 'receipt.json'), 'certified': receipt['certified']}), flush=True)
|
|
187
244
|
raise SystemExit(0 if receipt['certified'] else 1)
|
|
@@ -25,7 +25,7 @@ Copy only emitted counts and anonymous task/claim slots. These placeholders rema
|
|
|
25
25
|
- Missing check keys per run: unknown
|
|
26
26
|
- Section-overlap notices and confirmed section edits: unknown
|
|
27
27
|
|
|
28
|
-
Task-check history alone does not prove checks.fact_check ran. A model label or task approval alone does not prove a person verdict or agreement. Preserve every emitted null.
|
|
28
|
+
Task-check history alone does not prove checks.fact_check ran; only entries that carry the hub's own `checkKey` do (#901), and an older keyless entry makes the group unknown. A model label or task approval alone does not prove a person verdict or agreement; a verdict is the person's `ahub task claim` entry, and agreement needs the model's own verdict entered with it. Preserve every emitted null.
|
|
29
29
|
|
|
30
30
|
## Verification verdicts
|
|
31
31
|
|
package/docs/verified.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"documents": {
|
|
4
4
|
"README.md": {
|
|
5
|
-
"verifiedAgainst": "
|
|
5
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
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": "
|
|
14
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
15
15
|
"paths": [
|
|
16
16
|
"src/",
|
|
17
17
|
"templates/"
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
20
|
"docs/operations.md": {
|
|
21
|
-
"verifiedAgainst": "
|
|
21
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
22
22
|
"paths": [
|
|
23
23
|
"package.json",
|
|
24
24
|
"src/",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"docs/quickstart.md": {
|
|
38
|
-
"verifiedAgainst": "
|
|
38
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
39
39
|
"paths": [
|
|
40
40
|
"package.json",
|
|
41
41
|
"src/",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
"docs/agent-notes/adapters.md": {
|
|
46
|
-
"verifiedAgainst": "
|
|
46
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
47
47
|
"paths": [
|
|
48
48
|
"src/adapters/",
|
|
49
49
|
"src/pi/",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
]
|
|
82
82
|
},
|
|
83
83
|
"docs/agent-notes/benchmarks.md": {
|
|
84
|
-
"verifiedAgainst": "
|
|
84
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
85
85
|
"paths": [
|
|
86
86
|
"scripts/benchmarks/",
|
|
87
87
|
"test/benchmarks/",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
]
|
|
94
94
|
},
|
|
95
95
|
"docs/agent-notes/budget.md": {
|
|
96
|
-
"verifiedAgainst": "
|
|
96
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
97
97
|
"paths": [
|
|
98
98
|
"src/hub/budget.ts",
|
|
99
99
|
"src/cli/statusline-tee.ts",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
]
|
|
118
118
|
},
|
|
119
119
|
"docs/agent-notes/bus.md": {
|
|
120
|
-
"verifiedAgainst": "
|
|
120
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
121
121
|
"paths": [
|
|
122
122
|
"src/hub/bus.ts",
|
|
123
123
|
"src/hub/envelope.ts",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
]
|
|
133
133
|
},
|
|
134
134
|
"docs/agent-notes/daemon.md": {
|
|
135
|
-
"verifiedAgainst": "
|
|
135
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
136
136
|
"paths": [
|
|
137
137
|
"src/hub/daemon.ts",
|
|
138
138
|
"src/hub/control-client.ts",
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
]
|
|
206
206
|
},
|
|
207
207
|
"docs/agent-notes/local-worker.md": {
|
|
208
|
-
"verifiedAgainst": "
|
|
208
|
+
"verifiedAgainst": "aeeaeb048860316dd4e08d84a658519ed6c0ad24",
|
|
209
209
|
"paths": [
|
|
210
210
|
"src/adapters/local-worker.ts",
|
|
211
211
|
"src/local/",
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
]
|
|
218
218
|
},
|
|
219
219
|
"docs/agent-notes/models.md": {
|
|
220
|
-
"verifiedAgainst": "
|
|
220
|
+
"verifiedAgainst": "aeeaeb048860316dd4e08d84a658519ed6c0ad24",
|
|
221
221
|
"paths": [
|
|
222
222
|
"src/models/",
|
|
223
223
|
"src/hub/inference.ts",
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
]
|
|
226
226
|
},
|
|
227
227
|
"docs/agent-notes/tasks.md": {
|
|
228
|
-
"verifiedAgainst": "
|
|
228
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
229
229
|
"paths": [
|
|
230
230
|
"src/hub/tasks.ts",
|
|
231
231
|
"src/hub/board.ts",
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
]
|
|
265
265
|
},
|
|
266
266
|
"docs/agent-notes/tests.md": {
|
|
267
|
-
"verifiedAgainst": "
|
|
267
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
268
268
|
"paths": [
|
|
269
269
|
"test/",
|
|
270
270
|
"scripts/check.sh",
|
|
@@ -292,7 +292,7 @@
|
|
|
292
292
|
]
|
|
293
293
|
},
|
|
294
294
|
"docs/agent-notes/README.md": {
|
|
295
|
-
"verifiedAgainst": "
|
|
295
|
+
"verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
|
|
296
296
|
"paths": [
|
|
297
297
|
"src/cli/console-state.ts",
|
|
298
298
|
"src/cli/attention.ts",
|
|
@@ -301,7 +301,7 @@
|
|
|
301
301
|
]
|
|
302
302
|
},
|
|
303
303
|
"src/cli/help.ts": {
|
|
304
|
-
"verifiedAgainst": "
|
|
304
|
+
"verifiedAgainst": "aeeaeb048860316dd4e08d84a658519ed6c0ad24",
|
|
305
305
|
"paths": [
|
|
306
306
|
"src/cli/main.ts",
|
|
307
307
|
"src/cli/launch.ts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staix/agent-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
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",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"bun": ">=1.4.2"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@staix/agent-mem": "0.
|
|
20
|
+
"@staix/agent-mem": "0.13.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|