@abelxiaoxing/cadence 1.0.1 → 1.0.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.
- package/package.json +1 -1
- package/prompts/abel-implement.md +13 -2
- package/skills/abel-workflow/SKILL.md +7 -1
- package/src/candidate-preflight.ts +1163 -0
- package/src/child-session.ts +46 -5
- package/src/contracts.ts +379 -48
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +55 -10
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +91 -12
- package/src/patch.ts +73 -3
- package/src/result-store.ts +40 -3
- package/src/runtime.ts +775 -66
- package/src/scheduler.ts +1 -0
- package/src/scoped-tools.ts +27 -6
- package/src/submit-tool.ts +3 -1
- package/src/worker.ts +136 -26
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
copyFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import {
|
|
18
|
+
diffWritePaths,
|
|
19
|
+
isValidRelativePath,
|
|
20
|
+
type VerificationContract,
|
|
21
|
+
validateVerificationContract,
|
|
22
|
+
} from "./contracts.ts";
|
|
23
|
+
import {
|
|
24
|
+
type Bound,
|
|
25
|
+
type DirBound,
|
|
26
|
+
type FileBound,
|
|
27
|
+
isCurrent,
|
|
28
|
+
snapshotDirManifest,
|
|
29
|
+
} from "./file-snapshot.ts";
|
|
30
|
+
|
|
31
|
+
export type { VerificationContract } from "./contracts.ts";
|
|
32
|
+
|
|
33
|
+
export interface BaselineEntry {
|
|
34
|
+
path: string;
|
|
35
|
+
kind: "file" | "deleted";
|
|
36
|
+
sha256?: string;
|
|
37
|
+
bytes?: number;
|
|
38
|
+
executable?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CandidatePreflightInput {
|
|
42
|
+
root: string;
|
|
43
|
+
diff: Buffer;
|
|
44
|
+
writeSet: string[];
|
|
45
|
+
snapshot: Bound;
|
|
46
|
+
baseline: BaselineEntry[];
|
|
47
|
+
verification: VerificationContract;
|
|
48
|
+
packageManifest: FileBound;
|
|
49
|
+
lockfile: FileBound;
|
|
50
|
+
dependencyTarget: FileBound | DirBound;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type CandidatePreflightResult =
|
|
55
|
+
| {
|
|
56
|
+
ok: true;
|
|
57
|
+
classification: VerificationContract["classification"];
|
|
58
|
+
targets: string[];
|
|
59
|
+
commandId: string;
|
|
60
|
+
exitCode: number;
|
|
61
|
+
testCount: number;
|
|
62
|
+
identityMatch: boolean;
|
|
63
|
+
checkoutRemoved: true;
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
ok: false;
|
|
67
|
+
class: "artifact" | "design" | "environment" | "stale" | "cancelled";
|
|
68
|
+
code: string;
|
|
69
|
+
commandId?: string;
|
|
70
|
+
exitCode?: number;
|
|
71
|
+
testCount?: number;
|
|
72
|
+
identityMatch?: boolean;
|
|
73
|
+
checkoutRemoved: boolean;
|
|
74
|
+
excerpt?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export interface CandidatePreflightDependencies {
|
|
78
|
+
mkdtemp: typeof mkdtempSync;
|
|
79
|
+
remove: typeof rmSync;
|
|
80
|
+
spawn: (
|
|
81
|
+
command: string,
|
|
82
|
+
args: string[],
|
|
83
|
+
options: CommandOptions,
|
|
84
|
+
) => CommandResult | Promise<CommandResult>;
|
|
85
|
+
realpath: typeof realpathSync;
|
|
86
|
+
bwrapPath: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface CommandResult {
|
|
90
|
+
status: number | null;
|
|
91
|
+
stdout: string;
|
|
92
|
+
stderr: string;
|
|
93
|
+
error?: Error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface CommandOptions {
|
|
97
|
+
cwd: string;
|
|
98
|
+
shell: false;
|
|
99
|
+
encoding: "utf8";
|
|
100
|
+
maxBuffer: number;
|
|
101
|
+
timeout: number;
|
|
102
|
+
input?: Buffer;
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface FailureDetails {
|
|
107
|
+
commandId?: string;
|
|
108
|
+
exitCode?: number;
|
|
109
|
+
testCount?: number;
|
|
110
|
+
identityMatch?: boolean;
|
|
111
|
+
excerpt?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class PreflightFailure extends Error {
|
|
115
|
+
constructor(
|
|
116
|
+
readonly failureClass:
|
|
117
|
+
| "artifact"
|
|
118
|
+
| "design"
|
|
119
|
+
| "environment"
|
|
120
|
+
| "stale"
|
|
121
|
+
| "cancelled",
|
|
122
|
+
readonly code: string,
|
|
123
|
+
readonly details: FailureDetails = {},
|
|
124
|
+
) {
|
|
125
|
+
super(code);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
130
|
+
const LOCKFILES = [
|
|
131
|
+
"bun.lock",
|
|
132
|
+
"bun.lockb",
|
|
133
|
+
"package-lock.json",
|
|
134
|
+
"pnpm-lock.yaml",
|
|
135
|
+
"yarn.lock",
|
|
136
|
+
] as const;
|
|
137
|
+
const LOAD_FAILURE =
|
|
138
|
+
/(?:syntaxerror|parse error|failed to load|cannot find (?:module|package)|no test files|tests?\s+no tests|error ts\d+)/iu;
|
|
139
|
+
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
140
|
+
const SANDBOX_BUN_TARGET = "/usr/bin/bun";
|
|
141
|
+
const SANDBOX_REPORT_TARGET = "/tmp/cadence-verification-report.json";
|
|
142
|
+
const MAX_REPORT_BYTES = 2 * 1024 * 1024;
|
|
143
|
+
|
|
144
|
+
const defaultDependencies: CandidatePreflightDependencies = {
|
|
145
|
+
mkdtemp: mkdtempSync,
|
|
146
|
+
remove: rmSync,
|
|
147
|
+
spawn: spawnCommand,
|
|
148
|
+
realpath: realpathSync,
|
|
149
|
+
bwrapPath: "/usr/bin/bwrap",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function spawnCommand(
|
|
153
|
+
command: string,
|
|
154
|
+
args: string[],
|
|
155
|
+
options: CommandOptions,
|
|
156
|
+
): Promise<CommandResult> {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
const stdout: Buffer[] = [];
|
|
159
|
+
const stderr: Buffer[] = [];
|
|
160
|
+
let buffered = 0;
|
|
161
|
+
let commandError: Error | undefined;
|
|
162
|
+
let settled = false;
|
|
163
|
+
let child: ChildProcessWithoutNullStreams;
|
|
164
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
165
|
+
const finish = (status: number | null) => {
|
|
166
|
+
if (settled) return;
|
|
167
|
+
settled = true;
|
|
168
|
+
if (timeout) clearTimeout(timeout);
|
|
169
|
+
resolve({
|
|
170
|
+
status,
|
|
171
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
172
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
173
|
+
...(commandError ? { error: commandError } : {}),
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
const collect = (target: Buffer[], chunk: Buffer) => {
|
|
177
|
+
buffered += chunk.length;
|
|
178
|
+
if (buffered > options.maxBuffer) {
|
|
179
|
+
commandError ??= new Error("preflight command output exceeded limit");
|
|
180
|
+
child.kill("SIGKILL");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
target.push(chunk);
|
|
184
|
+
};
|
|
185
|
+
try {
|
|
186
|
+
child = spawn(command, args, {
|
|
187
|
+
cwd: options.cwd,
|
|
188
|
+
shell: false,
|
|
189
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
190
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
191
|
+
}) as ChildProcessWithoutNullStreams;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
commandError = error as Error;
|
|
194
|
+
resolve({ status: null, stdout: "", stderr: "", error: commandError });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
timeout = setTimeout(() => {
|
|
198
|
+
commandError ??= new Error("preflight command timed out");
|
|
199
|
+
child.kill("SIGKILL");
|
|
200
|
+
}, options.timeout);
|
|
201
|
+
timeout.unref();
|
|
202
|
+
child.stdout.on("data", (chunk: Buffer) => collect(stdout, chunk));
|
|
203
|
+
child.stderr.on("data", (chunk: Buffer) => collect(stderr, chunk));
|
|
204
|
+
child.on("error", (error) => {
|
|
205
|
+
commandError ??= error;
|
|
206
|
+
});
|
|
207
|
+
child.on("close", (status) => finish(status));
|
|
208
|
+
child.stdin.on("error", () => undefined);
|
|
209
|
+
child.stdin.end(options.input);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reject(
|
|
214
|
+
failureClass: PreflightFailure["failureClass"],
|
|
215
|
+
code: string,
|
|
216
|
+
details: FailureDetails = {},
|
|
217
|
+
): never {
|
|
218
|
+
throw new PreflightFailure(failureClass, code, details);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function failed(
|
|
222
|
+
failureClass: PreflightFailure["failureClass"],
|
|
223
|
+
code: string,
|
|
224
|
+
details: FailureDetails = {},
|
|
225
|
+
checkoutRemoved = true,
|
|
226
|
+
): CandidatePreflightResult {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
class: failureClass,
|
|
230
|
+
code,
|
|
231
|
+
...details,
|
|
232
|
+
checkoutRemoved,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function removeCheckout(
|
|
237
|
+
temp: string,
|
|
238
|
+
dependencies: CandidatePreflightDependencies,
|
|
239
|
+
): { failed: boolean; removed: boolean } {
|
|
240
|
+
let cleanupFailed = false;
|
|
241
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
242
|
+
try {
|
|
243
|
+
dependencies.remove(temp, { recursive: true, force: true });
|
|
244
|
+
} catch {
|
|
245
|
+
cleanupFailed = true;
|
|
246
|
+
}
|
|
247
|
+
let removed = false;
|
|
248
|
+
try {
|
|
249
|
+
removed = lstatSync(temp, { throwIfNoEntry: false }) === undefined;
|
|
250
|
+
} catch {
|
|
251
|
+
cleanupFailed = true;
|
|
252
|
+
}
|
|
253
|
+
if (removed) return { failed: cleanupFailed, removed: true };
|
|
254
|
+
cleanupFailed = true;
|
|
255
|
+
}
|
|
256
|
+
return { failed: true, removed: false };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function sha256(bytes: Buffer): string {
|
|
260
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function safePath(root: string, relative: string): string {
|
|
264
|
+
if (
|
|
265
|
+
!isValidRelativePath(relative) ||
|
|
266
|
+
relative.trim() !== relative ||
|
|
267
|
+
/[\r\n\t]/u.test(relative)
|
|
268
|
+
) {
|
|
269
|
+
reject("artifact", "noncanonical-path");
|
|
270
|
+
}
|
|
271
|
+
const absolute = path.resolve(root, relative);
|
|
272
|
+
if (absolute === root || !absolute.startsWith(`${root}${path.sep}`)) {
|
|
273
|
+
reject("artifact", "escaping-path");
|
|
274
|
+
}
|
|
275
|
+
return absolute;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function sameDirBound(actual: DirBound | null, expected: DirBound): boolean {
|
|
279
|
+
return (
|
|
280
|
+
actual !== null &&
|
|
281
|
+
actual.kind === "dir" &&
|
|
282
|
+
actual.manifest === expected.manifest
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function validSnapshot(snapshot: Bound): boolean {
|
|
287
|
+
if (
|
|
288
|
+
snapshot === null ||
|
|
289
|
+
typeof snapshot !== "object" ||
|
|
290
|
+
Array.isArray(snapshot)
|
|
291
|
+
) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
for (const [relative, value] of Object.entries(snapshot)) {
|
|
295
|
+
if (!isValidRelativePath(relative) || !value || typeof value !== "object")
|
|
296
|
+
return false;
|
|
297
|
+
if (value.kind === "file") {
|
|
298
|
+
if (
|
|
299
|
+
!SHA256.test(value.sha256) ||
|
|
300
|
+
!Number.isSafeInteger(value.bytes) ||
|
|
301
|
+
value.bytes < 0
|
|
302
|
+
)
|
|
303
|
+
return false;
|
|
304
|
+
} else if (value.kind === "dir") {
|
|
305
|
+
if (!SHA256.test(value.manifest)) return false;
|
|
306
|
+
} else if (value.kind === "absent") {
|
|
307
|
+
if (value.absent !== true) return false;
|
|
308
|
+
} else {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function run(
|
|
316
|
+
dependencies: CandidatePreflightDependencies,
|
|
317
|
+
cwd: string,
|
|
318
|
+
command: string,
|
|
319
|
+
args: string[],
|
|
320
|
+
input?: Buffer,
|
|
321
|
+
signal?: AbortSignal,
|
|
322
|
+
): Promise<CommandResult> {
|
|
323
|
+
const options: CommandOptions = {
|
|
324
|
+
cwd,
|
|
325
|
+
shell: false,
|
|
326
|
+
encoding: "utf8",
|
|
327
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
328
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
329
|
+
...(input === undefined ? {} : { input }),
|
|
330
|
+
...(signal === undefined ? {} : { signal }),
|
|
331
|
+
};
|
|
332
|
+
try {
|
|
333
|
+
const result = await dependencies.spawn(command, args, options);
|
|
334
|
+
return {
|
|
335
|
+
status: result.status,
|
|
336
|
+
stdout: result.stdout ?? "",
|
|
337
|
+
stderr: result.stderr ?? "",
|
|
338
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
339
|
+
};
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return {
|
|
342
|
+
status: null,
|
|
343
|
+
stdout: "",
|
|
344
|
+
stderr: "",
|
|
345
|
+
error: error as Error,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function commandExcerpt(
|
|
351
|
+
result: Pick<CommandResult, "stdout" | "stderr">,
|
|
352
|
+
secrets: string[],
|
|
353
|
+
): string {
|
|
354
|
+
let text = `${result.stdout}\n${result.stderr}`;
|
|
355
|
+
for (const secret of secrets) {
|
|
356
|
+
if (secret) text = text.replaceAll(secret, "<path>");
|
|
357
|
+
}
|
|
358
|
+
return [...text]
|
|
359
|
+
.map((character) =>
|
|
360
|
+
character === "\n" ||
|
|
361
|
+
((character.codePointAt(0) ?? 0) >= 0x20 &&
|
|
362
|
+
(character.codePointAt(0) ?? 0) <= 0x7e)
|
|
363
|
+
? character
|
|
364
|
+
: "?",
|
|
365
|
+
)
|
|
366
|
+
.join("")
|
|
367
|
+
.slice(-1024);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function requireCurrentHost(input: CandidatePreflightInput): void {
|
|
371
|
+
if (!isCurrent(input.root, input.snapshot)) reject("stale", "stale-snapshot");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function validateInput(input: CandidatePreflightInput): {
|
|
375
|
+
targets: string[];
|
|
376
|
+
lockPath: string;
|
|
377
|
+
dependencyPath: string;
|
|
378
|
+
} {
|
|
379
|
+
if (!Buffer.isBuffer(input.diff) || input.diff.length === 0)
|
|
380
|
+
reject("artifact", "invalid-diff-bytes");
|
|
381
|
+
if (!validSnapshot(input.snapshot)) reject("artifact", "invalid-snapshot");
|
|
382
|
+
const verification = validateVerificationContract(input.verification);
|
|
383
|
+
if (!verification.ok) reject("artifact", "invalid-verification-contract");
|
|
384
|
+
|
|
385
|
+
let targets: string[];
|
|
386
|
+
try {
|
|
387
|
+
targets = diffWritePaths(input.diff.toString("utf8")).paths;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
reject("artifact", "invalid-diff", {
|
|
390
|
+
excerpt: String((error as Error).message).slice(0, 256),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const writeSet = [...new Set(input.writeSet)];
|
|
395
|
+
if (
|
|
396
|
+
writeSet.length !== input.writeSet.length ||
|
|
397
|
+
writeSet.some((relative) => !isValidRelativePath(relative)) ||
|
|
398
|
+
targets.some((target) => !writeSet.includes(target))
|
|
399
|
+
) {
|
|
400
|
+
reject("artifact", "write-set-mismatch");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const packageBound = input.snapshot["package.json"];
|
|
404
|
+
if (
|
|
405
|
+
packageBound?.kind !== "file" ||
|
|
406
|
+
packageBound.sha256 !== input.packageManifest.sha256 ||
|
|
407
|
+
packageBound.bytes !== input.packageManifest.bytes
|
|
408
|
+
) {
|
|
409
|
+
reject("artifact", "package-bound-mismatch");
|
|
410
|
+
}
|
|
411
|
+
const lockPath = LOCKFILES.find((candidate) => {
|
|
412
|
+
const bound = input.snapshot[candidate];
|
|
413
|
+
return (
|
|
414
|
+
bound?.kind === "file" &&
|
|
415
|
+
bound.sha256 === input.lockfile.sha256 &&
|
|
416
|
+
bound.bytes === input.lockfile.bytes
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
if (!lockPath) reject("artifact", "lockfile-bound-mismatch");
|
|
420
|
+
|
|
421
|
+
const dependencyPath = "node_modules";
|
|
422
|
+
const dependencyBound = input.snapshot[dependencyPath];
|
|
423
|
+
if (
|
|
424
|
+
!dependencyBound ||
|
|
425
|
+
dependencyBound.kind !== input.dependencyTarget.kind ||
|
|
426
|
+
(dependencyBound.kind === "file" &&
|
|
427
|
+
input.dependencyTarget.kind === "file" &&
|
|
428
|
+
(dependencyBound.sha256 !== input.dependencyTarget.sha256 ||
|
|
429
|
+
dependencyBound.bytes !== input.dependencyTarget.bytes)) ||
|
|
430
|
+
(dependencyBound.kind === "dir" &&
|
|
431
|
+
input.dependencyTarget.kind === "dir" &&
|
|
432
|
+
dependencyBound.manifest !== input.dependencyTarget.manifest)
|
|
433
|
+
) {
|
|
434
|
+
reject("artifact", "dependency-bound-mismatch");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const entries = new Map<string, BaselineEntry>();
|
|
438
|
+
for (const entry of input.baseline) {
|
|
439
|
+
if (!entry || !isValidRelativePath(entry.path) || entries.has(entry.path)) {
|
|
440
|
+
reject("artifact", "baseline-shape");
|
|
441
|
+
}
|
|
442
|
+
if (entry.kind === "file") {
|
|
443
|
+
if (
|
|
444
|
+
!SHA256.test(entry.sha256 ?? "") ||
|
|
445
|
+
!Number.isSafeInteger(entry.bytes) ||
|
|
446
|
+
(entry.bytes ?? -1) < 0 ||
|
|
447
|
+
typeof entry.executable !== "boolean"
|
|
448
|
+
) {
|
|
449
|
+
reject("artifact", "baseline-shape");
|
|
450
|
+
}
|
|
451
|
+
const bound = input.snapshot[entry.path];
|
|
452
|
+
if (
|
|
453
|
+
bound?.kind !== "file" ||
|
|
454
|
+
bound.sha256 !== entry.sha256 ||
|
|
455
|
+
bound.bytes !== entry.bytes
|
|
456
|
+
) {
|
|
457
|
+
reject("artifact", "baseline-bound-mismatch");
|
|
458
|
+
}
|
|
459
|
+
} else if (
|
|
460
|
+
entry.kind !== "deleted" ||
|
|
461
|
+
entry.sha256 !== undefined ||
|
|
462
|
+
entry.bytes !== undefined ||
|
|
463
|
+
entry.executable !== undefined ||
|
|
464
|
+
input.snapshot[entry.path]?.kind !== "absent"
|
|
465
|
+
) {
|
|
466
|
+
reject("artifact", "baseline-shape");
|
|
467
|
+
}
|
|
468
|
+
entries.set(entry.path, entry);
|
|
469
|
+
}
|
|
470
|
+
for (const [relative, bound] of Object.entries(input.snapshot)) {
|
|
471
|
+
if (
|
|
472
|
+
(bound.kind === "file" || bound.kind === "absent") &&
|
|
473
|
+
!entries.has(relative)
|
|
474
|
+
) {
|
|
475
|
+
reject("artifact", "incomplete-baseline");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (targets.some((target) => !entries.has(target)))
|
|
479
|
+
reject("artifact", "target-baseline-missing");
|
|
480
|
+
for (const testPath of input.verification.argv.slice(3)) {
|
|
481
|
+
if (!entries.has(testPath))
|
|
482
|
+
reject("artifact", "verification-baseline-missing");
|
|
483
|
+
}
|
|
484
|
+
return { targets, lockPath, dependencyPath };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function verifyBaselineAt(root: string, baseline: BaselineEntry[]): void {
|
|
488
|
+
for (const entry of baseline) {
|
|
489
|
+
const absolute = safePath(root, entry.path);
|
|
490
|
+
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
491
|
+
if (entry.kind === "deleted") {
|
|
492
|
+
if (stat) reject("stale", "baseline-deletion-drift");
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (!stat?.isFile() || stat.isSymbolicLink())
|
|
496
|
+
reject("artifact", "baseline-file-kind");
|
|
497
|
+
const bytes = readFileSync(absolute);
|
|
498
|
+
if (
|
|
499
|
+
bytes.length !== entry.bytes ||
|
|
500
|
+
sha256(bytes) !== entry.sha256 ||
|
|
501
|
+
Boolean(stat.mode & 0o111) !== entry.executable
|
|
502
|
+
) {
|
|
503
|
+
reject("stale", "baseline-file-drift");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function scanCheckout(checkout: string): void {
|
|
509
|
+
const visit = (directory: string): void => {
|
|
510
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
511
|
+
if (directory === checkout && entry.name === ".git") continue;
|
|
512
|
+
const absolute = path.join(directory, entry.name);
|
|
513
|
+
const stat = lstatSync(absolute);
|
|
514
|
+
if (stat.isSymbolicLink()) reject("artifact", "checkout-symlink");
|
|
515
|
+
if (stat.isDirectory()) visit(absolute);
|
|
516
|
+
else if (!stat.isFile()) reject("artifact", "checkout-special-file");
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
visit(checkout);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function resolveBunExecutable(
|
|
523
|
+
dependencies: CandidatePreflightDependencies,
|
|
524
|
+
): string {
|
|
525
|
+
const candidates = [
|
|
526
|
+
...(process.versions.bun ? [process.execPath] : []),
|
|
527
|
+
...(process.env.PATH ?? "")
|
|
528
|
+
.split(path.delimiter)
|
|
529
|
+
.filter(Boolean)
|
|
530
|
+
.map((entry) => path.join(entry, "bun")),
|
|
531
|
+
];
|
|
532
|
+
for (const candidate of candidates) {
|
|
533
|
+
try {
|
|
534
|
+
const executable = dependencies.realpath(candidate);
|
|
535
|
+
const stat = lstatSync(executable);
|
|
536
|
+
if (stat.isFile() && (stat.mode & 0o111) !== 0) return executable;
|
|
537
|
+
} catch {}
|
|
538
|
+
}
|
|
539
|
+
reject("environment", "bun-executable-unavailable");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function requireRegularGitModes(
|
|
543
|
+
dependencies: CandidatePreflightDependencies,
|
|
544
|
+
checkout: string,
|
|
545
|
+
signal?: AbortSignal,
|
|
546
|
+
): Promise<void> {
|
|
547
|
+
const result = await run(
|
|
548
|
+
dependencies,
|
|
549
|
+
checkout,
|
|
550
|
+
"git",
|
|
551
|
+
["ls-files", "--stage", "-z"],
|
|
552
|
+
undefined,
|
|
553
|
+
signal,
|
|
554
|
+
);
|
|
555
|
+
if (signal?.aborted) reject("cancelled", "cancelled");
|
|
556
|
+
if (result.error || result.status !== 0)
|
|
557
|
+
reject("environment", "git-index-unavailable");
|
|
558
|
+
for (const record of result.stdout.split("\0").filter(Boolean)) {
|
|
559
|
+
const mode = record.slice(0, 6);
|
|
560
|
+
if (mode !== "100644" && mode !== "100755")
|
|
561
|
+
reject("artifact", mode === "160000" ? "submodule" : "nonregular-mode");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function reconstructBaseline(
|
|
566
|
+
input: CandidatePreflightInput,
|
|
567
|
+
checkout: string,
|
|
568
|
+
): void {
|
|
569
|
+
for (const entry of input.baseline) {
|
|
570
|
+
const source = safePath(input.root, entry.path);
|
|
571
|
+
const target = safePath(checkout, entry.path);
|
|
572
|
+
if (entry.kind === "deleted") {
|
|
573
|
+
const stat = lstatSync(target, { throwIfNoEntry: false });
|
|
574
|
+
if (stat?.isDirectory()) reject("artifact", "baseline-deletion-kind");
|
|
575
|
+
rmSync(target, { force: true });
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
579
|
+
copyFileSync(source, target);
|
|
580
|
+
const stat = lstatSync(target);
|
|
581
|
+
const mode = entry.executable ? 0o755 : 0o644;
|
|
582
|
+
if ((stat.mode & 0o777) !== mode) {
|
|
583
|
+
chmodSync(target, mode);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
verifyBaselineAt(checkout, input.baseline);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function copySafeDirectory(source: string, target: string): void {
|
|
590
|
+
const directory = lstatSync(source, { throwIfNoEntry: false });
|
|
591
|
+
if (!directory?.isDirectory() || directory.isSymbolicLink())
|
|
592
|
+
reject("artifact", "baseline-directory-kind");
|
|
593
|
+
mkdirSync(target, { recursive: true });
|
|
594
|
+
chmodSync(target, directory.mode & 0o777);
|
|
595
|
+
for (const name of readdirSync(source).sort()) {
|
|
596
|
+
const sourceEntry = path.join(source, name);
|
|
597
|
+
const targetEntry = path.join(target, name);
|
|
598
|
+
const stat = lstatSync(sourceEntry);
|
|
599
|
+
if (stat.isSymbolicLink()) reject("artifact", "baseline-directory-symlink");
|
|
600
|
+
if (stat.isDirectory()) {
|
|
601
|
+
copySafeDirectory(sourceEntry, targetEntry);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (!stat.isFile()) reject("artifact", "baseline-directory-special-file");
|
|
605
|
+
copyFileSync(sourceEntry, targetEntry);
|
|
606
|
+
chmodSync(targetEntry, stat.mode & 0o777);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function reconstructBoundDirectories(
|
|
611
|
+
input: CandidatePreflightInput,
|
|
612
|
+
checkout: string,
|
|
613
|
+
dependencyPath: string,
|
|
614
|
+
): void {
|
|
615
|
+
const directories = Object.entries(input.snapshot)
|
|
616
|
+
.filter(
|
|
617
|
+
(entry): entry is [string, DirBound] =>
|
|
618
|
+
entry[1].kind === "dir" && entry[0] !== dependencyPath,
|
|
619
|
+
)
|
|
620
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
621
|
+
const roots = directories.filter(
|
|
622
|
+
([relative], index) =>
|
|
623
|
+
!directories.some(
|
|
624
|
+
([parent], parentIndex) =>
|
|
625
|
+
parentIndex !== index && relative.startsWith(`${parent}/`),
|
|
626
|
+
),
|
|
627
|
+
);
|
|
628
|
+
for (const [relative, expected] of roots) {
|
|
629
|
+
const source = safePath(input.root, relative);
|
|
630
|
+
const target = safePath(checkout, relative);
|
|
631
|
+
rmSync(target, { recursive: true, force: true });
|
|
632
|
+
copySafeDirectory(source, target);
|
|
633
|
+
if (!sameDirBound(snapshotDirManifest(checkout, relative), expected)) {
|
|
634
|
+
if (!sameDirBound(snapshotDirManifest(input.root, relative), expected))
|
|
635
|
+
reject("stale", "baseline-directory-drift");
|
|
636
|
+
reject("artifact", "baseline-directory-mismatch");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function sandboxArgs(
|
|
642
|
+
checkout: string,
|
|
643
|
+
dependencySource: string,
|
|
644
|
+
dependencyIsDirectory: boolean,
|
|
645
|
+
bunExecutable: string,
|
|
646
|
+
invocation: string[],
|
|
647
|
+
reportSource?: string,
|
|
648
|
+
): string[] {
|
|
649
|
+
const candidate = "/candidate";
|
|
650
|
+
const args = [
|
|
651
|
+
"--unshare-net",
|
|
652
|
+
"--unshare-pid",
|
|
653
|
+
"--die-with-parent",
|
|
654
|
+
"--new-session",
|
|
655
|
+
"--proc",
|
|
656
|
+
"/proc",
|
|
657
|
+
"--dev",
|
|
658
|
+
"/dev",
|
|
659
|
+
"--tmpfs",
|
|
660
|
+
"/tmp",
|
|
661
|
+
"--clearenv",
|
|
662
|
+
"--setenv",
|
|
663
|
+
"PATH",
|
|
664
|
+
"/usr/bin:/usr/local/bin:/bin",
|
|
665
|
+
"--setenv",
|
|
666
|
+
"HOME",
|
|
667
|
+
"/nonexistent",
|
|
668
|
+
"--setenv",
|
|
669
|
+
"CI",
|
|
670
|
+
"1",
|
|
671
|
+
];
|
|
672
|
+
for (const systemPath of ["/usr", "/bin", "/lib", "/lib64", "/etc"]) {
|
|
673
|
+
if (existsSync(systemPath)) args.push("--ro-bind", systemPath, systemPath);
|
|
674
|
+
}
|
|
675
|
+
args.push("--ro-bind", bunExecutable, SANDBOX_BUN_TARGET);
|
|
676
|
+
const dependencyDestination = path.posix.join(candidate, "node_modules");
|
|
677
|
+
const localDependency = path.join(checkout, "node_modules");
|
|
678
|
+
rmSync(localDependency, { recursive: true, force: true });
|
|
679
|
+
if (dependencyIsDirectory) {
|
|
680
|
+
mkdirSync(localDependency, { recursive: true });
|
|
681
|
+
} else {
|
|
682
|
+
writeFileSync(localDependency, "");
|
|
683
|
+
}
|
|
684
|
+
args.push(
|
|
685
|
+
"--dir",
|
|
686
|
+
candidate,
|
|
687
|
+
"--bind",
|
|
688
|
+
checkout,
|
|
689
|
+
candidate,
|
|
690
|
+
"--ro-bind",
|
|
691
|
+
dependencySource,
|
|
692
|
+
dependencyDestination,
|
|
693
|
+
...(reportSource ? ["--bind", reportSource, SANDBOX_REPORT_TARGET] : []),
|
|
694
|
+
"--chdir",
|
|
695
|
+
candidate,
|
|
696
|
+
"--",
|
|
697
|
+
...invocation,
|
|
698
|
+
);
|
|
699
|
+
return args;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
interface StructuredTestReport {
|
|
703
|
+
total: number;
|
|
704
|
+
failed: number;
|
|
705
|
+
success: boolean;
|
|
706
|
+
failedAssertions: string[];
|
|
707
|
+
fileErrors: string[];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function structuredTestReport(reportPath: string): StructuredTestReport {
|
|
711
|
+
const stat = lstatSync(reportPath, { throwIfNoEntry: false });
|
|
712
|
+
if (
|
|
713
|
+
!stat?.isFile() ||
|
|
714
|
+
stat.isSymbolicLink() ||
|
|
715
|
+
stat.size === 0 ||
|
|
716
|
+
stat.size > MAX_REPORT_BYTES
|
|
717
|
+
) {
|
|
718
|
+
reject("artifact", "verification-report-missing");
|
|
719
|
+
}
|
|
720
|
+
let value: unknown;
|
|
721
|
+
try {
|
|
722
|
+
value = JSON.parse(readFileSync(reportPath, "utf8"));
|
|
723
|
+
} catch {
|
|
724
|
+
reject("artifact", "verification-report-invalid");
|
|
725
|
+
}
|
|
726
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
727
|
+
reject("artifact", "verification-report-invalid");
|
|
728
|
+
const report = value as Record<string, unknown>;
|
|
729
|
+
const total = report.numTotalTests;
|
|
730
|
+
const failed = report.numFailedTests;
|
|
731
|
+
if (
|
|
732
|
+
!Number.isSafeInteger(total) ||
|
|
733
|
+
(total as number) < 0 ||
|
|
734
|
+
!Number.isSafeInteger(failed) ||
|
|
735
|
+
(failed as number) < 0 ||
|
|
736
|
+
(failed as number) > (total as number) ||
|
|
737
|
+
typeof report.success !== "boolean" ||
|
|
738
|
+
!Array.isArray(report.testResults)
|
|
739
|
+
) {
|
|
740
|
+
reject("artifact", "verification-report-invalid");
|
|
741
|
+
}
|
|
742
|
+
const failedAssertions: string[] = [];
|
|
743
|
+
const fileErrors: string[] = [];
|
|
744
|
+
let failedRecords = 0;
|
|
745
|
+
for (const file of report.testResults) {
|
|
746
|
+
if (!file || typeof file !== "object" || Array.isArray(file))
|
|
747
|
+
reject("artifact", "verification-report-invalid");
|
|
748
|
+
const fileRecord = file as Record<string, unknown>;
|
|
749
|
+
const assertions = fileRecord.assertionResults;
|
|
750
|
+
if (!Array.isArray(assertions) || typeof fileRecord.message !== "string")
|
|
751
|
+
reject("artifact", "verification-report-invalid");
|
|
752
|
+
if (fileRecord.message) fileErrors.push(fileRecord.message);
|
|
753
|
+
for (const assertion of assertions) {
|
|
754
|
+
if (
|
|
755
|
+
!assertion ||
|
|
756
|
+
typeof assertion !== "object" ||
|
|
757
|
+
Array.isArray(assertion)
|
|
758
|
+
)
|
|
759
|
+
reject("artifact", "verification-report-invalid");
|
|
760
|
+
const record = assertion as Record<string, unknown>;
|
|
761
|
+
if (record.status !== "failed") continue;
|
|
762
|
+
if (
|
|
763
|
+
typeof record.fullName !== "string" ||
|
|
764
|
+
typeof record.title !== "string" ||
|
|
765
|
+
!Array.isArray(record.failureMessages) ||
|
|
766
|
+
record.failureMessages.some((message) => typeof message !== "string")
|
|
767
|
+
) {
|
|
768
|
+
reject("artifact", "verification-report-invalid");
|
|
769
|
+
}
|
|
770
|
+
failedRecords++;
|
|
771
|
+
failedAssertions.push(
|
|
772
|
+
record.fullName,
|
|
773
|
+
record.title,
|
|
774
|
+
...(record.failureMessages as string[]),
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (failedRecords !== failed)
|
|
779
|
+
reject("artifact", "verification-report-invalid");
|
|
780
|
+
return {
|
|
781
|
+
total: total as number,
|
|
782
|
+
failed: failed as number,
|
|
783
|
+
success: report.success,
|
|
784
|
+
failedAssertions,
|
|
785
|
+
fileErrors,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function normalizeTarget(
|
|
790
|
+
input: CandidatePreflightInput,
|
|
791
|
+
result: CommandResult,
|
|
792
|
+
secrets: string[],
|
|
793
|
+
report?: StructuredTestReport,
|
|
794
|
+
): CandidatePreflightResult {
|
|
795
|
+
const staticCheck = isStaticCheck(input.verification);
|
|
796
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
797
|
+
const count = staticCheck ? 0 : (report?.total ?? 0);
|
|
798
|
+
const identityMatch =
|
|
799
|
+
input.verification.classification !== "expected-red" ||
|
|
800
|
+
(staticCheck
|
|
801
|
+
? output.includes(input.verification.expectedFailure ?? "")
|
|
802
|
+
: (report?.failedAssertions.some((failure) =>
|
|
803
|
+
failure.includes(input.verification.expectedFailure ?? ""),
|
|
804
|
+
) ?? false));
|
|
805
|
+
const loadable =
|
|
806
|
+
staticCheck ||
|
|
807
|
+
((report?.fileErrors.length ?? 0) === 0 && !LOAD_FAILURE.test(output));
|
|
808
|
+
const enoughTests = staticCheck || count >= input.verification.minTests;
|
|
809
|
+
const accepted =
|
|
810
|
+
input.verification.classification === "expected-red"
|
|
811
|
+
? result.status !== 0 &&
|
|
812
|
+
(staticCheck || (report?.failed ?? 0) > 0) &&
|
|
813
|
+
enoughTests &&
|
|
814
|
+
identityMatch &&
|
|
815
|
+
loadable
|
|
816
|
+
: result.status === 0 &&
|
|
817
|
+
(staticCheck || report?.success === true) &&
|
|
818
|
+
enoughTests &&
|
|
819
|
+
loadable;
|
|
820
|
+
const details = {
|
|
821
|
+
commandId: input.verification.id,
|
|
822
|
+
exitCode: result.status ?? 1,
|
|
823
|
+
testCount: count,
|
|
824
|
+
identityMatch,
|
|
825
|
+
};
|
|
826
|
+
if (
|
|
827
|
+
input.verification.classification === "expected-red" &&
|
|
828
|
+
result.status === 0 &&
|
|
829
|
+
enoughTests &&
|
|
830
|
+
loadable
|
|
831
|
+
) {
|
|
832
|
+
return failed("design", "red-contract-invalid", {
|
|
833
|
+
...details,
|
|
834
|
+
excerpt: commandExcerpt(result, secrets),
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
if (!accepted) {
|
|
838
|
+
return failed("artifact", "verification-rejected", {
|
|
839
|
+
...details,
|
|
840
|
+
excerpt: commandExcerpt(result, secrets),
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
ok: true,
|
|
845
|
+
classification: input.verification.classification,
|
|
846
|
+
targets: [],
|
|
847
|
+
...details,
|
|
848
|
+
checkoutRemoved: true,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function isStaticCheck(verification: VerificationContract): boolean {
|
|
853
|
+
return (
|
|
854
|
+
verification.argv.length === 3 &&
|
|
855
|
+
verification.argv[0] === "bun" &&
|
|
856
|
+
verification.argv[1] === "run" &&
|
|
857
|
+
verification.argv[2] === "check"
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function resolveDependencySource(
|
|
862
|
+
dependencies: CandidatePreflightDependencies,
|
|
863
|
+
root: string,
|
|
864
|
+
dependencyPath: string,
|
|
865
|
+
): string {
|
|
866
|
+
const unresolved = safePath(root, dependencyPath);
|
|
867
|
+
let stat: ReturnType<typeof lstatSync> | undefined;
|
|
868
|
+
try {
|
|
869
|
+
stat = lstatSync(unresolved, { throwIfNoEntry: false });
|
|
870
|
+
} catch {
|
|
871
|
+
reject("environment", "dependency-path-unsafe");
|
|
872
|
+
}
|
|
873
|
+
if (!stat || stat.isSymbolicLink())
|
|
874
|
+
reject("environment", "dependency-path-unsafe");
|
|
875
|
+
let canonical: string;
|
|
876
|
+
try {
|
|
877
|
+
canonical = dependencies.realpath(unresolved);
|
|
878
|
+
} catch {
|
|
879
|
+
reject("environment", "dependency-path-unsafe");
|
|
880
|
+
}
|
|
881
|
+
if (canonical === root || !canonical.startsWith(`${root}${path.sep}`))
|
|
882
|
+
reject("environment", "dependency-path-unsafe");
|
|
883
|
+
return canonical;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export async function preflightCandidate(
|
|
887
|
+
input: CandidatePreflightInput,
|
|
888
|
+
dependencies: CandidatePreflightDependencies = defaultDependencies,
|
|
889
|
+
): Promise<CandidatePreflightResult> {
|
|
890
|
+
let temp: string | undefined;
|
|
891
|
+
let checkout: string | undefined;
|
|
892
|
+
let targets: string[] = [];
|
|
893
|
+
let outcome: CandidatePreflightResult;
|
|
894
|
+
try {
|
|
895
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
896
|
+
if (!validSnapshot(input.snapshot)) reject("artifact", "invalid-snapshot");
|
|
897
|
+
requireCurrentHost(input);
|
|
898
|
+
const validated = validateInput(input);
|
|
899
|
+
targets = validated.targets;
|
|
900
|
+
verifyBaselineAt(input.root, input.baseline);
|
|
901
|
+
|
|
902
|
+
let root: string;
|
|
903
|
+
try {
|
|
904
|
+
root = dependencies.realpath(input.root);
|
|
905
|
+
} catch {
|
|
906
|
+
reject("environment", "root-unavailable");
|
|
907
|
+
}
|
|
908
|
+
const dependencySource = resolveDependencySource(
|
|
909
|
+
dependencies,
|
|
910
|
+
root,
|
|
911
|
+
validated.dependencyPath,
|
|
912
|
+
);
|
|
913
|
+
const prefix = path.join(
|
|
914
|
+
path.dirname(root),
|
|
915
|
+
`.${path.basename(root)}-candidate-`,
|
|
916
|
+
);
|
|
917
|
+
temp = dependencies.mkdtemp(prefix);
|
|
918
|
+
checkout = path.join(temp, "candidate");
|
|
919
|
+
|
|
920
|
+
const bundle = path.join(temp, "source.bundle");
|
|
921
|
+
const transportLockdown = [
|
|
922
|
+
"-c",
|
|
923
|
+
"protocol.allow=never",
|
|
924
|
+
"-c",
|
|
925
|
+
"protocol.file.allow=never",
|
|
926
|
+
"-c",
|
|
927
|
+
"protocol.ext.allow=never",
|
|
928
|
+
];
|
|
929
|
+
let result = await run(
|
|
930
|
+
dependencies,
|
|
931
|
+
root,
|
|
932
|
+
"git",
|
|
933
|
+
[...transportLockdown, "bundle", "create", bundle, "HEAD"],
|
|
934
|
+
undefined,
|
|
935
|
+
input.signal,
|
|
936
|
+
);
|
|
937
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
938
|
+
if (result.error || result.status !== 0)
|
|
939
|
+
reject("environment", "clone-failed");
|
|
940
|
+
result = await run(
|
|
941
|
+
dependencies,
|
|
942
|
+
temp,
|
|
943
|
+
"git",
|
|
944
|
+
["init", "-q", checkout],
|
|
945
|
+
undefined,
|
|
946
|
+
input.signal,
|
|
947
|
+
);
|
|
948
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
949
|
+
if (result.error || result.status !== 0)
|
|
950
|
+
reject("environment", "clone-failed");
|
|
951
|
+
result = await run(
|
|
952
|
+
dependencies,
|
|
953
|
+
checkout,
|
|
954
|
+
"git",
|
|
955
|
+
[
|
|
956
|
+
...transportLockdown,
|
|
957
|
+
"-c",
|
|
958
|
+
"protocol.file.allow=always",
|
|
959
|
+
"fetch",
|
|
960
|
+
"-q",
|
|
961
|
+
"--no-tags",
|
|
962
|
+
"--no-recurse-submodules",
|
|
963
|
+
bundle,
|
|
964
|
+
"HEAD",
|
|
965
|
+
],
|
|
966
|
+
undefined,
|
|
967
|
+
input.signal,
|
|
968
|
+
);
|
|
969
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
970
|
+
if (result.error || result.status !== 0)
|
|
971
|
+
reject("environment", "clone-failed");
|
|
972
|
+
result = await run(
|
|
973
|
+
dependencies,
|
|
974
|
+
checkout,
|
|
975
|
+
"git",
|
|
976
|
+
["checkout", "-q", "--detach", "FETCH_HEAD"],
|
|
977
|
+
undefined,
|
|
978
|
+
input.signal,
|
|
979
|
+
);
|
|
980
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
981
|
+
if (result.error || result.status !== 0)
|
|
982
|
+
reject("environment", "checkout-failed");
|
|
983
|
+
if (
|
|
984
|
+
existsSync(path.join(checkout, ".git", "objects", "info", "alternates"))
|
|
985
|
+
) {
|
|
986
|
+
reject("environment", "clone-alternates");
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
await requireRegularGitModes(dependencies, checkout, input.signal);
|
|
990
|
+
scanCheckout(checkout);
|
|
991
|
+
for (const entry of input.baseline) {
|
|
992
|
+
const ignored = await run(
|
|
993
|
+
dependencies,
|
|
994
|
+
root,
|
|
995
|
+
"git",
|
|
996
|
+
["check-ignore", "-q", "--", entry.path],
|
|
997
|
+
undefined,
|
|
998
|
+
input.signal,
|
|
999
|
+
);
|
|
1000
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
1001
|
+
if (ignored.error || ignored.status === null || ignored.status > 1)
|
|
1002
|
+
reject("environment", "git-ignore-check-failed");
|
|
1003
|
+
if (ignored.status === 0) reject("artifact", "ignored-baseline-input");
|
|
1004
|
+
}
|
|
1005
|
+
reconstructBoundDirectories(input, checkout, validated.dependencyPath);
|
|
1006
|
+
reconstructBaseline(input, checkout);
|
|
1007
|
+
scanCheckout(checkout);
|
|
1008
|
+
|
|
1009
|
+
result = await run(
|
|
1010
|
+
dependencies,
|
|
1011
|
+
checkout,
|
|
1012
|
+
"git",
|
|
1013
|
+
["apply", "--check", "--recount", "--whitespace=nowarn", "-"],
|
|
1014
|
+
input.diff,
|
|
1015
|
+
input.signal,
|
|
1016
|
+
);
|
|
1017
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
1018
|
+
if (result.error) reject("environment", "git-apply-check-unavailable");
|
|
1019
|
+
if (result.status !== 0)
|
|
1020
|
+
reject("artifact", "git-apply-check-failed", {
|
|
1021
|
+
excerpt: commandExcerpt(result, [root, checkout]),
|
|
1022
|
+
});
|
|
1023
|
+
result = await run(
|
|
1024
|
+
dependencies,
|
|
1025
|
+
checkout,
|
|
1026
|
+
"git",
|
|
1027
|
+
["apply", "--recount", "--whitespace=nowarn", "-"],
|
|
1028
|
+
input.diff,
|
|
1029
|
+
input.signal,
|
|
1030
|
+
);
|
|
1031
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
1032
|
+
if (result.error) reject("environment", "git-apply-unavailable");
|
|
1033
|
+
if (result.status !== 0) reject("artifact", "git-apply-failed");
|
|
1034
|
+
await requireRegularGitModes(dependencies, checkout, input.signal);
|
|
1035
|
+
scanCheckout(checkout);
|
|
1036
|
+
|
|
1037
|
+
for (const testPath of input.verification.argv.slice(3)) {
|
|
1038
|
+
const stat = lstatSync(safePath(checkout, testPath), {
|
|
1039
|
+
throwIfNoEntry: false,
|
|
1040
|
+
});
|
|
1041
|
+
if (!stat?.isFile() || stat.isSymbolicLink())
|
|
1042
|
+
reject("artifact", "verification-input-unavailable");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
let bwrap: string;
|
|
1046
|
+
let bunExecutable: string;
|
|
1047
|
+
try {
|
|
1048
|
+
bwrap = dependencies.realpath(dependencies.bwrapPath);
|
|
1049
|
+
bunExecutable = resolveBunExecutable(dependencies);
|
|
1050
|
+
} catch {
|
|
1051
|
+
reject("environment", "bubblewrap-or-dependency-unavailable");
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const probeArgs = sandboxArgs(
|
|
1055
|
+
checkout,
|
|
1056
|
+
dependencySource,
|
|
1057
|
+
input.dependencyTarget.kind === "dir",
|
|
1058
|
+
bunExecutable,
|
|
1059
|
+
["bun", "--version"],
|
|
1060
|
+
);
|
|
1061
|
+
result = await run(
|
|
1062
|
+
dependencies,
|
|
1063
|
+
checkout,
|
|
1064
|
+
bwrap,
|
|
1065
|
+
probeArgs,
|
|
1066
|
+
undefined,
|
|
1067
|
+
input.signal,
|
|
1068
|
+
);
|
|
1069
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
1070
|
+
if (result.error || result.status !== 0)
|
|
1071
|
+
reject("environment", "sandbox-runtime-unavailable", {
|
|
1072
|
+
exitCode: result.status ?? 1,
|
|
1073
|
+
excerpt: commandExcerpt(result, [root, checkout]),
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
if (!isStaticCheck(input.verification)) {
|
|
1077
|
+
const checkArgs = sandboxArgs(
|
|
1078
|
+
checkout,
|
|
1079
|
+
dependencySource,
|
|
1080
|
+
input.dependencyTarget.kind === "dir",
|
|
1081
|
+
bunExecutable,
|
|
1082
|
+
["bun", "run", "check"],
|
|
1083
|
+
);
|
|
1084
|
+
result = await run(
|
|
1085
|
+
dependencies,
|
|
1086
|
+
checkout,
|
|
1087
|
+
bwrap,
|
|
1088
|
+
checkArgs,
|
|
1089
|
+
undefined,
|
|
1090
|
+
input.signal,
|
|
1091
|
+
);
|
|
1092
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
1093
|
+
if (result.error || result.status === null)
|
|
1094
|
+
reject("environment", "bubblewrap-launch-failed", {
|
|
1095
|
+
excerpt: commandExcerpt(result, [root, checkout]),
|
|
1096
|
+
});
|
|
1097
|
+
if (result.status !== 0)
|
|
1098
|
+
reject("artifact", "candidate-check-failed", {
|
|
1099
|
+
commandId: input.verification.id,
|
|
1100
|
+
exitCode: result.status,
|
|
1101
|
+
excerpt: commandExcerpt(result, [root, checkout]),
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const structuredReportPath = isStaticCheck(input.verification)
|
|
1106
|
+
? undefined
|
|
1107
|
+
: path.join(temp, "verification-report.json");
|
|
1108
|
+
if (structuredReportPath)
|
|
1109
|
+
writeFileSync(structuredReportPath, "", { mode: 0o600 });
|
|
1110
|
+
const targetInvocation = structuredReportPath
|
|
1111
|
+
? [
|
|
1112
|
+
...input.verification.argv,
|
|
1113
|
+
"--reporter=json",
|
|
1114
|
+
`--outputFile=${SANDBOX_REPORT_TARGET}`,
|
|
1115
|
+
]
|
|
1116
|
+
: input.verification.argv;
|
|
1117
|
+
const targetArgs = sandboxArgs(
|
|
1118
|
+
checkout,
|
|
1119
|
+
dependencySource,
|
|
1120
|
+
input.dependencyTarget.kind === "dir",
|
|
1121
|
+
bunExecutable,
|
|
1122
|
+
targetInvocation,
|
|
1123
|
+
structuredReportPath,
|
|
1124
|
+
);
|
|
1125
|
+
result = await run(
|
|
1126
|
+
dependencies,
|
|
1127
|
+
checkout,
|
|
1128
|
+
bwrap,
|
|
1129
|
+
targetArgs,
|
|
1130
|
+
undefined,
|
|
1131
|
+
input.signal,
|
|
1132
|
+
);
|
|
1133
|
+
if (input.signal?.aborted) reject("cancelled", "cancelled");
|
|
1134
|
+
requireCurrentHost(input);
|
|
1135
|
+
if (result.error || result.status === null)
|
|
1136
|
+
reject("environment", "bubblewrap-launch-failed", {
|
|
1137
|
+
excerpt: commandExcerpt(result, [root, checkout]),
|
|
1138
|
+
});
|
|
1139
|
+
const report = structuredReportPath
|
|
1140
|
+
? structuredTestReport(structuredReportPath)
|
|
1141
|
+
: undefined;
|
|
1142
|
+
const normalized = normalizeTarget(input, result, [root, checkout], report);
|
|
1143
|
+
outcome = normalized.ok ? { ...normalized, targets } : normalized;
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (input.signal?.aborted) outcome = failed("cancelled", "cancelled");
|
|
1146
|
+
else if (error instanceof PreflightFailure)
|
|
1147
|
+
outcome = failed(error.failureClass, error.code, error.details);
|
|
1148
|
+
else
|
|
1149
|
+
outcome = failed("environment", "preflight-failed", {
|
|
1150
|
+
excerpt: String((error as Error).message).slice(0, 256),
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
if (temp === undefined) return outcome;
|
|
1154
|
+
const cleanup = removeCheckout(temp, dependencies);
|
|
1155
|
+
if (cleanup.failed)
|
|
1156
|
+
return failed(
|
|
1157
|
+
"environment",
|
|
1158
|
+
"checkout-cleanup-failed",
|
|
1159
|
+
{},
|
|
1160
|
+
cleanup.removed,
|
|
1161
|
+
);
|
|
1162
|
+
return outcome;
|
|
1163
|
+
}
|