@abelxiaoxing/cadence 1.8.4 → 2.0.1
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 +324 -117
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/agents/design-explorer.md +14 -18
- package/agents/diagnosis-worker.md +14 -14
- package/agents/implementation-worker.md +16 -17
- package/config/workflow-task.example.json +7 -0
- package/docs/remote-access.md +60 -0
- package/docs/reuse-and-distribution.md +44 -0
- package/docs/service-management.md +108 -0
- package/licenses/bundled-web.txt +6245 -0
- package/licenses/pi-web-ui-MIT.txt +21 -0
- package/licenses/pi-web-ui-reuse.md +22 -0
- package/package.json +92 -20
- package/src/index.ts +21 -1802
- package/src/native-build.ts +6 -0
- package/src/native-client.ts +103 -0
- package/src/native-export.ts +58 -0
- package/src/native-history.ts +359 -0
- package/src/operator-cli.mjs +453 -1937
- package/src/plan-proposal.ts +87 -0
- package/src/sdk-worker.mjs +549 -0
- package/src/service/contracts/active.ts +82 -0
- package/src/service/contracts/budget.ts +148 -0
- package/src/service/contracts/build.ts +67 -0
- package/src/service/contracts/capture.ts +48 -0
- package/src/service/contracts/core.ts +33 -0
- package/src/service/contracts/current.ts +261 -0
- package/src/service/contracts/domain.ts +68 -0
- package/src/service/contracts/history.ts +2245 -0
- package/src/service/contracts/models.ts +139 -0
- package/src/service/coordination.ts +862 -0
- package/src/service/private-files.ts +204 -0
- package/src/service-cli.mjs +5592 -0
- package/src/service-client.ts +747 -0
- package/src/service-contract.ts +245 -0
- package/src/service-host.mjs +22382 -0
- package/src/service-http-client.ts +425 -0
- package/src/service-wire.ts +372 -0
- package/src/task-shell.ts +1045 -0
- package/src/task-store.ts +479 -0
- package/src/task-types.ts +184 -0
- package/src/verification-diagnostics.ts +29 -72
- package/src/worker-capture.mjs +1197 -0
- package/src/worker-public.mjs +1753 -0
- package/src/worker-redaction.mjs +608 -0
- package/src/worker-tools.mjs +202 -0
- package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/web/dist/assets/cadence.css +2 -0
- package/web/dist/assets/cadence.js +7 -0
- package/web/dist/assets/markdown.js +296 -0
- package/web/dist/assets/react.js +9 -0
- package/web/dist/assets/rolldown-runtime.js +1 -0
- package/web/dist/assets/timeline-client.js +41 -0
- package/web/dist/assets/vis-timeline.css +1274 -0
- package/web/dist/assets/vis-timeline.js +32 -0
- package/web/dist/index.html +18 -0
- package/web/dist/manifest.json +505 -0
- package/config/plan-draft.example.json +0 -125
- package/config/plan-draft.multiple-tasks.example.json +0 -375
- package/config/plan-draft.quick.example.json +0 -78
- package/prompts/abel-design.md +0 -382
- package/prompts/abel-diagnose.md +0 -71
- package/prompts/abel-implement.md +0 -284
- package/prompts/abel-init.md +0 -52
- package/src/activation.ts +0 -44
- package/src/activity-contracts.ts +0 -82
- package/src/agent-registry.ts +0 -53
- package/src/apply-transaction.ts +0 -1437
- package/src/artifact-store.ts +0 -213
- package/src/build-windows-job.mjs +0 -95
- package/src/candidate-artifact.ts +0 -143
- package/src/candidate-context.ts +0 -289
- package/src/candidate-patch.ts +0 -271
- package/src/candidate-workspace.ts +0 -80
- package/src/canonical.ts +0 -16
- package/src/change-contract-schema.ts +0 -191
- package/src/change-contract.ts +0 -324
- package/src/change-verification.ts +0 -1617
- package/src/contracts.ts +0 -2466
- package/src/control-contracts.ts +0 -382
- package/src/delivery-compiler.ts +0 -1805
- package/src/delivery-revision.ts +0 -42
- package/src/design-control.ts +0 -1299
- package/src/design-diagnostics.ts +0 -181
- package/src/design-journal.ts +0 -1773
- package/src/directory-sync.ts +0 -36
- package/src/durable-contracts.ts +0 -227
- package/src/durable-workflow.ts +0 -822
- package/src/evidence-draft.ts +0 -122
- package/src/execution-profile.ts +0 -64
- package/src/execution-retention.ts +0 -77
- package/src/file-snapshot.ts +0 -163
- package/src/implement-continuation.ts +0 -559
- package/src/implement-graph.ts +0 -690
- package/src/implement-plan.ts +0 -72
- package/src/install-config.d.mts +0 -6
- package/src/isolation-backend.ts +0 -353
- package/src/model-source.ts +0 -22
- package/src/openspec-cli.ts +0 -494
- package/src/operator-cli-entry.mjs +0 -24
- package/src/operator-tools.ts +0 -309
- package/src/package-candidate-process.ts +0 -206
- package/src/package-candidate.ts +0 -24
- package/src/package-delivery.ts +0 -486
- package/src/package-state.ts +0 -320
- package/src/package-verification.ts +0 -635
- package/src/package-workflow.ts +0 -305
- package/src/packet-runtime.ts +0 -459
- package/src/phase-execution.ts +0 -2163
- package/src/pi-adapter.ts +0 -19
- package/src/plan-draft-summary.ts +0 -527
- package/src/plan-draft.ts +0 -502
- package/src/remove-path.ts +0 -32
- package/src/run-state.ts +0 -420
- package/src/run-store.ts +0 -1179
- package/src/safe-path.ts +0 -77
- package/src/scoped-grep-worker.mjs +0 -23
- package/src/scoped-tools.ts +0 -775
- package/src/single-task-draft.ts +0 -142
- package/src/sqlite-schema.ts +0 -171
- package/src/state-root.ts +0 -215
- package/src/storage-schema.ts +0 -360
- package/src/subagent-activity.ts +0 -1417
- package/src/subagent-process.ts +0 -476
- package/src/task-ledger.ts +0 -1338
- package/src/transport-budget.ts +0 -50
- package/src/verification-capability.ts +0 -1191
- package/src/verification-environment-io.ts +0 -389
- package/src/verification-environment.ts +0 -162
- package/src/verification-identity.ts +0 -77
- package/src/verification-scan.ts +0 -64
- package/src/windows-job-backend.ts +0 -419
- package/src/windows-job.c +0 -149
- package/src/workflow-engine.ts +0 -28
- package/src/workflow-policy.ts +0 -1670
- package/src/workflow-recovery-policy.ts +0 -97
- package/src/workflow-scheduling.ts +0 -105
- package/src/workflow-state-machine.ts +0 -4341
- package/src/workflow-status.ts +0 -576
- package/src/workspace-io-entry.mjs +0 -76
- package/src/workspace-io-worker.mjs +0 -1312
- package/src/workspace-io.ts +0 -89
- package/src/workspace-store.ts +0 -634
package/src/operator-cli.mjs
CHANGED
|
@@ -1,2006 +1,522 @@
|
|
|
1
|
-
// Generated by scripts/build-
|
|
1
|
+
// Generated by scripts/build-operator.mjs; edit operator-cli-entry.mjs and its source modules.
|
|
2
2
|
// src/operator-cli-entry.mjs
|
|
3
|
-
import path9 from "node:path";
|
|
4
|
-
|
|
5
|
-
// src/operator-tools.ts
|
|
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
|
-
import { DatabaseSync } from "node:sqlite";
|
|
10
|
-
|
|
11
|
-
// src/execution-profile.ts
|
|
12
3
|
import path from "node:path";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
function executionProfile(env = process.env) {
|
|
15
|
-
const mode = env.ABEL_EXECUTION_MODE ?? "isolated";
|
|
16
|
-
if (mode !== "isolated" && mode !== "local-trusted" && mode !== "host-trusted")
|
|
17
|
-
throw new Error("execution-mode-invalid");
|
|
18
|
-
const timeoutMs = Number(env.ABEL_VERIFICATION_TIMEOUT_MS ?? 600000);
|
|
19
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86400000)
|
|
20
|
-
throw new Error("verification-timeout-invalid");
|
|
21
|
-
const inheritEnvironment = (env.ABEL_VERIFICATION_ENV ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
22
|
-
if (inheritEnvironment.some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)))
|
|
23
|
-
throw new Error("verification-environment-names-invalid");
|
|
24
|
-
if (mode === "isolated" && inheritEnvironment.length)
|
|
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");
|
|
28
|
-
return {
|
|
29
|
-
mode,
|
|
30
|
-
timeoutMs,
|
|
31
|
-
bwrapPath: env.ABEL_BWRAP_PATH ?? "/usr/bin/bwrap",
|
|
32
|
-
inheritEnvironment,
|
|
33
|
-
...mode === "host-trusted" ? {
|
|
34
|
-
windowsHelperPath: env.ABEL_WINDOWS_JOB_HELPER ?? fileURLToPath(new URL("./windows-job.exe", import.meta.url))
|
|
35
|
-
} : {}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
4
|
|
|
39
|
-
// src/
|
|
40
|
-
import {
|
|
41
|
-
import
|
|
42
|
-
var PACKAGE_NAME = "@fission-ai/openspec";
|
|
43
|
-
var OPENSPEC_LIMITS = Object.freeze({
|
|
44
|
-
timeoutMs: 30000,
|
|
45
|
-
maxBuffer: 4 * 1024 * 1024
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
class OpenSpecCliError extends Error {
|
|
49
|
-
diagnostic;
|
|
50
|
-
constructor(command, phase, reason, detail = {}) {
|
|
51
|
-
super("design-openspec-unavailable");
|
|
52
|
-
this.name = "OpenSpecCliError";
|
|
53
|
-
this.diagnostic = Object.freeze({
|
|
54
|
-
code: "design-openspec-unavailable",
|
|
55
|
-
command,
|
|
56
|
-
phase,
|
|
57
|
-
reason,
|
|
58
|
-
...["ENOENT", "EINVAL", "EACCES", "EPERM", "ENOEXEC", "E2BIG"].includes(detail.systemCode ?? "") ? { systemCode: detail.systemCode } : {},
|
|
59
|
-
...Number.isSafeInteger(detail.exitCode) && (detail.exitCode ?? -1) >= 0 ? { exitCode: String(detail.exitCode) } : {}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function record(value) {
|
|
64
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
65
|
-
}
|
|
66
|
-
function inside(root, target) {
|
|
67
|
-
const relative = path2.relative(root, target);
|
|
68
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
|
|
69
|
-
}
|
|
70
|
-
function envValue(environment, name) {
|
|
71
|
-
const key = Object.keys(environment).sort().find((candidate) => process.platform === "win32" ? candidate.toUpperCase() === name.toUpperCase() : candidate === name);
|
|
72
|
-
return key === undefined ? undefined : environment[key];
|
|
73
|
-
}
|
|
74
|
-
function packageEntry(root) {
|
|
75
|
-
const canonicalRoot = realpathSync(root);
|
|
76
|
-
const manifestPath = realpathSync(path2.join(canonicalRoot, "package.json"));
|
|
77
|
-
const manifestStat = lstatSync(manifestPath);
|
|
78
|
-
if (!inside(canonicalRoot, manifestPath) || !manifestStat.isFile() || manifestStat.size > 1024 * 1024)
|
|
79
|
-
throw new Error("manifest-limit");
|
|
80
|
-
const manifest = record(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
81
|
-
const bin = typeof manifest?.bin === "string" ? manifest.bin : record(manifest?.bin)?.openspec;
|
|
82
|
-
if (manifest?.name !== PACKAGE_NAME || typeof bin !== "string" || path2.isAbsolute(bin))
|
|
83
|
-
throw new Error("manifest-invalid");
|
|
84
|
-
const entry = realpathSync(path2.resolve(canonicalRoot, bin));
|
|
85
|
-
if (!inside(canonicalRoot, entry) || !/\.(?:cjs|mjs|js)$/iu.test(entry) || !lstatSync(entry).isFile())
|
|
86
|
-
throw new Error("entry-invalid");
|
|
87
|
-
return entry;
|
|
88
|
-
}
|
|
89
|
-
function canonicalConsumerRoot(consumerRoot) {
|
|
90
|
-
try {
|
|
91
|
-
const root = realpathSync(consumerRoot);
|
|
92
|
-
if (!lstatSync(root).isDirectory())
|
|
93
|
-
throw new Error("cwd-invalid");
|
|
94
|
-
return root;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
throw new OpenSpecCliError("resolve", "resolution", "cwd-unavailable", {
|
|
97
|
-
systemCode: error.code
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function commandIn(directory) {
|
|
102
|
-
const names = process.platform === "win32" ? ["openspec.exe", "openspec.cmd", "openspec"] : ["openspec"];
|
|
103
|
-
const entries = process.platform === "win32" ? readdirSync(directory) : names;
|
|
104
|
-
for (const name of names) {
|
|
105
|
-
const matches = entries.filter((entry) => entry.toLowerCase() === name);
|
|
106
|
-
if (matches.length > 1)
|
|
107
|
-
throw new Error("ambiguous-command");
|
|
108
|
-
if (matches.length === 1) {
|
|
109
|
-
const candidate = path2.join(directory, matches[0]);
|
|
110
|
-
if (lstatSync(candidate, { throwIfNoEntry: false }))
|
|
111
|
-
return candidate;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
function resolveOpenSpecInvocation(consumerRoot, environment = process.env) {
|
|
117
|
-
const configuredRoot = envValue(environment, "ABEL_OPENSPEC_PACKAGE_ROOT");
|
|
118
|
-
const configuredNode = envValue(environment, "ABEL_OPENSPEC_NODE");
|
|
119
|
-
const node = configuredNode ?? (process.release.name === "node" && !process.versions.bun ? process.execPath : undefined);
|
|
120
|
-
let executable;
|
|
121
|
-
try {
|
|
122
|
-
if (!node || !path2.isAbsolute(node) || /\.(?:cmd|bat|ps1)$/iu.test(node)) {
|
|
123
|
-
throw new Error("runtime-invalid");
|
|
124
|
-
}
|
|
125
|
-
executable = realpathSync(node);
|
|
126
|
-
if (!lstatSync(executable).isFile())
|
|
127
|
-
throw new Error("runtime-invalid");
|
|
128
|
-
} catch {
|
|
129
|
-
throw new OpenSpecCliError("resolve", "resolution", "runtime-unavailable");
|
|
130
|
-
}
|
|
131
|
-
if (configuredRoot !== undefined) {
|
|
132
|
-
try {
|
|
133
|
-
if (!path2.isAbsolute(configuredRoot))
|
|
134
|
-
throw new Error("root-invalid");
|
|
135
|
-
return { executable, entry: packageEntry(configuredRoot) };
|
|
136
|
-
} catch {
|
|
137
|
-
throw new OpenSpecCliError("resolve", "resolution", "installation-invalid");
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
const consumer = canonicalConsumerRoot(consumerRoot);
|
|
141
|
-
const directories = (envValue(environment, "PATH") ?? "").split(path2.delimiter);
|
|
142
|
-
for (const directory of directories) {
|
|
143
|
-
if (!path2.isAbsolute(directory))
|
|
144
|
-
continue;
|
|
145
|
-
let command;
|
|
146
|
-
let canonicalDirectory;
|
|
147
|
-
try {
|
|
148
|
-
canonicalDirectory = realpathSync(directory);
|
|
149
|
-
if (inside(consumer, canonicalDirectory))
|
|
150
|
-
continue;
|
|
151
|
-
command = commandIn(canonicalDirectory);
|
|
152
|
-
} catch {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (!command)
|
|
156
|
-
continue;
|
|
157
|
-
try {
|
|
158
|
-
const target = realpathSync(command);
|
|
159
|
-
if (inside(consumer, target)) {
|
|
160
|
-
throw new OpenSpecCliError("resolve", "resolution", "unsafe-installation");
|
|
161
|
-
}
|
|
162
|
-
const layoutRoots = [
|
|
163
|
-
path2.join(canonicalDirectory, "node_modules", PACKAGE_NAME),
|
|
164
|
-
path2.resolve(canonicalDirectory, "../lib/node_modules", PACKAGE_NAME)
|
|
165
|
-
];
|
|
166
|
-
const roots = [];
|
|
167
|
-
let ancestor = path2.dirname(target);
|
|
168
|
-
for (let depth = 0;depth < 8; depth++) {
|
|
169
|
-
roots.push(ancestor);
|
|
170
|
-
const parent = path2.dirname(ancestor);
|
|
171
|
-
if (parent === ancestor)
|
|
172
|
-
break;
|
|
173
|
-
ancestor = parent;
|
|
174
|
-
}
|
|
175
|
-
const linked = lstatSync(command).isSymbolicLink();
|
|
176
|
-
const candidates = linked ? roots : layoutRoots;
|
|
177
|
-
for (const root of candidates) {
|
|
178
|
-
let entry;
|
|
179
|
-
try {
|
|
180
|
-
entry = packageEntry(root);
|
|
181
|
-
if (linked && entry !== target)
|
|
182
|
-
continue;
|
|
183
|
-
} catch {
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (inside(consumer, entry)) {
|
|
187
|
-
throw new OpenSpecCliError("resolve", "resolution", "unsafe-installation");
|
|
188
|
-
}
|
|
189
|
-
return { executable, entry };
|
|
190
|
-
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
if (error instanceof OpenSpecCliError)
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
throw new OpenSpecCliError("resolve", "resolution", "installation-invalid");
|
|
196
|
-
}
|
|
197
|
-
throw new OpenSpecCliError("resolve", "resolution", "installation-not-found");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// src/state-root.ts
|
|
201
|
-
import { createHash } from "node:crypto";
|
|
202
|
-
import { chmodSync, lstatSync as lstatSync2, mkdirSync, realpathSync as realpathSync2 } from "node:fs";
|
|
203
|
-
import os from "node:os";
|
|
204
|
-
import path3 from "node:path";
|
|
205
|
-
class StateRootError extends Error {
|
|
206
|
-
code;
|
|
207
|
-
constructor(code) {
|
|
208
|
-
super(code);
|
|
209
|
-
this.name = "StateRootError";
|
|
210
|
-
this.code = code;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function isContainedBy(parent, candidate) {
|
|
214
|
-
const relative = path3.relative(parent, candidate);
|
|
215
|
-
return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
|
|
216
|
-
}
|
|
217
|
-
function inspectExistingComponents(absolutePath) {
|
|
218
|
-
const parsed = path3.parse(absolutePath);
|
|
219
|
-
const relativeParts = absolutePath.slice(parsed.root.length).split(path3.sep).filter(Boolean);
|
|
220
|
-
let current = parsed.root;
|
|
221
|
-
for (const part of relativeParts) {
|
|
222
|
-
current = path3.join(current, part);
|
|
223
|
-
let stat;
|
|
224
|
-
try {
|
|
225
|
-
stat = lstatSync2(current);
|
|
226
|
-
} catch (error) {
|
|
227
|
-
if (error.code === "ENOENT")
|
|
228
|
-
return;
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
231
|
-
if (stat.isSymbolicLink()) {
|
|
232
|
-
throw new StateRootError("state-component-symlink");
|
|
233
|
-
}
|
|
234
|
-
if (!stat.isDirectory() && current !== absolutePath) {
|
|
235
|
-
throw new StateRootError("state-component-type");
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
function canonicalFuturePath(input) {
|
|
240
|
-
const absolute = path3.resolve(input);
|
|
241
|
-
inspectExistingComponents(absolute);
|
|
242
|
-
let cursor = absolute;
|
|
243
|
-
const missing = [];
|
|
244
|
-
for (;; ) {
|
|
245
|
-
try {
|
|
246
|
-
return path3.join(realpathSync2.native(cursor), ...missing.reverse());
|
|
247
|
-
} catch (error) {
|
|
248
|
-
if (error.code !== "ENOENT")
|
|
249
|
-
throw error;
|
|
250
|
-
const parent = path3.dirname(cursor);
|
|
251
|
-
if (parent === cursor)
|
|
252
|
-
throw error;
|
|
253
|
-
missing.push(path3.basename(cursor));
|
|
254
|
-
cursor = parent;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function requireAbsoluteDirectoryBase(xdgStateHome, homeDir) {
|
|
259
|
-
if (xdgStateHome && path3.isAbsolute(xdgStateHome)) {
|
|
260
|
-
return canonicalFuturePath(xdgStateHome);
|
|
261
|
-
}
|
|
262
|
-
if (!path3.isAbsolute(homeDir)) {
|
|
263
|
-
throw new StateRootError("invalid-state-base");
|
|
264
|
-
}
|
|
265
|
-
return canonicalFuturePath(path3.join(homeDir, ".local", "state"));
|
|
266
|
-
}
|
|
267
|
-
function resolveStateRoot(options) {
|
|
268
|
-
let consumerRoot;
|
|
269
|
-
try {
|
|
270
|
-
consumerRoot = realpathSync2.native(path3.resolve(options.consumerRoot));
|
|
271
|
-
if (!lstatSync2(consumerRoot).isDirectory()) {
|
|
272
|
-
throw new StateRootError("consumer-root-unavailable");
|
|
273
|
-
}
|
|
274
|
-
} catch (error) {
|
|
275
|
-
if (error instanceof StateRootError)
|
|
276
|
-
throw error;
|
|
277
|
-
throw new StateRootError("consumer-root-unavailable");
|
|
278
|
-
}
|
|
279
|
-
const stateBase = requireAbsoluteDirectoryBase(options.xdgStateHome, options.homeDir ?? os.homedir());
|
|
280
|
-
const consumerRootHash = createHash("sha256").update(consumerRoot).digest("hex");
|
|
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));
|
|
284
|
-
if (isContainedBy(consumerRoot, rootDir)) {
|
|
285
|
-
throw new StateRootError("state-root-inside-consumer");
|
|
286
|
-
}
|
|
287
|
-
return {
|
|
288
|
-
consumerRoot,
|
|
289
|
-
consumerRootHash,
|
|
290
|
-
stateBase,
|
|
291
|
-
productRoot,
|
|
292
|
-
rootsDir,
|
|
293
|
-
rootDir,
|
|
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")
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// src/transport-budget.ts
|
|
302
|
-
var REQUEST_BOUNDS = Object.freeze({
|
|
303
|
-
firstProgressMs: 90000,
|
|
304
|
-
streamIdleMs: 180000,
|
|
305
|
-
cancellationDrainMs: 250
|
|
306
|
-
});
|
|
307
|
-
function requestBounds(env = process.env) {
|
|
308
|
-
const read = (name, fallback) => {
|
|
309
|
-
const value = Number(env[name] ?? fallback);
|
|
310
|
-
if (!Number.isSafeInteger(value) || value < 1 || value > 1200000)
|
|
311
|
-
throw new Error("request-budget-invalid");
|
|
312
|
-
return value;
|
|
313
|
-
};
|
|
314
|
-
return {
|
|
315
|
-
...REQUEST_BOUNDS,
|
|
316
|
-
firstProgressMs: read("ABEL_FIRST_PROGRESS_MS", REQUEST_BOUNDS.firstProgressMs),
|
|
317
|
-
streamIdleMs: read("ABEL_STREAM_IDLE_MS", REQUEST_BOUNDS.streamIdleMs)
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// src/verification-capability.ts
|
|
5
|
+
// src/task-store.ts
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
322
8
|
import {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
9
|
+
closeSync,
|
|
10
|
+
constants,
|
|
11
|
+
fstatSync,
|
|
12
|
+
fsyncSync,
|
|
13
|
+
lstatSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
openSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
readSync,
|
|
18
|
+
realpathSync,
|
|
19
|
+
renameSync,
|
|
20
|
+
unlinkSync,
|
|
21
|
+
writeFileSync
|
|
327
22
|
} from "node:fs";
|
|
328
|
-
import
|
|
23
|
+
import { homedir, hostname } from "node:os";
|
|
24
|
+
import {
|
|
25
|
+
dirname,
|
|
26
|
+
isAbsolute,
|
|
27
|
+
join,
|
|
28
|
+
parse,
|
|
29
|
+
relative,
|
|
30
|
+
resolve,
|
|
31
|
+
sep
|
|
32
|
+
} from "node:path";
|
|
329
33
|
|
|
330
|
-
// src/
|
|
331
|
-
var
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
reason: {
|
|
349
|
-
type: "string",
|
|
350
|
-
enum: ["parent-directed-retry", "route-changed", "context-extended"]
|
|
351
|
-
}
|
|
352
|
-
},
|
|
353
|
-
required: ["incidentKey", "failureSequence", "reason"]
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
var commandSchema = (command, fields) => ({
|
|
357
|
-
type: "object",
|
|
358
|
-
properties: {
|
|
359
|
-
command: { type: "string", enum: [command] },
|
|
360
|
-
...Object.fromEntries(fields.map((field) => [field, CONTROL_SCHEMA_PROPERTIES[field]]))
|
|
361
|
-
},
|
|
362
|
-
required: ["command", ...fields],
|
|
363
|
-
additionalProperties: false
|
|
364
|
-
});
|
|
365
|
-
var CONTROL_COMMAND_PARAMETERS = {
|
|
366
|
-
type: "object",
|
|
367
|
-
properties: CONTROL_SCHEMA_PROPERTIES,
|
|
368
|
-
required: ["command", "stage", "change"],
|
|
369
|
-
anyOf: [
|
|
370
|
-
commandSchema("start", ["stage", "change", "operationId"]),
|
|
371
|
-
commandSchema("status", ["stage", "change"]),
|
|
372
|
-
commandSchema("resume", ["stage", "change", "operationId"]),
|
|
373
|
-
commandSchema("resume", ["stage", "change", "operationId", "recovery"]),
|
|
374
|
-
commandSchema("resume", [
|
|
375
|
-
"stage",
|
|
376
|
-
"change",
|
|
377
|
-
"operationId",
|
|
378
|
-
"deliveryRevision",
|
|
379
|
-
"receiptHash",
|
|
380
|
-
"recovery"
|
|
381
|
-
]),
|
|
382
|
-
commandSchema("resume", [
|
|
383
|
-
"stage",
|
|
384
|
-
"change",
|
|
385
|
-
"operationId",
|
|
386
|
-
"deliveryRevision",
|
|
387
|
-
"receiptHash"
|
|
388
|
-
]),
|
|
389
|
-
commandSchema("rebind", ["stage", "change", "operationId", "routeId"]),
|
|
390
|
-
commandSchema("cancel", ["stage", "change", "operationId"]),
|
|
391
|
-
commandSchema("discard", ["stage", "change", "operationId"])
|
|
392
|
-
],
|
|
393
|
-
additionalProperties: false
|
|
394
|
-
};
|
|
395
|
-
var CONTROL_TOOL_FIELDS = new Set([
|
|
396
|
-
"command",
|
|
397
|
-
"stage",
|
|
398
|
-
"change",
|
|
399
|
-
"operationId",
|
|
400
|
-
"deliveryRevision",
|
|
401
|
-
"receiptHash",
|
|
402
|
-
"routeId",
|
|
403
|
-
"recovery"
|
|
404
|
-
]);
|
|
405
|
-
var COMMAND_FIELDS = Object.freeze({
|
|
406
|
-
start: new Set(["command", "stage", "change", "operationId"]),
|
|
407
|
-
status: new Set(["command", "stage", "change"]),
|
|
408
|
-
resume: new Set([
|
|
409
|
-
"command",
|
|
410
|
-
"stage",
|
|
411
|
-
"change",
|
|
412
|
-
"operationId",
|
|
413
|
-
"deliveryRevision",
|
|
414
|
-
"receiptHash",
|
|
415
|
-
"recovery"
|
|
416
|
-
]),
|
|
417
|
-
rebind: new Set(["command", "stage", "change", "operationId", "routeId"]),
|
|
418
|
-
cancel: new Set(["command", "stage", "change", "operationId"]),
|
|
419
|
-
discard: new Set(["command", "stage", "change", "operationId"])
|
|
420
|
-
});
|
|
421
|
-
// src/contracts.ts
|
|
422
|
-
var LIMITS = {
|
|
423
|
-
maxActiveChildSessions: 4,
|
|
424
|
-
maxEnvelopeBytes: 64 * 1024,
|
|
425
|
-
phaseTimeoutMs: 20 * 60 * 1000,
|
|
426
|
-
maxCompleteResultBytes: 64 * 1024
|
|
427
|
-
};
|
|
428
|
-
var VERIFICATION_LIMITS = Object.freeze({
|
|
429
|
-
minSteps: 1,
|
|
430
|
-
maxSteps: 8,
|
|
431
|
-
minTestFiles: 1,
|
|
432
|
-
maxTestFiles: 64
|
|
433
|
-
});
|
|
434
|
-
var NONCANONICAL = /(^|\/)\.\.(\/|$)|(^|\/)\/|^\//;
|
|
435
|
-
var RELATIVE_PATH_PATTERN = String.raw`^(?:\.|(?!/)(?![a-zA-Z]:/)(?!\./)(?![\s\S]*\.\.)(?![\s\S]*[\\\u0000])(?![\s\S]*//)(?![\s\S]*/\.(?:/|$))(?![\s\S]*/$)[\s\S]+)$`;
|
|
436
|
-
function isValidRelativePath(p) {
|
|
437
|
-
return typeof p === "string" && p.length > 0 && p.length <= 512 && !NONCANONICAL.test(p) && !p.startsWith("/") && !/^[a-z]:\//iu.test(p) && (p === "." || !p.startsWith("./") && !p.endsWith("/") && !p.split("/").includes(".")) && !p.includes("..") && !p.includes("\\") && !p.includes("\x00");
|
|
438
|
-
}
|
|
439
|
-
var UNSAFE_VERIFICATION_TOKEN = /[;&|`$<>\n\r\0]/u;
|
|
440
|
-
var VERIFICATION_NAME = /^[a-z0-9][a-z0-9._:@/-]*$/iu;
|
|
441
|
-
var EXECUTABLE_NAME = /^[a-z0-9][a-z0-9._-]*$/iu;
|
|
442
|
-
var IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
443
|
-
function validVerificationToken(value) {
|
|
444
|
-
const optionValue = typeof value === "string" && value.includes("=") ? value.slice(value.indexOf("=") + 1) : value;
|
|
445
|
-
return typeof value === "string" && value.length > 0 && value.length <= 512 && value.trim() === value && !UNSAFE_VERIFICATION_TOKEN.test(value) && !value.includes("\\") && !value.startsWith("/") && !/^[a-z]:\//iu.test(value) && !/(^|\/)\.\.(\/|$)/u.test(value) && typeof optionValue === "string" && !optionValue.startsWith("/") && !/^[a-z]:\//iu.test(optionValue) && !/(^|\/)\.\.(\/|$)/u.test(optionValue);
|
|
446
|
-
}
|
|
447
|
-
function validVerificationArgs(value) {
|
|
448
|
-
return Array.isArray(value) && value.length <= 128 && value.every(validVerificationToken);
|
|
449
|
-
}
|
|
450
|
-
function validScriptCommand(value) {
|
|
451
|
-
return typeof value === "string" && value.length > 0 && value.length <= 1024 && value.trim() === value && !value.includes("\x00");
|
|
452
|
-
}
|
|
453
|
-
function validVerificationIdentity(contract) {
|
|
454
|
-
if (!validIdentifier(contract.id) || !["expected-red", "expected-green", "expected-refactor"].includes(String(contract.classification))) {
|
|
455
|
-
return false;
|
|
456
|
-
}
|
|
457
|
-
return contract.classification === "expected-red" ? typeof contract.expectedFailure === "string" && contract.expectedFailure.length > 0 && contract.expectedFailure.length <= 512 : contract.expectedFailure === undefined;
|
|
458
|
-
}
|
|
459
|
-
function validatePackageScriptRunner(value) {
|
|
460
|
-
if (!hasExactKeys(value, ["kind", "packageManager", "script", "command"]) || value.kind !== "package-script" || !["bun", "npm", "pnpm", "yarn"].includes(String(value.packageManager)) || typeof value.script !== "string" || !VERIFICATION_NAME.test(value.script) || value.script.includes("/") || !validScriptCommand(value.command)) {
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
function validateExecutableRunner(value) {
|
|
466
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
467
|
-
return false;
|
|
468
|
-
const runner = value;
|
|
469
|
-
if (runner.kind === "package-script") {
|
|
470
|
-
return validatePackageScriptRunner(runner);
|
|
471
|
-
}
|
|
472
|
-
if (runner.kind === "local-binary" && hasExactKeys(runner, ["kind", "executable"])) {
|
|
473
|
-
return typeof runner.executable === "string" && EXECUTABLE_NAME.test(runner.executable);
|
|
474
|
-
}
|
|
475
|
-
return runner.kind === "npx" && hasExactKeys(runner, ["kind", "executable", "noInstall"]) && typeof runner.executable === "string" && EXECUTABLE_NAME.test(runner.executable) && runner.noInstall === true;
|
|
476
|
-
}
|
|
477
|
-
function validateVitestRunner(value) {
|
|
478
|
-
if (value.kind === "package-script") {
|
|
479
|
-
if (UNSAFE_VERIFICATION_TOKEN.test(value.command))
|
|
480
|
-
return false;
|
|
481
|
-
const tokens = value.command.split(/\s+/u);
|
|
482
|
-
return tokens[0] === "vitest" || tokens[0] === "npx" && tokens[1] === "--no-install" && tokens[2] === "vitest";
|
|
483
|
-
}
|
|
484
|
-
return value.executable === "vitest";
|
|
485
|
-
}
|
|
486
|
-
function validateAtomicVerification(value) {
|
|
487
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
488
|
-
return false;
|
|
489
|
-
const contract = value;
|
|
490
|
-
if (contract.executionBindings !== undefined && (!contract.executionBindings || typeof contract.executionBindings !== "object" || Array.isArray(contract.executionBindings) || Object.entries(contract.executionBindings).some(([key, hash]) => !VERIFICATION_CONFIGURATION_PATHS.includes(key) || hash !== null && (typeof hash !== "string" || !/^[a-f0-9]{64}$/u.test(hash)))))
|
|
491
|
-
return false;
|
|
492
|
-
const expectedFailure = [
|
|
493
|
-
...contract.classification === "expected-red" ? ["expectedFailure"] : [],
|
|
494
|
-
...contract.executionBindings === undefined ? [] : ["executionBindings"]
|
|
495
|
-
];
|
|
496
|
-
if (contract.kind === "vitest") {
|
|
497
|
-
const executableRunner = contract.runner;
|
|
498
|
-
return hasExactKeys(contract, [
|
|
499
|
-
"kind",
|
|
500
|
-
"id",
|
|
501
|
-
"runner",
|
|
502
|
-
"testFiles",
|
|
503
|
-
"args",
|
|
504
|
-
"classification",
|
|
505
|
-
...expectedFailure,
|
|
506
|
-
"minTests"
|
|
507
|
-
]) && validVerificationIdentity(contract) && validateExecutableRunner(contract.runner) && validateVitestRunner(executableRunner) && Array.isArray(contract.testFiles) && contract.testFiles.length >= VERIFICATION_LIMITS.minTestFiles && contract.testFiles.length <= VERIFICATION_LIMITS.maxTestFiles && contract.testFiles.every(isValidRelativePath) && new Set(contract.testFiles).size === contract.testFiles.length && validVerificationArgs(contract.args) && Number.isSafeInteger(contract.minTests) && contract.minTests >= 1;
|
|
508
|
-
}
|
|
509
|
-
if (contract.kind === "package-script") {
|
|
510
|
-
const runner2 = {
|
|
511
|
-
kind: "package-script",
|
|
512
|
-
packageManager: contract.packageManager,
|
|
513
|
-
script: contract.script,
|
|
514
|
-
command: contract.command
|
|
515
|
-
};
|
|
516
|
-
return hasExactKeys(contract, [
|
|
517
|
-
"kind",
|
|
518
|
-
"id",
|
|
519
|
-
"packageManager",
|
|
520
|
-
"script",
|
|
521
|
-
"command",
|
|
522
|
-
"args",
|
|
523
|
-
"classification",
|
|
524
|
-
...expectedFailure
|
|
525
|
-
]) && validVerificationIdentity(contract) && validatePackageScriptRunner(runner2) && validVerificationArgs(contract.args);
|
|
526
|
-
}
|
|
527
|
-
if (contract.kind !== "static-check")
|
|
528
|
-
return false;
|
|
529
|
-
const runner = contract.runner;
|
|
530
|
-
const validRunner = validateExecutableRunner(runner) || hasExactKeys(runner, ["kind", "script"]) && runner.kind === "node" && isValidRelativePath(runner.script);
|
|
531
|
-
return hasExactKeys(contract, [
|
|
532
|
-
"kind",
|
|
533
|
-
"id",
|
|
534
|
-
"runner",
|
|
535
|
-
"args",
|
|
536
|
-
"classification",
|
|
537
|
-
...expectedFailure
|
|
538
|
-
]) && validVerificationIdentity(contract) && validRunner && validVerificationArgs(contract.args);
|
|
539
|
-
}
|
|
540
|
-
function validateStructuredVerification(value) {
|
|
541
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
542
|
-
return false;
|
|
543
|
-
const contract = value;
|
|
544
|
-
if (contract.kind !== "steps") {
|
|
545
|
-
return validateAtomicVerification(contract);
|
|
546
|
-
}
|
|
547
|
-
if (!hasExactKeys(contract, ["kind", "id", "classification", "steps"]) || !validIdentifier(contract.id) || !["expected-red", "expected-green", "expected-refactor"].includes(String(contract.classification)) || !Array.isArray(contract.steps) || contract.steps.length < VERIFICATION_LIMITS.minSteps || contract.steps.length > VERIFICATION_LIMITS.maxSteps || !contract.steps.every((step) => validateAtomicVerification(step))) {
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
const steps = contract.steps;
|
|
551
|
-
return steps.slice(0, -1).every((step) => step.classification === "expected-green") && steps.at(-1)?.classification === contract.classification && new Set(steps.map((step) => step.id)).size === steps.length;
|
|
552
|
-
}
|
|
553
|
-
function validateVerificationContract(value) {
|
|
554
|
-
if (validateStructuredVerification(value)) {
|
|
555
|
-
return { ok: true, value: structuredClone(value) };
|
|
556
|
-
}
|
|
34
|
+
// src/task-types.ts
|
|
35
|
+
var TASK_SCHEMA = "cadence-workflow-assistant/1";
|
|
36
|
+
var MAX_RECORD_BYTES = 128 * 1024;
|
|
37
|
+
function object(value) {
|
|
38
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function boundedString(value, max) {
|
|
41
|
+
return typeof value === "string" && Buffer.byteLength(value) <= max && !value.includes("\x00");
|
|
42
|
+
}
|
|
43
|
+
function validId(value) {
|
|
44
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
45
|
+
}
|
|
46
|
+
function decodeCheck(value) {
|
|
47
|
+
if (!object(value) || !boundedString(value.name, 128) || !value.name.trim() || !boundedString(value.command, 4096) || !value.command.trim() || !Number.isInteger(value.timeoutMs) || Number(value.timeoutMs) < 100 || Number(value.timeoutMs) > 1200000)
|
|
48
|
+
throw new Error("Invalid check definition");
|
|
49
|
+
const controls = /[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/u;
|
|
50
|
+
if (controls.test(value.command) || controls.test(value.name) || /[\n\t]/u.test(value.name))
|
|
51
|
+
throw new Error("Check commands/names must be visibly representable in the terminal");
|
|
557
52
|
return {
|
|
558
|
-
|
|
559
|
-
|
|
53
|
+
name: value.name,
|
|
54
|
+
command: value.command,
|
|
55
|
+
timeoutMs: Number(value.timeoutMs)
|
|
560
56
|
};
|
|
561
57
|
}
|
|
562
|
-
function
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
]
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
function hasExactKeys(value, required, optional = []) {
|
|
602
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
603
|
-
return false;
|
|
604
|
-
const record2 = value;
|
|
605
|
-
const allowed = new Set([...required, ...optional]);
|
|
606
|
-
return required.every((key) => Object.hasOwn(record2, key)) && Object.keys(record2).every((key) => allowed.has(key));
|
|
607
|
-
}
|
|
608
|
-
function validIdentifier(value) {
|
|
609
|
-
return typeof value === "string" && IDENTIFIER.test(value);
|
|
58
|
+
function decodeTask(value) {
|
|
59
|
+
if (!object(value) || value.schema !== TASK_SCHEMA || typeof value.id !== "string" || !validId(value.id) || !Number.isSafeInteger(value.revision) || Number(value.revision) < 1 || typeof value.kind !== "string" || !["code", "research", "diagnosis"].includes(value.kind) || typeof value.phase !== "string" || ![
|
|
60
|
+
"planning",
|
|
61
|
+
"running",
|
|
62
|
+
"review",
|
|
63
|
+
"interrupted",
|
|
64
|
+
"failed",
|
|
65
|
+
"done"
|
|
66
|
+
].includes(value.phase) || !boundedString(value.goal, 8192) || !value.goal.trim() || !boundedString(value.plan, 32768) || !boundedString(value.cwd, 4096) || !boundedString(value.result, 16384) || !boundedString(value.baseline, 16384) || !Array.isArray(value.checks) || value.checks.length > 32 || !Array.isArray(value.activity) || value.activity.length > 64)
|
|
67
|
+
throw new Error("Unknown or corrupt task record");
|
|
68
|
+
for (const check of value.checks) {
|
|
69
|
+
decodeCheck(check);
|
|
70
|
+
if (!object(check) || !boundedString(check.cwd, 4096) || !(check.exitCode === null || Number.isInteger(check.exitCode)) || typeof check.durationMs !== "number" || !Number.isFinite(check.durationMs) || check.durationMs < 0 || typeof check.status !== "string" || !["passed", "failed", "canceled", "stale"].includes(check.status) || !boundedString(check.fingerprint, 128) || check.log !== undefined && (typeof check.log !== "string" || !/^[a-f0-9-]+\.log$/.test(check.log)))
|
|
71
|
+
throw new Error("Corrupt check record");
|
|
72
|
+
}
|
|
73
|
+
for (const entry of value.activity) {
|
|
74
|
+
if (!object(entry) || !boundedString(entry.at, 64) || !boundedString(entry.text, 512))
|
|
75
|
+
throw new Error("Corrupt activity");
|
|
76
|
+
}
|
|
77
|
+
if (value.review !== undefined && (!object(value.review) || !boundedString(value.review.fingerprint, 128) || typeof value.review.outcome !== "string" || !["reviewed", "reviewed-with-issues"].includes(value.review.outcome)))
|
|
78
|
+
throw new Error("Corrupt review");
|
|
79
|
+
const allowed = new Set([
|
|
80
|
+
"schema",
|
|
81
|
+
"id",
|
|
82
|
+
"revision",
|
|
83
|
+
"kind",
|
|
84
|
+
"goal",
|
|
85
|
+
"plan",
|
|
86
|
+
"cwd",
|
|
87
|
+
"phase",
|
|
88
|
+
"result",
|
|
89
|
+
"baseline",
|
|
90
|
+
"checks",
|
|
91
|
+
"review",
|
|
92
|
+
"activity"
|
|
93
|
+
]);
|
|
94
|
+
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
95
|
+
throw new Error("Unknown task fields");
|
|
96
|
+
return structuredClone(value);
|
|
610
97
|
}
|
|
611
98
|
|
|
612
|
-
// src/
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
99
|
+
// src/verification-diagnostics.ts
|
|
100
|
+
function diagnosticText(value, maximum = 4096, secrets = []) {
|
|
101
|
+
maximum = Math.max(0, Math.floor(maximum));
|
|
102
|
+
let clean = value;
|
|
103
|
+
for (const secret of [...new Set(secrets)].filter(Boolean).sort((a, b) => b.length - a.length))
|
|
104
|
+
clean = clean.split(secret).join("[redacted]");
|
|
105
|
+
clean = clean.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, "").replace(/(Bearer\s+)\S+/gi, "$1[redacted]").replace(/((?:api[_-]?key|token|password|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[redacted]").replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, "$1[redacted]@");
|
|
106
|
+
if (Buffer.byteLength(clean) <= maximum)
|
|
107
|
+
return clean;
|
|
108
|
+
const marker = maximum >= 12 ? `
|
|
109
|
+
[truncated]` : ".".repeat(maximum);
|
|
110
|
+
const bytes = Buffer.from(clean).subarray(0, Math.max(0, maximum - Buffer.byteLength(marker)));
|
|
111
|
+
return bytes.toString("utf8").replace(/\ufffd$/, "") + marker;
|
|
112
|
+
}
|
|
616
113
|
|
|
617
|
-
// src/
|
|
618
|
-
|
|
619
|
-
import path4 from "node:path";
|
|
620
|
-
function observeSafePath(root, relative) {
|
|
621
|
-
if (!isValidRelativePath(relative)) {
|
|
622
|
-
return { kind: "unsafe", reason: "invalid-path" };
|
|
623
|
-
}
|
|
114
|
+
// src/task-store.ts
|
|
115
|
+
function parseRecord(data) {
|
|
624
116
|
try {
|
|
625
|
-
|
|
626
|
-
const rootStat = lstatSync3(resolvedRoot, { throwIfNoEntry: false });
|
|
627
|
-
if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) {
|
|
628
|
-
return { kind: "unsafe", reason: "root-unavailable" };
|
|
629
|
-
}
|
|
630
|
-
if (relative === ".")
|
|
631
|
-
return { kind: "directory" };
|
|
632
|
-
let current = resolvedRoot;
|
|
633
|
-
const parts = relative.split("/");
|
|
634
|
-
for (const [index, part] of parts.entries()) {
|
|
635
|
-
current = path4.join(current, part);
|
|
636
|
-
if (!current.startsWith(`${resolvedRoot}${path4.sep}`)) {
|
|
637
|
-
return { kind: "unsafe", reason: "invalid-path" };
|
|
638
|
-
}
|
|
639
|
-
const stat = lstatSync3(current, { throwIfNoEntry: false });
|
|
640
|
-
if (!stat)
|
|
641
|
-
return { kind: "absent" };
|
|
642
|
-
if (stat.isSymbolicLink()) {
|
|
643
|
-
return { kind: "unsafe", reason: "symlink" };
|
|
644
|
-
}
|
|
645
|
-
if (index < parts.length - 1) {
|
|
646
|
-
if (!stat.isDirectory()) {
|
|
647
|
-
return { kind: "unsafe", reason: "parent-not-directory" };
|
|
648
|
-
}
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
if (stat.isFile())
|
|
652
|
-
return { kind: "file" };
|
|
653
|
-
if (stat.isDirectory())
|
|
654
|
-
return { kind: "directory" };
|
|
655
|
-
return { kind: "unsafe", reason: "special-file" };
|
|
656
|
-
}
|
|
117
|
+
return JSON.parse(data.toString("utf8"));
|
|
657
118
|
} catch {
|
|
658
|
-
|
|
119
|
+
throw new Error("Corrupt JSON state; preserved without reset");
|
|
659
120
|
}
|
|
660
|
-
return { kind: "unsafe", reason: "invalid-path" };
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// src/file-snapshot.ts
|
|
664
|
-
function sha256(bytes) {
|
|
665
|
-
return createHash2("sha256").update(bytes).digest("hex");
|
|
666
121
|
}
|
|
667
|
-
function
|
|
668
|
-
|
|
669
|
-
return null;
|
|
670
|
-
const abs = resolve(root, relPath);
|
|
671
|
-
const bytes = readFileSync2(abs);
|
|
672
|
-
return { sha256: sha256(bytes), bytes: bytes.length, kind: "file" };
|
|
122
|
+
function absent(error) {
|
|
123
|
+
return object(error) && error.code === "ENOENT";
|
|
673
124
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
125
|
+
function safeDirectory(path, create = false, inspectExisting) {
|
|
126
|
+
const full = resolve(path);
|
|
127
|
+
const root = parse(full).root;
|
|
128
|
+
if (inspectExisting) {
|
|
129
|
+
const st = lstatSync(root);
|
|
130
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
131
|
+
throw new Error("Unsafe directory");
|
|
132
|
+
inspectExisting(root, st);
|
|
133
|
+
}
|
|
134
|
+
let at = root;
|
|
135
|
+
for (const part of full.slice(root.length).split(sep).filter(Boolean)) {
|
|
136
|
+
at = join(at, part);
|
|
137
|
+
let existing;
|
|
138
|
+
try {
|
|
139
|
+
const st = lstatSync(at);
|
|
140
|
+
if (!st.isDirectory())
|
|
141
|
+
throw new Error("Unsafe directory");
|
|
142
|
+
const checked = lstatSync(at);
|
|
143
|
+
if (checked.isSymbolicLink() || inspectExisting && !checked.isDirectory())
|
|
144
|
+
throw new Error("Unsafe directory");
|
|
145
|
+
existing = checked;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (!absent(error))
|
|
148
|
+
throw error;
|
|
149
|
+
if (!create)
|
|
695
150
|
return false;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}))
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
function adapterFailure(verificationId, code, message, inputObservation) {
|
|
705
|
-
return {
|
|
706
|
-
ok: false,
|
|
707
|
-
diagnostic: {
|
|
708
|
-
kind: "verification-adapter",
|
|
709
|
-
code,
|
|
710
|
-
verificationId,
|
|
711
|
-
message,
|
|
712
|
-
...inputObservation ? { inputObservation } : {}
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
function fileStatus(root, relative) {
|
|
717
|
-
const observation = observeSafePath(root, relative);
|
|
718
|
-
if (observation.kind === "file")
|
|
719
|
-
return "regular";
|
|
720
|
-
return observation.kind === "absent" ? "absent" : "unsafe";
|
|
721
|
-
}
|
|
722
|
-
function regularFile(root, relative) {
|
|
723
|
-
return fileStatus(root, relative) === "regular";
|
|
724
|
-
}
|
|
725
|
-
function localExecutable(root, executable, dependencyOwner = root, environment = {}) {
|
|
726
|
-
const dependencyRoot = path5.resolve(dependencyOwner, "node_modules");
|
|
727
|
-
try {
|
|
728
|
-
const canonicalDependencyRoot = realpathSync3(dependencyRoot);
|
|
729
|
-
for (const name of localExecutableNames(executable, environment)) {
|
|
730
|
-
const unresolved = resolveCandidateName(path5.resolve(dependencyRoot, ".bin"), name, environment.platform ?? process.platform);
|
|
731
|
-
if (!unresolved)
|
|
732
|
-
continue;
|
|
733
|
-
const canonical = realpathSync3(unresolved);
|
|
734
|
-
const stat = lstatSync5(canonical);
|
|
735
|
-
if (canonical !== canonicalDependencyRoot && pathWithin(canonicalDependencyRoot, canonical, environment.platform ?? process.platform) && regularExecutableFile(stat, environment.platform ?? process.platform)) {
|
|
736
|
-
return true;
|
|
151
|
+
try {
|
|
152
|
+
mkdirSync(at, { mode: 448 });
|
|
153
|
+
} catch (mkdirError) {
|
|
154
|
+
if (!object(mkdirError) || mkdirError.code !== "EEXIST")
|
|
155
|
+
throw mkdirError;
|
|
737
156
|
}
|
|
157
|
+
if (!lstatSync(at).isDirectory() || lstatSync(at).isSymbolicLink())
|
|
158
|
+
throw new Error("Unsafe directory");
|
|
738
159
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
return false;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
function pathApi(platform, ...values) {
|
|
745
|
-
if (platform !== "win32")
|
|
746
|
-
return path5.posix;
|
|
747
|
-
if (process.platform === "win32" || values.some((value) => /^[a-z]:[\\/]/iu.test(value) || value.startsWith("\\\\"))) {
|
|
748
|
-
return path5.win32;
|
|
749
|
-
}
|
|
750
|
-
return path5.posix;
|
|
751
|
-
}
|
|
752
|
-
function pathWithin(root, candidate, platform) {
|
|
753
|
-
const platformPath = pathApi(platform, root, candidate);
|
|
754
|
-
const normalizedRoot = platformPath.resolve(root);
|
|
755
|
-
const relative = platformPath.relative(normalizedRoot, platformPath.resolve(candidate));
|
|
756
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${platformPath.sep}`) && !platformPath.isAbsolute(relative);
|
|
757
|
-
}
|
|
758
|
-
function regularExecutableFile(stat, platform) {
|
|
759
|
-
return stat.isFile() && (platform === "win32" || (stat.mode & 73) !== 0);
|
|
760
|
-
}
|
|
761
|
-
function windowsPathExtensions(pathExt) {
|
|
762
|
-
const seen = new Set;
|
|
763
|
-
const extensions = [];
|
|
764
|
-
for (const entry of (pathExt ?? DEFAULT_WINDOWS_PATH_EXT).split(";")) {
|
|
765
|
-
const extension = entry.trim().toLowerCase();
|
|
766
|
-
if (!WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) || seen.has(extension)) {
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
seen.add(extension);
|
|
770
|
-
extensions.push(extension);
|
|
771
|
-
}
|
|
772
|
-
return extensions;
|
|
773
|
-
}
|
|
774
|
-
function runnerNames(command, environment) {
|
|
775
|
-
if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(command))
|
|
776
|
-
return [];
|
|
777
|
-
if ((environment.platform ?? process.platform) !== "win32") {
|
|
778
|
-
return [command];
|
|
779
|
-
}
|
|
780
|
-
const extension = path5.extname(command).toLowerCase();
|
|
781
|
-
if (extension) {
|
|
782
|
-
return WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) ? [command] : [];
|
|
160
|
+
if (existing)
|
|
161
|
+
inspectExisting?.(at, existing);
|
|
783
162
|
}
|
|
784
|
-
return
|
|
785
|
-
}
|
|
786
|
-
function localExecutableNames(executable, environment) {
|
|
787
|
-
if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(executable))
|
|
788
|
-
return [];
|
|
789
|
-
if ((environment.platform ?? process.platform) !== "win32") {
|
|
790
|
-
return [executable];
|
|
791
|
-
}
|
|
792
|
-
const extension = path5.extname(executable).toLowerCase();
|
|
793
|
-
if (extension) {
|
|
794
|
-
return WINDOWS_EXECUTABLE_EXTENSIONS.has(extension) ? [executable] : [];
|
|
795
|
-
}
|
|
796
|
-
return [
|
|
797
|
-
executable,
|
|
798
|
-
...windowsPathExtensions(environment.pathExt).map((candidateExtension) => `${executable}${candidateExtension}`)
|
|
799
|
-
];
|
|
800
|
-
}
|
|
801
|
-
function resolveCandidateName(directory, name, platform) {
|
|
802
|
-
const exact = path5.join(directory, name);
|
|
803
|
-
if (platform !== "win32")
|
|
804
|
-
return exact;
|
|
805
|
-
try {
|
|
806
|
-
const matches = readdirSync3(directory).filter((entry) => entry.toLowerCase() === name.toLowerCase());
|
|
807
|
-
return matches.length === 1 ? path5.join(directory, matches[0]) : null;
|
|
808
|
-
} catch {
|
|
809
|
-
return null;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
function within(root, candidate) {
|
|
813
|
-
return candidate === root || candidate.startsWith(`${root}${path5.sep}`);
|
|
814
|
-
}
|
|
815
|
-
function commonDirectory(left, right, platform = process.platform) {
|
|
816
|
-
const platformPath = pathApi(platform, left, right);
|
|
817
|
-
const leftResolved = platformPath.resolve(left);
|
|
818
|
-
const rightResolved = platformPath.resolve(right);
|
|
819
|
-
const leftRoot = platformPath.parse(leftResolved).root;
|
|
820
|
-
const rightRoot = platformPath.parse(rightResolved).root;
|
|
821
|
-
const comparable = (value) => platform === "win32" ? value.toLowerCase() : value;
|
|
822
|
-
if (comparable(leftRoot) !== comparable(rightRoot))
|
|
823
|
-
return leftRoot;
|
|
824
|
-
const parts = (value, root) => platformPath.relative(root, value).split(platformPath.sep).filter(Boolean);
|
|
825
|
-
const leftParts = parts(leftResolved, leftRoot);
|
|
826
|
-
const rightParts = parts(rightResolved, rightRoot);
|
|
827
|
-
const common = [];
|
|
828
|
-
for (let index = 0;index < Math.min(leftParts.length, rightParts.length); index++) {
|
|
829
|
-
const leftPart = leftParts[index];
|
|
830
|
-
const rightPart = rightParts[index];
|
|
831
|
-
if (comparable(leftPart) !== comparable(rightPart))
|
|
832
|
-
break;
|
|
833
|
-
common.push(leftPart);
|
|
834
|
-
}
|
|
835
|
-
return platformPath.join(leftRoot, ...common);
|
|
836
|
-
}
|
|
837
|
-
function unsafeMountSource(source) {
|
|
838
|
-
if (BROAD_MOUNT_ROOTS.has(source))
|
|
839
|
-
return true;
|
|
840
|
-
if (path5.resolve(source) === path5.parse(path5.resolve(source)).root)
|
|
841
|
-
return true;
|
|
842
|
-
const parts = path5.relative(path5.parse(path5.resolve(source)).root, path5.resolve(source)).split(path5.sep).filter(Boolean).map((part) => part.toLowerCase());
|
|
843
|
-
return parts[0] === "home" && parts.length <= 2 || parts[0] === "users" && parts.length <= 2 || parts[0] === "root" && parts.length <= 1;
|
|
844
|
-
}
|
|
845
|
-
function safeRunnerSource(paths, platform) {
|
|
846
|
-
let source = path5.dirname(paths[0]);
|
|
847
|
-
for (const candidate of paths.slice(1)) {
|
|
848
|
-
source = commonDirectory(source, path5.dirname(candidate), platform);
|
|
849
|
-
}
|
|
850
|
-
if (unsafeMountSource(source))
|
|
851
|
-
return null;
|
|
852
|
-
try {
|
|
853
|
-
const stat = lstatSync5(source);
|
|
854
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
855
|
-
return null;
|
|
856
|
-
} catch {
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
return paths.every((candidate) => pathWithin(source, candidate, platform)) ? source : null;
|
|
860
|
-
}
|
|
861
|
-
function resolvedRunnerEnvironment(environment) {
|
|
862
|
-
return {
|
|
863
|
-
platform: environment.platform ?? process.platform,
|
|
864
|
-
path: environment.path ?? process.env.PATH ?? "",
|
|
865
|
-
pathExt: environment.pathExt ?? process.env.PATHEXT ?? DEFAULT_WINDOWS_PATH_EXT,
|
|
866
|
-
pathDelimiter: environment.pathDelimiter ?? path5.delimiter
|
|
867
|
-
};
|
|
163
|
+
return true;
|
|
868
164
|
}
|
|
869
|
-
function
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const node = resolveVerificationRunner("node", environment);
|
|
874
|
-
if (!node || node.fixedArgs || path5.extname(node.executablePath).toLowerCase() !== ".exe") {
|
|
875
|
-
return null;
|
|
876
|
-
}
|
|
877
|
-
const cli = path5.join(path5.dirname(launcherPath), "node_modules", "npm", "bin", `${name}-cli.js`);
|
|
165
|
+
function readBounded(path, max) {
|
|
166
|
+
if (!safeDirectory(dirname(path)))
|
|
167
|
+
return;
|
|
168
|
+
let fd;
|
|
878
169
|
try {
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return {
|
|
888
|
-
command,
|
|
889
|
-
executablePath: node.executablePath,
|
|
890
|
-
fixedArgs: [canonicalCli]
|
|
891
|
-
};
|
|
892
|
-
} catch {
|
|
893
|
-
return null;
|
|
170
|
+
const st = lstatSync(path);
|
|
171
|
+
if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > max)
|
|
172
|
+
throw new Error("Unsafe or oversized file");
|
|
173
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (absent(error))
|
|
176
|
+
return;
|
|
177
|
+
throw error;
|
|
894
178
|
}
|
|
895
|
-
}
|
|
896
|
-
function localPackageDirectories(dependencyRoot) {
|
|
897
|
-
const packages = [];
|
|
898
179
|
try {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
packages.push(path5.join(candidate, scoped.name));
|
|
910
|
-
}
|
|
911
|
-
}
|
|
180
|
+
const st = fstatSync(fd);
|
|
181
|
+
if (!st.isFile() || st.nlink !== 1 || st.size > max)
|
|
182
|
+
throw new Error("Unsafe or oversized file");
|
|
183
|
+
const buffer = Buffer.alloc(max + 1);
|
|
184
|
+
let size = 0;
|
|
185
|
+
while (size < buffer.length) {
|
|
186
|
+
const n = readSync(fd, buffer, size, buffer.length - size, null);
|
|
187
|
+
if (!n)
|
|
188
|
+
break;
|
|
189
|
+
size += n;
|
|
912
190
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
function localPackageBinTargets(dependencyOwner, executable, platform) {
|
|
919
|
-
const dependencyRoot = path5.resolve(dependencyOwner, "node_modules");
|
|
920
|
-
let canonicalDependencyRoot;
|
|
921
|
-
try {
|
|
922
|
-
canonicalDependencyRoot = realpathSync3(dependencyRoot);
|
|
923
|
-
} catch {
|
|
924
|
-
return [];
|
|
925
|
-
}
|
|
926
|
-
const targets = [];
|
|
927
|
-
for (const packageDirectory of localPackageDirectories(dependencyRoot)) {
|
|
928
|
-
try {
|
|
929
|
-
const directoryStat = lstatSync5(packageDirectory);
|
|
930
|
-
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
|
|
931
|
-
continue;
|
|
932
|
-
}
|
|
933
|
-
const canonicalPackage = realpathSync3(packageDirectory);
|
|
934
|
-
if (!pathWithin(canonicalDependencyRoot, canonicalPackage, platform) || canonicalPackage === canonicalDependencyRoot) {
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
const manifestPath = path5.join(canonicalPackage, "package.json");
|
|
938
|
-
const manifestStat = lstatSync5(manifestPath);
|
|
939
|
-
if (!manifestStat.isFile() || manifestStat.isSymbolicLink())
|
|
940
|
-
continue;
|
|
941
|
-
const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
942
|
-
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
943
|
-
continue;
|
|
944
|
-
}
|
|
945
|
-
const record2 = manifest;
|
|
946
|
-
const packageName = typeof record2.name === "string" ? record2.name.split("/").pop() ?? "" : "";
|
|
947
|
-
const bin = record2.bin;
|
|
948
|
-
let relative;
|
|
949
|
-
if (typeof bin === "string" && (platform === "win32" ? packageName.toLowerCase() === executable.toLowerCase() : packageName === executable)) {
|
|
950
|
-
relative = bin;
|
|
951
|
-
} else if (bin && typeof bin === "object" && !Array.isArray(bin)) {
|
|
952
|
-
const entries = Object.entries(bin).filter(([name]) => platform === "win32" ? name.toLowerCase() === executable.toLowerCase() : name === executable);
|
|
953
|
-
if (entries.length === 1)
|
|
954
|
-
relative = entries[0]?.[1];
|
|
955
|
-
}
|
|
956
|
-
if (typeof relative !== "string" || relative.length === 0 || relative.includes("\x00")) {
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
const unresolved = path5.resolve(canonicalPackage, relative);
|
|
960
|
-
if (!pathWithin(canonicalPackage, unresolved, platform))
|
|
961
|
-
continue;
|
|
962
|
-
const canonical = realpathSync3(unresolved);
|
|
963
|
-
const stat = lstatSync5(canonical);
|
|
964
|
-
if (stat.isFile() && pathWithin(canonicalPackage, canonical, platform) && pathWithin(canonicalDependencyRoot, canonical, platform)) {
|
|
965
|
-
targets.push(canonical);
|
|
966
|
-
}
|
|
967
|
-
} catch {}
|
|
191
|
+
if (size > max)
|
|
192
|
+
throw new Error("Oversized file");
|
|
193
|
+
return buffer.subarray(0, size);
|
|
194
|
+
} finally {
|
|
195
|
+
closeSync(fd);
|
|
968
196
|
}
|
|
969
|
-
return [...new Set(targets)];
|
|
970
197
|
}
|
|
971
|
-
function
|
|
972
|
-
const environment = resolvedRunnerEnvironment(runnerEnvironment);
|
|
973
|
-
if (environment.platform !== "win32")
|
|
974
|
-
return null;
|
|
975
|
-
const targets = localPackageBinTargets(dependencyOwner, executable, environment.platform);
|
|
976
|
-
if (targets.length !== 1)
|
|
977
|
-
return null;
|
|
978
|
-
const target = targets[0];
|
|
979
|
-
const extension = path5.extname(target).toLowerCase();
|
|
980
|
-
if (extension === ".exe") {
|
|
981
|
-
return { command: executable, executablePath: target };
|
|
982
|
-
}
|
|
983
|
-
if (![".js", ".cjs", ".mjs"].includes(extension))
|
|
984
|
-
return null;
|
|
985
|
-
const node = resolveVerificationRunner("node", environment);
|
|
986
|
-
if (!node || node.fixedArgs)
|
|
987
|
-
return null;
|
|
198
|
+
function gitEnvironment() {
|
|
988
199
|
return {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
200
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_"))),
|
|
201
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
202
|
+
LC_ALL: "C"
|
|
992
203
|
};
|
|
993
204
|
}
|
|
994
|
-
function
|
|
995
|
-
const
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (seenDirectories.has(key))
|
|
1004
|
-
return [];
|
|
1005
|
-
seenDirectories.add(key);
|
|
1006
|
-
return runnerNames(command, environment).flatMap((name) => {
|
|
1007
|
-
const candidate = resolveCandidateName(resolvedDirectory, name, environment.platform);
|
|
1008
|
-
return candidate ? [candidate] : [];
|
|
1009
|
-
});
|
|
1010
|
-
})
|
|
1011
|
-
];
|
|
1012
|
-
const seenCandidates = new Set;
|
|
1013
|
-
for (const candidate of candidates) {
|
|
1014
|
-
try {
|
|
1015
|
-
const executablePath = path5.resolve(candidate);
|
|
1016
|
-
const candidateKey = environment.platform === "win32" ? executablePath.toLowerCase() : executablePath;
|
|
1017
|
-
if (seenCandidates.has(candidateKey))
|
|
1018
|
-
continue;
|
|
1019
|
-
seenCandidates.add(candidateKey);
|
|
1020
|
-
const canonical = realpathSync3(executablePath);
|
|
1021
|
-
const stat = lstatSync5(canonical);
|
|
1022
|
-
if (!regularExecutableFile(stat, environment.platform))
|
|
1023
|
-
continue;
|
|
1024
|
-
if (environment.platform === "win32") {
|
|
1025
|
-
const extension = path5.extname(executablePath).toLowerCase();
|
|
1026
|
-
if (extension === ".cmd") {
|
|
1027
|
-
const shim = windowsShimBinding(command, executablePath, environment);
|
|
1028
|
-
if (shim)
|
|
1029
|
-
return shim;
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
if (extension !== ".exe")
|
|
1033
|
-
continue;
|
|
1034
|
-
if (!safeRunnerSource([executablePath, canonical], environment.platform)) {
|
|
1035
|
-
continue;
|
|
1036
|
-
}
|
|
1037
|
-
return { command, executablePath };
|
|
1038
|
-
}
|
|
1039
|
-
const systemVisible = SYSTEM_MOUNT_ROOTS.some((root) => within(root, executablePath) && within(root, canonical));
|
|
1040
|
-
if (systemVisible)
|
|
1041
|
-
return { command, executablePath };
|
|
1042
|
-
const mountSource = commonDirectory(path5.dirname(executablePath), path5.dirname(canonical), environment.platform);
|
|
1043
|
-
if (unsafeMountSource(mountSource))
|
|
1044
|
-
continue;
|
|
1045
|
-
const sourceStat = lstatSync5(mountSource);
|
|
1046
|
-
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink())
|
|
1047
|
-
continue;
|
|
1048
|
-
return { command, executablePath, mountSource };
|
|
1049
|
-
} catch {}
|
|
1050
|
-
}
|
|
1051
|
-
return null;
|
|
1052
|
-
}
|
|
1053
|
-
function packageManifest(root) {
|
|
1054
|
-
if (!regularFile(root, "package.json"))
|
|
1055
|
-
return null;
|
|
1056
|
-
try {
|
|
1057
|
-
const value = JSON.parse(readFileSync3(path5.join(root, "package.json"), "utf8"));
|
|
1058
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1059
|
-
} catch {
|
|
1060
|
-
return null;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
function validatePackageScript(root, verificationId, packageManager, script, approvedCommand, _dependencyOwner = root, runnerEnvironment = {}) {
|
|
1064
|
-
const packageRunner = resolveVerificationRunner(packageManager, runnerEnvironment);
|
|
1065
|
-
if (!packageRunner) {
|
|
1066
|
-
return adapterFailure(verificationId, "runner-missing", `approved package manager ${packageManager} is unavailable or cannot be mounted safely`);
|
|
1067
|
-
}
|
|
1068
|
-
const manifest = packageManifest(root);
|
|
1069
|
-
const scripts = manifest?.scripts;
|
|
1070
|
-
const actual = scripts && typeof scripts === "object" && !Array.isArray(scripts) ? scripts[script] : undefined;
|
|
1071
|
-
if (typeof actual !== "string") {
|
|
1072
|
-
return adapterFailure(verificationId, "script-missing", `package.json script ${script} is missing`);
|
|
1073
|
-
}
|
|
1074
|
-
if (actual !== approvedCommand) {
|
|
1075
|
-
return adapterFailure(verificationId, "script-command-mismatch", `package.json script ${script} differs from the Gate B contract`);
|
|
1076
|
-
}
|
|
1077
|
-
const runtimes = ["node", "bun", "npm", "npx", "pnpm", "yarn"].flatMap((command) => {
|
|
1078
|
-
const binding = resolveVerificationRunner(command, runnerEnvironment);
|
|
1079
|
-
return binding ? [binding] : [];
|
|
205
|
+
function resolveWorkspace(cwd) {
|
|
206
|
+
const actual = realpathSync(cwd);
|
|
207
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
208
|
+
cwd: actual,
|
|
209
|
+
env: gitEnvironment(),
|
|
210
|
+
encoding: "utf8",
|
|
211
|
+
timeout: 3000,
|
|
212
|
+
maxBuffer: 8192,
|
|
213
|
+
windowsHide: true
|
|
1080
214
|
});
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
"
|
|
1086
|
-
|
|
1087
|
-
"
|
|
1088
|
-
|
|
1089
|
-
"
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
"
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
continue;
|
|
1108
|
-
}
|
|
1109
|
-
if (character === "\\" && quote === '"') {
|
|
1110
|
-
active += character;
|
|
1111
|
-
escaped = true;
|
|
1112
|
-
continue;
|
|
1113
|
-
}
|
|
1114
|
-
if (quote) {
|
|
1115
|
-
active += character;
|
|
1116
|
-
if (character === quote)
|
|
1117
|
-
quote = "";
|
|
1118
|
-
} else if (character === '"' || character === "'") {
|
|
1119
|
-
quote = character;
|
|
1120
|
-
active += character;
|
|
1121
|
-
} else if (character === "#" || character === ";" && active.trim() === "") {
|
|
1122
|
-
break;
|
|
1123
|
-
} else if (character === "{" || character === "}" || character === "," && flowDepth > 0) {
|
|
1124
|
-
if (character === "{")
|
|
1125
|
-
flowDepth++;
|
|
1126
|
-
if (character === "}")
|
|
1127
|
-
flowDepth = Math.max(0, flowDepth - 1);
|
|
1128
|
-
parts.push(active);
|
|
1129
|
-
active = "";
|
|
1130
|
-
} else
|
|
1131
|
-
active += character;
|
|
1132
|
-
}
|
|
1133
|
-
parts.push(active);
|
|
1134
|
-
for (const part of parts) {
|
|
1135
|
-
if (part.includes("${"))
|
|
1136
|
-
return true;
|
|
1137
|
-
const directive = part.match(/^\s*(?:"((?:\\.|[^"\\])*)"|'([^']*)'|([^\s=]+))\s*(.*)$/u);
|
|
1138
|
-
if (!directive)
|
|
1139
|
-
continue;
|
|
1140
|
-
const rawKey = directive[1] ?? directive[2] ?? directive[3];
|
|
1141
|
-
if (!rawKey.endsWith(":") && !directive[4])
|
|
1142
|
-
continue;
|
|
1143
|
-
const key = rawKey.replace(/:$/u, "").split(":").at(-1)?.replace(/[-_]/gu, "").toLowerCase();
|
|
1144
|
-
if (key && restricted.has(key))
|
|
1145
|
-
return true;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
return false;
|
|
1149
|
-
}
|
|
1150
|
-
function validateAtomicCapability(root, step, dependencyOwner, runnerEnvironment, executionWritePaths) {
|
|
1151
|
-
if (step.kind === "vitest" && [
|
|
1152
|
-
...step.args,
|
|
1153
|
-
...step.runner.kind === "package-script" ? step.runner.command.split(/\s+/u) : []
|
|
1154
|
-
].some((arg) => /^--(?:reporters?|outputFile)(?:[.=]|$)/u.test(arg))) {
|
|
1155
|
-
return adapterFailure(step.id, "report-arguments-conflict", "Vitest report arguments are owned by the adapter");
|
|
1156
|
-
}
|
|
1157
|
-
if (step.executionBindings) {
|
|
1158
|
-
for (const [relative, expected] of Object.entries(step.executionBindings)) {
|
|
1159
|
-
if (executionWritePaths.includes(relative))
|
|
1160
|
-
continue;
|
|
1161
|
-
const observation = observeSafePath(root, relative);
|
|
1162
|
-
if (expected === null && observation.kind !== "absent" || expected !== null && snapshotFile(root, relative)?.sha256 !== expected) {
|
|
1163
|
-
return adapterFailure(step.id, "verification-config-mismatch", "Bound execution inputs changed; recompile the verification contract");
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
for (const relative of verificationBoundInputPaths(step).filter((file) => VERIFICATION_CONFIGURATION_PATHS.includes(file))) {
|
|
1168
|
-
const observation = observeSafePath(root, relative);
|
|
1169
|
-
if (!["file", "absent"].includes(observation.kind))
|
|
1170
|
-
return adapterFailure(step.id, "verification-config-unsafe", "Execution configuration must be a regular file");
|
|
1171
|
-
if (observation.kind !== "file" || ![
|
|
1172
|
-
".npmrc",
|
|
1173
|
-
".yarnrc",
|
|
1174
|
-
".yarnrc.yml",
|
|
1175
|
-
"bunfig.toml",
|
|
1176
|
-
"pnpm-workspace.yaml"
|
|
1177
|
-
].includes(relative))
|
|
1178
|
-
continue;
|
|
1179
|
-
if (lstatSync5(path5.join(root, relative)).size > 64 * 1024 || executionProfile().mode === "isolated" && unsupportedExecutionConfiguration(readFileSync3(path5.join(root, relative), "utf8"))) {
|
|
1180
|
-
return adapterFailure(step.id, "verification-config-unsafe", "Execution configuration requires unsupported credentials or host configuration");
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
if (step.kind === "package-script") {
|
|
1184
|
-
return validatePackageScript(root, step.id, step.packageManager, step.script, step.command, dependencyOwner, runnerEnvironment);
|
|
1185
|
-
}
|
|
1186
|
-
const runner = step.runner;
|
|
1187
|
-
if (runner.kind === "package-script") {
|
|
1188
|
-
const script = validatePackageScript(root, step.id, runner.packageManager, runner.script, runner.command, dependencyOwner, runnerEnvironment);
|
|
1189
|
-
if (!script.ok)
|
|
1190
|
-
return script;
|
|
1191
|
-
if (step.kind === "vitest" && !localExecutable(root, "vitest", dependencyOwner, runnerEnvironment)) {
|
|
1192
|
-
return adapterFailure(step.id, "local-executable-missing", "Vitest is not installed in consumer node_modules");
|
|
1193
|
-
}
|
|
1194
|
-
return script;
|
|
1195
|
-
}
|
|
1196
|
-
if (runner.kind === "node") {
|
|
1197
|
-
const node = resolveVerificationRunner("node", runnerEnvironment);
|
|
1198
|
-
return node ? capabilitySuccess(step.id, [node]) : adapterFailure(step.id, "runner-missing", "Node is unavailable or cannot be mounted safely");
|
|
1199
|
-
}
|
|
1200
|
-
if (!localExecutable(root, runner.executable, dependencyOwner, runnerEnvironment)) {
|
|
1201
|
-
return adapterFailure(step.id, "local-executable-missing", `local executable ${runner.executable} is unavailable`);
|
|
1202
|
-
}
|
|
1203
|
-
if (runner.kind === "npx") {
|
|
1204
|
-
const npx = resolveVerificationRunner("npx", runnerEnvironment);
|
|
1205
|
-
return npx ? capabilitySuccess(step.id, [npx]) : adapterFailure(step.id, "runner-missing", "npx is unavailable or cannot be mounted safely");
|
|
1206
|
-
}
|
|
1207
|
-
if ((runnerEnvironment.platform ?? process.platform) === "win32") {
|
|
1208
|
-
const binding = resolveLocalVerificationRunner(dependencyOwner, runner.executable, runnerEnvironment);
|
|
1209
|
-
return binding ? capabilitySuccess(step.id, [binding]) : adapterFailure(step.id, "local-executable-missing", `local executable ${runner.executable} has no safe direct Windows binding`);
|
|
1210
|
-
}
|
|
1211
|
-
return capabilitySuccess(step.id);
|
|
1212
|
-
}
|
|
1213
|
-
function validateVerificationAdapterCapability(root, value, options = {}) {
|
|
1214
|
-
return validateCapability(root, value, options, false);
|
|
1215
|
-
}
|
|
1216
|
-
function validateCapability(root, value, options, requireInputAvailability) {
|
|
1217
|
-
const validation = validateVerificationContract(value);
|
|
1218
|
-
if (!validation.ok) {
|
|
1219
|
-
const id = value && typeof value === "object" && !Array.isArray(value) && typeof value.id === "string" ? String(value.id) : undefined;
|
|
1220
|
-
return {
|
|
1221
|
-
ok: false,
|
|
1222
|
-
diagnostic: {
|
|
1223
|
-
kind: "design-readiness",
|
|
1224
|
-
code: "verification-contract-unsupported",
|
|
1225
|
-
...id ? { verificationId: id } : {},
|
|
1226
|
-
message: validation.reason
|
|
1227
|
-
}
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
const dependencyOwner = options.dependencyOwner ?? root;
|
|
1231
|
-
return validateAcceptedVerificationCapability(root, validation.value, dependencyOwner, options.runnerEnvironment ?? {}, requireInputAvailability, options.executionWritePaths ?? []);
|
|
1232
|
-
}
|
|
1233
|
-
function validateAcceptedVerificationCapability(root, verification, dependencyOwner, runnerEnvironment, requireInputAvailability, executionWritePaths) {
|
|
1234
|
-
if (requireInputAvailability) {
|
|
1235
|
-
for (const input of verificationInputPaths(verification)) {
|
|
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);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
const runnerBindings = [];
|
|
1244
|
-
for (const step of verificationSteps(verification)) {
|
|
1245
|
-
const capability = validateAtomicCapability(root, step, dependencyOwner, runnerEnvironment, executionWritePaths);
|
|
1246
|
-
if (!capability.ok)
|
|
1247
|
-
return capability;
|
|
1248
|
-
runnerBindings.push(...capability.runnerBindings);
|
|
1249
|
-
}
|
|
1250
|
-
return capabilitySuccess(verification.id, runnerBindings);
|
|
1251
|
-
}
|
|
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) {
|
|
215
|
+
const topLevel = result.stdout?.replace(process.platform === "win32" ? /\r?\n$/ : /\n$/, "");
|
|
216
|
+
if (result.status === 0 && topLevel)
|
|
217
|
+
return { root: realpathSync(topLevel), cwd: actual };
|
|
218
|
+
if (result.error && !object(result.error))
|
|
219
|
+
throw new Error("Cannot resolve workspace");
|
|
220
|
+
if (result.error && object(result.error) && result.error.code !== "ENOENT")
|
|
221
|
+
throw new Error("Workspace lookup failed");
|
|
222
|
+
if (!result.error && !result.stderr.includes("not a git repository"))
|
|
223
|
+
throw new Error("Git workspace lookup refused");
|
|
224
|
+
return { root: actual, cwd: actual };
|
|
225
|
+
}
|
|
226
|
+
function defaultStateHome() {
|
|
227
|
+
const candidate = process.platform === "win32" ? process.env.LOCALAPPDATA : process.env.XDG_STATE_HOME;
|
|
228
|
+
if (candidate && !isAbsolute(candidate))
|
|
229
|
+
throw new Error("State home must be absolute");
|
|
230
|
+
return candidate ?? join(homedir(), process.platform === "win32" ? "AppData/Local" : ".local/state");
|
|
231
|
+
}
|
|
232
|
+
function recordText(record) {
|
|
233
|
+
return `${JSON.stringify(record, null, 2)}
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
function contentIdentity(text) {
|
|
237
|
+
return createHash("sha256").update(text).digest("hex");
|
|
238
|
+
}
|
|
239
|
+
function canonicalAmbientRoot(path) {
|
|
240
|
+
const full = resolve(path);
|
|
1269
241
|
try {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
242
|
+
return realpathSync(full);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (!absent(error))
|
|
245
|
+
throw error;
|
|
246
|
+
try {
|
|
247
|
+
lstatSync(full);
|
|
248
|
+
throw new Error("Dangling ambient root");
|
|
249
|
+
} catch (statError) {
|
|
250
|
+
if (!absent(statError))
|
|
251
|
+
throw statError;
|
|
252
|
+
}
|
|
253
|
+
const parent = dirname(full);
|
|
254
|
+
if (parent === full)
|
|
255
|
+
throw error;
|
|
256
|
+
return join(canonicalAmbientRoot(parent), full.slice(parent.length));
|
|
1274
257
|
}
|
|
1275
258
|
}
|
|
1276
|
-
function validEnvironmentName(name) {
|
|
1277
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(name);
|
|
1278
|
-
}
|
|
1279
259
|
|
|
1280
|
-
class
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
]);
|
|
260
|
+
class TaskStore {
|
|
261
|
+
root;
|
|
262
|
+
tasks;
|
|
263
|
+
privateRoot;
|
|
264
|
+
constructor(root, stateHome = defaultStateHome()) {
|
|
265
|
+
this.root = realpathSync(root);
|
|
266
|
+
this.tasks = join(this.root, ".cadence", "assistant", "tasks");
|
|
267
|
+
this.privateRoot = join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1", createHash("sha256").update(this.root).digest("hex"));
|
|
268
|
+
const within = relative(this.root, resolve(this.privateRoot));
|
|
269
|
+
if (!within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within))
|
|
270
|
+
throw new Error("Private state home must be outside the working copy");
|
|
271
|
+
}
|
|
272
|
+
get lockPath() {
|
|
273
|
+
return join(this.privateRoot, "checkout.lock");
|
|
274
|
+
}
|
|
275
|
+
inspectPrivate() {
|
|
276
|
+
const namespace = dirname(this.privateRoot);
|
|
277
|
+
for (const path of [namespace, this.privateRoot]) {
|
|
278
|
+
if (!safeDirectory(path))
|
|
279
|
+
continue;
|
|
280
|
+
const st = lstatSync(path);
|
|
281
|
+
if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
282
|
+
throw new Error("Private state permissions refused");
|
|
1312
283
|
}
|
|
1313
284
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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");
|
|
285
|
+
inspectPrivateFile(path, max) {
|
|
286
|
+
this.inspectPrivate();
|
|
287
|
+
if (!safeDirectory(this.privateRoot))
|
|
288
|
+
return;
|
|
289
|
+
try {
|
|
290
|
+
const st = lstatSync(path);
|
|
291
|
+
if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > max || process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
292
|
+
throw new Error("Unsafe private state file");
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (!absent(error))
|
|
295
|
+
throw error;
|
|
1340
296
|
}
|
|
1341
297
|
}
|
|
1342
|
-
|
|
298
|
+
inspectLock() {
|
|
299
|
+
this.inspectPrivateFile(this.lockPath, 8192);
|
|
300
|
+
const data = readBounded(this.lockPath, 8192);
|
|
301
|
+
if (!data)
|
|
302
|
+
return;
|
|
303
|
+
const lock = parseRecord(data);
|
|
304
|
+
if (!object(lock) || lock.schema !== "cadence-assistant-lock/1" || lock.root !== this.root || typeof lock.token !== "string" || !validId(lock.token) || typeof lock.runId !== "string" || !validId(lock.runId) || typeof lock.host !== "string" || !Number.isInteger(lock.pid) || Number(lock.pid) < 1)
|
|
305
|
+
throw new Error("Unknown or corrupt coordination lock");
|
|
306
|
+
return lock;
|
|
307
|
+
}
|
|
308
|
+
acquire() {
|
|
309
|
+
this.inspectPrivate();
|
|
310
|
+
safeDirectory(this.privateRoot, true);
|
|
311
|
+
const lock = {
|
|
312
|
+
schema: "cadence-assistant-lock/1",
|
|
313
|
+
token: randomUUID(),
|
|
314
|
+
runId: randomUUID(),
|
|
315
|
+
pid: process.pid,
|
|
316
|
+
host: hostname(),
|
|
317
|
+
root: this.root
|
|
318
|
+
};
|
|
319
|
+
let fd;
|
|
1343
320
|
try {
|
|
1344
|
-
|
|
1345
|
-
return false;
|
|
1346
|
-
return Boolean(await this.#probe(this.#bwrapPath));
|
|
321
|
+
fd = openSync(this.lockPath, "wx", 384);
|
|
1347
322
|
} catch {
|
|
1348
|
-
|
|
323
|
+
throw new Error("Checkout busy or unresolved lock; inspect and explicitly recover only a dead owner");
|
|
1349
324
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
"
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
"
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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);
|
|
325
|
+
try {
|
|
326
|
+
writeFileSync(fd, JSON.stringify(lock));
|
|
327
|
+
fsyncSync(fd);
|
|
328
|
+
} finally {
|
|
329
|
+
closeSync(fd);
|
|
330
|
+
}
|
|
331
|
+
return lock;
|
|
332
|
+
}
|
|
333
|
+
assertOwner(lock) {
|
|
334
|
+
if (JSON.stringify(this.inspectLock()) !== JSON.stringify(lock))
|
|
335
|
+
throw new Error("Coordination lock changed");
|
|
336
|
+
}
|
|
337
|
+
release(lock) {
|
|
338
|
+
if (lock.pid !== process.pid || lock.host !== hostname())
|
|
339
|
+
throw new Error("Only the owning process may release a lock");
|
|
340
|
+
this.assertOwner(lock);
|
|
341
|
+
unlinkSync(this.lockPath);
|
|
342
|
+
}
|
|
343
|
+
recover(expected, acknowledgeEscapedProcesses) {
|
|
344
|
+
if (!acknowledgeEscapedProcesses || expected.host !== hostname())
|
|
345
|
+
throw new Error("Recovery requires informed local-owner confirmation");
|
|
346
|
+
this.assertOwner(expected);
|
|
347
|
+
try {
|
|
348
|
+
process.kill(expected.pid, 0);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (!object(error) || error.code !== "ESRCH")
|
|
351
|
+
throw new Error("Owner status unknown");
|
|
352
|
+
this.assertOwner(expected);
|
|
353
|
+
unlinkSync(this.lockPath);
|
|
354
|
+
return;
|
|
1418
355
|
}
|
|
1419
|
-
|
|
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());
|
|
356
|
+
throw new Error("Owner is live; stop it first");
|
|
1535
357
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
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;
|
|
358
|
+
load(id) {
|
|
359
|
+
return this.loadSnapshot(id)?.record;
|
|
1575
360
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
"
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
return;
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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);
|
|
361
|
+
loadSnapshot(id) {
|
|
362
|
+
if (!validId(id))
|
|
363
|
+
throw new Error("Invalid task id");
|
|
364
|
+
const directory = join(this.tasks, id);
|
|
365
|
+
if (safeDirectory(directory) && readdirSync(directory).some((name) => name !== "task.json"))
|
|
366
|
+
throw new Error("Unknown task directory content; preserved without reset");
|
|
367
|
+
const data = readBounded(join(this.tasks, id, "task.json"), MAX_RECORD_BYTES);
|
|
368
|
+
if (!data)
|
|
369
|
+
return;
|
|
370
|
+
const record = decodeTask(parseRecord(data));
|
|
371
|
+
if (record.id !== id)
|
|
372
|
+
throw new Error("Task identity mismatch");
|
|
373
|
+
return { record, identity: contentIdentity(data) };
|
|
374
|
+
}
|
|
375
|
+
list() {
|
|
376
|
+
if (!safeDirectory(this.tasks))
|
|
377
|
+
return [];
|
|
378
|
+
const entries = readdirSync(this.tasks);
|
|
379
|
+
if (entries.length > 1000)
|
|
380
|
+
throw new Error("Task listing limit exceeded");
|
|
381
|
+
return entries.map((id) => {
|
|
382
|
+
const task = this.load(id);
|
|
383
|
+
if (!task)
|
|
384
|
+
throw new Error("Unknown task directory");
|
|
385
|
+
return task;
|
|
386
|
+
});
|
|
1642
387
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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;
|
|
388
|
+
save(record, expectedRevision, held, expectedIdentity) {
|
|
389
|
+
const next = decodeTask(record);
|
|
390
|
+
if (next.revision !== expectedRevision + 1)
|
|
391
|
+
throw new Error("Revision conflict");
|
|
392
|
+
const text = recordText(next);
|
|
393
|
+
if (Buffer.byteLength(text) > MAX_RECORD_BYTES)
|
|
394
|
+
throw new Error("Task record limit");
|
|
395
|
+
const lock = held ?? this.acquire();
|
|
396
|
+
if (lock.pid !== process.pid || lock.host !== hostname())
|
|
397
|
+
throw new Error("Not the lock owner");
|
|
1660
398
|
try {
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
};
|
|
399
|
+
this.assertOwner(lock);
|
|
400
|
+
const current = this.loadSnapshot(next.id);
|
|
401
|
+
if ((current?.record.revision ?? 0) !== expectedRevision)
|
|
402
|
+
throw new Error("Revision conflict");
|
|
403
|
+
if (current?.identity !== expectedIdentity)
|
|
404
|
+
throw new Error("Task content changed; reopen it");
|
|
405
|
+
const dir = join(this.tasks, next.id);
|
|
406
|
+
safeDirectory(dir, true);
|
|
407
|
+
if (!current && readdirSync(dir).length)
|
|
408
|
+
throw new Error("Unknown task directory");
|
|
409
|
+
const temporary = join(dir, `task.${randomUUID()}.tmp`);
|
|
410
|
+
const fd = openSync(temporary, "wx", 384);
|
|
1699
411
|
try {
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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;
|
|
412
|
+
writeFileSync(fd, text);
|
|
413
|
+
fsyncSync(fd);
|
|
414
|
+
} finally {
|
|
415
|
+
closeSync(fd);
|
|
1713
416
|
}
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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();
|
|
417
|
+
this.assertOwner(lock);
|
|
418
|
+
const latest = readBounded(join(dir, "task.json"), MAX_RECORD_BYTES);
|
|
419
|
+
if ((latest ? contentIdentity(latest) : undefined) !== expectedIdentity)
|
|
420
|
+
throw new Error("Task content changed; replacement preserved without overwrite");
|
|
421
|
+
renameSync(temporary, join(dir, "task.json"));
|
|
422
|
+
return next;
|
|
423
|
+
} finally {
|
|
424
|
+
if (!held)
|
|
425
|
+
this.release(lock);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
checks() {
|
|
429
|
+
const data = readBounded(join(this.root, ".cadence", "assistant", "config.json"), 32768);
|
|
430
|
+
if (!data)
|
|
431
|
+
return [];
|
|
432
|
+
const value = parseRecord(data);
|
|
433
|
+
if (!object(value) || value.schema !== TASK_CONFIG_SCHEMA || !Array.isArray(value.checks) || value.checks.length > 32 || Object.keys(value).some((key) => !["schema", "checks"].includes(key)))
|
|
434
|
+
throw new Error("Unknown check configuration");
|
|
435
|
+
const checks = value.checks.map(decodeCheck);
|
|
436
|
+
if (new Set(checks.map((check) => check.name)).size !== checks.length || checks.some((check) => check.name === "Manual command"))
|
|
437
|
+
throw new Error("Duplicate or reserved check name");
|
|
438
|
+
return checks;
|
|
439
|
+
}
|
|
440
|
+
readLog(name) {
|
|
441
|
+
if (!/^[a-f0-9-]{36}\.log$/.test(name))
|
|
442
|
+
throw new Error("Invalid diagnostic name");
|
|
443
|
+
this.inspectPrivateFile(join(this.privateRoot, name), 65536);
|
|
444
|
+
const data = readBounded(join(this.privateRoot, name), 65536);
|
|
445
|
+
return data ? diagnosticText(data.toString("utf8"), 65536) : "Private diagnostic unavailable";
|
|
446
|
+
}
|
|
447
|
+
writeLog(lock, output) {
|
|
448
|
+
if (lock.pid !== process.pid || lock.host !== hostname())
|
|
449
|
+
throw new Error("Not the lock owner");
|
|
450
|
+
this.assertOwner(lock);
|
|
451
|
+
const name = `${randomUUID()}.log`;
|
|
452
|
+
writeFileSync(join(this.privateRoot, name), diagnosticText(output, 65536), {
|
|
453
|
+
flag: "wx",
|
|
454
|
+
mode: 384
|
|
1769
455
|
});
|
|
456
|
+
return name;
|
|
1770
457
|
}
|
|
1771
458
|
}
|
|
1772
|
-
|
|
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
|
-
}
|
|
459
|
+
var TASK_CONFIG_SCHEMA = "cadence-assistant-checks/1";
|
|
1795
460
|
|
|
1796
461
|
// src/operator-tools.ts
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
function packageManagerSelection(root, declaration) {
|
|
1804
|
-
const managers = Object.keys(PACKAGE_LOCKS);
|
|
1805
|
-
const lockfiles = managers.flatMap((manager) => PACKAGE_LOCKS[manager].filter((file) => lstatSync8(path8.join(root, file), { throwIfNoEntry: false })?.isFile()));
|
|
1806
|
-
const inferred = managers.filter((manager) => PACKAGE_LOCKS[manager].some((file) => lockfiles.includes(file)));
|
|
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;
|
|
1808
|
-
const selectedPackageManager = declaration !== undefined ? declared ?? null : inferred.length === 1 ? inferred[0] ?? null : inferred.length > 1 ? null : "npm";
|
|
1809
|
-
const selectionSource = declaration !== undefined ? declared ? "package.json" : "invalid-declaration" : inferred.length === 1 ? "lockfile" : inferred.length > 1 ? "ambiguous" : "default";
|
|
462
|
+
function inspectConsumer(cwd) {
|
|
463
|
+
const workspace = resolveWorkspace(cwd);
|
|
464
|
+
const store = new TaskStore(workspace.root);
|
|
465
|
+
const tasks = store.list();
|
|
466
|
+
const checks = store.checks();
|
|
467
|
+
const lock = store.inspectLock();
|
|
1810
468
|
return {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
} catch (error) {
|
|
1824
|
-
checks.push({
|
|
1825
|
-
name,
|
|
1826
|
-
ok: false,
|
|
1827
|
-
detail: error instanceof Error ? error.message : "unavailable",
|
|
1828
|
-
nextStep
|
|
1829
|
-
});
|
|
1830
|
-
}
|
|
469
|
+
ok: true,
|
|
470
|
+
schema: "cadence-assistant-doctor/1",
|
|
471
|
+
root: workspace.root,
|
|
472
|
+
cwd: workspace.cwd,
|
|
473
|
+
taskCount: tasks.length,
|
|
474
|
+
checks: checks.map(({ name, timeoutMs }) => ({
|
|
475
|
+
name: diagnosticText(name),
|
|
476
|
+
timeoutMs
|
|
477
|
+
})),
|
|
478
|
+
checkStatus: "not-run",
|
|
479
|
+
coordination: lock ? "locked; inspect in the terminal workflow" : "unlocked",
|
|
480
|
+
note: "Observation only. Missing state stays absent. Old history is not imported. External and older writers are not contained."
|
|
1831
481
|
};
|
|
1832
|
-
check("node", () => {
|
|
1833
|
-
if (Number(process.versions.node.split(".")[0]) < 22 || Number(process.versions.node.split(".")[0]) === 22 && Number(process.versions.node.split(".")[1]) < 13)
|
|
1834
|
-
throw new Error("Node 22.13+ required");
|
|
1835
|
-
return process.versions.node;
|
|
1836
|
-
}, "Install Node >=22.13.");
|
|
1837
|
-
check("git", () => {
|
|
1838
|
-
const result = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
1839
|
-
cwd: root,
|
|
1840
|
-
encoding: "utf8",
|
|
1841
|
-
timeout: 5000,
|
|
1842
|
-
maxBuffer: 65536
|
|
1843
|
-
});
|
|
1844
|
-
if (result.status !== 0)
|
|
1845
|
-
throw new Error("git-repository-unavailable");
|
|
1846
|
-
return result.stdout.trim();
|
|
1847
|
-
}, "Install Git and initialize the consumer repository.");
|
|
1848
|
-
check("openspec", () => {
|
|
1849
|
-
resolveOpenSpecInvocation(root);
|
|
1850
|
-
return "resolved (protocol not executed)";
|
|
1851
|
-
}, "Install @fission-ai/openspec@1.5.0, or configure ABEL_OPENSPEC_PACKAGE_ROOT / ABEL_OPENSPEC_NODE before launching the host.");
|
|
1852
|
-
check("execution", () => {
|
|
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
|
-
}
|
|
1859
|
-
if (process.platform !== "linux")
|
|
1860
|
-
throw new Error("linux-pid-isolation-required");
|
|
1861
|
-
const result = spawnSync2(profile.bwrapPath, [
|
|
1862
|
-
"--ro-bind",
|
|
1863
|
-
"/",
|
|
1864
|
-
"/",
|
|
1865
|
-
profile.mode === "local-trusted" ? "--unshare-pid" : "--unshare-all",
|
|
1866
|
-
"--",
|
|
1867
|
-
"/bin/true"
|
|
1868
|
-
], { timeout: 5000, encoding: "utf8", maxBuffer: 65536 });
|
|
1869
|
-
if (result.status !== 0)
|
|
1870
|
-
throw new Error("bubblewrap-probe-failed");
|
|
1871
|
-
return `${profile.mode}: Bubblewrap PID namespace probe passed`;
|
|
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).");
|
|
1873
|
-
check("request-budgets", () => JSON.stringify(requestBounds()), "Set ABEL_FIRST_PROGRESS_MS and ABEL_STREAM_IDLE_MS to positive milliseconds <=1200000.");
|
|
1874
|
-
check("state-root", () => resolveStateRoot({
|
|
1875
|
-
consumerRoot: root,
|
|
1876
|
-
xdgStateHome: process.env.XDG_STATE_HOME
|
|
1877
|
-
}).rootDir, "Use an absolute repository-external XDG_STATE_HOME.");
|
|
1878
|
-
check("workspace-dependencies", () => {
|
|
1879
|
-
const dependencies = path8.join(root, "node_modules");
|
|
1880
|
-
if (!lstatSync8(dependencies, { throwIfNoEntry: false }))
|
|
1881
|
-
return "no node_modules; prepare dependencies before verification";
|
|
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]);
|
|
1883
|
-
if (names.length > 20000)
|
|
1884
|
-
throw new Error("dependency-view-too-large");
|
|
1885
|
-
let links = 0;
|
|
1886
|
-
for (const name of names) {
|
|
1887
|
-
const file = path8.join(dependencies, name);
|
|
1888
|
-
if (!lstatSync8(file).isSymbolicLink())
|
|
1889
|
-
continue;
|
|
1890
|
-
const resolved = realpathSync4(file);
|
|
1891
|
-
const relative = path8.relative(root, resolved);
|
|
1892
|
-
if (relative === ".." || relative.startsWith(`..${path8.sep}`) || path8.isAbsolute(relative))
|
|
1893
|
-
throw new Error("dependency-link-outside-consumer");
|
|
1894
|
-
if (!resolved.startsWith(`${dependencies}${path8.sep}`))
|
|
1895
|
-
links++;
|
|
1896
|
-
}
|
|
1897
|
-
return `${links} repository workspace link(s); their targets must be included in the candidate baseline`;
|
|
1898
|
-
}, "Repair dangling/external workspace links and ensure local package sources are tracked or admitted in the candidate.");
|
|
1899
|
-
const manifest = path8.join(root, "package.json");
|
|
1900
|
-
let selection = packageManagerSelection(root, undefined);
|
|
1901
|
-
if (lstatSync8(manifest, { throwIfNoEntry: false })?.isFile()) {
|
|
1902
|
-
check("manifest", () => {
|
|
1903
|
-
const parsed2 = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
1904
|
-
if (!parsed2 || typeof parsed2 !== "object")
|
|
1905
|
-
throw new Error("manifest-invalid");
|
|
1906
|
-
return Object.keys(parsed2.scripts ?? {}).join(", ") || "no package scripts";
|
|
1907
|
-
}, "Repair package.json.");
|
|
1908
|
-
const parsed = (() => {
|
|
1909
|
-
try {
|
|
1910
|
-
const value = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
1911
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1912
|
-
} catch {
|
|
1913
|
-
return {};
|
|
1914
|
-
}
|
|
1915
|
-
})();
|
|
1916
|
-
selection = packageManagerSelection(root, parsed.packageManager);
|
|
1917
|
-
check("package-manager", () => {
|
|
1918
|
-
if (!selection.selectedPackageManager)
|
|
1919
|
-
throw new Error(`package-manager-${selection.selectionSource}`);
|
|
1920
|
-
return `${selection.selectedPackageManager} (${selection.selectionSource})`;
|
|
1921
|
-
}, "Declare the intended packageManager in package.json; doctor does not read private plans, install runners or remove lockfiles.");
|
|
1922
|
-
for (const script of ["test", "check", "build"].filter((name) => typeof parsed.scripts?.[name] === "string")) {
|
|
1923
|
-
check(`script:${script}`, () => {
|
|
1924
|
-
const manager = selection.selectedPackageManager;
|
|
1925
|
-
if (!manager)
|
|
1926
|
-
throw new Error(`package-manager-${selection.selectionSource}`);
|
|
1927
|
-
const result = validateVerificationAdapterCapability(root, {
|
|
1928
|
-
kind: "package-script",
|
|
1929
|
-
id: `doctor-${script}`,
|
|
1930
|
-
packageManager: manager,
|
|
1931
|
-
script,
|
|
1932
|
-
command: parsed.scripts[script],
|
|
1933
|
-
args: [],
|
|
1934
|
-
classification: "expected-green"
|
|
1935
|
-
});
|
|
1936
|
-
if (!result.ok)
|
|
1937
|
-
throw new Error(`${manager}:${result.diagnostic.code}`);
|
|
1938
|
-
return "statically admitted; tests not executed";
|
|
1939
|
-
}, "Check the runner, installed dependencies and execution configuration; this probe does not execute your tests.");
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
return { root, ...selection, ok: checks.every((entry) => entry.ok), checks };
|
|
1943
482
|
}
|
|
1944
|
-
function
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
483
|
+
function inspectRuns(cwd) {
|
|
484
|
+
const workspace = resolveWorkspace(cwd);
|
|
485
|
+
const store = new TaskStore(workspace.root);
|
|
486
|
+
return {
|
|
487
|
+
ok: true,
|
|
488
|
+
schema: "cadence-assistant-history/1",
|
|
489
|
+
root: workspace.root,
|
|
490
|
+
tasks: store.list().map((task) => ({
|
|
491
|
+
id: task.id,
|
|
492
|
+
kind: task.kind,
|
|
493
|
+
phase: task.phase === "running" ? "interrupted" : task.phase,
|
|
494
|
+
goal: diagnosticText(task.goal, 512),
|
|
495
|
+
revision: task.revision,
|
|
496
|
+
checks: task.checks.map(({ name, status, exitCode, durationMs }) => ({
|
|
497
|
+
name: diagnosticText(name, 128),
|
|
498
|
+
status,
|
|
499
|
+
exitCode,
|
|
500
|
+
durationMs
|
|
501
|
+
})),
|
|
502
|
+
review: task.review?.outcome ?? "not-reviewed"
|
|
503
|
+
})),
|
|
504
|
+
note: "Context only; no work resumed. Stored check outcomes are historical, not a current freshness verification. Legacy stores remain untouched."
|
|
1965
505
|
};
|
|
1966
|
-
visit(root);
|
|
1967
|
-
return { entries, bytes, truncated };
|
|
1968
|
-
}
|
|
1969
|
-
function inspectRuns(root) {
|
|
1970
|
-
const state = resolveStateRoot({
|
|
1971
|
-
consumerRoot: root,
|
|
1972
|
-
xdgStateHome: process.env.XDG_STATE_HOME
|
|
1973
|
-
});
|
|
1974
|
-
const usage = storageUsage(state.rootDir);
|
|
1975
|
-
if (!lstatSync8(state.databasePath, { throwIfNoEntry: false }))
|
|
1976
|
-
return { stateRoot: state.rootDir, usage, runs: [], truncated: false };
|
|
1977
|
-
const db = new DatabaseSync(state.databasePath, { readOnly: true });
|
|
1978
|
-
try {
|
|
1979
|
-
const rows = db.prepare("SELECT run_id, stage, change_name, state, sequence FROM runs ORDER BY run_id LIMIT 1001").all();
|
|
1980
|
-
return {
|
|
1981
|
-
stateRoot: state.rootDir,
|
|
1982
|
-
usage,
|
|
1983
|
-
runs: rows.slice(0, 1000),
|
|
1984
|
-
truncated: rows.length > 1000
|
|
1985
|
-
};
|
|
1986
|
-
} finally {
|
|
1987
|
-
db.close();
|
|
1988
|
-
}
|
|
1989
506
|
}
|
|
1990
507
|
|
|
1991
508
|
// src/operator-cli-entry.mjs
|
|
1992
|
-
var [command = "doctor", root = process.cwd()] = process.argv.slice(2);
|
|
1993
509
|
try {
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
510
|
+
const [command = "doctor", root = process.cwd(), ...extra] = process.argv.slice(2);
|
|
511
|
+
if (!["doctor", "runs"].includes(command) || extra.length)
|
|
512
|
+
throw new Error("usage");
|
|
513
|
+
const result = command === "doctor" ? inspectConsumer(path.resolve(root)) : inspectRuns(path.resolve(root));
|
|
1997
514
|
console.log(JSON.stringify(result, null, 2));
|
|
1998
|
-
|
|
1999
|
-
process.exitCode = 1;
|
|
2000
|
-
} catch (error) {
|
|
515
|
+
} catch {
|
|
2001
516
|
console.error(JSON.stringify({
|
|
2002
517
|
ok: false,
|
|
2003
|
-
error:
|
|
518
|
+
error: "operator-refused",
|
|
519
|
+
usage: "node <cadence>/src/operator-cli.mjs doctor|runs [workspace]"
|
|
2004
520
|
}));
|
|
2005
521
|
process.exitCode = 1;
|
|
2006
522
|
}
|