@securecode-ai/mcp 0.5.5 → 0.5.7

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.
Files changed (186) hide show
  1. package/README.md +52 -12
  2. package/dist/api/types.d.ts +7 -0
  3. package/dist/approval/auditLog.d.ts +1 -1
  4. package/dist/approval/auditLog.js +11 -3
  5. package/dist/approval/auditLog.js.map +1 -1
  6. package/dist/approval/broker.d.ts +7 -2
  7. package/dist/approval/broker.js +239 -110
  8. package/dist/approval/broker.js.map +1 -1
  9. package/dist/approval/policy.d.ts +10 -0
  10. package/dist/approval/policy.js +104 -0
  11. package/dist/approval/policy.js.map +1 -0
  12. package/dist/approval/types.d.ts +11 -2
  13. package/dist/approval/types.js +10 -1
  14. package/dist/approval/types.js.map +1 -1
  15. package/dist/attack/agentScanExecutor.d.ts +35 -1
  16. package/dist/attack/agentScanExecutor.js +405 -76
  17. package/dist/attack/agentScanExecutor.js.map +1 -1
  18. package/dist/attack/agentScanLoop.d.ts +20 -0
  19. package/dist/attack/agentScanLoop.js +1264 -60
  20. package/dist/attack/agentScanLoop.js.map +1 -1
  21. package/dist/attack/agentScanProtocol.d.ts +382 -6
  22. package/dist/attack/agentScanProtocol.js +110 -4
  23. package/dist/attack/agentScanProtocol.js.map +1 -1
  24. package/dist/attack/agentTrace.d.ts +73 -0
  25. package/dist/attack/agentTrace.js +228 -0
  26. package/dist/attack/agentTrace.js.map +1 -0
  27. package/dist/attack/architectureScoutExecutor.d.ts +18 -0
  28. package/dist/attack/architectureScoutExecutor.js +211 -0
  29. package/dist/attack/architectureScoutExecutor.js.map +1 -0
  30. package/dist/attack/architectureScoutLoop.d.ts +36 -0
  31. package/dist/attack/architectureScoutLoop.js +403 -0
  32. package/dist/attack/architectureScoutLoop.js.map +1 -0
  33. package/dist/attack/architectureScoutProtocol.d.ts +209 -0
  34. package/dist/attack/architectureScoutProtocol.js +47 -0
  35. package/dist/attack/architectureScoutProtocol.js.map +1 -0
  36. package/dist/attack/candidateStore.d.ts +95 -0
  37. package/dist/attack/candidateStore.js +231 -0
  38. package/dist/attack/candidateStore.js.map +1 -0
  39. package/dist/attack/evidenceLedger.d.ts +67 -0
  40. package/dist/attack/evidenceLedger.js +192 -0
  41. package/dist/attack/evidenceLedger.js.map +1 -0
  42. package/dist/attack/finishGate.d.ts +62 -0
  43. package/dist/attack/finishGate.js +209 -0
  44. package/dist/attack/finishGate.js.map +1 -0
  45. package/dist/attack/fixCodeMerge.d.ts +27 -0
  46. package/dist/attack/fixCodeMerge.js +42 -0
  47. package/dist/attack/fixCodeMerge.js.map +1 -0
  48. package/dist/attack/fixVerifyLoop.d.ts +55 -0
  49. package/dist/attack/fixVerifyLoop.js +187 -0
  50. package/dist/attack/fixVerifyLoop.js.map +1 -0
  51. package/dist/attack/investigationProfiles.d.ts +36 -0
  52. package/dist/attack/investigationProfiles.js +144 -0
  53. package/dist/attack/investigationProfiles.js.map +1 -0
  54. package/dist/attack/investigationState.d.ts +227 -0
  55. package/dist/attack/investigationState.js +666 -0
  56. package/dist/attack/investigationState.js.map +1 -0
  57. package/dist/attack/mutationOperators.d.ts +22 -0
  58. package/dist/attack/mutationOperators.js +170 -0
  59. package/dist/attack/mutationOperators.js.map +1 -0
  60. package/dist/attack/mutationTest.d.ts +33 -0
  61. package/dist/attack/mutationTest.js +110 -0
  62. package/dist/attack/mutationTest.js.map +1 -0
  63. package/dist/attack/proofGate.d.ts +20 -0
  64. package/dist/attack/proofGate.js +104 -0
  65. package/dist/attack/proofGate.js.map +1 -0
  66. package/dist/attack/proofTypes.d.ts +57 -0
  67. package/dist/attack/proofTypes.js +32 -0
  68. package/dist/attack/proofTypes.js.map +1 -0
  69. package/dist/attack/protocolValidator.d.ts +50 -0
  70. package/dist/attack/protocolValidator.js +427 -0
  71. package/dist/attack/protocolValidator.js.map +1 -0
  72. package/dist/attack/qualityMetrics.d.ts +133 -0
  73. package/dist/attack/qualityMetrics.js +226 -0
  74. package/dist/attack/qualityMetrics.js.map +1 -0
  75. package/dist/attack/scanScheduler.d.ts +52 -0
  76. package/dist/attack/scanScheduler.js +265 -0
  77. package/dist/attack/scanScheduler.js.map +1 -0
  78. package/dist/attack/scanState.d.ts +58 -0
  79. package/dist/attack/scanState.js +102 -0
  80. package/dist/attack/scanState.js.map +1 -0
  81. package/dist/attack/searchIntent.d.ts +16 -0
  82. package/dist/attack/searchIntent.js +57 -0
  83. package/dist/attack/searchIntent.js.map +1 -0
  84. package/dist/attack/verifyLoop.d.ts +43 -0
  85. package/dist/attack/verifyLoop.js +314 -14
  86. package/dist/attack/verifyLoop.js.map +1 -1
  87. package/dist/attack/workItem.d.ts +57 -0
  88. package/dist/attack/workItem.js +225 -0
  89. package/dist/attack/workItem.js.map +1 -0
  90. package/dist/audit/findingReviewQueue.d.ts +109 -0
  91. package/dist/audit/findingReviewQueue.js +335 -0
  92. package/dist/audit/findingReviewQueue.js.map +1 -0
  93. package/dist/audit/scanAuditLog.d.ts +160 -0
  94. package/dist/audit/scanAuditLog.js +404 -0
  95. package/dist/audit/scanAuditLog.js.map +1 -0
  96. package/dist/dependency/dependencyChecker.js +35 -9
  97. package/dist/dependency/dependencyChecker.js.map +1 -1
  98. package/dist/dependency/exploitPriority.d.ts +33 -0
  99. package/dist/dependency/exploitPriority.js +78 -0
  100. package/dist/dependency/exploitPriority.js.map +1 -0
  101. package/dist/dependency/finding.d.ts +16 -0
  102. package/dist/dependency/osvClient.js +2 -0
  103. package/dist/dependency/osvClient.js.map +1 -1
  104. package/dist/dependency/types.d.ts +12 -1
  105. package/dist/mcp/server.js +13 -2
  106. package/dist/mcp/server.js.map +1 -1
  107. package/dist/mcp/tools.d.ts +1 -0
  108. package/dist/mcp/tools.js +127 -6
  109. package/dist/mcp/tools.js.map +1 -1
  110. package/dist/project-map/agentMemory.d.ts +83 -1
  111. package/dist/project-map/agentMemory.js +197 -7
  112. package/dist/project-map/agentMemory.js.map +1 -1
  113. package/dist/project-map/architectureContext.d.ts +202 -0
  114. package/dist/project-map/architectureContext.js +421 -0
  115. package/dist/project-map/architectureContext.js.map +1 -0
  116. package/dist/project-map/blastRadius.d.ts +54 -0
  117. package/dist/project-map/blastRadius.js +196 -0
  118. package/dist/project-map/blastRadius.js.map +1 -0
  119. package/dist/project-map/callGraphExtractor.d.ts +13 -0
  120. package/dist/project-map/callGraphExtractor.js +222 -0
  121. package/dist/project-map/callGraphExtractor.js.map +1 -0
  122. package/dist/project-map/capabilityRegistry.d.ts +46 -0
  123. package/dist/project-map/capabilityRegistry.js +91 -0
  124. package/dist/project-map/capabilityRegistry.js.map +1 -0
  125. package/dist/project-map/findTests.d.ts +29 -0
  126. package/dist/project-map/findTests.js +236 -0
  127. package/dist/project-map/findTests.js.map +1 -0
  128. package/dist/project-map/handlerInventory.d.ts +112 -0
  129. package/dist/project-map/handlerInventory.js +184 -0
  130. package/dist/project-map/handlerInventory.js.map +1 -0
  131. package/dist/project-map/implementationResolver.d.ts +43 -0
  132. package/dist/project-map/implementationResolver.js +217 -0
  133. package/dist/project-map/implementationResolver.js.map +1 -0
  134. package/dist/project-map/mapContext.d.ts +6 -1
  135. package/dist/project-map/mapContext.js +1 -0
  136. package/dist/project-map/mapContext.js.map +1 -1
  137. package/dist/project-map/scanCache.d.ts +57 -3
  138. package/dist/project-map/scanCache.js +61 -2
  139. package/dist/project-map/scanCache.js.map +1 -1
  140. package/dist/project-map/symbolIndex.d.ts +39 -0
  141. package/dist/project-map/symbolIndex.js +385 -0
  142. package/dist/project-map/symbolIndex.js.map +1 -0
  143. package/dist/tooling/agentEvalScoring.d.ts +77 -0
  144. package/dist/tooling/agentEvalScoring.js +140 -0
  145. package/dist/tooling/agentEvalScoring.js.map +1 -0
  146. package/dist/tooling/agentRegression.d.ts +53 -0
  147. package/dist/tooling/agentRegression.js +99 -0
  148. package/dist/tooling/agentRegression.js.map +1 -0
  149. package/dist/tools/agentScan.d.ts +69 -0
  150. package/dist/tools/agentScan.js +702 -43
  151. package/dist/tools/agentScan.js.map +1 -1
  152. package/dist/tools/findingReviewTools.d.ts +22 -0
  153. package/dist/tools/findingReviewTools.js +121 -0
  154. package/dist/tools/findingReviewTools.js.map +1 -0
  155. package/dist/tools/fix.js +1 -1
  156. package/dist/tools/fix.js.map +1 -1
  157. package/dist/tools/map.js +191 -1
  158. package/dist/tools/map.js.map +1 -1
  159. package/dist/tools/runTests.d.ts +2 -0
  160. package/dist/tools/runTests.js +29 -0
  161. package/dist/tools/runTests.js.map +1 -0
  162. package/dist/utils/effectMock.d.ts +1 -0
  163. package/dist/utils/effectMock.js +162 -0
  164. package/dist/utils/effectMock.js.map +1 -0
  165. package/dist/utils/gitContext.d.ts +26 -0
  166. package/dist/utils/gitContext.js +317 -0
  167. package/dist/utils/gitContext.js.map +1 -0
  168. package/dist/utils/localTestRunner.d.ts +57 -2
  169. package/dist/utils/localTestRunner.js +122 -115
  170. package/dist/utils/localTestRunner.js.map +1 -1
  171. package/dist/utils/securityConfig.d.ts +17 -0
  172. package/dist/utils/securityConfig.js +192 -0
  173. package/dist/utils/securityConfig.js.map +1 -0
  174. package/dist/utils/testCommandPolicy.d.ts +36 -0
  175. package/dist/utils/testCommandPolicy.js +252 -0
  176. package/dist/utils/testCommandPolicy.js.map +1 -0
  177. package/dist/utils/testRunner.d.ts +56 -0
  178. package/dist/utils/testRunner.js +246 -0
  179. package/dist/utils/testRunner.js.map +1 -0
  180. package/dist/utils/testSafety.d.ts +40 -2
  181. package/dist/utils/testSafety.js +205 -26
  182. package/dist/utils/testSafety.js.map +1 -1
  183. package/dist/utils/verificationSandbox.d.ts +113 -0
  184. package/dist/utils/verificationSandbox.js +656 -0
  185. package/dist/utils/verificationSandbox.js.map +1 -0
  186. package/package.json +1 -1
