machine-bridge-mcp 1.2.6 → 1.2.8
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 +21 -0
- package/CONTRIBUTING.md +26 -19
- package/README.md +15 -4
- package/SECURITY.md +3 -3
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +10 -8
- package/docs/AUDIT.md +26 -0
- package/docs/ENGINEERING.md +2 -2
- package/docs/OPERATIONS.md +5 -3
- package/docs/PROJECT_STANDARDS.md +9 -5
- package/docs/RELEASING.md +64 -32
- package/docs/TESTING.md +7 -4
- package/package.json +11 -6
- package/scripts/github-push.mjs +49 -0
- package/scripts/github-release.mjs +15 -8
- package/scripts/local-release-acceptance.mjs +136 -0
- package/scripts/release-acceptance.mjs +169 -0
- package/scripts/release-impact-check.mjs +19 -3
- package/src/local/browser-bridge.mjs +67 -22
- package/src/local/cli-local-admin.mjs +2 -3
- package/src/local/daemon-process.mjs +39 -4
- package/src/local/execution-limits.mjs +36 -0
- package/src/local/job-runner.mjs +5 -8
- package/src/local/loopback-health.mjs +67 -0
- package/src/local/managed-jobs.mjs +22 -9
- package/src/local/process-contract.mjs +19 -0
- package/src/local/process-execution.mjs +11 -10
- package/src/local/process-sessions.mjs +25 -50
- package/src/local/process-tracker.mjs +1 -1
- package/src/local/process-tree.mjs +53 -0
- package/src/local/runtime-reporting.mjs +3 -1
- package/src/local/runtime.mjs +4 -5
- package/src/local/shell.mjs +2 -19
- package/src/worker/index.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { closeSync, constants as fsConstants, existsSync, ftruncateSync, lstatSync, readSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { assertStateMaintenanceAvailable, ensureOwnerOnlyDir, ownerOnlyFile } from "./state.mjs";
|
|
7
7
|
import { createExclusiveFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
@@ -12,6 +12,7 @@ import { BridgeError } from "./errors.mjs";
|
|
|
12
12
|
import { inspectResourceFile, normalizeResourceRegistry, validatePlan } from "./managed-job-plan.mjs";
|
|
13
13
|
export { inspectResourceFile, publicResourceRegistry, validateResourceName } from "./managed-job-plan.mjs";
|
|
14
14
|
import { clampInteger } from "./numbers.mjs";
|
|
15
|
+
import { classifyOperationalError } from "./log.mjs";
|
|
15
16
|
|
|
16
17
|
const JOB_ID = /^job_[A-Za-z0-9_-]{24,}$/;
|
|
17
18
|
const MAX_JOBS = 50;
|
|
@@ -124,7 +125,7 @@ export class ManagedJobManager {
|
|
|
124
125
|
status.cleanup_guarantee = "best-effort-finally-and-recovery";
|
|
125
126
|
atomicWriteJson(statusFile, status, 256 * 1024);
|
|
126
127
|
try {
|
|
127
|
-
launchRunner(dir);
|
|
128
|
+
launchRunner(dir, false, "", { logger: this.logger });
|
|
128
129
|
} catch (error) {
|
|
129
130
|
failRunnerLaunch(dir, status, error);
|
|
130
131
|
throw error;
|
|
@@ -181,7 +182,7 @@ export class ManagedJobManager {
|
|
|
181
182
|
atomicWriteJson(join(dir, "status.json"), status, 256 * 1024);
|
|
182
183
|
if (launch) {
|
|
183
184
|
try {
|
|
184
|
-
launchRunner(dir);
|
|
185
|
+
launchRunner(dir, false, "", { logger: this.logger });
|
|
185
186
|
} catch (error) {
|
|
186
187
|
failRunnerLaunch(dir, status, error);
|
|
187
188
|
throw error;
|
|
@@ -356,7 +357,7 @@ export class ManagedJobManager {
|
|
|
356
357
|
markRecoveryExhausted(dir, file, status, recoveryAttempts);
|
|
357
358
|
return;
|
|
358
359
|
}
|
|
359
|
-
const runnerPid = relaunchInterruptedJob(dir, file, status, recoveryAttempts, recoveryLock.token);
|
|
360
|
+
const runnerPid = relaunchInterruptedJob(dir, file, status, recoveryAttempts, recoveryLock.token, this.logger);
|
|
360
361
|
recoveryLock.handoff(runnerPid);
|
|
361
362
|
handedOff = true;
|
|
362
363
|
} finally {
|
|
@@ -513,7 +514,7 @@ function markRecoveryExhausted(dir, statusFile, status, recoveryAttempts) {
|
|
|
513
514
|
scrubFinishedPlan(dir, status);
|
|
514
515
|
}
|
|
515
516
|
|
|
516
|
-
function relaunchInterruptedJob(dir, statusFile, status, recoveryAttempts, recoveryToken) {
|
|
517
|
+
function relaunchInterruptedJob(dir, statusFile, status, recoveryAttempts, recoveryToken, logger) {
|
|
517
518
|
status.status = "interrupted";
|
|
518
519
|
status.updated_at = new Date().toISOString();
|
|
519
520
|
status.finished_at = status.updated_at;
|
|
@@ -522,7 +523,7 @@ function relaunchInterruptedJob(dir, statusFile, status, recoveryAttempts, recov
|
|
|
522
523
|
atomicWriteJson(statusFile, status, 256 * 1024);
|
|
523
524
|
rmSync(join(dir, "runtime"), { recursive: true, force: true });
|
|
524
525
|
rmSync(join(dir, "runner.pid"), { force: true });
|
|
525
|
-
return launchRunner(dir, true, recoveryToken);
|
|
526
|
+
return launchRunner(dir, true, recoveryToken, { logger });
|
|
526
527
|
}
|
|
527
528
|
|
|
528
529
|
function acquireRecoveryLock(dir) {
|
|
@@ -634,7 +635,7 @@ function replacePrivateTextFile(file, content) {
|
|
|
634
635
|
ownerOnlyFile(file);
|
|
635
636
|
}
|
|
636
637
|
|
|
637
|
-
function launchRunner(dir, recover = false, recoveryToken = "") {
|
|
638
|
+
export function launchRunner(dir, recover = false, recoveryToken = "", options = {}) {
|
|
638
639
|
const args = [RUNNER_PATH, "--job-dir", dir];
|
|
639
640
|
if (recover) args.push("--recover");
|
|
640
641
|
const stdoutFile = join(dir, "runner.out.log");
|
|
@@ -647,10 +648,12 @@ function launchRunner(dir, recover = false, recoveryToken = "") {
|
|
|
647
648
|
try {
|
|
648
649
|
stdoutFd = openPrivateAppendFile(stdoutFile);
|
|
649
650
|
stderrFd = openPrivateAppendFile(stderrFile);
|
|
650
|
-
|
|
651
|
+
const spawnProcess = typeof options.spawnProcess === "function" ? options.spawnProcess : spawn;
|
|
652
|
+
child = spawnProcess(process.execPath, args, {
|
|
651
653
|
detached: true,
|
|
652
654
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
653
655
|
windowsHide: true,
|
|
656
|
+
shell: false,
|
|
654
657
|
env: recoveryToken ? { ...process.env, MBM_RECOVERY_LOCK_TOKEN: recoveryToken } : process.env,
|
|
655
658
|
});
|
|
656
659
|
} finally {
|
|
@@ -659,8 +662,18 @@ function launchRunner(dir, recover = false, recoveryToken = "") {
|
|
|
659
662
|
}
|
|
660
663
|
ownerOnlyFile(stdoutFile);
|
|
661
664
|
ownerOnlyFile(stderrFile);
|
|
665
|
+
const logger = options.logger || console;
|
|
666
|
+
child.once?.("error", (error) => {
|
|
667
|
+
logger.error?.("managed job runner process reported an asynchronous failure", {
|
|
668
|
+
job_id: basename(dir),
|
|
669
|
+
recovery: recover,
|
|
670
|
+
error_class: classifyOperationalError(error),
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
const pid = Number(child.pid);
|
|
674
|
+
if (!Number.isInteger(pid) || pid <= 0) throw new Error("managed job runner did not receive a process id");
|
|
662
675
|
child.unref();
|
|
663
|
-
return
|
|
676
|
+
return pid;
|
|
664
677
|
}
|
|
665
678
|
|
|
666
679
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const MAX_COMMAND_BYTES = 64 * 1024;
|
|
2
|
+
export const MAX_ARGV_ITEMS = 256;
|
|
3
|
+
|
|
4
|
+
export function validateArgv(value) {
|
|
5
|
+
if (!Array.isArray(value) || !value.length || value.length > MAX_ARGV_ITEMS) {
|
|
6
|
+
throw new Error(`argv must contain 1-${MAX_ARGV_ITEMS} strings`);
|
|
7
|
+
}
|
|
8
|
+
const argv = value.map((item) => {
|
|
9
|
+
if (typeof item !== "string" || item.includes("\0")) {
|
|
10
|
+
throw new Error("argv entries must be strings without NUL bytes");
|
|
11
|
+
}
|
|
12
|
+
return item;
|
|
13
|
+
});
|
|
14
|
+
if (!argv[0]) throw new Error("argv[0] must not be empty");
|
|
15
|
+
if (Buffer.byteLength(JSON.stringify(argv)) > MAX_COMMAND_BYTES) {
|
|
16
|
+
throw new Error(`argv exceeds maximum size (${MAX_COMMAND_BYTES} bytes)`);
|
|
17
|
+
}
|
|
18
|
+
return argv;
|
|
19
|
+
}
|
|
@@ -2,12 +2,13 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
3
|
import { BoundedOutput } from "./bounded-output.mjs";
|
|
4
4
|
import { executionEnv, workspaceShellCommand } from "./shell.mjs";
|
|
5
|
-
import { MAX_COMMAND_BYTES,
|
|
5
|
+
import { MAX_COMMAND_BYTES, validateArgv } from "./process-contract.mjs";
|
|
6
|
+
import { terminateProcessTreeWithEscalation } from "./process-tree.mjs";
|
|
6
7
|
import { BridgeError } from "./errors.mjs";
|
|
7
8
|
import { clampInteger } from "./numbers.mjs";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_PROCESS_OUTPUT_BYTES, MAX_PROCESS_STDIN_BYTES, MAX_PROCESS_TIMEOUT_SECONDS, MIN_PROCESS_TIMEOUT_SECONDS,
|
|
11
|
+
} from "./execution-limits.mjs";
|
|
11
12
|
|
|
12
13
|
function spawnDirectProcess(command, args, options) {
|
|
13
14
|
// Keep the production child_process API call structurally separate from the
|
|
@@ -41,7 +42,7 @@ export class ProcessExecutionService {
|
|
|
41
42
|
const argv = validateArgv(args.argv);
|
|
42
43
|
const cwd = await this.resolveExistingPath(args.cwd || ".");
|
|
43
44
|
if (!(await stat(cwd)).isDirectory()) throw new BridgeError("invalid_request", "cwd is not a directory");
|
|
44
|
-
return this.run(argv[0], argv.slice(1), clampInteger(args.timeout_seconds, 120,
|
|
45
|
+
return this.run(argv[0], argv.slice(1), clampInteger(args.timeout_seconds, 120, MIN_PROCESS_TIMEOUT_SECONDS, MAX_PROCESS_TIMEOUT_SECONDS) * 1000, false, DEFAULT_PROCESS_OUTPUT_BYTES, context, cwd);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
async runRegistered(args, context = {}) {
|
|
@@ -52,9 +53,9 @@ export class ProcessExecutionService {
|
|
|
52
53
|
if (!(await stat(cwd)).isDirectory()) throw new BridgeError("invalid_request", "registered command cwd is not a directory");
|
|
53
54
|
const requested = args.timeout_seconds === undefined
|
|
54
55
|
? command.timeoutSeconds
|
|
55
|
-
: clampInteger(args.timeout_seconds, command.timeoutSeconds,
|
|
56
|
+
: clampInteger(args.timeout_seconds, command.timeoutSeconds, MIN_PROCESS_TIMEOUT_SECONDS, MAX_PROCESS_TIMEOUT_SECONDS);
|
|
56
57
|
const timeoutSeconds = Math.min(requested, command.timeoutSeconds);
|
|
57
|
-
const result = await this.run(argv[0], argv.slice(1), timeoutSeconds * 1000, false,
|
|
58
|
+
const result = await this.run(argv[0], argv.slice(1), timeoutSeconds * 1000, false, DEFAULT_PROCESS_OUTPUT_BYTES, context, cwd);
|
|
58
59
|
return { name: command.name, cwd: this.displayPath(cwd), timeout_seconds: timeoutSeconds, ...result };
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -69,16 +70,16 @@ export class ProcessExecutionService {
|
|
|
69
70
|
if (command.includes("\0")) throw new BridgeError("invalid_request", "command contains a NUL byte");
|
|
70
71
|
if (Buffer.byteLength(command) > MAX_COMMAND_BYTES) throw new BridgeError("limit_exceeded", `command exceeds maximum size (${MAX_COMMAND_BYTES} bytes)`);
|
|
71
72
|
const shell = workspaceShellCommand(command);
|
|
72
|
-
return this.run(shell.cmd, shell.args, clampInteger(timeoutSeconds, 120,
|
|
73
|
+
return this.run(shell.cmd, shell.args, clampInteger(timeoutSeconds, 120, MIN_PROCESS_TIMEOUT_SECONDS, MAX_PROCESS_TIMEOUT_SECONDS) * 1000, false, DEFAULT_PROCESS_OUTPUT_BYTES, context);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
terminateAll(signal = "SIGTERM", escalate = false) {
|
|
76
77
|
this.processTracker.terminateAll(signal, escalate);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
async run(cmd, args, timeoutMs, allowFailure = false, maxOutputBytes =
|
|
80
|
+
async run(cmd, args, timeoutMs, allowFailure = false, maxOutputBytes = DEFAULT_PROCESS_OUTPUT_BYTES, context = {}, cwd = this.workspace, stdin = null) {
|
|
80
81
|
this.throwIfCancelled(context);
|
|
81
|
-
if (stdin !== null && Buffer.byteLength(String(stdin)) >
|
|
82
|
+
if (stdin !== null && Buffer.byteLength(String(stdin)) > MAX_PROCESS_STDIN_BYTES) {
|
|
82
83
|
throw new BridgeError("limit_exceeded", "process stdin exceeds 1 MiB");
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -2,16 +2,14 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { executionEnv } from "./shell.mjs";
|
|
5
|
+
import { validateArgv } from "./process-contract.mjs";
|
|
6
|
+
import { terminateProcessTree, terminateProcessTreeWithEscalation } from "./process-tree.mjs";
|
|
5
7
|
import { createToolAuthorizer } from "./policy.mjs";
|
|
6
8
|
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
7
9
|
import { clampInteger } from "./numbers.mjs";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const MAX_PROCESS_SESSIONS = 8;
|
|
12
|
-
const MAX_SESSION_OUTPUT_BYTES = 1024 * 1024;
|
|
13
|
-
const MAX_PROCESS_STDIN_BYTES = 64 * 1024;
|
|
14
|
-
const PROCESS_SESSION_RETENTION_MS = 30 * 60 * 1000;
|
|
10
|
+
import {
|
|
11
|
+
MAX_PROCESS_SESSIONS, MAX_PROCESS_SESSION_OUTPUT_BYTES, MAX_PROCESS_SESSION_STDIN_BYTES, PROCESS_SESSION_RETENTION_MS,
|
|
12
|
+
} from "./execution-limits.mjs";
|
|
15
13
|
|
|
16
14
|
export class ProcessSessionManager {
|
|
17
15
|
constructor({ workspace, policy, authorizeTool = null, runtimeDir, processTracker, resolveCwd, displayPath, throwIfCancelled }) {
|
|
@@ -70,6 +68,7 @@ export class ProcessSessionManager {
|
|
|
70
68
|
signal: null,
|
|
71
69
|
stdinClosed: false,
|
|
72
70
|
waiters: new Set(),
|
|
71
|
+
terminationTimer: null,
|
|
73
72
|
};
|
|
74
73
|
this.sessions.set(session.id, session);
|
|
75
74
|
this.trackChild(child, context.callId);
|
|
@@ -94,6 +93,11 @@ export class ProcessSessionManager {
|
|
|
94
93
|
notifySessionWaiters(session);
|
|
95
94
|
});
|
|
96
95
|
|
|
96
|
+
child.on("error", (error) => {
|
|
97
|
+
appendSessionStream(session.stderr, Buffer.from(`${boundedErrorMessage(error)}\n`));
|
|
98
|
+
session.lastActivity = Date.now();
|
|
99
|
+
notifySessionWaiters(session);
|
|
100
|
+
});
|
|
97
101
|
try {
|
|
98
102
|
await waitForSpawn(child);
|
|
99
103
|
} catch (error) {
|
|
@@ -101,12 +105,6 @@ export class ProcessSessionManager {
|
|
|
101
105
|
this.untrackChild(child);
|
|
102
106
|
throw error;
|
|
103
107
|
}
|
|
104
|
-
|
|
105
|
-
child.on("error", (error) => {
|
|
106
|
-
appendSessionStream(session.stderr, Buffer.from(`${boundedErrorMessage(error)}\n`));
|
|
107
|
-
session.lastActivity = Date.now();
|
|
108
|
-
notifySessionWaiters(session);
|
|
109
|
-
});
|
|
110
108
|
this.throwIfCancelled(context);
|
|
111
109
|
return this.summary(session);
|
|
112
110
|
}
|
|
@@ -144,7 +142,7 @@ export class ProcessSessionManager {
|
|
|
144
142
|
const session = this.get(args.session_id);
|
|
145
143
|
if (session.closedAt !== null) throw new Error("process session has already exited");
|
|
146
144
|
const data = String(args.data ?? "");
|
|
147
|
-
if (Buffer.byteLength(data) >
|
|
145
|
+
if (Buffer.byteLength(data) > MAX_PROCESS_SESSION_STDIN_BYTES) throw new Error(`stdin data exceeds maximum size (${MAX_PROCESS_SESSION_STDIN_BYTES} bytes)`);
|
|
148
146
|
if (session.stdinClosed || session.child.stdin.destroyed) throw new Error("process session stdin is closed");
|
|
149
147
|
this.throwIfCancelled(context);
|
|
150
148
|
if (data) {
|
|
@@ -165,9 +163,18 @@ export class ProcessSessionManager {
|
|
|
165
163
|
const session = this.get(args.session_id);
|
|
166
164
|
this.throwIfCancelled(context);
|
|
167
165
|
const wasRunning = session.closedAt === null;
|
|
168
|
-
|
|
166
|
+
const force = args.force === true;
|
|
167
|
+
if (wasRunning && force) terminateProcessTree(session.child, "SIGKILL");
|
|
168
|
+
else if (wasRunning && !session.terminationTimer) {
|
|
169
|
+
session.terminationTimer = terminateProcessTreeWithEscalation(session.child);
|
|
170
|
+
}
|
|
169
171
|
session.lastActivity = Date.now();
|
|
170
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
...this.summary(session),
|
|
174
|
+
termination_requested: wasRunning,
|
|
175
|
+
force,
|
|
176
|
+
force_after_ms: wasRunning && !force ? 2000 : null,
|
|
177
|
+
};
|
|
171
178
|
}
|
|
172
179
|
|
|
173
180
|
get(sessionId) {
|
|
@@ -220,38 +227,6 @@ export class ProcessSessionManager {
|
|
|
220
227
|
}
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
export function validateArgv(value) {
|
|
224
|
-
if (!Array.isArray(value) || !value.length || value.length > MAX_ARGV_ITEMS) throw new Error(`argv must contain 1-${MAX_ARGV_ITEMS} strings`);
|
|
225
|
-
const argv = value.map((item) => {
|
|
226
|
-
if (typeof item !== "string" || item.includes("\0")) throw new Error("argv entries must be strings without NUL bytes");
|
|
227
|
-
return item;
|
|
228
|
-
});
|
|
229
|
-
if (!argv[0]) throw new Error("argv[0] must not be empty");
|
|
230
|
-
if (Buffer.byteLength(JSON.stringify(argv)) > MAX_COMMAND_BYTES) throw new Error(`argv exceeds maximum size (${MAX_COMMAND_BYTES} bytes)`);
|
|
231
|
-
return argv;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function terminateProcessTree(child, signal) {
|
|
235
|
-
if (!child?.pid) return;
|
|
236
|
-
if (process.platform === "win32") {
|
|
237
|
-
try {
|
|
238
|
-
const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
239
|
-
killer.unref();
|
|
240
|
-
return;
|
|
241
|
-
} catch {}
|
|
242
|
-
}
|
|
243
|
-
try { process.kill(-child.pid, signal); } catch {
|
|
244
|
-
try { child.kill(signal); } catch {}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function terminateProcessTreeWithEscalation(child, options = {}) {
|
|
249
|
-
const graceMs = Number.isFinite(Number(options.graceMs)) ? Math.max(0, Number(options.graceMs)) : 2000;
|
|
250
|
-
const schedule = typeof options.setTimeout === "function" ? options.setTimeout : setTimeout;
|
|
251
|
-
terminateProcessTree(child, "SIGTERM");
|
|
252
|
-
return schedule(() => terminateProcessTree(child, "SIGKILL"), graceMs);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
230
|
function waitForSpawn(child) {
|
|
256
231
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
257
232
|
const onSpawn = () => { cleanup(); resolvePromise(); };
|
|
@@ -273,8 +248,8 @@ function appendSessionStream(stream, chunk) {
|
|
|
273
248
|
const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
|
|
274
249
|
stream.totalBytes += input.length;
|
|
275
250
|
let combined = stream.buffer.length ? Buffer.concat([stream.buffer, input]) : Buffer.from(input);
|
|
276
|
-
if (combined.length >
|
|
277
|
-
const dropped = combined.length -
|
|
251
|
+
if (combined.length > MAX_PROCESS_SESSION_OUTPUT_BYTES) {
|
|
252
|
+
const dropped = combined.length - MAX_PROCESS_SESSION_OUTPUT_BYTES;
|
|
278
253
|
combined = combined.subarray(dropped);
|
|
279
254
|
stream.baseOffset += dropped;
|
|
280
255
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PROCESS_TERMINATION_GRACE_MS = 2000;
|
|
4
|
+
|
|
5
|
+
export function terminateProcessTree(child, signal = "SIGTERM", options = {}) {
|
|
6
|
+
if (!child?.pid) return false;
|
|
7
|
+
const platform = options.platform || process.platform;
|
|
8
|
+
if (platform === "win32") {
|
|
9
|
+
try {
|
|
10
|
+
const spawnProcess = typeof options.spawnProcess === "function" ? options.spawnProcess : spawn;
|
|
11
|
+
const force = signal === "SIGKILL";
|
|
12
|
+
const killer = spawnProcess("taskkill.exe", ["/PID", String(child.pid), "/T", ...(force ? ["/F"] : [])], {
|
|
13
|
+
stdio: "ignore",
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
shell: false,
|
|
16
|
+
});
|
|
17
|
+
const fallback = () => {
|
|
18
|
+
try { child.kill(signal); } catch {}
|
|
19
|
+
};
|
|
20
|
+
killer?.once?.("error", fallback);
|
|
21
|
+
killer?.once?.("exit", (code) => { if (code !== 0) fallback(); });
|
|
22
|
+
killer?.unref?.();
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
// Fall through to ChildProcess.kill for environments without taskkill.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const killProcess = typeof options.killProcess === "function" ? options.killProcess : process.kill.bind(process);
|
|
30
|
+
try {
|
|
31
|
+
killProcess(-child.pid, signal);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
try {
|
|
35
|
+
return child.kill(signal) !== false;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function terminateProcessTreeWithEscalation(child, options = {}) {
|
|
43
|
+
const graceMs = Number.isFinite(Number(options.graceMs))
|
|
44
|
+
? Math.max(0, Number(options.graceMs))
|
|
45
|
+
: DEFAULT_PROCESS_TERMINATION_GRACE_MS;
|
|
46
|
+
const schedule = typeof options.setTimeout === "function" ? options.setTimeout : setTimeout;
|
|
47
|
+
const terminate = typeof options.terminate === "function" ? options.terminate : terminateProcessTree;
|
|
48
|
+
terminate(child, "SIGTERM", options);
|
|
49
|
+
return schedule(() => {
|
|
50
|
+
terminate(child, "SIGKILL", options);
|
|
51
|
+
options.onEscalated?.();
|
|
52
|
+
}, graceMs);
|
|
53
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
allToolNames, isCanonicalFullPolicy, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
4
4
|
POLICY_PROFILES, SERVER_NAME,
|
|
5
5
|
} from "./tools.mjs";
|
|
6
|
+
import { executionGuardrailsSnapshot } from "./execution-limits.mjs";
|
|
6
7
|
|
|
7
8
|
export function buildRuntimeInfo({
|
|
8
9
|
workspace,
|
|
@@ -47,7 +48,7 @@ export function buildRuntimeInfo({
|
|
|
47
48
|
},
|
|
48
49
|
tools: ["server_info", ...toolNames],
|
|
49
50
|
observability: {
|
|
50
|
-
relay_readiness: "
|
|
51
|
+
relay_readiness: "end-to-end-relay-probe-verified",
|
|
51
52
|
brief_relay_interruptions: "debug-only",
|
|
52
53
|
raw_transport_details: "debug-only",
|
|
53
54
|
per_tool_events: "structured-debug-events",
|
|
@@ -63,6 +64,7 @@ export function buildRuntimeInfo({
|
|
|
63
64
|
relay: relayStatus(),
|
|
64
65
|
runtime_dir: policy.exposeAbsolutePaths ? runtimeDir : "<private-runtime-dir>",
|
|
65
66
|
processes: processTracker.snapshot(),
|
|
67
|
+
execution_guardrails: executionGuardrailsSnapshot(),
|
|
66
68
|
process_sessions: processSessionManager.status(),
|
|
67
69
|
managed_jobs: managedJobManager.status(),
|
|
68
70
|
local_resources: managedJobManager.resourceInfo(),
|
package/src/local/runtime.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { isRelayReadyContext, RelayConnection } from "./relay-connection.mjs";
|
|
6
6
|
import { ProcessSessionManager } from "./process-sessions.mjs";
|
|
7
|
-
|
|
7
|
+
import { MAX_CONCURRENT_TOOL_CALLS } from "./execution-limits.mjs";
|
|
8
|
+
export { MAX_COMMAND_BYTES } from "./process-contract.mjs";
|
|
8
9
|
import { MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, PolicyGate, SERVER_NAME } from "./tools.mjs";
|
|
9
10
|
import { publicError } from "./errors.mjs";
|
|
10
11
|
import { ProcessTracker } from "./process-tracker.mjs";
|
|
@@ -36,7 +37,6 @@ import {
|
|
|
36
37
|
} from "./runtime-capabilities.mjs";
|
|
37
38
|
|
|
38
39
|
const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
39
|
-
const MAX_CONCURRENT_TOOL_CALLS = 16;
|
|
40
40
|
const SLOW_TOOL_CALL_MS = 30_000;
|
|
41
41
|
|
|
42
42
|
const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
@@ -212,6 +212,7 @@ export class LocalRuntime {
|
|
|
212
212
|
readResourceText,
|
|
213
213
|
readResourceBinary,
|
|
214
214
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
215
|
+
logger: this.logger,
|
|
215
216
|
});
|
|
216
217
|
this.toolExecutor = new ToolExecutor({
|
|
217
218
|
handlers: Object.fromEntries(Object.entries(RUNTIME_TOOL_HANDLERS).map(([name, handler]) => [
|
|
@@ -234,9 +235,7 @@ export class LocalRuntime {
|
|
|
234
235
|
});
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
tools() {
|
|
238
|
-
return this.policyGate.names().filter((name) => name !== "server_info");
|
|
239
|
-
}
|
|
238
|
+
tools() { return this.policyGate.names().filter((name) => name !== "server_info"); }
|
|
240
239
|
|
|
241
240
|
runtimeInfo() {
|
|
242
241
|
return buildRuntimeInfo({
|
package/src/local/shell.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { packageRoot } from "./state.mjs";
|
|
5
5
|
import { BoundedOutput } from "./bounded-output.mjs";
|
|
6
|
+
import { terminateProcessTreeWithEscalation } from "./process-tree.mjs";
|
|
6
7
|
|
|
7
8
|
export function runExecutable(command, args = [], options = {}) {
|
|
8
9
|
const executable = validateExecutable(command);
|
|
@@ -28,8 +29,7 @@ export function runExecutable(command, args = [], options = {}) {
|
|
|
28
29
|
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
29
30
|
timer = setTimeout(() => {
|
|
30
31
|
timedOut = true;
|
|
31
|
-
|
|
32
|
-
killTimer = setTimeout(() => terminateCommandTree(child, true), 2000);
|
|
32
|
+
killTimer = terminateProcessTreeWithEscalation(child);
|
|
33
33
|
}, timeoutMs);
|
|
34
34
|
timer.unref?.();
|
|
35
35
|
}
|
|
@@ -84,23 +84,6 @@ function validateExecutableArgs(value) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function terminateCommandTree(child, force) {
|
|
88
|
-
if (!child?.pid) return;
|
|
89
|
-
if (process.platform === "win32") {
|
|
90
|
-
try {
|
|
91
|
-
const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", ...(force ? ["/F"] : [])], {
|
|
92
|
-
stdio: "ignore",
|
|
93
|
-
windowsHide: true,
|
|
94
|
-
});
|
|
95
|
-
killer.unref();
|
|
96
|
-
return;
|
|
97
|
-
} catch {}
|
|
98
|
-
}
|
|
99
|
-
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
100
|
-
try { process.kill(-child.pid, signal); } catch {
|
|
101
|
-
try { child.kill(signal); } catch {}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
87
|
|
|
105
88
|
function capturedResult(code, stdout, stderr, extraStderr = "") {
|
|
106
89
|
const stderrText = [stderr.text(), extraStderr].filter(Boolean).join("\n");
|
package/src/worker/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "./http.ts";
|
|
28
28
|
|
|
29
29
|
const SERVER_NAME = String(serverMetadata.name);
|
|
30
|
-
const SERVER_VERSION = "1.2.
|
|
30
|
+
const SERVER_VERSION = "1.2.8";
|
|
31
31
|
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
32
32
|
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
33
33
|
const JSONRPC_VERSION = "2.0";
|