@staix/agent-hub 0.12.36 → 0.12.38

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 (85) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +18 -11
  3. package/docs/agent-notes/adapters.md +19 -6
  4. package/docs/agent-notes/budget.md +2 -1
  5. package/docs/agent-notes/bus.md +3 -2
  6. package/docs/agent-notes/daemon.md +9 -3
  7. package/docs/agent-notes/tasks.md +8 -4
  8. package/docs/agent-notes/tests.md +2 -2
  9. package/docs/memory.md +3 -1
  10. package/docs/operations.md +57 -10
  11. package/docs/quickstart.md +15 -7
  12. package/docs/sdlc.md +29 -3
  13. package/docs/security.md +10 -2
  14. package/docs/smoke.md +38 -2
  15. package/docs/specs/2026-09-19-agent-hub-design.md +58 -4
  16. package/docs/verification/2026-10-19-0.12.37.md +60 -0
  17. package/docs/verification/2026-10-19-agy-p1-t0.md +55 -0
  18. package/docs/verification/2026-10-19-bus-defer-reset.md +9 -0
  19. package/docs/verification/2026-10-19-jcode-0.98.1.md +30 -0
  20. package/docs/verification/2026-10-19-kimi-agy-reviewer-isolation.md +220 -0
  21. package/docs/verification/2026-10-19-reviewer-isolation.md +119 -0
  22. package/docs/verification/2026-10-20-0.12.38.md +56 -0
  23. package/docs/verified.json +27 -15
  24. package/package.json +2 -2
  25. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  26. package/plugins/agent-hub/server.js +5627 -3670
  27. package/src/adapters/acp.ts +3 -2
  28. package/src/adapters/agy/definition.ts +61 -0
  29. package/src/adapters/agy/entry.ts +76 -0
  30. package/src/adapters/agy/hooks.ts +81 -0
  31. package/src/adapters/claude-channel.ts +1 -0
  32. package/src/adapters/claude-print.ts +18 -156
  33. package/src/adapters/codex-review-isolation.ts +290 -0
  34. package/src/adapters/definitions/builtin-metadata.ts +9 -6
  35. package/src/adapters/definitions/native-cli-entry-claude.ts +3 -0
  36. package/src/adapters/definitions/native-cli-entry-codex.ts +5 -1
  37. package/src/adapters/definitions/native-cli-entry-jcode.ts +2 -0
  38. package/src/adapters/definitions/native-cli-entry-kimi.ts +12 -4
  39. package/src/adapters/definitions/native-cli-entry-local.ts +3 -0
  40. package/src/adapters/definitions/native-cli-entry-pi.ts +7 -1
  41. package/src/adapters/definitions/native-cli-entry-qwen.ts +3 -0
  42. package/src/adapters/definitions/native-cli-entry.ts +2 -0
  43. package/src/adapters/definitions/native-cli-retained.ts +15 -0
  44. package/src/adapters/definitions/native-factories.ts +17 -0
  45. package/src/adapters/definitions/native-policy-aux.ts +1 -1
  46. package/src/adapters/definitions/qwen.ts +1 -1
  47. package/src/adapters/definitions/reviewer-variants.ts +33 -0
  48. package/src/adapters/jcode.ts +20 -5
  49. package/src/adapters/kimi-review.ts +161 -0
  50. package/src/adapters/registry-types.ts +36 -4
  51. package/src/adapters/registry.ts +28 -7
  52. package/src/adapters/reviewer-mcp.ts +85 -0
  53. package/src/adapters/reviewer-print.ts +382 -0
  54. package/src/cli/agy-mcp.ts +370 -0
  55. package/src/cli/console-state.ts +6 -0
  56. package/src/cli/console.ts +2 -0
  57. package/src/cli/facts-hook.ts +15 -2
  58. package/src/cli/help.ts +2 -0
  59. package/src/cli/identity.ts +1 -1
  60. package/src/cli/init.ts +7 -0
  61. package/src/cli/launch.ts +3 -1
  62. package/src/cli/main.ts +20 -0
  63. package/src/cli/memory.ts +13 -12
  64. package/src/cli/output.ts +5 -0
  65. package/src/hub/board.ts +34 -6
  66. package/src/hub/bus.ts +17 -6
  67. package/src/hub/config-apply.ts +3 -2
  68. package/src/hub/control-client.ts +7 -1
  69. package/src/hub/daemon.ts +266 -46
  70. package/src/hub/envelope.ts +1 -1
  71. package/src/hub/host-executable.ts +1 -1
  72. package/src/hub/hub-tools.ts +5 -0
  73. package/src/hub/instances.ts +13 -3
  74. package/src/hub/memory-profile.ts +67 -11
  75. package/src/hub/native-hook-launch.ts +37 -0
  76. package/src/hub/peer-catalog.ts +2 -3
  77. package/src/hub/planning.ts +2 -0
  78. package/src/hub/release-admission.ts +64 -0
  79. package/src/hub/reviewer-pool-config.ts +11 -0
  80. package/src/hub/reviewer-pool.ts +76 -0
  81. package/src/hub/routing.ts +45 -3
  82. package/src/hub/sdlc-release.ts +113 -0
  83. package/src/hub/tasks.ts +261 -36
  84. package/src/ui/index.html +6 -1
  85. package/templates/AGENTS.block.md +1 -1
@@ -39,11 +39,27 @@ export function createLocal(ctx: CreateContext): PeerAdapter {
39
39
  }
40
40
 
41
41
  export function createClaudeReviewer(ctx: CreateContext): PeerAdapter {
42
+ if ("reviewerPrint" in ctx.daemon) {
43
+ const { ClaudePrintPeer } = require("../claude-print.ts") as typeof import("../claude-print.ts");
44
+ return new ClaudePrintPeer(ctx.instance, ctx.daemon.reviewerPrint);
45
+ }
42
46
  if (!("claudePrint" in ctx.daemon)) throw new Error("Claude reviewer requires its print transport options");
43
47
  const { ClaudePrintPeer } = require("../claude-print.ts") as typeof import("../claude-print.ts");
44
48
  return new ClaudePrintPeer(ctx.instance, ctx.daemon.claudePrint);
45
49
  }
46
50
 
