brepjs-verify 0.8.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +152 -0
- package/README.md +18 -18
- package/dist/brepjs-verify.cjs +1 -1
- package/dist/brepjs-verify.js +1 -1
- package/dist/cli/main.cjs +1 -1
- package/dist/cli/main.js +1 -1
- package/dist/{diff-DDEu6YJM.cjs → diff-3ivpxBET.cjs} +1 -1
- package/dist/{diff-BCECMYSQ.js → diff-DyilrTFJ.js} +1 -1
- package/dist/mcp/server.cjs +121 -32
- package/dist/mcp/server.js +122 -33
- package/dist/sandbox/runProgram.d.ts +14 -0
- package/package.json +7 -8
- package/viewer/dist/assets/brepjs-cDw_z4Rj.js +60 -0
- package/viewer/dist/assets/{index-CnQ8btWD.js → index-BBdw65cO.js} +1 -1
- package/viewer/dist/assets/{kernelWorker-CMzbzBfs.js → kernelWorker-DeA3Hcd0.js} +1 -1
- package/viewer/dist/index.html +1 -1
- package/viewer/dist/wasm/occt-wasm.wasm +0 -0
- package/viewer/dist/assets/brepjs-NH9yA5tB.js +0 -59
package/dist/mcp/server.js
CHANGED
|
@@ -3,12 +3,11 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import {
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
7
|
import { appendFile, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
8
8
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
10
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
-
import { promisify } from "node:util";
|
|
12
11
|
import { createHash } from "node:crypto";
|
|
13
12
|
//#region src/sandbox/runProgram.ts
|
|
14
13
|
/**
|
|
@@ -20,11 +19,22 @@ import { createHash } from "node:crypto";
|
|
|
20
19
|
* rather than hanging or crashing the host. Results cross the boundary as serialized JSON (never
|
|
21
20
|
* live WASM handles), and the temp program directory is always cleaned up.
|
|
22
21
|
*/
|
|
23
|
-
var execFileAsync = promisify(execFile);
|
|
24
22
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
25
23
|
var DEFAULT_MAX_MEMORY_MB = 2048;
|
|
26
24
|
var MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
27
25
|
/**
|
|
26
|
+
* In-flight sandbox process *groups*, keyed by group-leader pid. Tracked so a dying host can reap
|
|
27
|
+
* its runs (see installSandboxShutdownHandlers) — the per-run timeout can't, since its timer dies
|
|
28
|
+
* with the host.
|
|
29
|
+
*/
|
|
30
|
+
var activeGroups = /* @__PURE__ */ new Set();
|
|
31
|
+
/** SIGKILL (or `signal`) an entire detached process group by its leader pid; ignore if already gone. */
|
|
32
|
+
function killGroup(pid, signal) {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(process.platform === "win32" ? pid : -pid, signal);
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
28
38
|
* Clamp a caller-supplied limit to a positive, finite value, falling back to the default otherwise.
|
|
29
39
|
* Critical for the timeout: Node's `execFile` treats `timeout: 0` (and it ignores negatives) as
|
|
30
40
|
* "no timeout", which would silently disable the sandbox's only runaway protection.
|
|
@@ -53,39 +63,14 @@ async function runVerifyCli(code, makeArgs, opts) {
|
|
|
53
63
|
const partPath = join(dir, "part.brep.ts");
|
|
54
64
|
await writeFile(partPath, code);
|
|
55
65
|
const cliArgs = makeArgs(partPath);
|
|
56
|
-
|
|
57
|
-
const args = useTsx ? [
|
|
66
|
+
return await spawnCliOutcome(useTsx ? "npx" : process.execPath, useTsx ? [
|
|
58
67
|
"tsx",
|
|
59
68
|
cliEntry,
|
|
60
69
|
...cliArgs
|
|
61
|
-
] : [cliEntry, ...cliArgs]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
killSignal: "SIGKILL",
|
|
66
|
-
maxBuffer: MAX_OUTPUT_BYTES,
|
|
67
|
-
env: {
|
|
68
|
-
...process.env,
|
|
69
|
-
NODE_OPTIONS: [process.env["NODE_OPTIONS"], `--max-old-space-size=${maxMemoryMb}`].filter(Boolean).join(" ")
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
return {
|
|
73
|
-
stdout,
|
|
74
|
-
stderr,
|
|
75
|
-
timedOut: false,
|
|
76
|
-
outputTooLarge: false,
|
|
77
|
-
exitCode: 0
|
|
78
|
-
};
|
|
79
|
-
} catch (err) {
|
|
80
|
-
const e = err;
|
|
81
|
-
return {
|
|
82
|
-
stdout: e.stdout ?? "",
|
|
83
|
-
stderr: e.stderr ?? (err instanceof Error ? err.message : String(err)),
|
|
84
|
-
timedOut: Boolean(e.killed) && e.code !== "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
|
|
85
|
-
outputTooLarge: e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
|
|
86
|
-
exitCode: typeof e.code === "number" ? e.code : null
|
|
87
|
-
};
|
|
88
|
-
}
|
|
70
|
+
] : [cliEntry, ...cliArgs], {
|
|
71
|
+
...process.env,
|
|
72
|
+
NODE_OPTIONS: [process.env["NODE_OPTIONS"], `--max-old-space-size=${maxMemoryMb}`].filter(Boolean).join(" ")
|
|
73
|
+
}, timeoutMs);
|
|
89
74
|
} finally {
|
|
90
75
|
await rm(dir, {
|
|
91
76
|
recursive: true,
|
|
@@ -93,6 +78,109 @@ async function runVerifyCli(code, makeArgs, opts) {
|
|
|
93
78
|
});
|
|
94
79
|
}
|
|
95
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Spawn the verify CLI in its OWN process group and enforce the wall-clock budget by SIGKILLing the
|
|
83
|
+
* WHOLE group on timeout — not just the direct child.
|
|
84
|
+
*
|
|
85
|
+
* Why a process group rather than Node's built-in `execFile` timeout: in dev/test the CLI runs as
|
|
86
|
+
* `npx tsx <main.ts>`, so the process that actually executes the part (and can spin on a CPU-bound
|
|
87
|
+
* OCCT op) is a *grandchild* behind npx+tsx. `child_process`' `timeout`/`killSignal` signals only
|
|
88
|
+
* the direct child, so a fired timeout SIGKILLs `npx` and orphans the still-spinning grandchild
|
|
89
|
+
* forever (it reparents to init and keeps burning a core). Spawning `detached` makes the child a
|
|
90
|
+
* process-group leader that npx's descendants inherit, so `process.kill(-pid, …)` reaps the entire
|
|
91
|
+
* tree. The production path (`node dist/cli/main.js`) has no intermediary, but the group kill is
|
|
92
|
+
* equally correct there.
|
|
93
|
+
*/
|
|
94
|
+
function spawnCliOutcome(cmd, args, env, timeoutMs) {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const child = spawn(cmd, args, {
|
|
97
|
+
env,
|
|
98
|
+
detached: true,
|
|
99
|
+
stdio: [
|
|
100
|
+
"ignore",
|
|
101
|
+
"pipe",
|
|
102
|
+
"pipe"
|
|
103
|
+
]
|
|
104
|
+
});
|
|
105
|
+
if (child.pid !== void 0) activeGroups.add(child.pid);
|
|
106
|
+
let stdout = "";
|
|
107
|
+
let stderr = "";
|
|
108
|
+
let stdoutBytes = 0;
|
|
109
|
+
let timedOut = false;
|
|
110
|
+
let outputTooLarge = false;
|
|
111
|
+
let settled = false;
|
|
112
|
+
const killTree = (signal) => {
|
|
113
|
+
if (child.pid !== void 0) killGroup(child.pid, signal);
|
|
114
|
+
};
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
timedOut = true;
|
|
117
|
+
killTree("SIGKILL");
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
child.stdout?.on("data", (chunk) => {
|
|
120
|
+
if (outputTooLarge) return;
|
|
121
|
+
stdoutBytes += chunk.length;
|
|
122
|
+
if (stdoutBytes > MAX_OUTPUT_BYTES) {
|
|
123
|
+
outputTooLarge = true;
|
|
124
|
+
killTree("SIGKILL");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
stdout += chunk.toString();
|
|
128
|
+
});
|
|
129
|
+
let stderrBytes = 0;
|
|
130
|
+
child.stderr?.on("data", (chunk) => {
|
|
131
|
+
stderrBytes += chunk.length;
|
|
132
|
+
if (stderrBytes <= MAX_OUTPUT_BYTES) stderr += chunk.toString();
|
|
133
|
+
});
|
|
134
|
+
const finish = (exitCode) => {
|
|
135
|
+
if (settled) return;
|
|
136
|
+
settled = true;
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
if (child.pid !== void 0) activeGroups.delete(child.pid);
|
|
139
|
+
resolve({
|
|
140
|
+
stdout,
|
|
141
|
+
stderr,
|
|
142
|
+
timedOut,
|
|
143
|
+
outputTooLarge,
|
|
144
|
+
exitCode
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
child.on("error", (err) => {
|
|
148
|
+
if (!stderr) stderr = err.message;
|
|
149
|
+
finish(null);
|
|
150
|
+
});
|
|
151
|
+
child.on("close", (code) => finish(code));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** SIGKILL every in-flight sandbox process group now. Exported so a host can reap explicitly. */
|
|
155
|
+
function killActiveSandboxes(signal = "SIGKILL") {
|
|
156
|
+
for (const pid of activeGroups) killGroup(pid, signal);
|
|
157
|
+
activeGroups.clear();
|
|
158
|
+
}
|
|
159
|
+
var shutdownHandlersInstalled = false;
|
|
160
|
+
/**
|
|
161
|
+
* Install process-shutdown hooks that reap any in-flight sandbox process groups when THIS process
|
|
162
|
+
* (the host — e.g. the MCP server) terminates. Idempotent; call once at startup from an entrypoint.
|
|
163
|
+
*
|
|
164
|
+
* Rationale: the per-run timeout (`spawnCliOutcome`) only protects a run while the host is alive —
|
|
165
|
+
* its timer dies with the host. If the host is stopped (the agent disconnects) before a run's
|
|
166
|
+
* budget elapses, the `detached` sandbox group is in its own session and survives, burning a core
|
|
167
|
+
* indefinitely. These hooks SIGKILL every tracked group on the way down so a dying host doesn't
|
|
168
|
+
* leak its children. (A hard SIGKILL of the host can't be trapped — that residual needs the kernel's
|
|
169
|
+
* PR_SET_PDEATHSIG, which Node doesn't expose.)
|
|
170
|
+
*/
|
|
171
|
+
function installSandboxShutdownHandlers() {
|
|
172
|
+
if (shutdownHandlersInstalled) return;
|
|
173
|
+
shutdownHandlersInstalled = true;
|
|
174
|
+
process.on("exit", () => killActiveSandboxes("SIGKILL"));
|
|
175
|
+
process.once("SIGTERM", () => {
|
|
176
|
+
killActiveSandboxes("SIGKILL");
|
|
177
|
+
process.exit(143);
|
|
178
|
+
});
|
|
179
|
+
process.once("SIGINT", () => {
|
|
180
|
+
killActiveSandboxes("SIGKILL");
|
|
181
|
+
process.exit(130);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
96
184
|
function tryParse(out) {
|
|
97
185
|
if (!out.trim()) return null;
|
|
98
186
|
try {
|
|
@@ -346,6 +434,7 @@ async function exportPartTool(args) {
|
|
|
346
434
|
* "build → verify" step the agent loop is built on. Uses the SDK's low-level `Server` with plain
|
|
347
435
|
* JSON-Schema tool definitions (no direct zod dependency in this package).
|
|
348
436
|
*/
|
|
437
|
+
installSandboxShutdownHandlers();
|
|
349
438
|
var pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
350
439
|
var server = new Server({
|
|
351
440
|
name: "brepjs-verify",
|
|
@@ -53,6 +53,20 @@ export interface ExportFormats {
|
|
|
53
53
|
* "no timeout", which would silently disable the sandbox's only runaway protection.
|
|
54
54
|
*/
|
|
55
55
|
export declare function positiveOrDefault(value: number | undefined, fallback: number): number;
|
|
56
|
+
/** SIGKILL every in-flight sandbox process group now. Exported so a host can reap explicitly. */
|
|
57
|
+
export declare function killActiveSandboxes(signal?: NodeJS.Signals): void;
|
|
58
|
+
/**
|
|
59
|
+
* Install process-shutdown hooks that reap any in-flight sandbox process groups when THIS process
|
|
60
|
+
* (the host — e.g. the MCP server) terminates. Idempotent; call once at startup from an entrypoint.
|
|
61
|
+
*
|
|
62
|
+
* Rationale: the per-run timeout (`spawnCliOutcome`) only protects a run while the host is alive —
|
|
63
|
+
* its timer dies with the host. If the host is stopped (the agent disconnects) before a run's
|
|
64
|
+
* budget elapses, the `detached` sandbox group is in its own session and survives, burning a core
|
|
65
|
+
* indefinitely. These hooks SIGKILL every tracked group on the way down so a dying host doesn't
|
|
66
|
+
* leak its children. (A hard SIGKILL of the host can't be trapped — that residual needs the kernel's
|
|
67
|
+
* PR_SET_PDEATHSIG, which Node doesn't expose.)
|
|
68
|
+
*/
|
|
69
|
+
export declare function installSandboxShutdownHandlers(): void;
|
|
56
70
|
/** Execute `code` (an agent-authored `.brep.ts` module) in an isolated, resource-bounded child. */
|
|
57
71
|
export declare function runProgram(code: string, opts?: RunProgramOptions): Promise<RunProgramResult>;
|
|
58
72
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brepjs-verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Agent skill + verify/preview tooling for authoring parametric brepjs CAD code",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -53,9 +53,8 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
56
|
-
"@types/node": "^25.9.1",
|
|
57
56
|
"brepjs": "^18.0.0",
|
|
58
|
-
"commander": "^
|
|
57
|
+
"commander": "^15.0.0",
|
|
59
58
|
"occt-wasm": "^3.0.0",
|
|
60
59
|
"typescript": "^6.0.3"
|
|
61
60
|
},
|
|
@@ -63,20 +62,20 @@
|
|
|
63
62
|
"puppeteer": "^25.0.4"
|
|
64
63
|
},
|
|
65
64
|
"devDependencies": {
|
|
66
|
-
"@anthropic-ai/sdk": "0.
|
|
65
|
+
"@anthropic-ai/sdk": "0.102.0",
|
|
67
66
|
"@react-three/drei": "^10.7.7",
|
|
68
67
|
"@react-three/fiber": "^9.6.1",
|
|
69
|
-
"@types/node": "
|
|
68
|
+
"@types/node": "25.9.2",
|
|
70
69
|
"@types/react": "^19.2.15",
|
|
71
70
|
"@types/react-dom": "^19.2.3",
|
|
72
71
|
"@types/three": "^0.184.1",
|
|
73
72
|
"@vitejs/plugin-react": "^6.0.2",
|
|
74
73
|
"brepjs-viewer": "*",
|
|
75
74
|
"eslint": "^10.4.0",
|
|
76
|
-
"react": "
|
|
77
|
-
"react-dom": "
|
|
75
|
+
"react": "19.2.7",
|
|
76
|
+
"react-dom": "19.2.7",
|
|
78
77
|
"three": "^0.184.0",
|
|
79
|
-
"tsx": "
|
|
78
|
+
"tsx": "4.22.4",
|
|
80
79
|
"vite": "^8.0.0",
|
|
81
80
|
"vite-plugin-dts": "^5.0.1",
|
|
82
81
|
"vitest": "^4.0.0",
|