@abelxiaoxing/cadence 1.4.1 → 1.5.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 +74 -6
- package/config/plan-draft.quick.example.json +78 -0
- package/package.json +6 -2
- package/prompts/abel-design.md +8 -1
- package/prompts/abel-diagnose.md +1 -1
- package/prompts/abel-implement.md +10 -1
- package/prompts/abel-init.md +1 -1
- package/src/change-verification.ts +25 -1
- package/src/child-budget.ts +58 -0
- package/src/child-model.ts +17 -4
- package/src/child-session.ts +20 -24
- package/src/contracts.ts +16 -0
- package/src/delivery-compiler.ts +2 -0
- package/src/durable-contracts.ts +3 -0
- package/src/execution-profile.ts +38 -0
- package/src/isolation-backend.ts +62 -40
- 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 +3 -0
- package/src/package-verification.ts +86 -8
- package/src/package-workflow.ts +73 -35
- package/src/plan-draft-summary.ts +4 -1
- package/src/single-task-draft.ts +142 -0
- package/src/transport-budget.ts +21 -1
- 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 +2 -0
- package/src/workspace-io-entry.mjs +19 -1
- package/src/workspace-io-worker.mjs +268 -50
- package/src/workspace-io.ts +6 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { DesignPlanValidationError } from "./design-diagnostics.ts";
|
|
2
|
+
import type {
|
|
3
|
+
DraftVerification,
|
|
4
|
+
PlanDraft,
|
|
5
|
+
PlanTaskInput,
|
|
6
|
+
} from "./plan-draft.ts";
|
|
7
|
+
|
|
8
|
+
/** Small author input. Evidence, scope and the complete suite remain explicit. */
|
|
9
|
+
export interface SingleTaskDraft {
|
|
10
|
+
changeId: string;
|
|
11
|
+
changeContract?: PlanDraft["changeContract"];
|
|
12
|
+
singleTask: {
|
|
13
|
+
taskId: string;
|
|
14
|
+
objective: string;
|
|
15
|
+
context: PlanTaskInput["context"];
|
|
16
|
+
roots: string[];
|
|
17
|
+
read: string[];
|
|
18
|
+
greenWrite: string[];
|
|
19
|
+
redWrite?: string[];
|
|
20
|
+
expectedFailure?: string;
|
|
21
|
+
verificationMode?: "behavior" | "mechanical" | "refactor";
|
|
22
|
+
verification: DraftVerification;
|
|
23
|
+
fullSuite: DraftVerification;
|
|
24
|
+
impactClosure: PlanTaskInput["impactClosure"];
|
|
25
|
+
agents: PlanTaskInput["agents"];
|
|
26
|
+
recovery?: { artifactAttempts: number; repairAttempts: number };
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function invalid(code: string, field = "singleTask"): never {
|
|
31
|
+
throw new DesignPlanValidationError(code, [{ code, field }]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Mechanical expansion only; the ordinary compiler validates all resulting fields. */
|
|
39
|
+
export function expandSingleTaskDraft(value: unknown): unknown {
|
|
40
|
+
if (!record(value) || !Object.hasOwn(value, "singleTask")) return value;
|
|
41
|
+
if (
|
|
42
|
+
Object.keys(value).some(
|
|
43
|
+
(key) => !["changeId", "changeContract", "singleTask"].includes(key),
|
|
44
|
+
) ||
|
|
45
|
+
!record(value.singleTask)
|
|
46
|
+
)
|
|
47
|
+
invalid("single-task-draft-invalid");
|
|
48
|
+
const input = value.singleTask;
|
|
49
|
+
const required = [
|
|
50
|
+
"taskId",
|
|
51
|
+
"objective",
|
|
52
|
+
"context",
|
|
53
|
+
"roots",
|
|
54
|
+
"read",
|
|
55
|
+
"greenWrite",
|
|
56
|
+
"verification",
|
|
57
|
+
"fullSuite",
|
|
58
|
+
"impactClosure",
|
|
59
|
+
"agents",
|
|
60
|
+
];
|
|
61
|
+
const optional = [
|
|
62
|
+
"redWrite",
|
|
63
|
+
"expectedFailure",
|
|
64
|
+
"verificationMode",
|
|
65
|
+
"recovery",
|
|
66
|
+
];
|
|
67
|
+
if (
|
|
68
|
+
required.some((key) => !Object.hasOwn(input, key)) ||
|
|
69
|
+
Object.keys(input).some((key) => ![...required, ...optional].includes(key))
|
|
70
|
+
)
|
|
71
|
+
invalid("single-task-draft-invalid");
|
|
72
|
+
const task = structuredClone(
|
|
73
|
+
input,
|
|
74
|
+
) as unknown as SingleTaskDraft["singleTask"];
|
|
75
|
+
const mode = task.verificationMode ?? "behavior";
|
|
76
|
+
if (mode === "behavior" && (!task.redWrite?.length || !task.expectedFailure))
|
|
77
|
+
invalid("single-task-red-required");
|
|
78
|
+
if (
|
|
79
|
+
mode !== "behavior" &&
|
|
80
|
+
(task.redWrite !== undefined || task.expectedFailure !== undefined)
|
|
81
|
+
)
|
|
82
|
+
invalid("single-task-red-unexpected");
|
|
83
|
+
if (
|
|
84
|
+
Object.hasOwn(input, "recovery") &&
|
|
85
|
+
(!record(task.recovery) ||
|
|
86
|
+
!Object.hasOwn(task.recovery, "artifactAttempts") ||
|
|
87
|
+
!Object.hasOwn(task.recovery, "repairAttempts") ||
|
|
88
|
+
Object.keys(task.recovery).some(
|
|
89
|
+
(key) => !["artifactAttempts", "repairAttempts"].includes(key),
|
|
90
|
+
))
|
|
91
|
+
)
|
|
92
|
+
invalid("single-task-recovery-invalid");
|
|
93
|
+
return {
|
|
94
|
+
changeId: value.changeId,
|
|
95
|
+
...(value.changeContract === undefined
|
|
96
|
+
? {}
|
|
97
|
+
: { changeContract: value.changeContract }),
|
|
98
|
+
tasks: [
|
|
99
|
+
{
|
|
100
|
+
taskId: task.taskId,
|
|
101
|
+
objective: task.objective,
|
|
102
|
+
context: task.context,
|
|
103
|
+
roots: task.roots,
|
|
104
|
+
read: task.read,
|
|
105
|
+
dependsOn: [],
|
|
106
|
+
verificationMode: mode,
|
|
107
|
+
phases: {
|
|
108
|
+
...(mode === "behavior"
|
|
109
|
+
? {
|
|
110
|
+
red: {
|
|
111
|
+
write: task.redWrite,
|
|
112
|
+
delete: [],
|
|
113
|
+
verification: {
|
|
114
|
+
...task.verification,
|
|
115
|
+
expectedFailure: task.expectedFailure,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
120
|
+
green: {
|
|
121
|
+
write: task.greenWrite,
|
|
122
|
+
delete: [],
|
|
123
|
+
verification: task.verification,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
affectedVerification: task.fullSuite,
|
|
127
|
+
repairVerification: task.fullSuite,
|
|
128
|
+
scheduling: { conflicts: [], resources: [] },
|
|
129
|
+
agents: task.agents,
|
|
130
|
+
approvedDependencies: [],
|
|
131
|
+
impactClosure: task.impactClosure,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
outputs: [],
|
|
135
|
+
verification: {
|
|
136
|
+
baseline: { fullSuite: task.fullSuite },
|
|
137
|
+
change: { fullSuite: task.fullSuite, postApply: task.fullSuite },
|
|
138
|
+
artifactCorrection: { maxAttempts: task.recovery?.artifactAttempts ?? 2 },
|
|
139
|
+
repair: { maxAttempts: task.recovery?.repairAttempts ?? 1 },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
package/src/transport-budget.ts
CHANGED
|
@@ -28,5 +28,25 @@ export function isTransportTimeoutCode(
|
|
|
28
28
|
export function transportFailureError(code: string): Error {
|
|
29
29
|
return isTransportTimeoutCode(code)
|
|
30
30
|
? new TransportTimeout(code)
|
|
31
|
-
: new Error(
|
|
31
|
+
: new Error(
|
|
32
|
+
code === "child-provider-rate-limited" ? code : "transport-failure",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Snapshot at the start of each request; Workers cannot override these values. */
|
|
37
|
+
export function requestBounds(env: NodeJS.ProcessEnv = process.env) {
|
|
38
|
+
const read = (name: string, fallback: number) => {
|
|
39
|
+
const value = Number(env[name] ?? fallback);
|
|
40
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1_200_000)
|
|
41
|
+
throw new Error("request-budget-invalid");
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
...REQUEST_BOUNDS,
|
|
46
|
+
firstProgressMs: read(
|
|
47
|
+
"ABEL_FIRST_PROGRESS_MS",
|
|
48
|
+
REQUEST_BOUNDS.firstProgressMs,
|
|
49
|
+
),
|
|
50
|
+
streamIdleMs: read("ABEL_STREAM_IDLE_MS", REQUEST_BOUNDS.streamIdleMs),
|
|
51
|
+
};
|
|
32
52
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
verificationInputPaths,
|
|
17
17
|
verificationSteps,
|
|
18
18
|
} from "./contracts.ts";
|
|
19
|
+
import { executionProfile } from "./execution-profile.ts";
|
|
19
20
|
import {
|
|
20
21
|
type Bound,
|
|
21
22
|
isCurrent,
|
|
@@ -828,9 +829,10 @@ function validateAtomicCapability(
|
|
|
828
829
|
continue;
|
|
829
830
|
if (
|
|
830
831
|
lstatSync(path.join(root, relative)).size > 64 * 1024 ||
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
832
|
+
(executionProfile().mode !== "local-trusted" &&
|
|
833
|
+
unsupportedExecutionConfiguration(
|
|
834
|
+
readFileSync(path.join(root, relative), "utf8"),
|
|
835
|
+
))
|
|
834
836
|
) {
|
|
835
837
|
return adapterFailure(
|
|
836
838
|
step.id,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
VerificationFailureSummary,
|
|
3
|
+
VerificationObservation,
|
|
4
|
+
} from "./contracts.ts";
|
|
5
|
+
|
|
6
|
+
/** Diagnostic text is evidence for repair, never a failure identity or authority. */
|
|
7
|
+
|
|
8
|
+
export function diagnosticText(value: string, maximum = 4096): string {
|
|
9
|
+
const clean = value
|
|
10
|
+
.replace(
|
|
11
|
+
new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "gu"),
|
|
12
|
+
"",
|
|
13
|
+
)
|
|
14
|
+
.replace(/(Bearer\s+)\S+/giu, "$1[redacted]")
|
|
15
|
+
.replace(
|
|
16
|
+
/((?:api[_-]?key|token|password|secret)\s*[:=]\s*)[^\s,;]+/giu,
|
|
17
|
+
"$1[redacted]",
|
|
18
|
+
)
|
|
19
|
+
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/giu, "$1[redacted]@");
|
|
20
|
+
return clean.length <= maximum
|
|
21
|
+
? clean
|
|
22
|
+
: `${clean.slice(0, Math.floor(maximum / 2))}\n…[truncated]…\n${clean.slice(-Math.floor(maximum / 2))}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Bounded operation-local feedback; no raw transcripts or global cross-run cache. */
|
|
26
|
+
export class VerificationFeedback {
|
|
27
|
+
readonly #entries = new Map<string, VerificationFailureSummary>();
|
|
28
|
+
observe(result: VerificationObservation): void {
|
|
29
|
+
if (result.kind === "cancelled") return;
|
|
30
|
+
const id =
|
|
31
|
+
result.kind === "unavailable"
|
|
32
|
+
? result.verificationId
|
|
33
|
+
: result.evidence.id;
|
|
34
|
+
this.#entries.delete(id);
|
|
35
|
+
if (result.kind === "accepted") return;
|
|
36
|
+
const summary = result.diagnostic ?? {
|
|
37
|
+
verificationId: id,
|
|
38
|
+
code: result.code,
|
|
39
|
+
failures: [],
|
|
40
|
+
stdout: "",
|
|
41
|
+
stderr: "",
|
|
42
|
+
truncated: false,
|
|
43
|
+
nextStep:
|
|
44
|
+
"Repair the verification environment or adapter before changing product code.",
|
|
45
|
+
};
|
|
46
|
+
this.#entries.set(id, structuredClone(summary));
|
|
47
|
+
const oldest = this.#entries.keys().next().value;
|
|
48
|
+
if (this.#entries.size > 32 && oldest !== undefined)
|
|
49
|
+
this.#entries.delete(oldest);
|
|
50
|
+
}
|
|
51
|
+
current(): VerificationFailureSummary[] {
|
|
52
|
+
const result: VerificationFailureSummary[] = [];
|
|
53
|
+
let bytes = 0;
|
|
54
|
+
for (const entry of [...this.#entries.values()].reverse()) {
|
|
55
|
+
const summary = {
|
|
56
|
+
...entry,
|
|
57
|
+
failures: entry.failures
|
|
58
|
+
.slice(0, 4)
|
|
59
|
+
.map((text) => diagnosticText(text, 256)),
|
|
60
|
+
stdout: diagnosticText(entry.stdout, 512),
|
|
61
|
+
stderr: diagnosticText(entry.stderr, 1024),
|
|
62
|
+
truncated:
|
|
63
|
+
entry.truncated ||
|
|
64
|
+
entry.failures.length > 4 ||
|
|
65
|
+
entry.failures.some((text) => text.length > 256) ||
|
|
66
|
+
entry.stdout.length > 512 ||
|
|
67
|
+
entry.stderr.length > 1024,
|
|
68
|
+
};
|
|
69
|
+
const size = Buffer.byteLength(JSON.stringify(summary), "utf8");
|
|
70
|
+
if (bytes + size > 6000) break;
|
|
71
|
+
result.push(summary);
|
|
72
|
+
bytes += size;
|
|
73
|
+
}
|
|
74
|
+
return structuredClone(result.reverse());
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -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
|
+
}
|