51
+ export function createCodexReviewer(ctx: CreateContext): PeerAdapter {
52
+ if (!("reviewerPrint" in ctx.daemon)) throw new Error("Codex reviewer requires its fresh print transport options");
53
+ const config = ctx.kindConfig as { codex_bin?: string };
54
+ return createReviewer({ ...ctx, daemon: { reviewerPrint: { ...ctx.daemon.reviewerPrint, command: ctx.daemon.reviewerPrint.command ?? [config.codex_bin ?? "codex"] } } });
55
+ }
56
+
57
+ export function createReviewer(ctx: CreateContext): PeerAdapter {
58
+ if (!("reviewerPrint" in ctx.daemon)) throw new Error("Reviewer requires its fresh print transport options");
59
+ const { ReviewerPrintPeer } = require("../reviewer-print.ts") as typeof import("../reviewer-print.ts");
60
+ return new ReviewerPrintPeer(ctx.instance, ctx.daemon.reviewerPrint);
61
+ }
62
+
47
63
  /** Preserve existing transport option types and object identity; selection stays host-owned. */
48
64
  export const DAEMON_PORTS = {
49
65
  "codex-appserver": (options: Extract<DaemonPorts, { codex: unknown }>["codex"]) => ({ codex: options }),
@@ -51,5 +67,6 @@ export const DAEMON_PORTS = {
51
67
  "owned-acp": (options: Extract<DaemonPorts, { jcode: unknown }>["jcode"]) => ({ jcode: options }),
52
68
  "pi-rpc": (options: Extract<DaemonPorts, { pi: unknown }>["pi"]) => ({ pi: options }),
53
69
  "local-worker": (options: Extract<DaemonPorts, { local: unknown }>["local"]) => ({ local: options }),
70
+ "reviewer-print": (options: Extract<DaemonPorts, { reviewerPrint: unknown }>["reviewerPrint"]) => ({ reviewerPrint: options }),
54
71
  "claude-print": (options: Extract<DaemonPorts, { claudePrint: unknown }>["claudePrint"]) => ({ claudePrint: options }),
55
72
  } satisfies Record<DaemonEngine, unknown>;
@@ -15,7 +15,7 @@ export function codexCrashPlan(meta: Record<string, unknown>): NativeCrashPlan {
15
15
  }
16
16
 
17
17
  export function jcodeCrashPlan(_meta: Record<string, unknown>): NativeCrashPlan {
18
- return { how: "jcode: native 0.94.0 load does not prove the restored id; reopen the original conversation with its native client, no automatic fresh replacement" };
18
+ return { how: "jcode: native ACP load (verified on 0.94.0 and 0.98.1) does not prove the restored id; reopen the original conversation with its native client, no automatic fresh replacement" };
19
19
  }
20
20
 
21
21
  export function kimiCrashPlan(meta: Record<string, unknown>): NativeCrashPlan {
@@ -69,7 +69,7 @@ export const QWEN_DEFINITION: AdapterDefinition = {
69
69
  turnEnd: "adapter",
70
70
  budget: { account: "qwen" },
71
71
  capabilities: { watch: true },
72
- cli: { command: "qwen", startWords: () => ["qwen"] },
72
+ cli: { command: "qwen", launchConfigKeys: ["qwen_cmd"], startWords: () => ["qwen"] },
73
73
  display: QWEN_DISPLAY,
74
74
  hooks: { agentIdentity: init => { qwenNativeIdentity("qwen", true, init); }, recallPlatform: "qwen" },
75
75
  };
@@ -0,0 +1,33 @@
1
+ import type { AdapterDefinition } from "../registry-types.ts";
2
+ import { claudeReviewerBackend } from "../claude-print.ts";
3
+ import { codexReviewerBackend } from "../codex-review-isolation.ts";
4
+ import { kimiReviewerBackend } from "../kimi-review.ts";
5
+ import { createReviewer, createCodexReviewer } from "./native-factories.ts";
6
+ import { CODEX_DISPLAY, KIMI_DISPLAY } from "./native-display.ts";
7
+
8
+ export { claudeReviewerBackend };
9
+ /** No launch path is exposed by metadata-only placeholders. */
10
+ function unavailableLaunch(): never { throw new Error("Reviewer unavailable: verified runtime support is deferred to #533"); }
11
+
12
+ function reviewerVariant(kind: string, displayName: string, reviewer: NonNullable<AdapterDefinition["reviewer"]>, display: AdapterDefinition["display"]): AdapterDefinition {
13
+ return {
14
+ api: 1, kind, family: kind, displayName, transport: "hub-native", roles: ["reviewer"],
15
+ defaultOnRoster: false, trust: { onPrem: false, hubSandboxed: false },
16
+ start: { ...(reviewer.backend ? { create: createReviewer, daemonPort: "reviewer-print" as const } : {}), forms: ["headless"], defaultForm: "headless", ownsTerminal: () => false,
17
+ launch: unavailableLaunch,
18
+ conductorStartable: true, hubSwitches: [], ownedFlags: [] },
19
+ identity: { mcp: "env" },
20
+ permission: { modes: {}, readOnly: { kind: "hub-gate", allow: ["read", "search", "think"] }, unmanaged: "fresh read-only reviewer; an isolation receipt is required before startup" },
21
+ session: { idField: "sessionId", currentId: () => undefined, freshOnRestore: true, launchKind: "reviewer-print", crashPlan: () => ({ resume: {}, how: "starts a fresh isolated review session" }) },
22
+ turnEnd: "adapter", budget: { account: kind }, routing: { reviewOnly: true },
23
+ reviewer, cli: { command: kind, startWords: () => [] }, display,
24
+ };
25
+ }
26
+ const codexVariant = reviewerVariant("codex", "Codex reviewer", { backend: codexReviewerBackend, defaultPool: true, declaration: "explicit" }, CODEX_DISPLAY);
27
+ export const CODEX_REVIEWER_VARIANT: AdapterDefinition = { ...codexVariant, start: { ...codexVariant.start, create: createCodexReviewer } };
28
+ export const KIMI_REVIEWER_VARIANT = reviewerVariant("kimi", "Kimi reviewer", { backend: kimiReviewerBackend, defaultPool: true, declaration: "explicit" }, KIMI_DISPLAY);
29
+ /** Data-only declared placeholder for route explanations; no factory, backend or native argv. */
30
+ export const AGY_REVIEWER_VARIANT: AdapterDefinition = {
31
+ ...reviewerVariant("agy", "agy reviewer", { defaultPool: false, declaration: "automatic", unavailable: "agy is an implementer peer (#485)" }, { palette: { fg: "#6558a6", bg: "#f1effa" } }),
32
+ optional: true,
33
+ };
@@ -17,15 +17,29 @@ export function jcodeCommand(command: string[], kind: "serve" | "acp", socket: s
17
17
  return [...command, kind, "--socket", socket, "--cwd", cwd, "--no-update", "--no-selfdev", ...(model ? ["--model", model] : []), ...(profile ? ["--provider-profile", profile] : []),
18
18
  ...(kind === "serve" ? ["--temporary-server", "--owner-pid", String(process.pid), "--server-name", "agent-hub-jcode"] : ["--mcp-tools", "eager"])];
19
19
  }
20
+ /** Set on every owned jcode process: no Claude MCP import, and no native telemetry (jcode-telemetry-core is_enabled, #529). */
21
+ const JCODE_OWNED_ENV = { JCODE_DISABLE_CLAUDE_MCP: "1", JCODE_NO_TELEMETRY: "1" } as const;
22
+ /** The oldest native release accepted; its ACP, serve and MCP contract was verified live on 0.98.1 (docs/verification). */
23
+ export const JCODE_MIN_VERSION = "0.98.0";
24
+ /** The first native release assumed to break that contract: the next major. Lower it when a release is shown to break. */
25
+ export const JCODE_BREAKING_VERSION = "1.0.0";
26
+ const release = (version: string) => /^(\d+)\.(\d+)\.(\d+)$/.exec(version)?.slice(1).map(Number);
27
+ const compare = (a: number[], b: number[]) => a[0]! - b[0]! || a[1]! - b[1]! || a[2]! - b[2]!;
28
+ /** A plain release from JCODE_MIN_VERSION up to, not including, JCODE_BREAKING_VERSION; the minor is not pinned (#529). */
29
+ export function jcodeVersionSupported(version: unknown): boolean {
30
+ const v = typeof version === "string" ? release(version) : undefined;
31
+ return !!v && compare(v, release(JCODE_MIN_VERSION)!) >= 0 && compare(v, release(JCODE_BREAKING_VERSION)!) < 0;
32
+ }
33
+
20
34
  /** The wrapper contract preserves every configured prefix argument before the native probe. */
21
35
  export function verifiedJcodeCommand(command: string[], cwd: string): boolean {
22
36
  const [bin, ...prefix] = command;
23
37
  if (!bin) return false;
24
38
  try {
25
- const result = spawnSync(bin, [...prefix, "--quiet", "version", "--json"], { cwd, env: peerChildEnv("jcode"), encoding: "utf8", timeout: 10_000, maxBuffer: 64 * 1024 });
39
+ const result = spawnSync(bin, [...prefix, "--quiet", "version", "--json"], { cwd, env: { ...peerChildEnv("jcode"), ...JCODE_OWNED_ENV }, encoding: "utf8", timeout: 10_000, maxBuffer: 64 * 1024 });
26
40
  if (result.status !== 0) return false;
27
41
  const version = JSON.parse(result.stdout);
28
- return version.semver === "0.94.0";
42
+ return jcodeVersionSupported(version.semver);
29
43
  } catch { return false; }
30
44
  }
31
45
 
@@ -41,9 +55,10 @@ export class JcodePeer extends AcpPeer {
41
55
  constructor(private readonly jcode: JcodeOptions) {
42
56
  if (!isAbsolute(jcode.home)) throw new Error("jcode_home must be an absolute native data path");
43
57
  jcodeCommand(jcode.command, "acp", "preview", jcode.cwd);
44
- const root = mkdtempSync(join(tmpdir(), "ahub-jcode-")), socket = join(root, "s");
58
+ // The name must end in ".sock": jcode names its debug socket by replacing that suffix, so "s" binds one path twice.
59
+ const root = mkdtempSync(join(tmpdir(), "ahub-jcode-")), socket = join(root, "j.sock");
45
60
  const cmd = jcodeCommand(jcode.command, "acp", socket, jcode.cwd, jcode.launchModel, jcode.providerProfile);
46
- super("jcode", { ...jcode, cmd, env: { ...jcode.env, JCODE_HOME: jcode.home, JCODE_SOCKET: socket, JCODE_DISABLE_CLAUDE_MCP: "1" }, mcpServers: [], requireLoadSessionEcho: true, expectedAgent: { name: "jcode", version: "0.94.0" } });
61
+ super("jcode", { ...jcode, cmd, env: { ...jcode.env, JCODE_HOME: jcode.home, JCODE_SOCKET: socket, ...JCODE_OWNED_ENV }, mcpServers: [], requireLoadSessionEcho: true, expectedAgent: { name: "jcode", version: jcodeVersionSupported } });
47
62
  this.root = root; this.socket = socket;
48
63
  }
49
64
 
@@ -53,7 +68,7 @@ export class JcodePeer extends AcpPeer {
53
68
  if (existsSync(this.socket)) { this.preserveRoot = true; throw new Error("Jcode private socket is occupied; no daemon was adopted or removed"); }
54
69
  this.ending = false;
55
70
  const [bin, ...args] = jcodeCommand(this.jcode.command, "serve", this.socket, this.jcode.cwd, this.jcode.launchModel, this.jcode.providerProfile);
56
- const server = spawn(bin!, args, { cwd: this.jcode.cwd, detached: true, env: { ...peerChildEnv("jcode"), JCODE_HOME: this.jcode.home, JCODE_SOCKET: this.socket, JCODE_READY_FD: "3", JCODE_DISABLE_CLAUDE_MCP: "1" }, stdio: ["ignore", "ignore", "ignore", "pipe"] });
71
+ const server = spawn(bin!, args, { cwd: this.jcode.cwd, detached: true, env: { ...peerChildEnv("jcode"), JCODE_HOME: this.jcode.home, JCODE_SOCKET: this.socket, JCODE_READY_FD: "3", ...JCODE_OWNED_ENV }, stdio: ["ignore", "ignore", "ignore", "pipe"] });
57
72
  this.server = server; trackGroup(server);
58
73
  try {
59
74
  await new Promise<void>((resolve, reject) => {
@@ -0,0 +1,161 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
7
+ import { realPath } from "../hub/project.ts";
8
+ import type { ReviewerBackend, ReviewerPrepareContext } from "./reviewer-print.ts";
9
+
10
+ const VERSION = "2.2.0", MANAGED = "managed:kimi-code";
11
+ const object = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);
12
+ const hash = (text: string) => createHash("sha256").update(text).digest("hex");
13
+ /** Native discovery can walk above cwd; check lexical aliases as well as the physical temporary root. */
14
+ function assertCleanCwdAncestors(...candidates: readonly string[]): void {
15
+ const seen = new Set<string>();
16
+ for (const candidate of candidates) {
17
+ let ancestor = resolve(candidate);
18
+ for (;;) {
19
+ if (!seen.has(ancestor)) {
20
+ seen.add(ancestor);
21
+ for (const marker of [".git", ".kimi-code", "AGENTS.md"]) {
22
+ if (lstatSync(join(ancestor, marker), { throwIfNoEntry: false }) !== undefined) {
23
+ throw new Error("private cwd ancestor contains ambient configuration");
24
+ }
25
+ }
26
+ }
27
+ const parent = dirname(ancestor); if (parent === ancestor) break; ancestor = parent;
28
+ }
29
+ }
30
+ }
31
+ const read = (file: string, limit = 262_144) => {
32
+ const stat = lstatSync(file); if (!stat.isFile() || stat.size > limit) throw new Error("unsupported reviewer file");
33
+ return readFileSync(file, "utf8");
34
+ };
35
+ const scalar = (value: unknown, depth = 0): string => {
36
+ if (depth > 4) throw new Error("managed model setting too deeply nested");
37
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
38
+ if (Array.isArray(value) && value.every(v => typeof v === "string")) return JSON.stringify(value);
39
+ if (object(value) && Object.keys(value).length <= 32) return `{ ${Object.entries(value).map(([key, item]) => `${JSON.stringify(key)} = ${scalar(item, depth + 1)}`).join(", ")} }`;
40
+ throw new Error("unsupported managed model setting");
41
+ };
42
+ function managedConfig(raw: string): { config: string; lock: string } {
43
+ const config = Bun.TOML.parse(raw) as Record<string, unknown>;
44
+ if (!object(config.providers) || !object(config.models) || typeof config.default_model !== "string") throw new Error("managed Kimi configuration required");
45
+ const provider = config.providers[MANAGED], model = config.models[config.default_model];
46
+ if (!object(provider) || provider.type !== "kimi" || provider.api_key && provider.api_key !== "" || !object(provider.oauth) || provider.oauth.storage !== "file" || !object(model) || model.provider !== MANAGED) throw new Error("managed file-backed Kimi OAuth required");
47
+ const oauth = provider.oauth;
48
+ const key = oauth.key;
49
+ const lock = !key || key === "kimi-code" || key === "oauth/kimi-code" ? "kimi-code" : typeof key === "string" && key.startsWith("oauth/") ? key.slice(6) : key;
50
+ if (typeof lock !== "string" || !lock || basename(lock) !== lock || lock.startsWith(".")) throw new Error("unsupported OAuth slot");
51
+ if (typeof provider.base_url !== "string") throw new Error("managed Kimi endpoint required");
52
+ const endpoint = new URL(provider.base_url);
53
+ if (endpoint.protocol !== "https:" || !["api.kimi.ai", "api.kimi.com"].includes(endpoint.hostname) || endpoint.username || endpoint.password) throw new Error("unsupported managed Kimi endpoint");
54
+ if (oauth.oauth_host !== undefined) {
55
+ const auth = new URL(String(oauth.oauth_host));
56
+ if (auth.protocol !== "https:" || !["auth.kimi.ai", "auth.kimi.com"].includes(auth.hostname) || auth.username || auth.password) throw new Error("unsupported OAuth endpoint");
57
+ }
58
+ const lines = ["telemetry = false", "hooks = []", `default_model = ${scalar(config.default_model)}`,
59
+ ...(config.thinking === undefined ? [] : [`thinking = ${scalar(config.thinking)}`]),
60
+ `[providers.${scalar(MANAGED)}]`, 'type = "kimi"', `base_url = ${scalar(provider.base_url)}`, `[providers.${scalar(MANAGED)}.oauth]`];
61
+ for (const name of ["storage", "key", "oauth_host"]) if (oauth[name] !== undefined) lines.push(`${name} = ${scalar(oauth[name])}`);
62
+ lines.push(`[models.${scalar(config.default_model)}]`);
63
+ const modelFields = ["provider", "model", "protocol", "max_context_size", "max_input_size", "capabilities", "display_name", "support_efforts", "default_effort", "beta_api", "adaptive_thinking", "overrides"];
64
+ if (Object.keys(model).some(name => !modelFields.includes(name))) throw new Error("unsupported managed model setting");
65
+ if (model.overrides !== undefined && (!object(model.overrides) || Object.keys(model.overrides).some(name => !modelFields.includes(name) || ["provider", "overrides"].includes(name)))) throw new Error("unsupported managed model override");
66
+ for (const name of modelFields) if (model[name] !== undefined) lines.push(`${name} = ${scalar(model[name])}`);
67
+ return { config: lines.join("\n") + "\n", lock };
68
+ }
69
+ function mcpConfig(context: ReviewerPrepareContext): string {
70
+ const config: unknown = JSON.parse(context.mcpConfig);
71
+ if (!object(config) || !object(config.mcpServers)) throw new Error("review MCP unavailable");
72
+ if (context.task === undefined && Object.keys(config.mcpServers).length === 0) return context.mcpConfig;
73
+ if (Object.keys(config.mcpServers).join() !== "agent-hub") throw new Error("ambient MCP refused");
74
+ const server = config.mcpServers["agent-hub"];
75
+ if (!object(server) || Object.keys(server).some(k => !["transport", "url", "headers"].includes(k)) || server.transport !== "http" || typeof server.url !== "string" || !object(server.headers) || Object.keys(server.headers).join() !== "Authorization" || typeof server.headers.Authorization !== "string" || !server.headers.Authorization.startsWith("Bearer ")) throw new Error("task-bound HTTP MCP required");
76
+ const url = new URL(server.url);
77
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || url.pathname !== "/mcp" || url.username || url.password || url.search || url.hash) throw new Error("local review MCP required");
78
+ return context.mcpConfig;
79
+ }
80
+ async function version(command: readonly string[], cwd: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<string> {
81
+ return await new Promise((accept, reject) => {
82
+ const child = spawn(command[0]!, [...command.slice(1), "--version"], { cwd, env, detached: true, stdio: ["ignore", "pipe", "ignore"] });
83
+ let text = "", done = false;
84
+ const finish = async (code: number | null) => {
85
+ if (done) return; done = true; clearTimeout(timer); signal.removeEventListener("abort", abort);
86
+ try { await stopOwnedProcess(child, { group: true }); } catch { reject(new Error("version probe cleanup failed")); return; }
87
+ if (code !== 0 || signal.aborted) reject(new Error("version probe failed")); else accept(text.trim());
88
+ };
89
+ const abort = () => { void finish(null); };
90
+ const timer = setTimeout(abort, 5_000);
91
+ signal.addEventListener("abort", abort, { once: true });
92
+ child.once("spawn", () => trackGroup(child));
93
+ child.stdout.on("data", (bytes: Buffer) => { text += bytes.toString(); if (text.length > 4096) abort(); });
94
+ child.once("error", () => { void finish(null); }); child.once("close", code => { void finish(code); });
95
+ if (signal.aborted) abort();
96
+ });
97
+ }
98
+ /** Do not SIGKILL a live native refresh holder. A bounded incomplete stop retains the peer handle. */
99
+ export async function stopKimiReview(child: ChildProcess, locks: readonly string[], graceMs = 10_000): Promise<void> {
100
+ const locked = () => locks.some(file => existsSync(file));
101
+ if (child.exitCode === null && child.signalCode === null && locked()) {
102
+ child.kill("SIGTERM");
103
+ const end = Date.now() + graceMs;
104
+ while (child.exitCode === null && child.signalCode === null && locked() && Date.now() < end) await Bun.sleep(50);
105
+ if (child.exitCode === null && child.signalCode === null && locked()) throw new Error("Kimi OAuth refresh still locked; graceful stop incomplete");
106
+ }
107
+ await stopOwnedProcess(child, { group: true });
108
+ }
109
+
110
+ export const kimiReviewerBackend: ReviewerBackend = {
111
+ id: "kimi", taskMcp: true, nativePolicy: true,
112
+ async prepare(context) {
113
+ let own: string | undefined;
114
+ try {
115
+ if (context.signal.aborted) throw new Error("cancelled");
116
+ const sourcePath = process.env.KIMI_CODE_HOME ?? join(process.env.HOME ?? homedir(), ".kimi-code"), source = realPath(sourcePath);
117
+ const sourceConfig = join(source, "config.toml"), original = read(sourceConfig), selected = managedConfig(original);
118
+ const sources = ["credentials", "oauth", "device_id"].map(name => realPath(join(source, name)));
119
+ if (!sources.every((file, index) => index < 2 ? lstatSync(file).isDirectory() : lstatSync(file).isFile())) throw new Error("native OAuth directories and device identity required");
120
+ const mcp = mcpConfig(context);
121
+ if (Buffer.byteLength(context.input ?? "") > 1_048_576) throw new Error("review facts exceed limit");
122
+ const lexicalRoot = mkdtempSync(join(process.env.TMPDIR ?? tmpdir(), "ahub-kimi-review-"));
123
+ own = realPath(lexicalRoot);
124
+ assertCleanCwdAncestors(join(lexicalRoot, "workspace"), join(own, "workspace"));
125
+ const root = own, home = join(root, "home"), tool = join(root, "tool"), cwd = join(root, "workspace"), skills = join(root, "empty-skills"), temporary = join(root, "tmp");
126
+ for (const dir of [home, tool, cwd, skills, temporary]) mkdirSync(dir, { mode: 0o700 });
127
+ ["credentials", "oauth", "device_id"].forEach((name, index) => symlinkSync(sources[index]!, join(tool, name), index < 2 ? "dir" : "file"));
128
+ const agentFile = join(cwd, "reviewer.md"), configFile = join(tool, "config.toml"), mcpFile = join(tool, "mcp.json");
129
+ writeFileSync(configFile, selected.config, { mode: 0o600 }); writeFileSync(mcpFile, mcp, { mode: 0o600 });
130
+ writeFileSync(agentFile, `---\nname: hub-reviewer\ndescription: Fresh agent-hub reviewer\ntools:\n - mcp__agent-hub__hub_review\n - mcp__agent-hub__hub_task_show\nsubagents: []\n---\n\n${context.systemPrompt}\n\nUse only the supplied public facts and hub_task_show. Never claim to have read evidence that was not supplied.\n\n${context.input ?? "No review is active; startup self-check only."}\n`, { mode: 0o600 });
131
+ const env: NodeJS.ProcessEnv = { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: home, KIMI_CODE_HOME: tool, TMPDIR: `${temporary}/`, XDG_CONFIG_HOME: join(home, ".config"), LANG: process.env.LANG ?? "en_US.UTF-8", TERM: "dumb", ...(process.env.USER ? { USER: process.env.USER } : {}) };
132
+ const native = context.command ?? [Bun.which("kimi", { PATH: process.env.PATH }) ?? "kimi"];
133
+ const executable = realPath(Bun.which(native[0]!, { PATH: process.env.PATH }) ?? resolve(native[0]!));
134
+ const command = [executable, ...native.slice(1)];
135
+ const executableStat = lstatSync(executable);
136
+ if (await version(command, cwd, env, context.signal) !== VERSION) throw new Error("unsupported Kimi version; receipt covers 2.2.0");
137
+ const fingerprints = [configFile, mcpFile, agentFile].map(file => hash(read(file, 2_097_152)));
138
+ let cleaned = false;
139
+ const cleanup = () => { if (cleaned) return; rmSync(root, { recursive: true, force: true }); cleaned = true; };
140
+ return { command, args: ["-p", "Review the supplied task facts and record the evidence-based verdict with hub_review.", "--output-format", "stream-json", "--agent-file", agentFile, "--skills-dir", skills],
141
+ cwd, env, envMode: "replace", cleanup, watchCurrent: true,
142
+ stop: child => stopKimiReview(child, [join(sources[1]!, "kimi-code.lock"), join(sources[1]!, `${selected.lock}.lock`)]),
143
+ current: () => {
144
+ try {
145
+ assertCleanCwdAncestors(join(lexicalRoot,"workspace"),cwd);
146
+ const now = lstatSync(executable);
147
+ return !cleaned && !context.signal.aborted && realPath(sourcePath) === source && now.dev === executableStat.dev && now.ino === executableStat.ino && now.size === executableStat.size && now.mtimeMs === executableStat.mtimeMs && now.ctimeMs === executableStat.ctimeMs && hash(read(sourceConfig)) === hash(original)
148
+ && [configFile, mcpFile, agentFile].every((file, index) => hash(read(file, 2_097_152)) === fingerprints[index])
149
+ && ["credentials", "oauth", "device_id"].every((name, index) => realPath(join(source, name)) === sources[index] && realPath(join(tool, name)) === sources[index]);
150
+ } catch { return false; }
151
+ },
152
+ isolation: { backend: "kimi", version: VERSION, binary: executable, profile: "private-config-exact-two-tools-shared-native-oauth", readOnly: true, strictMcp: true, ephemeral: true,
153
+ enforcement: "native-policy", freshSession: true, hubToolsOnly: true,
154
+ evidence: ["#533 person ruling2026-10-19: native execution-time exact tool allowlist, no kernel sandbox", "private config/cwd/empty skills; shared OAuth directory symlinks and native lock/atomic rename", "task-bound in-process hub MCP; no full control token", "residual: interruption mid-refresh may require native re-login"] },
155
+ };
156
+ } catch (error) {
157
+ if (own) rmSync(own, { recursive: true, force: true });
158
+ return { unavailable: `Kimi reviewer unavailable: ${(error as Error).message}` };
159
+ }
160
+ },
161
+ };
@@ -1,3 +1,4 @@
1
+ import type { ReviewerBackend, ReviewerPrintOptions } from "./reviewer-print.ts";
1
2
  import type { SettingDef } from "../hub/settings.ts";
2
3
  import type { Addon } from "../hub/tooling.ts";
3
4
  import type { RecoveryContinuityBookmark } from "../hub/recovery-continuity.ts";
@@ -16,7 +17,7 @@ import type { PiOptions } from "./pi.ts";
16
17
  import type { ClaudePrintOptions } from "./claude-print.ts";
17
18
 
18
19
  export type StartForm = "tui" | "headless";
19
- export type DaemonEngine = "codex-appserver" | "acp" | "owned-acp" | "pi-rpc" | "local-worker" | "claude-print";
20
+ export type DaemonEngine = "codex-appserver" | "acp" | "owned-acp" | "pi-rpc" | "local-worker" | "claude-print" | "reviewer-print";
20
21
 
21
22
  /** Existing buildLaunch inputs plus the startup-bound instance and project identity. */
22
23
  export interface LaunchContext {
@@ -55,7 +56,8 @@ export type DaemonPorts =
55
56
  | { jcode: JcodeOptions }
56
57
  | { pi: PiOptions }
57
58
  | { local: LocalOptions }
58
- | { claudePrint: ClaudePrintOptions };
59
+ | { claudePrint: ClaudePrintOptions }
60
+ | { reviewerPrint: ReviewerPrintOptions };
59
61
 
60
62
  export interface CreateContext<Ports extends DaemonPorts = DaemonPorts> {
61
63
  instance: string;
@@ -115,12 +117,36 @@ export interface CapabilityManifest {
115
117
  }[];
116
118
  }
117
119
 
120
+ export interface NativeFactsInput {
121
+ event: "SessionStart" | "PreInvocation" | "PostInvocation" | "PreToolUse" | "PostToolUse" | "Stop";
122
+ phase: "session" | "start" | "pre" | "post" | "stop";
123
+ sessionId: string;
124
+ transcriptPath: string;
125
+ workspacePaths: string[];
126
+ tool: string;
127
+ input: Record<string, unknown>;
128
+ toolUseId?: string;
129
+ invocation?: number;
130
+ fullyIdle?: boolean;
131
+ terminationReason?: string;
132
+ }
133
+ export interface NativeFactsTranscript {
134
+ completion?: { id: string; index: number };
135
+ factIds: string[];
136
+ }
137
+ /** Fixed native codec and bounded readback, never supplied by project configuration. */
138
+ export interface NativeFactsPort {
139
+ decode(event: string, input: unknown): NativeFactsInput | undefined;
140
+ encode(event: string, text?: string): Record<string, unknown> | undefined;
141
+ read(session: string, transcript: string): NativeFactsTranscript;
142
+ }
143
+
118
144
  /** Only the frozen native seams are extensible; a definition cannot add an arbitrary daemon callback. */
119
145
  export interface NativeHooks {
120
146
  agentIdentity(init: unknown): void;
121
147
  onAttach(instance: string): void;
122
148
  onDetach(instance: string): void;
123
- factsHook: (msg: unknown) => unknown;
149
+ factsHook: NativeFactsPort;
124
150
  conversationRevert(turnId: string): Promise<void>;
125
151
  replaceSession(req: StartRequest): Promise<unknown>;
126
152
  pluginInventory(ctx: { home: string; cwd?: string; env: Readonly<Record<string, string | undefined>> }): Addon[];
@@ -227,6 +253,8 @@ export interface AdapterDefinition {
227
253
  routing?: {
228
254
  /** Takes reviews only: never chosen or nominated as a task owner, for any class. */
229
255
  reviewOnly?: true;
256
+ /** Fixed role ceiling; project roles cannot turn an unmanaged implementer into a planner/reviewer. */
257
+ allowedRoles?: readonly string[];
230
258
  localAllowedRefusal?: string;
231
259
  longContext?: "backend-limit" | "skip-owner";
232
260
  preferredTier?: boolean;
@@ -251,6 +279,8 @@ export interface AdapterDefinition {
251
279
  };
252
280
  cli: {
253
281
  command: string;
282
+ /** Configuration leaves that select this peer's next native launch command or provider profile. */
283
+ launchConfigKeys?: readonly string[];
254
284
  /** Restrictions only; core identity remains authoritative. */
255
285
  authority?: "person" | "conductor";
256
286
  personOnlyFlags?: readonly string[];
@@ -264,7 +294,9 @@ export interface AdapterDefinition {
264
294
  hooks?: Partial<NativeHooks>;
265
295
  /** #223: what a declared headless reviewer-only instance of this kind runs as. Fixed builtins only. */
266
296
  reviewerVariant?: AdapterDefinition;
297
+ /** Fixed host backend; runtime isolation proof is required before bus admission. */
298
+ reviewer?: { backend?: ReviewerBackend; defaultPool: boolean; unavailable?: string; declaration: "automatic" | "explicit" };
267
299
  }
268
300
 
269
301
  /** Explicit project declarations, passed by the bound host. No process-global instance registration. */
270
- export type KindMap = Readonly<Record<string, { readonly kind: string }>>;
302
+ export type KindMap = Readonly<Record<string, { readonly kind: string; readonly roles?: readonly string[]; readonly start_mode?: "tui" | "headless"; readonly review_mode?: "fresh" }>>;
@@ -1,3 +1,5 @@
1
+ import { AGY_DEFINITION } from "./agy/definition.ts";
2
+ import { AGY_REVIEWER_VARIANT } from "./definitions/reviewer-variants.ts";
1
3
  import { builtinCliEntries } from "./definitions/native-cli-entry.ts";
2
4
  import { builtinPreview } from "./definitions/native-cli-preview.ts";
3
5
  import { DAEMON_PORTS } from "./definitions/native-factories.ts";
@@ -12,7 +14,7 @@ export const ADAPTER_API = 1;
12
14
 
13
15
  /** Ordered compatibility projections stay typed independently of definition implementation inference. */
14
16
  export const KNOWN_PEERS = ["claude", "codex", "kimi", "pi", "local"] as const;
15
- export const OPTIONAL_PEERS = ["qwen", "jcode"] as const;
17
+ export const OPTIONAL_PEERS = ["qwen", "jcode", "agy"] as const;
16
18
  export const SUPPORTED_PEERS = [...KNOWN_PEERS, ...OPTIONAL_PEERS] as const;
17
19
  export const START_MODE_PEERS = ["pi", "codex", "kimi"] as const;
18
20
  export const OFFICE_PEER_ORDER: readonly string[] = KNOWN_PEERS;
@@ -25,7 +27,7 @@ function immutable<T>(value: T, seen = new WeakSet<object>()): T {
25
27
  return Object.freeze(value);
26
28
  }
27
29
 
28
- const fixedBuiltinDefinitions = new Set<AdapterDefinition>([...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, QWEN_DEFINITION]);
30
+ const fixedBuiltinDefinitions = new Set<AdapterDefinition>([...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, QWEN_DEFINITION, AGY_DEFINITION, AGY_REVIEWER_VARIANT]);
29
31
  for (const definition of [...fixedBuiltinDefinitions]) if (definition.reviewerVariant) fixedBuiltinDefinitions.add(definition.reviewerVariant);
30
32
 
31
33
  /** Keep the native catalog's order, independently of the legacy default-role object order. */
@@ -33,9 +35,12 @@ export const ADAPTERS: readonly AdapterDefinition[] = immutable([
33
35
  ...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind !== "jcode"),
34
36
  QWEN_DEFINITION,
35
37
  ...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind === "jcode"),
38
+ AGY_DEFINITION,
36
39
  ]);
40
+ // Unavailable declarations are metadata, not supported native peers or launchers.
41
+ const declarationOnlyDefinitions: readonly AdapterDefinition[] = immutable([]);
37
42
  const definitions = new Map<string, AdapterDefinition>();
38
- for (const definition of ADAPTERS) {
43
+ for (const definition of [...ADAPTERS, ...declarationOnlyDefinitions]) {
39
44
  assertDaemonEngine(definition);
40
45
  assertNativeCapabilityScope(definition);
41
46
  if (definition.reviewerVariant) { assertDaemonEngine(definition.reviewerVariant); assertNativeCapabilityScope(definition.reviewerVariant); }
@@ -43,6 +48,10 @@ for (const definition of ADAPTERS) {
43
48
  definitions.set(definition.kind, definition);
44
49
  }
45
50
 
51
+ /** Default kind slots derive from fixed reviewer policy, not a second vendor list in the host. */
52
+ export const REVIEW_POOL_KINDS: readonly string[] = Object.freeze([...definitions.values()].filter(definition => !!(definition.reviewerVariant ?? definition).reviewer).map(definition => definition.kind));
53
+ export const DEFAULT_REVIEW_POOL: readonly string[] = Object.freeze(ADAPTERS.filter(definition => (definition.reviewerVariant ?? definition).reviewer?.defaultPool === true).map(definition => definition.kind));
54
+
46
55
  /** Native quota handoff platform names, distinct from per-delivery recall injection policy. */
47
56
  export const QUOTA_HANDOFF_PLATFORMS: Readonly<Record<string, string>> = Object.freeze(Object.fromEntries(
48
57
  ADAPTERS.flatMap(adapter => adapter.hooks?.recallPlatform ? [[adapter.kind, adapter.hooks.recallPlatform]] : []),
@@ -77,14 +86,18 @@ export function definitionOf(id: string, instances?: KindMap): AdapterDefinition
77
86
  const kind = kindOf(id, instances);
78
87
  const definition = kind === undefined ? undefined : definitions.get(kind);
79
88
  // #223: only a declared extra (never the builtin id) with start_mode "headless" runs as the kind's reviewer variant.
80
- return definition?.reviewerVariant && kind !== id && ownField(ownField(instances, id), "start_mode") === "headless" ? definition.reviewerVariant : definition;
89
+ const declaration = ownField(instances, id), roles = ownField(declaration, "roles");
90
+ const reviewerOnly = Array.isArray(roles) && !types.isProxy(roles) && roles.length === 1 && Object.getOwnPropertyDescriptor(roles, "0")?.value === "reviewer";
91
+ const variant = definition?.reviewerVariant;
92
+ const fresh = variant?.reviewer?.declaration === "automatic" || ownField(declaration, "review_mode") === "fresh";
93
+ return variant && kind !== id && reviewerOnly && ownField(declaration, "start_mode") === "headless" && fresh ? variant : definition;
81
94
  }
82
95
  export function familyOf(id: string, instances?: KindMap): string | undefined {
83
96
  return definitionOf(id, instances)?.family;
84
97
  }
85
98
 
86
99
  /** Compatibility projection order is observable in default role maps and planner selection. */
87
- const ROLE_ORDER = ["claude", "codex", "kimi", "local", "pi", "qwen", "jcode"] as const;
100
+ const ROLE_ORDER = ["claude", "codex", "kimi", "local", "pi", "qwen", "jcode", "agy"] as const;
88
101
  export const PLANNER_PEERS = ["claude", "codex", "kimi", "qwen", "jcode"] as const;
89
102
 
90
103
  /** Callers own their role arrays, independent of immutable native policy. */
@@ -125,6 +138,8 @@ export function assertNativeCapabilityScope(definition: AdapterDefinition): void
125
138
  ["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
126
139
  ["routing", definition.routing !== undefined],
127
140
  ["reviewerVariant", definition.reviewerVariant !== undefined],
141
+ ["reviewer", definition.reviewer !== undefined],
142
+ ["hooks.factsHook", declared(definition.hooks, "factsHook")],
128
143
  ];
129
144
  const field = restricted.find(([, present]) => present)?.[0];
130
145
  if (field) throw new Error(`${field} is restricted to fixed builtin adapter definitions`);
@@ -133,8 +148,14 @@ export function assertNativeCapabilityScope(definition: AdapterDefinition): void
133
148
  /** Ordered catalog projections consumed by generic core hosts. */
134
149
  export const STARTABLE_PEERS = SUPPORTED_PEERS;
135
150
  export type SupportedPeer = typeof SUPPORTED_PEERS[number];
136
- export function isSupportedPeer(peer: unknown): peer is SupportedPeer { return typeof peer === "string" && definitionOf(peer) !== undefined; }
137
- export function isStartablePeer(peer: unknown): peer is SupportedPeer { return typeof peer === "string" && definitionOf(peer)?.start.conductorStartable === true; }
151
+ export function isSupportedPeer(peer: unknown, instances?: KindMap): peer is string {
152
+ if (typeof peer !== "string") return false;
153
+ if (ADAPTERS.some(definition => definition.kind === peer)) return true;
154
+ const kind = declaredKind(peer, instances);
155
+ const runtime = ADAPTERS.find(definition => definition.kind === kind), resolved = definitionOf(peer, instances);
156
+ return runtime !== undefined && resolved !== undefined && (resolved === runtime || resolved.reviewer?.unavailable === undefined);
157
+ }
158
+ export function isStartablePeer(peer: unknown): peer is SupportedPeer { return isSupportedPeer(peer) && definitionOf(peer)?.start.conductorStartable === true; }
138
159
 
139
160
  /** Body-free browser metadata, derived from the same native participation and managed-tool contracts. */
140
161
  export const ADAPTER_DISPLAY = Object.fromEntries(ADAPTERS.map(adapter => [adapter.kind, {
@@ -0,0 +1,85 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import type { HubTool } from "../hub/hub-tools.ts";
3
+ import { sanitize } from "../hub/envelope.ts";
4
+
5
+ export type ReviewerToolName = "hub_review" | "hub_task_show";
6
+ export interface ReviewerMcpSession { config: string; close(): void }
7
+ export interface ReviewerMcpOptions {
8
+ task: number;
9
+ current(): boolean;
10
+ tools: readonly HubTool[];
11
+ /** Fixed host port; the host still applies Tasks' role, privacy, head and running-child fences. */
12
+ call(name: ReviewerToolName, args: Record<string, unknown>): Promise<string>;
13
+ }
14
+ const NAMES: readonly ReviewerToolName[] = ["hub_review", "hub_task_show"];
15
+ const BODY_CAP = 256 * 1024, RESULT_CAP = 4 * 1024 * 1024;
16
+ const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
17
+
18
+ /** One revocable review capability. This server never reads a daemon control token or accepts a peer/role. */
19
+ export function createReviewerMcpSession(options: ReviewerMcpOptions): ReviewerMcpSession {
20
+ if (!Number.isSafeInteger(options.task) || options.task < 1) throw new Error("reviewer MCP needs a task");
21
+ const tools = NAMES.map(name => {
22
+ const tool = options.tools.find(candidate => candidate.name === name);
23
+ if (!tool) throw new Error(`reviewer MCP tool missing: ${name}`);
24
+ return structuredClone(tool);
25
+ });
26
+ const authority = Buffer.from(`Bearer ${randomBytes(32).toString("hex")}`);
27
+ let closed = false, calls = 0;
28
+ const current = () => { try { return !closed && options.current(); } catch { return false; } };
29
+ const server = Bun.serve({ hostname: "127.0.0.1", port: 0, maxRequestBodySize: BODY_CAP,
30
+ async fetch(request) {
31
+ if (request.headers.has("origin")) return new Response("forbidden", { status: 403 });
32
+ const supplied = Buffer.from(request.headers.get("authorization") ?? "");
33
+ if (closed || supplied.length !== authority.length || !timingSafeEqual(supplied, authority)) return new Response("unauthorized", { status: 401 });
34
+ if (new URL(request.url).pathname !== "/mcp") return new Response("not found", { status: 404 });
35
+ if (request.method !== "POST") return new Response("method not allowed", { status: 405, headers: { Allow: "POST" } });
36
+ if (Number(request.headers.get("content-length")) > BODY_CAP) return new Response("body too large", { status: 413 });
37
+ let body: unknown;
38
+ try {
39
+ const reader = request.body?.getReader();
40
+ let size = 0; const parts: Uint8Array[] = [];
41
+ if (reader) for (;;) {
42
+ const part = await reader.read();
43
+ if (part.done) break;
44
+ size += part.value.byteLength;
45
+ if (size > BODY_CAP) { await reader.cancel(); return new Response("body too large", { status: 413 }); }
46
+ parts.push(part.value);
47
+ }
48
+ body = JSON.parse(Buffer.concat(parts).toString("utf8"));
49
+ } catch { return new Response("invalid request", { status: 400 }); }
50
+ if (!record(body) || body.jsonrpc !== "2.0" || typeof body.method !== "string") return new Response("invalid request", { status: 400 });
51
+ const id = body.id;
52
+ const reply = (result: unknown) => Response.json({ jsonrpc: "2.0", id, result });
53
+ const error = (message: string, code = -32602) => Response.json({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
54
+ if (!current()) return error("review authority ended", -32001);
55
+ if (body.method === "notifications/initialized" && id === undefined) return new Response(null, { status: 202 });
56
+ if (typeof id !== "string" && typeof id !== "number") return error("request id required");
57
+ if (body.method === "initialize") return reply({ protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "agent-hub-review", version: "1" } });
58
+ if (body.method === "tools/list") return reply({ tools });
59
+ if (body.method !== "tools/call") return error("method unavailable", -32601);
60
+ if (!record(body.params)) return error("tool parameters required");
61
+ const name = body.params.name;
62
+ if (name !== "hub_review" && name !== "hub_task_show") return error("tool unavailable");
63
+ let args = body.params.arguments;
64
+ if (typeof args === "string") { try { args = JSON.parse(args); } catch { return error("invalid tool arguments"); } }
65
+ if (!record(args) || args.id !== options.task || !Number.isSafeInteger(args.id)) return error("tool is bound to its review task");
66
+ if (++calls > 64) return error("review tool call limit", -32002);
67
+ if (!current()) return error("review authority ended", -32001);
68
+ try {
69
+ const result = await options.call(name, args);
70
+ // Tasks.review rechecks its captured authority through its final write. A successful verdict
71
+ // ends that in_review binding itself; acknowledge its committed result, never fresh reads.
72
+ if (name !== "hub_review" && !current()) return error("review authority ended", -32001);
73
+ if (Buffer.byteLength(result) > RESULT_CAP) return error("tool result exceeds review limit");
74
+ return reply({ content: [{ type: "text", text: result }] });
75
+ } catch (failure) {
76
+ // The same host tool boundary supplies refusals to the released MCP transport.
77
+ const message = failure instanceof Error ? sanitize(failure.message).slice(0, 4000) : "review tool refused";
78
+ return reply({ isError: true, content: [{ type: "text", text: message }] });
79
+ }
80
+ },
81
+ });
82
+ return { config: JSON.stringify({ mcpServers: { "agent-hub": { transport: "http", url: `http://127.0.0.1:${server.port}/mcp`, headers: { Authorization: authority.toString() } } } }),
83
+ close() { if (closed) return; closed = true; authority.fill(0); void server.stop(true); },
84
+ };
85
+ }