dsh-plugin-coding-delegate 0.0.0-stage → 0.2.2

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +920 -2
  3. package/cordis.patch.example.yml +48 -0
  4. package/cordis.patch.yml +13 -0
  5. package/dist/attestation.d.ts +25 -0
  6. package/dist/attestation.js +44 -0
  7. package/dist/containment.d.ts +33 -0
  8. package/dist/containment.js +63 -0
  9. package/dist/context-quality.d.ts +79 -0
  10. package/dist/context-quality.js +181 -0
  11. package/dist/context.d.ts +39 -0
  12. package/dist/context.js +136 -0
  13. package/dist/contracts.d.ts +174 -0
  14. package/dist/contracts.js +415 -0
  15. package/dist/declaration-egress-hook.d.ts +45 -0
  16. package/dist/declaration-egress-hook.js +125 -0
  17. package/dist/declaration-egress.d.ts +37 -0
  18. package/dist/declaration-egress.js +169 -0
  19. package/dist/delegation.d.ts +339 -0
  20. package/dist/delegation.js +933 -0
  21. package/dist/emission.d.ts +70 -0
  22. package/dist/emission.js +310 -0
  23. package/dist/guard.d.ts +162 -0
  24. package/dist/guard.js +873 -0
  25. package/dist/host-events.d.ts +15 -0
  26. package/dist/host-events.js +27 -0
  27. package/dist/index.d.ts +560 -0
  28. package/dist/index.js +1398 -0
  29. package/dist/local-classifier.d.ts +52 -0
  30. package/dist/local-classifier.js +159 -0
  31. package/dist/logging.d.ts +21 -0
  32. package/dist/logging.js +90 -0
  33. package/dist/paths.d.ts +10 -0
  34. package/dist/paths.js +83 -0
  35. package/dist/profiles.d.ts +35 -0
  36. package/dist/profiles.js +95 -0
  37. package/dist/retry-context.d.ts +52 -0
  38. package/dist/retry-context.js +97 -0
  39. package/dist/roles.d.ts +129 -0
  40. package/dist/roles.js +363 -0
  41. package/dist/routing.d.ts +35 -0
  42. package/dist/routing.js +88 -0
  43. package/dist/savings-tracker.d.ts +128 -0
  44. package/dist/savings-tracker.js +340 -0
  45. package/dist/session-events.d.ts +17 -0
  46. package/dist/session-events.js +31 -0
  47. package/dist/verification.d.ts +104 -0
  48. package/dist/verification.js +560 -0
  49. package/package.json +66 -4
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A failure described without source: enough to diagnose, not enough to leak code.
3
+ */
4
+ export interface RedactedFailure {
5
+ kind: 'assertion' | 'compile' | 'timeout' | 'runtime' | 'unknown';
6
+ /** Subtest name, when the runner reports one. */
7
+ name?: string;
8
+ /** file:line or file:line:col. */
9
+ location?: string;
10
+ /** Runner or compiler error code, e.g. TS1434 or ERR_ASSERTION. */
11
+ code?: string;
12
+ /** The assertion label, kept only when it reads as prose rather than code. */
13
+ message?: string;
14
+ }
15
+ export interface TestResults {
16
+ passed: number;
17
+ failed: number;
18
+ /** Redacted summary. Raw output never leaves the machine by default. */
19
+ output: string;
20
+ errorSummary?: string;
21
+ /** Structured failures: names, locations, error kinds. */
22
+ failures?: RedactedFailure[];
23
+ /** True when the output above was passed through the redactor. */
24
+ redacted?: boolean;
25
+ /** Where the FULL raw output was written locally, for the worker and the operator. */
26
+ rawOutputPath?: string;
27
+ }
28
+ /**
29
+ * Reduce raw verification output to a source-free structure.
30
+ *
31
+ * Only named fields are ever copied out of a failure block; every other line inside it
32
+ * (stack frames, expected/actual, diff markers, quoted code) is discarded by omission
33
+ * rather than by pattern-matching each leak shape.
34
+ */
35
+ export declare function redactVerificationOutput(raw: string): RedactedFailure[];
36
+ /** One line per failure: kind, name, location, code, prose message. Never source. */
37
+ export declare function describeFailures(failures: RedactedFailure[]): string[];
38
+ export declare function parseTestOutput(output: string, exitCode?: number, options?: {
39
+ redact?: boolean;
40
+ rawOutputPath?: string;
41
+ }): TestResults;
42
+ /**
43
+ * The bound on how long a verification command may run, in milliseconds.
44
+ *
45
+ * 30 s was the value hardcoded inside `runSandboxVerification`, and it is kept as the default so that an
46
+ * operator who configures nothing sees no change. It is exported because it is the answer to "how long
47
+ * do I have?", and previously that answer was only available by reading the source.
48
+ */
49
+ export declare const DEFAULT_VERIFICATION_TIMEOUT_MS = 30000;
50
+ /**
51
+ * Coerce a configured timeout, falling back to the default rather than to *no bound*.
52
+ *
53
+ * The direction matters. "No timeout" on a command that is model-selected and runs with the DSH
54
+ * process's authority is not a permission, it is a hang — so a value that is not a positive finite
55
+ * number is refused and the default applies. A fractional value is floored to a whole millisecond
56
+ * because that is the unit the spawn takes.
57
+ */
58
+ export declare function resolveVerificationTimeoutMs(value: unknown): number;
59
+ export interface VerificationPolicy {
60
+ mode: 'ask' | 'allow' | 'deny';
61
+ allowlist: string[];
62
+ allowInProcessFallback: boolean;
63
+ /**
64
+ * How long the verification command may run. Never absent: there is always a bound, because the
65
+ * alternative is a command that never returns.
66
+ */
67
+ timeoutMs: number;
68
+ /**
69
+ * The PROJECT's own check -- a build, a full suite -- run after the unit's contract, and given the
70
+ * power to void an otherwise passing unit.
71
+ *
72
+ * Operator configuration rather than model input. That is why it is deliberately absent from the tool
73
+ * schema and is not put through the approval seam: the operator wrote this string in their profile
74
+ * patch, exactly as they would write it in CI, so there is nothing for them to approve that they did
75
+ * not already write. `runVerification` is model-selected and is gated for precisely that reason.
76
+ *
77
+ * It exists because every other check in this plugin is per unit: `contractFiles` judges one unit's
78
+ * tests, verification runs one unit's command, and nothing asks whether the tree still works.
79
+ */
80
+ coherenceVerification?: string;
81
+ }
82
+ export declare const DEFAULT_VERIFICATION_POLICY: VerificationPolicy;
83
+ /** The program a shell command would run, normalised for allowlist comparison. */
84
+ export declare function commandProgram(command: string): string;
85
+ /**
86
+ * Decide whether a model-supplied verification command may run. Pure, so the policy is
87
+ * testable without a server or an approval seam. `runVerification` is model-selected and
88
+ * executes with the DSH process's full authority, so silence is never consent: anything
89
+ * not explicitly permitted resolves to `ask`, and `ask` with no approver available is a
90
+ * refusal at the call site.
91
+ */
92
+ export declare function evaluateVerificationPolicy(command: string, policy?: VerificationPolicy): {
93
+ kind: 'allow' | 'ask' | 'deny';
94
+ program: string;
95
+ reason: string;
96
+ };
97
+ export declare function runInProcessFallback(cmd: string, workspaceDir: string): string;
98
+ export declare function runSandboxVerification(verificationCommand: string, workspaceDir?: string, options?: {
99
+ redact?: boolean;
100
+ rawLogPath?: string;
101
+ allowInProcessFallback?: boolean;
102
+ /** Overrides the default bound; an unusable value falls back to it rather than removing it. */
103
+ timeoutMs?: number;
104
+ }): TestResults;
@@ -0,0 +1,560 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_VERIFICATION_POLICY = exports.DEFAULT_VERIFICATION_TIMEOUT_MS = void 0;
37
+ exports.redactVerificationOutput = redactVerificationOutput;
38
+ exports.describeFailures = describeFailures;
39
+ exports.parseTestOutput = parseTestOutput;
40
+ exports.resolveVerificationTimeoutMs = resolveVerificationTimeoutMs;
41
+ exports.commandProgram = commandProgram;
42
+ exports.evaluateVerificationPolicy = evaluateVerificationPolicy;
43
+ exports.runInProcessFallback = runInProcessFallback;
44
+ exports.runSandboxVerification = runSandboxVerification;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const child_process = __importStar(require("child_process"));
48
+ const logging_1 = require("./logging");
49
+ const index_1 = require("./index");
50
+ /**
51
+ * Does this text look like source rather than a label? Assertion messages should be
52
+ * prose; anything code-shaped is dropped rather than forwarded.
53
+ */
54
+ function looksLikeCode(text) {
55
+ return (/[{};]/.test(text) ||
56
+ /=>/.test(text) ||
57
+ /\b(?:function|const|let|var|return|import|export|class|def|public|private)\b/.test(text) ||
58
+ /\b(?:expected|actual)\b\s*[:=]/i.test(text));
59
+ }
60
+ function cleanMessage(raw) {
61
+ const collapsed = String(raw).trim().replace(/^['"]|['"]$/g, '').replace(/\s+/g, ' ');
62
+ if (!collapsed || /^\|-?$/.test(collapsed))
63
+ return undefined;
64
+ if (looksLikeCode(collapsed))
65
+ return undefined;
66
+ return collapsed.length > 160 ? collapsed.slice(0, 157) + '...' : collapsed;
67
+ }
68
+ /**
69
+ * Bound a retained field and strip anything the secret rules recognise. Entropy is on:
70
+ * these strings are exactly what leaves the machine, so a high-entropy blob in a test
71
+ * name must not ride along merely because it lacks a recognisable keyword.
72
+ */
73
+ function sanitizeRetainedField(value, maxLength = 200) {
74
+ if (typeof value !== 'string')
75
+ return undefined;
76
+ const collapsed = value.replace(/\s+/g, ' ').trim();
77
+ if (!collapsed)
78
+ return undefined;
79
+ const scanned = (0, index_1.scanDLP)(collapsed);
80
+ if (scanned.hasSensitiveData)
81
+ return '[redacted: ' + scanned.violations.join(', ') + ']';
82
+ return collapsed.length > maxLength ? collapsed.slice(0, maxLength) + ' [truncated]' : collapsed;
83
+ }
84
+ /**
85
+ * Reduce raw verification output to a source-free structure.
86
+ *
87
+ * Only named fields are ever copied out of a failure block; every other line inside it
88
+ * (stack frames, expected/actual, diff markers, quoted code) is discarded by omission
89
+ * rather than by pattern-matching each leak shape.
90
+ */
91
+ function redactVerificationOutput(raw) {
92
+ if (!raw)
93
+ return [];
94
+ const failures = [];
95
+ let current = null;
96
+ const flush = () => {
97
+ if (current && (current.name || current.location || current.code))
98
+ failures.push(current);
99
+ current = null;
100
+ };
101
+ for (const line of raw.split(/\r?\n/)) {
102
+ const t = line.trim();
103
+ if (!t)
104
+ continue;
105
+ const compile = /^(.+?)\((\d+),(\d+)\):\s*error\s+(TS\d+):\s*(.*)$/.exec(t);
106
+ if (compile) {
107
+ flush();
108
+ failures.push({
109
+ kind: 'compile',
110
+ location: compile[1] + ':' + compile[2] + ':' + compile[3],
111
+ code: compile[4],
112
+ message: cleanMessage(compile[5]),
113
+ });
114
+ continue;
115
+ }
116
+ const compile2 = /^error\s+(TS\d+):\s*(.*)$/.exec(t);
117
+ if (compile2) {
118
+ flush();
119
+ failures.push({ kind: 'compile', code: compile2[1], message: cleanMessage(compile2[2]) });
120
+ continue;
121
+ }
122
+ // A passing test ends the block above it. Without this, every line up to the next `not ok` was read as
123
+ // part of the previous failure -- including the `ok` lines of tests named for a timeout, which is how an
124
+ // assertion failure came to be reported as [timeout].
125
+ if (/^ok\s+\d+/i.test(t)) {
126
+ flush();
127
+ continue;
128
+ }
129
+ const notOk = /^not ok\s+(\d+)\s*-\s*(.*)$/i.exec(t);
130
+ if (notOk) {
131
+ flush();
132
+ current = { kind: 'assertion', name: (notOk[1] + '. ' + notOk[2]).replace(/\s+/g, ' ').trim() };
133
+ continue;
134
+ }
135
+ if (current) {
136
+ const loc = /^location:\s*(.+)$/.exec(t);
137
+ if (loc) {
138
+ current.location = loc[1].replace(/^['"]|['"]$/g, '').trim();
139
+ continue;
140
+ }
141
+ const code = /^code:\s*(.+)$/.exec(t);
142
+ if (code) {
143
+ const value = code[1].replace(/^['"]|['"]$/g, '').trim();
144
+ current.code = value;
145
+ // A failed expectation is an assertion, and nothing later in the block may relabel it: the word
146
+ // "timeout" appears in this suite's own test names, and a stray mention used to win.
147
+ if (/^ERR_ASSERTION$/i.test(value))
148
+ current.kind = 'assertion';
149
+ continue;
150
+ }
151
+ const err = /^error:\s*(.*)$/.exec(t);
152
+ if (err) {
153
+ const message = cleanMessage(err[1]);
154
+ if (message)
155
+ current.message = message;
156
+ continue;
157
+ }
158
+ if (/timeout/i.test(t) && !/^ERR_ASSERTION$/i.test(String(current.code ?? ''))) {
159
+ current.kind = 'timeout';
160
+ }
161
+ // Everything else inside a failure block is deliberately not copied.
162
+ continue;
163
+ }
164
+ if (/AssertionError/.test(t)) {
165
+ const message = cleanMessage(t);
166
+ failures.push({ kind: 'assertion', ...(message ? { message } : {}) });
167
+ }
168
+ }
169
+ flush();
170
+ // Names, locations and codes are test- and model-controlled text and they travel to the
171
+ // cloud. A TAP label can carry a credential as easily as a diff can, so every retained
172
+ // field is put through the same secret rules the DLP gate uses. Counts and structure
173
+ // still travel; only the offending value is replaced by a marker.
174
+ return failures
175
+ .map((failure) => {
176
+ const bounded = { kind: failure.kind };
177
+ const name = sanitizeRetainedField(failure.name);
178
+ const location = sanitizeRetainedField(failure.location);
179
+ const code = sanitizeRetainedField(failure.code, 40);
180
+ const message = sanitizeRetainedField(failure.message, 300);
181
+ if (name)
182
+ bounded.name = name;
183
+ if (location)
184
+ bounded.location = location;
185
+ if (code)
186
+ bounded.code = code;
187
+ if (message)
188
+ bounded.message = message;
189
+ return bounded;
190
+ })
191
+ .filter((failure) => Boolean(failure.name || failure.location || failure.code || failure.message));
192
+ }
193
+ /** One line per failure: kind, name, location, code, prose message. Never source. */
194
+ function describeFailures(failures) {
195
+ return failures.map((f) => {
196
+ const parts = ['- [' + f.kind + ']'];
197
+ if (f.name)
198
+ parts.push(f.name);
199
+ if (f.location)
200
+ parts.push('@ ' + f.location);
201
+ if (f.code)
202
+ parts.push('(' + f.code + ')');
203
+ if (f.message)
204
+ parts.push(': ' + f.message);
205
+ return parts.join(' ');
206
+ });
207
+ }
208
+ function parseTestOutput(output, exitCode, options = {}) {
209
+ if (!output) {
210
+ // A silent command is only a success when it also exited cleanly.
211
+ if (exitCode !== undefined && exitCode !== 0) {
212
+ return {
213
+ passed: 0,
214
+ failed: 1,
215
+ output: 'command exited with code ' + exitCode + ' and produced no output',
216
+ errorSummary: 'exit code ' + exitCode,
217
+ };
218
+ }
219
+ return { passed: 0, failed: 0, output: '' };
220
+ }
221
+ let passed = 0;
222
+ let failed = 0;
223
+ const lines = output.split('\n');
224
+ const failureLines = [];
225
+ const hasTap = lines.some((l) => /^ok\s+\d+|^not ok\s+\d+/i.test(l.trim()));
226
+ if (hasTap) {
227
+ for (const line of lines) {
228
+ const trimmed = line.trim();
229
+ if (/^not ok\s+/i.test(trimmed)) {
230
+ failed++;
231
+ failureLines.push(trimmed);
232
+ }
233
+ else if (/^ok\s+/i.test(trimmed)) {
234
+ passed++;
235
+ }
236
+ }
237
+ }
238
+ else {
239
+ for (const line of lines) {
240
+ const trimmed = line.trim();
241
+ if (!trimmed)
242
+ continue;
243
+ if (/✓|✔|PASSED/i.test(trimmed) && !/FAILED|not ok/i.test(trimmed)) {
244
+ passed++;
245
+ }
246
+ else if (/(?:✕|✖|FAILED|AssertionError|Error:)/i.test(trimmed)) {
247
+ failed++;
248
+ failureLines.push(trimmed);
249
+ }
250
+ }
251
+ }
252
+ // The exit code is authoritative. Unrecognized output must NEVER count as a pass:
253
+ // `tsc` reports failures as "error TS1234:", which matches none of the markers
254
+ // above, so the previous fallback turned a broken build into status SUCCESS.
255
+ if (exitCode !== undefined && exitCode !== 0) {
256
+ failed = Math.max(failed, 1);
257
+ if (failureLines.length === 0) {
258
+ failureLines.push(output.slice(0, 300) || 'command exited with code ' + exitCode);
259
+ }
260
+ }
261
+ else if (passed === 0 && failed === 0) {
262
+ if (output.includes('AssertionError') ||
263
+ output.includes('Error:') ||
264
+ output.includes('FAIL') ||
265
+ /\berror\b/i.test(output)) {
266
+ failed = 1;
267
+ failureLines.push(output.slice(0, 300));
268
+ }
269
+ else {
270
+ passed = 1;
271
+ }
272
+ }
273
+ // Raw output is a source-egress channel, so by default only structure is returned.
274
+ // The full text is on disk (rawOutputPath) for the delegated worker to work from.
275
+ const redact = options.redact !== false;
276
+ const failures = redact ? redactVerificationOutput(output) : [];
277
+ const described = describeFailures(failures);
278
+ const headline = failed + ' failed, ' + passed + ' passed' + (exitCode !== undefined ? ' (exit ' + exitCode + ')' : '');
279
+ const body = redact
280
+ ? [
281
+ headline,
282
+ ...(described.length > 0
283
+ ? described.slice(0, 10)
284
+ : failed > 0
285
+ ? ['- [unknown] no structured failure could be extracted; see the raw log']
286
+ : []),
287
+ ...(options.rawOutputPath ? ['raw output: ' + options.rawOutputPath] : []),
288
+ ].join('\n')
289
+ : output.slice(0, 2000);
290
+ const summarySource = redact
291
+ ? described.slice(0, 5).join('; ') || (failed > 0 ? 'unstructured failure; see the raw log' : undefined)
292
+ : failureLines.length > 0
293
+ ? failureLines.slice(0, 5).join('; ')
294
+ : undefined;
295
+ return {
296
+ passed,
297
+ failed,
298
+ output: body.slice(0, 2000),
299
+ ...(summarySource ? { errorSummary: summarySource } : {}),
300
+ ...(redact ? { failures, redacted: true } : { redacted: false }),
301
+ ...(options.rawOutputPath ? { rawOutputPath: options.rawOutputPath } : {}),
302
+ };
303
+ }
304
+ /**
305
+ * The bound on how long a verification command may run, in milliseconds.
306
+ *
307
+ * 30 s was the value hardcoded inside `runSandboxVerification`, and it is kept as the default so that an
308
+ * operator who configures nothing sees no change. It is exported because it is the answer to "how long
309
+ * do I have?", and previously that answer was only available by reading the source.
310
+ */
311
+ exports.DEFAULT_VERIFICATION_TIMEOUT_MS = 30000;
312
+ /**
313
+ * Coerce a configured timeout, falling back to the default rather than to *no bound*.
314
+ *
315
+ * The direction matters. "No timeout" on a command that is model-selected and runs with the DSH
316
+ * process's authority is not a permission, it is a hang — so a value that is not a positive finite
317
+ * number is refused and the default applies. A fractional value is floored to a whole millisecond
318
+ * because that is the unit the spawn takes.
319
+ */
320
+ function resolveVerificationTimeoutMs(value) {
321
+ const n = Number(value);
322
+ if (!Number.isFinite(n) || n <= 0)
323
+ return exports.DEFAULT_VERIFICATION_TIMEOUT_MS;
324
+ return Math.floor(n);
325
+ }
326
+ exports.DEFAULT_VERIFICATION_POLICY = {
327
+ mode: 'ask',
328
+ allowlist: [],
329
+ allowInProcessFallback: false,
330
+ timeoutMs: exports.DEFAULT_VERIFICATION_TIMEOUT_MS,
331
+ };
332
+ /** The program a shell command would run, normalised for allowlist comparison. */
333
+ function commandProgram(command) {
334
+ const text = String(command || '').trim();
335
+ if (!text)
336
+ return '';
337
+ // A quoted first token may contain spaces ("C:\Program Files\nodejs\node.exe"), so it
338
+ // must be taken whole; splitting on whitespace first would read it as "C:\Program".
339
+ const quoted = /^"([^"]+)"|^'([^']+)'/.exec(text);
340
+ const first = quoted ? quoted[1] ?? quoted[2] ?? '' : text.split(/\s+/)[0] || '';
341
+ const bare = first.replace(/^["']|["']$/g, '');
342
+ return path.basename(bare).toLowerCase().replace(/\.(?:exe|cmd|bat|ps1)$/, '');
343
+ }
344
+ /**
345
+ * Decide whether a model-supplied verification command may run. Pure, so the policy is
346
+ * testable without a server or an approval seam. `runVerification` is model-selected and
347
+ * executes with the DSH process's full authority, so silence is never consent: anything
348
+ * not explicitly permitted resolves to `ask`, and `ask` with no approver available is a
349
+ * refusal at the call site.
350
+ */
351
+ function evaluateVerificationPolicy(command, policy = exports.DEFAULT_VERIFICATION_POLICY) {
352
+ const program = commandProgram(command);
353
+ if (!program) {
354
+ return { kind: 'deny', program, reason: 'the verification command was empty' };
355
+ }
356
+ if (policy.mode === 'deny') {
357
+ return {
358
+ kind: 'deny',
359
+ program,
360
+ reason: "verificationApproval is 'deny', so no verification command is executed",
361
+ };
362
+ }
363
+ const allowlisted = policy.allowlist.some((entry) => commandProgram(String(entry)) === program);
364
+ if (policy.mode === 'allow' || allowlisted) {
365
+ return {
366
+ kind: 'allow',
367
+ program,
368
+ reason: allowlisted
369
+ ? "program '" + program + "' is on verificationAllowlist"
370
+ : "verificationApproval is 'allow'",
371
+ };
372
+ }
373
+ return {
374
+ kind: 'ask',
375
+ program,
376
+ reason: "the verification command '" + command + "' would run with the full authority of the DSH process " +
377
+ 'and is not confined to the workspace',
378
+ };
379
+ }
380
+ function runInProcessFallback(cmd, workspaceDir) {
381
+ // Identify a real module path in the command, ignoring flags: the previous regex
382
+ // captured "--test", "-e" and "--version" as if they were file paths, then
383
+ // reported a bogus "target test file not found" failure for them.
384
+ const candidates = cmd
385
+ .split(/\s+/)
386
+ .map((token) => token.replace(/^["']|["']$/g, ''))
387
+ .filter((token) => token && !token.startsWith('-') && /\.(?:[cm]?[jt]sx?)$/i.test(token));
388
+ const targetFile = candidates.find((token) => fs.existsSync(path.isAbsolute(token) ? token : path.resolve(workspaceDir, token))) ?? null;
389
+ if (!targetFile) {
390
+ // Fail closed: with no identifiable target module there is nothing to verify,
391
+ // and claiming success here would report an unverified command as passing.
392
+ return ('not ok 1 - In-process verification fallback could not identify a target module in command ' +
393
+ "'" + cmd + "'. Refusing to report success.");
394
+ }
395
+ const resolvedPath = path.isAbsolute(targetFile) ? targetFile : path.resolve(workspaceDir, targetFile);
396
+ if (!fs.existsSync(resolvedPath)) {
397
+ return "not ok 1 - Target test file '" + targetFile + "' not found at '" + resolvedPath + "'.";
398
+ }
399
+ let capturedOutput = '';
400
+ const originalLog = console.log;
401
+ const originalError = console.error;
402
+ const originalExit = process.exit;
403
+ const originalExitCode = process.exitCode;
404
+ try {
405
+ console.log = (...args) => {
406
+ capturedOutput += args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n';
407
+ originalLog(...args);
408
+ };
409
+ console.error = (...args) => {
410
+ capturedOutput += args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n';
411
+ originalError(...args);
412
+ };
413
+ process.exit = (code) => {
414
+ throw new Error('test code called process.exit(' + (code ?? 0) + '); refusing to terminate the host process');
415
+ };
416
+ // The module signals failure through captured TAP text, so its exit code is noise
417
+ // here -- and leaving it set would misreport the HOST's own result.
418
+ process.exitCode = undefined;
419
+ delete require.cache[require.resolve(resolvedPath)];
420
+ require(resolvedPath);
421
+ capturedOutput =
422
+ capturedOutput || 'ok 1 - Executed ' + path.basename(resolvedPath) + ' in-process successfully.';
423
+ }
424
+ catch (err) {
425
+ capturedOutput +=
426
+ 'not ok 1 - In-Process Test Failure in ' + path.basename(resolvedPath) + '\n ' +
427
+ (err?.stack || err?.message || String(err)) + '\n';
428
+ }
429
+ finally {
430
+ console.log = originalLog;
431
+ console.error = originalError;
432
+ process.exit = originalExit;
433
+ process.exitCode = originalExitCode;
434
+ }
435
+ return capturedOutput;
436
+ }
437
+ /**
438
+ * Run a verification command and capture its output.
439
+ *
440
+ * Output is captured through FILE DESCRIPTORS rather than pipes, deliberately. DSH's confined
441
+ * sandbox modes refuse a piped spawn outright (`spawn EPERM`), which made the ordinary
442
+ * subprocess path unusable in the default configuration and left the in-process fallback as the
443
+ * only path that worked -- the wrong trade in every direction, since that fallback executes
444
+ * model-influenced code inside the server. A descriptor avoids the pipe, so verification runs
445
+ * as an ordinary child process for everyone, and the fallback is not needed at all.
446
+ */
447
+ function captureCommandOutput(cmd, workspaceDir, timeoutMs) {
448
+ const dir = (0, logging_1.resolveDataDir)();
449
+ const unique = process.pid + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
450
+ const outPath = path.join(dir, 'verify-' + unique + '.stdout');
451
+ const errPath = path.join(dir, 'verify-' + unique + '.stderr');
452
+ let outFd;
453
+ let errFd;
454
+ let result = null;
455
+ try {
456
+ fs.mkdirSync(dir, { recursive: true });
457
+ outFd = fs.openSync(outPath, 'w');
458
+ errFd = fs.openSync(errPath, 'w');
459
+ result = child_process.spawnSync(cmd, {
460
+ shell: true,
461
+ cwd: workspaceDir,
462
+ timeout: timeoutMs,
463
+ stdio: ['ignore', outFd, errFd],
464
+ });
465
+ }
466
+ catch (err) {
467
+ result = { error: err, status: null };
468
+ }
469
+ finally {
470
+ for (const fd of [outFd, errFd]) {
471
+ if (typeof fd === 'number') {
472
+ try {
473
+ fs.closeSync(fd);
474
+ }
475
+ catch {
476
+ // already closed
477
+ }
478
+ }
479
+ }
480
+ }
481
+ const readFile = (file) => {
482
+ try {
483
+ return fs.readFileSync(file, 'utf8');
484
+ }
485
+ catch {
486
+ return '';
487
+ }
488
+ };
489
+ const output = (readFile(outPath) + '\n' + readFile(errPath)).trim();
490
+ for (const file of [outPath, errPath]) {
491
+ try {
492
+ fs.rmSync(file, { force: true });
493
+ }
494
+ catch {
495
+ // Best effort: a leftover temp file is not worth failing a verification over.
496
+ }
497
+ }
498
+ const spawnError = result?.error ?? null;
499
+ const exitCode = typeof result?.status === 'number' ? result.status : spawnError ? 1 : 0;
500
+ return {
501
+ output: output || (spawnError ? String(spawnError.message || spawnError) : ''),
502
+ exitCode,
503
+ spawnError,
504
+ };
505
+ }
506
+ function runSandboxVerification(verificationCommand, workspaceDir = process.cwd(), options = {}) {
507
+ if (!verificationCommand || !verificationCommand.trim()) {
508
+ return { passed: 0, failed: 0, output: 'No verification command specified.' };
509
+ }
510
+ const cmd = verificationCommand.trim();
511
+ let output = '';
512
+ let spawnError = null;
513
+ let exitCode = 0;
514
+ const captured = captureCommandOutput(cmd, workspaceDir, resolveVerificationTimeoutMs(options.timeoutMs));
515
+ output = captured.output;
516
+ spawnError = captured.spawnError;
517
+ exitCode = captured.exitCode;
518
+ if (spawnError &&
519
+ (spawnError.code === 'EPERM' ||
520
+ String(spawnError).includes('EPERM') ||
521
+ String(spawnError).includes('spawn EPERM'))) {
522
+ // Even a descriptor spawn was refused, so this host blocks verification entirely. The
523
+ // in-process fallback still exists, but it re-runs model-influenced code inside the
524
+ // SERVER process, so it is opt-in only: answering a denial by removing the sandbox
525
+ // inverts the control. Fail closed.
526
+ if (options.allowInProcessFallback === true) {
527
+ output = runInProcessFallback(cmd, workspaceDir);
528
+ return parseTestOutput(output, undefined, {
529
+ redact: options.redact,
530
+ rawOutputPath: persistRaw(output, options.rawLogPath),
531
+ });
532
+ }
533
+ const refusal = 'not ok 1 - the sandbox refused to spawn the verification command (EPERM) and the ' +
534
+ 'in-process fallback is disabled by default. Refusing to report success.\n' + output;
535
+ return parseTestOutput(refusal, exitCode, {
536
+ redact: options.redact,
537
+ rawOutputPath: persistRaw(refusal, options.rawLogPath),
538
+ });
539
+ }
540
+ return parseTestOutput(output, exitCode, {
541
+ redact: options.redact,
542
+ rawOutputPath: persistRaw(output, options.rawLogPath),
543
+ });
544
+ }
545
+ /**
546
+ * Keep the full verification output on the local machine. The redacted structure is what
547
+ * travels; this is what a worker or an operator reads when they need the real thing.
548
+ */
549
+ function persistRaw(output, rawLogPath) {
550
+ if (!rawLogPath)
551
+ return undefined;
552
+ try {
553
+ fs.mkdirSync(path.dirname(rawLogPath), { recursive: true });
554
+ fs.writeFileSync(rawLogPath, output, 'utf8');
555
+ return rawLogPath;
556
+ }
557
+ catch {
558
+ return undefined;
559
+ }
560
+ }