machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.26
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 +134 -0
- package/CONTRIBUTING.md +3 -3
- package/GOVERNANCE.md +2 -2
- package/README.md +24 -6
- package/browser-extension/manifest.json +1 -1
- package/docs/AGENT_CONTEXT.md +10 -7
- package/docs/ARCHITECTURE.md +35 -22
- package/docs/AUDIT.md +85 -1
- package/docs/CLIENTS.md +6 -2
- package/docs/ENGINEERING.md +31 -9
- package/docs/LOCAL_AUTOMATION.md +4 -2
- package/docs/LOGGING.md +8 -8
- package/docs/OPERATIONS.md +43 -17
- package/docs/PRIVACY.md +18 -4
- package/docs/PROJECT_STANDARDS.md +2 -2
- package/docs/RELEASING.md +35 -11
- package/docs/TESTING.md +36 -16
- package/docs/THREAT_MODEL.md +20 -5
- package/docs/TOOL_REFERENCE.md +18 -12
- package/docs/UPGRADING.md +32 -0
- package/package.json +15 -6
- package/scripts/check-plan.mjs +8 -0
- package/scripts/coverage-check.mjs +30 -1
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/github-release.mjs +22 -16
- package/scripts/install-published-prerelease.mjs +7 -7
- package/scripts/official-mcp-conformance.mjs +243 -0
- package/scripts/persistent-activation-process.mjs +36 -0
- package/scripts/release-candidate-manifest.mjs +12 -0
- package/scripts/release-publication-guard.mjs +65 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/sbom-check.mjs +99 -0
- package/scripts/start-release-candidate.mjs +39 -13
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +25 -4
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/child-process-settlement.mjs +103 -0
- package/src/local/cli-activate.mjs +42 -5
- package/src/local/cli-service.mjs +55 -5
- package/src/local/cli.mjs +59 -10
- package/src/local/daemon-process.mjs +24 -3
- package/src/local/delegated-process-sandbox.mjs +1 -0
- package/src/local/execution-routing.mjs +231 -0
- package/src/local/git-service.mjs +3 -1
- package/src/local/job-runner.mjs +55 -19
- package/src/local/macos-trust-broker.mjs +7 -0
- package/src/local/managed-job-runner-claim.mjs +54 -0
- package/src/local/managed-job-runner.mjs +13 -2
- package/src/local/process-execution.mjs +2 -2
- package/src/local/process-identity.mjs +11 -0
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +49 -41
- package/src/local/process-tree.mjs +1 -1
- package/src/local/relay-call-recovery.mjs +40 -21
- package/src/local/runtime-activation.mjs +357 -38
- package/src/local/runtime-capabilities.mjs +22 -6
- package/src/local/runtime-diagnostics.mjs +9 -2
- package/src/local/runtime.mjs +18 -4
- package/src/local/service-convergence.mjs +33 -0
- package/src/local/service-owner.mjs +147 -0
- package/src/local/service-restart-handoff.mjs +22 -8
- package/src/local/service-runtime.mjs +145 -0
- package/src/local/service.mjs +143 -25
- package/src/local/state.mjs +104 -7
- package/src/local/stdio.mjs +139 -45
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/tool-executor.mjs +24 -6
- package/src/local/tools.mjs +6 -5
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +30 -53
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +256 -0
- package/src/shared/mcp-subscriptions.d.mts +4 -0
- package/src/shared/mcp-subscriptions.mjs +59 -0
- package/src/shared/relay-contract.json +1 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +11 -4
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-catalog.json +18 -12
- package/src/worker/durable-stream-calls.ts +12 -24
- package/src/worker/http.ts +36 -2
- package/src/worker/index.ts +181 -165
- package/src/worker/mcp-http-contract.ts +276 -0
- package/src/worker/mcp-jsonrpc.ts +12 -6
- package/src/worker/mcp-legacy-dispatch.ts +104 -0
- package/src/worker/mcp-modern-controller.ts +199 -0
- package/src/worker/mcp-modern-proxy.ts +126 -0
- package/src/worker/mcp-modern-stream.ts +71 -0
- package/src/worker/mcp-session.ts +12 -3
- package/src/worker/mcp-stream-proxy-contract.ts +67 -0
- package/src/worker/mcp-stream-proxy.ts +17 -60
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/tool-catalog.ts +29 -1
- package/src/worker/tool-timeout.ts +53 -12
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +10 -1
- package/src/worker/worker-runtime-config.ts +19 -0
- package/src/worker/worker-static-routes.ts +7 -2
- package/tsconfig.local.json +7 -1
package/src/local/cli.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { workerHealth } from "./worker-health.mjs";
|
|
|
29
29
|
export { workerHealthUserReason } from "./worker-health.mjs";
|
|
30
30
|
import { activeStateJobs, activeStateLocks, knownProfileStates, knownWorkerNames } from "./state-inventory.mjs";
|
|
31
31
|
import {
|
|
32
|
+
acquireMachineServiceLockWithWait,
|
|
32
33
|
acquireMaintenanceLock,
|
|
33
34
|
acquireStartupLockWithWait,
|
|
34
35
|
daemonLockPathForState,
|
|
@@ -54,7 +55,7 @@ import {
|
|
|
54
55
|
const localAdminCommands = createLocalAdminCommands({ chooseWorkspace, confirm });
|
|
55
56
|
const accountCommand = createAccountCommand({ chooseWorkspace, confirm });
|
|
56
57
|
const approvalCommand = createApprovalCommand({ chooseWorkspace, confirm });
|
|
57
|
-
const serviceCommand = createServiceCommand({ chooseWorkspace, stateRootFromArgs, structuredLogger });
|
|
58
|
+
const serviceCommand = createServiceCommand({ chooseWorkspace, stateRootFromArgs, structuredLogger, acquireMachineServiceLockWithWait, currentPackageVersion });
|
|
58
59
|
const activateCommand = createActivateCommand({
|
|
59
60
|
chooseWorkspace,
|
|
60
61
|
prepareRemoteState,
|
|
@@ -190,9 +191,12 @@ async function startCommand(args) {
|
|
|
190
191
|
const serviceEnvironment = loadServiceEnvironment(state.paths.stateRoot);
|
|
191
192
|
logger.debug?.("Loaded persisted service network environment", { keys: serviceEnvironment.keys });
|
|
192
193
|
}
|
|
193
|
-
|
|
194
|
+
let startupLock = null;
|
|
195
|
+
let serviceLock = null;
|
|
194
196
|
|
|
195
197
|
try {
|
|
198
|
+
serviceLock = await acquireRuntimeStartServiceLock(args, acquireMachineServiceLockWithWait, logger);
|
|
199
|
+
startupLock = await acquireStartupLockWithWait(state, { operation: "start", logger });
|
|
196
200
|
const startMode = await prepareStartMode(args, state, logger);
|
|
197
201
|
const daemonLock = await acquireDaemonLockWithTakeover(state, {
|
|
198
202
|
takeOverServiceOwner: startMode.takeOverServiceOwner,
|
|
@@ -206,9 +210,12 @@ async function startCommand(args) {
|
|
|
206
210
|
reportExistingDaemon(args, state, daemonLock.owner, logger);
|
|
207
211
|
return;
|
|
208
212
|
}
|
|
213
|
+
serviceLock?.release?.();
|
|
214
|
+
serviceLock = null;
|
|
209
215
|
await startRemoteRuntime({ args, workspace, state, daemonLock, logger });
|
|
210
216
|
} finally {
|
|
211
|
-
|
|
217
|
+
serviceLock?.release?.();
|
|
218
|
+
startupLock?.release?.();
|
|
212
219
|
}
|
|
213
220
|
}
|
|
214
221
|
|
|
@@ -264,6 +271,20 @@ function reportExistingDaemon(args, state, owner, logger) {
|
|
|
264
271
|
logger.plain(` Workspace: ${state.workspace.path}`);
|
|
265
272
|
}
|
|
266
273
|
|
|
274
|
+
export function runtimeStartRequiresMachineServiceLock(args = {}) {
|
|
275
|
+
return args.daemonOnly !== true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function acquireRuntimeStartServiceLock(args = {}, acquireLock = acquireMachineServiceLockWithWait, logger = console) {
|
|
279
|
+
if (!runtimeStartRequiresMachineServiceLock(args)) return null;
|
|
280
|
+
if (typeof acquireLock !== "function") throw new TypeError("runtime start requires a machine-service lock acquirer");
|
|
281
|
+
const lock = await acquireLock({ operation: "runtime-start", logger });
|
|
282
|
+
if (!lock?.acquired || typeof lock.release !== "function") {
|
|
283
|
+
throw new Error("machine-service operation lock could not be acquired for runtime start");
|
|
284
|
+
}
|
|
285
|
+
return lock;
|
|
286
|
+
}
|
|
287
|
+
|
|
267
288
|
export function isIdempotentDaemonOnlyStart(args) {
|
|
268
289
|
if (!args.daemonOnly || args.json) return false;
|
|
269
290
|
return !Boolean(
|
|
@@ -286,18 +307,39 @@ async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }
|
|
|
286
307
|
const readiness = await prepareRemoteState({ args, workspace, state, logger });
|
|
287
308
|
runtime = createRemoteRuntime({ args, workspace, state, daemonLock, deviceSessionIdentity: readiness.deviceSessionIdentity });
|
|
288
309
|
await runtime.start();
|
|
310
|
+
if (typeof daemonLock.update !== "function") throw new Error("daemon lock cannot publish startup readiness");
|
|
311
|
+
daemonLock.update({ startupReady: true, startupReadyAt: new Date().toISOString() });
|
|
289
312
|
reportRemoteReady(args, state, readiness, logger);
|
|
313
|
+
if (args.daemonOnly) {
|
|
314
|
+
const { startAutostartLogMaintenance } = await import("./autostart-log-maintenance.mjs");
|
|
315
|
+
startAutostartLogMaintenance(state.paths.stateRoot, {
|
|
316
|
+
onError(error) {
|
|
317
|
+
logger.event?.("warn", "service.log_maintenance.failed", {
|
|
318
|
+
error_class: classifyOperationalError(error),
|
|
319
|
+
}, "Background log maintenance failed");
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
290
323
|
keepProcessAlive({ daemon: runtime, lock: daemonLock, logger });
|
|
291
324
|
} catch (error) {
|
|
292
|
-
|
|
293
|
-
daemonLock.release();
|
|
294
|
-
throw error;
|
|
325
|
+
throw cleanupRuntimeStartFailure(error, runtime, daemonLock);
|
|
295
326
|
}
|
|
296
327
|
}
|
|
297
328
|
|
|
298
|
-
|
|
329
|
+
export function cleanupRuntimeStartFailure(error, runtime, daemonLock) {
|
|
330
|
+
const cleanupErrors = [];
|
|
331
|
+
try { runtime?.stop?.(); } catch (failure) { cleanupErrors.push(failure); }
|
|
332
|
+
try { daemonLock?.release?.(); } catch (failure) { cleanupErrors.push(failure); }
|
|
333
|
+
return cleanupErrors.length
|
|
334
|
+
? new AggregateError([error, ...cleanupErrors],
|
|
335
|
+
"runtime startup failed and local cleanup was incomplete")
|
|
336
|
+
: error;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function prepareRemoteState({ args, workspace, state, logger, onRemotePrepared }) {
|
|
299
340
|
if (!args.daemonOnly) {
|
|
300
341
|
await convergeRemoteConfiguration({ args, state });
|
|
342
|
+
onRemotePrepared?.();
|
|
301
343
|
} else if (!state.worker.url) {
|
|
302
344
|
throw new Error("--daemon-only requires an existing Worker URL; run start once without --daemon-only");
|
|
303
345
|
} else if (state.worker.pendingDeviceIdentity) {
|
|
@@ -579,8 +621,11 @@ async function rotateSecretsCommand(args) {
|
|
|
579
621
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
|
|
580
622
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
581
623
|
const operationLogger = createLogger({ level: args.quiet ? "error" : "warn", component: "service" });
|
|
582
|
-
|
|
624
|
+
let startupLock = null;
|
|
625
|
+
let serviceLock = null;
|
|
583
626
|
try {
|
|
627
|
+
serviceLock = await acquireMachineServiceLockWithWait({ operation: "rotate-secrets", logger: operationLogger });
|
|
628
|
+
startupLock = await acquireStartupLockWithWait(state, { operation: "rotate-secrets", logger: operationLogger });
|
|
584
629
|
await stopOwnedPlatformService({
|
|
585
630
|
state,
|
|
586
631
|
inspectWorkspaceDaemon,
|
|
@@ -610,14 +655,15 @@ async function rotateSecretsCommand(args) {
|
|
|
610
655
|
console.log("Prepared a two-phase rotation for account administration, device root, and token-version secrets.");
|
|
611
656
|
console.log("All account access tokens are invalid. Run machine-mcp to deploy, verify, and atomically promote the pending device root.");
|
|
612
657
|
} finally {
|
|
613
|
-
startupLock
|
|
658
|
+
startupLock?.release?.();
|
|
659
|
+
serviceLock?.release?.();
|
|
614
660
|
}
|
|
615
661
|
}
|
|
616
662
|
|
|
617
663
|
async function installAutostartBestEffort({ workspace, stateRoot, entryScript, logger }) {
|
|
618
664
|
try {
|
|
619
665
|
const { installAutostart } = await import("./service.mjs");
|
|
620
|
-
const result = await installAutostart({ workspace, stateRoot, entryScript, logger: structuredLogger(true) });
|
|
666
|
+
const result = await installAutostart({ workspace, stateRoot, entryScript, version: currentPackageVersion(), logger: structuredLogger(true) });
|
|
621
667
|
if (result?.ok) logger.info("Autostart installed for future logins", { provider: result.provider });
|
|
622
668
|
else logger.warn("Autostart installation reported a problem; run `machine-mcp service status` for details", {
|
|
623
669
|
provider: result?.provider || "unknown",
|
|
@@ -662,7 +708,9 @@ async function uninstallCommand(args) {
|
|
|
662
708
|
const pid = maintenance.owner?.pid ? `pid ${maintenance.owner.pid}` : "another process";
|
|
663
709
|
throw new Error(`another state maintenance operation is active (${pid})`);
|
|
664
710
|
}
|
|
711
|
+
let serviceLock = null;
|
|
665
712
|
try {
|
|
713
|
+
serviceLock = await acquireMachineServiceLockWithWait({ operation: "uninstall" });
|
|
666
714
|
if (currentValidation.exists) validateStateRootForRemoval(stateRoot);
|
|
667
715
|
assertNoActiveJobsForUninstall(stateRoot);
|
|
668
716
|
const autostartRemoved = await removeAutostartBestEffort(stateRoot);
|
|
@@ -679,6 +727,7 @@ async function uninstallCommand(args) {
|
|
|
679
727
|
console.log("If installed globally, remove the npm package with:");
|
|
680
728
|
console.log(" npm uninstall -g machine-bridge-mcp");
|
|
681
729
|
} finally {
|
|
730
|
+
serviceLock?.release?.();
|
|
682
731
|
maintenance?.release?.();
|
|
683
732
|
}
|
|
684
733
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import {
|
|
@@ -171,18 +172,27 @@ export function workspaceDaemonOwnsPlatformAutostart(status = {}) {
|
|
|
171
172
|
&& status.mode === "service";
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
export function inspectWorkspaceDaemon(state) {
|
|
175
|
+
export function inspectWorkspaceDaemon(state, options = {}) {
|
|
175
176
|
const owner = readDaemonLockOwner(daemonLockPathForState(state));
|
|
176
|
-
if (!owner) return { present: false, alive: false, verified_service_daemon: false };
|
|
177
|
+
if (!owner) return { present: false, alive: false, verified_service_daemon: false, startup_readiness_verified: false };
|
|
177
178
|
const alive = Boolean(owner.pid && isPidAlive(owner.pid));
|
|
178
|
-
|
|
179
|
+
let identity = alive
|
|
179
180
|
? inspectWorkspaceDaemonOwner(state, owner)
|
|
180
181
|
: { verified_service_daemon: false, reason: "stale_lock" };
|
|
182
|
+
if (identity.verified_service_daemon && options.expectedVersion
|
|
183
|
+
&& owner.version !== options.expectedVersion) {
|
|
184
|
+
identity = { verified_service_daemon: false, reason: "version_mismatch" };
|
|
185
|
+
}
|
|
186
|
+
if (identity.verified_service_daemon && options.expectedEntryScript
|
|
187
|
+
&& !sameCanonicalFile(owner.entryScript, options.expectedEntryScript)) {
|
|
188
|
+
identity = { verified_service_daemon: false, reason: "entrypoint_mismatch" };
|
|
189
|
+
}
|
|
181
190
|
return {
|
|
182
191
|
present: true,
|
|
183
192
|
alive,
|
|
184
193
|
verified_service_daemon: identity.verified_service_daemon,
|
|
185
194
|
identity_reason: identity.reason,
|
|
195
|
+
startup_readiness_verified: identity.verified_service_daemon && owner.startupReady === true,
|
|
186
196
|
...publicDaemonOwner(owner),
|
|
187
197
|
};
|
|
188
198
|
}
|
|
@@ -239,6 +249,17 @@ function sameCanonicalPath(left, right) {
|
|
|
239
249
|
}
|
|
240
250
|
}
|
|
241
251
|
|
|
252
|
+
function sameCanonicalFile(left, right) {
|
|
253
|
+
if (typeof left !== "string" || typeof right !== "string") return false;
|
|
254
|
+
try {
|
|
255
|
+
const a = realpathSync(left);
|
|
256
|
+
const b = realpathSync(right);
|
|
257
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
242
263
|
function publicDaemonOwner(owner) {
|
|
243
264
|
return {
|
|
244
265
|
pid: Number(owner?.pid) || null,
|
|
@@ -87,6 +87,7 @@ export function probeMacosDelegatedSandbox(options = {}) {
|
|
|
87
87
|
const execute = (argv) => run(MACOS_SANDBOX_EXEC, ["-p", profile, ...argv], {
|
|
88
88
|
encoding: "utf8",
|
|
89
89
|
timeout: 5_000,
|
|
90
|
+
killSignal: "SIGKILL",
|
|
90
91
|
maxBuffer: 64 * 1024,
|
|
91
92
|
windowsHide: true,
|
|
92
93
|
env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", HOME: workspace, TMPDIR: runtimeDir, LANG: "C", LC_ALL: "C" },
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { relevanceScore } from "./capability-ranking.mjs";
|
|
4
|
+
import { toolDefinition, toolNamesForPolicy } from "./policy.mjs";
|
|
5
|
+
|
|
6
|
+
const MAX_ROUTES = 6;
|
|
7
|
+
const MAX_RANKED_TOOLS = 12;
|
|
8
|
+
const MAX_RECOMMENDED_TOOLS = 18;
|
|
9
|
+
|
|
10
|
+
const ROUTES = Object.freeze([
|
|
11
|
+
route("guided-workflow", "Guided workflow", [
|
|
12
|
+
"load_local_skill", "agent_context", "list_local_skills",
|
|
13
|
+
], "Use a project or user skill when a repeatable domain workflow already exists.", [
|
|
14
|
+
"skill", "workflow", "instructions", "playbook", "guide", "技能", "工作流", "规范", "流程",
|
|
15
|
+
]),
|
|
16
|
+
route("registered-command", "Registered command", [
|
|
17
|
+
"run_local_command", "list_local_commands",
|
|
18
|
+
], "Use a fixed argv/cwd command when the repository already defines the operation.", [
|
|
19
|
+
"package script", "registered command", "repeatable command", "npm script", "构建脚本", "项目命令", "重复执行",
|
|
20
|
+
]),
|
|
21
|
+
route("shell", "Direct shell", [
|
|
22
|
+
"exec_command", "run_process",
|
|
23
|
+
], "Use Bash or direct argv for efficient ad hoc composition, investigation, and ordinary CLI work. This remains the general escape hatch and is not sandboxed.", [
|
|
24
|
+
"bash", "shell", "terminal", "cli", "command", "script", "debug", "diagnose", "benchmark", "audit", "probe",
|
|
25
|
+
"命令", "终端", "脚本", "排查", "调试", "基准", "审查", "测试", "构建",
|
|
26
|
+
]),
|
|
27
|
+
route("process-session", "Interactive process", [
|
|
28
|
+
"start_process", "read_process", "write_process", "kill_process",
|
|
29
|
+
], "Use a retained process session for interactive stdin, incremental output, servers, watchers, and REPL-style work.", [
|
|
30
|
+
"interactive", "stdin", "repl", "watch", "tail", "stream output", "dev server", "long output", "交互", "实时日志", "输入", "常驻进程",
|
|
31
|
+
]),
|
|
32
|
+
route("managed-job", "Durable managed job", [
|
|
33
|
+
"stage_job", "start_job", "read_job", "list_jobs", "cancel_job",
|
|
34
|
+
], "Use a durable job for long-running or multi-step work that must survive relay interruption and still attempt cleanup.", [
|
|
35
|
+
"background", "detached", "durable", "long running", "resume", "cleanup", "finally", "overnight", "continuous", "retry",
|
|
36
|
+
"后台", "持久", "断线", "恢复", "清理", "长时间", "持续", "重试", "多步骤",
|
|
37
|
+
]),
|
|
38
|
+
route("workspace-edit", "Workspace files", [
|
|
39
|
+
"search_text", "read_file", "list_files", "list_dir", "edit_file", "apply_patch", "write_file", "view_image",
|
|
40
|
+
], "Use bounded structured file operations for inspection and precise repository edits; combine with shell when a CLI is more efficient.", [
|
|
41
|
+
"file", "source", "code", "edit", "write", "patch", "refactor", "search", "inspect", "repository",
|
|
42
|
+
"文件", "源码", "代码", "修改", "写入", "补丁", "重构", "搜索", "仓库",
|
|
43
|
+
]),
|
|
44
|
+
route("git-review", "Git inspection", [
|
|
45
|
+
"git_status", "git_diff", "git_log", "git_show",
|
|
46
|
+
], "Use Git-specific read surfaces for bounded status, diffs, history, and revision inspection.", [
|
|
47
|
+
"git", "commit", "diff", "branch", "history", "revision", "提交", "分支", "差异", "历史", "版本",
|
|
48
|
+
]),
|
|
49
|
+
route("browser", "Existing browser profile", [
|
|
50
|
+
"browser_status", "browser_list_tabs", "browser_manage_tabs", "browser_get_source", "browser_inspect_page",
|
|
51
|
+
"browser_wait", "browser_action", "browser_fill_form", "browser_screenshot", "browser_upload_files",
|
|
52
|
+
], "Use the paired daily browser for authenticated websites, complex forms, DOM inspection, and actions that depend on the user's existing session.", [
|
|
53
|
+
"browser", "website", "web page", "tab", "dom", "form", "login", "authenticated", "chrome", "网页", "浏览器", "网站", "标签页", "表单", "登录",
|
|
54
|
+
]),
|
|
55
|
+
route("application", "Desktop application", [
|
|
56
|
+
"list_local_applications", "open_local_application", "inspect_local_application", "operate_local_application",
|
|
57
|
+
], "Use structured desktop automation for installed applications and macOS Accessibility surfaces.", [
|
|
58
|
+
"application", "desktop", "gui", "window", "accessibility", "mac app", "应用", "桌面", "界面", "窗口", "软件",
|
|
59
|
+
]),
|
|
60
|
+
route("protected-resource", "Protected local resource", [
|
|
61
|
+
"list_local_resources", "generate_ssh_key_resource",
|
|
62
|
+
], "Use registered resource aliases for secrets or files that should not be copied into MCP arguments.", [
|
|
63
|
+
"credential", "secret", "token", "private key", "ssh key", "password", "resource alias", "凭据", "密钥", "令牌", "密码", "资源别名",
|
|
64
|
+
]),
|
|
65
|
+
route("diagnostics", "Runtime diagnostics", [
|
|
66
|
+
"server_info", "project_overview", "diagnose_runtime",
|
|
67
|
+
], "Use fixed diagnostics to distinguish authorization, relay, filesystem, process, shell, and runtime failures.", [
|
|
68
|
+
"status", "health", "diagnose", "runtime", "relay", "policy", "authorization", "状态", "健康", "诊断", "运行时", "权限", "连接",
|
|
69
|
+
]),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const ROUTE_FALLBACKS = Object.freeze({
|
|
73
|
+
"guided-workflow": ["registered-command", "shell"],
|
|
74
|
+
"registered-command": ["shell", "process-session"],
|
|
75
|
+
shell: ["process-session", "managed-job"],
|
|
76
|
+
"process-session": ["shell", "managed-job"],
|
|
77
|
+
"managed-job": ["process-session", "shell"],
|
|
78
|
+
"workspace-edit": ["shell", "git-review"],
|
|
79
|
+
"git-review": ["workspace-edit", "shell"],
|
|
80
|
+
browser: ["diagnostics", "shell"],
|
|
81
|
+
application: ["diagnostics", "shell"],
|
|
82
|
+
"protected-resource": ["diagnostics"],
|
|
83
|
+
diagnostics: ["shell"],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build advisory, set-level routing for the current task. It never hides tools,
|
|
88
|
+
* changes policy, or makes shell execution conditional on the recommendation.
|
|
89
|
+
* @param {unknown} task
|
|
90
|
+
* @param {{
|
|
91
|
+
* policy?: Record<string, unknown>,
|
|
92
|
+
* seedTools?: unknown[],
|
|
93
|
+
* commandRelevant?: boolean,
|
|
94
|
+
* skillRelevant?: boolean,
|
|
95
|
+
* applicationMatches?: unknown[],
|
|
96
|
+
* browserAvailable?: boolean,
|
|
97
|
+
* }} [options]
|
|
98
|
+
*/
|
|
99
|
+
export function buildExecutionRouting(task, options = {}) {
|
|
100
|
+
const text = String(task || "");
|
|
101
|
+
const availableNames = new Set(toolNamesForPolicy(options.policy || {}));
|
|
102
|
+
const toolScores = [...availableNames]
|
|
103
|
+
.map((name) => toolDefinition(name))
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.map((tool) => ({
|
|
106
|
+
tool: String(tool.name),
|
|
107
|
+
score: relevanceScore(text, `${tool.title || ""} ${tool.description || ""}`, tool.name),
|
|
108
|
+
}))
|
|
109
|
+
.filter((item) => item.score > 0)
|
|
110
|
+
.sort((left, right) => right.score - left.score || left.tool.localeCompare(right.tool));
|
|
111
|
+
const scoreByTool = new Map(toolScores.map((item) => [item.tool, item.score]));
|
|
112
|
+
|
|
113
|
+
const scoredRoutes = ROUTES
|
|
114
|
+
.map((definition) => scoreRoute(definition, text, availableNames, scoreByTool, options))
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
|
|
117
|
+
|
|
118
|
+
if (!scoredRoutes.length) {
|
|
119
|
+
const fallback = ROUTES.find((item) => item.id === "shell" && item.tools.some((tool) => availableNames.has(tool)))
|
|
120
|
+
|| ROUTES.find((item) => item.id === "diagnostics" && item.tools.some((tool) => availableNames.has(tool)));
|
|
121
|
+
if (fallback) scoredRoutes.push(scoreRoute(fallback, text, availableNames, scoreByTool, options, 1));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const routes = scoredRoutes.slice(0, MAX_ROUTES);
|
|
125
|
+
const availableRouteIds = new Set(ROUTES
|
|
126
|
+
.filter((definition) => definition.tools.some((tool) => availableNames.has(tool)))
|
|
127
|
+
.map((definition) => definition.id));
|
|
128
|
+
const primary = routes[0] || null;
|
|
129
|
+
const second = routes[1] || null;
|
|
130
|
+
const scoreGap = primary && second ? primary.score - second.score : primary ? primary.score : 0;
|
|
131
|
+
const ambiguity = !primary
|
|
132
|
+
? "none"
|
|
133
|
+
: second && scoreGap <= 2
|
|
134
|
+
? "high"
|
|
135
|
+
: second && scoreGap <= 5
|
|
136
|
+
? "medium"
|
|
137
|
+
: "low";
|
|
138
|
+
|
|
139
|
+
const recommendedTools = unique([
|
|
140
|
+
...routes.slice(0, 3).flatMap((item) => item.tools.slice(0, 5)),
|
|
141
|
+
...(Array.isArray(options.seedTools) ? options.seedTools : []),
|
|
142
|
+
...toolScores.slice(0, MAX_RANKED_TOOLS).map((item) => item.tool),
|
|
143
|
+
]).filter((tool) => availableNames.has(tool)).slice(0, MAX_RECOMMENDED_TOOLS);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
schema_version: 1,
|
|
147
|
+
strategy: "set-level advisory routing with direct shell retained as a general escape hatch",
|
|
148
|
+
score_semantics: "deterministic relative ranking within this response; scores are not probabilities and are not comparable across versions",
|
|
149
|
+
policy_effective_tool_count: availableNames.size,
|
|
150
|
+
primary_route: primary ? publicRoute(primary, availableRouteIds) : null,
|
|
151
|
+
routes: routes.map((routeValue) => publicRoute(routeValue, availableRouteIds)),
|
|
152
|
+
ranked_tools: toolScores.slice(0, MAX_RANKED_TOOLS),
|
|
153
|
+
ambiguity: {
|
|
154
|
+
level: ambiguity,
|
|
155
|
+
score_gap: scoreGap,
|
|
156
|
+
competing_routes: ambiguity === "none" ? [] : routes.slice(0, ambiguity === "high" ? 3 : 2).map((item) => item.id),
|
|
157
|
+
},
|
|
158
|
+
recommended_tools: recommendedTools,
|
|
159
|
+
recovery_guidance: [
|
|
160
|
+
"Diagnose policy, relay, or runtime failures before changing execution surfaces.",
|
|
161
|
+
"After an ambiguous mutation failure, inspect stable state before retrying; do not assume the side effect did not occur.",
|
|
162
|
+
"A fallback route is an alternative execution surface, not permission to bypass host or effective-policy denial.",
|
|
163
|
+
],
|
|
164
|
+
enforcement: "advisory_only; the MCP host may choose any tool allowed by the effective policy",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function route(id, title, tools, guidance, keywords) {
|
|
169
|
+
return Object.freeze({ id, title, tools: Object.freeze(tools), guidance, keywords: Object.freeze(keywords) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function scoreRoute(definition, task, availableNames, scoreByTool, options, fallbackScore = 0) {
|
|
173
|
+
const tools = definition.tools.filter((tool) => availableNames.has(tool));
|
|
174
|
+
if (!tools.length) return null;
|
|
175
|
+
let score = relevanceScore(task, `${definition.title} ${definition.guidance} ${definition.keywords.join(" ")}`, definition.id);
|
|
176
|
+
const reasons = [];
|
|
177
|
+
const boost = dynamicBoost(definition.id, task, options);
|
|
178
|
+
score += boost.score;
|
|
179
|
+
reasons.push(...boost.reasons);
|
|
180
|
+
const memberScores = tools.map((tool) => scoreByTool.get(tool) || 0).sort((left, right) => right - left);
|
|
181
|
+
score += Math.min(6, (memberScores[0] || 0) + Math.floor((memberScores[1] || 0) / 2));
|
|
182
|
+
if (definition.id === "shell") {
|
|
183
|
+
score += 2;
|
|
184
|
+
reasons.push("general_escape_hatch_available");
|
|
185
|
+
}
|
|
186
|
+
score = Math.max(score, fallbackScore);
|
|
187
|
+
if (score <= 0) return null;
|
|
188
|
+
return {
|
|
189
|
+
id: definition.id,
|
|
190
|
+
title: definition.title,
|
|
191
|
+
score,
|
|
192
|
+
tools,
|
|
193
|
+
guidance: definition.guidance,
|
|
194
|
+
reasons: unique(reasons),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function dynamicBoost(id, task, options) {
|
|
199
|
+
const lower = String(task || "").toLowerCase();
|
|
200
|
+
const reasons = [];
|
|
201
|
+
let score = 0;
|
|
202
|
+
const add = (amount, reason) => { score += amount; reasons.push(reason); };
|
|
203
|
+
if (id === "guided-workflow" && options.skillRelevant === true) add(18, "relevant_skill_found");
|
|
204
|
+
if (id === "registered-command" && options.commandRelevant === true) add(20, "relevant_registered_command_found");
|
|
205
|
+
if (id === "application" && Array.isArray(options.applicationMatches) && options.applicationMatches.length > 0) add(20, "installed_application_match");
|
|
206
|
+
if (id === "browser" && options.browserAvailable === true && /browser|chrome|edge|brave|网页|浏览器|表单|网站|登录/.test(lower)) add(14, "browser_intent");
|
|
207
|
+
if (id === "managed-job" && /background|detached|durable|long[- ]?running|resume|cleanup|finally|overnight|continuous|后台|持久|断线|清理|长时间|持续|重试|多步骤/.test(lower)) add(14, "durability_or_cleanup_intent");
|
|
208
|
+
if (id === "process-session" && /interactive|stdin|repl|watch|tail|stream|dev server|交互|实时日志|输入|常驻进程/.test(lower)) add(12, "interactive_process_intent");
|
|
209
|
+
if (id === "shell" && /bash|shell|terminal|cli|command|script|debug|diagnos|benchmark|audit|probe|命令|终端|脚本|排查|调试|基准|审查|测试|构建/.test(lower)) add(10, "shell_or_cli_intent");
|
|
210
|
+
if (id === "workspace-edit" && /file|source|code|edit|write|patch|refactor|repository|文件|源码|代码|修改|写入|补丁|重构|仓库/.test(lower)) add(10, "workspace_change_intent");
|
|
211
|
+
if (id === "git-review" && /git|commit|diff|branch|history|revision|提交|分支|差异|历史|版本/.test(lower)) add(12, "git_intent");
|
|
212
|
+
if (id === "protected-resource" && /credential|secret|token|private key|ssh key|password|凭据|密钥|令牌|密码/.test(lower)) add(14, "protected_data_intent");
|
|
213
|
+
if (id === "diagnostics" && /status|health|diagnos|runtime|relay|policy|authorization|状态|健康|诊断|运行时|权限|连接/.test(lower)) add(10, "runtime_diagnostic_intent");
|
|
214
|
+
return { score, reasons };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function publicRoute(routeValue, availableRouteIds) {
|
|
218
|
+
return {
|
|
219
|
+
id: routeValue.id,
|
|
220
|
+
title: routeValue.title,
|
|
221
|
+
score: routeValue.score,
|
|
222
|
+
tools: [...routeValue.tools],
|
|
223
|
+
guidance: routeValue.guidance,
|
|
224
|
+
reasons: [...routeValue.reasons],
|
|
225
|
+
fallback_routes: (ROUTE_FALLBACKS[routeValue.id] || []).filter((id) => availableRouteIds.has(id)),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function unique(values) {
|
|
230
|
+
return [...new Set(values.map((value) => String(value || "")).filter(Boolean))];
|
|
231
|
+
}
|
|
@@ -3,6 +3,8 @@ import { dirname, isAbsolute, relative, sep } from "node:path";
|
|
|
3
3
|
import { BridgeError } from "./errors.mjs";
|
|
4
4
|
import { clampInteger } from "./numbers.mjs";
|
|
5
5
|
|
|
6
|
+
export const GIT_METADATA_TIMEOUT_MS = 30_000;
|
|
7
|
+
|
|
6
8
|
export class GitService {
|
|
7
9
|
constructor({ resolveExistingPath, displayPath, runInternalProcess, gitExecutable, maximumBytes }) {
|
|
8
10
|
this.resolveExistingPath = resolveExistingPath;
|
|
@@ -64,7 +66,7 @@ export class GitService {
|
|
|
64
66
|
const target = await this.resolveExistingPath(inputPath, context);
|
|
65
67
|
const info = await stat(target);
|
|
66
68
|
const cwd = info.isDirectory() ? target : dirname(target);
|
|
67
|
-
const result = await this.runInternalProcess(this.gitExecutable(), ["-c", "core.fsmonitor=false", "-C", cwd, "rev-parse", "--show-toplevel"],
|
|
69
|
+
const result = await this.runInternalProcess(this.gitExecutable(), ["-c", "core.fsmonitor=false", "-C", cwd, "rev-parse", "--show-toplevel"], GIT_METADATA_TIMEOUT_MS, true, 512 * 1024, context);
|
|
68
70
|
if (result.code !== 0) return { ok: false, result, target };
|
|
69
71
|
const root = result.stdout.trim();
|
|
70
72
|
const repoRelative = relative(root, target);
|
package/src/local/job-runner.mjs
CHANGED
|
@@ -4,12 +4,15 @@ import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs
|
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
6
6
|
import { executionEnv } from "./shell.mjs";
|
|
7
|
+
import { childExitedBeforeTimeout, createChildProcessSettlement } from "./child-process-settlement.mjs";
|
|
7
8
|
import { terminateProcessTreeWithEscalation } from "./process-tree.mjs";
|
|
8
|
-
import {
|
|
9
|
+
import { removeOwnedJsonFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
9
10
|
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
10
|
-
import { currentProcessStartTimeMs } from "./process-identity.mjs";
|
|
11
|
+
import { currentProcessStartTimeMs, processState } from "./process-identity.mjs";
|
|
11
12
|
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
12
13
|
import { persistManagedJobTerminal } from "./managed-job-terminal.mjs";
|
|
14
|
+
import { sanitizeLogText } from "./log.mjs";
|
|
15
|
+
import { confirmRunnerClaim } from "./managed-job-runner-claim.mjs";
|
|
13
16
|
|
|
14
17
|
const RESOURCE_TOKEN = /\{\{resource:([a-z][a-z0-9._-]{0,63})\}\}/g;
|
|
15
18
|
const TEMP_TOKEN = /\{\{temp:([a-z][a-z0-9._-]{0,63})\}\}/g;
|
|
@@ -27,7 +30,9 @@ const jobDir = resolve(jobDirInput);
|
|
|
27
30
|
if (!JOB_ID.test(basename(jobDir))) throw new Error("--job-dir must name a managed job directory");
|
|
28
31
|
const recover = options.recover === true;
|
|
29
32
|
const recoveryLockToken = typeof process.env.MBM_RECOVERY_LOCK_TOKEN === "string" ? process.env.MBM_RECOVERY_LOCK_TOKEN : "";
|
|
33
|
+
const launchToken = typeof process.env.MBM_RUNNER_LAUNCH_TOKEN === "string" ? process.env.MBM_RUNNER_LAUNCH_TOKEN : "";
|
|
30
34
|
delete process.env.MBM_RECOVERY_LOCK_TOKEN;
|
|
35
|
+
delete process.env.MBM_RUNNER_LAUNCH_TOKEN;
|
|
31
36
|
const planFile = join(jobDir, "plan.json");
|
|
32
37
|
const statusFile = join(jobDir, "status.json");
|
|
33
38
|
const resultFile = join(jobDir, "result.json");
|
|
@@ -57,7 +62,9 @@ for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
|
57
62
|
const initial = readJson(statusFile, MAX_STATUS_BYTES);
|
|
58
63
|
assertLaunchState(initial);
|
|
59
64
|
try {
|
|
60
|
-
|
|
65
|
+
await confirmRunnerClaim({
|
|
66
|
+
file: runnerPidFile, pid: process.pid, processStartedAt: RUNNER_PROCESS_STARTED_AT, launchToken,
|
|
67
|
+
});
|
|
61
68
|
if (recover) await releaseRecoveryClaim();
|
|
62
69
|
const plan = readJson(planFile, 1024 * 1024);
|
|
63
70
|
assertPlanIntegrity(plan, initial);
|
|
@@ -67,6 +74,7 @@ try {
|
|
|
67
74
|
process.exitCode = 1;
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
|
|
70
78
|
async function releaseRecoveryClaim() {
|
|
71
79
|
if (!/^[a-f0-9]{32}$/.test(recoveryLockToken)) throw new Error("recovery runner is missing its ownership token");
|
|
72
80
|
const file = join(jobDir, "recovery.lock");
|
|
@@ -177,6 +185,9 @@ function assertPlanIntegrity(plan, status) {
|
|
|
177
185
|
|
|
178
186
|
function recordFatalRunnerError(error) {
|
|
179
187
|
const now = new Date().toISOString();
|
|
188
|
+
try {
|
|
189
|
+
process.stderr.write(`managed job runner fatal: error_class=${classifyError(error)} message=${sanitizeLogText(error?.message || error, 512)}\n`);
|
|
190
|
+
} catch {}
|
|
180
191
|
let status = {};
|
|
181
192
|
try { status = readJson(statusFile, MAX_STATUS_BYTES); } catch {}
|
|
182
193
|
const finalStatus = recover ? "recovery_failed" : "runner_failed";
|
|
@@ -286,6 +297,14 @@ function spawnStep(argv, { cwd, env, input, timeoutMs, cancellationAware, captur
|
|
|
286
297
|
let closed = false;
|
|
287
298
|
let killTimer = null;
|
|
288
299
|
const timer = setTimeout(() => {
|
|
300
|
+
if (childExitedBeforeTimeout({
|
|
301
|
+
exitCode: child.exitCode,
|
|
302
|
+
signalCode: child.signalCode,
|
|
303
|
+
processState: processState(child.pid),
|
|
304
|
+
})) {
|
|
305
|
+
settlement.onExit(child.exitCode, child.signalCode);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
289
308
|
timedOut = true;
|
|
290
309
|
killTimer = terminateProcessTreeWithEscalation(child);
|
|
291
310
|
}, timeoutMs);
|
|
@@ -308,28 +327,45 @@ function spawnStep(argv, { cwd, env, input, timeoutMs, cancellationAware, captur
|
|
|
308
327
|
stderr = next.buffer;
|
|
309
328
|
stderrTruncated += next.truncated;
|
|
310
329
|
});
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
330
|
+
const settlement = createChildProcessSettlement({
|
|
331
|
+
readExitState: () => ({ code: child.exitCode, signal: child.signalCode }),
|
|
332
|
+
onFallback() {
|
|
333
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) {
|
|
334
|
+
try { stream?.destroy?.(); } catch {}
|
|
335
|
+
}
|
|
336
|
+
try { child.unref(); } catch {}
|
|
337
|
+
},
|
|
338
|
+
onSettle(code, signal) {
|
|
339
|
+
finish(() => {
|
|
340
|
+
if (cancellationAware && isCancellationRequested()) {
|
|
341
|
+
rejectPromise(new JobCancelledError());
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
resolvePromise({
|
|
345
|
+
code: Number.isInteger(code) ? code : 1,
|
|
346
|
+
signal: signal ? String(signal) : null,
|
|
347
|
+
timedOut,
|
|
348
|
+
stdout,
|
|
349
|
+
stderr,
|
|
350
|
+
stdoutTruncated,
|
|
351
|
+
stderrTruncated,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
child.on("error", (error) => {
|
|
357
|
+
settlement.cancel();
|
|
358
|
+
finish(() => rejectPromise(error));
|
|
359
|
+
});
|
|
360
|
+
child.on("exit", (code, signal) => settlement.onExit(code, signal));
|
|
361
|
+
child.on("close", (code, signal) => settlement.onClose(code, signal));
|
|
327
362
|
if (input && input.length) child.stdin.end(input);
|
|
328
363
|
else child.stdin.end();
|
|
329
364
|
|
|
330
365
|
function finish(callback) {
|
|
331
366
|
if (closed) return;
|
|
332
367
|
closed = true;
|
|
368
|
+
settlement.cancel();
|
|
333
369
|
clearTimeout(timer);
|
|
334
370
|
if (killTimer && !timedOut) clearTimeout(killTimer);
|
|
335
371
|
clearInterval(cancellationPoll);
|
|
@@ -216,6 +216,7 @@ export function buildDevelopmentTrustBrokerBinary(profileDir, options = {}) {
|
|
|
216
216
|
], {
|
|
217
217
|
encoding: "utf8",
|
|
218
218
|
timeout: 120_000,
|
|
219
|
+
killSignal: "SIGKILL",
|
|
219
220
|
maxBuffer: MAX_OUTPUT_BYTES,
|
|
220
221
|
env: minimalEnvironment(),
|
|
221
222
|
});
|
|
@@ -225,6 +226,7 @@ export function buildDevelopmentTrustBrokerBinary(profileDir, options = {}) {
|
|
|
225
226
|
const sign = (options.spawnSync || spawnSync)("/usr/bin/codesign", ["--force", "--sign", "-", temporary], {
|
|
226
227
|
encoding: "utf8",
|
|
227
228
|
timeout: 30_000,
|
|
229
|
+
killSignal: "SIGKILL",
|
|
228
230
|
maxBuffer: MAX_OUTPUT_BYTES,
|
|
229
231
|
env: minimalEnvironment(),
|
|
230
232
|
});
|
|
@@ -257,12 +259,16 @@ function runBroker(binary, args, { input, timeoutMs }, options = {}) {
|
|
|
257
259
|
input,
|
|
258
260
|
encoding: "utf8",
|
|
259
261
|
timeout: timeoutMs,
|
|
262
|
+
killSignal: "SIGKILL",
|
|
260
263
|
maxBuffer: MAX_OUTPUT_BYTES,
|
|
261
264
|
env: minimalEnvironment(),
|
|
262
265
|
windowsHide: true,
|
|
263
266
|
});
|
|
264
267
|
if (result.status !== 0 || result.error) {
|
|
265
268
|
const diagnostic = boundedDiagnostic(result.stderr || result.error?.message || result.signal || `exit ${result.status}`);
|
|
269
|
+
if (result.error?.code === "ETIMEDOUT") {
|
|
270
|
+
throw new MacosTrustBrokerUnavailableError("macOS trust broker timed out");
|
|
271
|
+
}
|
|
266
272
|
if (/-34018|missing entitlement|required entitlement/i.test(diagnostic)) {
|
|
267
273
|
throw new MacosTrustBrokerUnavailableError(
|
|
268
274
|
"macOS trust broker lacks a provisioning-profile-validated data-protection Keychain entitlement",
|
|
@@ -297,6 +303,7 @@ function codesignOptions() {
|
|
|
297
303
|
return {
|
|
298
304
|
encoding: "utf8",
|
|
299
305
|
timeout: 30_000,
|
|
306
|
+
killSignal: "SIGKILL",
|
|
300
307
|
maxBuffer: MAX_OUTPUT_BYTES,
|
|
301
308
|
env: minimalEnvironment(),
|
|
302
309
|
};
|