@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,38 @@
|
|
|
1
|
+
export interface ExecutionProfile {
|
|
2
|
+
mode: "isolated" | "local-trusted";
|
|
3
|
+
timeoutMs: number;
|
|
4
|
+
bwrapPath: string;
|
|
5
|
+
inheritEnvironment: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Operator configuration only. Unknown modes never silently disable isolation. */
|
|
9
|
+
export function executionProfile(
|
|
10
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
11
|
+
): ExecutionProfile {
|
|
12
|
+
const mode = env.ABEL_EXECUTION_MODE ?? "isolated";
|
|
13
|
+
if (mode !== "isolated" && mode !== "local-trusted")
|
|
14
|
+
throw new Error("execution-mode-invalid");
|
|
15
|
+
const timeoutMs = Number(env.ABEL_VERIFICATION_TIMEOUT_MS ?? 600_000);
|
|
16
|
+
if (
|
|
17
|
+
!Number.isSafeInteger(timeoutMs) ||
|
|
18
|
+
timeoutMs < 1 ||
|
|
19
|
+
timeoutMs > 86_400_000
|
|
20
|
+
)
|
|
21
|
+
throw new Error("verification-timeout-invalid");
|
|
22
|
+
const inheritEnvironment = (env.ABEL_VERIFICATION_ENV ?? "")
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((name) => name.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
if (
|
|
27
|
+
inheritEnvironment.some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name))
|
|
28
|
+
)
|
|
29
|
+
throw new Error("verification-environment-names-invalid");
|
|
30
|
+
if (mode === "isolated" && inheritEnvironment.length)
|
|
31
|
+
throw new Error("verification-environment-requires-trusted-mode");
|
|
32
|
+
return {
|
|
33
|
+
mode,
|
|
34
|
+
timeoutMs,
|
|
35
|
+
bwrapPath: env.ABEL_BWRAP_PATH ?? "/usr/bin/bwrap",
|
|
36
|
+
inheritEnvironment,
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/isolation-backend.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface BubblewrapIsolationOptions {
|
|
|
54
54
|
timeoutMs?: number;
|
|
55
55
|
maxOutputBytes?: number;
|
|
56
56
|
terminateGraceMs?: number;
|
|
57
|
+
/** Explicit operator opt-in; never an availability fallback. */
|
|
58
|
+
localTrusted?: boolean;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
function defaultProbe(executable: string): boolean {
|
|
@@ -124,6 +126,7 @@ class BoundedLog {
|
|
|
124
126
|
|
|
125
127
|
/** Linux isolation capability. Lack of Bubblewrap is a pause, never a fallback. */
|
|
126
128
|
export class BubblewrapIsolationBackend {
|
|
129
|
+
readonly #localTrusted: boolean;
|
|
127
130
|
readonly #bwrapPath: string;
|
|
128
131
|
readonly #probe: (path: string) => boolean | Promise<boolean>;
|
|
129
132
|
readonly #spawn: typeof spawn;
|
|
@@ -132,6 +135,7 @@ export class BubblewrapIsolationBackend {
|
|
|
132
135
|
readonly #terminateGraceMs: number;
|
|
133
136
|
|
|
134
137
|
constructor(options: BubblewrapIsolationOptions = {}) {
|
|
138
|
+
this.#localTrusted = options.localTrusted === true;
|
|
135
139
|
this.#bwrapPath = options.bwrapPath ?? "/usr/bin/bwrap";
|
|
136
140
|
this.#probe = options.probe ?? defaultProbe;
|
|
137
141
|
this.#spawn = options.spawnProcess ?? spawn;
|
|
@@ -154,6 +158,7 @@ export class BubblewrapIsolationBackend {
|
|
|
154
158
|
|
|
155
159
|
async available(): Promise<boolean> {
|
|
156
160
|
try {
|
|
161
|
+
if (this.#localTrusted && process.platform !== "linux") return false;
|
|
157
162
|
return Boolean(await this.#probe(this.#bwrapPath));
|
|
158
163
|
} catch {
|
|
159
164
|
return false;
|
|
@@ -183,47 +188,64 @@ export class BubblewrapIsolationBackend {
|
|
|
183
188
|
if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) {
|
|
184
189
|
throw new Error("isolation-root-invalid");
|
|
185
190
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
191
|
+
// Trusted mode shares host files/network, but still needs a PID namespace:
|
|
192
|
+
// process groups alone cannot contain detached descendants. No fallback.
|
|
193
|
+
const argv = this.#localTrusted
|
|
194
|
+
? [
|
|
195
|
+
"--die-with-parent",
|
|
196
|
+
"--new-session",
|
|
197
|
+
"--unshare-pid",
|
|
198
|
+
"--bind",
|
|
199
|
+
"/",
|
|
200
|
+
"/",
|
|
201
|
+
"--proc",
|
|
202
|
+
"/proc",
|
|
203
|
+
"--dev",
|
|
204
|
+
"/dev",
|
|
205
|
+
"--chdir",
|
|
206
|
+
root,
|
|
207
|
+
"--clearenv",
|
|
208
|
+
]
|
|
209
|
+
: [
|
|
210
|
+
"--die-with-parent",
|
|
211
|
+
"--new-session",
|
|
212
|
+
"--unshare-all",
|
|
213
|
+
"--proc",
|
|
214
|
+
"/proc",
|
|
215
|
+
"--dev",
|
|
216
|
+
"/dev",
|
|
217
|
+
"--tmpfs",
|
|
218
|
+
"/tmp",
|
|
219
|
+
"--bind",
|
|
220
|
+
root,
|
|
221
|
+
"/workspace",
|
|
222
|
+
"--chdir",
|
|
223
|
+
"/workspace",
|
|
224
|
+
"--clearenv",
|
|
225
|
+
"--setenv",
|
|
226
|
+
"PATH",
|
|
227
|
+
"/usr/local/bin:/usr/bin:/bin",
|
|
228
|
+
];
|
|
229
|
+
if (!this.#localTrusted) {
|
|
230
|
+
for (const systemRoot of ["/usr", "/bin", "/lib", "/lib64"]) {
|
|
231
|
+
if (lstatSync(systemRoot, { throwIfNoEntry: false }))
|
|
232
|
+
argv.push("--ro-bind", systemRoot, systemRoot);
|
|
233
|
+
}
|
|
234
|
+
for (const mount of input.mounts ?? []) {
|
|
235
|
+
if (!path.isAbsolute(mount.source) || !path.isAbsolute(mount.target))
|
|
236
|
+
throw new Error("isolation-mount-invalid");
|
|
237
|
+
argv.push(
|
|
238
|
+
mount.writable ? "--bind" : "--ro-bind",
|
|
239
|
+
mount.source,
|
|
240
|
+
mount.target,
|
|
241
|
+
);
|
|
214
242
|
}
|
|
215
|
-
argv.push(
|
|
216
|
-
mount.writable ? "--bind" : "--ro-bind",
|
|
217
|
-
mount.source,
|
|
218
|
-
mount.target,
|
|
219
|
-
);
|
|
220
243
|
}
|
|
221
244
|
for (const [name, value] of Object.entries(
|
|
222
245
|
input.environment ?? {},
|
|
223
246
|
).sort()) {
|
|
224
|
-
if (!validEnvironmentName(name) || value.includes("\0"))
|
|
247
|
+
if (!validEnvironmentName(name) || value.includes("\0"))
|
|
225
248
|
throw new Error("isolation-environment-invalid");
|
|
226
|
-
}
|
|
227
249
|
argv.push("--setenv", name, value);
|
|
228
250
|
}
|
|
229
251
|
argv.push("--", input.executable, ...(input.args ?? []));
|
|
@@ -243,16 +265,16 @@ export class BubblewrapIsolationBackend {
|
|
|
243
265
|
input.signal?.removeEventListener("abort", abort);
|
|
244
266
|
resolve(result);
|
|
245
267
|
};
|
|
268
|
+
const kill = (signal: NodeJS.Signals) => {
|
|
269
|
+
child?.kill(signal);
|
|
270
|
+
};
|
|
246
271
|
const terminate = (result: IsolationRunResult): void => {
|
|
247
272
|
if (settled || terminationResult) return;
|
|
248
273
|
terminationResult = result;
|
|
249
274
|
if (executionTimer) clearTimeout(executionTimer);
|
|
250
|
-
killTimer = setTimeout(
|
|
251
|
-
() => child?.kill("SIGKILL"),
|
|
252
|
-
this.#terminateGraceMs,
|
|
253
|
-
);
|
|
275
|
+
killTimer = setTimeout(() => kill("SIGKILL"), this.#terminateGraceMs);
|
|
254
276
|
killTimer.unref?.();
|
|
255
|
-
|
|
277
|
+
kill("SIGTERM");
|
|
256
278
|
};
|
|
257
279
|
const abort = (): void => {
|
|
258
280
|
terminate({ ok: false, state: "cancelled", code: "cancelled" });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { inspectConsumer, inspectRuns } from "./operator-tools.ts";
|
|
3
|
+
|
|
4
|
+
const [command = "doctor", root = process.cwd()] = process.argv.slice(2);
|
|
5
|
+
try {
|
|
6
|
+
if (!["doctor", "runs"].includes(command))
|
|
7
|
+
throw new Error(
|
|
8
|
+
"Usage: node --experimental-strip-types <cadence>/src/operator-cli.mjs doctor|runs [consumer-root]",
|
|
9
|
+
);
|
|
10
|
+
const result =
|
|
11
|
+
command === "doctor"
|
|
12
|
+
? inspectConsumer(path.resolve(root))
|
|
13
|
+
: inspectRuns(path.resolve(root));
|
|
14
|
+
console.log(JSON.stringify(result, null, 2));
|
|
15
|
+
if (command === "doctor" && !result.ok) process.exitCode = 1;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
ok: false,
|
|
20
|
+
error: error instanceof Error ? error.message : "operator-command-failed",
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|