@mettlelabs/mettle 0.0.0-beta.1 → 0.0.0-beta.1-linux-x64

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 (60) hide show
  1. package/THIRD_PARTY_NOTICES.md +11479 -0
  2. package/bin/mettle +4 -0
  3. package/harness/audit-investigation.mjs +203 -0
  4. package/harness/chunk-13tgmdey.mjs +33761 -0
  5. package/harness/chunk-1sygmp11.mjs +18142 -0
  6. package/harness/chunk-1ykxpmaz.mjs +7887 -0
  7. package/harness/chunk-2gt4cy7x.mjs +2290 -0
  8. package/harness/chunk-2pm2sayw.mjs +248 -0
  9. package/harness/chunk-32y07kkp.mjs +3747 -0
  10. package/harness/chunk-3m7jn5ac.mjs +16242 -0
  11. package/harness/chunk-3wvjs93w.mjs +538 -0
  12. package/harness/chunk-4bepyxmq.mjs +113 -0
  13. package/harness/chunk-4jtnt9k5.mjs +95 -0
  14. package/harness/chunk-50sbrf09.mjs +18972 -0
  15. package/harness/chunk-56csr8sw.mjs +322 -0
  16. package/harness/chunk-5cvb1jnb.mjs +41 -0
  17. package/harness/chunk-5wgmv4np.mjs +585 -0
  18. package/harness/chunk-65eyq32y.mjs +225 -0
  19. package/harness/chunk-6fnh6qhk.mjs +195 -0
  20. package/harness/chunk-6kgqw9ny.mjs +32303 -0
  21. package/harness/chunk-7b242j0n.mjs +14 -0
  22. package/harness/chunk-7bg2gnv2.mjs +7540 -0
  23. package/harness/chunk-ageqxj9v.mjs +63008 -0
  24. package/harness/chunk-b0qbt5jd.mjs +14 -0
  25. package/harness/chunk-b5g1r3fz.mjs +4785 -0
  26. package/harness/chunk-b71gpkxg.mjs +1514 -0
  27. package/harness/chunk-bdv1rx4a.mjs +20364 -0
  28. package/harness/chunk-c1631s2x.mjs +12241 -0
  29. package/harness/chunk-d5xgxsnp.mjs +74 -0
  30. package/harness/chunk-erg1fbh0.mjs +0 -0
  31. package/harness/chunk-fh78v9vn.mjs +19366 -0
  32. package/harness/chunk-fjx29qvp.mjs +50 -0
  33. package/harness/chunk-gryka6kg.mjs +39 -0
  34. package/harness/chunk-jb0r19et.mjs +180 -0
  35. package/harness/chunk-mfn574ss.mjs +179 -0
  36. package/harness/chunk-msg872ea.mjs +334 -0
  37. package/harness/chunk-nz5sv9as.mjs +1642 -0
  38. package/harness/chunk-p9d3cgvq.mjs +452 -0
  39. package/harness/chunk-pq6wvwxr.mjs +12 -0
  40. package/harness/chunk-qcm9arf8.mjs +7736 -0
  41. package/harness/chunk-qr8zfqjd.mjs +24 -0
  42. package/harness/chunk-rz2tz6jp.mjs +26 -0
  43. package/harness/chunk-sbkgc3kn.mjs +8 -0
  44. package/harness/chunk-v66qvpnb.mjs +169 -0
  45. package/harness/chunk-vjaxzab3.mjs +136 -0
  46. package/harness/chunk-vr1n5nza.mjs +1045 -0
  47. package/harness/chunk-vsf2zajv.mjs +3867 -0
  48. package/harness/chunk-wq253fj4.mjs +19041 -0
  49. package/harness/chunk-wyv7ceq4.mjs +0 -0
  50. package/harness/chunk-xnqknk2r.mjs +198 -0
  51. package/harness/chunk-xqyc4ca2.mjs +1890 -0
  52. package/harness/chunk-z6n9thea.mjs +45146 -0
  53. package/harness/chunk-zty0d3bw.mjs +0 -0
  54. package/harness/chunk-zvkxtj82.mjs +42 -0
  55. package/harness/issue-api-token.mjs +35 -0
  56. package/harness/launcher.mjs +159 -0
  57. package/harness/sandbox.mjs +132 -0
  58. package/harness/server.mjs +66322 -0
  59. package/package.json +13 -15
  60. package/bin/mettle.js +0 -51
