@staix/agent-hub 0.12.39 → 0.12.40
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 +26 -0
- package/README.md +15 -10
- package/docs/agent-notes/adapters.md +4 -1
- package/docs/agent-notes/budget.md +2 -0
- package/docs/agent-notes/bus.md +2 -2
- package/docs/agent-notes/daemon.md +10 -6
- package/docs/agent-notes/models.md +4 -4
- package/docs/agent-notes/tasks.md +3 -0
- package/docs/agent-notes/tests.md +1 -1
- package/docs/memory.md +1 -1
- package/docs/operations.md +152 -28
- package/docs/quickstart.md +19 -5
- package/docs/security.md +19 -7
- package/docs/smoke.md +2 -2
- package/docs/specs/2026-09-19-agent-hub-design.md +158 -70
- package/docs/specs/2026-10-04-switchyard-source-port-design.md +12 -7
- package/docs/verification/2026-10-20-0.12.40.md +63 -0
- package/docs/verified.json +38 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +413 -147
- package/src/adapters/acp.ts +34 -1
- package/src/adapters/claude-channel.ts +35 -4
- package/src/adapters/codex-appserver.ts +12 -3
- package/src/adapters/definitions/builtin-metadata.ts +2 -0
- package/src/adapters/definitions/native-cli-doctor.ts +8 -5
- package/src/adapters/definitions/native-cli-entry-claude.ts +7 -2
- package/src/adapters/definitions/native-cli-entry-codex.ts +13 -14
- package/src/adapters/definitions/native-cli-entry-jcode.ts +13 -3
- package/src/adapters/definitions/native-cli-entry-pi.ts +9 -7
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +55 -1
- package/src/adapters/jcode.ts +110 -4
- package/src/adapters/local-worker.ts +21 -37
- package/src/adapters/pi.ts +7 -0
- package/src/cli/console-state.ts +1 -0
- package/src/cli/console.ts +11 -1
- package/src/cli/help.ts +13 -8
- package/src/cli/identity-audit.ts +33 -7
- package/src/cli/jcode-mcp.ts +1 -1
- package/src/cli/main.ts +134 -24
- package/src/cli/output.ts +5 -2
- package/src/cli/status-lines.ts +1 -1
- package/src/cli/terminal-recovery.ts +54 -4
- package/src/cli/upgrade-interactive.ts +5 -0
- package/src/cli/upgrade-runtime.ts +13 -1
- package/src/cli/upgrade.ts +67 -1
- package/src/hub/board.ts +4 -2
- package/src/hub/bus.ts +8 -3
- package/src/hub/checks.ts +15 -5
- package/src/hub/conductor.ts +2 -1
- package/src/hub/config-apply.ts +20 -2
- package/src/hub/config-reload.ts +2 -8
- package/src/hub/control-client.ts +26 -3
- package/src/hub/daemon.ts +155 -68
- package/src/hub/delivery-superseded.ts +19 -2
- package/src/hub/events.ts +4 -1
- package/src/hub/hub-tools.ts +4 -4
- package/src/hub/inference.ts +4 -8
- package/src/hub/lifecycle.ts +3 -3
- package/src/hub/main-checkout.ts +36 -0
- package/src/hub/memory-profile-check.ts +6 -2
- package/src/hub/memory-profile.ts +18 -1
- package/src/hub/peers.ts +2 -0
- package/src/hub/ports.ts +1 -0
- package/src/hub/routing.ts +24 -14
- package/src/hub/settings.ts +1 -1
- package/src/hub/task-repository.ts +136 -0
- package/src/hub/tasks.ts +101 -21
- package/src/hub/terminal-launch.ts +130 -0
- package/src/hub/usage.ts +44 -18
- package/src/models/route/config.ts +1 -1
- package/src/omniroute/client.ts +7 -19
- package/templates/AGENTS.block.md +1 -0
- package/templates/routing.toml +10 -44
- package/src/switchyard/config.ts +0 -45
- package/src/switchyard/sidecar.ts +0 -136
package/src/adapters/jcode.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
2
3
|
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readSync, rmSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { isAbsolute, join } from "node:path";
|
|
5
6
|
import type { Readable } from "node:stream";
|
|
6
|
-
import type
|
|
7
|
+
import { renderDigest, type Envelope, type PeerState } from "../hub/envelope.ts";
|
|
7
8
|
import { AcpPeer, type AcpOptions } from "./acp.ts";
|
|
8
9
|
import { peerChildEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
|
|
9
10
|
|
|
@@ -11,6 +12,8 @@ export interface JcodeOptions extends Omit<AcpOptions, "cmd" | "qwen025" | "expe
|
|
|
11
12
|
command: string[];
|
|
12
13
|
home: string;
|
|
13
14
|
providerProfile?: string;
|
|
15
|
+
/** How long a native steer may take to be acknowledged (#572); default 5 s. */
|
|
16
|
+
steerTimeoutMs?: number;
|
|
14
17
|
}
|
|
15
18
|
export function jcodeCommand(command: string[], kind: "serve" | "acp", socket: string, cwd: string, model?: string, profile?: string): string[] {
|
|
16
19
|
if (!command.length || command.some(word => typeof word !== "string" || !word)) throw new Error("jcode_cmd must name an executable argv");
|
|
@@ -57,6 +60,10 @@ function savedSession(home: string, id: string): boolean {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/** Set on every owned jcode process: no Claude MCP import, and no native telemetry (jcode-telemetry-core is_enabled, #529). */
|
|
63
|
+
/** A TUI launch option that differs from the running owned session; carries the flag name only. */
|
|
64
|
+
export class JcodeOptionMismatch extends Error {
|
|
65
|
+
constructor(readonly option: "--session-id" | "--model" | "--provider-profile") { super("Jcode launch options differ from its running session"); }
|
|
66
|
+
}
|
|
60
67
|
const JCODE_OWNED_ENV = { JCODE_DISABLE_CLAUDE_MCP: "1", JCODE_NO_TELEMETRY: "1" } as const;
|
|
61
68
|
/** The oldest native release accepted; its ACP, serve and MCP contract was verified live on 0.98.1 (docs/verification). */
|
|
62
69
|
export const JCODE_MIN_VERSION = "0.98.0";
|
|
@@ -70,6 +77,65 @@ export function jcodeVersionSupported(version: unknown): boolean {
|
|
|
70
77
|
return !!v && compare(v, release(JCODE_MIN_VERSION)!) >= 0 && compare(v, release(JCODE_BREAKING_VERSION)!) < 0;
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
/** A native steer the server did not take, with nothing sent: the message may safely stay queued. */
|
|
81
|
+
class SteerRefused extends Error {}
|
|
82
|
+
/** A native steer that was sent but not acknowledged: it may have gone in, so it is never requeued as if it had not. */
|
|
83
|
+
class SteerUncertain extends Error {}
|
|
84
|
+
const STEER_TIMEOUT_MS = 5000;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* #572 AC1: a second client on the hub's own private server socket queues a soft interrupt into the running session
|
|
88
|
+
* (jcode injects it at the next tool boundary, or when a text-only turn ends). Established on 0.99.0 under an isolated
|
|
89
|
+
* home: ping lists `soft_interrupt_ids`, subscribe with `target_session_id` joins the same session without a new one,
|
|
90
|
+
* `state` says whether a turn runs, and the injection survives this client's disconnect. Nothing here cancels the turn.
|
|
91
|
+
* `canSend` runs immediately before the soft interrupt is written and names why the hub no longer wants it sent. What
|
|
92
|
+
* cannot be closed from here is the native side: the server's `state` reply is a snapshot, so a turn that ends and
|
|
93
|
+
* another that begins inside the server between its reply and the write still receives the message. Such a steer is
|
|
94
|
+
* acknowledged, adopted by nobody, and held for review; an upstream atomic "steer turn N" request would remove it.
|
|
95
|
+
*/
|
|
96
|
+
function nativeSteer(socket: string, cwd: string, sessionId: string, text: string, clientId: string, timeoutMs = STEER_TIMEOUT_MS, canSend: () => string | undefined = () => undefined): Promise<void> {
|
|
97
|
+
return new Promise<void>((resolve, reject) => {
|
|
98
|
+
const conn = createConnection(socket);
|
|
99
|
+
let phase: "ping" | "subscribe" | "state" | "sent" = "ping", buffer = "", finished = false;
|
|
100
|
+
const finish = (error?: Error) => {
|
|
101
|
+
if (finished) return;
|
|
102
|
+
finished = true; clearTimeout(timer); conn.destroy();
|
|
103
|
+
if (error) reject(error); else resolve();
|
|
104
|
+
};
|
|
105
|
+
const fail = (why: string) => finish(phase === "sent" ? new SteerUncertain(`Jcode steering outcome is uncertain: ${why}`) : new SteerRefused(`Jcode owned server cannot take a steer: ${why}`));
|
|
106
|
+
const timer = setTimeout(() => fail(`no answer in the ${phase} step`), timeoutMs);
|
|
107
|
+
const send = (value: Record<string, unknown>) => conn.write(`${JSON.stringify(value)}\n`);
|
|
108
|
+
conn.once("error", () => fail("connection failed"));
|
|
109
|
+
conn.once("close", () => fail("connection closed"));
|
|
110
|
+
conn.once("connect", () => send({ type: "ping", id: 1 }));
|
|
111
|
+
conn.on("data", chunk => {
|
|
112
|
+
buffer += chunk.toString();
|
|
113
|
+
if (buffer.length > 1024 * 1024) return fail("oversized reply");
|
|
114
|
+
const lines = buffer.split("\n"); buffer = lines.pop()!;
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
let msg: { type?: unknown; id?: unknown; capabilities?: unknown; session_id?: unknown; is_processing?: unknown };
|
|
117
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
118
|
+
if (msg.type === "error" && typeof msg.id === "number" && msg.id >= 1) return fail("the server refused a request");
|
|
119
|
+
if (phase === "ping" && msg.type === "pong" && msg.id === 1) {
|
|
120
|
+
if (!Array.isArray(msg.capabilities) || !msg.capabilities.includes("soft_interrupt_ids")) return fail("no soft interrupt support");
|
|
121
|
+
phase = "subscribe"; send({ type: "subscribe", id: 2, working_dir: cwd, target_session_id: sessionId, continue_on_disconnect: true });
|
|
122
|
+
} else if (phase === "subscribe" && msg.type === "session") {
|
|
123
|
+
if (msg.session_id !== sessionId) return fail("the server answered for another session");
|
|
124
|
+
phase = "state"; send({ type: "state", id: 3 });
|
|
125
|
+
} else if (phase === "state" && msg.type === "state" && msg.id === 3) {
|
|
126
|
+
if (msg.session_id !== sessionId || msg.is_processing !== true) return fail("the session has no running turn");
|
|
127
|
+
// The last check before anything is sent (review round 2 I1): the hub's own turn, launch, session and stop state may
|
|
128
|
+
// have changed during the connect, ping, subscribe and state round trips. Past this write the server may have taken
|
|
129
|
+
// the message, so it is never refused as unsent again.
|
|
130
|
+
const changed = canSend();
|
|
131
|
+
if (changed) return fail(changed);
|
|
132
|
+
phase = "sent"; send({ type: "soft_interrupt", id: 4, content: text, urgent: false, client_id: clientId });
|
|
133
|
+
} else if (phase === "sent" && msg.type === "ack" && msg.id === 4) finish();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
73
139
|
/** The wrapper contract preserves every configured prefix argument before the native probe. */
|
|
74
140
|
export function verifiedJcodeCommand(command: string[], cwd: string): boolean {
|
|
75
141
|
const [bin, ...prefix] = command;
|
|
@@ -162,6 +228,44 @@ export class JcodePeer extends AcpPeer {
|
|
|
162
228
|
}
|
|
163
229
|
if (failure) throw failure;
|
|
164
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* #572 AC1: an `important` message to a busy Jcode goes into its running turn through its own server (a soft
|
|
233
|
+
* interrupt), with the receipt contract of Codex's steer: `failed_safe` when nothing was sent, `needs_review` when it
|
|
234
|
+
* may have gone in, `accepted` once the server acknowledged it, and `completed` when the turn ends normally.
|
|
235
|
+
* The hub never cancels the turn to deliver.
|
|
236
|
+
*/
|
|
237
|
+
async steer(envs: Envelope[], deliveryId?: string): Promise<void> {
|
|
238
|
+
const refuse = (reason: string): never => {
|
|
239
|
+
if (deliveryId) this.delivery({ id: deliveryId, state: "failed_safe", reason });
|
|
240
|
+
throw new Error(reason);
|
|
241
|
+
};
|
|
242
|
+
const sessionId = super.recoveryMetadata().sessionId;
|
|
243
|
+
const socket = lstatSync(this.socket, { throwIfNoEntry: false });
|
|
244
|
+
if (this.state !== "busy") refuse(`jcode is ${this.state}`);
|
|
245
|
+
if (this.ending || !this.server || this.server.exitCode !== null || this.server.signalCode !== null ||
|
|
246
|
+
!socket?.isSocket() || socket.dev !== this.socketIdentity?.dev || socket.ino !== this.socketIdentity.ino || typeof sessionId !== "string") refuse("owned Jcode server is unavailable");
|
|
247
|
+
// The turn this steer is for. The native acknowledgement is asynchronous: only this exact turn may take the message.
|
|
248
|
+
const turn = this.currentTurn(), launch = this.server!, identity = this.socketIdentity; // both proven present above
|
|
249
|
+
const stillCurrent = (): string | undefined => {
|
|
250
|
+
if (this.currentTurn() !== turn || this.state !== "busy") return `jcode's turn changed before the steer was sent (${this.state})`;
|
|
251
|
+
if (this.ending || this.server !== launch || launch.exitCode !== null || launch.signalCode !== null || this.socketIdentity !== identity) return "owned Jcode server changed before the steer was sent";
|
|
252
|
+
if (super.recoveryMetadata().sessionId !== sessionId) return "jcode's session changed before the steer was sent";
|
|
253
|
+
const now = lstatSync(this.socket, { throwIfNoEntry: false });
|
|
254
|
+
if (!now?.isSocket() || now.dev !== identity?.dev || now.ino !== identity.ino) return "owned Jcode socket changed before the steer was sent";
|
|
255
|
+
return undefined;
|
|
256
|
+
};
|
|
257
|
+
try { await nativeSteer(this.socket, this.jcode.cwd, sessionId as string, renderDigest(envs, true), deliveryId ?? crypto.randomUUID(), this.jcode.steerTimeoutMs, stillCurrent); }
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error instanceof SteerUncertain) {
|
|
260
|
+
if (deliveryId) this.delivery({ id: deliveryId, state: "needs_review", reason: "steering outcome is uncertain" });
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
return refuse(error instanceof Error ? error.message : "Jcode steer failed");
|
|
264
|
+
}
|
|
265
|
+
// The turn may have ended, or another begun, while the server answered: it did take the message, so it is held for review, never refused as unsent and never adopted into a different turn.
|
|
266
|
+
try { this.adoptIntoTurn(turn, envs, deliveryId); }
|
|
267
|
+
catch { if (deliveryId) this.delivery({ id: deliveryId, state: "needs_review", reason: "steered turn ended before its receipt" }); throw new SteerUncertain("Jcode steering outcome is uncertain: its turn ended first"); }
|
|
268
|
+
}
|
|
165
269
|
tuiLaunch(expected?: JcodeTuiBinding, options: { model?: string; providerProfile?: string; sessionId?: string } = {}): JcodeTuiState {
|
|
166
270
|
const sessionId = super.recoveryMetadata().sessionId;
|
|
167
271
|
const socket = lstatSync(this.socket, { throwIfNoEntry: false });
|
|
@@ -169,9 +273,11 @@ export class JcodePeer extends AcpPeer {
|
|
|
169
273
|
!socket?.isSocket() || socket.dev !== this.socketIdentity?.dev || socket.ino !== this.socketIdentity.ino || typeof sessionId !== "string") throw new Error("owned Jcode launch is unavailable");
|
|
170
274
|
const binding = { launchId: this.launchId, sessionId };
|
|
171
275
|
if (expected && (expected.launchId !== binding.launchId || expected.sessionId !== binding.sessionId)) throw new Error("Jcode launch changed");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
276
|
+
// Names the option, never its value: the flag is the person's own and carries nothing private (#559).
|
|
277
|
+
const differs = options.sessionId !== undefined && options.sessionId !== sessionId ? "--session-id"
|
|
278
|
+
: options.model !== undefined && options.model !== this.jcode.launchModel ? "--model"
|
|
279
|
+
: options.providerProfile !== undefined && options.providerProfile !== this.jcode.providerProfile ? "--provider-profile" : undefined;
|
|
280
|
+
if (differs) throw new JcodeOptionMismatch(differs);
|
|
175
281
|
if (!savedSession(this.jcode.home, sessionId)) return { state: "waiting", binding };
|
|
176
282
|
const [cmd, ...args] = jcodeTuiCommand(this.jcode.command, this.socket, sessionId, this.jcode.launchModel, this.jcode.providerProfile);
|
|
177
283
|
return { state: "ready", binding, launch: { cmd: cmd!, args, env: { JCODE_HOME: this.jcode.home, JCODE_SOCKET: this.socket, ...JCODE_OWNED_ENV } } };
|
|
@@ -4,15 +4,16 @@ import type { RouteLabelEvent, RouteTurnOutcome } from "../models/route/labels.t
|
|
|
4
4
|
import type { StaySwitchPolicy } from "../models/route/stage.ts";
|
|
5
5
|
import type { ToolObservation } from "../models/route/signals.ts";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { renderDigest, replyAudience, replyParent, STANDING_INSTRUCTION, USER, type Envelope, type EnvelopeOpts, type PeerId } from "../hub/envelope.ts";
|
|
7
|
+
import { MAX_HOP, renderDigest, replyAudience, replyParent, STANDING_INSTRUCTION, USER, type Envelope, type EnvelopeOpts, type PeerId } from "../hub/envelope.ts";
|
|
8
8
|
import { CONDUCTOR_TOOL_NAMES, CONDUCTOR_TOOLS, TASK_TOOL_NAMES, TASK_TOOLS, profileTaskTools } from "../hub/hub-tools.ts";
|
|
9
9
|
import { BasePeer } from "../hub/peers.ts";
|
|
10
|
+
import type { PublishResult } from "../hub/bus.ts";
|
|
10
11
|
import { profile, proxyEnv, type SandboxNetwork } from "../local/sandbox.ts";
|
|
11
12
|
import { runTool, toolResultFailed, TOOL_SCHEMAS, touchedPaths, ApprovalWaitStop, type ToolApproval, type ApprovalProvenance, type ToolContext } from "../local/tools.ts";
|
|
12
13
|
import type { Capture } from "../memory/capture.ts";
|
|
13
14
|
import type { ChatMessage, ChatResult, OmniRoute, ToolCall } from "../omniroute/client.ts";
|
|
14
15
|
import { safeModelLabel } from "../omniroute/usage.ts";
|
|
15
|
-
import
|
|
16
|
+
import { effectiveLocalRoute } from "../hub/routing.ts";
|
|
16
17
|
import type { ExecutionBudgetDecision } from "../hub/execution-budget.ts";
|
|
17
18
|
|
|
18
19
|
export interface LocalOptions {
|
|
@@ -21,11 +22,9 @@ export interface LocalOptions {
|
|
|
21
22
|
sdlcCriteria?: boolean;
|
|
22
23
|
cwd: string;
|
|
23
24
|
omni: OmniRoute;
|
|
24
|
-
/**
|
|
25
|
-
sidecar?: Sidecar;
|
|
26
|
-
/** Switchyard route id asked of the sidecar. */
|
|
25
|
+
/** In-process hub route id; legacy routes use the fixed model. */
|
|
27
26
|
route?: string;
|
|
28
|
-
/** Model
|
|
27
|
+
/** Model alias sent straight to OmniRoute when no hub route is active or its call fails. */
|
|
29
28
|
fixedModel: string;
|
|
30
29
|
hubRoutes?: () => Record<string, HubRoute>;
|
|
31
30
|
/** routing.toml `stay_switch` for hub stage routes (#197). */
|
|
@@ -81,6 +80,7 @@ export class LocalPeer extends BasePeer {
|
|
|
81
80
|
private readonly history: ChatMessage[] = [];
|
|
82
81
|
private readonly sessionId = `agent-hub-local-${randomUUID()}`;
|
|
83
82
|
private readonly routes: HubRouteRuntime;
|
|
83
|
+
private readonly warnedLegacyRoutes = new Set<string>();
|
|
84
84
|
private routeEnvs: Envelope[] = [];
|
|
85
85
|
private routePii = false;
|
|
86
86
|
private routeTask?: number;
|
|
@@ -237,10 +237,8 @@ export class LocalPeer extends BasePeer {
|
|
|
237
237
|
const turnSignal = this.abort!.signal;
|
|
238
238
|
// claude-mem's observer is a cloud model: nothing of a PII turn is captured.
|
|
239
239
|
const capture = policy?.pii ? undefined : this.opts.capture;
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
const viaOffCampus = !(policy?.route ?? this.opts.route)?.startsWith("hub/") && !!this.opts.sidecar?.upstream && this.opts.omni.isAccessHost(this.opts.sidecar.upstream);
|
|
243
|
-
if (policy?.pii && (viaOffCampus || !(await this.opts.omni.onCampus()))) {
|
|
240
|
+
// Unknown gateway state is never permission to send PII.
|
|
241
|
+
if (policy?.pii && !(await this.opts.omni.onCampus())) {
|
|
244
242
|
return "Refused: this is a PII task and the only reachable gateway is off campus (Cloudflare Access). Connect the VPN and assign it again.";
|
|
245
243
|
}
|
|
246
244
|
this.routeEnvs = envs;
|
|
@@ -328,8 +326,9 @@ export class LocalPeer extends BasePeer {
|
|
|
328
326
|
sandboxEnv: { ...proxyEnv(this.opts.tools.bashNetwork ?? false), AGENTHUB_PEER_ID: this.id },
|
|
329
327
|
signal: turnSignal,
|
|
330
328
|
send: (text, to) => {
|
|
331
|
-
const
|
|
332
|
-
if (typeof
|
|
329
|
+
const result = this.onMessage?.(text, pii ? reply : { inReplyTo: replyParent(envs), to: to?.length ? to : replyAudience(envs) }) as PublishResult | string | undefined;
|
|
330
|
+
if (typeof result === "string") return `not sent: ${result}`;
|
|
331
|
+
if (result?.dropped === "hop") return `not delivered: reply chain reached the hop cap (${MAX_HOP}); send a new message without reply_to`;
|
|
333
332
|
return pii ? "sent to the console user only (PII task)" : "sent";
|
|
334
333
|
},
|
|
335
334
|
};
|
|
@@ -372,11 +371,14 @@ export class LocalPeer extends BasePeer {
|
|
|
372
371
|
for (const tool_call_id of missing) msgs.push({ role: "tool", tool_call_id, content: result });
|
|
373
372
|
}
|
|
374
373
|
|
|
375
|
-
/**
|
|
374
|
+
/** Hub-owned in-process policy, with a fixed OmniRoute alias as fallback. */
|
|
376
375
|
private async call(turnMsgs: ChatMessage[], policy: { route?: string; fixedModel?: string; pii?: boolean; task?: string } | undefined, envs: Envelope[]): Promise<ChatResult> {
|
|
377
|
-
const { omni
|
|
378
|
-
|
|
379
|
-
|
|
376
|
+
const { omni } = this.opts;
|
|
377
|
+
const route = effectiveLocalRoute(this.opts.route, policy?.route, policy?.fixedModel);
|
|
378
|
+
if (route && !route.startsWith("hub/") && !this.warnedLegacyRoutes.has(route)) {
|
|
379
|
+
this.warnedLegacyRoutes.add(route);
|
|
380
|
+
this.opts.log?.(`legacy route ${route} is deprecated and ignored; using fixed model`);
|
|
381
|
+
}
|
|
380
382
|
const fixedModel = policy?.fixedModel ?? this.opts.fixedModel;
|
|
381
383
|
const task = policy?.task === undefined ? undefined : Number(policy.task);
|
|
382
384
|
const tools = [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : [])];
|
|
@@ -384,19 +386,6 @@ export class LocalPeer extends BasePeer {
|
|
|
384
386
|
const messages: ChatMessage[] = [{ role: "system", content: system(this.opts.cwd, this.opts.preamble) }, ...this.history, ...turnMsgs];
|
|
385
387
|
const routed = await this.hubCall(route, messages, policy, signal);
|
|
386
388
|
if (routed) return routed;
|
|
387
|
-
const via = await this.sidecarEndpoint(route, !!policy?.pii);
|
|
388
|
-
if (via) {
|
|
389
|
-
await this.requireBudget(envs, "model_calls");
|
|
390
|
-
try {
|
|
391
|
-
const res = await omni.chat({ model: route!, messages, tools }, { via, sessionId: this.sessionId, signal });
|
|
392
|
-
this.lastServedBy = `switchyard ${route} -> ${res.selectedModel ?? "?"}`;
|
|
393
|
-
this.recordUsage(res, route!, task);
|
|
394
|
-
return res;
|
|
395
|
-
} catch (e) {
|
|
396
|
-
if (signal.aborted) throw e;
|
|
397
|
-
sidecar!.disable((e as Error).message);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
389
|
await this.requireBudget(envs, "model_calls");
|
|
401
390
|
const res = await omni.chat({ model: fixedModel, messages, tools }, { signal, ...(policy?.pii ? { onCampusOnly: true } : {}) });
|
|
402
391
|
this.lastServedBy = `omniroute ${fixedModel} (provider ${res.provider ?? "?"})`;
|
|
@@ -441,13 +430,8 @@ export class LocalPeer extends BasePeer {
|
|
|
441
430
|
}
|
|
442
431
|
}
|
|
443
432
|
|
|
444
|
-
private async
|
|
445
|
-
|
|
446
|
-
return this.opts.sidecar?.endpoint();
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private async conclude(msgs: ChatMessage[], policy: { route?: string; pii: boolean } | undefined, turn: number, step: number, maxSteps: number, answer: string, signal: AbortSignal, decisionId?: string): Promise<string | undefined> {
|
|
450
|
-
const route = policy?.route ?? this.opts.route, config = this.hubConfig(route);
|
|
433
|
+
private async conclude(msgs: ChatMessage[], policy: { route?: string; fixedModel?: string; pii: boolean } | undefined, turn: number, step: number, maxSteps: number, answer: string, signal: AbortSignal, decisionId?: string): Promise<string | undefined> {
|
|
434
|
+
const route = effectiveLocalRoute(this.opts.route, policy?.route, policy?.fixedModel), config = this.hubConfig(route);
|
|
451
435
|
if (!route || !config) return answer;
|
|
452
436
|
const messages: ChatMessage[] = [{ role: "system", content: system(this.opts.cwd, this.opts.preamble) }, ...this.history, ...msgs];
|
|
453
437
|
const feedback = await this.routes.review(route, config, messages, this.routeScope(!!policy?.pii, turn), !!policy?.pii, signal, decisionId);
|
|
@@ -472,7 +456,7 @@ export class LocalPeer extends BasePeer {
|
|
|
472
456
|
...(res.usage ? { usage: res.usage } : {}),
|
|
473
457
|
requestedModel: safeModelLabel(requestedModel) ?? "unknown",
|
|
474
458
|
...(Number.isSafeInteger(task) && task! > 0 ? { task } : {}),
|
|
475
|
-
...(safeModelLabel(res.servedModel
|
|
459
|
+
...(safeModelLabel(res.servedModel) ? { servedModel: safeModelLabel(res.servedModel)! } : {}),
|
|
476
460
|
...(safeModelLabel(res.provider) ? { provider: safeModelLabel(res.provider)! } : {}),
|
|
477
461
|
});
|
|
478
462
|
} catch {
|
package/src/adapters/pi.ts
CHANGED
|
@@ -429,6 +429,13 @@ export class PiPeer extends BasePeer {
|
|
|
429
429
|
private verifiedOwner(): boolean {
|
|
430
430
|
return this.ownerClaimed && !!this.ownerPid && !!this.ownerSignature && processSignature(this.ownerPid) === this.ownerSignature;
|
|
431
431
|
}
|
|
432
|
+
|
|
433
|
+
terminalOwner(_launchId: string): { generation: string; nativePid: number } | undefined {
|
|
434
|
+
if (this.opts.mode !== "tui" || !this.started || this.stopping || this.rosterDetached || !this.verifiedOwner() || !this.sessionId || !this.sessionFile ||
|
|
435
|
+
this.opts.sessionId && this.opts.sessionId !== this.sessionId || this.opts.sessionFile && this.opts.sessionFile !== this.sessionFile) return undefined;
|
|
436
|
+
const generation = this.sessionGeneration;
|
|
437
|
+
return generation && this.ownerPid ? { generation, nativePid: this.ownerPid } : undefined;
|
|
438
|
+
}
|
|
432
439
|
private abortTools(): Promise<void>[] {
|
|
433
440
|
this.executionAbort?.abort();
|
|
434
441
|
this.idleBashReservations.clear();
|
package/src/cli/console-state.ts
CHANGED
|
@@ -891,6 +891,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
891
891
|
if (p.permissionMode && p.permissionMode !== "ask") summary.push(span(` ${p.permissionMode}`, "attention"));
|
|
892
892
|
if (p.needsReview) summary.push(span(` review${p.needsReview}`, "failure"));
|
|
893
893
|
if (p.paused) summary.push(span(" paused", "attention"));
|
|
894
|
+
if (typeof p.importantWaitingSince === "number") summary.push(span(` important waiting ${duration(now - p.importantWaitingSince)}`, "attention")); // #572 AC1: a busy ACP peer that cannot be steered
|
|
894
895
|
if (p.toolsOnly) summary.push(span(definitionOf(id)?.attach?.pullOnlyForms?.includes("tui") ? ` pull-only, read ${typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never"}` : " tools-only: ahub claude", "attention"));
|
|
895
896
|
}
|
|
896
897
|
if (s.pendingRestart?.length) summary.push(span(`${summary.length ? " | " : ""}restart pending: ${s.pendingRestart.map(row => row.key).join(", ")}`, "attention"));
|
package/src/cli/console.ts
CHANGED
|
@@ -86,7 +86,8 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
86
86
|
const { client } = options; const terminal = options.terminal ?? nativeTerminal();
|
|
87
87
|
const color = options.color ?? resolveColor(undefined, { isTTY: terminal.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR }) === true;
|
|
88
88
|
let state = initialConsoleState(!!options.panels); state.project = basename(options.cwd);
|
|
89
|
-
|
|
89
|
+
let pet = loadPetConfig(options.cwd);
|
|
90
|
+
let petSignature = JSON.stringify([pet.enabled, pet.character, pet.size, pet.glyphs, pet.blink, pet.bell]);
|
|
90
91
|
state.petEnabled = pet.enabled && !options.noPet; state.petBlink = pet.blink;
|
|
91
92
|
state.petChoices = CHARACTER_IDS; state.petCharacter = pet.character ?? "stai";
|
|
92
93
|
state.petSize = pet.size; state.petGlyphs = petGlyphs(pet.glyphs);
|
|
@@ -194,6 +195,15 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
194
195
|
if (!active) return;
|
|
195
196
|
const [status, budget, tasks, queue, ready, settings] = replies;
|
|
196
197
|
captureConsoleSelection(state);
|
|
198
|
+
try {
|
|
199
|
+
const latestPet = loadPetConfig(options.cwd);
|
|
200
|
+
const nextSignature = JSON.stringify([latestPet.enabled, latestPet.character, latestPet.size, latestPet.glyphs, latestPet.blink, latestPet.bell]);
|
|
201
|
+
if (nextSignature !== petSignature) {
|
|
202
|
+
pet = latestPet; petSignature = nextSignature;
|
|
203
|
+
state.petEnabled = pet.enabled && !options.noPet; state.petBlink = pet.blink;
|
|
204
|
+
state.petCharacter = pet.character ?? "stai"; state.petSize = pet.size; state.petGlyphs = petGlyphs(pet.glyphs);
|
|
205
|
+
}
|
|
206
|
+
} catch { /* retain running console presentation while a config file is unreadable */ }
|
|
197
207
|
if (status.status?.tasks && status.ok !== false) state.taskCounts = status.status.tasks;
|
|
198
208
|
if (status.status && status.ok !== false) state.pendingRestart = Array.isArray(status.status.pendingRestart) ? status.status.pendingRestart : [];
|
|
199
209
|
if (status.status && status.ok !== false) state.retainedConfig = Array.isArray(status.status.retainedConfig) ? status.status.retainedConfig : [];
|
package/src/cli/help.ts
CHANGED
|
@@ -21,6 +21,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
21
21
|
["ahub console --no-pet", "turn off attention for this session; combines with --panels and --color"],
|
|
22
22
|
["ahub pet --window [--close]", "start or close the macOS floating window, from a person's terminal"],
|
|
23
23
|
["ahub pet [--short]", "read attention without connecting; --short prints ahub!N only when action is needed"],
|
|
24
|
+
["ahub pet --palette <blue|teal|violet|ochre>", "save the floating window's base color to pet.palette in .agenthub/config.local.json"],
|
|
24
25
|
["ahub sdlc status [task] | explain <task> | export <task> | record <task> <receipt-json>", "read head-bound lifecycle gates and the next actor/action (person only)"],
|
|
25
26
|
["ahub sdlc queue <owner/repo> | release <owner/repo> [live-issue...]", "read-only merge and release evidence (person only)"],
|
|
26
27
|
["ahub sdlc accept <task> --issue <n> [--repo owner/repo]", "accept the task's spec issue at its current body; build entry needs it in enforce (person only)"],
|
|
@@ -36,8 +37,8 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
36
37
|
["ahub doctor --orphans [--kill] [--json] [--color=auto|always|never]", "list registrations whose project root is gone; --kill stops their daemons (SIGTERM, then SIGKILL) only after the process identity checks out"],
|
|
37
38
|
["ahub kill", "stop this project's hub"],
|
|
38
39
|
["ahub reset [--all] [--yes]", "list, then with --yes stop the hub and discard every queued and needs_review delivery, clear holds and pauses and drop session pointers; --all moves the state directory to .agenthub/archive instead"],
|
|
39
|
-
["ahub restart [--dry-run] [--yes]", "recover this project's runtime; on a terminal without a flag it reviews the plan with you, follows the progress and offers the next steps"],
|
|
40
|
-
["ahub upgrade [--to <version>] [--dry-run] [--yes]", "review and upgrade running projects; --to defaults to the latest release (--yes needs it), and a newer release's own coordinator takes over; on a terminal without --dry-run or --yes: review, end agents first if you choose, apply, follow, and resume, cancel, end or reset on the spot"],
|
|
40
|
+
["ahub restart [--dry-run] [--yes] [--json] [--verbose] [--color=auto|always|never]", "recover this project's runtime; on a terminal without a flag it reviews the plan with you, follows the progress and offers the next steps"],
|
|
41
|
+
["ahub upgrade [--to <version>] [--dry-run] [--yes] [--json] [--verbose] [--color=auto|always|never]", "review and upgrade running projects; --to defaults to the latest release (--yes needs it), and a newer release's own coordinator takes over; on a terminal without --dry-run or --yes: review, end agents first if you choose, apply, follow, and resume, cancel, end or reset on the spot"],
|
|
41
42
|
["ahub recovery [status|resume|abort] [<operation-id>]", "inspect, resume or cancel a preflight; status names the next actions; without an id, the operation that holds the lock, and with nothing else on a terminal its screen"],
|
|
42
43
|
["ahub recovery reattach <operation-id> codex|claude|pi", "person only: repair that blocked peer with its recorded session; leaves the rest for recovery resume"],
|
|
43
44
|
["ahub recovery dispose [<operation-id>] --fresh-session codex|claude --reason <text>", "end a partial operation by starting that peer as a new session, recording the lost conversation"],
|
|
@@ -49,10 +50,12 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
49
50
|
["ahub claude [--print-command] [--unattended] [args...]", "launch Claude Code with the hub channel"],
|
|
50
51
|
["ahub codex [--print-command] [--unattended] [args...]", "start the Codex adapter and attach the TUI"],
|
|
51
52
|
["ahub kimi [--print-command] [--mode tui|headless] [--model <alias>]", "default: Kimi's own TUI, pull-only via hub_inbox; no push, steer, watchdog, console approvals or automatic quota handoff. Select headless for ACP"],
|
|
52
|
-
["ahub jcode [--mode headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>] [--print-command]", "optional owned
|
|
53
|
+
["ahub jcode [--mode tui|headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>] [--print-command]", "optional Jcode (roster.jcode, default off): its TUI on the hub-owned ACP session, opened once that session is saved; --mode headless for ACP only; native policy unmanaged, no hub sandbox"],
|
|
54
|
+
["ahub agy [--print-command | --dry-run] [args...]", "optional Antigravity CLI 1.4.2 TUI in this terminal (roster.agy, default off): pull-only via hub_inbox, implementer/verifier only; native permissions unmanaged, no hub sandbox"],
|
|
53
55
|
["ahub qwen [--mode headless] [--model <alias>] [--session-id <id>] [--print-command]", "start optional Qwen Code 0.25.0 ACP (roster.qwen, default off)"],
|
|
54
|
-
["ahub pi [--print-command] [--mode headless|tui | --headless] [--backend auto|dgx|mlx] [--session-id <id>] [--session-file <path>] [--replace-session]", "start Pi: its TUI in this terminal unless --headless or the start mode setting (peers.pi.start_mode) says headless; without a terminal the hub opens one or says how"],
|
|
56
|
+
["ahub pi [--print-command] [--mode headless|tui | --headless] [--backend auto|dgx|mlx] [--model <alias>] [--session-id <id>] [--session-file <path>] [--replace-session]", "start Pi: its TUI in this terminal unless --headless or the start mode setting (peers.pi.start_mode) says headless; without a terminal the hub opens one or says how"],
|
|
55
57
|
["ahub local [--route <id> | --model <id>]", "start the hub-native worker on the self-hosted models (routing.toml)"],
|
|
58
|
+
["ahub claude|codex|kimi|qwen|pi|local --as <instance> [options...]", "start an extra instance of that kind declared under instances in .agenthub/config.local.json"],
|
|
56
59
|
["ahub role list [--group <group>] | show <id>", "read bounded catalog guidance with layer and content hash"],
|
|
57
60
|
["ahub role assign <peer> <id> | clear <peer> [<id>]", "person only: add or clear catalog guidance, applied at the next peer start"],
|
|
58
61
|
["ahub stop <peer>", "human only: stop a hub-owned headless peer; end TUI agents in their terminals"],
|
|
@@ -67,7 +70,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
67
70
|
["ahub permit <id> <option>", "answer a permission request shown by tail (\"deny\" cancels)"],
|
|
68
71
|
["ahub roster [--json] [--color=auto|always|never]", "human only: every known peer, participation, start mode and retained work"],
|
|
69
72
|
["ahub roster add <peer> | remove <peer> [--yes] [--now]", "human only: change live participation through settings; removal previews retained messages and task handoff"],
|
|
70
|
-
["ahub settings [list] [--json] | get <key>", "human only: the settings the dashboard can change, each with its value, the file it comes from and when it applies"],
|
|
73
|
+
["ahub settings [list] [--json] | get <key> [--json]", "human only: the settings the dashboard can change, each with its value, the file it comes from and when it applies"],
|
|
71
74
|
["ahub settings set <key> <value|inherit> [--yes] [--preview] [--now] | undo | reload", "human only: change or reload validated settings, written to .agenthub/config.local.json or routing.local.toml (never a tracked file); --preview shows the effect and saves nothing; undo puts back the last write"],
|
|
72
75
|
["ahub pause|resume <peer>", "hold a peer's deliveries in its queue / release them"],
|
|
73
76
|
["ahub queue list [--peer <id>] [--json] [--color=auto|always|never]", "inspect durable deliveries"],
|
|
@@ -88,8 +91,8 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
88
91
|
["ahub task assign <id> <peer> [--role owner|reviewer] [--revision N]", "name the owner (default) or reviewer; a task that still waits reserves its owner; --revision refuses stale console actions"],
|
|
89
92
|
["ahub task unassign <id> [--role owner|reviewer] [--revision N]", "person only: remove the owner/reservation or reviewer without routing a replacement or stopping native work"],
|
|
90
93
|
["ahub task cancel <id> --reason <text> [--superseded-by <ref>] [--revision N]", "person only: close obsolete work with a reason; retain its history without stopping native work or releasing dependents"],
|
|
91
|
-
["ahub task label <id> ok|regressed|reverted|incomplete|wrong|abandoned", "your later verdict on an approved task, kept in the research records (research must be on)"],
|
|
92
|
-
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]...", "give a review verdict on a task"],
|
|
94
|
+
["ahub task label <id> ok|regressed|reverted|incomplete|wrong|abandoned [--created <time>]", "your later verdict on an approved task, kept in the research records (research must be on)"],
|
|
95
|
+
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]... [--head <sha>]", "give a review verdict on a task"],
|
|
93
96
|
["ahub review brief <id>", "print the review brief of a task's commit: reviewable and excluded files, rules, coverage to account for"],
|
|
94
97
|
["ahub route explain <id>", "why a task went where it went, its reserved owner included"],
|
|
95
98
|
["ahub route explain --class <c> <title...>", "what would happen to such a task now"],
|
|
@@ -113,7 +116,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
113
116
|
["ahub research export [--format jsonl|csv] [--since 30d] [--all]", "the research records for outside analysis: ids, counts and tokens, never text"],
|
|
114
117
|
["ahub research backfill", "build this project's research records once from events.jsonl"],
|
|
115
118
|
["ahub bench run <suite.json> --arm <label> [--repeat N] [--tasks a,b] [--addon <name>=on|off]... [--memory off|claude-mem|native|both]", "run a benchmark suite against the peers attached here, in a project kept for benchmarks (it resets the work tree); --addon and --memory declare the arm's add-on and memory state and refuse a setup that differs"],
|
|
116
|
-
["ahub bench list|status", "benchmark runs on this machine / the one running now"],
|
|
119
|
+
["ahub bench list|status [--json]", "benchmark runs on this machine / the one running now"],
|
|
117
120
|
["ahub bench report <run> [--json]", "pass rate, first pass, rework, tokens and wall time per task and overall"],
|
|
118
121
|
["ahub bench compare <run|arm>... [--suite <name>] [--mixed] [--json]", "compare arms of one suite with 95% bootstrap intervals; inconclusive below 5 counted attempts per arm"],
|
|
119
122
|
["ahub bench export [--format jsonl|csv]", "benchmark records for outside analysis: ids, outcomes and counts, never suite text"],
|
|
@@ -123,6 +126,8 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
123
126
|
["ahub memory promote|demote|forget <id> | supersede <old> <new> | rebuild", "a person's changes to memory: move an entry between tiers, forget it, mark it superseded, or rebuild the index from the logs"],
|
|
124
127
|
["ahub memory sync [--remote <private git url>] | machine [--label <name>]", "sync hub memory with your other machines through a private git remote (a person's), or show or name this machine"],
|
|
125
128
|
["ahub memory import claude-mem [--db <path>] [--limit N] [--restart] | dedupe [--apply] | suggestions | confirm|reject <id>", "copy the hub's notes from claude-mem into hub memory (a person's); review the model's suggested edges"],
|
|
129
|
+
["ahub memory decay [--days N] [--json]", "person only: list long-term entries older than N days (default 90) that no open goal links; review only, nothing is changed"],
|
|
130
|
+
["ahub memory profile-check [--json]", "read-only check of the running hubs' memory bindings against their memory profiles; exits 1 when one is not ready"],
|
|
126
131
|
["ahub goal [list] [--all] | show <id> [--json]", "long-term tasks: state, which machine holds each and until when, linked board tasks"],
|
|
127
132
|
["ahub goal new <title> [--milestone <text>]... [--depends <id>]... [--shared] | link <id> <task> | state <id> <state> | done <id> | take <id> [--force]", "a person's changes to long-term tasks; take --force voids other machines' claims"],
|
|
128
133
|
]],
|
|
@@ -22,7 +22,7 @@ const splitLabel = (label: string): [string, string[]] => { const [command = "un
|
|
|
22
22
|
* the checked 256-file / 64 KiB ceiling by at most one bounded row per concurrent writer.
|
|
23
23
|
* Unpublished temp files count against the ceiling, so a crashed writer cannot grow the spool forever.
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
function reserveCliAudit(stateDir: string, peer: string, command: string, outcome: CliAudit["outcome"]) {
|
|
26
26
|
const row: CliAudit = { v: 1, id: crypto.randomUUID(), at: Date.now(), peer, command, outcome };
|
|
27
27
|
if (!valid(row)) throw new Error("invalid CLI audit identifiers");
|
|
28
28
|
const dir = join(stateDir, "cli-audit");
|
|
@@ -36,13 +36,39 @@ export function recordCliAudit(stateDir: string, peer: string, command: string,
|
|
|
36
36
|
const text = JSON.stringify(row) + "\n";
|
|
37
37
|
if (Buffer.byteLength(text) > MAX_ROW_BYTES || files.length >= MAX_FILES || bytes + Buffer.byteLength(text) > MAX_BYTES) throw new Error("CLI audit outbox is full");
|
|
38
38
|
const pending = join(dir, `${row.id}.tmp`);
|
|
39
|
+
try { writeFileSync(pending, text, { mode: 0o600, flag: "wx" }); }
|
|
40
|
+
catch (error) { try { unlinkSync(pending); } catch { /* only this writer's unpublished file */ } throw error; }
|
|
41
|
+
return {
|
|
42
|
+
id: row.id,
|
|
43
|
+
commit(outcome: CliAudit["outcome"]) {
|
|
44
|
+
if (!valid({ ...row, outcome })) throw new Error("invalid CLI audit identifiers");
|
|
45
|
+
writeFileSync(pending, JSON.stringify({ ...row, outcome }) + "\n", { mode: 0o600 });
|
|
46
|
+
renameSync(pending, join(dir, `${row.id}.json`));
|
|
47
|
+
},
|
|
48
|
+
close() { try { unlinkSync(pending); } catch { /* only this writer's unpublished file */ } },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function recordCliAudit(stateDir: string, peer: string, command: string, outcome: CliAudit["outcome"]): void {
|
|
53
|
+
const pending = reserveCliAudit(stateDir, peer, command, outcome);
|
|
54
|
+
try { pending.commit(outcome); }
|
|
55
|
+
finally { pending.close(); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Reserve the bounded audit before connecting; old drains never see an unsupported ready row. */
|
|
59
|
+
export async function recordDelegatedCliAudit(stateDir: string, peer: string, command: string, features: () => Promise<readonly string[]>): Promise<string> {
|
|
60
|
+
let pending: ReturnType<typeof reserveCliAudit>;
|
|
61
|
+
try { pending = reserveCliAudit(stateDir, peer, command, "delegated"); }
|
|
62
|
+
catch { throw new Error("live-test delegation audit unavailable; nothing was executed"); }
|
|
39
63
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
throw
|
|
45
|
-
|
|
64
|
+
let supported = false;
|
|
65
|
+
try { supported = (await features()).includes("cli_audit_delegated"); }
|
|
66
|
+
catch { /* unavailable authentication is not proof of an audit consumer */ }
|
|
67
|
+
try { pending.commit(supported ? "delegated" : "refused"); }
|
|
68
|
+
catch { throw new Error("live-test delegation audit unavailable; nothing was executed"); }
|
|
69
|
+
if (!supported) throw new Error("running hub cannot retain delegated CLI audits; upgrade it from a person's terminal, or use the audited upgrade/recovery path; nothing was executed");
|
|
70
|
+
return pending.id;
|
|
71
|
+
} finally { pending.close(); }
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
/** Consume ready files only; rebuild fixed fields so titles or other injected text cannot be rendered. */
|
package/src/cli/jcode-mcp.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Compatibility exports; the project MCP layer is native adapter policy. */
|
|
2
|
-
export { JCODE_MCP_FILE, JCODE_MCP_NAME, jcodeMcpEntry, jcodeMcpReady, configureJcodeMcp } from "../adapters/definitions/native-cli-jcode-mcp.ts";
|
|
2
|
+
export { JCODE_MCP_FILE, JCODE_MCP_NAME, jcodeMcpEntry, jcodeMcpReady, configureJcodeMcp, jcodeExtraMcpServers } from "../adapters/definitions/native-cli-jcode-mcp.ts";
|