@staix/agent-hub 0.12.45 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +83 -116
- package/docs/agent-notes/README.md +6 -0
- package/docs/agent-notes/adapters.md +10 -7
- package/docs/agent-notes/budget.md +6 -1
- package/docs/agent-notes/bus.md +6 -4
- package/docs/agent-notes/daemon.md +6 -1
- package/docs/agent-notes/local-worker.md +2 -1
- package/docs/agent-notes/tasks.md +10 -0
- package/docs/agent-notes/tests.md +4 -2
- package/docs/dogfood.md +92 -0
- package/docs/events.md +14 -1
- package/docs/operations.md +250 -19
- package/docs/profiles.md +2 -2
- package/docs/quickstart.md +8 -2
- package/docs/sdlc.md +36 -6
- package/docs/security.md +20 -11
- package/docs/smoke.md +9 -4
- package/docs/specs/2026-09-19-agent-hub-design.md +759 -8
- package/docs/verification/2026-10-19-reviewer-isolation.md +2 -0
- package/docs/verification/2026-10-22-code-facts.md +78 -0
- package/docs/verification/2026-10-22-codex-reviewer-isolation.md +47 -0
- package/docs/verification/2026-10-22-kimi-qwen-compat.md +79 -0
- package/docs/verification/2026-10-23-0.13.0.md +89 -0
- package/docs/verification/dogfood/2026-10-22.md +3109 -0
- package/docs/verified.json +14 -14
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +613 -329
- package/src/adapters/agy/entry.ts +5 -4
- package/src/adapters/claude-channel.ts +20 -9
- package/src/adapters/codex-appserver.ts +18 -2
- package/src/adapters/codex-review-isolation.ts +12 -5
- package/src/adapters/compat.ts +140 -0
- package/src/adapters/definitions/builtin-metadata.ts +2 -1
- package/src/adapters/definitions/native-cli-doctor.ts +12 -5
- package/src/adapters/definitions/native-cli-entry-jcode.ts +9 -6
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +9 -2
- package/src/adapters/definitions/native-cli-preview-common.ts +2 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +8 -2
- package/src/adapters/definitions/native-cli-preview-qwen.ts +2 -1
- package/src/adapters/definitions/qwen-native.ts +7 -3
- package/src/adapters/definitions/qwen.ts +2 -1
- package/src/adapters/jcode.ts +72 -31
- package/src/adapters/kimi-review.ts +6 -3
- package/src/adapters/registry-types.ts +3 -1
- package/src/adapters/registry.ts +1 -0
- package/src/cli/agents.ts +40 -0
- package/src/cli/console-review.ts +54 -0
- package/src/cli/console-state.ts +124 -29
- package/src/cli/console-symbols.ts +74 -0
- package/src/cli/console-usage.ts +89 -0
- package/src/cli/console.ts +26 -1
- package/src/cli/help.ts +16 -6
- package/src/cli/identity.ts +29 -6
- package/src/cli/init.ts +109 -11
- package/src/cli/main.ts +122 -11
- package/src/cli/output.ts +44 -1
- package/src/cli/preview.ts +1 -1
- package/src/cli/sdlc-release-guard.ts +82 -0
- package/src/hub/action-class.ts +362 -0
- package/src/hub/action-policy.ts +106 -0
- package/src/hub/board.ts +43 -8
- package/src/hub/budget.ts +71 -2
- package/src/hub/bus.ts +43 -3
- package/src/hub/checks.ts +80 -31
- package/src/hub/code-facts.ts +223 -0
- package/src/hub/config-apply.ts +10 -1
- package/src/hub/config-trust.ts +7 -4
- package/src/hub/daemon.ts +237 -22
- package/src/hub/dogfood-read.ts +199 -0
- package/src/hub/dogfood.ts +163 -0
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +11 -2
- package/src/hub/hub-tools.ts +24 -2
- package/src/hub/inbox-polling.ts +25 -0
- package/src/hub/instances.ts +73 -1
- package/src/hub/profiles.ts +7 -2
- package/src/hub/release-worktree.ts +56 -0
- package/src/hub/review-brief.ts +36 -0
- package/src/hub/sdlc-queue.ts +31 -9
- package/src/hub/sdlc-release-authorization.ts +173 -0
- package/src/hub/sdlc.ts +49 -11
- package/src/hub/settings.ts +5 -2
- package/src/hub/snapshots.ts +93 -8
- package/src/hub/symbol-normalizer.ts +26 -0
- package/src/hub/task-code-symbols.ts +90 -0
- package/src/hub/task-sweep.ts +3 -2
- package/src/hub/tasks.ts +292 -54
- package/src/hub/test-integrity.ts +162 -0
- package/src/hub/usage-rollup.ts +361 -0
- package/src/local/sandbox.ts +5 -3
- package/src/ui/index.html +1 -0
- package/templates/AGENTS.block.md +1 -1
- package/templates/profiles/dev.json +6 -1
- package/templates/profiles/general.json +6 -1
- package/templates/profiles/research.json +6 -1
- package/templates/profiles/writing.json +6 -1
- package/templates/release-guard.block.md +6 -0
package/src/adapters/jcode.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { assessVersion, COMPAT, type CompatVerdict } from "./compat.ts";
|
|
2
3
|
import { createConnection } from "node:net";
|
|
3
4
|
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readSync, rmSync } from "node:fs";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
@@ -12,21 +13,25 @@ export interface JcodeOptions extends Omit<AcpOptions, "cmd" | "qwen025" | "expe
|
|
|
12
13
|
command: string[];
|
|
13
14
|
home: string;
|
|
14
15
|
providerProfile?: string;
|
|
16
|
+
/** The hub peer id this Jcode acts for: "jcode" for the base peer, an instance id for an extra one (#685). */
|
|
17
|
+
peer?: string;
|
|
18
|
+
/** Initial native provider (`-p`), passed exactly as the person wrote it (#685). The hub never handles model credentials. */
|
|
19
|
+
provider?: string;
|
|
15
20
|
/** How long a native steer may take to be acknowledged (#572); default 5 s. */
|
|
16
21
|
steerTimeoutMs?: number;
|
|
17
22
|
}
|
|
18
|
-
export function jcodeCommand(command: string[], kind: "serve" | "acp", socket: string, cwd: string, model?: string, profile?: string): string[] {
|
|
23
|
+
export function jcodeCommand(command: string[], kind: "serve" | "acp", socket: string, cwd: string, model?: string, profile?: string, provider?: string, peer = "jcode"): string[] {
|
|
19
24
|
if (!command.length || command.some(word => typeof word !== "string" || !word)) throw new Error("jcode_cmd must name an executable argv");
|
|
20
|
-
return [...command, kind, "--socket", socket, "--cwd", cwd, "--no-update", "--no-selfdev", ...(model ? ["--model", model] : []), ...(profile ? ["--provider-profile", profile] : []),
|
|
21
|
-
...(kind === "serve" ? ["--temporary-server", "--owner-pid", String(process.pid), "--server-name",
|
|
25
|
+
return [...command, kind, "--socket", socket, "--cwd", cwd, "--no-update", "--no-selfdev", ...(provider ? ["-p", provider] : []), ...(model ? ["--model", model] : []), ...(profile ? ["--provider-profile", profile] : []),
|
|
26
|
+
...(kind === "serve" ? ["--temporary-server", "--owner-pid", String(process.pid), "--server-name", `agent-hub-${peer}`] : ["--mcp-tools", "eager"])];
|
|
22
27
|
}
|
|
23
28
|
export interface JcodeTuiBinding { launchId: string; sessionId: string }
|
|
24
29
|
export type JcodeTuiState = { state: "waiting"; binding: JcodeTuiBinding } |
|
|
25
30
|
{ state: "ready"; binding: JcodeTuiBinding; launch: { cmd: string; args: string[]; env: NodeJS.ProcessEnv } };
|
|
26
31
|
|
|
27
|
-
export function jcodeTuiCommand(command: string[], socket: string, sessionId: string, model?: string, profile?: string): string[] {
|
|
32
|
+
export function jcodeTuiCommand(command: string[], socket: string, sessionId: string, model?: string, profile?: string, provider?: string): string[] {
|
|
28
33
|
if (!command.length || command.some(word => typeof word !== "string" || !word)) throw new Error("jcode_cmd must name an executable argv");
|
|
29
|
-
return [...command, "--socket", socket, "--resume", sessionId, "--no-update", "--no-selfdev",
|
|
34
|
+
return [...command, "--socket", socket, "--resume", sessionId, "--no-update", "--no-selfdev", ...(provider ? ["-p", provider] : []),
|
|
30
35
|
...(model ? ["--model", model] : []), ...(profile ? ["--provider-profile", profile] : [])];
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -65,17 +70,8 @@ export class JcodeOptionMismatch extends Error {
|
|
|
65
70
|
constructor(readonly option: "--session-id" | "--model" | "--provider-profile") { super("Jcode launch options differ from its running session"); }
|
|
66
71
|
}
|
|
67
72
|
const JCODE_OWNED_ENV = { JCODE_DISABLE_CLAUDE_MCP: "1", JCODE_NO_TELEMETRY: "1" } as const;
|
|
68
|
-
/**
|
|
69
|
-
export const
|
|
70
|
-
/** The first native release assumed to break that contract: the next major. Lower it when a release is shown to break. */
|
|
71
|
-
export const JCODE_BREAKING_VERSION = "1.0.0";
|
|
72
|
-
const release = (version: string) => /^(\d+)\.(\d+)\.(\d+)$/.exec(version)?.slice(1).map(Number);
|
|
73
|
-
const compare = (a: number[], b: number[]) => a[0]! - b[0]! || a[1]! - b[1]! || a[2]! - b[2]!;
|
|
74
|
-
/** A plain release from JCODE_MIN_VERSION up to, not including, JCODE_BREAKING_VERSION; the minor is not pinned (#529). */
|
|
75
|
-
export function jcodeVersionSupported(version: unknown): boolean {
|
|
76
|
-
const v = typeof version === "string" ? release(version) : undefined;
|
|
77
|
-
return !!v && compare(v, release(JCODE_MIN_VERSION)!) >= 0 && compare(v, release(JCODE_BREAKING_VERSION)!) < 0;
|
|
78
|
-
}
|
|
73
|
+
/** #691: the verdict lives in `src/adapters/compat.ts` (line, verified versions, the person's trust); never pin here. */
|
|
74
|
+
export const jcodeVersionSupported = (version: unknown): boolean => assessVersion("jcode", version).ok;
|
|
79
75
|
|
|
80
76
|
/** A native steer the server did not take, with nothing sent: the message may safely stay queued. */
|
|
81
77
|
class SteerRefused extends Error {}
|
|
@@ -137,20 +133,56 @@ function nativeSteer(socket: string, cwd: string, sessionId: string, text: strin
|
|
|
137
133
|
}
|
|
138
134
|
|
|
139
135
|
/** The wrapper contract preserves every configured prefix argument before the native probe. */
|
|
140
|
-
export function
|
|
136
|
+
export function jcodeVersion(command: string[], cwd: string): CompatVerdict {
|
|
141
137
|
const [bin, ...prefix] = command;
|
|
142
|
-
if (!bin) return
|
|
138
|
+
if (!bin) return assessVersion("jcode", undefined);
|
|
143
139
|
try {
|
|
144
140
|
const result = spawnSync(bin, [...prefix, "--quiet", "version", "--json"], { cwd, env: { ...peerChildEnv("jcode"), ...JCODE_OWNED_ENV }, encoding: "utf8", timeout: 10_000, maxBuffer: 64 * 1024 });
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
return assessVersion("jcode", result.status === 0 ? COMPAT.jcode.parse(result.stdout) : undefined);
|
|
142
|
+
} catch { return assessVersion("jcode", undefined); }
|
|
143
|
+
}
|
|
144
|
+
export const verifiedJcodeCommand = (command: string[], cwd: string): boolean => jcodeVersion(command, cwd).ok;
|
|
145
|
+
/** Collects at most `cap` bytes; the chunk that would pass it, and every later one, is refused and not kept. */
|
|
146
|
+
export function boundedOutput(cap: number): { push(chunk: Buffer): boolean; text(): string; readonly size: number; readonly overflowed: boolean } {
|
|
147
|
+
const chunks: Buffer[] = [];
|
|
148
|
+
let size = 0, overflowed = false;
|
|
149
|
+
return {
|
|
150
|
+
push(chunk) { if (overflowed || size + chunk.length > cap) { overflowed = true; return false; } chunks.push(chunk); size += chunk.length; return true; },
|
|
151
|
+
text: () => Buffer.concat(chunks).toString("utf8"),
|
|
152
|
+
get size() { return size; },
|
|
153
|
+
get overflowed() { return overflowed; },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The daemon's preflight: the same probe without blocking the event loop (a slow wrapper must not stall peers). The probe owns its
|
|
158
|
+
* process group: at the deadline, or once stdout passes 64 KiB, the whole group is stopped, descendants included (kimi-review's pattern).
|
|
159
|
+
*/
|
|
160
|
+
export function jcodeVersionAsync(command: string[], cwd: string, timeoutMs = 10_000): Promise<CompatVerdict> {
|
|
161
|
+
const [bin, ...prefix] = command;
|
|
162
|
+
if (!bin) return Promise.resolve(assessVersion("jcode", undefined));
|
|
163
|
+
return new Promise(resolve => {
|
|
164
|
+
let child: ChildProcess;
|
|
165
|
+
try { child = spawn(bin, [...prefix, "--quiet", "version", "--json"], { cwd, env: { ...peerChildEnv("jcode"), ...JCODE_OWNED_ENV }, detached: true, stdio: ["ignore", "pipe", "ignore"] }); }
|
|
166
|
+
catch { resolve(assessVersion("jcode", undefined)); return; }
|
|
167
|
+
const output = boundedOutput(64 * 1024);
|
|
168
|
+
let done = false;
|
|
169
|
+
const finish = async (code: number | null) => {
|
|
170
|
+
if (done) return; done = true; clearTimeout(timer);
|
|
171
|
+
try { await stopOwnedProcess(child, { group: true }); } catch { resolve(assessVersion("jcode", undefined)); return; }
|
|
172
|
+
resolve(assessVersion("jcode", code === 0 && !output.overflowed ? COMPAT.jcode.parse(output.text()) : undefined));
|
|
173
|
+
};
|
|
174
|
+
const timer = setTimeout(() => { void finish(null); }, timeoutMs);
|
|
175
|
+
child.once("spawn", () => trackGroup(child));
|
|
176
|
+
// Chunks after the cap or after settling are drained and discarded, never buffered.
|
|
177
|
+
child.stdout!.on("data", (bytes: Buffer) => { if (!done && !output.push(bytes)) void finish(null); });
|
|
178
|
+
child.once("error", () => { void finish(null); });
|
|
179
|
+
child.once("close", code => { void finish(code); });
|
|
180
|
+
});
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
/** Both processes are ours. The user's global daemon and its sockets are never stopped. */
|
|
152
184
|
export class JcodePeer extends AcpPeer {
|
|
153
|
-
private readonly launchId
|
|
185
|
+
private readonly launchId: string;
|
|
154
186
|
private socketIdentity: { dev: number; ino: number } | undefined;
|
|
155
187
|
private readonly root: string;
|
|
156
188
|
private readonly socket: string;
|
|
@@ -161,21 +193,28 @@ export class JcodePeer extends AcpPeer {
|
|
|
161
193
|
|
|
162
194
|
constructor(private readonly jcode: JcodeOptions) {
|
|
163
195
|
if (!isAbsolute(jcode.home)) throw new Error("jcode_home must be an absolute native data path");
|
|
196
|
+
const peer = jcode.peer ?? "jcode";
|
|
164
197
|
jcodeCommand(jcode.command, "acp", "preview", jcode.cwd);
|
|
165
198
|
// The name must end in ".sock": jcode names its debug socket by replacing that suffix, so "s" binds one path twice.
|
|
166
199
|
const root = mkdtempSync(join(tmpdir(), "ahub-jcode-")), socket = join(root, "j.sock");
|
|
167
|
-
const cmd = jcodeCommand(jcode.command, "acp", socket, jcode.cwd, jcode.launchModel, jcode.providerProfile);
|
|
168
|
-
|
|
169
|
-
this.
|
|
200
|
+
const cmd = jcodeCommand(jcode.command, "acp", socket, jcode.cwd, jcode.launchModel, jcode.providerProfile, jcode.provider, peer);
|
|
201
|
+
const launchId = crypto.randomUUID();
|
|
202
|
+
// The ACP child gets peerChildEnv(this.id): the instance id, so its hub tools act as this peer and never as the base Jcode. The launch id
|
|
203
|
+
// goes with it: the daemon admits a hub-tools hello for this peer only when it carries the id of this peer's current owned launch (#368).
|
|
204
|
+
super(peer, { ...jcode, cmd, env: { ...jcode.env, JCODE_HOME: jcode.home, JCODE_SOCKET: socket, AGENTHUB_LAUNCH_ID: launchId, ...JCODE_OWNED_ENV }, mcpServers: [], requireLoadSessionEcho: true, expectedAgent: { name: "jcode", version: jcodeVersionSupported } });
|
|
205
|
+
this.root = root; this.socket = socket; this.launchId = launchId;
|
|
170
206
|
}
|
|
171
207
|
|
|
208
|
+
/** True only for the id of this peer's current owned launch, while it is not ending: the proof a hub-tools hello must carry (#368). */
|
|
209
|
+
ownsLaunch(launchId: unknown): boolean { return !this.ending && typeof launchId === "string" && launchId === this.launchId; }
|
|
210
|
+
|
|
172
211
|
override async start(): Promise<void> {
|
|
173
212
|
// The native loader otherwise imports Codex MCP into the user's store on first use.
|
|
174
213
|
if (!existsSync(join(this.jcode.home, "mcp.json"))) throw new Error("initialize Jcode's global MCP store with its native client before enabling this peer; the hub never initializes or copies it");
|
|
175
214
|
if (existsSync(this.socket)) { this.preserveRoot = true; throw new Error("Jcode private socket is occupied; no daemon was adopted or removed"); }
|
|
176
215
|
this.ending = false;
|
|
177
|
-
const [bin, ...args] = jcodeCommand(this.jcode.command, "serve", this.socket, this.jcode.cwd, this.jcode.launchModel, this.jcode.providerProfile);
|
|
178
|
-
const server = spawn(bin!, args, { cwd: this.jcode.cwd, detached: true, env: { ...peerChildEnv(
|
|
216
|
+
const [bin, ...args] = jcodeCommand(this.jcode.command, "serve", this.socket, this.jcode.cwd, this.jcode.launchModel, this.jcode.providerProfile, this.jcode.provider, this.id);
|
|
217
|
+
const server = spawn(bin!, args, { cwd: this.jcode.cwd, detached: true, env: { ...peerChildEnv(this.id), JCODE_HOME: this.jcode.home, JCODE_SOCKET: this.socket, JCODE_READY_FD: "3", AGENTHUB_LAUNCH_ID: this.launchId, ...JCODE_OWNED_ENV }, stdio: ["ignore", "ignore", "ignore", "pipe"] });
|
|
179
218
|
this.server = server; trackGroup(server);
|
|
180
219
|
try {
|
|
181
220
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -279,11 +318,13 @@ export class JcodePeer extends AcpPeer {
|
|
|
279
318
|
: options.providerProfile !== undefined && options.providerProfile !== this.jcode.providerProfile ? "--provider-profile" : undefined;
|
|
280
319
|
if (differs) throw new JcodeOptionMismatch(differs);
|
|
281
320
|
if (!savedSession(this.jcode.home, sessionId)) return { state: "waiting", binding };
|
|
282
|
-
const [cmd, ...args] = jcodeTuiCommand(this.jcode.command, this.socket, sessionId, this.jcode.launchModel, this.jcode.providerProfile);
|
|
283
|
-
|
|
321
|
+
const [cmd, ...args] = jcodeTuiCommand(this.jcode.command, this.socket, sessionId, this.jcode.launchModel, this.jcode.providerProfile, this.jcode.provider);
|
|
322
|
+
// The TUI is the person's own client of this peer's server. The shared project MCP entry names no peer (#685), so every launch
|
|
323
|
+
// path supplies its own id: the base Jcode's too, or its MCP child would fall back to the channel default.
|
|
324
|
+
return { state: "ready", binding, launch: { cmd: cmd!, args, env: { JCODE_HOME: this.jcode.home, JCODE_SOCKET: this.socket, AGENTHUB_PEER_ID: this.id, AGENTHUB_LAUNCH_ID: this.launchId, ...JCODE_OWNED_ENV } } };
|
|
284
325
|
}
|
|
285
326
|
override recoveryMetadata(): Record<string, unknown> {
|
|
286
|
-
return { ...super.recoveryMetadata(), launch: { kind: "jcode", peer:
|
|
327
|
+
return { ...super.recoveryMetadata(), launch: { kind: "jcode", peer: this.id, mode: "headless", ...(this.jcode.launchModel ? { model: this.jcode.launchModel } : {}), ...(this.jcode.providerProfile ? { providerProfile: this.jcode.providerProfile } : {}) } };
|
|
287
328
|
}
|
|
288
329
|
}
|
|
289
330
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { assessVersion, COMPAT } from "./compat.ts";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -7,7 +8,7 @@ import { stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
|
|
|
7
8
|
import { realPath } from "../hub/project.ts";
|
|
8
9
|
import type { ReviewerBackend, ReviewerPrepareContext } from "./reviewer-print.ts";
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const MANAGED = "managed:kimi-code";
|
|
11
12
|
const object = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);
|
|
12
13
|
const hash = (text: string) => createHash("sha256").update(text).digest("hex");
|
|
13
14
|
/** Native discovery can walk above cwd; check lexical aliases as well as the physical temporary root. */
|
|
@@ -133,7 +134,9 @@ export const kimiReviewerBackend: ReviewerBackend = {
|
|
|
133
134
|
const executable = realPath(Bun.which(native[0]!, { PATH: process.env.PATH }) ?? resolve(native[0]!));
|
|
134
135
|
const command = [executable, ...native.slice(1)];
|
|
135
136
|
const executableStat = lstatSync(executable);
|
|
136
|
-
|
|
137
|
+
// #691: the verified versions and the person's trust live in compat.ts.
|
|
138
|
+
const verdict = assessVersion("kimi-review", COMPAT["kimi-review"].parse(await version(command, cwd, env, context.signal)));
|
|
139
|
+
if (!verdict.ok) throw new Error(verdict.reason);
|
|
137
140
|
const fingerprints = [configFile, mcpFile, agentFile].map(file => hash(read(file, 2_097_152)));
|
|
138
141
|
let cleaned = false;
|
|
139
142
|
const cleanup = () => { if (cleaned) return; rmSync(root, { recursive: true, force: true }); cleaned = true; };
|
|
@@ -149,7 +152,7 @@ export const kimiReviewerBackend: ReviewerBackend = {
|
|
|
149
152
|
&& ["credentials", "oauth", "device_id"].every((name, index) => realPath(join(source, name)) === sources[index] && realPath(join(tool, name)) === sources[index]);
|
|
150
153
|
} catch { return false; }
|
|
151
154
|
},
|
|
152
|
-
isolation: { backend: "kimi", version:
|
|
155
|
+
isolation: { backend: "kimi", version: verdict.version, binary: executable, profile: "private-config-exact-two-tools-shared-native-oauth", readOnly: true, strictMcp: true, ephemeral: true,
|
|
153
156
|
enforcement: "native-policy", freshSession: true, hubToolsOnly: true,
|
|
154
157
|
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
158
|
};
|
|
@@ -202,7 +202,9 @@ export interface AdapterDefinition {
|
|
|
202
202
|
};
|
|
203
203
|
identity: { mcp: "acp-session" | "project-file" | "env" | "channel-plugin" };
|
|
204
204
|
/** Declaration admission only; execution-root and runtime owner proof remain host responsibilities. */
|
|
205
|
-
instances?: { mode: "external" | "headless"; roles?: readonly string[]
|
|
205
|
+
instances?: { mode: "external" | "headless"; roles?: readonly string[];
|
|
206
|
+
/** Each declared instance runs its own native data home (`jcode_home`) and may pass its own provider, model and profile names (#685). Fixed builtins only. */
|
|
207
|
+
nativeHome?: true };
|
|
206
208
|
attach?: {
|
|
207
209
|
/** The authenticated external hello may claim this id; independent of MCP configuration shape. */
|
|
208
210
|
externalHello?: boolean;
|
package/src/adapters/registry.ts
CHANGED
|
@@ -232,6 +232,7 @@ export function assertNativeCapabilityScope(definition: AdapterDefinition): void
|
|
|
232
232
|
["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
|
|
233
233
|
["routing", definition.routing !== undefined],
|
|
234
234
|
["projection", definition.projection !== undefined],
|
|
235
|
+
["instances.nativeHome", declared(definition.instances, "nativeHome")], // #685: a fixed-builtin assumption, one native data home per declared instance
|
|
235
236
|
["reviewerVariant", definition.reviewerVariant !== undefined],
|
|
236
237
|
["reviewer", definition.reviewer !== undefined],
|
|
237
238
|
["hooks.factsHook", declared(definition.hooks, "factsHook")],
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { assessVersion, COMPAT, describe, isCompatId, readTrust, trustFile, writeTrust, type CompatId, type CompatVerdict } from "../adapters/compat.ts";
|
|
3
|
+
|
|
4
|
+
/** #691: what `ahub agents` and `ahub doctor` show for each gated native CLI. */
|
|
5
|
+
export interface AgentVersionRow { id: CompatId; executable?: string; verdict: CompatVerdict; policy: string }
|
|
6
|
+
|
|
7
|
+
/** `commands` carries the configured argv (codex_bin, jcode_cmd, qwen_cmd); the probe words follow it. */
|
|
8
|
+
export function agentVersions(commands: Partial<Record<CompatId, readonly string[]>> = {}): AgentVersionRow[] {
|
|
9
|
+
const trusted = readTrust();
|
|
10
|
+
return (Object.keys(COMPAT) as CompatId[]).map(id => {
|
|
11
|
+
const [defaultBin, ...words] = COMPAT[id].probe;
|
|
12
|
+
const [bin = defaultBin, ...prefix] = commands[id] ?? [defaultBin];
|
|
13
|
+
const executable = Bun.which(bin) ?? undefined;
|
|
14
|
+
if (!executable) return { id, verdict: { ok: false, state: "unknown", reason: `${COMPAT[id].label} not found on PATH` }, policy: describe(id) };
|
|
15
|
+
const result = spawnSync(executable, [...prefix, ...words], { encoding: "utf8", timeout: 10_000, maxBuffer: 64 * 1024, env: { ...process.env, JCODE_NO_TELEMETRY: "1", JCODE_DISABLE_CLAUDE_MCP: "1" } });
|
|
16
|
+
return { id, executable, verdict: assessVersion(id, result.status === 0 ? COMPAT[id].parse(result.stdout ?? "") : undefined, trusted), policy: describe(id) };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function runAgents(args: string[], commands: Partial<Record<CompatId, readonly string[]>>, fail: (message: string) => never): void {
|
|
21
|
+
const [sub, id, version, ...rest] = args.filter(arg => arg !== "--json");
|
|
22
|
+
if (sub === "trust") {
|
|
23
|
+
const revoke = rest.includes("--revoke");
|
|
24
|
+
if (!isCompatId(id) || !version || rest.some(arg => arg !== "--revoke")) fail(`usage: ahub agents trust <${Object.keys(COMPAT).join("|")}> <version> [--revoke]`);
|
|
25
|
+
// The issue requires saying what trust covers before it is recorded.
|
|
26
|
+
if (!revoke) console.log(`${COMPAT[id].label} ${version}: ${COMPAT[id].trustNote}`);
|
|
27
|
+
let rows;
|
|
28
|
+
try { rows = writeTrust(id, version, revoke); } catch (error) { fail((error as Error).message); }
|
|
29
|
+
if (revoke) return console.log(`revoked: ${id} ${version} (${rows.length} trusted on this machine, ${trustFile()})`);
|
|
30
|
+
console.log(`trusted on this machine: ${COMPAT[id].label} ${version}\nUndo: ahub agents trust ${id} ${version} --revoke`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (sub !== undefined) fail("usage: ahub agents [--json] | ahub agents trust <id> <version> [--revoke]");
|
|
34
|
+
const rows = agentVersions(commands);
|
|
35
|
+
if (args.includes("--json")) return console.log(JSON.stringify(rows.map(row => ({ id: row.id, executable: row.executable ?? null, ...row.verdict, policy: row.policy })), null, 2));
|
|
36
|
+
for (const row of rows) {
|
|
37
|
+
const v = row.verdict;
|
|
38
|
+
console.log(`${row.id.padEnd(13)} ${(v.version ?? "-").padEnd(10)} ${v.state.padEnd(9)} ${row.policy}${v.ok ? (v.note ? `\n${" ".repeat(14)}${v.note}` : "") : `\n${" ".repeat(14)}${v.reason}`}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// #711 / #380: the console's review guidance. Pure strings and decisions, no terminal code and no state import: console-state.ts renders them.
|
|
2
|
+
// Nothing here changes authority: the console runs `ahub review` as the person, and the hub decides.
|
|
3
|
+
|
|
4
|
+
export type Verdict = "approved" | "changes_requested";
|
|
5
|
+
/** The task a review choice was opened for, taken from the selected row or the open detail. */
|
|
6
|
+
export interface ReviewChoice { id: number; title: string; reviewerIsUser: boolean }
|
|
7
|
+
|
|
8
|
+
const MAX_TITLE = 40;
|
|
9
|
+
const clean = (text: string) => text.replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
10
|
+
/** The title as the choice line shows it: quoted, one line, cut to the width; a private task is `[pii]`. */
|
|
11
|
+
export function choiceTitle(row: { title?: unknown; signals?: unknown }): string {
|
|
12
|
+
const priv = (Array.isArray(row.signals) && row.signals.some(s => s === "pii" || s === "pii_path")) || row.title === "[pii]";
|
|
13
|
+
if (priv) return "[pii]";
|
|
14
|
+
const text = clean(String(row.title ?? ""));
|
|
15
|
+
return JSON.stringify(text.length > MAX_TITLE ? `${text.slice(0, MAX_TITLE - 3)}...` : text);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The choice for a task row, or undefined when it is not one the person can review (not in review). */
|
|
19
|
+
export function reviewChoiceFor(row: any): ReviewChoice | undefined {
|
|
20
|
+
if (!row || row.state !== "in_review" || !Number.isSafeInteger(Number(row.id)) || Number(row.id) <= 0) return undefined;
|
|
21
|
+
return { id: Number(row.id), title: choiceTitle(row), reviewerIsUser: row.reviewer === "user" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The prompt line while the choice is open (#711 change 1). The title is already quoted. */
|
|
25
|
+
export const choicePrompt = (c: ReviewChoice): string => `review #${c.id} ${c.title}: a approve c request changes Esc cancel`;
|
|
26
|
+
/** What `a` and `c` prefill on the input line. The trailing space is where the note goes. */
|
|
27
|
+
export const verdictInput = (id: number, verdict: Verdict): string => `review ${id} ${verdict} `;
|
|
28
|
+
/** The guide line shown above the input after a choice (#711 change 1). */
|
|
29
|
+
export const verdictGuide = (id: number, verdict: Verdict): string =>
|
|
30
|
+
verdict === "approved" ? `Enter approves #${id}. You can type a note first. Esc cancels.` : "Say what to fix, then Enter. Esc cancels.";
|
|
31
|
+
|
|
32
|
+
/** The footer hint for a task in review (#711 change 2): the verb, and whose review it is when it is the person's. */
|
|
33
|
+
export const reviewHint = (reviewer: unknown): string => reviewer === "user" ? "your review: r approve or request changes" : "r approve or request changes";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A `review <id> <verdict> [note]` line the person typed, checked before it is sent: a changes request must say what to fix (#711 change 1). Returns the refusal,
|
|
37
|
+
* or undefined when the line is fine or is not a review line at all (other commands are validated where they always were).
|
|
38
|
+
*/
|
|
39
|
+
export function reviewRefusal(args: readonly string[]): string | undefined {
|
|
40
|
+
if (args[0] !== "review") return undefined;
|
|
41
|
+
const verdict = args[2];
|
|
42
|
+
if (verdict !== "changes_requested") return undefined;
|
|
43
|
+
const note = args.slice(3).filter(word => !word.startsWith("--")).join(" ").trim();
|
|
44
|
+
return note ? undefined : "say what to fix before requesting changes";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The notice after a review command finished: `#372 approved`, `#372 changes requested`, or the CLI's own refusal verbatim (#711 change 3). */
|
|
48
|
+
export function reviewResult(args: readonly string[], code: number, diagnostic: string): { text: string; tone: "success" | "failure" } | undefined {
|
|
49
|
+
if (args[0] !== "review" || !/^\d+$/.test(args[1] ?? "")) return undefined;
|
|
50
|
+
const verdict = args[2];
|
|
51
|
+
if (verdict !== "approved" && verdict !== "changes_requested") return undefined;
|
|
52
|
+
if (code) return { text: clean(diagnostic) || `command exited ${code}`, tone: "failure" };
|
|
53
|
+
return { text: `#${args[1]} ${verdict === "approved" ? "approved" : "changes requested"}`, tone: "success" };
|
|
54
|
+
}
|