@abelxiaoxing/cadence 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -4
- package/config/.env.example +6 -0
- package/config/plan-draft.example.json +3 -0
- package/config/plan-draft.multiple-tasks.example.json +30 -0
- package/package.json +1 -1
- package/prompts/abel-design.md +33 -3
- package/prompts/abel-implement.md +36 -4
- package/src/apply-transaction.ts +28 -8
- package/src/build-windows-job.mjs +95 -0
- package/src/candidate-workspace.ts +6 -0
- package/src/change-verification.ts +764 -67
- package/src/contracts.ts +8 -0
- package/src/delivery-compiler.ts +452 -9
- package/src/design-control.ts +2 -2
- package/src/design-diagnostics.ts +6 -4
- package/src/durable-contracts.ts +21 -3
- package/src/durable-workflow.ts +55 -40
- package/src/execution-profile.ts +28 -2
- package/src/execution-retention.ts +77 -0
- package/src/implement-continuation.ts +559 -0
- package/src/implement-graph.ts +114 -0
- package/src/implement-plan.ts +2 -0
- package/src/index.ts +151 -1
- package/src/isolation-backend.ts +2 -1
- package/src/operator-cli.mjs +664 -104
- package/src/operator-tools.ts +7 -1
- package/src/package-state.ts +11 -0
- package/src/package-verification.ts +66 -18
- package/src/package-workflow.ts +6 -0
- package/src/phase-execution.ts +18 -7
- package/src/plan-draft-summary.ts +385 -7
- package/src/plan-draft.ts +9 -0
- package/src/remove-path.ts +32 -0
- package/src/subagent-activity.ts +3 -1
- package/src/verification-capability.ts +18 -6
- package/src/verification-environment-io.ts +123 -14
- package/src/verification-environment.ts +50 -3
- package/src/windows-job-backend.ts +419 -0
- package/src/windows-job.c +149 -0
- package/src/workflow-policy.ts +296 -5
- package/src/workflow-recovery-policy.ts +2 -0
- package/src/workflow-state-machine.ts +314 -80
- package/src/workflow-status.ts +251 -59
- package/src/workspace-io-entry.mjs +2 -2
- package/src/workspace-io-worker.mjs +207 -110
package/src/operator-cli.mjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
// Generated by scripts/build-workspace-io.mjs; edit operator-cli-entry.mjs and its source modules.
|
|
2
2
|
// src/operator-cli-entry.mjs
|
|
3
|
-
import
|
|
3
|
+
import path9 from "node:path";
|
|
4
4
|
|
|
5
5
|
// src/operator-tools.ts
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
import { lstatSync as
|
|
8
|
-
import
|
|
6
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7
|
+
import { lstatSync as lstatSync8, readdirSync as readdirSync4, readFileSync as readFileSync5, realpathSync as realpathSync4 } from "node:fs";
|
|
8
|
+
import path8 from "node:path";
|
|
9
9
|
import { DatabaseSync } from "node:sqlite";
|
|
10
10
|
|
|
11
11
|
// src/execution-profile.ts
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
12
14
|
function executionProfile(env = process.env) {
|
|
13
15
|
const mode = env.ABEL_EXECUTION_MODE ?? "isolated";
|
|
14
|
-
if (mode !== "isolated" && mode !== "local-trusted")
|
|
16
|
+
if (mode !== "isolated" && mode !== "local-trusted" && mode !== "host-trusted")
|
|
15
17
|
throw new Error("execution-mode-invalid");
|
|
16
18
|
const timeoutMs = Number(env.ABEL_VERIFICATION_TIMEOUT_MS ?? 600000);
|
|
17
19
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86400000)
|
|
@@ -21,17 +23,22 @@ function executionProfile(env = process.env) {
|
|
|
21
23
|
throw new Error("verification-environment-names-invalid");
|
|
22
24
|
if (mode === "isolated" && inheritEnvironment.length)
|
|
23
25
|
throw new Error("verification-environment-requires-trusted-mode");
|
|
26
|
+
if (mode === "host-trusted" && env.ABEL_WINDOWS_JOB_HELPER !== undefined && (!env.ABEL_WINDOWS_JOB_HELPER || env.ABEL_WINDOWS_JOB_HELPER.includes("\x00") || !(path.isAbsolute(env.ABEL_WINDOWS_JOB_HELPER) || path.win32.isAbsolute(env.ABEL_WINDOWS_JOB_HELPER))))
|
|
27
|
+
throw new Error("windows-job-helper-path-invalid");
|
|
24
28
|
return {
|
|
25
29
|
mode,
|
|
26
30
|
timeoutMs,
|
|
27
31
|
bwrapPath: env.ABEL_BWRAP_PATH ?? "/usr/bin/bwrap",
|
|
28
|
-
inheritEnvironment
|
|
32
|
+
inheritEnvironment,
|
|
33
|
+
...mode === "host-trusted" ? {
|
|
34
|
+
windowsHelperPath: env.ABEL_WINDOWS_JOB_HELPER ?? fileURLToPath(new URL("./windows-job.exe", import.meta.url))
|
|
35
|
+
} : {}
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
// src/openspec-cli.ts
|
|
33
40
|
import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
34
|
-
import
|
|
41
|
+
import path2 from "node:path";
|
|
35
42
|
var PACKAGE_NAME = "@fission-ai/openspec";
|
|
36
43
|
var OPENSPEC_LIMITS = Object.freeze({
|
|
37
44
|
timeoutMs: 30000,
|
|
@@ -57,8 +64,8 @@ function record(value) {
|
|
|
57
64
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
58
65
|
}
|
|
59
66
|
function inside(root, target) {
|
|
60
|
-
const relative =
|
|
61
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
67
|
+
const relative = path2.relative(root, target);
|
|
68
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
|
|
62
69
|
}
|
|
63
70
|
function envValue(environment, name) {
|
|
64
71
|
const key = Object.keys(environment).sort().find((candidate) => process.platform === "win32" ? candidate.toUpperCase() === name.toUpperCase() : candidate === name);
|
|
@@ -66,15 +73,15 @@ function envValue(environment, name) {
|
|
|
66
73
|
}
|
|
67
74
|
function packageEntry(root) {
|
|
68
75
|
const canonicalRoot = realpathSync(root);
|
|
69
|
-
const manifestPath = realpathSync(
|
|
76
|
+
const manifestPath = realpathSync(path2.join(canonicalRoot, "package.json"));
|
|
70
77
|
const manifestStat = lstatSync(manifestPath);
|
|
71
78
|
if (!inside(canonicalRoot, manifestPath) || !manifestStat.isFile() || manifestStat.size > 1024 * 1024)
|
|
72
79
|
throw new Error("manifest-limit");
|
|
73
80
|
const manifest = record(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
74
81
|
const bin = typeof manifest?.bin === "string" ? manifest.bin : record(manifest?.bin)?.openspec;
|
|
75
|
-
if (manifest?.name !== PACKAGE_NAME || typeof bin !== "string" ||
|
|
82
|
+
if (manifest?.name !== PACKAGE_NAME || typeof bin !== "string" || path2.isAbsolute(bin))
|
|
76
83
|
throw new Error("manifest-invalid");
|
|
77
|
-
const entry = realpathSync(
|
|
84
|
+
const entry = realpathSync(path2.resolve(canonicalRoot, bin));
|
|
78
85
|
if (!inside(canonicalRoot, entry) || !/\.(?:cjs|mjs|js)$/iu.test(entry) || !lstatSync(entry).isFile())
|
|
79
86
|
throw new Error("entry-invalid");
|
|
80
87
|
return entry;
|
|
@@ -99,7 +106,7 @@ function commandIn(directory) {
|
|
|
99
106
|
if (matches.length > 1)
|
|
100
107
|
throw new Error("ambiguous-command");
|
|
101
108
|
if (matches.length === 1) {
|
|
102
|
-
const candidate =
|
|
109
|
+
const candidate = path2.join(directory, matches[0]);
|
|
103
110
|
if (lstatSync(candidate, { throwIfNoEntry: false }))
|
|
104
111
|
return candidate;
|
|
105
112
|
}
|
|
@@ -112,7 +119,7 @@ function resolveOpenSpecInvocation(consumerRoot, environment = process.env) {
|
|
|
112
119
|
const node = configuredNode ?? (process.release.name === "node" && !process.versions.bun ? process.execPath : undefined);
|
|
113
120
|
let executable;
|
|
114
121
|
try {
|
|
115
|
-
if (!node || !
|
|
122
|
+
if (!node || !path2.isAbsolute(node) || /\.(?:cmd|bat|ps1)$/iu.test(node)) {
|
|
116
123
|
throw new Error("runtime-invalid");
|
|
117
124
|
}
|
|
118
125
|
executable = realpathSync(node);
|
|
@@ -123,7 +130,7 @@ function resolveOpenSpecInvocation(consumerRoot, environment = process.env) {
|
|
|
123
130
|
}
|
|
124
131
|
if (configuredRoot !== undefined) {
|
|
125
132
|
try {
|
|
126
|
-
if (!
|
|
133
|
+
if (!path2.isAbsolute(configuredRoot))
|
|
127
134
|
throw new Error("root-invalid");
|
|
128
135
|
return { executable, entry: packageEntry(configuredRoot) };
|
|
129
136
|
} catch {
|
|
@@ -131,9 +138,9 @@ function resolveOpenSpecInvocation(consumerRoot, environment = process.env) {
|
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
const consumer = canonicalConsumerRoot(consumerRoot);
|
|
134
|
-
const directories = (envValue(environment, "PATH") ?? "").split(
|
|
141
|
+
const directories = (envValue(environment, "PATH") ?? "").split(path2.delimiter);
|
|
135
142
|
for (const directory of directories) {
|
|
136
|
-
if (!
|
|
143
|
+
if (!path2.isAbsolute(directory))
|
|
137
144
|
continue;
|
|
138
145
|
let command;
|
|
139
146
|
let canonicalDirectory;
|
|
@@ -153,14 +160,14 @@ function resolveOpenSpecInvocation(consumerRoot, environment = process.env) {
|
|
|
153
160
|
throw new OpenSpecCliError("resolve", "resolution", "unsafe-installation");
|
|
154
161
|
}
|
|
155
162
|
const layoutRoots = [
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
path2.join(canonicalDirectory, "node_modules", PACKAGE_NAME),
|
|
164
|
+
path2.resolve(canonicalDirectory, "../lib/node_modules", PACKAGE_NAME)
|
|
158
165
|
];
|
|
159
166
|
const roots = [];
|
|
160
|
-
let ancestor =
|
|
167
|
+
let ancestor = path2.dirname(target);
|
|
161
168
|
for (let depth = 0;depth < 8; depth++) {
|
|
162
169
|
roots.push(ancestor);
|
|
163
|
-
const parent =
|
|
170
|
+
const parent = path2.dirname(ancestor);
|
|
164
171
|
if (parent === ancestor)
|
|
165
172
|
break;
|
|
166
173
|
ancestor = parent;
|
|
@@ -194,7 +201,7 @@ function resolveOpenSpecInvocation(consumerRoot, environment = process.env) {
|
|
|
194
201
|
import { createHash } from "node:crypto";
|
|
195
202
|
import { chmodSync, lstatSync as lstatSync2, mkdirSync, realpathSync as realpathSync2 } from "node:fs";
|
|
196
203
|
import os from "node:os";
|
|
197
|
-
import
|
|
204
|
+
import path3 from "node:path";
|
|
198
205
|
class StateRootError extends Error {
|
|
199
206
|
code;
|
|
200
207
|
constructor(code) {
|
|
@@ -204,15 +211,15 @@ class StateRootError extends Error {
|
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
function isContainedBy(parent, candidate) {
|
|
207
|
-
const relative =
|
|
208
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
214
|
+
const relative = path3.relative(parent, candidate);
|
|
215
|
+
return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
|
|
209
216
|
}
|
|
210
217
|
function inspectExistingComponents(absolutePath) {
|
|
211
|
-
const parsed =
|
|
212
|
-
const relativeParts = absolutePath.slice(parsed.root.length).split(
|
|
218
|
+
const parsed = path3.parse(absolutePath);
|
|
219
|
+
const relativeParts = absolutePath.slice(parsed.root.length).split(path3.sep).filter(Boolean);
|
|
213
220
|
let current = parsed.root;
|
|
214
221
|
for (const part of relativeParts) {
|
|
215
|
-
current =
|
|
222
|
+
current = path3.join(current, part);
|
|
216
223
|
let stat;
|
|
217
224
|
try {
|
|
218
225
|
stat = lstatSync2(current);
|
|
@@ -230,37 +237,37 @@ function inspectExistingComponents(absolutePath) {
|
|
|
230
237
|
}
|
|
231
238
|
}
|
|
232
239
|
function canonicalFuturePath(input) {
|
|
233
|
-
const absolute =
|
|
240
|
+
const absolute = path3.resolve(input);
|
|
234
241
|
inspectExistingComponents(absolute);
|
|
235
242
|
let cursor = absolute;
|
|
236
243
|
const missing = [];
|
|
237
244
|
for (;; ) {
|
|
238
245
|
try {
|
|
239
|
-
return
|
|
246
|
+
return path3.join(realpathSync2.native(cursor), ...missing.reverse());
|
|
240
247
|
} catch (error) {
|
|
241
248
|
if (error.code !== "ENOENT")
|
|
242
249
|
throw error;
|
|
243
|
-
const parent =
|
|
250
|
+
const parent = path3.dirname(cursor);
|
|
244
251
|
if (parent === cursor)
|
|
245
252
|
throw error;
|
|
246
|
-
missing.push(
|
|
253
|
+
missing.push(path3.basename(cursor));
|
|
247
254
|
cursor = parent;
|
|
248
255
|
}
|
|
249
256
|
}
|
|
250
257
|
}
|
|
251
258
|
function requireAbsoluteDirectoryBase(xdgStateHome, homeDir) {
|
|
252
|
-
if (xdgStateHome &&
|
|
259
|
+
if (xdgStateHome && path3.isAbsolute(xdgStateHome)) {
|
|
253
260
|
return canonicalFuturePath(xdgStateHome);
|
|
254
261
|
}
|
|
255
|
-
if (!
|
|
262
|
+
if (!path3.isAbsolute(homeDir)) {
|
|
256
263
|
throw new StateRootError("invalid-state-base");
|
|
257
264
|
}
|
|
258
|
-
return canonicalFuturePath(
|
|
265
|
+
return canonicalFuturePath(path3.join(homeDir, ".local", "state"));
|
|
259
266
|
}
|
|
260
267
|
function resolveStateRoot(options) {
|
|
261
268
|
let consumerRoot;
|
|
262
269
|
try {
|
|
263
|
-
consumerRoot = realpathSync2.native(
|
|
270
|
+
consumerRoot = realpathSync2.native(path3.resolve(options.consumerRoot));
|
|
264
271
|
if (!lstatSync2(consumerRoot).isDirectory()) {
|
|
265
272
|
throw new StateRootError("consumer-root-unavailable");
|
|
266
273
|
}
|
|
@@ -271,9 +278,9 @@ function resolveStateRoot(options) {
|
|
|
271
278
|
}
|
|
272
279
|
const stateBase = requireAbsoluteDirectoryBase(options.xdgStateHome, options.homeDir ?? os.homedir());
|
|
273
280
|
const consumerRootHash = createHash("sha256").update(consumerRoot).digest("hex");
|
|
274
|
-
const productRoot = canonicalFuturePath(
|
|
275
|
-
const rootsDir = canonicalFuturePath(
|
|
276
|
-
const rootDir = canonicalFuturePath(
|
|
281
|
+
const productRoot = canonicalFuturePath(path3.join(stateBase, "abel-cadence"));
|
|
282
|
+
const rootsDir = canonicalFuturePath(path3.join(productRoot, "roots"));
|
|
283
|
+
const rootDir = canonicalFuturePath(path3.join(rootsDir, consumerRootHash));
|
|
277
284
|
if (isContainedBy(consumerRoot, rootDir)) {
|
|
278
285
|
throw new StateRootError("state-root-inside-consumer");
|
|
279
286
|
}
|
|
@@ -284,10 +291,10 @@ function resolveStateRoot(options) {
|
|
|
284
291
|
productRoot,
|
|
285
292
|
rootsDir,
|
|
286
293
|
rootDir,
|
|
287
|
-
databasePath:
|
|
288
|
-
artifactsDir:
|
|
289
|
-
workspacesDir:
|
|
290
|
-
transactionsDir:
|
|
294
|
+
databasePath: path3.join(rootDir, "control.sqlite3"),
|
|
295
|
+
artifactsDir: path3.join(rootDir, "artifacts"),
|
|
296
|
+
workspacesDir: path3.join(rootDir, "workspaces"),
|
|
297
|
+
transactionsDir: path3.join(rootDir, "transactions")
|
|
291
298
|
};
|
|
292
299
|
}
|
|
293
300
|
|
|
@@ -318,7 +325,7 @@ import {
|
|
|
318
325
|
readFileSync as readFileSync3,
|
|
319
326
|
realpathSync as realpathSync3
|
|
320
327
|
} from "node:fs";
|
|
321
|
-
import
|
|
328
|
+
import path5 from "node:path";
|
|
322
329
|
|
|
323
330
|
// src/control-contracts.ts
|
|
324
331
|
var CONTROL_SCHEMA_PROPERTIES = {
|
|
@@ -609,13 +616,13 @@ import { join, resolve } from "node:path";
|
|
|
609
616
|
|
|
610
617
|
// src/safe-path.ts
|
|
611
618
|
import { lstatSync as lstatSync3 } from "node:fs";
|
|
612
|
-
import
|
|
619
|
+
import path4 from "node:path";
|
|
613
620
|
function observeSafePath(root, relative) {
|
|
614
621
|
if (!isValidRelativePath(relative)) {
|
|
615
622
|
return { kind: "unsafe", reason: "invalid-path" };
|
|
616
623
|
}
|
|
617
624
|
try {
|
|
618
|
-
const resolvedRoot =
|
|
625
|
+
const resolvedRoot = path4.resolve(root);
|
|
619
626
|
const rootStat = lstatSync3(resolvedRoot, { throwIfNoEntry: false });
|
|
620
627
|
if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) {
|
|
621
628
|
return { kind: "unsafe", reason: "root-unavailable" };
|
|
@@ -625,8 +632,8 @@ function observeSafePath(root, relative) {
|
|
|
625
632
|
let current = resolvedRoot;
|
|
626
633
|
const parts = relative.split("/");
|
|
627
634
|
for (const [index, part] of parts.entries()) {
|
|
628
|
-
current =
|
|
629
|
-
if (!current.startsWith(`${resolvedRoot}${
|
|
635
|
+
current = path4.join(current, part);
|
|
636
|
+
if (!current.startsWith(`${resolvedRoot}${path4.sep}`)) {
|
|
630
637
|
return { kind: "unsafe", reason: "invalid-path" };
|
|
631
638
|
}
|
|
632
639
|
const stat = lstatSync3(current, { throwIfNoEntry: false });
|
|
@@ -652,9 +659,6 @@ function observeSafePath(root, relative) {
|
|
|
652
659
|
}
|
|
653
660
|
return { kind: "unsafe", reason: "invalid-path" };
|
|
654
661
|
}
|
|
655
|
-
function isSafeRegularFile(root, relative) {
|
|
656
|
-
return observeSafePath(root, relative).kind === "file";
|
|
657
|
-
}
|
|
658
662
|
|
|
659
663
|
// src/file-snapshot.ts
|
|
660
664
|
function sha256(bytes) {
|
|
@@ -697,10 +701,16 @@ function capabilitySuccess(verificationId, runnerBindings = []) {
|
|
|
697
701
|
}))
|
|
698
702
|
};
|
|
699
703
|
}
|
|
700
|
-
function adapterFailure(verificationId, code, message) {
|
|
704
|
+
function adapterFailure(verificationId, code, message, inputObservation) {
|
|
701
705
|
return {
|
|
702
706
|
ok: false,
|
|
703
|
-
diagnostic: {
|
|
707
|
+
diagnostic: {
|
|
708
|
+
kind: "verification-adapter",
|
|
709
|
+
code,
|
|
710
|
+
verificationId,
|
|
711
|
+
message,
|
|
712
|
+
...inputObservation ? { inputObservation } : {}
|
|
713
|
+
}
|
|
704
714
|
};
|
|
705
715
|
}
|
|
706
716
|
function fileStatus(root, relative) {
|
|
@@ -713,11 +723,11 @@ function regularFile(root, relative) {
|
|
|
713
723
|
return fileStatus(root, relative) === "regular";
|
|
714
724
|
}
|
|
715
725
|
function localExecutable(root, executable, dependencyOwner = root, environment = {}) {
|
|
716
|
-
const dependencyRoot =
|
|
726
|
+
const dependencyRoot = path5.resolve(dependencyOwner, "node_modules");
|
|
717
727
|
try {
|
|
718
728
|
const canonicalDependencyRoot = realpathSync3(dependencyRoot);
|
|
719
729
|
for (const name of localExecutableNames(executable, environment)) {
|
|
720
|
-
const unresolved = resolveCandidateName(
|
|
730
|
+
const unresolved = resolveCandidateName(path5.resolve(dependencyRoot, ".bin"), name, environment.platform ?? process.platform);
|
|
721
731
|
if (!unresolved)
|
|
722
732
|
continue;
|
|
723
733
|
const canonical = realpathSync3(unresolved);
|
|
@@ -733,11 +743,11 @@ function localExecutable(root, executable, dependencyOwner = root, environment =
|
|
|
733
743
|
}
|
|
734
744
|
function pathApi(platform, ...values) {
|
|
735
745
|
if (platform !== "win32")
|
|
736
|
-
return
|
|
746
|
+
return path5.posix;
|
|
737
747
|
if (process.platform === "win32" || values.some((value) => /^[a-z]:[\\/]/iu.test(value) || value.startsWith("\\\\"))) {
|
|
738
|
-
return
|
|
748
|
+
return path5.win32;
|
|
739
749
|
}
|
|
740
|
-
return
|
|
750
|
+
return path5.posix;
|
|
741
751
|
}
|
|
742
752
|
function pathWithin(root, candidate, platform) {
|
|
743
753
|
const platformPath = pathApi(platform, root, candidate);
|
|
@@ -767,7 +777,7 @@ function runnerNames(command, environment) {
|
|
|
767
777
|
if ((environment.platform ?? process.platform) !== "win32") {
|
|
768
778
|
return [command];
|
|
769
779
|
}
|
|
770
|
-
const extension =
|
|
780
|
+
const extension = path5.extname(command).toLowerCase();
|
|
771
781
|
if (extension) {
|
|
772
782
|
return WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) ? [command] : [];
|
|
773
783
|
}
|
|
@@ -779,7 +789,7 @@ function localExecutableNames(executable, environment) {
|
|
|
779
789
|
if ((environment.platform ?? process.platform) !== "win32") {
|
|
780
790
|
return [executable];
|
|
781
791
|
}
|
|
782
|
-
const extension =
|
|
792
|
+
const extension = path5.extname(executable).toLowerCase();
|
|
783
793
|
if (extension) {
|
|
784
794
|
return WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) ? [executable] : [];
|
|
785
795
|
}
|
|
@@ -789,18 +799,18 @@ function localExecutableNames(executable, environment) {
|
|
|
789
799
|
];
|
|
790
800
|
}
|
|
791
801
|
function resolveCandidateName(directory, name, platform) {
|
|
792
|
-
const exact =
|
|
802
|
+
const exact = path5.join(directory, name);
|
|
793
803
|
if (platform !== "win32")
|
|
794
804
|
return exact;
|
|
795
805
|
try {
|
|
796
806
|
const matches = readdirSync3(directory).filter((entry) => entry.toLowerCase() === name.toLowerCase());
|
|
797
|
-
return matches.length === 1 ?
|
|
807
|
+
return matches.length === 1 ? path5.join(directory, matches[0]) : null;
|
|
798
808
|
} catch {
|
|
799
809
|
return null;
|
|
800
810
|
}
|
|
801
811
|
}
|
|
802
812
|
function within(root, candidate) {
|
|
803
|
-
return candidate === root || candidate.startsWith(`${root}${
|
|
813
|
+
return candidate === root || candidate.startsWith(`${root}${path5.sep}`);
|
|
804
814
|
}
|
|
805
815
|
function commonDirectory(left, right, platform = process.platform) {
|
|
806
816
|
const platformPath = pathApi(platform, left, right);
|
|
@@ -827,15 +837,15 @@ function commonDirectory(left, right, platform = process.platform) {
|
|
|
827
837
|
function unsafeMountSource(source) {
|
|
828
838
|
if (BROAD_MOUNT_ROOTS.has(source))
|
|
829
839
|
return true;
|
|
830
|
-
if (
|
|
840
|
+
if (path5.resolve(source) === path5.parse(path5.resolve(source)).root)
|
|
831
841
|
return true;
|
|
832
|
-
const parts =
|
|
842
|
+
const parts = path5.relative(path5.parse(path5.resolve(source)).root, path5.resolve(source)).split(path5.sep).filter(Boolean).map((part) => part.toLowerCase());
|
|
833
843
|
return parts[0] === "home" && parts.length <= 2 || parts[0] === "users" && parts.length <= 2 || parts[0] === "root" && parts.length <= 1;
|
|
834
844
|
}
|
|
835
845
|
function safeRunnerSource(paths, platform) {
|
|
836
|
-
let source =
|
|
846
|
+
let source = path5.dirname(paths[0]);
|
|
837
847
|
for (const candidate of paths.slice(1)) {
|
|
838
|
-
source = commonDirectory(source,
|
|
848
|
+
source = commonDirectory(source, path5.dirname(candidate), platform);
|
|
839
849
|
}
|
|
840
850
|
if (unsafeMountSource(source))
|
|
841
851
|
return null;
|
|
@@ -853,18 +863,18 @@ function resolvedRunnerEnvironment(environment) {
|
|
|
853
863
|
platform: environment.platform ?? process.platform,
|
|
854
864
|
path: environment.path ?? process.env.PATH ?? "",
|
|
855
865
|
pathExt: environment.pathExt ?? process.env.PATHEXT ?? DEFAULT_WINDOWS_PATH_EXT,
|
|
856
|
-
pathDelimiter: environment.pathDelimiter ??
|
|
866
|
+
pathDelimiter: environment.pathDelimiter ?? path5.delimiter
|
|
857
867
|
};
|
|
858
868
|
}
|
|
859
869
|
function windowsShimBinding(command, launcherPath, environment) {
|
|
860
|
-
const name =
|
|
870
|
+
const name = path5.basename(command, path5.extname(command)).toLowerCase();
|
|
861
871
|
if (name !== "npm" && name !== "npx")
|
|
862
872
|
return null;
|
|
863
873
|
const node = resolveVerificationRunner("node", environment);
|
|
864
|
-
if (!node || node.fixedArgs ||
|
|
874
|
+
if (!node || node.fixedArgs || path5.extname(node.executablePath).toLowerCase() !== ".exe") {
|
|
865
875
|
return null;
|
|
866
876
|
}
|
|
867
|
-
const cli =
|
|
877
|
+
const cli = path5.join(path5.dirname(launcherPath), "node_modules", "npm", "bin", `${name}-cli.js`);
|
|
868
878
|
try {
|
|
869
879
|
const canonicalLauncher = realpathSync3(launcherPath);
|
|
870
880
|
const canonicalCli = realpathSync3(cli);
|
|
@@ -889,14 +899,14 @@ function localPackageDirectories(dependencyRoot) {
|
|
|
889
899
|
for (const entry of readdirSync3(dependencyRoot, { withFileTypes: true })) {
|
|
890
900
|
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
891
901
|
continue;
|
|
892
|
-
const candidate =
|
|
902
|
+
const candidate = path5.join(dependencyRoot, entry.name);
|
|
893
903
|
if (!entry.name.startsWith("@")) {
|
|
894
904
|
packages.push(candidate);
|
|
895
905
|
continue;
|
|
896
906
|
}
|
|
897
907
|
for (const scoped of readdirSync3(candidate, { withFileTypes: true })) {
|
|
898
908
|
if (scoped.isDirectory() && !scoped.name.startsWith(".")) {
|
|
899
|
-
packages.push(
|
|
909
|
+
packages.push(path5.join(candidate, scoped.name));
|
|
900
910
|
}
|
|
901
911
|
}
|
|
902
912
|
}
|
|
@@ -906,7 +916,7 @@ function localPackageDirectories(dependencyRoot) {
|
|
|
906
916
|
return packages;
|
|
907
917
|
}
|
|
908
918
|
function localPackageBinTargets(dependencyOwner, executable, platform) {
|
|
909
|
-
const dependencyRoot =
|
|
919
|
+
const dependencyRoot = path5.resolve(dependencyOwner, "node_modules");
|
|
910
920
|
let canonicalDependencyRoot;
|
|
911
921
|
try {
|
|
912
922
|
canonicalDependencyRoot = realpathSync3(dependencyRoot);
|
|
@@ -924,7 +934,7 @@ function localPackageBinTargets(dependencyOwner, executable, platform) {
|
|
|
924
934
|
if (!pathWithin(canonicalDependencyRoot, canonicalPackage, platform) || canonicalPackage === canonicalDependencyRoot) {
|
|
925
935
|
continue;
|
|
926
936
|
}
|
|
927
|
-
const manifestPath =
|
|
937
|
+
const manifestPath = path5.join(canonicalPackage, "package.json");
|
|
928
938
|
const manifestStat = lstatSync5(manifestPath);
|
|
929
939
|
if (!manifestStat.isFile() || manifestStat.isSymbolicLink())
|
|
930
940
|
continue;
|
|
@@ -946,7 +956,7 @@ function localPackageBinTargets(dependencyOwner, executable, platform) {
|
|
|
946
956
|
if (typeof relative !== "string" || relative.length === 0 || relative.includes("\x00")) {
|
|
947
957
|
continue;
|
|
948
958
|
}
|
|
949
|
-
const unresolved =
|
|
959
|
+
const unresolved = path5.resolve(canonicalPackage, relative);
|
|
950
960
|
if (!pathWithin(canonicalPackage, unresolved, platform))
|
|
951
961
|
continue;
|
|
952
962
|
const canonical = realpathSync3(unresolved);
|
|
@@ -966,7 +976,7 @@ function resolveLocalVerificationRunner(dependencyOwner, executable, runnerEnvir
|
|
|
966
976
|
if (targets.length !== 1)
|
|
967
977
|
return null;
|
|
968
978
|
const target = targets[0];
|
|
969
|
-
const extension =
|
|
979
|
+
const extension = path5.extname(target).toLowerCase();
|
|
970
980
|
if (extension === ".exe") {
|
|
971
981
|
return { command: executable, executablePath: target };
|
|
972
982
|
}
|
|
@@ -988,7 +998,7 @@ function resolveVerificationRunner(command, runnerEnvironment = {}) {
|
|
|
988
998
|
const candidates = [
|
|
989
999
|
...command === "bun" && process.versions.bun && environment.platform === process.platform ? [process.execPath] : [],
|
|
990
1000
|
...directories.flatMap((directory) => {
|
|
991
|
-
const resolvedDirectory =
|
|
1001
|
+
const resolvedDirectory = path5.resolve(directory);
|
|
992
1002
|
const key = environment.platform === "win32" ? resolvedDirectory.toLowerCase() : resolvedDirectory;
|
|
993
1003
|
if (seenDirectories.has(key))
|
|
994
1004
|
return [];
|
|
@@ -1002,7 +1012,7 @@ function resolveVerificationRunner(command, runnerEnvironment = {}) {
|
|
|
1002
1012
|
const seenCandidates = new Set;
|
|
1003
1013
|
for (const candidate of candidates) {
|
|
1004
1014
|
try {
|
|
1005
|
-
const executablePath =
|
|
1015
|
+
const executablePath = path5.resolve(candidate);
|
|
1006
1016
|
const candidateKey = environment.platform === "win32" ? executablePath.toLowerCase() : executablePath;
|
|
1007
1017
|
if (seenCandidates.has(candidateKey))
|
|
1008
1018
|
continue;
|
|
@@ -1012,7 +1022,7 @@ function resolveVerificationRunner(command, runnerEnvironment = {}) {
|
|
|
1012
1022
|
if (!regularExecutableFile(stat, environment.platform))
|
|
1013
1023
|
continue;
|
|
1014
1024
|
if (environment.platform === "win32") {
|
|
1015
|
-
const extension =
|
|
1025
|
+
const extension = path5.extname(executablePath).toLowerCase();
|
|
1016
1026
|
if (extension === ".cmd") {
|
|
1017
1027
|
const shim = windowsShimBinding(command, executablePath, environment);
|
|
1018
1028
|
if (shim)
|
|
@@ -1029,7 +1039,7 @@ function resolveVerificationRunner(command, runnerEnvironment = {}) {
|
|
|
1029
1039
|
const systemVisible = SYSTEM_MOUNT_ROOTS.some((root) => within(root, executablePath) && within(root, canonical));
|
|
1030
1040
|
if (systemVisible)
|
|
1031
1041
|
return { command, executablePath };
|
|
1032
|
-
const mountSource = commonDirectory(
|
|
1042
|
+
const mountSource = commonDirectory(path5.dirname(executablePath), path5.dirname(canonical), environment.platform);
|
|
1033
1043
|
if (unsafeMountSource(mountSource))
|
|
1034
1044
|
continue;
|
|
1035
1045
|
const sourceStat = lstatSync5(mountSource);
|
|
@@ -1044,7 +1054,7 @@ function packageManifest(root) {
|
|
|
1044
1054
|
if (!regularFile(root, "package.json"))
|
|
1045
1055
|
return null;
|
|
1046
1056
|
try {
|
|
1047
|
-
const value = JSON.parse(readFileSync3(
|
|
1057
|
+
const value = JSON.parse(readFileSync3(path5.join(root, "package.json"), "utf8"));
|
|
1048
1058
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1049
1059
|
} catch {
|
|
1050
1060
|
return null;
|
|
@@ -1166,7 +1176,7 @@ function validateAtomicCapability(root, step, dependencyOwner, runnerEnvironment
|
|
|
1166
1176
|
"pnpm-workspace.yaml"
|
|
1167
1177
|
].includes(relative))
|
|
1168
1178
|
continue;
|
|
1169
|
-
if (lstatSync5(
|
|
1179
|
+
if (lstatSync5(path5.join(root, relative)).size > 64 * 1024 || executionProfile().mode === "isolated" && unsupportedExecutionConfiguration(readFileSync3(path5.join(root, relative), "utf8"))) {
|
|
1170
1180
|
return adapterFailure(step.id, "verification-config-unsafe", "Execution configuration requires unsupported credentials or host configuration");
|
|
1171
1181
|
}
|
|
1172
1182
|
}
|
|
@@ -1223,8 +1233,10 @@ function validateCapability(root, value, options, requireInputAvailability) {
|
|
|
1223
1233
|
function validateAcceptedVerificationCapability(root, verification, dependencyOwner, runnerEnvironment, requireInputAvailability, executionWritePaths) {
|
|
1224
1234
|
if (requireInputAvailability) {
|
|
1225
1235
|
for (const input of verificationInputPaths(verification)) {
|
|
1226
|
-
|
|
1227
|
-
|
|
1236
|
+
const status = fileStatus(root, input);
|
|
1237
|
+
if (status !== "regular") {
|
|
1238
|
+
const inputObservation = { path: input, kind: status };
|
|
1239
|
+
return adapterFailure(verification.id, status === "absent" ? "input-missing" : "input-unsafe", `verification input ${input} is ${status}`, inputObservation);
|
|
1228
1240
|
}
|
|
1229
1241
|
}
|
|
1230
1242
|
}
|
|
@@ -1238,6 +1250,549 @@ function validateAcceptedVerificationCapability(root, verification, dependencyOw
|
|
|
1238
1250
|
return capabilitySuccess(verification.id, runnerBindings);
|
|
1239
1251
|
}
|
|
1240
1252
|
|
|
1253
|
+
// src/windows-job-backend.ts
|
|
1254
|
+
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
1255
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1256
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
1257
|
+
import path7 from "node:path";
|
|
1258
|
+
|
|
1259
|
+
// src/isolation-backend.ts
|
|
1260
|
+
import { spawn } from "node:child_process";
|
|
1261
|
+
import { accessSync, constants, lstatSync as lstatSync6 } from "node:fs";
|
|
1262
|
+
import path6 from "node:path";
|
|
1263
|
+
var ISOLATION_RUN_LIMITS = Object.freeze({
|
|
1264
|
+
timeoutMs: 10 * 60000,
|
|
1265
|
+
maxOutputBytes: 256 * 1024,
|
|
1266
|
+
terminateGraceMs: 1000
|
|
1267
|
+
});
|
|
1268
|
+
function defaultProbe(executable) {
|
|
1269
|
+
try {
|
|
1270
|
+
accessSync(executable, constants.X_OK);
|
|
1271
|
+
return true;
|
|
1272
|
+
} catch {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
function validEnvironmentName(name) {
|
|
1277
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(name);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
class BoundedLog {
|
|
1281
|
+
bytes = 0;
|
|
1282
|
+
#head = Buffer.alloc(0);
|
|
1283
|
+
#tail = Buffer.alloc(0);
|
|
1284
|
+
limit;
|
|
1285
|
+
#witness;
|
|
1286
|
+
#witnessTail = Buffer.alloc(0);
|
|
1287
|
+
witnessMatched = false;
|
|
1288
|
+
constructor(limit, witness) {
|
|
1289
|
+
this.limit = limit;
|
|
1290
|
+
if (witness)
|
|
1291
|
+
this.#witness = Buffer.from(witness);
|
|
1292
|
+
}
|
|
1293
|
+
append(chunk) {
|
|
1294
|
+
const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1295
|
+
this.bytes += input.byteLength;
|
|
1296
|
+
if (this.#witness && !this.witnessMatched) {
|
|
1297
|
+
const searchable = Buffer.concat([this.#witnessTail, input]);
|
|
1298
|
+
this.witnessMatched = searchable.includes(this.#witness);
|
|
1299
|
+
this.#witnessTail = this.witnessMatched ? Buffer.alloc(0) : Buffer.from(searchable.subarray(Math.max(0, searchable.length - this.#witness.length + 1)));
|
|
1300
|
+
}
|
|
1301
|
+
const headLimit = Math.ceil(this.limit / 2);
|
|
1302
|
+
const take = Math.min(input.length, headLimit - this.#head.length);
|
|
1303
|
+
if (take > 0)
|
|
1304
|
+
this.#head = Buffer.concat([this.#head, input.subarray(0, take)]);
|
|
1305
|
+
const rest = input.subarray(take);
|
|
1306
|
+
const tailLimit = this.limit - headLimit;
|
|
1307
|
+
if (rest.length && tailLimit) {
|
|
1308
|
+
this.#tail = rest.length >= tailLimit ? Buffer.from(rest.subarray(-tailLimit)) : Buffer.concat([
|
|
1309
|
+
this.#tail.subarray(Math.max(0, this.#tail.length + rest.length - tailLimit)),
|
|
1310
|
+
rest
|
|
1311
|
+
]);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
text() {
|
|
1315
|
+
return Buffer.concat([this.#head, this.#tail]).toString("utf8");
|
|
1316
|
+
}
|
|
1317
|
+
metadata() {
|
|
1318
|
+
return { bytes: this.bytes, truncated: this.bytes > this.limit };
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
class BubblewrapIsolationBackend {
|
|
1323
|
+
#localTrusted;
|
|
1324
|
+
#bwrapPath;
|
|
1325
|
+
#probe;
|
|
1326
|
+
#spawn;
|
|
1327
|
+
#timeoutMs;
|
|
1328
|
+
#maxOutputBytes;
|
|
1329
|
+
#terminateGraceMs;
|
|
1330
|
+
constructor(options = {}) {
|
|
1331
|
+
this.#localTrusted = options.localTrusted === true;
|
|
1332
|
+
this.#bwrapPath = options.bwrapPath ?? "/usr/bin/bwrap";
|
|
1333
|
+
this.#probe = options.probe ?? defaultProbe;
|
|
1334
|
+
this.#spawn = options.spawnProcess ?? spawn;
|
|
1335
|
+
this.#timeoutMs = options.timeoutMs ?? ISOLATION_RUN_LIMITS.timeoutMs;
|
|
1336
|
+
this.#maxOutputBytes = options.maxOutputBytes ?? ISOLATION_RUN_LIMITS.maxOutputBytes;
|
|
1337
|
+
this.#terminateGraceMs = options.terminateGraceMs ?? ISOLATION_RUN_LIMITS.terminateGraceMs;
|
|
1338
|
+
if (!Number.isSafeInteger(this.#timeoutMs) || this.#timeoutMs < 1 || !Number.isSafeInteger(this.#maxOutputBytes) || this.#maxOutputBytes < 1 || !Number.isSafeInteger(this.#terminateGraceMs) || this.#terminateGraceMs < 1) {
|
|
1339
|
+
throw new Error("isolation-run-limits-invalid");
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async available() {
|
|
1343
|
+
try {
|
|
1344
|
+
if (this.#localTrusted && process.platform !== "linux")
|
|
1345
|
+
return false;
|
|
1346
|
+
return Boolean(await this.#probe(this.#bwrapPath));
|
|
1347
|
+
} catch {
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
async run(input) {
|
|
1352
|
+
if (input.outputWitness !== undefined && (typeof input.outputWitness !== "string" || input.outputWitness.length < 1 || input.outputWitness.length > 512))
|
|
1353
|
+
throw new Error("isolation-output-witness-invalid");
|
|
1354
|
+
if (!await this.available()) {
|
|
1355
|
+
return {
|
|
1356
|
+
ok: false,
|
|
1357
|
+
state: "paused",
|
|
1358
|
+
code: "isolation-backend-unavailable"
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
if (input.signal?.aborted) {
|
|
1362
|
+
return { ok: false, state: "cancelled", code: "cancelled" };
|
|
1363
|
+
}
|
|
1364
|
+
const root = path6.resolve(input.root);
|
|
1365
|
+
const rootStat = lstatSync6(root, { throwIfNoEntry: false });
|
|
1366
|
+
if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) {
|
|
1367
|
+
throw new Error("isolation-root-invalid");
|
|
1368
|
+
}
|
|
1369
|
+
const argv = this.#localTrusted ? [
|
|
1370
|
+
"--die-with-parent",
|
|
1371
|
+
"--new-session",
|
|
1372
|
+
"--unshare-pid",
|
|
1373
|
+
"--bind",
|
|
1374
|
+
"/",
|
|
1375
|
+
"/",
|
|
1376
|
+
"--proc",
|
|
1377
|
+
"/proc",
|
|
1378
|
+
"--dev",
|
|
1379
|
+
"/dev",
|
|
1380
|
+
"--chdir",
|
|
1381
|
+
root,
|
|
1382
|
+
"--clearenv"
|
|
1383
|
+
] : [
|
|
1384
|
+
"--die-with-parent",
|
|
1385
|
+
"--new-session",
|
|
1386
|
+
"--unshare-all",
|
|
1387
|
+
"--proc",
|
|
1388
|
+
"/proc",
|
|
1389
|
+
"--dev",
|
|
1390
|
+
"/dev",
|
|
1391
|
+
"--tmpfs",
|
|
1392
|
+
"/tmp",
|
|
1393
|
+
"--bind",
|
|
1394
|
+
root,
|
|
1395
|
+
"/workspace",
|
|
1396
|
+
"--chdir",
|
|
1397
|
+
"/workspace",
|
|
1398
|
+
"--clearenv",
|
|
1399
|
+
"--setenv",
|
|
1400
|
+
"PATH",
|
|
1401
|
+
"/usr/local/bin:/usr/bin:/bin"
|
|
1402
|
+
];
|
|
1403
|
+
if (!this.#localTrusted) {
|
|
1404
|
+
for (const systemRoot of ["/usr", "/bin", "/lib", "/lib64"]) {
|
|
1405
|
+
if (lstatSync6(systemRoot, { throwIfNoEntry: false }))
|
|
1406
|
+
argv.push("--ro-bind", systemRoot, systemRoot);
|
|
1407
|
+
}
|
|
1408
|
+
for (const mount of input.mounts ?? []) {
|
|
1409
|
+
if (!path6.isAbsolute(mount.source) || !path6.isAbsolute(mount.target))
|
|
1410
|
+
throw new Error("isolation-mount-invalid");
|
|
1411
|
+
argv.push(mount.writable ? "--bind" : "--ro-bind", mount.source, mount.target);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
for (const [name, value] of Object.entries(input.environment ?? {}).sort()) {
|
|
1415
|
+
if (!validEnvironmentName(name) || value.includes("\x00"))
|
|
1416
|
+
throw new Error("isolation-environment-invalid");
|
|
1417
|
+
argv.push("--setenv", name, value);
|
|
1418
|
+
}
|
|
1419
|
+
argv.push("--", input.executable, ...input.args ?? []);
|
|
1420
|
+
return new Promise((resolve2) => {
|
|
1421
|
+
let settled = false;
|
|
1422
|
+
const stdout = new BoundedLog(this.#maxOutputBytes, input.outputWitness);
|
|
1423
|
+
const stderr = new BoundedLog(this.#maxOutputBytes, input.outputWitness);
|
|
1424
|
+
let child;
|
|
1425
|
+
let executionTimer;
|
|
1426
|
+
let killTimer;
|
|
1427
|
+
let terminationResult;
|
|
1428
|
+
const settle = (result) => {
|
|
1429
|
+
if (settled)
|
|
1430
|
+
return;
|
|
1431
|
+
settled = true;
|
|
1432
|
+
if (executionTimer)
|
|
1433
|
+
clearTimeout(executionTimer);
|
|
1434
|
+
input.signal?.removeEventListener("abort", abort);
|
|
1435
|
+
resolve2(result);
|
|
1436
|
+
};
|
|
1437
|
+
const kill = (signal) => {
|
|
1438
|
+
child?.kill(signal);
|
|
1439
|
+
};
|
|
1440
|
+
const terminate = (result) => {
|
|
1441
|
+
if (settled || terminationResult)
|
|
1442
|
+
return;
|
|
1443
|
+
terminationResult = result;
|
|
1444
|
+
if (executionTimer)
|
|
1445
|
+
clearTimeout(executionTimer);
|
|
1446
|
+
killTimer = setTimeout(() => kill("SIGKILL"), this.#terminateGraceMs);
|
|
1447
|
+
killTimer.unref?.();
|
|
1448
|
+
kill("SIGTERM");
|
|
1449
|
+
};
|
|
1450
|
+
const abort = () => {
|
|
1451
|
+
terminate({ ok: false, state: "cancelled", code: "cancelled" });
|
|
1452
|
+
};
|
|
1453
|
+
try {
|
|
1454
|
+
child = this.#spawn(this.#bwrapPath, argv, {
|
|
1455
|
+
cwd: root,
|
|
1456
|
+
shell: false,
|
|
1457
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1458
|
+
});
|
|
1459
|
+
} catch {
|
|
1460
|
+
settle({
|
|
1461
|
+
ok: false,
|
|
1462
|
+
state: "paused",
|
|
1463
|
+
code: "isolation-backend-launch-failed"
|
|
1464
|
+
});
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const capture = (destination, chunk) => {
|
|
1468
|
+
if (!settled && !terminationResult)
|
|
1469
|
+
destination.append(chunk);
|
|
1470
|
+
};
|
|
1471
|
+
child.stdout?.on("data", (chunk) => {
|
|
1472
|
+
capture(stdout, chunk);
|
|
1473
|
+
});
|
|
1474
|
+
child.stderr?.on("data", (chunk) => {
|
|
1475
|
+
capture(stderr, chunk);
|
|
1476
|
+
});
|
|
1477
|
+
child.once("error", () => {
|
|
1478
|
+
if (terminationResult)
|
|
1479
|
+
return;
|
|
1480
|
+
settle({
|
|
1481
|
+
ok: false,
|
|
1482
|
+
state: "paused",
|
|
1483
|
+
code: "isolation-backend-launch-failed"
|
|
1484
|
+
});
|
|
1485
|
+
});
|
|
1486
|
+
child.once("close", (code) => {
|
|
1487
|
+
if (killTimer)
|
|
1488
|
+
clearTimeout(killTimer);
|
|
1489
|
+
settle(terminationResult ?? {
|
|
1490
|
+
ok: true,
|
|
1491
|
+
state: "completed",
|
|
1492
|
+
exitCode: code ?? 1,
|
|
1493
|
+
stdout: stdout.text(),
|
|
1494
|
+
stderr: stderr.text(),
|
|
1495
|
+
logs: { stdout: stdout.metadata(), stderr: stderr.metadata() },
|
|
1496
|
+
...input.outputWitness ? {
|
|
1497
|
+
outputWitnessMatched: stdout.witnessMatched || stderr.witnessMatched
|
|
1498
|
+
} : {}
|
|
1499
|
+
});
|
|
1500
|
+
});
|
|
1501
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
1502
|
+
if (input.signal?.aborted) {
|
|
1503
|
+
abort();
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
executionTimer = setTimeout(() => terminate({
|
|
1507
|
+
ok: false,
|
|
1508
|
+
state: "paused",
|
|
1509
|
+
code: "isolation-execution-timeout"
|
|
1510
|
+
}), this.#timeoutMs);
|
|
1511
|
+
executionTimer.unref?.();
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/windows-job-backend.ts
|
|
1517
|
+
var SHUTDOWN_MS = 5000;
|
|
1518
|
+
var invalid = () => {
|
|
1519
|
+
throw new Error("windows-job-request-invalid");
|
|
1520
|
+
};
|
|
1521
|
+
var text = (value) => typeof value === "string" && value.length <= 16384 && !value.includes("\x00") && !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value);
|
|
1522
|
+
var absolute = (value) => text(value) && /^[A-Za-z]:\\/u.test(value) && path7.win32.isAbsolute(value);
|
|
1523
|
+
function encodeWindowsJobRequest(input, timeoutMs) {
|
|
1524
|
+
if (!absolute(input.root) || !absolute(input.executable) || !input.executable.toLowerCase().endsWith(".exe") || input.mounts?.length || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86400000)
|
|
1525
|
+
invalid();
|
|
1526
|
+
const args = input.args ?? [];
|
|
1527
|
+
if (args.length > 128 || !args.every(text))
|
|
1528
|
+
invalid();
|
|
1529
|
+
const seen = new Set;
|
|
1530
|
+
const env = Object.entries(input.environment ?? {});
|
|
1531
|
+
for (const [name, value] of env) {
|
|
1532
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name) || !text(value) || seen.has(name.toUpperCase()))
|
|
1533
|
+
invalid();
|
|
1534
|
+
seen.add(name.toUpperCase());
|
|
1535
|
+
}
|
|
1536
|
+
const quote = (value) => `"${value.replace(/(\\*)"/gu, "$1$1\\\"").replace(/(\\+)$/gu, "$1$1")}"`;
|
|
1537
|
+
const command = [input.executable, ...args].map(quote).join(" ");
|
|
1538
|
+
if (command.length >= 32767)
|
|
1539
|
+
invalid();
|
|
1540
|
+
const environment = `${env.sort(([a], [b]) => a.toUpperCase() < b.toUpperCase() ? -1 : 1).map(([k, v]) => `${k}=${v}`).join("\x00")}\x00\x00`;
|
|
1541
|
+
const parts = [
|
|
1542
|
+
`${input.executable}\x00`,
|
|
1543
|
+
`${command}\x00`,
|
|
1544
|
+
`${input.root}\x00`,
|
|
1545
|
+
environment
|
|
1546
|
+
].map((s) => Buffer.from(s, "utf16le"));
|
|
1547
|
+
const header = Buffer.alloc(36);
|
|
1548
|
+
[2, timeoutMs, SHUTDOWN_MS, 0, ...parts.map((p) => p.length), 0].forEach((n, i) => {
|
|
1549
|
+
header.writeUInt32LE(n, i * 4);
|
|
1550
|
+
});
|
|
1551
|
+
const wire = Buffer.concat([header, ...parts]);
|
|
1552
|
+
if (wire.length > 65536 || parts.some((p) => p.length > 60000))
|
|
1553
|
+
invalid();
|
|
1554
|
+
return wire;
|
|
1555
|
+
}
|
|
1556
|
+
function verifyWindowsJobHelper(helper) {
|
|
1557
|
+
try {
|
|
1558
|
+
if (!path7.isAbsolute(helper))
|
|
1559
|
+
return false;
|
|
1560
|
+
const files = [helper, `${helper}.json`];
|
|
1561
|
+
if (files.some((file) => {
|
|
1562
|
+
const s = lstatSync7(file);
|
|
1563
|
+
return !s.isFile() || s.isSymbolicLink() || s.size > 8 * 1024 * 1024;
|
|
1564
|
+
}))
|
|
1565
|
+
return false;
|
|
1566
|
+
const bytes = readFileSync4(helper);
|
|
1567
|
+
const pe = bytes.length >= 64 ? bytes.readUInt32LE(60) : -1;
|
|
1568
|
+
if (bytes.toString("ascii", 0, 2) !== "MZ" || pe < 64 || pe + 6 > bytes.length || bytes.readUInt32LE(pe) !== 17744 || bytes.readUInt16LE(pe + 4) !== 34404)
|
|
1569
|
+
return false;
|
|
1570
|
+
const hash = (data) => createHash3("sha256").update(data).digest("hex");
|
|
1571
|
+
const manifest = JSON.parse(readFileSync4(`${helper}.json`, "utf8"));
|
|
1572
|
+
return manifest.version === 2 && manifest.arch === "x64" && manifest.helperSha256 === hash(bytes) && manifest.sourceSha256 === hash(readFileSync4(new URL("./windows-job.c", import.meta.url)));
|
|
1573
|
+
} catch {
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function observation(value) {
|
|
1578
|
+
if (!value || typeof value !== "object")
|
|
1579
|
+
return;
|
|
1580
|
+
const v = value;
|
|
1581
|
+
if (Object.keys(v).sort().join(",") !== "exitCode,managedSettled,outcome,reason,rootExited,version" || v.version !== 2 || !["complete", "failed", "uncertain"].includes(v.outcome) || ![
|
|
1582
|
+
"exit",
|
|
1583
|
+
"timeout",
|
|
1584
|
+
"cancelled",
|
|
1585
|
+
"launch-failed",
|
|
1586
|
+
"termination-unconfirmed"
|
|
1587
|
+
].includes(v.reason) || typeof v.rootExited !== "boolean" || typeof v.managedSettled !== "boolean" || !(v.exitCode === null || Number.isInteger(v.exitCode) && v.exitCode >= 0 && v.exitCode <= 4294967295))
|
|
1588
|
+
return;
|
|
1589
|
+
if (v.outcome === "complete" && (v.reason !== "exit" || !v.rootExited || !v.managedSettled || v.exitCode === null))
|
|
1590
|
+
return;
|
|
1591
|
+
if (v.outcome === "failed" && (!v.managedSettled || !["timeout", "cancelled", "launch-failed"].includes(v.reason)))
|
|
1592
|
+
return;
|
|
1593
|
+
return v;
|
|
1594
|
+
}
|
|
1595
|
+
var uncertain = () => ({
|
|
1596
|
+
ok: false,
|
|
1597
|
+
state: "paused",
|
|
1598
|
+
code: "isolation-termination-unconfirmed"
|
|
1599
|
+
});
|
|
1600
|
+
function classify(v, helperExit, output, cancelled) {
|
|
1601
|
+
if (!v?.managedSettled || v.outcome === "uncertain" || helperExit !== 0)
|
|
1602
|
+
return uncertain();
|
|
1603
|
+
if (cancelled || v.reason === "cancelled")
|
|
1604
|
+
return { ok: false, state: "cancelled", code: "cancelled" };
|
|
1605
|
+
if (v.reason === "timeout")
|
|
1606
|
+
return { ok: false, state: "paused", code: "isolation-execution-timeout" };
|
|
1607
|
+
if (v.reason === "launch-failed")
|
|
1608
|
+
return {
|
|
1609
|
+
ok: false,
|
|
1610
|
+
state: "paused",
|
|
1611
|
+
code: "isolation-backend-launch-failed"
|
|
1612
|
+
};
|
|
1613
|
+
if (v.outcome !== "complete" || v.exitCode === null)
|
|
1614
|
+
return uncertain();
|
|
1615
|
+
return {
|
|
1616
|
+
ok: true,
|
|
1617
|
+
state: "completed",
|
|
1618
|
+
exitCode: v.exitCode,
|
|
1619
|
+
stdout: output.text(),
|
|
1620
|
+
stderr: "",
|
|
1621
|
+
outputWitnessMatched: output.witnessMatched,
|
|
1622
|
+
logs: { stdout: output.metadata(), stderr: { bytes: 0, truncated: false } }
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
class WindowsJobBackend {
|
|
1627
|
+
#options;
|
|
1628
|
+
constructor(options) {
|
|
1629
|
+
for (const [value, maximum] of [
|
|
1630
|
+
[options.timeoutMs ?? ISOLATION_RUN_LIMITS.timeoutMs, 86400000],
|
|
1631
|
+
[
|
|
1632
|
+
options.maxOutputBytes ?? ISOLATION_RUN_LIMITS.maxOutputBytes,
|
|
1633
|
+
1024 * 1024
|
|
1634
|
+
]
|
|
1635
|
+
])
|
|
1636
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
1637
|
+
throw new Error("isolation-run-limits-invalid");
|
|
1638
|
+
this.#options = options;
|
|
1639
|
+
}
|
|
1640
|
+
available() {
|
|
1641
|
+
return (this.#options.platform ?? process.platform) === "win32" && (this.#options.arch ?? process.arch) === "x64" && (this.#options.probe ?? verifyWindowsJobHelper)(this.#options.helperPath);
|
|
1642
|
+
}
|
|
1643
|
+
async run(input) {
|
|
1644
|
+
if (!this.available())
|
|
1645
|
+
return {
|
|
1646
|
+
ok: false,
|
|
1647
|
+
state: "paused",
|
|
1648
|
+
code: "isolation-backend-unavailable"
|
|
1649
|
+
};
|
|
1650
|
+
if (input.signal?.aborted)
|
|
1651
|
+
return { ok: false, state: "cancelled", code: "cancelled" };
|
|
1652
|
+
if (input.outputWitness !== undefined && (!text(input.outputWitness) || input.outputWitness.length < 1 || input.outputWitness.length > 512))
|
|
1653
|
+
return {
|
|
1654
|
+
ok: false,
|
|
1655
|
+
state: "paused",
|
|
1656
|
+
code: "isolation-backend-launch-failed"
|
|
1657
|
+
};
|
|
1658
|
+
const timeoutMs = this.#options.timeoutMs ?? ISOLATION_RUN_LIMITS.timeoutMs;
|
|
1659
|
+
let wire;
|
|
1660
|
+
try {
|
|
1661
|
+
wire = encodeWindowsJobRequest(input, timeoutMs);
|
|
1662
|
+
} catch {
|
|
1663
|
+
return {
|
|
1664
|
+
ok: false,
|
|
1665
|
+
state: "paused",
|
|
1666
|
+
code: "isolation-backend-launch-failed"
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
const output = new BoundedLog(this.#options.maxOutputBytes ?? ISOLATION_RUN_LIMITS.maxOutputBytes, input.outputWitness);
|
|
1670
|
+
return new Promise((resolve2) => {
|
|
1671
|
+
let child;
|
|
1672
|
+
let line = "", final, bad = false, closed = false, launchFailed = false;
|
|
1673
|
+
let timer;
|
|
1674
|
+
let stopping = false;
|
|
1675
|
+
const armDeadline = (duration) => {
|
|
1676
|
+
clearTimeout(timer);
|
|
1677
|
+
timer = setTimeout(() => {
|
|
1678
|
+
bad = true;
|
|
1679
|
+
child.kill();
|
|
1680
|
+
finish(uncertain());
|
|
1681
|
+
}, duration);
|
|
1682
|
+
timer.unref?.();
|
|
1683
|
+
};
|
|
1684
|
+
const cancel = () => {
|
|
1685
|
+
if (stopping || closed)
|
|
1686
|
+
return;
|
|
1687
|
+
stopping = true;
|
|
1688
|
+
child.stdin?.end();
|
|
1689
|
+
armDeadline(SHUTDOWN_MS + 5000);
|
|
1690
|
+
};
|
|
1691
|
+
const finish = (result) => {
|
|
1692
|
+
if (closed)
|
|
1693
|
+
return;
|
|
1694
|
+
closed = true;
|
|
1695
|
+
clearTimeout(timer);
|
|
1696
|
+
input.signal?.removeEventListener("abort", cancel);
|
|
1697
|
+
resolve2(result);
|
|
1698
|
+
};
|
|
1699
|
+
try {
|
|
1700
|
+
child = (this.#options.spawnProcess ?? spawn2)(this.#options.helperPath, [], {
|
|
1701
|
+
cwd: input.root,
|
|
1702
|
+
shell: false,
|
|
1703
|
+
windowsHide: true,
|
|
1704
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1705
|
+
});
|
|
1706
|
+
} catch {
|
|
1707
|
+
finish({
|
|
1708
|
+
ok: false,
|
|
1709
|
+
state: "paused",
|
|
1710
|
+
code: "isolation-backend-launch-failed"
|
|
1711
|
+
});
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
const malformed = () => {
|
|
1715
|
+
bad = true;
|
|
1716
|
+
cancel();
|
|
1717
|
+
};
|
|
1718
|
+
child.stdout?.on("data", (chunk) => {
|
|
1719
|
+
if (closed || bad)
|
|
1720
|
+
return;
|
|
1721
|
+
line += chunk.toString("utf8");
|
|
1722
|
+
while (line.includes(`
|
|
1723
|
+
`)) {
|
|
1724
|
+
const end = line.indexOf(`
|
|
1725
|
+
`);
|
|
1726
|
+
const row = line.slice(0, end);
|
|
1727
|
+
line = line.slice(end + 1);
|
|
1728
|
+
if (row.length > 4096 || final) {
|
|
1729
|
+
malformed();
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
try {
|
|
1733
|
+
const value = JSON.parse(row);
|
|
1734
|
+
if (Object.keys(value).length === 1 && typeof value.outputHex === "string" && /^(?:[a-f0-9]{2}){1,1024}$/u.test(value.outputHex))
|
|
1735
|
+
output.append(Buffer.from(value.outputHex, "hex"));
|
|
1736
|
+
else {
|
|
1737
|
+
final = observation(value);
|
|
1738
|
+
if (!final) {
|
|
1739
|
+
malformed();
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
} catch {
|
|
1744
|
+
malformed();
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (line.length > 4096)
|
|
1749
|
+
malformed();
|
|
1750
|
+
});
|
|
1751
|
+
child.stderr?.on("data", () => {});
|
|
1752
|
+
child.stdin?.on("error", () => {
|
|
1753
|
+
bad = true;
|
|
1754
|
+
});
|
|
1755
|
+
child.once("error", () => {
|
|
1756
|
+
bad = true;
|
|
1757
|
+
launchFailed = child.pid === undefined;
|
|
1758
|
+
});
|
|
1759
|
+
child.once("close", (code) => finish(launchFailed ? {
|
|
1760
|
+
ok: false,
|
|
1761
|
+
state: "paused",
|
|
1762
|
+
code: "isolation-backend-launch-failed"
|
|
1763
|
+
} : bad || line.length ? uncertain() : classify(final, code, output, input.signal?.aborted === true)));
|
|
1764
|
+
armDeadline(timeoutMs + SHUTDOWN_MS + 1e4);
|
|
1765
|
+
input.signal?.addEventListener("abort", cancel, { once: true });
|
|
1766
|
+
child.stdin?.write(wire);
|
|
1767
|
+
if (input.signal?.aborted)
|
|
1768
|
+
cancel();
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function probeWindowsJob(helper) {
|
|
1773
|
+
if (process.platform !== "win32" || process.arch !== "x64" || !verifyWindowsJobHelper(helper))
|
|
1774
|
+
return false;
|
|
1775
|
+
const wire = encodeWindowsJobRequest({
|
|
1776
|
+
root: process.cwd(),
|
|
1777
|
+
executable: process.execPath,
|
|
1778
|
+
args: ["-e", "process.exit(0)"],
|
|
1779
|
+
environment: { SystemRoot: process.env.SystemRoot ?? "C:\\Windows" }
|
|
1780
|
+
}, 5000);
|
|
1781
|
+
const result = spawnSync(helper, ["--probe"], {
|
|
1782
|
+
input: wire,
|
|
1783
|
+
shell: false,
|
|
1784
|
+
encoding: "utf8",
|
|
1785
|
+
timeout: 15000,
|
|
1786
|
+
maxBuffer: 65536
|
|
1787
|
+
});
|
|
1788
|
+
try {
|
|
1789
|
+
const v = observation(JSON.parse(result.stdout.trim()));
|
|
1790
|
+
return !result.error && result.status === 0 && v?.outcome === "complete" && v.exitCode === 0;
|
|
1791
|
+
} catch {
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1241
1796
|
// src/operator-tools.ts
|
|
1242
1797
|
var PACKAGE_LOCKS = {
|
|
1243
1798
|
bun: ["bun.lock", "bun.lockb"],
|
|
@@ -1247,7 +1802,7 @@ var PACKAGE_LOCKS = {
|
|
|
1247
1802
|
};
|
|
1248
1803
|
function packageManagerSelection(root, declaration) {
|
|
1249
1804
|
const managers = Object.keys(PACKAGE_LOCKS);
|
|
1250
|
-
const lockfiles = managers.flatMap((manager) => PACKAGE_LOCKS[manager].filter((file) =>
|
|
1805
|
+
const lockfiles = managers.flatMap((manager) => PACKAGE_LOCKS[manager].filter((file) => lstatSync8(path8.join(root, file), { throwIfNoEntry: false })?.isFile()));
|
|
1251
1806
|
const inferred = managers.filter((manager) => PACKAGE_LOCKS[manager].some((file) => lockfiles.includes(file)));
|
|
1252
1807
|
const declared = typeof declaration === "string" ? managers.find((manager) => new RegExp(`^${manager}@[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][a-z0-9._+-]+)?$`, "iu").test(declaration)) : undefined;
|
|
1253
1808
|
const selectedPackageManager = declaration !== undefined ? declared ?? null : inferred.length === 1 ? inferred[0] ?? null : inferred.length > 1 ? null : "npm";
|
|
@@ -1280,7 +1835,7 @@ function inspectConsumer(root) {
|
|
|
1280
1835
|
return process.versions.node;
|
|
1281
1836
|
}, "Install Node >=22.13.");
|
|
1282
1837
|
check("git", () => {
|
|
1283
|
-
const result =
|
|
1838
|
+
const result = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
1284
1839
|
cwd: root,
|
|
1285
1840
|
encoding: "utf8",
|
|
1286
1841
|
timeout: 5000,
|
|
@@ -1296,9 +1851,14 @@ function inspectConsumer(root) {
|
|
|
1296
1851
|
}, "Install @fission-ai/openspec@1.5.0, or configure ABEL_OPENSPEC_PACKAGE_ROOT / ABEL_OPENSPEC_NODE before launching the host.");
|
|
1297
1852
|
check("execution", () => {
|
|
1298
1853
|
const profile = executionProfile();
|
|
1854
|
+
if (profile.mode === "host-trusted") {
|
|
1855
|
+
if (!probeWindowsJob(profile.windowsHelperPath ?? ""))
|
|
1856
|
+
throw new Error("windows-job-backend-unavailable");
|
|
1857
|
+
return "host-trusted: Windows x64 Job probe passed; host files/network accessible";
|
|
1858
|
+
}
|
|
1299
1859
|
if (process.platform !== "linux")
|
|
1300
1860
|
throw new Error("linux-pid-isolation-required");
|
|
1301
|
-
const result =
|
|
1861
|
+
const result = spawnSync2(profile.bwrapPath, [
|
|
1302
1862
|
"--ro-bind",
|
|
1303
1863
|
"/",
|
|
1304
1864
|
"/",
|
|
@@ -1309,45 +1869,45 @@ function inspectConsumer(root) {
|
|
|
1309
1869
|
if (result.status !== 0)
|
|
1310
1870
|
throw new Error("bubblewrap-probe-failed");
|
|
1311
1871
|
return `${profile.mode}: Bubblewrap PID namespace probe passed`;
|
|
1312
|
-
}, "On Linux install/enable Bubblewrap; set ABEL_BWRAP_PATH for a custom install. Trusted Linux projects may explicitly set ABEL_EXECUTION_MODE=local-trusted; Windows
|
|
1872
|
+
}, "On Linux install/enable Bubblewrap; set ABEL_BWRAP_PATH for a custom install. Trusted Linux projects may explicitly set ABEL_EXECUTION_MODE=local-trusted; Windows x64 trusted projects may explicitly select ABEL_EXECUTION_MODE=host-trusted and install the matching native Job helper (ABEL_WINDOWS_JOB_HELPER).");
|
|
1313
1873
|
check("request-budgets", () => JSON.stringify(requestBounds()), "Set ABEL_FIRST_PROGRESS_MS and ABEL_STREAM_IDLE_MS to positive milliseconds <=1200000.");
|
|
1314
1874
|
check("state-root", () => resolveStateRoot({
|
|
1315
1875
|
consumerRoot: root,
|
|
1316
1876
|
xdgStateHome: process.env.XDG_STATE_HOME
|
|
1317
1877
|
}).rootDir, "Use an absolute repository-external XDG_STATE_HOME.");
|
|
1318
1878
|
check("workspace-dependencies", () => {
|
|
1319
|
-
const dependencies =
|
|
1320
|
-
if (!
|
|
1879
|
+
const dependencies = path8.join(root, "node_modules");
|
|
1880
|
+
if (!lstatSync8(dependencies, { throwIfNoEntry: false }))
|
|
1321
1881
|
return "no node_modules; prepare dependencies before verification";
|
|
1322
|
-
const names = readdirSync4(dependencies).flatMap((name) => name.startsWith("@") &&
|
|
1882
|
+
const names = readdirSync4(dependencies).flatMap((name) => name.startsWith("@") && lstatSync8(path8.join(dependencies, name)).isDirectory() ? readdirSync4(path8.join(dependencies, name)).map((child) => `${name}/${child}`) : [name]);
|
|
1323
1883
|
if (names.length > 20000)
|
|
1324
1884
|
throw new Error("dependency-view-too-large");
|
|
1325
1885
|
let links = 0;
|
|
1326
1886
|
for (const name of names) {
|
|
1327
|
-
const file =
|
|
1328
|
-
if (!
|
|
1887
|
+
const file = path8.join(dependencies, name);
|
|
1888
|
+
if (!lstatSync8(file).isSymbolicLink())
|
|
1329
1889
|
continue;
|
|
1330
1890
|
const resolved = realpathSync4(file);
|
|
1331
|
-
const relative =
|
|
1332
|
-
if (relative === ".." || relative.startsWith(`..${
|
|
1891
|
+
const relative = path8.relative(root, resolved);
|
|
1892
|
+
if (relative === ".." || relative.startsWith(`..${path8.sep}`) || path8.isAbsolute(relative))
|
|
1333
1893
|
throw new Error("dependency-link-outside-consumer");
|
|
1334
|
-
if (!resolved.startsWith(`${dependencies}${
|
|
1894
|
+
if (!resolved.startsWith(`${dependencies}${path8.sep}`))
|
|
1335
1895
|
links++;
|
|
1336
1896
|
}
|
|
1337
1897
|
return `${links} repository workspace link(s); their targets must be included in the candidate baseline`;
|
|
1338
1898
|
}, "Repair dangling/external workspace links and ensure local package sources are tracked or admitted in the candidate.");
|
|
1339
|
-
const manifest =
|
|
1899
|
+
const manifest = path8.join(root, "package.json");
|
|
1340
1900
|
let selection = packageManagerSelection(root, undefined);
|
|
1341
|
-
if (
|
|
1901
|
+
if (lstatSync8(manifest, { throwIfNoEntry: false })?.isFile()) {
|
|
1342
1902
|
check("manifest", () => {
|
|
1343
|
-
const parsed2 = JSON.parse(
|
|
1903
|
+
const parsed2 = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
1344
1904
|
if (!parsed2 || typeof parsed2 !== "object")
|
|
1345
1905
|
throw new Error("manifest-invalid");
|
|
1346
1906
|
return Object.keys(parsed2.scripts ?? {}).join(", ") || "no package scripts";
|
|
1347
1907
|
}, "Repair package.json.");
|
|
1348
1908
|
const parsed = (() => {
|
|
1349
1909
|
try {
|
|
1350
|
-
const value = JSON.parse(
|
|
1910
|
+
const value = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
1351
1911
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1352
1912
|
} catch {
|
|
1353
1913
|
return {};
|
|
@@ -1390,7 +1950,7 @@ function storageUsage(root, maximum = 1e5) {
|
|
|
1390
1950
|
truncated = true;
|
|
1391
1951
|
return;
|
|
1392
1952
|
}
|
|
1393
|
-
const stat =
|
|
1953
|
+
const stat = lstatSync8(file, { throwIfNoEntry: false });
|
|
1394
1954
|
if (!stat)
|
|
1395
1955
|
return;
|
|
1396
1956
|
entries++;
|
|
@@ -1398,7 +1958,7 @@ function storageUsage(root, maximum = 1e5) {
|
|
|
1398
1958
|
bytes += stat.size;
|
|
1399
1959
|
else if (stat.isDirectory())
|
|
1400
1960
|
for (const name of readdirSync4(file)) {
|
|
1401
|
-
visit(
|
|
1961
|
+
visit(path8.join(file, name));
|
|
1402
1962
|
if (truncated)
|
|
1403
1963
|
break;
|
|
1404
1964
|
}
|
|
@@ -1412,7 +1972,7 @@ function inspectRuns(root) {
|
|
|
1412
1972
|
xdgStateHome: process.env.XDG_STATE_HOME
|
|
1413
1973
|
});
|
|
1414
1974
|
const usage = storageUsage(state.rootDir);
|
|
1415
|
-
if (!
|
|
1975
|
+
if (!lstatSync8(state.databasePath, { throwIfNoEntry: false }))
|
|
1416
1976
|
return { stateRoot: state.rootDir, usage, runs: [], truncated: false };
|
|
1417
1977
|
const db = new DatabaseSync(state.databasePath, { readOnly: true });
|
|
1418
1978
|
try {
|
|
@@ -1433,7 +1993,7 @@ var [command = "doctor", root = process.cwd()] = process.argv.slice(2);
|
|
|
1433
1993
|
try {
|
|
1434
1994
|
if (!["doctor", "runs"].includes(command))
|
|
1435
1995
|
throw new Error("Usage: node --experimental-strip-types <cadence>/src/operator-cli.mjs doctor|runs [consumer-root]");
|
|
1436
|
-
const result = command === "doctor" ? inspectConsumer(
|
|
1996
|
+
const result = command === "doctor" ? inspectConsumer(path9.resolve(root)) : inspectRuns(path9.resolve(root));
|
|
1437
1997
|
console.log(JSON.stringify(result, null, 2));
|
|
1438
1998
|
if (command === "doctor" && !result.ok)
|
|
1439
1999
|
process.exitCode = 1;
|