@@ -0,0 +1,1890 @@
1
+ import {
2
+ HarnessError,
3
+ canonicalJson,
4
+ sha256,
5
+ digest,
6
+ ROUTE_CAPABILITIES
7
+ } from "./chunk-2gt4cy7x.mjs";
8
+ import {
9
+ exports_iso
10
+ } from "./chunk-b71gpkxg.mjs";
11
+ import {
12
+ string,
13
+ url,
14
+ number,
15
+ boolean,
16
+ unknown,
17
+ array,
18
+ object,
19
+ strictObject,
20
+ looseObject,
21
+ discriminatedUnion,
22
+ record,
23
+ _enum,
24
+ literal
25
+ } from "./chunk-1sygmp11.mjs";
26
+
27
+ // apps/harness/src/storage/observations.ts
28
+ var CHILD_TIMEOUT_MAX_MS = 20000;
29
+ var COMMIT_HEX = "[a-f0-9]{40}(?:[a-f0-9]{24})?";
30
+ var CommitSchema = string().regex(new RegExp(`^${COMMIT_HEX}$`));
31
+ var IdSchema = string().regex(/^[a-zA-Z0-9_-]{1,128}$/);
32
+ var RepositoryPathSchema = string().min(1).max(512).refine((path) => !path.startsWith("/") && Array.from(path).every((character) => character.codePointAt(0) >= 32 && character.codePointAt(0) !== 127 && character !== "\\" && character !== ":") && path.split("/").every((part) => part !== "" && part !== "." && part !== ".."));
33
+ var RepositoryConfigSchema = strictObject({
34
+ repository: string().min(1).max(256),
35
+ gitDirectory: string().refine((path) => path.startsWith("/")).optional(),
36
+ snapshotFile: string().startsWith("/").optional(),
37
+ snapshotSha256: string().regex(/^[a-f0-9]{64}$/).optional(),
38
+ commit: CommitSchema,
39
+ paths: array(RepositoryPathSchema).min(1).max(1e4)
40
+ });
41
+ function hasRepositorySource(config) {
42
+ return config.gitDirectory !== undefined !== (config.snapshotFile !== undefined) && config.snapshotFile !== undefined === (config.snapshotSha256 !== undefined);
43
+ }
44
+ var RepositoryTestProfileSchema = strictObject({
45
+ id: IdSchema,
46
+ entrypoint: RepositoryPathSchema,
47
+ timeoutMs: number().int().min(100).max(CHILD_TIMEOUT_MAX_MS)
48
+ });
49
+ var BashProfilesSchema = array(strictObject({
50
+ id: IdSchema,
51
+ description: string().min(1).max(256),
52
+ script: string().min(1).max(8192).refine((script) => !script.includes("\x00")),
53
+ timeoutMs: number().int().min(100).max(CHILD_TIMEOUT_MAX_MS)
54
+ })).min(1).max(8).refine((profiles) => new Set(profiles.map((profile) => profile.id)).size === profiles.length);
55
+ var SemgrepProfileSchema = strictObject({
56
+ version: string().regex(/^\d+\.\d+\.\d+$/),
57
+ rules: string().min(1).max(16384)
58
+ });
59
+ var WebCaptureSchema = strictObject({
60
+ id: IdSchema,
61
+ url: url().max(2048),
62
+ identity: string().min(1).max(128),
63
+ method: _enum(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]),
64
+ status: number().int().min(100).max(599),
65
+ body: string().max(8192)
66
+ });
67
+ var PermissionPolicySchema = strictObject({
68
+ id: string().min(1).max(128),
69
+ revision: string().min(1).max(128),
70
+ target: url().max(2048),
71
+ buildIdentityRef: string().min(1).max(2048),
72
+ approvedBy: string().min(1).max(256),
73
+ approvedAt: exports_iso.datetime(),
74
+ identities: array(strictObject({
75
+ id: IdSchema,
76
+ tenant: string().min(1).max(128),
77
+ role: string().min(1).max(128)
78
+ })).min(1).max(16),
79
+ rules: array(strictObject({
80
+ id: IdSchema,
81
+ identity: string().min(1).max(128),
82
+ resource: string().min(1).max(512),
83
+ action: string().min(1).max(128),
84
+ expected: _enum(["allow", "deny"])
85
+ })).min(1).max(64)
86
+ }).superRefine((policy, context) => {
87
+ const identities = new Set(policy.identities.map((identity) => identity.id));
88
+ const rules = new Set(policy.rules.map((rule) => rule.id));
89
+ if (identities.size !== policy.identities.length || rules.size !== policy.rules.length || policy.rules.some((rule) => !identities.has(rule.identity)))
90
+ context.addIssue({
91
+ code: "custom",
92
+ message: "Policy identities and rules must be unique and bound."
93
+ });
94
+ });
95
+ var GVISOR_RUNTIME = "mettle-runsc";
96
+ var SandboxRuntimeSchema = _enum([GVISOR_RUNTIME, "runc"]);
97
+ var SandboxConfigSchema = strictObject({
98
+ socket: string().regex(/^\/(?:[^\0\r\n]+)$/).max(100),
99
+ image: string().regex(/^[a-z0-9][a-z0-9./:_-]*@sha256:[a-f0-9]{64}$/),
100
+ runtime: SandboxRuntimeSchema.optional(),
101
+ engineId: string().min(1).max(128).optional(),
102
+ location: _enum(["local", "vm"]).optional()
103
+ }).refine((config) => config.location !== "vm" || sandboxRuntime(config) === GVISOR_RUNTIME && Boolean(config.engineId), "VM execution requires a pinned engine identity and mettle-runsc.");
104
+ function sandboxRuntime(config) {
105
+ return config.runtime ?? GVISOR_RUNTIME;
106
+ }
107
+ function sandboxAllowsExecution(config, tool) {
108
+ return sandboxRuntime(config) === GVISOR_RUNTIME || !["sandbox_shell", "bash_run", "test_run"].includes(tool);
109
+ }
110
+ var TaskNameSchema = _enum([
111
+ "web_capture_read",
112
+ "web_capture_compare",
113
+ "repo_read",
114
+ "repo_search",
115
+ "change_inspect",
116
+ "test_run",
117
+ "semgrep_scan",
118
+ "secret_scan",
119
+ "dependency_scan",
120
+ "bash_run",
121
+ "http_request",
122
+ "web_discover",
123
+ "web_request",
124
+ "web_browse",
125
+ "session_health",
126
+ "browser_workflow",
127
+ "nuclei_scan",
128
+ "sqli_confirm",
129
+ "zap_scan"
130
+ ]);
131
+ var SCANNER_CAPABILITIES = [
132
+ { profile: "secretScan", capability: "secrets" },
133
+ { profile: "dependencyScan", capability: "supply_chain" }
134
+ ];
135
+ var SUBJECT_CAPABILITIES = {
136
+ runtime: ROUTE_CAPABILITIES.web,
137
+ source: [
138
+ ...new Set([
139
+ ...ROUTE_CAPABILITIES.repository,
140
+ ...ROUTE_CAPABILITIES.pull_request,
141
+ ...SCANNER_CAPABILITIES.map(({ capability }) => capability)
142
+ ])
143
+ ]
144
+ };
145
+ var PROBE_TOOL = "sandbox_probe";
146
+ var SHELL_TOOL = "sandbox_shell";
147
+ var SANDBOX_TOOLS = [
148
+ PROBE_TOOL,
149
+ SHELL_TOOL,
150
+ ...TaskNameSchema.options
151
+ ];
152
+
153
+ // apps/harness/src/sandbox/worker.ts
154
+ import { setTimeout as sleep } from "node:timers/promises";
155
+
156
+ // apps/harness/src/tools/registry.ts
157
+ function counted(count, noun, plural = `${noun}s`) {
158
+ return `${count} ${count === 1 ? noun : plural}`;
159
+ }
160
+ var RAN = {
161
+ completed: "Completed",
162
+ passed: "Passed",
163
+ failed: "Failed",
164
+ timed_out: "Timed out",
165
+ output_limit: "Output limit reached",
166
+ unavailable: "Unavailable"
167
+ };
168
+ function ran(status, exitCode) {
169
+ return exitCode === null ? RAN[status] : `${RAN[status]} (exit ${exitCode})`;
170
+ }
171
+ function scanned(scan, coverage) {
172
+ if (scan.status !== "completed")
173
+ return `Not completed (${scan.status.replaceAll("_", " ")})`;
174
+ const count = scan.findingCount ?? scan.findings.length;
175
+ return `${counted(count, "finding")} · ${coverage}`;
176
+ }
177
+
178
+ class ToolRegistry {
179
+ tools = new Map;
180
+ constructor(definitions) {
181
+ if (!definitions.length)
182
+ throw new HarnessError("invalid_tools", "At least one registered tool is required.");
183
+ for (const definition of definitions) {
184
+ if (!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(definition.name) || this.tools.has(definition.name) || !definition.description)
185
+ throw new HarnessError("invalid_tools", "Tool metadata is invalid or duplicated.");
186
+ this.tools.set(definition.name, Object.freeze({ ...definition }));
187
+ }
188
+ }
189
+ describe() {
190
+ return [...this.tools.values()].map(({ name, description, schema }) => ({
191
+ name,
192
+ description,
193
+ schema
194
+ }));
195
+ }
196
+ select(names) {
197
+ return new ToolRegistry([...this.tools.values()].filter((tool) => names.includes(tool.name)));
198
+ }
199
+ admit(name, input, canonical) {
200
+ const tool = this.tools.get(name);
201
+ if (!tool)
202
+ throw new HarnessError("tool_denied", "The tool is not registered for this run.");
203
+ const parsed = tool.schema.safeParse(input);
204
+ if (!parsed.success || canonicalJson(parsed.data) !== canonical)
205
+ throw new HarnessError("tool_denied", "Tool arguments must match the finalized proposal without transformation.");
206
+ return tool;
207
+ }
208
+ }
209
+
210
+ // apps/harness/src/sandbox/docker.ts
211
+ import { request } from "node:http";
212
+
213
+ // apps/harness/src/storage/http-scope.ts
214
+ var HTTP_HEADER_LIMIT = 16384;
215
+ var HTTP_RESPONSE_LIMIT = 65536;
216
+ var WEBSITE_RESPONSE_LIMIT = 1048576;
217
+ function withinHttpScope(scope, url) {
218
+ return url.origin === scope.origin && !url.username && !url.password && !url.hash && !/%2f|%5c|%00|%25/i.test(url.pathname) && (url.pathname === scope.pathname || url.pathname.startsWith(scope.pathname.endsWith("/") ? scope.pathname : `${scope.pathname}/`));
219
+ }
220
+
221
+ // apps/harness/src/storage/scanners.ts
222
+ var EcosystemSchema = _enum([
223
+ "npm",
224
+ "PyPI",
225
+ "Go",
226
+ "crates.io",
227
+ "Maven",
228
+ "Packagist",
229
+ "RubyGems"
230
+ ]);
231
+ var MAX_DATABASE_BYTES = 268435456;
232
+ var SEMGREP_VERSION_TIMEOUT_MS = 60000;
233
+ var SEMGREP_SCAN_TIMEOUT_MS = 120000;
234
+ var SEMGREP_TASK_TIMEOUT_MS = SEMGREP_VERSION_TIMEOUT_MS + SEMGREP_SCAN_TIMEOUT_MS + 30000;
235
+ var SecretScanProfileSchema = strictObject({
236
+ version: literal("8.30.1")
237
+ });
238
+ var DependencyScanProfileSchema = strictObject({
239
+ version: literal("2.6.0"),
240
+ paths: array(RepositoryPathSchema).min(1).max(64).refine((paths) => new Set(paths).size === paths.length),
241
+ database: strictObject({
242
+ revision: string().min(1).max(128),
243
+ capturedAt: exports_iso.datetime(),
244
+ ecosystems: array(strictObject({
245
+ name: EcosystemSchema,
246
+ sha256: string().regex(/^[a-f0-9]{64}$/)
247
+ })).min(1).max(EcosystemSchema.options.length).refine((items) => new Set(items.map((item) => item.name)).size === items.length)
248
+ })
249
+ });
250
+ var SCANNER_FINDINGS_MAX = 40;
251
+ var SOURCE_SCANNER_TIMEOUT_MS = 120000;
252
+ var SOURCE_SCANNER_VERSION_TIMEOUT_MS = 1e4;
253
+ var SOURCE_SCANNER_TASK_TIMEOUT_MS = 150000;
254
+ var ScannerStatusSchema = _enum([
255
+ "completed",
256
+ "failed",
257
+ "timed_out",
258
+ "output_limit",
259
+ "unavailable",
260
+ "version_mismatch",
261
+ "database_mismatch"
262
+ ]);
263
+
264
+ // apps/harness/src/sandbox/limits.ts
265
+ var TASK_TIMEOUT_MS = 30000;
266
+ var TASK_INPUT_LIMIT = 1048576;
267
+ var TASK_RESULT_LIMIT = 32768;
268
+ var BROKER_RESPONSE_LIMIT = 4 * Math.ceil(WEBSITE_RESPONSE_LIMIT / 3) + 12 * HTTP_HEADER_LIMIT + 1024;
269
+ var CHILD_OUTPUT_KEEP = 4096;
270
+ var TASK_CONTAINER_LIFETIME_S = Math.ceil(SEMGREP_TASK_TIMEOUT_MS / 1000) + 15;
271
+ var SANDBOX_UID = 65534;
272
+ var REPO_SEARCH_FILES = 100;
273
+ var REPO_SEARCH_MATCHES = 20;
274
+ var REPO_EXCERPT = 256;
275
+
276
+ // apps/harness/src/sandbox/exchange.ts
277
+ class DockerExchange {
278
+ exchange;
279
+ write;
280
+ fail;
281
+ framed = Buffer.alloc(0);
282
+ lines = "";
283
+ total = 0;
284
+ pending = false;
285
+ final;
286
+ decoder = new TextDecoder("utf-8", { fatal: true });
287
+ constructor(exchange, write, fail) {
288
+ this.exchange = exchange;
289
+ this.write = write;
290
+ this.fail = fail;
291
+ }
292
+ receive(chunk) {
293
+ this.total += chunk.length;
294
+ if (this.total > 1048576)
295
+ throw new Error;
296
+ this.framed = Buffer.concat([this.framed, chunk]);
297
+ while (this.framed.length >= 8) {
298
+ if (this.framed[0] !== 1 || this.framed.readUIntBE(1, 3) !== 0)
299
+ throw new Error;
300
+ const length = this.framed.readUInt32BE(4);
301
+ if (length > 65536)
302
+ throw new Error;
303
+ if (this.framed.length < length + 8)
304
+ break;
305
+ this.lines += this.decoder.decode(this.framed.subarray(8, length + 8), {
306
+ stream: true
307
+ });
308
+ this.framed = this.framed.subarray(length + 8);
309
+ if (Buffer.byteLength(this.lines) > 65536)
310
+ throw new Error;
311
+ while (this.lines.includes(`
312
+ `)) {
313
+ const boundary = this.lines.indexOf(`
314
+ `);
315
+ const line = this.lines.slice(0, boundary);
316
+ this.lines = this.lines.slice(boundary + 1);
317
+ if (this.final || this.pending)
318
+ throw new Error;
319
+ const message = JSON.parse(line);
320
+ if (message && Object.keys(message).length === 1 && "request" in message) {
321
+ this.pending = true;
322
+ this.exchange(message.request).then((response) => Buffer.from(`${JSON.stringify({ response })}
323
+ `), () => Buffer.from(`${JSON.stringify({ error: "refused" })}
324
+ `)).then((encoded) => {
325
+ if (encoded.length > BROKER_RESPONSE_LIMIT)
326
+ throw new Error;
327
+ this.pending = false;
328
+ this.write(encoded);
329
+ }).catch(this.fail);
330
+ } else {
331
+ if (!message || Object.keys(message).sort().join(",") !== "inputDigest,value")
332
+ throw new Error;
333
+ const body = Buffer.from(line);
334
+ if (body.length > TASK_RESULT_LIMIT)
335
+ throw new Error;
336
+ const header = Buffer.alloc(8);
337
+ header[0] = 1;
338
+ header.writeUInt32BE(body.length, 4);
339
+ this.final = Buffer.concat([header, body]);
340
+ }
341
+ }
342
+ }
343
+ }
344
+ result() {
345
+ if (!this.final || this.pending || this.lines || this.framed.length || this.decoder.decode())
346
+ throw new HarnessError("sandbox_protocol", "The brokered task did not end with one complete, bound result.");
347
+ return this.final;
348
+ }
349
+ }
350
+
351
+ // apps/harness/src/sandbox/docker.ts
352
+ class DockerEngine {
353
+ socket;
354
+ constructor(socket) {
355
+ this.socket = socket;
356
+ }
357
+ startExec(id, input, signal, timeoutMs, exchange) {
358
+ if (!/^[a-f0-9]{64}$/.test(id) || input.length > TASK_INPUT_LIMIT)
359
+ throw new HarnessError("sandbox_input_limit", "Invalid isolated exec input.");
360
+ return new Promise((resolve, reject) => {
361
+ let complete;
362
+ let rejectResult;
363
+ const result = new Promise((resolveResult, failResult) => {
364
+ complete = resolveResult;
365
+ rejectResult = failResult;
366
+ });
367
+ result.catch(() => {
368
+ return;
369
+ });
370
+ const payload = JSON.stringify({ Detach: false, Tty: false });
371
+ const chunks = [];
372
+ let size = 0;
373
+ let transport;
374
+ let finished = false;
375
+ const client = request({
376
+ socketPath: this.socket,
377
+ method: "POST",
378
+ path: `/v1.47/exec/${id}/start`,
379
+ agent: false,
380
+ headers: {
381
+ Connection: "Upgrade",
382
+ Upgrade: "tcp",
383
+ "Content-Type": "application/json",
384
+ "Content-Length": Buffer.byteLength(payload)
385
+ }
386
+ });
387
+ const release = () => {
388
+ clearTimeout(timer);
389
+ signal.removeEventListener("abort", abort);
390
+ };
391
+ const fail = (code = "sandbox_transport") => {
392
+ if (finished)
393
+ return;
394
+ finished = true;
395
+ release();
396
+ const error = new HarnessError(code, "Isolated exec transport failed; no result is invented.");
397
+ reject(error);
398
+ rejectResult(error);
399
+ transport?.destroy();
400
+ client.destroy();
401
+ };
402
+ const abort = () => fail();
403
+ let timer = setTimeout(() => fail(), Math.min(2000, timeoutMs));
404
+ signal.addEventListener("abort", abort, { once: true });
405
+ client.once("error", () => fail());
406
+ client.once("response", (response) => {
407
+ response.destroy();
408
+ fail("sandbox_protocol");
409
+ });
410
+ client.once("upgrade", (response, socket, head) => {
411
+ transport = socket;
412
+ if (finished || response.statusCode !== 101 || response.headers.upgrade?.toLowerCase() !== "tcp") {
413
+ socket.destroy();
414
+ fail("sandbox_protocol");
415
+ return;
416
+ }
417
+ clearTimeout(timer);
418
+ timer = setTimeout(() => fail(), timeoutMs);
419
+ const interactive = exchange ? new DockerExchange(exchange, (bytes) => {
420
+ if (!finished)
421
+ socket.write(bytes);
422
+ }, () => fail("web_uncertain")) : undefined;
423
+ const receive = (chunk) => {
424
+ if (interactive) {
425
+ try {
426
+ interactive.receive(chunk);
427
+ } catch {
428
+ fail("web_uncertain");
429
+ }
430
+ return;
431
+ }
432
+ size += chunk.length;
433
+ if (size > 65536)
434
+ fail("sandbox_output_limit");
435
+ else
436
+ chunks.push(chunk);
437
+ };
438
+ socket.on("data", receive);
439
+ socket.once("error", () => fail());
440
+ socket.once("close", () => {
441
+ if (!finished)
442
+ fail();
443
+ });
444
+ socket.once("end", () => {
445
+ if (finished)
446
+ return;
447
+ let body;
448
+ try {
449
+ body = interactive ? interactive.result() : Buffer.concat(chunks);
450
+ } catch {
451
+ fail("web_uncertain");
452
+ return;
453
+ }
454
+ finished = true;
455
+ release();
456
+ complete({ status: 200, body });
457
+ socket.destroy();
458
+ });
459
+ receive(head);
460
+ if (finished)
461
+ return;
462
+ resolve({ result });
463
+ if (interactive)
464
+ socket.write(Buffer.concat([input, Buffer.from(`
465
+ `)]));
466
+ else
467
+ socket.end(input);
468
+ });
469
+ if (signal.aborted)
470
+ fail();
471
+ else
472
+ client.end(payload);
473
+ });
474
+ }
475
+ call(method, path, body, signal, timeoutMs = 2000, limit = 65536) {
476
+ return new Promise((resolve, reject) => {
477
+ const payload = body === undefined ? undefined : JSON.stringify(body);
478
+ const client = request({
479
+ socketPath: this.socket,
480
+ method,
481
+ path: `/v1.47${path}`,
482
+ agent: false,
483
+ signal,
484
+ headers: payload ? {
485
+ "Content-Type": "application/json",
486
+ "Content-Length": Buffer.byteLength(payload)
487
+ } : {}
488
+ }, (response) => {
489
+ const chunks = [];
490
+ let size = 0;
491
+ response.on("data", (chunk) => {
492
+ size += chunk.length;
493
+ if (size > limit) {
494
+ response.destroy();
495
+ client.destroy();
496
+ reject(new HarnessError("sandbox_output_limit", "Sandbox transport output exceeded its limit."));
497
+ } else
498
+ chunks.push(chunk);
499
+ });
500
+ response.once("end", () => {
501
+ clearTimeout(timer);
502
+ resolve({
503
+ status: response.statusCode ?? 0,
504
+ body: Buffer.concat(chunks)
505
+ });
506
+ });
507
+ response.once("error", fail);
508
+ });
509
+ const timer = setTimeout(() => client.destroy(), timeoutMs);
510
+ const fail = () => {
511
+ clearTimeout(timer);
512
+ reject(new HarnessError("sandbox_transport", "The local sandbox engine request failed; its effect may be uncertain."));
513
+ };
514
+ client.once("error", fail);
515
+ client.once("close", () => clearTimeout(timer));
516
+ client.end(payload);
517
+ });
518
+ }
519
+ }
520
+ function dockerJson(response, status = 200) {
521
+ if (response.status !== status)
522
+ throw new HarnessError("sandbox_engine", "The sandbox engine rejected an operation.");
523
+ try {
524
+ return JSON.parse(response.body.toString("utf8"));
525
+ } catch {
526
+ throw new HarnessError("sandbox_protocol", "The sandbox engine returned an invalid response.");
527
+ }
528
+ }
529
+ function dockerStatus(response, status) {
530
+ if (response.status !== status)
531
+ throw new HarnessError("sandbox_engine", "The sandbox engine rejected an operation.");
532
+ }
533
+ function dockerCreated(response) {
534
+ if (response.status === 409)
535
+ throw new HarnessError("sandbox_engine", "The sandbox engine reports a conflicting container name; it must be reconciled before reuse.");
536
+ if (response.status !== 201)
537
+ throw new HarnessError("sandbox_rejected", "The sandbox engine acknowledged and refused container creation; no container was created.");
538
+ return dockerJson(response, 201);
539
+ }
540
+ function dockerFrames(response, streams = "stdout") {
541
+ dockerStatus(response, 200);
542
+ const chunks = [];
543
+ let offset = 0;
544
+ while (offset < response.body.length) {
545
+ const header = response.body.subarray(offset, offset + 8);
546
+ if (header.length !== 8 || header[0] !== 1 && (streams === "stdout" || header[0] !== 2) || header.readUIntBE(1, 3) !== 0)
547
+ throw new HarnessError("sandbox_output", "The sandbox returned invalid output.");
548
+ const end = offset + 8 + header.readUInt32BE(4);
549
+ if (end > response.body.length)
550
+ throw new HarnessError("sandbox_output", "The sandbox output was truncated.");
551
+ chunks.push(response.body.subarray(offset + 8, end));
552
+ offset = end;
553
+ }
554
+ return Buffer.concat(chunks);
555
+ }
556
+ function dockerLogs(response, streams = "stdout") {
557
+ const bytes = dockerFrames(response, streams);
558
+ try {
559
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
560
+ } catch {
561
+ throw new HarnessError("sandbox_output", "Sandbox output is not valid UTF-8.");
562
+ }
563
+ }
564
+
565
+ // apps/harness/src/sandbox/policy.ts
566
+ var PROBE_MARKER = `mettle-sandbox-probe-v2
567
+ `;
568
+ var PROBE_TIMEOUT_MS = 1e4;
569
+ var SHELL_OUTPUT_MAX = 16384;
570
+ var SHELL_KILL_AFTER_S = PROBE_TIMEOUT_MS / 1000 + 5;
571
+ var SHELL_LOG_LIMIT = 262144;
572
+ var ENGINE_LAUNCH_TIMEOUT_MS = 30000;
573
+ var RESOURCE_PROFILES = {
574
+ probe: {
575
+ Memory: 268435456,
576
+ MemorySwap: 268435456,
577
+ NanoCpus: 500000000,
578
+ PidsLimit: 32
579
+ },
580
+ tools: {
581
+ Memory: 1073741824,
582
+ MemorySwap: 1073741824,
583
+ NanoCpus: 1e9,
584
+ PidsLimit: 256,
585
+ Tmpfs: { "/tmp": "rw,noexec,nosuid,nodev,size=256m" }
586
+ },
587
+ browser: {
588
+ Memory: 2147483648,
589
+ MemorySwap: 2147483648,
590
+ NanoCpus: 2000000000,
591
+ PidsLimit: 512,
592
+ Tmpfs: { "/tmp": "rw,noexec,nosuid,nodev,size=512m" }
593
+ }
594
+ };
595
+ var ShellCommandSchema = strictObject({
596
+ command: string().min(1).max(2048).refine((command) => !command.includes("\x00"))
597
+ });
598
+ var RUNC_NETWORK_ASSERTION = [
599
+ "for flags_file in /sys/class/net/*/flags; do",
600
+ ' case "$flags_file" in */lo/flags) continue ;; esac',
601
+ ' flags=$(cat "$flags_file")',
602
+ ' test "$((flags & 1))" -eq 0',
603
+ "done",
604
+ `awk 'NR > 1 && $1 != "lo" { exit 1 }' /proc/net/route`,
605
+ "if test -e /proc/net/ipv6_route; then",
606
+ ` awk '$10 != "lo" { exit 1 }' /proc/net/ipv6_route`,
607
+ "fi"
608
+ ].join(`
609
+ `);
610
+ function probeScript(runtime) {
611
+ return [
612
+ "set -eu",
613
+ 'test "$(id -u)" = 65534',
614
+ 'test "$(id -g)" = 65534',
615
+ `awk '$1 == "CapEff:" { seen=1; if ($2 !~ /^0+$/) exit 1 } END { if (!seen) exit 1 }' /proc/self/status`,
616
+ `awk '$1 == "NoNewPrivs:" { seen=1; if ($2 != 1) exit 1 } END { if (!seen) exit 1 }' /proc/self/status`,
617
+ ...runtime === "runc" ? [
618
+ `awk '$1 == "Seccomp:" { seen=1; if ($2 != 2) exit 1 } END { if (!seen) exit 1 }' /proc/self/status`
619
+ ] : [],
620
+ `awk '$2 == "/" && $4 ~ /(^|,)ro(,|$)/ { seen=1 } END { if (!seen) exit 1 }' /proc/mounts`,
621
+ runtime === "runc" ? RUNC_NETWORK_ASSERTION : `awk -F: 'NR > 2 { name=$1; gsub(/ /, "", name); if (name != "lo") exit 1 }' /proc/net/dev`,
622
+ `/usr/local/bin/node -e 'const server = require("node:net").createServer(); server.on("error", () => process.exit(1)); server.listen(0, "127.0.0.1", () => server.close());'`,
623
+ "test ! -e /var/run/docker.sock",
624
+ "test ! -e /run/docker.sock",
625
+ "test ! -e /host",
626
+ "sleep 2",
627
+ "printf 'mettle-sandbox-probe-v2\\n'"
628
+ ].join(`
629
+ `);
630
+ }
631
+ function sandboxConfig(environment = process.env) {
632
+ const parsed = SandboxConfigSchema.safeParse({
633
+ socket: environment.HARNESS_SANDBOX_SOCKET ?? "/var/run/docker.sock",
634
+ image: environment.HARNESS_SANDBOX_IMAGE,
635
+ ...environment.HARNESS_SANDBOX_RUNTIME !== undefined ? { runtime: environment.HARNESS_SANDBOX_RUNTIME } : {}
636
+ });
637
+ if (!parsed.success)
638
+ throw new HarnessError("sandbox_config", "Use an approved repository@sha256 image, a local Unix socket, and HARNESS_SANDBOX_RUNTIME=mettle-runsc or runc.");
639
+ return Object.freeze(parsed.data);
640
+ }
641
+ function launchSpec(image, script, labels, profile, runtime) {
642
+ return {
643
+ Image: image,
644
+ User: "65534:65534",
645
+ WorkingDir: "/",
646
+ Entrypoint: ["/bin/sh"],
647
+ Cmd: ["-c", script],
648
+ Env: ["PATH=/usr/bin:/bin"],
649
+ Labels: labels,
650
+ NetworkDisabled: false,
651
+ Tty: false,
652
+ OpenStdin: false,
653
+ Healthcheck: { Test: ["NONE"] },
654
+ HostConfig: {
655
+ Runtime: runtime,
656
+ NetworkMode: "none",
657
+ ReadonlyRootfs: true,
658
+ Privileged: false,
659
+ CapDrop: ["ALL"],
660
+ SecurityOpt: [
661
+ "no-new-privileges:true",
662
+ ...runtime === "runc" ? ["seccomp=builtin"] : []
663
+ ],
664
+ ...RESOURCE_PROFILES[profile],
665
+ RestartPolicy: { Name: "no", MaximumRetryCount: 0 },
666
+ LogConfig: {
667
+ Type: "json-file",
668
+ Config: { "max-size": "64k", "max-file": "1" }
669
+ }
670
+ }
671
+ };
672
+ }
673
+ var SHELL_BOUND = `timeout -s KILL ${SHELL_KILL_AFTER_S} /bin/sh -c "$1" sh "$2"; exit "$?"`;
674
+ var SHELL_CAPTURE = [
675
+ "exec 4>&1",
676
+ `status=$( { { /bin/sh -c "$1" 2>&1 3>&- 4>&-; echo "$?" >&3; } | { head -c ${SHELL_OUTPUT_MAX + 1} >&4; cat >/dev/null; }; } 3>&1 )`,
677
+ 'exit "$status"'
678
+ ].join(`
679
+ `);
680
+ function shellSpec(image, command, labels, runtime) {
681
+ const base = launchSpec(image, SHELL_BOUND, labels, "probe", runtime);
682
+ return {
683
+ ...base,
684
+ Cmd: ["-c", SHELL_BOUND, "sh", SHELL_CAPTURE, command],
685
+ HostConfig: {
686
+ ...base.HostConfig,
687
+ LogConfig: {
688
+ Type: "json-file",
689
+ Config: { "max-size": "2m", "max-file": "1" }
690
+ }
691
+ }
692
+ };
693
+ }
694
+ function shellPolicyDigest(image, command, runtime) {
695
+ return digest({
696
+ spec: shellSpec(image, command, {}, runtime),
697
+ timeoutMs: PROBE_TIMEOUT_MS
698
+ });
699
+ }
700
+ function probePolicyDigest(image, runtime) {
701
+ return digest({
702
+ spec: launchSpec(image, probeScript(runtime), {}, "probe", runtime),
703
+ timeoutMs: PROBE_TIMEOUT_MS
704
+ });
705
+ }
706
+ function sandboxLabels(record) {
707
+ return {
708
+ "mettle.sandbox": "probe-v1",
709
+ "mettle.run": record.runId,
710
+ "mettle.call": record.callId,
711
+ "mettle.epoch": String(record.epoch),
712
+ "mettle.worker": record.workerId,
713
+ "mettle.policy": record.policyDigest
714
+ };
715
+ }
716
+
717
+ // apps/harness/src/storage/active-scanners.ts
718
+ import { isIPv4 } from "node:net";
719
+ var ACTIVE_SCAN_TIMEOUT_MS = 180000;
720
+ var ACTIVE_SCAN_TASK_TIMEOUT_MS = 210000;
721
+ var ACTIVE_SCAN_TOOLS = [
722
+ "nuclei_scan",
723
+ "sqli_confirm",
724
+ "zap_scan"
725
+ ];
726
+ var common = {
727
+ id: IdSchema,
728
+ url: url().max(2048),
729
+ maxRequests: number().int().min(1).max(128),
730
+ requestsPerSecond: number().int().min(1).max(5),
731
+ maxResponseBytes: number().int().min(1024).max(HTTP_RESPONSE_LIMIT),
732
+ maxTotalBytes: number().int().min(1024).max(4194304)
733
+ };
734
+ var ActiveScanProfileSchema = discriminatedUnion("tool", [
735
+ strictObject({
736
+ ...common,
737
+ tool: literal("nuclei_scan"),
738
+ version: literal("3.11.1"),
739
+ template: strictObject({
740
+ id: IdSchema,
741
+ name: string().min(1).max(256),
742
+ public: boolean(),
743
+ paths: array(string().startsWith("/").max(512).refine((path) => !/[{}#\\\s]/.test(path))).min(1).max(8),
744
+ status: array(number().int().min(200).max(599)).min(1).max(8),
745
+ words: array(string().min(1).max(256)).min(1).max(8)
746
+ })
747
+ }),
748
+ strictObject({
749
+ ...common,
750
+ tool: literal("sqli_confirm"),
751
+ version: literal("1.10"),
752
+ parameter: string().regex(/^[a-zA-Z0-9_]{1,64}$/),
753
+ technique: _enum(["B", "E"])
754
+ }),
755
+ strictObject({
756
+ ...common,
757
+ tool: literal("zap_scan"),
758
+ version: literal("2.17.0"),
759
+ rules: array(number().int().positive().max(999999)).min(1).max(8).refine((rules) => new Set(rules).size === rules.length)
760
+ })
761
+ ]);
762
+ var ActiveScannersSchema = strictObject({
763
+ approvedBy: string().min(1).max(256),
764
+ expiresAt: exports_iso.datetime(),
765
+ disposableTarget: strictObject({
766
+ instanceId: IdSchema,
767
+ buildIdentityRef: string().min(1).max(2048)
768
+ }),
769
+ endpoints: array(strictObject({
770
+ origin: url().max(2048),
771
+ address: string().refine((address) => {
772
+ if (!isIPv4(address))
773
+ return false;
774
+ const [first, second] = address.split(".").map(Number);
775
+ return first !== 0 && first < 224 && !(first === 169 && second === 254) && address !== "168.63.129.16" && address !== "100.100.100.200";
776
+ })
777
+ })).min(1).max(4),
778
+ profiles: array(ActiveScanProfileSchema).min(1).max(8)
779
+ });
780
+ function scannerEndpoint(config, target, proposed) {
781
+ const url2 = new URL(proposed);
782
+ const scope = new URL(target);
783
+ const endpoint = config.endpoints.find((entry) => entry.origin === url2.origin);
784
+ if (!endpoint || url2.href !== proposed || !["http:", "https:"].includes(url2.protocol) || !withinHttpScope(new URL(scope.pathname, endpoint.origin), url2))
785
+ throw new HarnessError("scanner_denied", "The scanner request is outside its approved origin, port or path.");
786
+ return endpoint;
787
+ }
788
+ function validateActiveScanners(config, target, buildIdentityRef) {
789
+ if (config.disposableTarget.buildIdentityRef !== buildIdentityRef || new Set(config.endpoints.map((entry) => entry.origin)).size !== config.endpoints.length || new Set(config.profiles.map((entry) => entry.id)).size !== config.profiles.length)
790
+ throw new HarnessError("scanner_denied", "Scanner grants require unique endpoints/profiles and the approved disposable build identity.");
791
+ for (const endpoint of config.endpoints) {
792
+ const url2 = new URL(endpoint.origin);
793
+ if (url2.origin !== endpoint.origin || url2.hostname !== new URL(target).hostname || !["http:", "https:"].includes(url2.protocol))
794
+ throw new HarnessError("scanner_denied", "Scanner origins must explicitly name the target's HTTP(S) ports.");
795
+ }
796
+ for (const profile of config.profiles) {
797
+ scannerEndpoint(config, target, profile.url);
798
+ if (profile.tool === "nuclei_scan")
799
+ for (const path of profile.template.paths)
800
+ scannerEndpoint(config, target, new URL(path, profile.url).href);
801
+ if (profile.tool === "sqli_confirm" && new URL(profile.url).searchParams.getAll(profile.parameter).length !== 1)
802
+ throw new HarnessError("scanner_denied", "SQLMap requires one approved query parameter and URL.");
803
+ }
804
+ }
805
+ var SCANNER_REQUEST_STRIP_HEADERS = [
806
+ "host",
807
+ "connection",
808
+ "content-length",
809
+ "transfer-encoding",
810
+ "upgrade",
811
+ "trailer",
812
+ "te",
813
+ "keep-alive",
814
+ "accept-encoding"
815
+ ];
816
+ function scannerRequestAllowed(profile, url2, method) {
817
+ const approved = new URL(profile.url);
818
+ if (url2.origin !== approved.origin)
819
+ return false;
820
+ if (profile.tool === "zap_scan")
821
+ return true;
822
+ if (method !== "GET" && method !== "HEAD")
823
+ return false;
824
+ if (profile.tool === "nuclei_scan")
825
+ return profile.template.paths.some((path) => new URL(path, approved).href === url2.href);
826
+ if (url2.pathname !== approved.pathname || url2.searchParams.getAll(profile.parameter).length !== 1)
827
+ return false;
828
+ const fixed = (entry) => [...entry.searchParams].filter(([name]) => name !== profile.parameter);
829
+ return JSON.stringify(fixed(url2)) === JSON.stringify(fixed(approved));
830
+ }
831
+ function sqlmapCandidate(finding, profile) {
832
+ return finding.status === "candidate" && finding.verification.result === "pending" && finding.cwe === "CWE-89" && finding.subject.kind === "runtime" && ("endpoint" in finding && finding.endpoint ? finding.endpoint.url === profile.url && finding.endpoint.method === "GET" && finding.endpoint.parameter === profile.parameter : finding.location === profile.url);
833
+ }
834
+
835
+ // apps/harness/src/sandbox/active-program.ts
836
+ var ACTIVE_PROGRAM = String.raw`
837
+ async function activeScan(tool, input, send) {
838
+ const fs = require("node:fs/promises");
839
+ const http = require("node:http");
840
+ const tls = require("node:tls");
841
+ const { spawn } = require("node:child_process");
842
+ const root = await fs.mkdtemp("/tmp/mettle-active-");
843
+ const profile = input.profile;
844
+ const env = { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, PYTHONDONTWRITEBYTECODE: "1", JAVA_TOOL_OPTIONS: "-Djava.awt.headless=true -Duser.home=" + root };
845
+ const processes = new Set();
846
+ const sockets = new Set();
847
+ const tunnels = new WeakMap();
848
+ let proxyFailure;
849
+ const run = (command, args, timeout = ${ACTIVE_SCAN_TIMEOUT_MS}) => new Promise((resolve, reject) => {
850
+ const child = spawn(command, args, { cwd: root, env, shell: false, stdio: ["ignore", "pipe", "pipe"], detached: true });
851
+ processes.add(child);
852
+ let bytes = 0;
853
+ let stdout = "";
854
+ let stderr = "";
855
+ let error;
856
+ const kill = () => { if (child.pid) { try { process.kill(-child.pid, "SIGKILL"); } catch {} } };
857
+ const timer = setTimeout(() => { error = new Error("timed_out"); kill(); }, timeout);
858
+ for (const [stream, field] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) stream.on("data", (chunk) => {
859
+ bytes += chunk.length;
860
+ if (field === "stdout") stdout += chunk.toString(); else stderr += chunk.toString();
861
+ if (bytes > 1048576) { error = new Error("output_limit"); kill(); }
862
+ });
863
+ child.once("error", (cause) => { error = cause; });
864
+ child.once("close", (code) => { clearTimeout(timer); processes.delete(child); if (error) reject(error); else resolve({ code, stdout, stderr }); });
865
+ });
866
+ const read = async (name) => {
867
+ const file = root + "/" + name;
868
+ const stat = await fs.lstat(file);
869
+ if (!stat.isFile() || stat.size > 1048576) throw new Error("output_limit");
870
+ return fs.readFile(file, "utf8");
871
+ };
872
+ const proxy = http.createServer({ maxHeaderSize: 16384 }, async (request, response) => {
873
+ try {
874
+ const origin = tunnels.get(request.socket);
875
+ const url = origin ? new URL(request.url, origin).href : request.url;
876
+ if (origin && new URL(url).origin !== origin) throw new Error();
877
+ let size = 0;
878
+ const chunks = [];
879
+ for await (const chunk of request) { size += chunk.length; if (size > 8192) throw new Error(); chunks.push(chunk); }
880
+ const headers = {};
881
+ for (const [key, value] of Object.entries(request.headers)) {
882
+ if (typeof value === "string" && !${JSON.stringify(SCANNER_REQUEST_STRIP_HEADERS)}.includes(key) && !key.startsWith("proxy-")) headers[key] = value;
883
+ }
884
+ const body = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
885
+ const result = await send({ url, method: request.method, headers, ...(size ? { body } : {}) });
886
+ response.writeHead(result.status, { ...result.headers, "content-type": result.contentType, connection: "close" });
887
+ response.end(Buffer.from(result.body, "base64"));
888
+ } catch (error) { proxyFailure = error; response.writeHead(502); response.end(); }
889
+ });
890
+ proxy.on("connection", (socket) => { sockets.add(socket); socket.on("close", () => sockets.delete(socket)); socket.setTimeout(15000, () => socket.destroy()); });
891
+ proxy.on("upgrade", (_request, socket) => { proxyFailure = new Error("upgrade_denied"); socket.destroy(); });
892
+ proxy.on("clientError", (error, socket) => { proxyFailure = error; socket.destroy(); });
893
+ try {
894
+ const certificate = await run("/usr/bin/openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", "-subj", "/CN=mettle-scanner-proxy", "-keyout", root + "/key.pem", "-out", root + "/cert.pem"], 10000);
895
+ if (certificate.code !== 0) throw new Error("proxy_unavailable");
896
+ const secureContext = tls.createSecureContext({ key: await read("key.pem"), cert: await read("cert.pem") });
897
+ proxy.on("connect", (request, socket, head) => {
898
+ try {
899
+ const url = new URL("https://" + request.url);
900
+ if (head.length || (request.url !== url.host && request.url !== url.hostname + ":443") || url.username || url.password || url.pathname !== "/") throw new Error();
901
+ socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
902
+ const secure = new tls.TLSSocket(socket, { isServer: true, secureContext });
903
+ tunnels.set(secure, url.origin);
904
+ secure.on("error", (error) => { proxyFailure = error; secure.destroy(); });
905
+ proxy.emit("connection", secure);
906
+ } catch (error) { proxyFailure = error; socket.destroy(); }
907
+ });
908
+ await new Promise((resolve, reject) => { proxy.once("error", reject); proxy.listen(0, "127.0.0.1", resolve); });
909
+ const proxyUrl = "http://127.0.0.1:" + proxy.address().port;
910
+ let findings = [];
911
+ if (tool === "nuclei_scan") {
912
+ const version = await run("/usr/local/bin/nuclei", ["-version"], 10000);
913
+ if (version.code !== 0 || !(version.stdout + version.stderr).includes("v" + profile.version)) throw new Error("version_mismatch");
914
+ const template = { id: profile.template.id, info: { name: profile.template.name, author: "mettle", severity: "info" }, http: [{ method: "GET", path: profile.template.paths.map((path) => new URL(path, profile.url).href), "matchers-condition": "and", matchers: [{ type: "status", status: profile.template.status }, { type: "word", part: "body", words: profile.template.words, condition: "and" }] }] };
915
+ await fs.writeFile(root + "/template.yaml", JSON.stringify(template));
916
+ await fs.writeFile(root + "/config.yaml", "{}\n");
917
+ const scanned = await run("/usr/local/bin/nuclei", ["-u", profile.url, "-t", root + "/template.yaml", "-config", root + "/config.yaml", "-o", root + "/nuclei.jsonl", "-jsonl", "-silent", "-omit-raw", "-omit-template", "-no-stdin", "-no-interactsh", "-disable-update-check", "-disable-redirects", "-no-httpx", "-proxy", proxyUrl, "-proxy-internal", "-rate-limit", String(profile.requestsPerSecond), "-c", "1", "-bs", "1", "-retries", "0", "-timeout", "5"]);
918
+ if (scanned.code !== 0 || /\[(?:ERR|FTL)\]/.test(scanned.stderr)) throw new Error("scan_failed");
919
+ findings = (await read("nuclei.jsonl")).split("\n").filter(Boolean).map((line) => {
920
+ const item = JSON.parse(line);
921
+ if (item["template-id"] !== profile.template.id || typeof item["matched-at"] !== "string") throw new Error();
922
+ return { ruleId: item["template-id"], url: item["matched-at"], title: profile.template.name };
923
+ });
924
+ } else if (tool === "sqli_confirm") {
925
+ const version = await run("/usr/bin/python3", ["/opt/mettle/sqlmap/sqlmap.py", "--version"], 10000);
926
+ if (version.code !== 0 || version.stdout.split("\n")[0].trim() !== profile.version + "#stable") throw new Error("version_mismatch");
927
+ await fs.writeFile(root + "/targets.txt", profile.url + "\n");
928
+ const scanned = await run("/usr/bin/python3", ["/opt/mettle/sqlmap/sqlmap.py", "-m", root + "/targets.txt", "-p", profile.parameter, "--technique=" + profile.technique, "--batch", "--skip-waf", "--level=1", "--risk=1", "--threads=1", "--timeout=5", "--retries=0", "--delay=" + String(1 / profile.requestsPerSecond), "--proxy=" + proxyUrl, "--ignore-redirects", "--disable-coloring", "--flush-session", "--output-dir=" + root + "/sqlmap", "--results-file=" + root + "/sqlmap.csv", "-v", "0"]);
929
+ const errors = (scanned.stdout + scanned.stderr).split("\n").filter((line) => /\[(?:CRITICAL|ERROR)\]/.test(line));
930
+ if (scanned.code !== 0 || errors.some((line) => !/\[(?:CRITICAL|ERROR)\] all tested parameters do not appear to be injectable\./.test(line))) throw new Error("scan_failed");
931
+ const parsed = await run("/usr/bin/python3", ["-c", "import csv,json,sys; print(json.dumps(list(csv.DictReader(open(sys.argv[1],newline='')))))", root + "/sqlmap.csv"], 10000);
932
+ if (parsed.code !== 0) throw new Error("scan_failed");
933
+ findings = JSON.parse(parsed.stdout).flatMap((item) => {
934
+ if (item['Target URL'] !== profile.url) throw new Error();
935
+ if (!item['Technique(s)'] || item['Note(s)']) return [];
936
+ if (item.Place !== "GET" || item.Parameter !== profile.parameter) throw new Error();
937
+ return [{ ruleId: "sqlmap-" + profile.technique, url: profile.url, title: "SQL injection observation", parameter: profile.parameter }];
938
+ });
939
+ } else if (tool === "zap_scan") {
940
+ const version = await run("/usr/bin/python3", ["-c", "import sys; from zipfile import ZipFile; archive = ZipFile('/zap/zap-' + sys.argv[1] + '.jar'); assert archive.getinfo('META-INF/MANIFEST.MF').file_size <= 16384; print(archive.read('META-INF/MANIFEST.MF').decode('utf-8'))", profile.version], 10000);
941
+ if (version.code !== 0 || !version.stdout.split(/\r?\n/).includes("Implementation-Version: " + profile.version)) throw new Error("version_mismatch");
942
+ const plan = { env: { contexts: [{ name: "approved", urls: [new URL(profile.url).origin + new URL(profile.url).pathname] }], parameters: { failOnError: true, failOnWarning: true, progressToStdout: false } }, jobs: [
943
+ { type: "passiveScan-config", parameters: { disableAllRules: true } },
944
+ { type: "activeScan-config", inputVectors: { scripts: false } },
945
+ { type: "requestor", parameters: {}, requests: [{ url: profile.url, method: "GET" }] },
946
+ { type: "activeScan", parameters: { context: "approved", maxRuleDurationInMins: 0, maxScanDurationInMins: 0, threadPerHost: 1, delayInMs: Math.ceil(1000 / profile.requestsPerSecond) }, policyDefinition: { defaultStrength: "Low", defaultThreshold: "Off", rules: profile.rules.map((id) => ({ id, strength: "Low", threshold: "Medium" })) } },
947
+ { type: "report", parameters: { template: "traditional-json", reportDir: root, reportFile: "zap.json" } }
948
+ ] };
949
+ await fs.writeFile(root + "/plan.yaml", JSON.stringify(plan));
950
+ const scanned = await run("/zap/zap.sh", ["-Xmx768m", "-silent", "-cmd", "-dir", root + "/zap", "-autorun", root + "/plan.yaml", "-config", "network.connection.httpProxy.enabled=true", "-config", "network.connection.httpProxy.host=127.0.0.1", "-config", "network.connection.httpProxy.port=" + proxy.address().port, "-config", "network.connection.timeoutInSecs=5", "-config", "autoupdate.checkOnStart=false", "-config", "autoupdate.downloadNewRelease=false", "-config", "autoupdate.installAddonUpdates=false"]);
951
+ if (scanned.code !== 0) throw new Error("scan_failed");
952
+ const report = JSON.parse(await read("zap.json"));
953
+ if (report['@version'] !== profile.version || !Array.isArray(report.site)) throw new Error();
954
+ findings = report.site.flatMap((site) => site.alerts.flatMap((alert) => {
955
+ if (!profile.rules.includes(Number(alert.pluginid))) throw new Error();
956
+ return alert.instances.map((item) => ({ ruleId: String(alert.pluginid), url: item.uri, title: String(alert.name).slice(0, 256) }));
957
+ }));
958
+ } else throw new Error();
959
+ if (proxyFailure) throw proxyFailure;
960
+ if (findings.some((finding) => typeof finding.url !== "string" || finding.url.length > 1024)) throw new Error();
961
+ return { status: "completed", version: profile.version, findingCount: findings.length, findings: findings.slice(0, 10), complete: findings.length <= 10 };
962
+ } catch {
963
+ return { status: "failed", version: profile.version, findingCount: 0, findings: [], complete: false };
964
+ } finally {
965
+ for (const child of processes) { try { process.kill(-child.pid, "SIGKILL"); } catch {} }
966
+ for (const socket of sockets) socket.destroy();
967
+ await new Promise((resolve) => proxy.close(resolve));
968
+ await fs.rm(root, { recursive: true, force: true });
969
+ }
970
+ }
971
+ `;
972
+
973
+ // apps/harness/src/sandbox/website-program.ts
974
+ var WEBSITE_PROGRAM = String.raw`
975
+ function websiteScope(input, raw, method = "GET") {
976
+ try {
977
+ const url = new URL(raw);
978
+ return (input.scopes || [{ url: input.target, methods: ["GET", "HEAD"] }]).some((entry) => {
979
+ const scope = new URL(entry.url);
980
+ return entry.methods.includes(method) && url.origin === scope.origin && !url.username && !url.password && !url.hash &&
981
+ !/%(?:2f|5c|00|25)/i.test(url.pathname) &&
982
+ (url.pathname === scope.pathname || url.pathname.startsWith(scope.pathname.endsWith("/") ? scope.pathname : scope.pathname + "/"));
983
+ });
984
+ } catch { return false; }
985
+ }
986
+ function websiteLinks(body, base) {
987
+ const result = [];
988
+ for (const match of body.matchAll(/<(a|script|form)\b([^>]{0,8192})>/gi)) {
989
+ const tag = match[1].toLowerCase();
990
+ const attribute = tag === "script" ? "src" : tag === "form" ? "action" : "href";
991
+ const value = new RegExp("\\b" + attribute + "\\s*=\\s*[\"']([^\"'<>\\s]{1,2048})[\"']", "i").exec(match[2]);
992
+ if (!value) continue;
993
+ try {
994
+ const url = new URL(value[1].replace(/&amp;/g, "&"), base); url.hash = "";
995
+ if (!["https:", "http:"].includes(url.protocol) || url.href.length > 2048) continue;
996
+ const formMethod = /\bmethod\s*=\s*["']post["']/i.test(match[2]) ? "POST" : "GET";
997
+ result.push({ url: url.href, method: tag === "form" ? formMethod : "GET", kind: tag === "form" ? "form" : tag === "script" ? "script" : "page" });
998
+ } catch {}
999
+ }
1000
+ return result.sort((a, b) => (a.kind === "page" ? 0 : 1) - (b.kind === "page" ? 0 : 1));
1001
+ }
1002
+ async function websiteObserve(tool, input, send) {
1003
+ const initial = { url: input.url || input.target, method: input.method || "GET", ...(input.body !== undefined ? { body: input.body } : {}), ...(input.headers ? { headers: input.headers } : {}) };
1004
+ const queue = [{ request: initial, depth: 0 }];
1005
+ const seen = new Set(), pages = [], endpoints = new Map(), leads = new Map();
1006
+ let omitted = 0, redirects = 0;
1007
+ while (queue.length && pages.length < input.maxPages && seen.size < 100) {
1008
+ const next = queue.shift();
1009
+ if (seen.has(next.request.url)) continue;
1010
+ seen.add(next.request.url);
1011
+ const response = await send(next.request);
1012
+ const body = Buffer.from(response.body, "base64").toString("utf8");
1013
+ if ([301,302,303,307,308].includes(response.status)) {
1014
+ const location = response.headers && response.headers.location;
1015
+ if (typeof location === "string" && redirects++ < 5 && ["GET", "HEAD"].includes(next.request.method)) {
1016
+ const redirect = new URL(location, next.request.url); redirect.hash = "";
1017
+ if (websiteScope(input, redirect.href, next.request.method)) queue.unshift({ request: { url: redirect.href, method: next.request.method }, depth: next.depth });
1018
+ else { omitted++; leads.set(redirect.href, { url: redirect.href, method: "GET", kind: "redirect", inScope: false }); }
1019
+ } else omitted++;
1020
+ }
1021
+ const url = new URL(next.request.url);
1022
+ endpoints.set(url.href, { id: crypto.createHash("sha256").update(url.href).digest("hex"), url: url.href, method: next.request.method, parameters: [...new Set(url.searchParams.keys())].slice(0,16), observed: true });
1023
+ const textual = /^(text\/|application\/(.*json|.*xml|javascript|x-javascript))/i.test(response.contentType);
1024
+ pages.push({ url: url.href, status: response.status, contentType: response.contentType, excerpt: textual ? (response.excerpt ?? body).slice(0, tool === "web_request" ? 2048 : 512) : "[Binary response; body not retained as text]", truncated: textual && body.length > (tool === "web_request" ? 2048 : 512), ...(response.evidenceRef ? { evidenceRef: response.evidenceRef } : {}) });
1025
+ if (!response.contentType.includes("text/html")) continue;
1026
+ for (const link of websiteLinks(body, url.href)) {
1027
+ const inScope = websiteScope(input, link.url, link.method);
1028
+ if (leads.size < 40) leads.set(link.url, { ...link, inScope }); else omitted++;
1029
+ if (!inScope) { omitted++; continue; }
1030
+ if (endpoints.size < 40 && !endpoints.has(link.url)) endpoints.set(link.url, { id: crypto.createHash("sha256").update(link.url).digest("hex"), url: link.url, method: link.method, parameters: [], observed: false });
1031
+ if (tool !== "web_discover" || link.kind !== "page") continue;
1032
+ if (/\.(?:css|png|jpe?g|gif|svg|ico|avif|webp|woff2?|ttf|mp4|pdf)(?:\?|$)/i.test(link.url)) continue;
1033
+ if (next.depth < input.maxDepth && queue.length < 40 && !seen.has(link.url)) queue.push({ request: { url: link.url, method: "GET" }, depth: next.depth + 1 });
1034
+ else if (!seen.has(link.url)) omitted++;
1035
+ }
1036
+ }
1037
+ const value = { pages, endpoints: [...endpoints.values()], leads: [...leads.values()], omitted: omitted + queue.length,
1038
+ coverage: "Bounded HTTP observations and static page/script/form discovery. Linked origins do not grant access. Unvisited leads and authenticated/business workflows remain untested unless separately recorded.", complete: false, verification: "not-independent" };
1039
+ while (Buffer.byteLength(JSON.stringify(value)) > 16000) {
1040
+ if (value.leads.length) value.leads.pop(); else if (value.endpoints.length) value.endpoints.pop(); else value.pages.pop();
1041
+ value.omitted++;
1042
+ }
1043
+ return value;
1044
+ }
1045
+
1046
+ async function websiteBrowse(input, send) {
1047
+ const version = require("/opt/mettle-browser/node_modules/playwright/package.json").version;
1048
+ if (version !== input.version) throw new Error("browser_version");
1049
+ const { chromium } = require("/opt/mettle-browser/node_modules/playwright");
1050
+ const browser = await chromium.launch({ headless: true, chromiumSandbox: false, timeout: 10000, args: ["--disable-dev-shm-usage"] });
1051
+ const blocked = [];
1052
+ let failure, requests = 0, pending = Promise.resolve(), accepting = true;
1053
+ try {
1054
+ const context = await browser.newContext({ serviceWorkers: "block", acceptDownloads: false });
1055
+ context.setDefaultTimeout(5000);
1056
+ await context.routeWebSocket("**/*", (socket) => { if (blocked.length < 20) blocked.push({ url: socket.url().slice(0, 2048), reason: "websocket" }); socket.close(); });
1057
+ await context.route("**/*", async (route) => {
1058
+ const request = route.request();
1059
+ const url = request.url();
1060
+ const hints = request.headers();
1061
+ if (hints["next-router-prefetch"] === "1" || /(?:^|[\s,;])prefetch(?:$|[\s,;])/.test(hints["sec-purpose"] || hints.purpose || "")) {
1062
+ if (blocked.length < 20) blocked.push({ url: url.slice(0, 2048), reason: "prefetch" });
1063
+ await route.abort(); return;
1064
+ }
1065
+ if (!accepting || !websiteScope(input, url, request.method()) || ["image", "font", "media"].includes(request.resourceType()) || requests >= 40) {
1066
+ if (blocked.length < 20) blocked.push({ url: url.slice(0, 2048), reason: !accepting ? "workflow_complete" : !websiteScope(input, url, request.method()) ? "scope" : requests >= 40 ? "request_limit" : "asset" });
1067
+ await route.abort(); return;
1068
+ }
1069
+ requests++;
1070
+ const work = pending.then(async () => {
1071
+ try {
1072
+ if (failure) { await route.abort(); return; }
1073
+ const headers = Object.fromEntries(Object.entries(await request.allHeaders()).filter(([name]) => input.requestHeaders.includes(name)));
1074
+ const resourceType = request.resourceType();
1075
+ const response = await send({ url, method: request.method(), headers, ...(["script", "stylesheet"].includes(resourceType) ? { resourceType } : {}), ...(request.postData() !== null ? { body: request.postData() } : {}) });
1076
+ const responseHeaders = Object.fromEntries(Object.entries(response.headers).filter(([, value]) => value !== undefined).map(([name, value]) => [name, Array.isArray(value) ? value.join(name === "set-cookie" ? "\n" : ", ") : value]));
1077
+ await route.fulfill({ status: response.status, headers: responseHeaders, body: Buffer.from(response.body, "base64") });
1078
+ } catch (error) { failure = error; await route.abort().catch(() => {}); }
1079
+ });
1080
+ pending = work;
1081
+ await work;
1082
+ });
1083
+ const page = await context.newPage();
1084
+ await page.goto(input.url, { waitUntil: "domcontentloaded", timeout: 60000 });
1085
+ for (const action of input.actions) {
1086
+ if (action.type === "click") await page.locator(action.selector).click();
1087
+ else if (action.type === "fill") await page.locator(action.selector).fill(action.value);
1088
+ else if (action.type === "waitFor") await page.locator(action.selector).filter({ hasText: action.text }).waitFor({ state: "visible" });
1089
+ else throw new Error("browser_action");
1090
+ }
1091
+ await pending;
1092
+ if (failure) throw failure;
1093
+ const text = await page.locator("body").innerText();
1094
+ const value = { url: page.url(), title: (await page.title()).slice(0, 256), text: text.slice(0, 4096), truncated: text.length > 4096, actions: input.actions.length, blocked,
1095
+ complete: false, coverage: "One isolated browser workflow; blocked resources, omitted assets and all unexercised workflows remain untested. Cookies exist only within this call.", verification: "not-independent" };
1096
+ accepting = false;
1097
+ await pending;
1098
+ if (failure) throw failure;
1099
+ await context.close();
1100
+ return value;
1101
+ } finally { await browser.close(); }
1102
+ }
1103
+ `;
1104
+
1105
+ // apps/harness/src/sandbox/live-program.ts
1106
+ var LIVE_PROGRAM = String.raw`
1107
+ const readline = require("node:readline");
1108
+ const crypto = require("node:crypto");
1109
+ const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
1110
+ const iterator = lines[Symbol.asyncIterator]();
1111
+ ${ACTIVE_PROGRAM}
1112
+ ${WEBSITE_PROGRAM}
1113
+ (async () => {
1114
+ if (process.getuid() !== ${SANDBOX_UID}) throw new Error();
1115
+ const first = await iterator.next();
1116
+ if (first.done || Buffer.byteLength(first.value) > ${TASK_INPUT_LIMIT}) throw new Error();
1117
+ const { tool, input } = JSON.parse(first.value);
1118
+ let queue = Promise.resolve();
1119
+ const send = (request) => {
1120
+ const result = queue.then(async () => {
1121
+ process.stdout.write(JSON.stringify({ request }) + "\n");
1122
+ const next = await iterator.next();
1123
+ if (next.done || Buffer.byteLength(next.value) > ${BROKER_RESPONSE_LIMIT}) throw new Error();
1124
+ const message = JSON.parse(next.value);
1125
+ if (!message || !("response" in message)) throw new Error();
1126
+ return message.response;
1127
+ });
1128
+ queue = result;
1129
+ return result;
1130
+ };
1131
+ let value;
1132
+ if (tool === "web_discover" || tool === "web_request") {
1133
+ value = await websiteObserve(tool, input, send);
1134
+ } else if (tool === "web_browse") {
1135
+ value = await websiteBrowse(input, send);
1136
+ } else if (tool === "http_request") {
1137
+ const response = await send({ ...input.request, requestId: input.requestId });
1138
+ const body = Buffer.from(response.body, "base64").toString("utf8");
1139
+ value = { requestId: input.requestId, status: response.status, contentType: response.contentType, body: body.slice(0, 8192), truncated: body.length > 8192 };
1140
+ } else if (tool === "session_health") {
1141
+ value = { status: "healthy" };
1142
+ } else if (tool === "browser_workflow") {
1143
+ const version = require("/opt/mettle-browser/node_modules/playwright/package.json").version;
1144
+ if (version !== input.version) throw new Error();
1145
+ const { chromium } = require("/opt/mettle-browser/node_modules/playwright");
1146
+ const browser = await chromium.launch({ headless: true, chromiumSandbox: false, timeout: 10000, args: ["--disable-dev-shm-usage"] });
1147
+ try {
1148
+ const context = await browser.newContext({ serviceWorkers: "block", acceptDownloads: false });
1149
+ context.setDefaultTimeout(3000);
1150
+ await context.routeWebSocket("**/*", (socket) => socket.close());
1151
+ let failure;
1152
+ await context.route("**/*", async (route) => {
1153
+ try {
1154
+ const request = route.request();
1155
+ const response = await send({ url: request.url(), method: request.method(), ...(request.postData() !== null ? { body: request.postData() } : {}) });
1156
+ await route.fulfill({ status: response.status, contentType: response.contentType, body: Buffer.from(response.body, "base64") });
1157
+ } catch (error) { failure = error; await route.abort(); }
1158
+ });
1159
+ const page = await context.newPage();
1160
+ await page.goto(input.url, { waitUntil: "domcontentloaded", timeout: 10000 });
1161
+ for (const action of input.actions) {
1162
+ if (action.type === "click") await page.locator(action.selector).click();
1163
+ else if (action.type === "fill") await page.locator(action.selector).fill(action.value);
1164
+ else throw new Error();
1165
+ }
1166
+ await queue;
1167
+ if (failure) throw failure;
1168
+ const content = await page.locator("body").innerText();
1169
+ value = { workflowId: input.workflowId, url: page.url(), title: await page.title(), text: content.slice(0, 8192), truncated: content.length > 8192, actions: input.actions.length };
1170
+ await context.close();
1171
+ await queue;
1172
+ if (failure) throw failure;
1173
+ } finally { await browser.close(); }
1174
+ } else if (${JSON.stringify(ACTIVE_SCAN_TOOLS)}.includes(tool)) {
1175
+ value = await activeScan(tool, input, send);
1176
+ } else throw new Error();
1177
+ const result = JSON.stringify({ inputDigest: crypto.createHash("sha256").update(first.value).digest("hex"), value });
1178
+ if (Buffer.byteLength(result) > ${TASK_RESULT_LIMIT}) throw new Error();
1179
+ process.stdout.write(result + "\n");
1180
+ lines.close();
1181
+ process.stdin.destroy();
1182
+ })().catch(() => { lines.close(); process.stdin.destroy(); process.exitCode = 1; });
1183
+ `;
1184
+
1185
+ // apps/harness/src/sandbox/scanner-program.ts
1186
+ var SPAWN_STATUS_PROGRAM = `
1187
+ function spawnStatus(result, success = "completed", exceeded = false) {
1188
+ return result.error?.code === "ETIMEDOUT" ? "timed_out" : result.error?.code === "ENOBUFS" || exceeded ? "output_limit" : result.error ? "unavailable" : result.status === 0 ? success : "failed";
1189
+ }
1190
+ `;
1191
+ var SOURCE_SCANNER_PROGRAM = String.raw`
1192
+ function scanSource(tool, input, workspace, root) {
1193
+ const path = require("node:path");
1194
+ const child = require("node:child_process");
1195
+ const secret = tool === "secret_scan";
1196
+ const binary = secret ? "/usr/local/bin/gitleaks" : "/usr/local/bin/osv-scanner";
1197
+ const environment = { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, LC_ALL: "C" };
1198
+ if (!secret) environment.OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY = "/opt/mettle/osv";
1199
+ const invoke = (args, timeout, maxBuffer) => child.spawnSync(binary, args, {
1200
+ cwd: workspace, env: environment, stdio: ["ignore", "pipe", "pipe"],
1201
+ timeout, killSignal: "SIGKILL", maxBuffer, encoding: "utf8", shell: false
1202
+ });
1203
+
1204
+ const incomplete = (status) => ({ status, complete: false, findingCount: null, findings: [], scannedFiles: [] });
1205
+ const version = invoke(secret ? ["version"] : ["--version"], ${SOURCE_SCANNER_VERSION_TIMEOUT_MS}, 4096);
1206
+ if (version.error || version.status !== 0) return incomplete(spawnStatus(version));
1207
+ if (!String(version.stdout).split(/\s+/).some((part) => part.replace(/^v/, "") === input.version)) return incomplete("version_mismatch");
1208
+ const approved = new Set(input.files.map((file) => file.path));
1209
+ const relative = (name) => {
1210
+ if (typeof name !== "string") throw new Error();
1211
+ const normalized = path.isAbsolute(name) ? path.relative(workspace, name) : name.replace(/^\.\//, "");
1212
+ if (!approved.has(normalized)) throw new Error();
1213
+ return normalized;
1214
+ };
1215
+ const bounded = (value, max = 256) => {
1216
+ if (typeof value !== "string" || !value.length || value.length > max) throw new Error();
1217
+ return value;
1218
+ };
1219
+ let args;
1220
+ if (secret) {
1221
+ fs.writeFileSync(path.join(root, "gitleaks.toml"), "[extend]\nuseDefault = true\n", { flag: "wx", mode: 0o600 });
1222
+ fs.writeFileSync(path.join(root, "ignore"), "", { flag: "wx", mode: 0o600 });
1223
+ args = ["dir", "--config", path.join(root, "gitleaks.toml"), "--gitleaks-ignore-path", path.join(root, "ignore"), "--ignore-gitleaks-allow", "--redact=100", "--no-banner", "--no-color", "--log-level", "error", "--exit-code", "10", "--max-archive-depth", "0", "--max-decode-depth", "0", "--report-format", "json", "--report-path", path.join(root, "gitleaks.json"), workspace];
1224
+ } else {
1225
+ try {
1226
+ for (const entry of input.database.ecosystems) {
1227
+ if (!${JSON.stringify(EcosystemSchema.options)}.includes(entry.name)) throw new Error();
1228
+ const file = "/opt/mettle/osv/osv-scalibr/" + entry.name + "/all.zip";
1229
+ const info = fs.statSync(file);
1230
+ if (!info.isFile() || info.size <= 0 || info.size > ${MAX_DATABASE_BYTES}) throw new Error();
1231
+ if (crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== entry.sha256) throw new Error();
1232
+ }
1233
+ } catch { return incomplete("database_mismatch"); }
1234
+ fs.writeFileSync(path.join(root, "osv.toml"), "", { flag: "wx", mode: 0o600 });
1235
+ args = ["scan", "source", "--offline", "--no-call-analysis=all", "--no-resolve", "--no-ignore", "--all-packages", "--config", path.join(root, "osv.toml"), "--format", "json", "--recursive", workspace];
1236
+ }
1237
+ const result = invoke(args, ${SOURCE_SCANNER_TIMEOUT_MS}, 1048576);
1238
+ if (result.error || ![0, secret ? 10 : 1].includes(result.status)) return incomplete(spawnStatus(result));
1239
+ try {
1240
+ if (secret && fs.statSync(path.join(root, "gitleaks.json")).size > 1048576) return incomplete("output_limit");
1241
+ const raw = JSON.parse(secret ? fs.readFileSync(path.join(root, "gitleaks.json"), "utf8") : result.stdout);
1242
+ const findings = [];
1243
+ const scanned = new Set();
1244
+ let count = 0;
1245
+ const add = (finding) => { count++; if (findings.length < ${SCANNER_FINDINGS_MAX}) findings.push(finding); };
1246
+ if (secret) {
1247
+ if (!Array.isArray(raw)) throw new Error();
1248
+ for (const finding of raw) {
1249
+ if (!Number.isInteger(finding.StartLine) || finding.StartLine < 1 || !Number.isInteger(finding.EndLine) || finding.EndLine < finding.StartLine) throw new Error();
1250
+ add({ rule: bounded(finding.RuleID, 128), path: relative(finding.File), line: finding.StartLine, endLine: finding.EndLine });
1251
+ }
1252
+ if ((result.status === 10) !== (count > 0)) throw new Error();
1253
+ } else {
1254
+ if (!raw || !Array.isArray(raw.results)) throw new Error();
1255
+ const ecosystems = new Set(input.database.ecosystems.map((entry) => entry.name));
1256
+ for (const source of raw.results) {
1257
+ const file = relative(source.source?.path);
1258
+ if (!Array.isArray(source.packages)) throw new Error();
1259
+ scanned.add(file);
1260
+ for (const entry of source.packages) {
1261
+ const ecosystem = bounded(entry.package?.ecosystem, 64);
1262
+ if (!ecosystems.has(ecosystem) || !Array.isArray(entry.vulnerabilities ?? [])) throw new Error();
1263
+ for (const advisory of entry.vulnerabilities ?? []) add({
1264
+ path: file, ecosystem, package: bounded(entry.package.name), version: bounded(entry.package.version, 128), id: bounded(advisory.id, 128)
1265
+ });
1266
+ }
1267
+ }
1268
+ if ((result.status === 1) !== (count > 0) || !scanned.size) throw new Error();
1269
+ }
1270
+ const value = { status: "completed", complete: count <= ${SCANNER_FINDINGS_MAX} && (secret || scanned.size === approved.size), findingCount: count, findings, scannedFiles: secret ? "approved" : [...scanned] };
1271
+ return Buffer.byteLength(JSON.stringify(value)) > ${TASK_RESULT_LIMIT - 1024} ? incomplete("output_limit") : value;
1272
+ } catch { return incomplete("failed"); }
1273
+ }
1274
+ `;
1275
+
1276
+ // apps/harness/src/sandbox/task-program.ts
1277
+ var TASK_PROGRAM = String.raw`
1278
+ const fs = require("node:fs");
1279
+ const crypto = require("node:crypto");
1280
+ ${SPAWN_STATUS_PROGRAM}
1281
+ ${SOURCE_SCANNER_PROGRAM}
1282
+ try {
1283
+ const bytes = fs.readFileSync(0);
1284
+ if (bytes.length > ${TASK_INPUT_LIMIT} || process.getuid() !== ${SANDBOX_UID}) throw new Error();
1285
+ const request = JSON.parse(bytes.toString("utf8"));
1286
+ const { tool, args, input } = request;
1287
+ let value;
1288
+ if (tool === "web_capture_read") {
1289
+ if (args.id !== input.capture.id) throw new Error();
1290
+ value = { ...input.capture, provenance: "operator-supplied-unverified" };
1291
+ } else if (tool === "web_capture_compare") {
1292
+ const { left, right } = input;
1293
+ if (left.id !== args.left || right.id !== args.right || left.id === right.id || left.url !== right.url || left.method !== right.method) throw new Error();
1294
+ value = {
1295
+ left: left.id, right: right.id, url: left.url, method: left.method,
1296
+ leftIdentity: left.identity, rightIdentity: right.identity,
1297
+ leftStatus: left.status, rightStatus: right.status,
1298
+ sameBody: left.body === right.body, provenance: "operator-supplied-unverified"
1299
+ };
1300
+ } else if (tool === "change_inspect") {
1301
+ value = input;
1302
+ } else if (["test_run", "semgrep_scan", "bash_run", "secret_scan", "dependency_scan"].includes(tool)) {
1303
+ const path = require("node:path");
1304
+ const child = require("node:child_process");
1305
+ const root = fs.mkdtempSync("/tmp/mettle-task-");
1306
+ const workspace = path.join(root, "workspace");
1307
+ fs.mkdirSync(workspace, { recursive: true });
1308
+ for (const file of input.files) {
1309
+ if (!file.path || file.path.startsWith("/") || file.path.includes("\\") || file.path.split("/").some((part) => !part || part === "." || part === "..")) throw new Error();
1310
+ const destination = path.join(workspace, file.path);
1311
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
1312
+ fs.writeFileSync(destination, file.content, { flag: "wx", mode: 0o600 });
1313
+ }
1314
+ if (tool === "secret_scan" || tool === "dependency_scan") {
1315
+ value = scanSource(tool, input, workspace, root);
1316
+ } else if (tool === "bash_run") {
1317
+ const profile = input.profile;
1318
+ if (args.profileId !== profile.id || typeof profile.script !== "string" || !profile.script.length || profile.script.length > 8192 || profile.script.includes("\0") || !Number.isInteger(profile.timeoutMs) || profile.timeoutMs < 100 || profile.timeoutMs > ${CHILD_TIMEOUT_MAX_MS}) throw new Error();
1319
+ const script = path.join(root, "approved.bash");
1320
+ fs.writeFileSync(script, profile.script, { flag: "wx", mode: 0o600 });
1321
+ const result = child.spawnSync("/bin/bash", ["--noprofile", "--norc", "-p", "--", script], {
1322
+ cwd: workspace, env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, LC_ALL: "C" },
1323
+ stdio: ["ignore", "pipe", "pipe"], timeout: profile.timeoutMs, killSignal: "SIGKILL",
1324
+ maxBuffer: ${TASK_INPUT_LIMIT}, encoding: "utf8", shell: false
1325
+ });
1326
+ const stdout = result.stdout || "";
1327
+ const stderr = result.stderr || "";
1328
+ const exceeded = result.error?.code === "ENOBUFS";
1329
+ value = {
1330
+ subject: input.subject, profileId: profile.id,
1331
+ scriptSha256: crypto.createHash("sha256").update(profile.script).digest("hex"), verification: "not-independent",
1332
+ status: spawnStatus(result, "completed", exceeded),
1333
+ exitCode: result.status, signal: result.signal ?? null,
1334
+ stdout: stdout.slice(0, ${CHILD_OUTPUT_KEEP}), stderr: stderr.slice(0, ${CHILD_OUTPUT_KEEP}),
1335
+ truncated: stdout.length > ${CHILD_OUTPUT_KEEP} || stderr.length > ${CHILD_OUTPUT_KEEP} || exceeded
1336
+ };
1337
+ } else if (tool === "semgrep_scan") {
1338
+ const environment = { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, SEMGREP_SETTINGS_FILE: "/dev/null", SEMGREP_SEND_METRICS: "off", SEMGREP_ENABLE_VERSION_CHECK: "0" };
1339
+ const version = child.spawnSync("/usr/local/bin/semgrep", ["--version"], { cwd: workspace, env: environment, timeout: ${SEMGREP_VERSION_TIMEOUT_MS}, killSignal: "SIGKILL", maxBuffer: 4096, encoding: "utf8", shell: false });
1340
+ if (version.error?.code === "ETIMEDOUT") value = { status: "timed_out", output: "" };
1341
+ else if (version.error || version.status !== 0) value = { status: "unavailable", output: "" };
1342
+ else if (version.stdout.trim() !== input.version) value = { status: "version_mismatch", output: "" };
1343
+ else {
1344
+ const rules = path.join(root, "rules.yaml");
1345
+ fs.writeFileSync(rules, input.rules, { flag: "wx", mode: 0o600 });
1346
+ const scanned = child.spawnSync("/usr/local/bin/semgrep", ["scan", "--config", rules, "--json", "--quiet", "--metrics=off", "--disable-version-check", "--oss-only", "--jobs=1", "."], { cwd: workspace, env: environment, timeout: ${SEMGREP_SCAN_TIMEOUT_MS}, killSignal: "SIGKILL", maxBuffer: ${TASK_INPUT_LIMIT}, encoding: "utf8", shell: false });
1347
+ let status = spawnStatus(scanned);
1348
+ let output = "";
1349
+ if (status === "completed") {
1350
+ try {
1351
+ const report = JSON.parse(scanned.stdout || "");
1352
+ const results = Array.isArray(report.results) ? report.results : [];
1353
+ output = JSON.stringify({
1354
+ version: report.version,
1355
+ errors: (Array.isArray(report.errors) ? report.errors : []).slice(0, 1000).map(() => ({})),
1356
+ paths: { scanned: (report.paths && Array.isArray(report.paths.scanned) ? report.paths.scanned : []).slice(0, 10000) },
1357
+ resultCount: results.length,
1358
+ results: results.slice(0, ${SCANNER_FINDINGS_MAX}).map((entry) => ({ check_id: entry.check_id, path: entry.path, start: { line: entry.start.line }, end: { line: entry.end.line }, extra: { severity: entry.extra.severity, message: String(entry.extra.message).slice(0, 1024) } }))
1359
+ });
1360
+ } catch { status = "failed"; }
1361
+ }
1362
+ value = { status, output };
1363
+ }
1364
+ } else {
1365
+ if (args.profileId !== input.profile.id || !input.files.some((file) => file.path === input.profile.entrypoint) || input.profile.timeoutMs < 100 || input.profile.timeoutMs > ${CHILD_TIMEOUT_MAX_MS}) throw new Error();
1366
+ const result = child.spawnSync("/usr/local/bin/node", ["--test", "--", input.profile.entrypoint], {
1367
+ cwd: workspace, env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root },
1368
+ timeout: input.profile.timeoutMs, killSignal: "SIGKILL", maxBuffer: ${TASK_INPUT_LIMIT},
1369
+ encoding: "utf8", shell: false
1370
+ });
1371
+ const stdout = result.stdout || "";
1372
+ const stderr = result.stderr || "";
1373
+ value = {
1374
+ repository: input.repository, revision: input.revision, profileId: input.profile.id,
1375
+ patchProposalId: input.patchProposalId, verification: "not-independent",
1376
+ status: spawnStatus(result, "passed"),
1377
+ exitCode: result.status, stdout: stdout.slice(0, ${CHILD_OUTPUT_KEEP}), stderr: stderr.slice(0, ${CHILD_OUTPUT_KEEP}),
1378
+ truncated: stdout.length > ${CHILD_OUTPUT_KEEP} || stderr.length > ${CHILD_OUTPUT_KEEP} || result.error?.code === "ENOBUFS"
1379
+ };
1380
+ }
1381
+ } else if (tool === "repo_read" || tool === "repo_search") {
1382
+ const binding = { repository: input.repository, revision: input.revision };
1383
+ if (tool === "repo_read") {
1384
+ const file = input.files.find((entry) => entry.path === args.path);
1385
+ if (!file) throw new Error();
1386
+ value = { ...binding, path: file.path, content: file.content, sha256: crypto.createHash("sha256").update(file.content).digest("hex") };
1387
+ } else {
1388
+ const matches = [];
1389
+ let scanned = 0;
1390
+ let limited = false;
1391
+ for (const file of input.files) {
1392
+ for (const [index, line] of file.content.split("\n").entries()) {
1393
+ if (!line.includes(args.text)) continue;
1394
+ matches.push({ path: file.path, line: index + 1, excerpt: line.slice(0, ${REPO_EXCERPT}) });
1395
+ if (matches.length === ${REPO_SEARCH_MATCHES}) { limited = true; break; }
1396
+ }
1397
+ if (limited) break;
1398
+ scanned++;
1399
+ }
1400
+ value = { ...binding, scanned, total: input.total, complete: !limited && scanned === input.total, matches };
1401
+ }
1402
+ } else throw new Error();
1403
+ const result = JSON.stringify({ inputDigest: crypto.createHash("sha256").update(bytes).digest("hex"), value });
1404
+ if (Buffer.byteLength(result) > ${TASK_RESULT_LIMIT}) throw new Error();
1405
+ fs.writeFileSync(1, result);
1406
+ } catch { process.exitCode = 1; }
1407
+ `;
1408
+
1409
+ // apps/harness/src/sandbox/tasks.ts
1410
+ var SandboxReceiptSchema = strictObject({
1411
+ runtime: SandboxRuntimeSchema,
1412
+ image: string(),
1413
+ policyDigest: string(),
1414
+ inputDigest: string(),
1415
+ cleanupVerified: literal(true)
1416
+ });
1417
+ function isolatedTask(tool, args, input, session) {
1418
+ TaskNameSchema.parse(tool);
1419
+ const bytes = Buffer.from(canonicalJson({ tool, args, input }), "utf8");
1420
+ if (bytes.length > TASK_INPUT_LIMIT)
1421
+ throw new HarnessError("sandbox_input_limit", "The isolated task exceeds its one-MiB input limit.");
1422
+ return {
1423
+ tool,
1424
+ args,
1425
+ input: bytes,
1426
+ inputDigest: sha256(bytes.toString("utf8")),
1427
+ session
1428
+ };
1429
+ }
1430
+ function taskProfile(tool) {
1431
+ return ["browser_workflow", "web_browse", "zap_scan"].includes(tool) ? "browser" : "tools";
1432
+ }
1433
+ function taskTimeoutMs(tool) {
1434
+ if (["web_discover", "web_browse"].includes(tool))
1435
+ return ACTIVE_SCAN_TASK_TIMEOUT_MS;
1436
+ if (ACTIVE_SCAN_TOOLS.some((name) => name === tool))
1437
+ return ACTIVE_SCAN_TASK_TIMEOUT_MS;
1438
+ if (["secret_scan", "dependency_scan"].includes(tool))
1439
+ return SOURCE_SCANNER_TASK_TIMEOUT_MS;
1440
+ return tool === "semgrep_scan" ? SEMGREP_TASK_TIMEOUT_MS : TASK_TIMEOUT_MS;
1441
+ }
1442
+ function taskSpec(image, labels, profile, runtime) {
1443
+ return {
1444
+ ...launchSpec(image, "", labels, profile, runtime),
1445
+ Entrypoint: ["/bin/sleep"],
1446
+ Cmd: [String(TASK_CONTAINER_LIFETIME_S)]
1447
+ };
1448
+ }
1449
+ function taskExecSpec(live, runtime) {
1450
+ const base = launchSpec("", "", {}, "probe", runtime);
1451
+ const program = live ? LIVE_PROGRAM : TASK_PROGRAM;
1452
+ return {
1453
+ User: base.User,
1454
+ Privileged: false,
1455
+ Tty: false,
1456
+ AttachStdin: true,
1457
+ AttachStdout: true,
1458
+ AttachStderr: true,
1459
+ WorkingDir: base.WorkingDir,
1460
+ Env: base.Env,
1461
+ Cmd: [
1462
+ "/usr/local/bin/node",
1463
+ "-e",
1464
+ runtime === "runc" ? `if (!/^Seccomp:\\s+2$/m.test(require("node:fs").readFileSync("/proc/self/status", "utf8"))) process.exit(1);
1465
+ ${program}` : program
1466
+ ]
1467
+ };
1468
+ }
1469
+ function taskRuntimeDigest(image, runtime) {
1470
+ return digest({
1471
+ specs: {
1472
+ tools: taskSpec(image, {}, "tools", runtime),
1473
+ browser: taskSpec(image, {}, "browser", runtime)
1474
+ },
1475
+ exec: taskExecSpec(false, runtime),
1476
+ liveExec: taskExecSpec(true, runtime),
1477
+ timeoutMs: Object.fromEntries(TaskNameSchema.options.map((tool) => [tool, taskTimeoutMs(tool)]))
1478
+ });
1479
+ }
1480
+ function taskPolicyDigest(image, task, runtime) {
1481
+ const profile = taskProfile(task.tool);
1482
+ return digest({
1483
+ profile,
1484
+ spec: taskSpec(image, {}, profile, runtime),
1485
+ exec: taskExecSpec(isLiveTask(task.tool), runtime),
1486
+ tool: task.tool,
1487
+ inputDigest: task.inputDigest,
1488
+ timeoutMs: taskTimeoutMs(task.tool)
1489
+ });
1490
+ }
1491
+ function isLiveTask(tool) {
1492
+ return [
1493
+ "http_request",
1494
+ "web_discover",
1495
+ "web_request",
1496
+ "web_browse",
1497
+ "session_health",
1498
+ "browser_workflow",
1499
+ ...ACTIVE_SCAN_TOOLS
1500
+ ].includes(tool);
1501
+ }
1502
+
1503
+ // apps/harness/src/sandbox/worker.ts
1504
+ async function checkSandbox(config, engine) {
1505
+ const info = dockerJson(await engine.call("GET", "/info"));
1506
+ const selected = sandboxRuntime(SandboxConfigSchema.parse(config));
1507
+ const runtime = info?.Runtimes?.[selected];
1508
+ if (info?.OSType !== "linux" || typeof info.ID !== "string" || !info.ID || config.engineId !== undefined && info.ID !== config.engineId || !runtime || typeof runtime.path !== "string" || (selected === GVISOR_RUNTIME ? !/(^|\/)runsc$/.test(runtime.path) || !Array.isArray(runtime.runtimeArgs) || canonicalJson(runtime.runtimeArgs) !== canonicalJson(["--platform=systrap"]) : !/(^|\/)runc$/.test(runtime.path) || runtime.runtimeArgs !== undefined && (!Array.isArray(runtime.runtimeArgs) || runtime.runtimeArgs.length !== 0)) || info.MemoryLimit !== true || info.CpuCfsQuota !== true || info.PidsLimit !== true)
1509
+ throw new HarnessError("sandbox_unavailable", `A Linux engine with ${selected === GVISOR_RUNTIME ? "mettle-runsc systrap" : "runc without custom runtime arguments"} and resource controllers is required; runtime fallback is forbidden.`);
1510
+ if (selected === "runc" && (!Array.isArray(info.SecurityOptions) || !info.SecurityOptions.some((option) => typeof option === "string" && option.split(",").includes("name=seccomp"))))
1511
+ throw new HarnessError("sandbox_unavailable", "Local Docker requires kernel seccomp support and the explicit builtin profile.");
1512
+ const image = dockerJson(await engine.call("GET", `/images/${encodeURIComponent(config.image)}/json`));
1513
+ if (!Array.isArray(image?.RepoDigests) || !image.RepoDigests.includes(config.image) || Object.keys(image.Config?.Volumes ?? {}).length !== 0)
1514
+ throw new HarnessError("sandbox_image", "Preload the approved digest-pinned probe image without declared volumes.");
1515
+ return info.ID;
1516
+ }
1517
+ async function sandboxFromEnvironment(chat, environment = process.env) {
1518
+ if (!environment.HARNESS_SANDBOX_IMAGE) {
1519
+ if (environment.HARNESS_SANDBOX_RUNTIME !== undefined)
1520
+ throw new HarnessError("sandbox_config", "An explicit sandbox runtime requires HARNESS_SANDBOX_IMAGE.");
1521
+ return;
1522
+ }
1523
+ if (!chat)
1524
+ throw new HarnessError("sandbox_config", "HARNESS_SANDBOX_IMAGE requires HARNESS_CHAT=1.");
1525
+ const config = sandboxConfig(environment);
1526
+ await checkSandbox(config, new DockerEngine(config.socket));
1527
+ return config;
1528
+ }
1529
+ var CONTAINMENT_FINDINGS = new Set([
1530
+ "sandbox_policy",
1531
+ "sandbox_identity",
1532
+ "sandbox_probe_failed",
1533
+ "sandbox_output",
1534
+ "sandbox_missing"
1535
+ ]);
1536
+ function containmentFinding(error) {
1537
+ return error instanceof HarnessError && CONTAINMENT_FINDINGS.has(error.code);
1538
+ }
1539
+ function cancelled(error) {
1540
+ return !(error instanceof HarnessError) || error.code === "sandbox_transport" || error.code === "stop_requested";
1541
+ }
1542
+ function jobSpec(record2, job, runtime) {
1543
+ const labels = sandboxLabels(record2);
1544
+ if (job?.kind === "task")
1545
+ return taskSpec(record2.image, labels, taskProfile(job.task.tool), runtime);
1546
+ if (job?.kind === "shell")
1547
+ return shellSpec(record2.image, job.command, labels, runtime);
1548
+ return launchSpec(record2.image, probeScript(runtime), labels, "probe", runtime);
1549
+ }
1550
+
1551
+ class SandboxWorker {
1552
+ engine;
1553
+ journal;
1554
+ createBroker;
1555
+ config;
1556
+ runtime;
1557
+ constructor(config, engine, journal, createBroker) {
1558
+ this.engine = engine;
1559
+ this.journal = journal;
1560
+ this.createBroker = createBroker;
1561
+ this.config = Object.freeze(SandboxConfigSchema.parse(config));
1562
+ this.runtime = sandboxRuntime(this.config);
1563
+ }
1564
+ tool() {
1565
+ return {
1566
+ name: PROBE_TOOL,
1567
+ description: "Run the fixed offline sandbox installation probe; no target, shell arguments or file inputs are accepted.",
1568
+ schema: strictObject({}),
1569
+ outputSchema: strictObject({
1570
+ probe: literal("passed"),
1571
+ runtime: literal(this.runtime),
1572
+ image: string(),
1573
+ cleanupVerified: literal(true)
1574
+ }),
1575
+ label: () => "Check sandbox",
1576
+ summarizeArgs: () => "",
1577
+ summarizeResult: () => ({ summary: "Probe passed" }),
1578
+ execute: (_input, signal, callId) => this.execute(callId, signal, undefined)
1579
+ };
1580
+ }
1581
+ shellTools() {
1582
+ return sandboxAllowsExecution(this.config, SHELL_TOOL) ? [this.shellTool()] : [];
1583
+ }
1584
+ shellTool() {
1585
+ if (!sandboxAllowsExecution(this.config, SHELL_TOOL))
1586
+ throw new HarnessError("sandbox_denied", "Shell and repository code execution require gVisor.");
1587
+ return {
1588
+ name: SHELL_TOOL,
1589
+ description: `Run one POSIX shell command inside a disposable gVisor container: no network, no host files, no writable path, unprivileged user, ${PROBE_TIMEOUT_MS / 1000}-second limit, output cut at ${SHELL_OUTPUT_MAX} bytes. Returns exit code and captured output.`,
1590
+ schema: ShellCommandSchema,
1591
+ outputSchema: strictObject({
1592
+ command: string(),
1593
+ exitCode: number().int().nullable(),
1594
+ timedOut: boolean(),
1595
+ output: string(),
1596
+ truncated: boolean(),
1597
+ runtime: literal(this.runtime),
1598
+ image: string(),
1599
+ cleanupVerified: literal(true)
1600
+ }),
1601
+ label: () => "Run shell command",
1602
+ summarizeArgs: (args) => {
1603
+ const parsed = ShellCommandSchema.safeParse(args);
1604
+ return parsed.success ? `$ ${parsed.data.command.split(`
1605
+ `)[0]}` : "shell command";
1606
+ },
1607
+ summarizeResult: (output) => {
1608
+ const value = output;
1609
+ const suffix = value.truncated ? " (truncated)" : "";
1610
+ if (value.timedOut)
1611
+ return { summary: `Timed out${suffix}` };
1612
+ const lines = value.output ? value.output.split(`
1613
+ `).length : 0;
1614
+ return {
1615
+ summary: `Exit ${value.exitCode} · ${counted(lines, "line")}${suffix}`
1616
+ };
1617
+ },
1618
+ execute: async (input, signal, callId) => {
1619
+ const { command } = ShellCommandSchema.parse(input);
1620
+ return this.execute(callId, signal, { kind: "shell", command });
1621
+ }
1622
+ };
1623
+ }
1624
+ runTask = (tool, args, input, signal, callId, session) => {
1625
+ if (!sandboxAllowsExecution(this.config, tool))
1626
+ return Promise.reject(new HarnessError("sandbox_denied", "Shell and repository code execution require gVisor."));
1627
+ return this.execute(callId, signal, {
1628
+ kind: "task",
1629
+ task: isolatedTask(tool, args, input, session)
1630
+ });
1631
+ };
1632
+ async inspect(record2) {
1633
+ const response = await this.engine.call("GET", `/containers/${record2.containerId ?? record2.name}/json`);
1634
+ if (response.status === 404)
1635
+ return;
1636
+ const container = dockerJson(response);
1637
+ const expected = sandboxLabels(record2);
1638
+ if (!/^[a-f0-9]{64}$/.test(container?.Id ?? "") || record2.containerId && container.Id !== record2.containerId || container.Name !== `/${record2.name}` || container.Config?.Image !== record2.image || Object.entries(expected).some(([key, value]) => container.Config?.Labels?.[key] !== value))
1639
+ throw new HarnessError("sandbox_identity", "The container does not match its durable reservation; it will not be executed or removed.");
1640
+ return container;
1641
+ }
1642
+ assertPolicy(container, record2, job) {
1643
+ const spec = jobSpec(record2, job, this.runtime);
1644
+ for (const key of [
1645
+ "User",
1646
+ "WorkingDir",
1647
+ "Entrypoint",
1648
+ "Cmd",
1649
+ "Tty",
1650
+ "OpenStdin",
1651
+ "NetworkDisabled",
1652
+ "Healthcheck"
1653
+ ]) {
1654
+ if (canonicalJson(container.Config?.[key] ?? (key === "NetworkDisabled" ? false : null)) !== canonicalJson(spec[key]))
1655
+ throw new HarnessError("sandbox_policy", "The engine did not retain the required sandbox policy.");
1656
+ }
1657
+ for (const key of [
1658
+ "Runtime",
1659
+ "NetworkMode",
1660
+ "ReadonlyRootfs",
1661
+ "Privileged",
1662
+ "CapDrop",
1663
+ "SecurityOpt",
1664
+ "Memory",
1665
+ "MemorySwap",
1666
+ "NanoCpus",
1667
+ "PidsLimit",
1668
+ "RestartPolicy"
1669
+ ]) {
1670
+ if (canonicalJson(container.HostConfig?.[key] ?? null) !== canonicalJson(spec.HostConfig[key]))
1671
+ throw new HarnessError("sandbox_policy", "The engine did not retain the required sandbox policy.");
1672
+ }
1673
+ const env = container.Config?.Env ?? [];
1674
+ if (!spec.Env.every((entry) => env.includes(entry)))
1675
+ throw new HarnessError("sandbox_policy", "The engine did not retain the required sandbox environment.");
1676
+ const log = container.HostConfig?.LogConfig;
1677
+ if (log?.Type !== spec.HostConfig.LogConfig.Type || Object.entries(spec.HostConfig.LogConfig.Config).some(([key, value]) => log?.Config?.[key] !== value))
1678
+ throw new HarnessError("sandbox_policy", "The engine did not retain the required sandbox log policy.");
1679
+ const containment = object({
1680
+ Mounts: array(looseObject({ Type: string(), Destination: string() })),
1681
+ HostConfig: looseObject({
1682
+ Binds: array(string()).nullable(),
1683
+ Devices: array(unknown()).nullable(),
1684
+ DeviceRequests: array(unknown()).nullable(),
1685
+ CapAdd: array(string()).nullable(),
1686
+ VolumesFrom: array(string()).nullable(),
1687
+ PortBindings: record(string(), unknown()).nullable()
1688
+ })
1689
+ }).safeParse(container);
1690
+ if (!containment.success)
1691
+ throw new HarnessError("sandbox_policy", "The engine inspect response omitted a required containment field; an absent mount, device or port surface cannot be certified empty.");
1692
+ const tmpfs = spec.HostConfig.Tmpfs ?? {};
1693
+ const mounts = container.Mounts ?? [];
1694
+ if (canonicalJson(container.HostConfig?.Tmpfs ?? {}) !== canonicalJson(tmpfs) || mounts.some((mount) => mount?.Type !== "tmpfs" || !Object.hasOwn(tmpfs, mount.Destination)) || container.HostConfig?.Binds?.length || container.HostConfig?.Devices?.length || container.HostConfig?.CapAdd?.length || container.HostConfig?.VolumesFrom?.length || container.HostConfig?.DeviceRequests?.length || container.HostConfig?.PublishAllPorts || Object.keys(container.HostConfig?.PortBindings ?? {}).length || ["PidMode", "IpcMode", "UTSMode", "UsernsMode"].some((key) => container.HostConfig?.[key] === "host" || String(container.HostConfig?.[key] ?? "").startsWith("container:")))
1695
+ throw new HarnessError("sandbox_policy", "Unexpected sandbox mounts, devices or capabilities are forbidden.");
1696
+ }
1697
+ async cleanup(record2, workerId) {
1698
+ if (record2.workerId !== workerId)
1699
+ throw new HarnessError("sandbox_worker_mismatch", "Cleanup must run against the original Docker engine.");
1700
+ const container = await this.inspect(record2);
1701
+ if (!container) {
1702
+ if (!record2.containerId)
1703
+ throw new HarnessError("sandbox_uncertain", "Container creation was not acknowledged; absence cannot prove that a delayed create will not arrive.");
1704
+ } else {
1705
+ dockerStatus(await this.engine.call("DELETE", `/containers/${container.Id}?force=true`), 204);
1706
+ const absent = await this.engine.call("GET", `/containers/${container.Id}/json`);
1707
+ if (absent.status !== 404)
1708
+ throw new HarnessError("sandbox_cleanup_failed", "Container removal has not been verified.");
1709
+ }
1710
+ await this.journal.removed(record2);
1711
+ }
1712
+ async engineId() {
1713
+ const info = dockerJson(await this.engine.call("GET", "/info"));
1714
+ if (typeof info?.ID !== "string" || !info.ID)
1715
+ throw new HarnessError("sandbox_unavailable", "The original Docker engine identity is required.");
1716
+ return info.ID;
1717
+ }
1718
+ async reap(workerId) {
1719
+ const id = workerId ?? await this.engineId();
1720
+ const pending = await this.journal.pending();
1721
+ for (const record2 of pending)
1722
+ await this.cleanup(record2, id);
1723
+ return pending.length;
1724
+ }
1725
+ async execute(callId, signal, job) {
1726
+ const task = job?.kind === "task" ? job.task : undefined;
1727
+ const shell = job?.kind === "shell";
1728
+ if (signal.aborted)
1729
+ throw new HarnessError("stop_requested", "Sandbox execution was stopped before admission.");
1730
+ const workerId = await checkSandbox(this.config, this.engine);
1731
+ if (signal.aborted)
1732
+ throw new HarnessError("stop_requested", "Sandbox execution was stopped before allocation.");
1733
+ const record2 = await this.journal.reserve(callId, workerId, this.config.image, task ? taskPolicyDigest(this.config.image, task, this.runtime) : job?.kind === "shell" ? shellPolicyDigest(this.config.image, job.command, this.runtime) : probePolicyDigest(this.config.image, this.runtime), task ? { tool: task.tool, args: task.args } : job?.kind === "shell" ? { tool: SHELL_TOOL, args: { command: job.command } } : undefined);
1734
+ const deadline = AbortSignal.timeout(task ? taskTimeoutMs(task.tool) : PROBE_TIMEOUT_MS);
1735
+ const bounded = AbortSignal.any([signal, deadline]);
1736
+ let primary;
1737
+ let value;
1738
+ let exitCode = null;
1739
+ let output = Buffer.alloc(0);
1740
+ try {
1741
+ await this.journal.provision(record2, async () => {
1742
+ const created = dockerCreated(await this.engine.call("POST", `/containers/create?name=${record2.name}`, jobSpec(record2, job, this.runtime), bounded, ENGINE_LAUNCH_TIMEOUT_MS));
1743
+ if (!/^[a-f0-9]{64}$/.test(created?.Id ?? ""))
1744
+ throw new HarnessError("sandbox_protocol", "Container creation did not return a valid identity.");
1745
+ return created.Id;
1746
+ });
1747
+ const container = await this.inspect(record2);
1748
+ if (!container)
1749
+ throw new HarnessError("sandbox_missing", "The created sandbox disappeared before execution.");
1750
+ this.assertPolicy(container, record2, job);
1751
+ await this.journal.start(record2, async () => {
1752
+ if (bounded.aborted)
1753
+ throw new HarnessError("stop_requested", "Sandbox dispatch was cancelled.");
1754
+ dockerStatus(await this.engine.call("POST", `/containers/${record2.containerId}/start`, undefined, bounded, ENGINE_LAUNCH_TIMEOUT_MS), 204);
1755
+ });
1756
+ if (task)
1757
+ value = await this.runIsolated(record2, task, bounded);
1758
+ else {
1759
+ const exit = await this.engine.call("POST", `/containers/${record2.containerId}/wait?condition=not-running`, undefined, bounded, PROBE_TIMEOUT_MS).then((response) => dockerJson(response), (error) => {
1760
+ if (shell && deadline.aborted && !signal.aborted && cancelled(error))
1761
+ return;
1762
+ throw error;
1763
+ });
1764
+ if (exit !== undefined) {
1765
+ if (!Number.isInteger(exit?.StatusCode) || exit.Error)
1766
+ throw new HarnessError("sandbox_protocol", "The sandbox engine did not report a valid exit status.");
1767
+ if (!shell && exit.StatusCode !== 0)
1768
+ throw new HarnessError("sandbox_probe_failed", "The fixed sandbox probe did not exit successfully.");
1769
+ exitCode = exit.StatusCode;
1770
+ }
1771
+ const logs = await this.engine.call("GET", `/containers/${record2.containerId}/logs?stdout=true${shell ? "&stderr=true" : ""}`, undefined, shell ? signal : bounded, undefined, shell ? SHELL_LOG_LIMIT : undefined);
1772
+ if (shell)
1773
+ output = dockerFrames(logs, "both");
1774
+ else if (dockerLogs(logs) !== PROBE_MARKER)
1775
+ throw new HarnessError("sandbox_probe_failed", "The fixed sandbox probe returned unexpected output.");
1776
+ }
1777
+ if (signal.aborted || !shell && deadline.aborted)
1778
+ throw new HarnessError("stop_requested", "Sandbox execution was cancelled.");
1779
+ } catch (error) {
1780
+ primary = containmentFinding(error) ? error : signal.aborted ? new HarnessError("stop_requested", "Sandbox execution was cancelled; no result is invented.") : deadline.aborted && cancelled(error) ? new HarnessError("sandbox_timeout", "The sandbox exceeded its wall deadline.") : error;
1781
+ }
1782
+ try {
1783
+ if (primary instanceof HarnessError && primary.code === "sandbox_rejected" && !record2.containerId)
1784
+ await this.journal.removed(record2);
1785
+ else
1786
+ await this.cleanup(record2, workerId);
1787
+ } catch (cleanupError) {
1788
+ if (!containmentFinding(primary))
1789
+ primary = cleanupError;
1790
+ }
1791
+ if (primary !== undefined)
1792
+ throw primary;
1793
+ if (task)
1794
+ return {
1795
+ ...value,
1796
+ sandbox: {
1797
+ runtime: this.runtime,
1798
+ image: record2.image,
1799
+ policyDigest: record2.policyDigest,
1800
+ inputDigest: task.inputDigest,
1801
+ cleanupVerified: true
1802
+ }
1803
+ };
1804
+ if (job?.kind === "shell") {
1805
+ const truncated = output.length > SHELL_OUTPUT_MAX;
1806
+ return {
1807
+ command: job.command,
1808
+ exitCode,
1809
+ timedOut: exitCode === null,
1810
+ output: new TextDecoder("utf-8").decode(output.subarray(0, SHELL_OUTPUT_MAX), { stream: truncated }),
1811
+ truncated,
1812
+ runtime: this.runtime,
1813
+ image: record2.image,
1814
+ cleanupVerified: true
1815
+ };
1816
+ }
1817
+ return {
1818
+ probe: "passed",
1819
+ runtime: this.runtime,
1820
+ image: record2.image,
1821
+ cleanupVerified: true
1822
+ };
1823
+ }
1824
+ async runIsolated(record2, task, signal) {
1825
+ const live = isLiveTask(task.tool);
1826
+ if (live && !this.createBroker)
1827
+ throw new HarnessError("web_denied", "Live tasks require the operator-approved broker; no network fallback is allowed.");
1828
+ const spec = taskExecSpec(live, this.runtime);
1829
+ const created = dockerJson(await this.engine.call("POST", `/containers/${record2.containerId}/exec`, spec, signal), 201);
1830
+ if (!/^[a-f0-9]{64}$/.test(created?.Id ?? ""))
1831
+ throw new HarnessError("sandbox_protocol", "The engine did not return an isolated exec identity.");
1832
+ const inspect = () => this.engine.call("GET", `/exec/${created.Id}/json`, undefined, signal).then((response) => dockerJson(response));
1833
+ const initial = await inspect();
1834
+ if (initial.ID !== created.Id || initial.ContainerID !== record2.containerId || initial.Running !== false || initial.OpenStdin !== true || initial.OpenStdout !== true || initial.OpenStderr !== true || initial.ProcessConfig?.privileged !== false || initial.ProcessConfig?.tty !== false || initial.ProcessConfig?.user !== spec.User || initial.ProcessConfig?.entrypoint !== spec.Cmd[0] || canonicalJson(initial.ProcessConfig?.arguments) !== canonicalJson(spec.Cmd.slice(1)))
1835
+ throw new HarnessError("sandbox_policy", "The engine did not retain the fixed isolated task command.");
1836
+ await this.journal.assertTaskRunning(record2);
1837
+ const broker = live ? await this.createBroker(task, signal, record2.callId) : undefined;
1838
+ let refusal;
1839
+ const exchange = broker ? async (request) => {
1840
+ try {
1841
+ await this.journal.assertTaskRunning(record2);
1842
+ const response = await broker.request(request);
1843
+ await this.journal.assertTaskRunning(record2);
1844
+ return response;
1845
+ } catch (error) {
1846
+ refusal ??= error;
1847
+ throw error;
1848
+ }
1849
+ } : undefined;
1850
+ await this.journal.assertTaskRunning(record2);
1851
+ const session = await this.engine.startExec(created.Id, task.input, signal, taskTimeoutMs(task.tool), exchange);
1852
+ try {
1853
+ return await this.collectIsolated(record2, task, created.Id, session, inspect, signal, broker);
1854
+ } catch (error) {
1855
+ if (broker?.inFlight())
1856
+ throw new HarnessError("web_uncertain", "A live request was in flight when the task failed; inspect the journal before any retry.");
1857
+ if (refusal !== undefined)
1858
+ throw refusal;
1859
+ throw error;
1860
+ }
1861
+ }
1862
+ async collectIsolated(record2, task, execId, session, inspect, signal, broker) {
1863
+ const created = { Id: execId };
1864
+ const output = dockerLogs(await session.result);
1865
+ if (Buffer.byteLength(output) > TASK_RESULT_LIMIT)
1866
+ throw new HarnessError("sandbox_output_limit", "The isolated result exceeds its size limit.");
1867
+ while (true) {
1868
+ const status = await inspect();
1869
+ if (status.ID !== created.Id || status.ContainerID !== record2.containerId || typeof status.Running !== "boolean")
1870
+ throw new HarnessError("sandbox_identity", "The exec changed container identity.");
1871
+ if (!status.Running) {
1872
+ if (status.ExitCode !== 0)
1873
+ throw new HarnessError("sandbox_task_failed", "The fixed isolated observation failed; no result is invented.");
1874
+ break;
1875
+ }
1876
+ await sleep(50, undefined, { signal });
1877
+ }
1878
+ let result;
1879
+ try {
1880
+ result = JSON.parse(output);
1881
+ } catch {
1882
+ throw new HarnessError("sandbox_output", "The isolated result is not JSON.");
1883
+ }
1884
+ if (!result || result.inputDigest !== task.inputDigest || !result.value || typeof result.value !== "object" || Array.isArray(result.value))
1885
+ throw new HarnessError("sandbox_output", "The isolated output does not match its exact input.");
1886
+ return broker ? broker.finish(result.value) : result.value;
1887
+ }
1888
+ }
1889
+
1890
+ export { COMMIT_HEX, CommitSchema, IdSchema, RepositoryPathSchema, RepositoryConfigSchema, hasRepositorySource, RepositoryTestProfileSchema, BashProfilesSchema, SemgrepProfileSchema, WebCaptureSchema, PermissionPolicySchema, GVISOR_RUNTIME, SandboxConfigSchema, sandboxRuntime, sandboxAllowsExecution, SCANNER_CAPABILITIES, SUBJECT_CAPABILITIES, PROBE_TOOL, SHELL_TOOL, SANDBOX_TOOLS, counted, ran, scanned, ToolRegistry, HTTP_HEADER_LIMIT, HTTP_RESPONSE_LIMIT, WEBSITE_RESPONSE_LIMIT, withinHttpScope, SecretScanProfileSchema, DependencyScanProfileSchema, SCANNER_FINDINGS_MAX, ScannerStatusSchema, REPO_SEARCH_FILES, REPO_SEARCH_MATCHES, REPO_EXCERPT, DockerEngine, dockerJson, sandboxConfig, ACTIVE_SCAN_TASK_TIMEOUT_MS, ACTIVE_SCAN_TOOLS, ActiveScannersSchema, scannerEndpoint, validateActiveScanners, SCANNER_REQUEST_STRIP_HEADERS, scannerRequestAllowed, sqlmapCandidate, SandboxReceiptSchema, taskRuntimeDigest, checkSandbox, sandboxFromEnvironment, SandboxWorker };