@@ -0,0 +1,656 @@
1
+ "use strict";
2
+ /**
3
+ * Verification sandbox — the real security boundary for LLM-generated tests.
4
+ *
5
+ * Phase 2 of the agent scan pipeline takes an LLM-generated test script and
6
+ * executes it on the user's machine. The previous implementation ran the
7
+ * script via `execFileSync` with the full `process.env` spread and only a
8
+ * decorative blocklist (`checkTestSafety`) in front. Prompt-injected code
9
+ * in the scanned source could ride into the verify-generate prompt and
10
+ * produce a test script that exfiltrated secrets, hit the network, or
11
+ * spawned processes — `checkTestSafety` missed `fetch`, `http`, `https`,
12
+ * `fs`, `dns`, `eval`, `new Function`, dynamic imports, and `process.env`
13
+ * access.
14
+ *
15
+ * This module replaces that with a real isolation boundary. Pluggable
16
+ * backends provide kernel-enforced isolation:
17
+ *
18
+ * - DockerSandbox: `docker run --network=none --cpus=1
19
+ * --memory=256m --pids-limit=64 --user 1000:1000 -v <workspace>:/workspace:ro`
20
+ * — network disabled at the kernel level, workspace read-only, temp
21
+ * dir as tmpfs, no host env, non-root user. Works for any Node/Python
22
+ * test script unchanged.
23
+ * - DenoSandbox: `deno run --allow-read=<workspace> --allow-write=<tmp>
24
+ * --allow-env=NODE_ENV --no-prompt --no-remote` — real permission
25
+ * model, no network, no arbitrary fs, no arbitrary env. Only works
26
+ * for test scripts that don't use `require()` (Deno is ESM-only by
27
+ * default); the verify-generate prompt must be told to emit
28
+ * Deno-compatible code when Deno is the backend.
29
+ *
30
+ * If neither backend is available, `detectSandbox()` returns null and the
31
+ * caller MUST return INCONCLUSIVE. Never fall back to unrestricted
32
+ * `execFileSync` — that is the bug this module exists to fix.
33
+ *
34
+ * `checkTestSafety()` remains as defense-in-depth (it can reject scripts
35
+ * before we pay for a container start), but it is NOT the security
36
+ * boundary. The sandbox is.
37
+ */
38
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
39
+ if (k2 === undefined) k2 = k;
40
+ var desc = Object.getOwnPropertyDescriptor(m, k);
41
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
42
+ desc = { enumerable: true, get: function() { return m[k]; } };
43
+ }
44
+ Object.defineProperty(o, k2, desc);
45
+ }) : (function(o, m, k, k2) {
46
+ if (k2 === undefined) k2 = k;
47
+ o[k2] = m[k];
48
+ }));
49
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
50
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
51
+ }) : function(o, v) {
52
+ o["default"] = v;
53
+ });
54
+ var __importStar = (this && this.__importStar) || (function () {
55
+ var ownKeys = function(o) {
56
+ ownKeys = Object.getOwnPropertyNames || function (o) {
57
+ var ar = [];
58
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
59
+ return ar;
60
+ };
61
+ return ownKeys(o);
62
+ };
63
+ return function (mod) {
64
+ if (mod && mod.__esModule) return mod;
65
+ var result = {};
66
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
67
+ __setModuleDefault(result, mod);
68
+ return result;
69
+ };
70
+ })();
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.__UnsafeHostSandboxForTests = void 0;
73
+ exports.parseVerdictCommandMode = parseVerdictCommandMode;
74
+ exports.pickImageForCommand = pickImageForCommand;
75
+ exports.pickImageForRunner = pickImageForRunner;
76
+ exports.runnerInvocation = runnerInvocation;
77
+ exports.detectSandbox = detectSandbox;
78
+ exports._resetSandboxCacheForTests = _resetSandboxCacheForTests;
79
+ exports._setSandboxForTests = _setSandboxForTests;
80
+ const fs = __importStar(require("fs"));
81
+ const path = __importStar(require("path"));
82
+ const child_process_1 = require("child_process");
83
+ // ── Verdict parsing (shared by backends) ─────────────────────────────────────
84
+ const PASS_PATTERN = /^PASS:\s*(.+)$/m;
85
+ const FAIL_PATTERN = /^FAIL:\s*(.+)$/m;
86
+ function parseVerdict(output, exitCode, timedOut) {
87
+ if (timedOut)
88
+ return 'timeout';
89
+ if (PASS_PATTERN.test(output))
90
+ return 'pass';
91
+ if (FAIL_PATTERN.test(output))
92
+ return 'fail';
93
+ if (exitCode === 0)
94
+ return 'error'; // ran but didn't print a marker
95
+ return 'error';
96
+ }
97
+ /**
98
+ * Command-mode verdict: no PASS:/FAIL: markers. Exit code 0 = pass, nonzero = fail.
99
+ * This is the standard semantics for `npm test`, `pytest`, etc.
100
+ */
101
+ function parseVerdictCommandMode(exitCode, timedOut) {
102
+ if (timedOut)
103
+ return 'timeout';
104
+ return exitCode === 0 ? 'pass' : 'fail';
105
+ }
106
+ /**
107
+ * Pick the Docker image for a command-mode executable. Returns null if no
108
+ * suitable image is configured — the caller returns sandbox-unavailable.
109
+ */
110
+ function pickImageForCommand(executable) {
111
+ if (executable === 'npm' || executable === 'npx') {
112
+ return { image: process.env.SECURECODE_SANDBOX_IMAGE || 'node:20-alpine' };
113
+ }
114
+ if (executable === 'pnpm') {
115
+ const img = process.env.SECURECODE_SANDBOX_PNPM_IMAGE;
116
+ return img
117
+ ? { image: img }
118
+ : { image: null, reason: 'pnpm is not available in the default sandbox image. Set SECURECODE_SANDBOX_PNPM_IMAGE to a pnpm-enabled image to enable pnpm test execution.' };
119
+ }
120
+ if (executable === 'yarn') {
121
+ const img = process.env.SECURECODE_SANDBOX_YARN_IMAGE;
122
+ return img
123
+ ? { image: img }
124
+ : { image: null, reason: 'yarn is not available in the default sandbox image. Set SECURECODE_SANDBOX_YARN_IMAGE to a yarn-enabled image to enable yarn test execution.' };
125
+ }
126
+ if (executable === 'bun') {
127
+ const img = process.env.SECURECODE_SANDBOX_BUN_IMAGE;
128
+ return img
129
+ ? { image: img }
130
+ : { image: null, reason: 'bun is not available in the default sandbox image. Set SECURECODE_SANDBOX_BUN_IMAGE to a bun-enabled image to enable bun test execution.' };
131
+ }
132
+ if (executable === 'pytest') {
133
+ return { image: process.env.SECURECODE_SANDBOX_PY_IMAGE || 'python:3.11-slim' };
134
+ }
135
+ return { image: null, reason: `No sandbox image configured for executable: ${executable}` };
136
+ }
137
+ // ── Probe caching ───────────────────────────────────────────────────────────
138
+ const probeCache = new Map();
139
+ function probe(bin, args = ['--version']) {
140
+ const key = `${bin} ${args.join(' ')}`;
141
+ if (probeCache.has(key))
142
+ return probeCache.get(key);
143
+ let ok = false;
144
+ try {
145
+ const r = (0, child_process_1.spawnSync)(bin, args, {
146
+ stdio: 'ignore',
147
+ shell: false,
148
+ timeout: 4000,
149
+ });
150
+ ok = r.status === 0;
151
+ }
152
+ catch {
153
+ ok = false;
154
+ }
155
+ probeCache.set(key, ok);
156
+ return ok;
157
+ }
158
+ // ── Helpers ─────────────────────────────────────────────────────────────────
159
+ function fileExtFor(runner) {
160
+ if (runner === 'python' || runner === 'python3')
161
+ return '.py';
162
+ if (runner === 'deno')
163
+ return '.ts';
164
+ if (runner === 'tsx' || runner === 'bun' || runner === 'node' || runner === 'pnpm-tsx' || runner === 'yarn-tsx') {
165
+ return '.test.ts';
166
+ }
167
+ return '.test.js';
168
+ }
169
+ function isPythonRunner(runner) {
170
+ return runner === 'python' || runner === 'python3';
171
+ }
172
+ /**
173
+ * Pick the Docker image for a script-mode runner. Returns null if no suitable
174
+ * image is configured — the caller returns sandbox-unavailable so the user
175
+ * gets a clear "install X / set ENV to enable" message rather than a crash.
176
+ */
177
+ function pickImageForRunner(runner) {
178
+ if (isPythonRunner(runner)) {
179
+ return { image: process.env.SECURECODE_SANDBOX_PY_IMAGE || 'python:3.11-slim' };
180
+ }
181
+ switch (runner) {
182
+ case 'node':
183
+ case 'tsx':
184
+ case 'pnpm-tsx':
185
+ case 'yarn-tsx':
186
+ // node:20-alpine has `node` + `npx`; tsx runs via `npx tsx` (offline
187
+ // if already cached, otherwise fails safely — network is disabled).
188
+ return { image: process.env.SECURECODE_SANDBOX_IMAGE || 'node:20-alpine' };
189
+ case 'deno': {
190
+ const img = process.env.SECURECODE_SANDBOX_DENO_IMAGE;
191
+ return img
192
+ ? { image: img }
193
+ : { image: null, reason: 'deno runner requires a Deno image. Set SECURECODE_SANDBOX_DENO_IMAGE (e.g. denoland/deno) to enable deno script execution.' };
194
+ }
195
+ case 'bun': {
196
+ const img = process.env.SECURECODE_SANDBOX_BUN_IMAGE;
197
+ return img
198
+ ? { image: img }
199
+ : { image: null, reason: 'bun runner requires a Bun image. Set SECURECODE_SANDBOX_BUN_IMAGE (e.g. oven/bun) to enable bun script execution.' };
200
+ }
201
+ default:
202
+ return { image: null, reason: `No sandbox image configured for runner: ${runner}` };
203
+ }
204
+ }
205
+ /**
206
+ * Build the in-container command string for a runner + script path.
207
+ * The path is generated by us (unique suffix), so shell quoting is not
208
+ * required for safety; we still quote defensively in case the workspace
209
+ * path contains spaces.
210
+ */
211
+ function runnerInvocation(runner, scriptPath) {
212
+ const p = `"${scriptPath}"`;
213
+ switch (runner) {
214
+ case 'node':
215
+ return `node ${p}`;
216
+ case 'tsx':
217
+ // npx needs no network when the package is already in the image cache.
218
+ // In node:20-alpine tsx is NOT preinstalled, so this will fail safely
219
+ // unless SECURECODE_SANDBOX_IMAGE provides tsx. That's acceptable: a
220
+ // safe failure is better than silently running with `node` (which
221
+ // would choke on TS syntax and also mask the misconfiguration).
222
+ return `npx --no-install tsx ${p}`;
223
+ case 'pnpm-tsx':
224
+ return `pnpm exec tsx ${p}`;
225
+ case 'yarn-tsx':
226
+ return `yarn tsx ${p}`;
227
+ case 'bun':
228
+ return `bun run ${p}`;
229
+ case 'deno':
230
+ // Deno in Docker: allow-read the workspace so the test can import
231
+ // project modules. No network (--no-remote isn't available in the
232
+ // container deno CLI the same way; we rely on --network=none at the
233
+ // docker level). ESM-only.
234
+ return `deno run --allow-read=/workspace --allow-env=NODE_ENV --no-prompt ${p}`;
235
+ case 'python':
236
+ case 'python3':
237
+ return `python ${p}`;
238
+ default:
239
+ // Unknown runner — fall back to node rather than exec'ing arbitrary
240
+ // strings. The validator upstream should have rejected this already.
241
+ return `node ${p}`;
242
+ }
243
+ }
244
+ function uniqueSuffix() {
245
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
246
+ }
247
+ function ensureSandboxDir(workspaceRoot) {
248
+ const dir = path.join(workspaceRoot, '.securecode');
249
+ if (!fs.existsSync(dir))
250
+ fs.mkdirSync(dir, { recursive: true });
251
+ return dir;
252
+ }
253
+ function safeUnlink(p) {
254
+ if (!p)
255
+ return;
256
+ try {
257
+ fs.unlinkSync(p);
258
+ }
259
+ catch { /* best effort */ }
260
+ }
261
+ // Kill a process tree, best-effort across platforms.
262
+ function killTree(proc) {
263
+ try {
264
+ if (process.platform === 'win32') {
265
+ // Windows: use taskkill with /T to kill the tree.
266
+ if (proc.pid) {
267
+ (0, child_process_1.spawnSync)('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { stdio: 'ignore', shell: true });
268
+ }
269
+ }
270
+ else {
271
+ proc.kill('SIGKILL');
272
+ }
273
+ }
274
+ catch { /* already dead */ }
275
+ }
276
+ // ── Docker backend ─────────────────────────────────────────────────────────
277
+ class DockerSandbox {
278
+ name = 'docker';
279
+ get available() {
280
+ return probe('docker', ['info']);
281
+ }
282
+ async execute(opts) {
283
+ // ── Command mode: run a validated executable + args in the sandbox ──
284
+ if (opts.mode === 'command') {
285
+ return this.executeCommand(opts);
286
+ }
287
+ // ── Script mode (default): write script to temp file and run via runner ──
288
+ return this.executeScript(opts);
289
+ }
290
+ async executeCommand(opts) {
291
+ const executable = opts.executable;
292
+ const cmdArgs = opts.args || [];
293
+ if (!executable) {
294
+ return { verdict: 'error', output: 'Command mode requires "executable"', exitCode: -1, backend: this.name };
295
+ }
296
+ const imageInfo = pickImageForCommand(executable);
297
+ if (!imageInfo.image) {
298
+ return {
299
+ verdict: 'sandbox-unavailable',
300
+ output: imageInfo.reason || `No sandbox image for ${executable}`,
301
+ exitCode: -1,
302
+ backend: this.name,
303
+ };
304
+ }
305
+ const dockerArgs = [
306
+ 'run', '--rm', '-i',
307
+ '--network=none',
308
+ '--tmpfs', '/tmp:rw,noexec,nosuid,size=128m,uid=1000,gid=1000',
309
+ '--cpus=1',
310
+ '--memory=512m',
311
+ '--memory-swap=512m',
312
+ '--pids-limit=128',
313
+ '--user', '1000:1000',
314
+ '--workdir', '/workspace',
315
+ '--env', 'NODE_ENV=test',
316
+ '--env', 'CI=1',
317
+ '--env', 'HOME=/tmp',
318
+ '--env', 'npm_config_cache=/tmp/npm-cache',
319
+ '-v', `${opts.workspaceRoot}:/workspace:ro`,
320
+ imageInfo.image,
321
+ executable,
322
+ ...cmdArgs,
323
+ ];
324
+ return await runSandboxedProcess('docker', dockerArgs, opts, this.name);
325
+ }
326
+ async executeScript(opts) {
327
+ const sandboxDir = ensureSandboxDir(opts.workspaceRoot);
328
+ const runner = opts.runner || 'node';
329
+ const ext = fileExtFor(runner);
330
+ const testFile = path.join(sandboxDir, `verify-test-${uniqueSuffix()}${ext}`);
331
+ const setupExt = isPythonRunner(runner) ? '.py' : ext;
332
+ const setupFile = opts.setupScript
333
+ ? path.join(sandboxDir, `verify-setup-${uniqueSuffix()}${setupExt}`)
334
+ : null;
335
+ try {
336
+ fs.writeFileSync(testFile, opts.script || '', 'utf8');
337
+ if (setupFile && opts.setupScript) {
338
+ fs.writeFileSync(setupFile, opts.setupScript, 'utf8');
339
+ }
340
+ const imageInfo = pickImageForRunner(runner);
341
+ if (!imageInfo.image) {
342
+ return {
343
+ verdict: 'sandbox-unavailable',
344
+ output: imageInfo.reason || `No sandbox image for runner: ${runner}`,
345
+ exitCode: -1,
346
+ backend: this.name,
347
+ };
348
+ }
349
+ // The test and setup files are written inside workspaceRoot/.securecode/,
350
+ // so they're already accessible via the workspace bind mount. We compute
351
+ // the in-container path from the host path — no separate bind mount needed.
352
+ // (A separate bind mount would fail on Docker Desktop for Windows because
353
+ // it can't create a mountpoint inside a read-only bind mount.)
354
+ const toContainerPath = (hostPath) => '/workspace/' + path.relative(opts.workspaceRoot, hostPath).replace(/\\/g, '/');
355
+ const containerTestPath = toContainerPath(testFile);
356
+ const containerSetupPath = setupFile ? toContainerPath(setupFile) : '';
357
+ const dockerArgs = [
358
+ 'run', '--rm', '-i',
359
+ '--network=none',
360
+ '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m,uid=1000,gid=1000',
361
+ '--cpus=1',
362
+ '--memory=256m',
363
+ '--memory-swap=256m',
364
+ '--pids-limit=64',
365
+ '--user', '1000:1000',
366
+ '--workdir', '/workspace',
367
+ '--env', 'NODE_ENV=test',
368
+ '--env', 'CI=1',
369
+ '--env', 'HOME=/tmp',
370
+ '--env', 'npm_config_cache=/tmp/npm-cache',
371
+ '-v', `${opts.workspaceRoot}:/workspace:ro`,
372
+ ];
373
+ dockerArgs.push(imageInfo.image);
374
+ // Build the in-container command for the requested runner.
375
+ const testCmd = runnerInvocation(runner, containerTestPath);
376
+ const fullCmd = setupFile
377
+ ? `${runnerInvocation(runner, containerSetupPath)} && ${testCmd}`
378
+ : testCmd;
379
+ // Use sh -c so setup && test chain correctly. The script paths are
380
+ // generated by us (unique suffix) so there is no injection risk; the
381
+ // script *content* is already accepted-risk (it runs in the sandbox).
382
+ dockerArgs.push('sh', '-c', fullCmd);
383
+ return await runSandboxedProcess('docker', dockerArgs, opts, this.name);
384
+ }
385
+ finally {
386
+ safeUnlink(testFile);
387
+ safeUnlink(setupFile);
388
+ }
389
+ }
390
+ }
391
+ // ── Deno backend ────────────────────────────────────────────────────────────
392
+ //
393
+ // Deno's permission model is a real boundary: `--allow-none` (or no --allow-*
394
+ // flags) denies everything. We allow only:
395
+ // --allow-read=<workspace>,<testFile> (read workspace to import modules)
396
+ // --allow-write=<tmpdir> (test may write a temp file)
397
+ // --allow-env=NODE_ENV (only NODE_ENV; not process.env)
398
+ // --no-remote (no network module fetches)
399
+ // --no-prompt (fail instead of prompting)
400
+ //
401
+ // Caveat: Deno is ESM-only by default and cannot run tests that use require().
402
+ // The verify-generate prompt should detect Deno as the runner and emit
403
+ // ESM-compatible code. If the test uses require(), Deno will error out —
404
+ // which is a safe failure (no code executes).
405
+ class DenoSandbox {
406
+ name = 'deno';
407
+ get available() {
408
+ return probe('deno', ['--version']);
409
+ }
410
+ async execute(opts) {
411
+ // Command mode: Deno can't run package-manager test commands (npm, pytest, etc.)
412
+ // Return sandbox-unavailable so the caller surfaces the right message.
413
+ if (opts.mode === 'command') {
414
+ return {
415
+ verdict: 'sandbox-unavailable',
416
+ output: 'Deno sandbox cannot execute command-mode tests (npm/pnpm/yarn/bun/pytest). Docker is required for existing-test execution.',
417
+ exitCode: -1,
418
+ backend: this.name,
419
+ };
420
+ }
421
+ if (opts.runner === 'python' || opts.runner === 'python3') {
422
+ // Deno can't run Python.
423
+ return {
424
+ verdict: 'blocked',
425
+ output: 'Deno sandbox cannot execute Python tests.',
426
+ exitCode: -1,
427
+ backend: this.name,
428
+ };
429
+ }
430
+ const sandboxDir = ensureSandboxDir(opts.workspaceRoot);
431
+ const ext = '.mts'; // Deno prefers .mts for ESM TS
432
+ const testFile = path.join(sandboxDir, `verify-test-${uniqueSuffix()}${ext}`);
433
+ const tmpDir = path.join(sandboxDir, `verify-tmp-${uniqueSuffix()}`);
434
+ try {
435
+ fs.writeFileSync(testFile, opts.script || '', 'utf8');
436
+ fs.mkdirSync(tmpDir, { recursive: true });
437
+ // Run setup first (if any) in the same sandbox.
438
+ if (opts.setupScript) {
439
+ const setupFile = path.join(sandboxDir, `verify-setup-${uniqueSuffix()}${ext}`);
440
+ try {
441
+ fs.writeFileSync(setupFile, opts.setupScript, 'utf8');
442
+ const setupResult = await runSandboxedProcess('deno', [
443
+ 'run',
444
+ '--allow-read=' + opts.workspaceRoot,
445
+ '--allow-write=' + tmpDir,
446
+ '--allow-env=NODE_ENV',
447
+ '--no-remote',
448
+ '--no-prompt',
449
+ setupFile,
450
+ ], { ...opts, timeoutMs: Math.min(opts.timeoutMs, 10_000) }, this.name + '/setup');
451
+ if (setupResult.verdict === 'error' || setupResult.verdict === 'timeout') {
452
+ return {
453
+ verdict: 'error',
454
+ output: `Setup script failed: ${setupResult.output}`,
455
+ exitCode: setupResult.exitCode,
456
+ backend: this.name,
457
+ };
458
+ }
459
+ }
460
+ finally {
461
+ safeUnlink(setupFile);
462
+ }
463
+ }
464
+ const denoArgs = [
465
+ 'run',
466
+ '--allow-read=' + opts.workspaceRoot + ',' + testFile,
467
+ '--allow-write=' + tmpDir,
468
+ '--allow-env=NODE_ENV',
469
+ '--no-remote',
470
+ '--no-prompt',
471
+ testFile,
472
+ ];
473
+ return await runSandboxedProcess('deno', denoArgs, opts, this.name);
474
+ }
475
+ finally {
476
+ safeUnlink(testFile);
477
+ try {
478
+ fs.rmSync(tmpDir, { recursive: true, force: true });
479
+ }
480
+ catch { /* best effort */ }
481
+ }
482
+ }
483
+ }
484
+ // ── Subprocess runner shared by Docker/Deno backends ────────────────────────
485
+ function runSandboxedProcess(bin, args, opts, backendLabel) {
486
+ const isCommandMode = opts.mode === 'command';
487
+ return new Promise((resolve) => {
488
+ let stdout = '';
489
+ let stderr = '';
490
+ let timedOut = false;
491
+ let cancelled = false;
492
+ let settled = false;
493
+ const proc = (0, child_process_1.spawn)(bin, args, {
494
+ cwd: opts.workspaceRoot,
495
+ stdio: ['pipe', 'pipe', 'pipe'],
496
+ shell: false,
497
+ env: {
498
+ NODE_ENV: 'test',
499
+ CI: '1',
500
+ PATH: process.env.PATH || '',
501
+ PATHEXT: process.env.PATHEXT || '',
502
+ SystemRoot: process.env.SystemRoot || '',
503
+ },
504
+ windowsHide: true,
505
+ });
506
+ proc.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
507
+ proc.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
508
+ const timer = setTimeout(() => {
509
+ timedOut = true;
510
+ killTree(proc);
511
+ }, opts.timeoutMs);
512
+ const onAbort = () => {
513
+ if (settled)
514
+ return;
515
+ cancelled = true;
516
+ killTree(proc);
517
+ };
518
+ opts.signal?.addEventListener('abort', onAbort, { once: true });
519
+ proc.on('error', (err) => {
520
+ if (settled)
521
+ return;
522
+ settled = true;
523
+ clearTimeout(timer);
524
+ opts.signal?.removeEventListener('abort', onAbort);
525
+ resolve({
526
+ verdict: 'error',
527
+ output: `Failed to spawn ${bin}: ${err.message}`,
528
+ exitCode: -1,
529
+ backend: backendLabel,
530
+ });
531
+ });
532
+ proc.on('close', (code) => {
533
+ if (settled)
534
+ return;
535
+ settled = true;
536
+ clearTimeout(timer);
537
+ opts.signal?.removeEventListener('abort', onAbort);
538
+ const output = (stdout + '\n' + stderr).trim();
539
+ const exitCode = code ?? -1;
540
+ // Cancellation takes priority over timeout — if the user aborted,
541
+ // we must not report a timeout (which can trigger a retry).
542
+ if (cancelled) {
543
+ resolve({
544
+ verdict: 'cancelled',
545
+ output: `Test cancelled by user. Partial output:\n${output.slice(0, 4000)}`,
546
+ exitCode: -1,
547
+ backend: backendLabel,
548
+ });
549
+ return;
550
+ }
551
+ const verdict = isCommandMode
552
+ ? parseVerdictCommandMode(exitCode, timedOut)
553
+ : parseVerdict(output, exitCode, timedOut);
554
+ if (timedOut && verdict === 'error') {
555
+ resolve({
556
+ verdict: 'timeout',
557
+ output: `Test timed out after ${opts.timeoutMs}ms. Partial output:\n${output.slice(0, 4000)}`,
558
+ exitCode: -1,
559
+ backend: backendLabel,
560
+ });
561
+ return;
562
+ }
563
+ resolve({
564
+ verdict,
565
+ output: output.slice(0, 16000),
566
+ exitCode,
567
+ backend: backendLabel,
568
+ });
569
+ });
570
+ });
571
+ }
572
+ // ── Backend selection ───────────────────────────────────────────────────────
573
+ let cachedBackend;
574
+ /**
575
+ * Detect the best available sandbox backend. Docker is preferred (runs any
576
+ * Node/Python test unchanged); Deno is a fallback for TS/JS tests only.
577
+ * Returns null if neither is available — the caller MUST return INCONCLUSIVE.
578
+ */
579
+ function detectSandbox() {
580
+ if (cachedBackend !== undefined)
581
+ return cachedBackend;
582
+ const docker = new DockerSandbox();
583
+ if (docker.available) {
584
+ cachedBackend = docker;
585
+ return docker;
586
+ }
587
+ const deno = new DenoSandbox();
588
+ if (deno.available) {
589
+ cachedBackend = deno;
590
+ return deno;
591
+ }
592
+ cachedBackend = null;
593
+ return null;
594
+ }
595
+ /** Reset the cached backend selection. Used by tests. */
596
+ function _resetSandboxCacheForTests() {
597
+ cachedBackend = undefined;
598
+ }
599
+ /** Force a specific backend (for tests). Pass null to simulate "no sandbox". */
600
+ function _setSandboxForTests(backend) {
601
+ cachedBackend = backend;
602
+ }
603
+ // ── Unsafe host execution — ONLY for unit tests of the runner logic ─────────
604
+ //
605
+ // This backend runs scripts via direct `execFileSync` with the host's full
606
+ // privileges. It is NOT a security boundary. It exists solely so the runner's
607
+ // PASS/FAIL parsing and cleanup logic can be unit-tested without a real
608
+ // sandbox. Production code MUST NOT use it — `detectSandbox()` never returns
609
+ // it. The constructor requires `SECURECODE_TEST_MODE=1` so it cannot be
610
+ // accidentally instantiated in production.
611
+ class __UnsafeHostSandboxForTests {
612
+ name = 'unsafe-host-tests-only';
613
+ constructor() {
614
+ if (process.env.SECURECODE_TEST_MODE !== '1') {
615
+ throw new Error('__UnsafeHostSandboxForTests cannot be used in production. Set SECURECODE_TEST_MODE=1 only in tests.');
616
+ }
617
+ }
618
+ get available() { return true; }
619
+ async execute(opts) {
620
+ if (opts.mode === 'command') {
621
+ const executable = opts.executable || 'echo';
622
+ const cmdArgs = opts.args || [];
623
+ return await runSandboxedProcess(executable, cmdArgs, opts, this.name);
624
+ }
625
+ const sandboxDir = ensureSandboxDir(opts.workspaceRoot);
626
+ const ext = fileExtFor(opts.runner || 'node');
627
+ const testFile = path.join(sandboxDir, `verify-test-${uniqueSuffix()}${ext}`);
628
+ try {
629
+ fs.writeFileSync(testFile, opts.script || '', 'utf8');
630
+ let runnerBin = opts.runner || 'node';
631
+ let runnerArgs = [testFile];
632
+ if (opts.runner === 'tsx') {
633
+ runnerBin = 'npx';
634
+ runnerArgs = ['tsx', testFile];
635
+ }
636
+ else if (opts.runner === 'pnpm-tsx') {
637
+ runnerBin = 'pnpm';
638
+ runnerArgs = ['exec', 'tsx', testFile];
639
+ }
640
+ else if (opts.runner === 'yarn-tsx') {
641
+ runnerBin = 'yarn';
642
+ runnerArgs = ['tsx', testFile];
643
+ }
644
+ else if (opts.runner === 'deno') {
645
+ runnerBin = 'deno';
646
+ runnerArgs = ['run', '--allow-all', testFile];
647
+ }
648
+ return await runSandboxedProcess(runnerBin, runnerArgs, opts, this.name);
649
+ }
650
+ finally {
651
+ safeUnlink(testFile);
652
+ }
653
+ }
654
+ }
655
+ exports.__UnsafeHostSandboxForTests = __UnsafeHostSandboxForTests;
656
+ //# sourceMappingURL=verificationSandbox.js.map