@abelxiaoxing/cadence 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -8
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/config/plan-draft.quick.example.json +78 -0
- package/package.json +10 -4
- package/prompts/abel-design.md +101 -44
- package/prompts/abel-implement.md +9 -0
- package/src/activity-contracts.ts +82 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +31 -11
- package/src/candidate-workspace.ts +74 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +5 -0
- package/src/change-verification.ts +920 -0
- package/src/child-budget.ts +58 -0
- package/src/child-model.ts +142 -0
- package/src/child-session.ts +289 -326
- package/src/contracts.ts +200 -76
- package/src/delivery-compiler.ts +105 -120
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +113 -4
- package/src/design-diagnostics.ts +150 -0
- package/src/durable-contracts.ts +213 -0
- package/src/durable-workflow.ts +148 -3570
- package/src/evidence-draft.ts +122 -0
- package/src/execution-profile.ts +38 -0
- package/src/implement-graph.ts +3 -0
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +59 -522
- package/src/isolation-backend.ts +62 -40
- package/src/model-source.ts +22 -0
- package/src/operator-cli-entry.mjs +24 -0
- package/src/operator-cli.mjs +1421 -0
- package/src/operator-tools.ts +249 -0
- package/src/package-candidate.ts +227 -0
- package/src/package-verification.ts +86 -8
- package/src/package-workflow.ts +291 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +149 -0
- package/src/plan-draft.ts +493 -0
- package/src/single-task-draft.ts +142 -0
- package/src/subagent-activity.ts +32 -66
- package/src/submit-tool.ts +20 -59
- package/src/transport-budget.ts +52 -0
- package/src/verification-capability.ts +5 -3
- package/src/verification-diagnostics.ts +76 -0
- package/src/verification-environment-io.ts +280 -0
- package/src/verification-environment.ts +56 -154
- package/src/verification-scan.ts +64 -0
- package/src/worker-broker.ts +39 -57
- package/src/workflow-policy.ts +14 -53
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +105 -497
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +19 -1
- package/src/workspace-io-worker.mjs +275 -50
- package/src/workspace-io.ts +6 -1
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constants,
|
|
3
|
+
cpSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import type { ExecutionProfile } from "./execution-profile.ts";
|
|
14
|
+
import type { IsolationMount } from "./isolation-backend.ts";
|
|
15
|
+
import type { VerificationRunnerBinding } from "./verification-capability.ts";
|
|
16
|
+
|
|
17
|
+
const CACHES = [".vite", ".vite-temp"];
|
|
18
|
+
function within(root: string, candidate: string): boolean {
|
|
19
|
+
const relative = path.relative(root, candidate);
|
|
20
|
+
return (
|
|
21
|
+
relative !== ".." &&
|
|
22
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
23
|
+
!path.isAbsolute(relative)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A disposable dependency view: immutable packages and independently writable caches. */
|
|
28
|
+
export function prepareVerificationEnvironmentIo(
|
|
29
|
+
root: string,
|
|
30
|
+
dependencyOwner: string,
|
|
31
|
+
runnerBindings: readonly VerificationRunnerBinding[],
|
|
32
|
+
profile: ExecutionProfile,
|
|
33
|
+
environment: NodeJS.ProcessEnv,
|
|
34
|
+
checkCancelled: () => void,
|
|
35
|
+
privateRoot: string,
|
|
36
|
+
) {
|
|
37
|
+
const cleanup = () => rmSync(privateRoot, { recursive: true, force: true });
|
|
38
|
+
try {
|
|
39
|
+
const local = profile.mode === "local-trusted";
|
|
40
|
+
if (local && realpathSync(root) === realpathSync(dependencyOwner))
|
|
41
|
+
throw new Error("trusted-candidate-required");
|
|
42
|
+
const mounts: IsolationMount[] = [
|
|
43
|
+
{ source: privateRoot, target: "/cadence", writable: true },
|
|
44
|
+
];
|
|
45
|
+
for (const directory of [
|
|
46
|
+
"home",
|
|
47
|
+
"cache",
|
|
48
|
+
"tmp",
|
|
49
|
+
"reports",
|
|
50
|
+
"dependencies",
|
|
51
|
+
]) {
|
|
52
|
+
mkdirSync(path.join(privateRoot, directory), { mode: 0o700 });
|
|
53
|
+
}
|
|
54
|
+
writeFileSync(path.join(privateRoot, "home/user.npmrc"), "");
|
|
55
|
+
writeFileSync(path.join(privateRoot, "home/global.npmrc"), "");
|
|
56
|
+
const dependencies = path.join(dependencyOwner, "node_modules");
|
|
57
|
+
const stat = lstatSync(dependencies, { throwIfNoEntry: false });
|
|
58
|
+
if (stat) {
|
|
59
|
+
const target = path.join(root, "node_modules");
|
|
60
|
+
const targetStat = lstatSync(target, { throwIfNoEntry: false });
|
|
61
|
+
if (
|
|
62
|
+
!stat.isDirectory() ||
|
|
63
|
+
stat.isSymbolicLink() ||
|
|
64
|
+
(targetStat &&
|
|
65
|
+
(!targetStat.isDirectory() || targetStat.isSymbolicLink()))
|
|
66
|
+
) {
|
|
67
|
+
throw new Error("dependency-path-unsafe");
|
|
68
|
+
}
|
|
69
|
+
if (!targetStat) mkdirSync(target, { mode: 0o700 });
|
|
70
|
+
const view = path.join(privateRoot, "dependencies");
|
|
71
|
+
mounts.push({ source: view, target: "/workspace/node_modules" });
|
|
72
|
+
const entries = readdirSync(dependencies);
|
|
73
|
+
if (entries.length > 20_000) throw new Error("dependency-view-too-large");
|
|
74
|
+
const canonical = realpathSync(dependencies);
|
|
75
|
+
let viewEntries = 0;
|
|
76
|
+
const projectEntry = (name: string) => {
|
|
77
|
+
checkCancelled();
|
|
78
|
+
if (++viewEntries > 20_000)
|
|
79
|
+
throw new Error("dependency-view-too-large");
|
|
80
|
+
if (CACHES.includes(name)) return;
|
|
81
|
+
const source = path.join(dependencies, name);
|
|
82
|
+
const placeholder = path.join(view, name);
|
|
83
|
+
const entry = lstatSync(source);
|
|
84
|
+
if (entry.isSymbolicLink()) {
|
|
85
|
+
const resolved = realpathSync(source);
|
|
86
|
+
if (within(canonical, resolved)) {
|
|
87
|
+
symlinkSync(
|
|
88
|
+
path.relative(path.dirname(source), resolved),
|
|
89
|
+
placeholder,
|
|
90
|
+
);
|
|
91
|
+
} else if (within(realpathSync(dependencyOwner), resolved)) {
|
|
92
|
+
const relative = path.relative(
|
|
93
|
+
realpathSync(dependencyOwner),
|
|
94
|
+
resolved,
|
|
95
|
+
);
|
|
96
|
+
const candidate = path.join(root, relative);
|
|
97
|
+
if (!lstatSync(candidate, { throwIfNoEntry: false }))
|
|
98
|
+
throw new Error("workspace-dependency-missing");
|
|
99
|
+
symlinkSync(
|
|
100
|
+
local
|
|
101
|
+
? candidate
|
|
102
|
+
: `/workspace/${relative.split(path.sep).join("/")}`,
|
|
103
|
+
placeholder,
|
|
104
|
+
);
|
|
105
|
+
} else throw new Error("dependency-path-unsafe");
|
|
106
|
+
} else if (
|
|
107
|
+
entry.isDirectory() &&
|
|
108
|
+
name.startsWith("@") &&
|
|
109
|
+
!name.includes(path.sep)
|
|
110
|
+
) {
|
|
111
|
+
mkdirSync(placeholder);
|
|
112
|
+
for (const child of readdirSync(source))
|
|
113
|
+
projectEntry(path.join(name, child));
|
|
114
|
+
} else {
|
|
115
|
+
if (entry.isDirectory()) mkdirSync(placeholder);
|
|
116
|
+
else if (entry.isFile()) writeFileSync(placeholder, "");
|
|
117
|
+
else throw new Error("dependency-path-unsafe");
|
|
118
|
+
mounts.push({
|
|
119
|
+
source,
|
|
120
|
+
target: `/workspace/node_modules/${name.split(path.sep).join("/")}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
for (const name of entries) projectEntry(name);
|
|
125
|
+
for (const cache of CACHES) {
|
|
126
|
+
mkdirSync(path.join(view, cache));
|
|
127
|
+
const source = path.join(privateRoot, "cache", cache);
|
|
128
|
+
mkdirSync(source);
|
|
129
|
+
mounts.push({
|
|
130
|
+
source,
|
|
131
|
+
target: `/workspace/node_modules/${cache}`,
|
|
132
|
+
writable: true,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (local && stat) {
|
|
137
|
+
// Private copies keep package caches/writes away from consumer dependencies.
|
|
138
|
+
const copyEntry = (name: string) => {
|
|
139
|
+
checkCancelled();
|
|
140
|
+
const source = path.join(dependencies, name);
|
|
141
|
+
const destination = path.join(root, "node_modules", name);
|
|
142
|
+
if (CACHES.includes(name)) {
|
|
143
|
+
mkdirSync(destination, { recursive: true });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
removeEntry(destination, checkCancelled);
|
|
147
|
+
const stat = lstatSync(source);
|
|
148
|
+
if (stat.isSymbolicLink()) {
|
|
149
|
+
const resolved = realpathSync(source);
|
|
150
|
+
const translated = within(canonicalRoot(dependencyOwner), resolved)
|
|
151
|
+
? path.join(
|
|
152
|
+
root,
|
|
153
|
+
path.relative(canonicalRoot(dependencyOwner), resolved),
|
|
154
|
+
)
|
|
155
|
+
: resolved;
|
|
156
|
+
symlinkSync(translated, destination);
|
|
157
|
+
} else if (
|
|
158
|
+
stat.isDirectory() &&
|
|
159
|
+
name.startsWith("@") &&
|
|
160
|
+
!name.includes(path.sep)
|
|
161
|
+
) {
|
|
162
|
+
mkdirSync(destination);
|
|
163
|
+
for (const child of readdirSync(source))
|
|
164
|
+
copyEntry(path.join(name, child));
|
|
165
|
+
} else
|
|
166
|
+
cpSync(source, destination, {
|
|
167
|
+
recursive: true,
|
|
168
|
+
filter: () => {
|
|
169
|
+
checkCancelled();
|
|
170
|
+
return true;
|
|
171
|
+
},
|
|
172
|
+
mode: constants.COPYFILE_FICLONE,
|
|
173
|
+
verbatimSymlinks: true,
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
for (const name of readdirSync(path.join(privateRoot, "dependencies")))
|
|
177
|
+
copyEntry(name);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sources = new Map<string, string>();
|
|
181
|
+
const bindings = runnerBindings.map((binding) => {
|
|
182
|
+
if (local || !binding.mountSource) return { ...binding };
|
|
183
|
+
const source = path.resolve(binding.mountSource);
|
|
184
|
+
const stat = lstatSync(source);
|
|
185
|
+
const executable = realpathSync(binding.executablePath);
|
|
186
|
+
if (
|
|
187
|
+
!stat.isDirectory() ||
|
|
188
|
+
stat.isSymbolicLink() ||
|
|
189
|
+
!within(source, executable)
|
|
190
|
+
) {
|
|
191
|
+
throw new Error("runner-mount-unavailable");
|
|
192
|
+
}
|
|
193
|
+
let target = sources.get(source);
|
|
194
|
+
if (!target) {
|
|
195
|
+
target = `/cadence-runners/${sources.size}`;
|
|
196
|
+
sources.set(source, target);
|
|
197
|
+
mounts.push({ source, target });
|
|
198
|
+
}
|
|
199
|
+
const translate = (file: string) =>
|
|
200
|
+
path.posix.join(target, ...path.relative(source, file).split(path.sep));
|
|
201
|
+
return {
|
|
202
|
+
...binding,
|
|
203
|
+
executablePath: translate(executable),
|
|
204
|
+
...(binding.fixedArgs
|
|
205
|
+
? {
|
|
206
|
+
fixedArgs: binding.fixedArgs.map((arg) =>
|
|
207
|
+
path.isAbsolute(arg) && within(source, arg)
|
|
208
|
+
? translate(arg)
|
|
209
|
+
: arg,
|
|
210
|
+
),
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
checkCancelled();
|
|
216
|
+
return {
|
|
217
|
+
privateRoot,
|
|
218
|
+
mounts,
|
|
219
|
+
bindings,
|
|
220
|
+
reportRoot: local ? privateRoot : "/cadence",
|
|
221
|
+
environment: {
|
|
222
|
+
...(local
|
|
223
|
+
? Object.fromEntries(
|
|
224
|
+
profile.inheritEnvironment.flatMap((name) =>
|
|
225
|
+
environment[name] === undefined
|
|
226
|
+
? []
|
|
227
|
+
: [[name, environment[name] ?? ""]],
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
: {}),
|
|
231
|
+
CI: "1",
|
|
232
|
+
HOME: local ? path.join(privateRoot, "home") : "/cadence/home",
|
|
233
|
+
XDG_CACHE_HOME: local
|
|
234
|
+
? path.join(privateRoot, "cache")
|
|
235
|
+
: "/cadence/cache",
|
|
236
|
+
TMPDIR: local ? path.join(privateRoot, "tmp") : "/cadence/tmp",
|
|
237
|
+
npm_config_cache: local
|
|
238
|
+
? path.join(privateRoot, "cache/npm")
|
|
239
|
+
: "/cadence/cache/npm",
|
|
240
|
+
npm_config_userconfig: local
|
|
241
|
+
? path.join(privateRoot, "home/user.npmrc")
|
|
242
|
+
: "/cadence/home/user.npmrc",
|
|
243
|
+
npm_config_globalconfig: local
|
|
244
|
+
? path.join(privateRoot, "home/global.npmrc")
|
|
245
|
+
: "/cadence/home/global.npmrc",
|
|
246
|
+
PATH: [
|
|
247
|
+
...new Set(
|
|
248
|
+
// Honor explicitly resolved private runtimes before system fallbacks.
|
|
249
|
+
[
|
|
250
|
+
...bindings.filter((binding) => binding.mountSource),
|
|
251
|
+
...bindings.filter((binding) => !binding.mountSource),
|
|
252
|
+
].map((binding) => path.posix.dirname(binding.executablePath)),
|
|
253
|
+
),
|
|
254
|
+
...(local ? (environment.PATH ?? "").split(path.delimiter) : []),
|
|
255
|
+
"/usr/local/bin",
|
|
256
|
+
"/usr/bin",
|
|
257
|
+
"/bin",
|
|
258
|
+
].join(":"),
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
} catch (error) {
|
|
262
|
+
cleanup();
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function canonicalRoot(root: string): string {
|
|
268
|
+
return realpathSync(root);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Per-entry cancellation, including replacement of an earlier dependency view. */
|
|
272
|
+
function removeEntry(file: string, checkCancelled: () => void): void {
|
|
273
|
+
checkCancelled();
|
|
274
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
275
|
+
if (!stat) return;
|
|
276
|
+
if (stat.isDirectory())
|
|
277
|
+
for (const name of readdirSync(file))
|
|
278
|
+
removeEntry(path.join(file, name), checkCancelled);
|
|
279
|
+
rmSync(file, { recursive: stat.isDirectory(), force: true });
|
|
280
|
+
}
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
mkdtempSync,
|
|
5
|
-
readdirSync,
|
|
6
|
-
readlinkSync,
|
|
7
|
-
realpathSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
symlinkSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
} from "node:fs";
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { mkdtemp } from "node:fs/promises";
|
|
12
4
|
import path from "node:path";
|
|
13
5
|
import {
|
|
14
6
|
type StructuredVerificationContract,
|
|
15
7
|
VERIFICATION_CONFIGURATION_PATHS,
|
|
16
8
|
} from "./contracts.ts";
|
|
17
|
-
import
|
|
9
|
+
import { executionProfile } from "./execution-profile.ts";
|
|
18
10
|
import {
|
|
19
11
|
resolveVerificationRunner,
|
|
20
12
|
type VerificationRunnerBinding,
|
|
21
13
|
validateVerificationAdapterCapability,
|
|
22
14
|
verificationRunnerFiles,
|
|
23
15
|
} from "./verification-capability.ts";
|
|
16
|
+
import type { prepareVerificationEnvironmentIo } from "./verification-environment-io.ts";
|
|
17
|
+
import type { WorkspaceIoMetrics } from "./workspace-io.ts";
|
|
24
18
|
import { runWorkspaceIo } from "./workspace-io.ts";
|
|
25
19
|
|
|
26
20
|
export async function captureVerificationEnvironmentIdentity(
|
|
@@ -49,165 +43,73 @@ export async function captureVerificationEnvironmentIdentity(
|
|
|
49
43
|
for (const binding of capability.runnerBindings)
|
|
50
44
|
if (binding.mountSource) roots.push(binding.mountSource);
|
|
51
45
|
}
|
|
52
|
-
|
|
46
|
+
const installed = await runWorkspaceIo<string>({
|
|
53
47
|
root: dependencyOwner,
|
|
54
48
|
artifactRoot: dependencyOwner,
|
|
55
49
|
operation: "verificationIdentity",
|
|
56
50
|
args: [roots],
|
|
57
51
|
signal,
|
|
58
52
|
});
|
|
53
|
+
const profile = executionProfile();
|
|
54
|
+
return createHash("sha256")
|
|
55
|
+
.update(installed)
|
|
56
|
+
.update(JSON.stringify(profile))
|
|
57
|
+
.update(
|
|
58
|
+
JSON.stringify(
|
|
59
|
+
profile.inheritEnvironment.map((name) => [
|
|
60
|
+
name,
|
|
61
|
+
process.env[name] ?? null,
|
|
62
|
+
]),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
.digest("hex");
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
function
|
|
63
|
-
const relative = path.relative(root, candidate);
|
|
64
|
-
return (
|
|
65
|
-
relative !== ".." &&
|
|
66
|
-
!relative.startsWith(`..${path.sep}`) &&
|
|
67
|
-
!path.isAbsolute(relative)
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** A disposable dependency view: immutable packages and independently writable caches. */
|
|
72
|
-
export function prepareVerificationEnvironment(
|
|
68
|
+
/** All dependency traversal/copy/removal runs outside the host event loop. */
|
|
69
|
+
export async function prepareVerificationEnvironment(
|
|
73
70
|
root: string,
|
|
74
71
|
dependencyOwner: string,
|
|
75
72
|
runnerBindings: readonly VerificationRunnerBinding[],
|
|
73
|
+
signal?: AbortSignal,
|
|
74
|
+
onMetrics?: (metrics: WorkspaceIoMetrics) => void,
|
|
76
75
|
) {
|
|
77
|
-
|
|
76
|
+
signal?.throwIfAborted();
|
|
77
|
+
const profile = executionProfile();
|
|
78
|
+
const privateRoot = await mkdtemp(
|
|
78
79
|
path.join(path.dirname(root), ".cadence-verification-"),
|
|
79
80
|
);
|
|
80
|
-
const cleanup = () =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"home",
|
|
87
|
-
"cache",
|
|
88
|
-
"tmp",
|
|
89
|
-
"reports",
|
|
90
|
-
"dependencies",
|
|
91
|
-
]) {
|
|
92
|
-
mkdirSync(path.join(privateRoot, directory), { mode: 0o700 });
|
|
93
|
-
}
|
|
94
|
-
writeFileSync(path.join(privateRoot, "home/user.npmrc"), "");
|
|
95
|
-
writeFileSync(path.join(privateRoot, "home/global.npmrc"), "");
|
|
96
|
-
const dependencies = path.join(dependencyOwner, "node_modules");
|
|
97
|
-
const stat = lstatSync(dependencies, { throwIfNoEntry: false });
|
|
98
|
-
if (stat) {
|
|
99
|
-
const target = path.join(root, "node_modules");
|
|
100
|
-
const targetStat = lstatSync(target, { throwIfNoEntry: false });
|
|
101
|
-
if (
|
|
102
|
-
!stat.isDirectory() ||
|
|
103
|
-
stat.isSymbolicLink() ||
|
|
104
|
-
(targetStat &&
|
|
105
|
-
(!targetStat.isDirectory() || targetStat.isSymbolicLink()))
|
|
106
|
-
) {
|
|
107
|
-
throw new Error("dependency-path-unsafe");
|
|
108
|
-
}
|
|
109
|
-
if (!targetStat) mkdirSync(target, { mode: 0o700 });
|
|
110
|
-
const view = path.join(privateRoot, "dependencies");
|
|
111
|
-
mounts.push({ source: view, target: "/workspace/node_modules" });
|
|
112
|
-
const entries = readdirSync(dependencies);
|
|
113
|
-
if (entries.length > 20_000) throw new Error("dependency-view-too-large");
|
|
114
|
-
const canonical = realpathSync(dependencies);
|
|
115
|
-
for (const name of entries) {
|
|
116
|
-
if (CACHES.includes(name)) continue;
|
|
117
|
-
const source = path.join(dependencies, name);
|
|
118
|
-
const placeholder = path.join(view, name);
|
|
119
|
-
const entry = lstatSync(source);
|
|
120
|
-
if (entry.isSymbolicLink()) {
|
|
121
|
-
const link = readlinkSync(source);
|
|
122
|
-
if (
|
|
123
|
-
path.isAbsolute(link) ||
|
|
124
|
-
!within(canonical, realpathSync(source))
|
|
125
|
-
) {
|
|
126
|
-
throw new Error("dependency-path-unsafe");
|
|
127
|
-
}
|
|
128
|
-
symlinkSync(link, placeholder);
|
|
129
|
-
} else {
|
|
130
|
-
if (entry.isDirectory()) mkdirSync(placeholder);
|
|
131
|
-
else if (entry.isFile()) writeFileSync(placeholder, "");
|
|
132
|
-
else throw new Error("dependency-path-unsafe");
|
|
133
|
-
mounts.push({ source, target: `/workspace/node_modules/${name}` });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
for (const cache of CACHES) {
|
|
137
|
-
mkdirSync(path.join(view, cache));
|
|
138
|
-
const source = path.join(privateRoot, "cache", cache);
|
|
139
|
-
mkdirSync(source);
|
|
140
|
-
mounts.push({
|
|
141
|
-
source,
|
|
142
|
-
target: `/workspace/node_modules/${cache}`,
|
|
143
|
-
writable: true,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const sources = new Map<string, string>();
|
|
148
|
-
const bindings = runnerBindings.map((binding) => {
|
|
149
|
-
if (!binding.mountSource) return { ...binding };
|
|
150
|
-
const source = path.resolve(binding.mountSource);
|
|
151
|
-
const stat = lstatSync(source);
|
|
152
|
-
const executable = realpathSync(binding.executablePath);
|
|
153
|
-
if (
|
|
154
|
-
!stat.isDirectory() ||
|
|
155
|
-
stat.isSymbolicLink() ||
|
|
156
|
-
!within(source, executable)
|
|
157
|
-
) {
|
|
158
|
-
throw new Error("runner-mount-unavailable");
|
|
159
|
-
}
|
|
160
|
-
let target = sources.get(source);
|
|
161
|
-
if (!target) {
|
|
162
|
-
target = `/cadence-runners/${sources.size}`;
|
|
163
|
-
sources.set(source, target);
|
|
164
|
-
mounts.push({ source, target });
|
|
165
|
-
}
|
|
166
|
-
const translate = (file: string) =>
|
|
167
|
-
path.posix.join(target, ...path.relative(source, file).split(path.sep));
|
|
168
|
-
return {
|
|
169
|
-
...binding,
|
|
170
|
-
executablePath: translate(executable),
|
|
171
|
-
...(binding.fixedArgs
|
|
172
|
-
? {
|
|
173
|
-
fixedArgs: binding.fixedArgs.map((arg) =>
|
|
174
|
-
path.isAbsolute(arg) && within(source, arg)
|
|
175
|
-
? translate(arg)
|
|
176
|
-
: arg,
|
|
177
|
-
),
|
|
178
|
-
}
|
|
179
|
-
: {}),
|
|
180
|
-
};
|
|
81
|
+
const cleanup = () =>
|
|
82
|
+
runWorkspaceIo<void>({
|
|
83
|
+
root,
|
|
84
|
+
artifactRoot: root,
|
|
85
|
+
operation: "cleanupVerification",
|
|
86
|
+
args: [privateRoot],
|
|
181
87
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Honor explicitly resolved private runtimes before system fallbacks.
|
|
198
|
-
[
|
|
199
|
-
...bindings.filter((binding) => binding.mountSource),
|
|
200
|
-
...bindings.filter((binding) => !binding.mountSource),
|
|
201
|
-
].map((binding) => path.posix.dirname(binding.executablePath)),
|
|
88
|
+
try {
|
|
89
|
+
const prepared = await runWorkspaceIo<
|
|
90
|
+
ReturnType<typeof prepareVerificationEnvironmentIo>
|
|
91
|
+
>({
|
|
92
|
+
root,
|
|
93
|
+
artifactRoot: root,
|
|
94
|
+
operation: "prepareVerification",
|
|
95
|
+
args: [
|
|
96
|
+
root,
|
|
97
|
+
dependencyOwner,
|
|
98
|
+
runnerBindings,
|
|
99
|
+
profile,
|
|
100
|
+
Object.fromEntries(
|
|
101
|
+
["PATH", ...profile.inheritEnvironment].flatMap((name) =>
|
|
102
|
+
process.env[name] === undefined ? [] : [[name, process.env[name]]],
|
|
202
103
|
),
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
104
|
+
),
|
|
105
|
+
privateRoot,
|
|
106
|
+
],
|
|
107
|
+
signal,
|
|
108
|
+
onMetrics,
|
|
109
|
+
});
|
|
110
|
+
return { ...prepared, cleanup };
|
|
209
111
|
} catch (error) {
|
|
210
|
-
cleanup();
|
|
112
|
+
await cleanup();
|
|
211
113
|
throw error;
|
|
212
114
|
}
|
|
213
115
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Only callers admitted before I/O begins share a scan. A later currentness check
|
|
2
|
+
* must observe bytes after its own admission, not join an older in-flight scan.
|
|
3
|
+
* Last cancellation drains
|
|
4
|
+
* the worker before returning; a cancelled scan never admits a new subscriber. */
|
|
5
|
+
export function coalesceVerificationScans<P>(
|
|
6
|
+
scan: (plan: P, signal: AbortSignal) => Promise<string>,
|
|
7
|
+
) {
|
|
8
|
+
type Pending = {
|
|
9
|
+
controller: AbortController;
|
|
10
|
+
promise: Promise<string>;
|
|
11
|
+
subscribers: number;
|
|
12
|
+
started: boolean;
|
|
13
|
+
};
|
|
14
|
+
const pending = new Map<string, Pending>();
|
|
15
|
+
return (plan: P, signal?: AbortSignal): Promise<string> => {
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
const key = JSON.stringify(plan);
|
|
18
|
+
let entry = pending.get(key);
|
|
19
|
+
if (!entry || entry.started || entry.controller.signal.aborted) {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const created: Pending = {
|
|
22
|
+
controller,
|
|
23
|
+
subscribers: 0,
|
|
24
|
+
started: false,
|
|
25
|
+
promise: Promise.resolve(""),
|
|
26
|
+
};
|
|
27
|
+
created.promise = Promise.resolve()
|
|
28
|
+
.then(() => {
|
|
29
|
+
created.started = true;
|
|
30
|
+
return scan(plan, controller.signal);
|
|
31
|
+
})
|
|
32
|
+
.finally(() => {
|
|
33
|
+
if (pending.get(key) === created) pending.delete(key);
|
|
34
|
+
});
|
|
35
|
+
entry = created;
|
|
36
|
+
pending.set(key, entry);
|
|
37
|
+
}
|
|
38
|
+
const shared = entry;
|
|
39
|
+
shared.subscribers++;
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
const abort = () => {
|
|
43
|
+
if (cancelled) return;
|
|
44
|
+
cancelled = true;
|
|
45
|
+
if (--shared.subscribers === 0) shared.controller.abort(signal?.reason);
|
|
46
|
+
};
|
|
47
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
48
|
+
if (signal?.aborted) abort();
|
|
49
|
+
const settle = (value?: string, error?: unknown) => {
|
|
50
|
+
signal?.removeEventListener("abort", abort);
|
|
51
|
+
if (!cancelled) shared.subscribers--;
|
|
52
|
+
if (cancelled)
|
|
53
|
+
reject(signal?.reason ?? new DOMException("cancelled", "AbortError"));
|
|
54
|
+
else if (error !== undefined) reject(error);
|
|
55
|
+
else if (value !== undefined) resolve(value);
|
|
56
|
+
else reject(new Error("verification-scan-result-unavailable"));
|
|
57
|
+
};
|
|
58
|
+
shared.promise.then(
|
|
59
|
+
(value) => settle(value),
|
|
60
|
+
(error) => settle(undefined, error),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
}
|