@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.
@@ -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
+ }
@@ -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
- const argv = [
187
- "--die-with-parent",
188
- "--new-session",
189
- "--unshare-all",
190
- "--proc",
191
- "/proc",
192
- "--dev",
193
- "/dev",
194
- "--tmpfs",
195
- "/tmp",
196
- "--bind",
197
- root,
198
- "/workspace",
199
- "--chdir",
200
- "/workspace",
201
- "--clearenv",
202
- "--setenv",
203
- "PATH",
204
- "/usr/local/bin:/usr/bin:/bin",
205
- ];
206
- for (const systemRoot of ["/usr", "/bin", "/lib", "/lib64"]) {
207
- const stat = lstatSync(systemRoot, { throwIfNoEntry: false });
208
- if (!stat) continue;
209
- argv.push("--ro-bind", systemRoot, systemRoot);
210
- }
211
- for (const mount of input.mounts ?? []) {
212
- if (!path.isAbsolute(mount.source) || !path.isAbsolute(mount.target)) {
213
- throw new Error("isolation-mount-invalid");
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
- child?.kill("SIGTERM");
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
+ }