@miraland-labs/conduit-bridge 0.16.174 → 0.16.180

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connects a computer to one organization, claims work, and drives a local agent.
4
4
 
5
- **Package version:** `0.16.125` — the workspace brief reports `declared_demonstration` so the planner knows whether the repository can show itself (docs/historical/KERNEL_HISTORY_2026-09.md §51). Earlier (0.16.124) — when a project declares `.conduit/demonstrate`, Bridge runs that bounded command at the delivered head and attaches a stamped `preview` row (docs/historical/KERNEL_HISTORY_2026-09.md §47). Earlier (0.16.95) — Conduit-fuel lanes bind organization aliases (T144), the Anthropic base URL is the origin (T148), the investigation lane resolves its model (T154); a hosted container image lives under `hosted/` (docs/HOSTED_RUNNER.md). Earlier (0.16.27) — work-package execution budgets reach every agent turn, are bounded by the machine ceiling, and expose their effective source for operations. Local-fuel provider refusals make the affected lane unavailable instead of repeatedly spending attempts; an operator who changes the lane's account or plan can invalidate that observation with `ops quota-clear <driver>`. Bridge discovers bounded repository verification commands, including Make targets `test`, `check`, `verify`, `replay`, `typecheck`, `lint`, and `build`. `ops disconnect` asks before it removes the runner service and clears credentials; pass `--yes` to skip the question. `ops.env` is enrollment/bootstrap defaults only: an explicit workspace does not inherit an old repository, and project switches do not rewrite global project state. `ops enroll` exchanges a single-use token, registers the machine, binds project affinity, provisions fuel keys, brings detected drivers online, and starts the runner in one command. On Linux, `ops install` refuses any effective systemd drop-in. A switch intent remains queued until the target runner heartbeat proves Bound. After every Bridge publish, operators must re-run `ops install` (LaunchAgent pins an absolute `cli.js`).
5
+ **Package version:** `0.16.176`. Earlier (0.16.125) — the workspace brief reports `declared_demonstration` so the planner knows whether the repository can show itself (docs/historical/KERNEL_HISTORY_2026-09.md §51). Earlier (0.16.124) — when a project declares `.conduit/demonstrate`, Bridge runs that bounded command at the delivered head and attaches a stamped `preview` row (docs/historical/KERNEL_HISTORY_2026-09.md §47). Earlier (0.16.95) — Conduit-fuel lanes bind organization aliases (T144), the Anthropic base URL is the origin (T148), the investigation lane resolves its model (T154); a hosted container image lives under `hosted/` (docs/HOSTED_RUNNER.md). Earlier (0.16.27) — work-package execution budgets reach every agent turn, are bounded by the machine ceiling, and expose their effective source for operations. Local-fuel provider refusals make the affected lane unavailable instead of repeatedly spending attempts; an operator who changes the lane's account or plan can invalidate that observation with `ops quota-clear <driver>`. Bridge discovers bounded repository verification commands, including Make targets `test`, `check`, `verify`, `replay`, `typecheck`, `lint`, and `build`. `ops disconnect` asks before it removes the runner service and clears credentials; pass `--yes` to skip the question. `ops.env` is enrollment/bootstrap defaults only: an explicit workspace does not inherit an old repository, and project switches do not rewrite global project state. `ops enroll` exchanges a single-use token, registers the machine, binds project affinity, provisions fuel keys, brings detected drivers online, and starts the runner in one command. On Linux, `ops install` refuses any effective systemd drop-in. A switch intent remains queued until the target runner heartbeat proves Bound. After every Bridge publish, operators must re-run `ops install` (LaunchAgent pins an absolute `cli.js`).
6
6
 
7
7
  ## Prerequisites
8
8
 
@@ -84,7 +84,7 @@ persisted and excludes only that lane; `online`/`offline` does not erase it. `op
84
84
  installation and authentication, not remaining vendor allowance. After deliberately changing an
85
85
  account or plan, run `ops quota-clear <driver>` to clear the old observation; this does not probe the
86
86
  provider, and the next real claim records another refusal if the new allowance is still unavailable.
87
- Fix the named issue and let a fresh heartbeat land before choosing **Recheck** in Activity.
87
+ Fix the named issue and let a fresh heartbeat land before choosing **Recheck** in Tasks.
88
88
 
89
89
  Conduit labels an exact driver/CLI version **Certified** only after two consecutive real canaries:
90
90
  repository delivery with PR/evidence and a successful rework cycle. All other lanes are
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { assignmentPollBehindServer, ConduitClient, ConduitRequestError } from "
12
12
  import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, saveConfig, saveConfigPrefs, saveDriverQuota, savePendingConnection, suggestMachineName, } from "./config.js";
13
13
  import { runMcp } from "./mcp.js";
14
14
  import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from "./detect.js";
15
- import { DRIVERS, MODEL_TIERS } from "./driver.js";
15
+ import { DRIVERS, MODEL_TIERS, probeUntrustedCodeSandbox } from "./driver.js";
16
16
  import { driversHeartbeatReport, driverLabel, heartbeatStatusForDrivers, isSupportedDriverId, listDriverLanes, onlineDriverIds, seedDriversFromDetection, seedDriverLanes, setDriverFuel, setDriverModels, setDriversOnline, formatLaneTiers, laneDispatchBlock, laneStatuses, } from "./drivers.js";
17
17
  import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
18
18
  import { parseAgentTimeoutMinutes, parseMaxTurns, resolveMaxTurns } from "./execution-budget.js";
@@ -826,6 +826,7 @@ async function heartbeat(client, config, workspacePath, brief, preflight, intent
826
826
  // heartbeat.
827
827
  const preflightToSend = {
828
828
  ...preflight,
829
+ untrusted_code_isolation: await probeUntrustedCodeSandbox(),
829
830
  ...(assignmentPollBehindServer() ? { runner_behind_server: true } : {}),
830
831
  ...(process.env.CONDUIT_RUNNER_SERVICE === "container" ? { runner_service: "container" } : {}),
831
832
  ...(workspacePath && reportedGateRehearsals.length ? { gate_rehearsals: reportedGateRehearsals } : {}),
@@ -890,6 +891,7 @@ async function disconnect() {
890
891
  console.log(`Tip: stop a background runner with \`${bridgeUsage("uninstall-service")}\` if one is installed.`);
891
892
  }
892
893
  try {
894
+ await probeUntrustedCodeSandbox();
893
895
  if (command === "connect")
894
896
  await connect();
895
897
  else if (command === "join")
package/dist/driver.js CHANGED
@@ -1,10 +1,15 @@
1
- import { spawn } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import { boundedTail } from "./ensure-test-evidence.js";
3
3
  import { resolveAgentTimeout, resolveMaxTurns } from "./execution-budget.js";
4
4
  import { agentExitSignal, turnCapSignal, vendorKindForAntigravity } from "./failure-signal.js";
5
- import { existsSync } from "node:fs";
6
- import { mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
7
- import { join, resolve } from "node:path";
5
+ import { createServer } from "node:net";
6
+ import { createRequire } from "node:module";
7
+ import { fileURLToPath } from "node:url";
8
+ import { homedir, tmpdir } from "node:os";
9
+ import { promisify } from "node:util";
10
+ import { constants, existsSync } from "node:fs";
11
+ import { access, mkdir, mkdtemp, readFile, readdir, realpath, stat, rm, rmdir, writeFile } from "node:fs/promises";
12
+ import { basename, delimiter, dirname, join, resolve } from "node:path";
8
13
  import { z } from "zod";
9
14
  import { allowsExternalFetch, deniedCommands, executionClassPromptRules, isRunnableVerificationCommand, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectGrok, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
10
15
  /**
@@ -1994,6 +1999,249 @@ export function boundedEnvironment(fuel, fuelSource = "conduit", extraEnv) {
1994
1999
  Object.assign(env, extraEnv);
1995
2000
  return env;
1996
2001
  }
2002
+ /**
2003
+ * Gates and probes inherit build caches, never runner credentials or proxy settings.
2004
+ * This environment is not an OS sandbox: direct network routes and same-uid filesystem access
2005
+ * remain available until the hosted-runner sandbox lands. CARGO_HOME is an accepted shared cache.
2006
+ */
2007
+ export function untrustedCodeEnvironment(scratchHome) {
2008
+ const allowed = ["PATH", "TMPDIR", "TMP", "TEMP", "LANG", "LANGUAGE", "TERM", "COLORTERM", "NO_COLOR", "FORCE_COLOR",
2009
+ "CARGO_HOME", "RUSTUP_HOME", "CARGO_TARGET_DIR", "CARGO_INCREMENTAL",
2010
+ ...Object.keys(process.env).filter((name) => name.startsWith("LC_") || name.startsWith("CARGO_PROFILE_"))];
2011
+ return {
2012
+ ...Object.fromEntries(allowed.flatMap((name) => process.env[name] === undefined ? [] : [[name, process.env[name]]])),
2013
+ HOME: scratchHome,
2014
+ XDG_CONFIG_HOME: join(scratchHome, "config"),
2015
+ XDG_CACHE_HOME: join(scratchHome, "cache"),
2016
+ XDG_DATA_HOME: join(scratchHome, "data"),
2017
+ GIT_TERMINAL_PROMPT: "0",
2018
+ GIT_ASKPASS: "",
2019
+ GIT_CONFIG_GLOBAL: "/dev/null",
2020
+ GIT_CONFIG_SYSTEM: "/dev/null",
2021
+ GIT_SSH_COMMAND: "ssh -F /dev/null -o BatchMode=yes -o IdentitiesOnly=yes -o IdentityAgent=none -i /dev/null",
2022
+ };
2023
+ }
2024
+ /** Pure spawn projection; parent mounts precede their writable children. */
2025
+ export function buildUntrustedCodeSpawn(input, host) {
2026
+ const direct = { command: input.command, argv: input.argv, cwd: input.cwd, env: input.env };
2027
+ if (host.os_sandbox === "none")
2028
+ return direct;
2029
+ const writable = [...new Set([...input.writable, ...(host.worktreeMetadata ? [host.worktreeMetadata] : [])])]
2030
+ .sort((a, b) => a.split("/").length - b.split("/").length);
2031
+ const hidden = host.sourceRepository ? join(host.sourceRepository, ".conduit") : undefined;
2032
+ const rootWorkspace = input.cwd === host.sourceRepository;
2033
+ if (host.os_sandbox === "bwrap") {
2034
+ const argv = ["--die-with-parent", "--new-session", "--unshare-net", "--unshare-pid", "--unshare-ipc", "--unshare-uts",
2035
+ "--ro-bind", "/", "/", "--dev", "/dev", "--proc", "/proc",
2036
+ ...["/tmp", "/run", "/home", "/root"].flatMap((path) => ["--tmpfs", path]),
2037
+ ...(host.sourceRepository ? ["--ro-bind", host.sourceRepository, host.sourceRepository] : []),
2038
+ ...(host.toolchainRead ?? []).flatMap((path) => ["--ro-bind", path, path]),
2039
+ ...(rootWorkspace ? ["--bind", input.cwd, input.cwd] : []),
2040
+ ...(hidden ? ["--tmpfs", hidden] : []),
2041
+ ...writable.filter((path) => !rootWorkspace || path !== input.cwd).flatMap((path) => ["--bind", path, path]), "--chdir", input.cwd, "--clearenv",
2042
+ ...Object.entries(input.env).flatMap(([key, value]) => value === undefined ? [] : ["--setenv", key, value]),
2043
+ "--", input.command, ...input.argv];
2044
+ return { ...direct, command: host.executable ?? "/usr/bin/bwrap", argv };
2045
+ }
2046
+ const subpaths = (paths) => [...new Set(paths)].map((path) => {
2047
+ const subpath = `(subpath ${JSON.stringify(path)})`;
2048
+ return hidden && hidden.startsWith(`${path}/`)
2049
+ ? `(require-all ${subpath} (require-not (subpath ${JSON.stringify(hidden)})))` : subpath;
2050
+ }).join(" ");
2051
+ const readPaths = [...writable, ...(host.sourceRepository ? [host.sourceRepository] : []),
2052
+ "/usr", "/bin", "/opt/homebrew", "/Library", "/System", "/private/tmp", "/private/var/folders", ...(host.toolchainRead ?? [])];
2053
+ const ancestors = new Set();
2054
+ for (const path of readPaths) {
2055
+ for (let parent = dirname(path);; parent = dirname(parent)) {
2056
+ ancestors.add(parent);
2057
+ if (parent === dirname(parent))
2058
+ break;
2059
+ }
2060
+ }
2061
+ const metadata = [...ancestors].map((path) => `(literal ${JSON.stringify(path)})`).join(" ");
2062
+ const profile = `(version 1) (allow default) (deny network*) (deny file-write*)
2063
+ (allow file-write* ${subpaths(writable)}) (allow file-write-data (literal "/dev/null"))
2064
+ (deny file-read* (subpath ${JSON.stringify(host.home)}) ${hidden ? `(subpath ${JSON.stringify(hidden)})` : ""})
2065
+ (allow file-read* ${subpaths(readPaths)}) (allow file-read-metadata ${metadata})`;
2066
+ return { ...direct, command: host.executable ?? "/usr/bin/sandbox-exec", argv: ["-p", profile, "--", input.command, ...input.argv] };
2067
+ }
2068
+ /** Resolve the worktree's own Git administration area without granting a sibling's metadata. */
2069
+ async function sandboxRepositoryPaths(cwd) {
2070
+ try {
2071
+ const pointer = await readFile(join(cwd, ".git"), "utf8");
2072
+ const match = /^gitdir: (.+)\s*$/.exec(pointer.trim());
2073
+ if (!match)
2074
+ return {};
2075
+ const metadata = await realpath(resolve(cwd, match[1]));
2076
+ const common = await realpath(resolve(metadata, (await readFile(join(metadata, "commondir"), "utf8")).trim()));
2077
+ const backlink = await realpath((await readFile(join(metadata, "gitdir"), "utf8")).trim());
2078
+ if (dirname(metadata) !== join(common, "worktrees") || backlink !== join(cwd, ".git"))
2079
+ return {};
2080
+ return { sourceRepository: dirname(common), worktreeMetadata: metadata };
2081
+ }
2082
+ catch {
2083
+ if (await stat(join(cwd, ".git")).then((entry) => entry.isDirectory(), () => false))
2084
+ return { sourceRepository: cwd };
2085
+ return {};
2086
+ }
2087
+ }
2088
+ /** A delivered instrument's nested runs stay inside its already isolated scratch directory. */
2089
+ export function untrustedCodeTempRoot(workspace) {
2090
+ return process.env.HOME && process.env.TMPDIR === join(process.env.HOME, "tmp")
2091
+ ? process.env.TMPDIR : join(workspace, ".conduit", "tmp");
2092
+ }
2093
+ async function sandboxPathDirectories(env) {
2094
+ const home = await realpath(homedir());
2095
+ const directories = new Set();
2096
+ for (const path of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
2097
+ try {
2098
+ const canonical = await realpath(path);
2099
+ if (canonical !== home && !home.startsWith(`${canonical}/`) && (await stat(canonical)).isDirectory())
2100
+ directories.add(canonical);
2101
+ }
2102
+ catch { /* A missing PATH entry must not stop execvp inside the read-restricted sandbox. */ }
2103
+ }
2104
+ return [...directories];
2105
+ }
2106
+ /** Existing PATH directories and Bridge's installed runtime, never an enclosing HOME. */
2107
+ async function sandboxToolchainPaths(env) {
2108
+ const home = await realpath(homedir());
2109
+ const paths = new Set();
2110
+ const add = async (path) => {
2111
+ try {
2112
+ const canonical = await realpath(path);
2113
+ if (canonical === home || home.startsWith(`${canonical}/`))
2114
+ return;
2115
+ if ((await stat(canonical)).isDirectory())
2116
+ paths.add(canonical);
2117
+ }
2118
+ catch { /* Absent PATH entries are not toolchain directories. */ }
2119
+ };
2120
+ const visited = new Set();
2121
+ for (const path of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
2122
+ await add(path);
2123
+ try {
2124
+ for (const entry of await readdir(path, { withFileTypes: true })) {
2125
+ if (!entry.isSymbolicLink())
2126
+ continue;
2127
+ const target = await realpath(join(path, entry.name));
2128
+ if (!(await stat(target)).isFile())
2129
+ continue;
2130
+ // An executable symlink can need its package's lib/ beside bin/, not the enclosing nvm tree.
2131
+ for (let parent = dirname(target); parent !== home && basename(parent) !== ".nvm"; parent = dirname(parent)) {
2132
+ if (visited.has(parent) || parent === dirname(parent))
2133
+ break;
2134
+ visited.add(parent);
2135
+ if (existsSync(join(parent, "package.json"))) {
2136
+ await add(parent);
2137
+ break;
2138
+ }
2139
+ }
2140
+ }
2141
+ }
2142
+ catch { /* An unreadable or vanished PATH entry contributes no extra authority. */ }
2143
+ }
2144
+ await add(fileURLToPath(new URL("..", import.meta.url)));
2145
+ // The loader and its dependencies are part of Bridge's installed runtime tree.
2146
+ try {
2147
+ const loader = await realpath(createRequire(import.meta.url).resolve("tsx"));
2148
+ const index = loader.lastIndexOf("/node_modules/");
2149
+ if (index >= 0)
2150
+ await add(loader.slice(0, index + "/node_modules".length));
2151
+ }
2152
+ catch { /* An absent loader is reported by the existing probe command. */ }
2153
+ return [...paths];
2154
+ }
2155
+ /** Same CR15 environment on unsupported hosts; isolated temp and mounts on supported hosts. */
2156
+ export async function untrustedCodeSpawn(input) {
2157
+ const sandbox = await probeUntrustedCodeSandbox();
2158
+ if (sandbox.os_sandbox === "none")
2159
+ return input;
2160
+ const cwd = await realpath(input.cwd);
2161
+ const candidates = input.command.includes("/") ? [resolve(cwd, input.command)]
2162
+ : (input.env.PATH ?? "/usr/bin:/bin").split(":").map((path) => resolve(cwd, path, input.command));
2163
+ let command;
2164
+ let unavailable = "ENOENT";
2165
+ for (const path of candidates) {
2166
+ // Keep the command's own name: a rustup proxy such as `cargo -> rustup` dispatches on argv[0].
2167
+ try {
2168
+ await access(path, constants.X_OK);
2169
+ command = join(await realpath(dirname(path)), basename(path));
2170
+ break;
2171
+ }
2172
+ catch (error) {
2173
+ if (error.code === "EACCES")
2174
+ unavailable = "EACCES";
2175
+ }
2176
+ }
2177
+ if (!command)
2178
+ throw Object.assign(new Error(`spawn ${input.command} ${unavailable}`), { code: unavailable });
2179
+ if (!input.env.HOME)
2180
+ throw new Error("Untrusted code requires a scratch HOME");
2181
+ const home = await realpath(input.env.HOME);
2182
+ const temp = join(home, "tmp");
2183
+ await mkdir(temp, { recursive: true });
2184
+ const env = { ...input.env, PATH: (await sandboxPathDirectories(input.env)).join(delimiter), HOME: home, XDG_CONFIG_HOME: join(home, "config"),
2185
+ XDG_CACHE_HOME: join(home, "cache"), XDG_DATA_HOME: join(home, "data"), TMPDIR: temp, TMP: temp, TEMP: temp };
2186
+ const writable = [cwd, home, temp];
2187
+ for (const key of ["CARGO_TARGET_DIR", "CARGO_HOME"]) {
2188
+ const path = input.env[key];
2189
+ if (!path)
2190
+ continue;
2191
+ await mkdir(path, { recursive: true });
2192
+ writable.push(await realpath(path));
2193
+ }
2194
+ const toolchainRead = await sandboxToolchainPaths(env);
2195
+ // Installed toolchains are read like PATH directories; a gate never rewrites rustc for the next attempt.
2196
+ if (input.env.RUSTUP_HOME && existsSync(input.env.RUSTUP_HOME))
2197
+ toolchainRead.push(await realpath(input.env.RUSTUP_HOME));
2198
+ return buildUntrustedCodeSpawn({ ...input, command, cwd, env, writable }, {
2199
+ ...sandbox, home: await realpath(homedir()), toolchainRead, ...await sandboxRepositoryPaths(cwd),
2200
+ });
2201
+ }
2202
+ const sandboxExecFile = promisify(execFile);
2203
+ let sandboxProbe;
2204
+ /** One non-spending probe per process; a missing or unusable primitive is a reported host fact. */
2205
+ export function probeUntrustedCodeSandbox() {
2206
+ return sandboxProbe ??= probeHostSandbox();
2207
+ }
2208
+ async function probeHostSandbox() {
2209
+ const os_sandbox = process.platform === "linux" ? "bwrap" : process.platform === "darwin" ? "seatbelt" : "none";
2210
+ const executable = os_sandbox === "bwrap" ? "/usr/bin/bwrap" : "/usr/bin/sandbox-exec";
2211
+ if (os_sandbox === "none" || !existsSync(executable))
2212
+ return { os_sandbox: "none", reason: "No supported OS sandbox executable is installed" };
2213
+ let scratch;
2214
+ const server = createServer((socket) => socket.end());
2215
+ try {
2216
+ scratch = await realpath(await mkdtemp(join(tmpdir(), "conduit-sandbox-probe-")));
2217
+ const host = { os_sandbox, executable, reason: "", home: await realpath(homedir()),
2218
+ toolchainRead: await sandboxToolchainPaths(process.env) };
2219
+ const env = untrustedCodeEnvironment(scratch);
2220
+ const run = async (command, argv) => {
2221
+ const spawn = buildUntrustedCodeSpawn({ command, argv, cwd: scratch, env, writable: [scratch] }, host);
2222
+ await sandboxExecFile(spawn.command, spawn.argv, { cwd: spawn.cwd, env: spawn.env, timeout: 5_000, maxBuffer: 4_000 });
2223
+ };
2224
+ await run("/usr/bin/true", []);
2225
+ // A live listener is the positive control: a closed port would fail even without isolation.
2226
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); });
2227
+ const address = server.address();
2228
+ if (!address || typeof address === "string")
2229
+ throw new Error("Sandbox network probe has no listener");
2230
+ await run(process.execPath, ["-e", `const s=require('node:net').connect(${address.port},'127.0.0.1');s.on('connect',()=>{s.destroy();process.exit(1)});s.on('error',()=>process.exit(0));s.setTimeout(2000,()=>{s.destroy();process.exit(0)});`]);
2231
+ return { os_sandbox, reason: "OS sandbox executed a child and blocked the network probe" };
2232
+ }
2233
+ catch (error) {
2234
+ const detail = error instanceof Error ? error.message : String(error);
2235
+ return { os_sandbox: "none", reason: detail.split("\n").find((line) => /bwrap:|sandbox-exec:/.test(line))?.trim().slice(0, 500)
2236
+ ?? detail.split("\n")[0].slice(0, 500) };
2237
+ }
2238
+ finally {
2239
+ if (server.listening)
2240
+ await new Promise((resolve) => server.close(() => resolve()));
2241
+ if (scratch)
2242
+ await rm(scratch, { recursive: true, force: true });
2243
+ }
2244
+ }
1997
2245
  /** The `/v1` a fuelled agent is pointed at — and the one its model names are validated against. */
1998
2246
  export function fuelEndpoint(baseUrl) {
1999
2247
  return `${baseUrl.replace(/\/+$/, "")}/v1`;
@@ -4,8 +4,20 @@
4
4
  * it never substitutes for it.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
- import { boundedEnvironment, criterionKey, resolveCriterionReference, signalGroup, STDIO_DRAIN_MS, CHILD_STOP_GRACE_MS, OWN_PROCESS_GROUP, trackProcessGroup, untrackProcessGroup } from "./driver.js";
7
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { untrustedCodeTempRoot, untrustedCodeSpawn, untrustedCodeEnvironment, criterionKey, resolveCriterionReference, signalGroup, STDIO_DRAIN_MS, CHILD_STOP_GRACE_MS, OWN_PROCESS_GROUP, trackProcessGroup, untrackProcessGroup } from "./driver.js";
8
10
  import { isBoundedVerificationCommand, isRunnableVerificationCommand } from "./execution-class.js";
11
+ import { workspaceDependenciesMissingFailure } from "./failure-signal.js";
12
+ /** A typed spawn refusal, distinct from a command that ran and failed. */
13
+ export class VerificationCommandUnavailableError extends Error {
14
+ failure;
15
+ constructor(command, code, detail) {
16
+ super(`Verification command could not start: ${command} (${code}): ${detail}`);
17
+ this.name = "VerificationCommandUnavailableError";
18
+ this.failure = workspaceDependenciesMissingFailure(this.message);
19
+ }
20
+ }
9
21
  const TEST_EVIDENCE_DETAILS_MIN = 32;
10
22
  /** Cold `cargo test` in an attempt worktree routinely exceeds two minutes of compile. */
11
23
  export const VERIFICATION_TIMEOUT_MS = 600_000;
@@ -437,8 +449,19 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
437
449
  partial[stream].append(parts.at(-1));
438
450
  };
439
451
  const output = () => [lines.text().replace(/\n$/, ""), partial.stdout.text(), partial.stderr.text()].filter(Boolean).join("\n");
452
+ const tempRoot = untrustedCodeTempRoot(workspace);
453
+ await mkdir(tempRoot, { recursive: true });
454
+ const scratchHome = await mkdtemp(join(tempRoot, "gate-home-"));
455
+ const commandSpawn = await untrustedCodeSpawn({ command: bin, argv: argv.slice(1), cwd: workspace, env: untrustedCodeEnvironment(scratchHome) })
456
+ .catch(async (error) => {
457
+ await rm(scratchHome, { recursive: true, force: true });
458
+ const unavailable = error;
459
+ if (typeof unavailable.code === "string")
460
+ throw new VerificationCommandUnavailableError(command, unavailable.code, unavailable.message);
461
+ throw error;
462
+ });
440
463
  return new Promise((resolve, reject) => {
441
- const child = spawn(bin, argv.slice(1), { cwd: workspace, env: boundedEnvironment(),
464
+ const child = spawn(commandSpawn.command, commandSpawn.argv, { cwd: commandSpawn.cwd, env: commandSpawn.env,
442
465
  stdio: ["ignore", "pipe", "pipe"], detached: OWN_PROCESS_GROUP });
443
466
  const group = OWN_PROCESS_GROUP ? child.pid : undefined;
444
467
  if (group !== undefined)
@@ -494,7 +517,7 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
494
517
  if (typeof error.code === "string") {
495
518
  settled = true;
496
519
  cleanup();
497
- reject(error);
520
+ reject(new VerificationCommandUnavailableError(command, error.code, error.message));
498
521
  }
499
522
  else {
500
523
  if (!stderr.text())
@@ -507,7 +530,7 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
507
530
  clearTimeout(timer);
508
531
  drainTimer = setTimeout(() => settle(code), STDIO_DRAIN_MS);
509
532
  });
510
- });
533
+ }).finally(() => rm(scratchHome, { recursive: true, force: true }));
511
534
  }
512
535
  /**
513
536
  * Bridge may attach genuine command output; it must not invent which criteria that output proves.
package/dist/execution.js CHANGED
@@ -31,7 +31,7 @@ import { agentClaimsRepositoryWork, claimsVsGitMismatch, isLandTreeEmpty, report
31
31
  const execFileAsync = promisify(execFile);
32
32
  const deliveryReportSchemaPath = fileURLToPath(new URL("../schemas/delivery-report.schema.json", import.meta.url));
33
33
  const repairBriefSchemaPath = fileURLToPath(new URL("../schemas/repair-brief.schema.json", import.meta.url));
34
- import { boundedTail, captureVerificationFailure, DeliveryReportDefectError, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationFailedError, VerificationGateRanNothingError, VerificationGateTimedOutError, VERIFICATION_TIMEOUT_MS } from "./ensure-test-evidence.js";
34
+ import { boundedTail, captureVerificationFailure, DeliveryReportDefectError, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationCommandUnavailableError, VerificationFailedError, VerificationGateRanNothingError, VerificationGateTimedOutError, VERIFICATION_TIMEOUT_MS } from "./ensure-test-evidence.js";
35
35
  import { ensureChangeEvidence } from "./ensure-change-evidence.js";
36
36
  import { ensureDemonstrationEvidence } from "./ensure-demonstration-evidence.js";
37
37
  import { ensureNormativeEvidence } from "./ensure-normative-evidence.js";
@@ -1100,6 +1100,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1100
1100
  // of the failed code would make the durable retry mechanically impossible.
1101
1101
  let retainAttemptWorktree = diagnosis;
1102
1102
  const reportVerificationGateTimeout = async (input) => {
1103
+ retainAttemptWorktree = true;
1103
1104
  const response = await queueTerminal(client, taskId, { action: "fail", body: {
1104
1105
  error: input.detail,
1105
1106
  retryable: false,
@@ -1107,12 +1108,13 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1107
1108
  ...(input.pullRequestUrl ? { pull_request_url: input.pullRequestUrl } : {}),
1108
1109
  idempotency_key: `bridge:verification-gate-timeout:${active.attemptId}`,
1109
1110
  } });
1110
- retainAttemptWorktree = response.retain_worktree === true;
1111
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1111
1112
  console.error(`Assignment ${taskId} verification gate did not finish: ${redactSecrets(input.detail)}`);
1112
1113
  };
1113
1114
  if (diagnosis) {
1114
1115
  if (!sourceAttemptId) {
1115
- await queueTerminal(client, taskId, {
1116
+ retainAttemptWorktree = true;
1117
+ const response = await queueTerminal(client, taskId, {
1116
1118
  action: "fail",
1117
1119
  body: {
1118
1120
  failure: {
@@ -1129,11 +1131,13 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1129
1131
  idempotency_key: `bridge:diagnosis-source-missing:${active.attemptId}`,
1130
1132
  },
1131
1133
  });
1134
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1132
1135
  return;
1133
1136
  }
1134
1137
  const retained = options.existingWorktree ?? attemptWorktreePath(workspace, sourceAttemptId);
1135
1138
  if (!(await proveResumeWorktree(workspace, sourceAttemptId, retained))) {
1136
- await queueTerminal(client, taskId, {
1139
+ retainAttemptWorktree = true;
1140
+ const response = await queueTerminal(client, taskId, {
1137
1141
  action: "fail",
1138
1142
  body: {
1139
1143
  failure: {
@@ -1150,6 +1154,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1150
1154
  idempotency_key: `bridge:diagnosis-worktree-missing:${active.attemptId}`,
1151
1155
  },
1152
1156
  });
1157
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1153
1158
  return;
1154
1159
  }
1155
1160
  attemptWorkspace = retained;
@@ -1161,13 +1166,17 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1161
1166
  }
1162
1167
  else {
1163
1168
  if (!startCommit) {
1164
- await queueTerminal(client, taskId, { action: "fail", body: { error: "No start commit for attempt worktree", retryable: true, failure: bridgeCause("agent_execution_failed", "No start commit for attempt worktree"), idempotency_key: `bridge:no-start-commit:${active.attemptId}` } });
1169
+ retainAttemptWorktree = true;
1170
+ const response = await queueTerminal(client, taskId, { action: "fail", body: { error: "No start commit for attempt worktree", retryable: true, failure: bridgeCause("agent_execution_failed", "No start commit for attempt worktree"), idempotency_key: `bridge:no-start-commit:${active.attemptId}` } });
1171
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1165
1172
  return;
1166
1173
  }
1167
1174
  if (executionContract.requested_base_commit) {
1168
1175
  const resolved = await resolveAttemptStartCommit(workspace, executionContract.requested_base_commit, startCommit);
1169
1176
  if (!resolved) {
1170
- await queueTerminal(client, taskId, { action: "fail", body: { error: "Required base commit is not available in this workspace", retryable: true, failure: bridgeCause("base_not_ancestor", "Required base commit is not available in this workspace"), idempotency_key: `bridge:base-not-ancestor:${active.attemptId}` } });
1177
+ retainAttemptWorktree = true;
1178
+ const response = await queueTerminal(client, taskId, { action: "fail", body: { error: "Required base commit is not available in this workspace", retryable: true, failure: bridgeCause("base_not_ancestor", "Required base commit is not available in this workspace"), idempotency_key: `bridge:base-not-ancestor:${active.attemptId}` } });
1179
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1171
1180
  return;
1172
1181
  }
1173
1182
  worktreeStart = resolved;
@@ -1175,7 +1184,9 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1175
1184
  else if (liveBrief?.base_commit && liveBrief.base_commit !== startCommit) {
1176
1185
  const reachable = await ensureCommitAvailable(workspace, startCommit).catch(() => false);
1177
1186
  if (!reachable) {
1178
- await queueTerminal(client, taskId, { action: "fail", body: { error: "Delivered head is not available in this workspace", retryable: true, failure: bridgeCause("agent_execution_failed", "Delivered head is not available in this workspace"), idempotency_key: `bridge:workspace-mismatch:${active.attemptId}` } });
1187
+ retainAttemptWorktree = true;
1188
+ const response = await queueTerminal(client, taskId, { action: "fail", body: { error: "Delivered head is not available in this workspace", retryable: true, failure: bridgeCause("agent_execution_failed", "Delivered head is not available in this workspace"), idempotency_key: `bridge:workspace-mismatch:${active.attemptId}` } });
1189
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1179
1190
  console.error(`Assignment ${taskId} preflight failed: Delivered head is not available in this workspace`);
1180
1191
  return;
1181
1192
  }
@@ -1192,12 +1203,19 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1192
1203
  const message = code === "source_workspace_dirty"
1193
1204
  ? "Source workspace has uncommitted changes; refuse to start an isolated attempt"
1194
1205
  : `Unable to create attempt worktree: ${code}`;
1195
- await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: true, ...(code === "source_workspace_dirty" ? { failure: bridgeCause("workspace_dirty", message) } : {}), idempotency_key: `bridge:worktree:${active.attemptId}:${code}` } });
1206
+ retainAttemptWorktree = true;
1207
+ const response = await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: true, ...(code === "source_workspace_dirty" ? { failure: bridgeCause("workspace_dirty", message) } : {}), idempotency_key: `bridge:worktree:${active.attemptId}:${code}` } });
1208
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1196
1209
  console.error(`Assignment ${taskId} worktree create failed: ${redactSecrets(message)}`);
1197
1210
  return;
1198
1211
  }
1199
1212
  await client.updateAttempt(taskId, { worktreePath: attemptWorkspace });
1200
1213
  }
1214
+ const removeReleasedPreflightWorktree = async () => {
1215
+ if (!retainAttemptWorktree) {
1216
+ await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1217
+ }
1218
+ };
1201
1219
  // T136: a first revision's witness starts at the commit the attempt worktree actually checked
1202
1220
  // out. When main has moved past the requested base, that resolved start is the claimed head;
1203
1221
  // diffing from the requested base would report commits that predate the agent as its work.
@@ -1235,7 +1253,8 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1235
1253
  const message = error instanceof Error ? error.message : String(error);
1236
1254
  const retryable = message.startsWith(NORMATIVE_REF_PREFIX)
1237
1255
  && /could not clone|could not checkout|timed out|Could not resolve|connection/i.test(message);
1238
- await queueTerminal(client, taskId, {
1256
+ retainAttemptWorktree = true;
1257
+ const response = await queueTerminal(client, taskId, {
1239
1258
  action: "fail",
1240
1259
  body: {
1241
1260
  error: message,
@@ -1246,6 +1265,8 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1246
1265
  idempotency_key: `bridge:normative-ref:${active.attemptId}`,
1247
1266
  },
1248
1267
  });
1268
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1269
+ await removeReleasedPreflightWorktree();
1249
1270
  console.error(`Assignment ${taskId} normative reference materialization failed: ${redactSecrets(message)}`);
1250
1271
  return;
1251
1272
  }
@@ -1257,11 +1278,6 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1257
1278
  // pins the attempt's verification_commands_digest to the tree the gates actually ran in, so a
1258
1279
  // later refresh of the source checkout cannot invalidate a running attempt.
1259
1280
  const attemptBrief = await buildWorkspaceBrief(attemptWorkspace).catch(() => liveBrief);
1260
- const removeReleasedDiagnosticWorktree = async (response) => {
1261
- if (diagnosis && !retainDiagnosticWorktree(response)) {
1262
- await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1263
- }
1264
- };
1265
1281
  // Stamped class is the source of truth (Bridge 0.11+). Do not re-derive from grants.
1266
1282
  let executionClass;
1267
1283
  try {
@@ -1272,21 +1288,26 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1272
1288
  console.error(`Assignment ${taskId}: ${detail}`);
1273
1289
  // Non-retryable: control plane restamps on dispatch/claim. Infinite retry here burned path-4
1274
1290
  // attempt budget when a pre-inversion task row still lacked the stamp.
1291
+ retainAttemptWorktree = true;
1275
1292
  const response = await queueTerminal(client, taskId, {
1276
1293
  action: "fail",
1277
1294
  body: { error: detail, retryable: false, failure: bridgeCause("execution_unclassified", detail), idempotency_key: `bridge:fail:${active.attemptId}:missing-class` },
1278
1295
  });
1279
- await removeReleasedDiagnosticWorktree(response);
1296
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1297
+ await removeReleasedPreflightWorktree();
1280
1298
  return;
1281
1299
  }
1282
1300
  // An artifact deliverable that is not publish_artifact cannot write/publish — fail closed.
1283
1301
  if (artifactDelivery && executionClass !== "publish_artifact") {
1284
1302
  const detail = `Artifact deliverable stamped as execution class ${executionClass}, which cannot write or publish it. Revise the plan to use publish_artifact.`;
1285
1303
  console.error(`Assignment ${taskId}: ${detail}`);
1286
- await queueTerminal(client, taskId, {
1304
+ retainAttemptWorktree = true;
1305
+ const response = await queueTerminal(client, taskId, {
1287
1306
  action: "fail",
1288
1307
  body: { error: detail, retryable: false, failure: bridgeCause("execution_unclassified", detail), idempotency_key: `bridge:fail:${active.attemptId}:artifact-class` },
1289
1308
  });
1309
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1310
+ await removeReleasedPreflightWorktree();
1290
1311
  return;
1291
1312
  }
1292
1313
  // Full classFloor must survive project/machine ceilings (observe/verify need repo_read too).
@@ -1294,16 +1315,19 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1294
1315
  if (!floor.ok) {
1295
1316
  const detail = `Class floor unmet: ${floor.reason}`;
1296
1317
  console.error(`Assignment ${taskId}: ${detail}`);
1318
+ retainAttemptWorktree = true;
1297
1319
  const response = await queueTerminal(client, taskId, {
1298
1320
  action: "fail",
1299
1321
  body: { error: detail, retryable: false, failure: bridgeCause("execution_unclassified", detail), idempotency_key: `bridge:fail:${active.attemptId}:class-floor` },
1300
1322
  });
1301
- await removeReleasedDiagnosticWorktree(response);
1323
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1324
+ await removeReleasedPreflightWorktree();
1302
1325
  return;
1303
1326
  }
1304
1327
  const builtInDriverId = Object.entries(DRIVERS).find(([, candidate]) => candidate === driver)?.[0] ?? null;
1305
1328
  if (diagnosis && builtInDriverId && !supportsReadOnlyDiagnosis(builtInDriverId)) {
1306
1329
  const detail = `${driver.name} cannot combine bounded test execution with enforced read-only repository access.`;
1330
+ retainAttemptWorktree = true;
1307
1331
  const response = await queueTerminal(client, taskId, {
1308
1332
  action: "fail",
1309
1333
  body: {
@@ -1321,12 +1345,14 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1321
1345
  idempotency_key: `bridge:diagnosis-driver-authority:${active.attemptId}`,
1322
1346
  },
1323
1347
  });
1324
- await removeReleasedDiagnosticWorktree(response);
1348
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1349
+ await removeReleasedPreflightWorktree();
1325
1350
  console.error(`Assignment ${taskId} diagnosis held: ${detail}`);
1326
1351
  return;
1327
1352
  }
1328
1353
  const diagnosticFailure = workPackage?.diagnostic_context?.failure_output;
1329
1354
  if (diagnosis && !diagnosticFailure) {
1355
+ retainAttemptWorktree = true;
1330
1356
  const response = await queueTerminal(client, taskId, {
1331
1357
  action: "fail",
1332
1358
  body: {
@@ -1336,7 +1362,8 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1336
1362
  idempotency_key: `bridge:diagnosis-context-missing:${active.attemptId}`,
1337
1363
  },
1338
1364
  });
1339
- await removeReleasedDiagnosticWorktree(response);
1365
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1366
+ await removeReleasedPreflightWorktree();
1340
1367
  return;
1341
1368
  }
1342
1369
  const evidenceCommands = verificationEvidenceCommands(attemptBrief?.verification ?? [], attemptBrief?.declared_verification ?? [], spec.acceptance ?? []);
@@ -1347,6 +1374,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1347
1374
  // reads the same missing_verification Hold.
1348
1375
  if (executionKind === "authoring" && needsTestEvidence(spec, grants) && !evidenceCommands.length) {
1349
1376
  const detail = missingVerificationDetail(worktreeStart);
1377
+ retainAttemptWorktree = true;
1350
1378
  const response = await queueTerminal(client, taskId, {
1351
1379
  action: "fail",
1352
1380
  body: {
@@ -1356,9 +1384,10 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1356
1384
  idempotency_key: `bridge:missing-verification:${active.attemptId}`,
1357
1385
  },
1358
1386
  });
1387
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1359
1388
  // A fresh tree stands at the base commit and no agent ran, so it holds no diagnostic value. A
1360
1389
  // resumed tree holds the agent's own work and is kept.
1361
- if (!options.existingWorktree && response.retain_worktree !== true) {
1390
+ if (!options.existingWorktree && !retainAttemptWorktree) {
1362
1391
  await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1363
1392
  }
1364
1393
  console.error(`Assignment ${taskId} refused before the agent started: ${redactSecrets(detail)}`);
@@ -1382,7 +1411,22 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1382
1411
  declaredVerificationCommands: attemptBrief?.declared_verification ?? [],
1383
1412
  acceptance: spec.acceptance ?? [],
1384
1413
  changeScope: spec.change_scope ?? [],
1414
+ }).catch(async (error) => {
1415
+ if (!(error instanceof VerificationCommandUnavailableError))
1416
+ throw error;
1417
+ retainAttemptWorktree = true;
1418
+ const response = await queueTerminal(client, taskId, { action: "fail", body: {
1419
+ error: error.message, retryable: false, failure: error.failure,
1420
+ idempotency_key: `bridge:verification-command-unavailable:${active.attemptId}`,
1421
+ } });
1422
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1423
+ if (!retainAttemptWorktree)
1424
+ await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1425
+ console.error(`Assignment ${taskId} refused before the agent started: ${redactSecrets(error.message)}`);
1426
+ return null;
1385
1427
  });
1428
+ if (!dryRun)
1429
+ return;
1386
1430
  if (dryRun.ranNothing) {
1387
1431
  const { command, exitCode, reason } = dryRun.ranNothing;
1388
1432
  const detail = `Verification gate ran nothing on the base commit ${worktreeStart}: ${command} (${reason}, exit ${exitCode})`;
@@ -1397,6 +1441,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1397
1441
  exit_code: exitCode,
1398
1442
  vendor_message: reason,
1399
1443
  };
1444
+ retainAttemptWorktree = true;
1400
1445
  const response = await queueTerminal(client, taskId, {
1401
1446
  action: "fail",
1402
1447
  body: {
@@ -1406,8 +1451,9 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1406
1451
  idempotency_key: `bridge:verification-gate-ran-nothing:${active.attemptId}`,
1407
1452
  },
1408
1453
  });
1454
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1409
1455
  // No agent ran and the tree stands at the base commit, so it holds no diagnostic value.
1410
- if (response.retain_worktree !== true) {
1456
+ if (!retainAttemptWorktree) {
1411
1457
  await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1412
1458
  }
1413
1459
  console.error(`Assignment ${taskId} refused before the agent started: ${redactSecrets(detail)}`);
@@ -1416,6 +1462,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1416
1462
  if (dryRun.timedOut) {
1417
1463
  const { command, stderrTail, stdoutTail } = dryRun.timedOut;
1418
1464
  const detail = `Verification gate did not finish on the base commit ${worktreeStart}: ${command} (stopped after ${Math.round(VERIFICATION_TIMEOUT_MS / 60_000)} minutes)`;
1465
+ retainAttemptWorktree = true;
1419
1466
  const response = await queueTerminal(client, taskId, {
1420
1467
  action: "fail",
1421
1468
  body: {
@@ -1425,8 +1472,9 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1425
1472
  idempotency_key: `bridge:verification-gate-timeout:${active.attemptId}`,
1426
1473
  },
1427
1474
  });
1475
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1428
1476
  // No agent ran and the tree stands at the base commit, so it holds no diagnostic value.
1429
- if (response.retain_worktree !== true) {
1477
+ if (!retainAttemptWorktree) {
1430
1478
  await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1431
1479
  }
1432
1480
  console.error(`Assignment ${taskId} refused before the agent started: ${redactSecrets(detail)}`);
@@ -1435,6 +1483,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1435
1483
  if (dryRun.redBase) {
1436
1484
  const { command, exitCode, stderrTail, stdoutTail } = dryRun.redBase;
1437
1485
  const detail = `Verification fails on the base commit ${worktreeStart}: ${command} (exit ${exitCode ?? "unknown"})`;
1486
+ retainAttemptWorktree = true;
1438
1487
  const response = await queueTerminal(client, taskId, {
1439
1488
  action: "fail",
1440
1489
  body: {
@@ -1444,8 +1493,9 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1444
1493
  idempotency_key: `bridge:verification-red-base:${active.attemptId}`,
1445
1494
  },
1446
1495
  });
1496
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1447
1497
  // No agent ran and the tree stands at the base commit, so it holds no diagnostic value.
1448
- if (response.retain_worktree !== true) {
1498
+ if (!retainAttemptWorktree) {
1449
1499
  await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1450
1500
  }
1451
1501
  console.error(`Assignment ${taskId} refused before the agent started: ${redactSecrets(detail)}`);
@@ -1453,6 +1503,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1453
1503
  }
1454
1504
  if (dryRun.outsideScope.length) {
1455
1505
  const detail = verificationScopeConflictDetail(dryRun.outsideScope, spec.change_scope ?? []);
1506
+ retainAttemptWorktree = true;
1456
1507
  const response = await queueTerminal(client, taskId, {
1457
1508
  action: "fail",
1458
1509
  body: {
@@ -1462,9 +1513,10 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1462
1513
  idempotency_key: `bridge:verification-scope-conflict:${active.attemptId}`,
1463
1514
  },
1464
1515
  });
1516
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1465
1517
  // No agent ran and the tree stands at the base commit, so it holds no diagnostic value.
1466
1518
  // Keep it only when the control plane says it still needs it.
1467
- if (response.retain_worktree !== true) {
1519
+ if (!retainAttemptWorktree) {
1468
1520
  await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1469
1521
  }
1470
1522
  console.error(`Assignment ${taskId} refused before the agent started: ${redactSecrets(detail)}`);
@@ -1499,6 +1551,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1499
1551
  const diagnosisFingerprintBefore = diagnosis ? await worktreeStateFingerprint(attemptWorkspace) : null;
1500
1552
  if (diagnosis && diagnosisFingerprintBefore === null) {
1501
1553
  const detail = "Bridge could not fingerprint the retained worktree before read-only diagnosis.";
1554
+ retainAttemptWorktree = true;
1502
1555
  const response = await queueTerminal(client, taskId, {
1503
1556
  action: "fail",
1504
1557
  body: {
@@ -1517,6 +1570,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1517
1570
  },
1518
1571
  });
1519
1572
  retainAttemptWorktree = retainDiagnosticWorktree(response);
1573
+ await removeReleasedPreflightWorktree();
1520
1574
  console.error(`Assignment ${taskId} diagnosis held: ${detail}`);
1521
1575
  return;
1522
1576
  }
@@ -1539,6 +1593,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1539
1593
  const pinnedFacts = active.phase === "agent_running" ? active.executionFacts : undefined;
1540
1594
  const executionFacts = pinnedFacts ? resumeExecutionFacts(pinnedFacts, observedFacts) : observedFacts;
1541
1595
  const verificationCommands = pinnedFacts ? active.verificationCommands ?? [] : attemptBrief?.verification ?? [];
1596
+ const declaredVerificationCommands = pinnedFacts ? active.declaredVerificationCommands ?? [] : attemptBrief?.declared_verification ?? [];
1542
1597
  // A resumed attempt must still be doing the work it claimed. When a pinned fact moved — the
1543
1598
  // verification command list, the repository, the base commit — the attempt is over: report it
1544
1599
  // once with the fact named, and let the owner's Retry pin the facts again.
@@ -1549,7 +1604,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1549
1604
  }
1550
1605
  // The command list is pinned with the digest it produced: on a later turn the report can name the
1551
1606
  // gate that moved instead of showing two hex strings.
1552
- await client.updateAttempt(taskId, { executionFacts, verificationCommands });
1607
+ await client.updateAttempt(taskId, { executionFacts, verificationCommands, declaredVerificationCommands });
1553
1608
  await client.attemptRequest(taskId, "progress", {
1554
1609
  phase: diagnosis ? "inspecting" : "agent_running",
1555
1610
  execution_facts: executionFacts,
@@ -1629,18 +1684,18 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1629
1684
  // the retry resumes from the head reported here instead of starting over at the base. The tree
1630
1685
  // stays on this computer, where that head is always reachable.
1631
1686
  const reportTurnTimeout = async (message) => {
1632
- retainAttemptWorktree = true;
1633
1687
  const resume = diagnosis || machineBoundTimeout(agentTimeout) ? null : await checkpointWorkInProgress({
1634
1688
  workspace: attemptWorkspace, spec, grants, baseCommit: deliveryBaseCommit,
1635
1689
  }).catch((error) => {
1636
1690
  console.error(`Work in progress was not committed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
1637
1691
  return null;
1638
1692
  });
1693
+ retainAttemptWorktree = true;
1639
1694
  const response = await queueTerminal(client, taskId, {
1640
1695
  action: "fail",
1641
1696
  body: timeoutFailureBody(agentTimeout, active.attemptId, resume),
1642
1697
  });
1643
- retainAttemptWorktree = Boolean(resume) || response.retain_worktree === true;
1698
+ retainAttemptWorktree = Boolean(resume) || retainDiagnosticWorktree(response);
1644
1699
  console.error(`Assignment ${taskId} timed out: ${redactSecrets(message)}`);
1645
1700
  };
1646
1701
  const failAttemptFromDriverResult = async (result) => {
@@ -1678,7 +1733,6 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1678
1733
  })
1679
1734
  : null;
1680
1735
  if (capturedVerification?.timedOut) {
1681
- retainAttemptWorktree = true;
1682
1736
  await reportVerificationGateTimeout({ command: capturedVerification.command, detail: capturedVerification.detail,
1683
1737
  stderr: capturedVerification.stderr ?? "", stdout: capturedVerification.stdout ?? "" });
1684
1738
  return retainAttemptWorktree;
@@ -1707,7 +1761,6 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1707
1761
  // the same run and must not silently reclassify an unretryable failure.
1708
1762
  // Keep the tree until the replay-safe terminal response explicitly says whether diagnosis was
1709
1763
  // queued. A network fault here must not delete the evidence before Bridge can replay terminal.
1710
- retainAttemptWorktree = true;
1711
1764
  // The driver's typed cause rides beside the prose, never instead of it. A recovered
1712
1765
  // verification log describes the same run: the signal becomes Bridge's verification witness
1713
1766
  // and the prose carries the evidence, so the verify class stays typed without new tables.
@@ -1722,6 +1775,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1722
1775
  ? { ...result.signal, phase: "verification", witness: "bridge", kind: "verification_failed", command: capturedVerification.command, stderr_tail: undefined, stdout_tail: undefined, ...(diffStat ? { diff_stat: diffStat } : {}) }
1723
1776
  : result.signal
1724
1777
  : undefined;
1778
+ retainAttemptWorktree = true;
1725
1779
  const response = await queueTerminal(client, taskId, { action: "fail", body: {
1726
1780
  error: message,
1727
1781
  retryable: bindingRequired ? false : retryableAgentFailure(agentMessage),
@@ -1734,7 +1788,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1734
1788
  ...(signal ? { signal } : {}),
1735
1789
  idempotency_key: `bridge:fail:${active.attemptId}`,
1736
1790
  } });
1737
- retainAttemptWorktree = response.retain_worktree === true;
1791
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
1738
1792
  console.error(`Assignment ${taskId} failed: ${redactSecrets(message)}`);
1739
1793
  return retainAttemptWorktree;
1740
1794
  };
@@ -1842,6 +1896,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1842
1896
  const detail = diagnosisWitness === "changed"
1843
1897
  ? "The diagnostic lane changed the retained worktree during a read-only run."
1844
1898
  : "Bridge could not fingerprint the retained worktree after read-only diagnosis.";
1899
+ retainAttemptWorktree = true;
1845
1900
  const response = await queueTerminal(client, taskId, {
1846
1901
  action: "fail",
1847
1902
  body: {
@@ -1870,6 +1925,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1870
1925
  // A spent allowance keeps its prose reading here (the control plane quotes the reset clock
1871
1926
  // from it); a computer fact in the CLI's words is named by code, as on the authoring path.
1872
1927
  const diagnosisCause = providerQuotaRefusal(message) ? null : agentOutputCause(message);
1928
+ retainAttemptWorktree = true;
1873
1929
  const response = await queueTerminal(client, taskId, {
1874
1930
  action: "fail",
1875
1931
  body: {
@@ -1892,6 +1948,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1892
1948
  }
1893
1949
  catch (error) {
1894
1950
  const message = error instanceof Error ? error.message : "Diagnostic RepairBrief was invalid";
1951
+ retainAttemptWorktree = true;
1895
1952
  const response = await queueTerminal(client, taskId, {
1896
1953
  action: "fail",
1897
1954
  body: {
@@ -1904,6 +1961,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1904
1961
  console.error(`Assignment ${taskId} diagnosis returned no usable brief: ${redactSecrets(message)}`);
1905
1962
  return;
1906
1963
  }
1964
+ retainAttemptWorktree = true;
1907
1965
  const response = await queueTerminal(client, taskId, {
1908
1966
  action: "complete",
1909
1967
  body: {
@@ -1997,6 +2055,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1997
2055
  if (await isLandTreeEmpty({ workspace: attemptWorkspace, spec, grants, baseCommit: deliveryBaseCommit })) {
1998
2056
  const base = spec.repository?.base_commit ?? "unknown";
1999
2057
  const classified = classifyFinalizeFailure(agentNoLandCommitMessage(base));
2058
+ retainAttemptWorktree = true;
2000
2059
  const response = await queueTerminal(client, taskId, {
2001
2060
  action: "fail",
2002
2061
  body: {
@@ -2016,6 +2075,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2016
2075
  else {
2017
2076
  const base = spec.repository?.base_commit ?? "unknown";
2018
2077
  const classified = classifyFinalizeFailure(agentNoLandCommitMessage(base));
2078
+ retainAttemptWorktree = true;
2019
2079
  const response = await queueTerminal(client, taskId, {
2020
2080
  action: "fail",
2021
2081
  body: {
@@ -2062,7 +2122,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2062
2122
  idempotency_key: `bridge:no-delivery-report:${active.attemptId}`,
2063
2123
  },
2064
2124
  });
2065
- retainAttemptWorktree = response.retain_worktree === true;
2125
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
2066
2126
  console.error(`Assignment ${taskId} attempted no delivery report: ${redactSecrets(detail)}`);
2067
2127
  return;
2068
2128
  }
@@ -2177,6 +2237,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2177
2237
  if (await isLandTreeEmpty({ workspace: attemptWorkspace, spec, grants, baseCommit: deliveryBaseCommit })) {
2178
2238
  const base = spec.repository?.base_commit ?? "unknown";
2179
2239
  const classified = classifyFinalizeFailure(agentNoLandCommitMessage(base));
2240
+ retainAttemptWorktree = true;
2180
2241
  const response = await queueTerminal(client, taskId, {
2181
2242
  action: "fail",
2182
2243
  body: {
@@ -2208,7 +2269,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2208
2269
  driver,
2209
2270
  executionClass,
2210
2271
  capabilities: spec.required_capabilities ?? [],
2211
- verificationCommands: attemptBrief?.verification ?? [],
2272
+ verificationCommands,
2212
2273
  objective: task.objective,
2213
2274
  selection,
2214
2275
  fuel,
@@ -2257,8 +2318,8 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2257
2318
  report,
2258
2319
  spec,
2259
2320
  grants,
2260
- verificationCommands: attemptBrief?.verification ?? [],
2261
- declaredVerificationCommands: attemptBrief?.declared_verification ?? [],
2321
+ verificationCommands,
2322
+ declaredVerificationCommands,
2262
2323
  });
2263
2324
  report = await ensureChangeEvidence({
2264
2325
  workspace: attemptWorkspace,
@@ -2308,7 +2369,6 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2308
2369
  // never a repair attempt or a blind retry, which is what burned six deliveries on 2026-09-04
2309
2370
  // against a bare `python3 -m unittest` that discovered nothing.
2310
2371
  if (error instanceof VerificationGateRanNothingError) {
2311
- retainAttemptWorktree = true;
2312
2372
  const signal = {
2313
2373
  phase: "verification",
2314
2374
  witness: "bridge",
@@ -2317,6 +2377,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2317
2377
  exit_code: error.exitCode,
2318
2378
  vendor_message: error.vendorMessage,
2319
2379
  };
2380
+ retainAttemptWorktree = true;
2320
2381
  const response = await queueTerminal(client, taskId, {
2321
2382
  action: "fail",
2322
2383
  body: {
@@ -2329,7 +2390,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2329
2390
  idempotency_key: `bridge:invalid-delivery:${active.attemptId}`,
2330
2391
  },
2331
2392
  });
2332
- retainAttemptWorktree = response.retain_worktree === true;
2393
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
2333
2394
  console.error(`Assignment ${taskId} verification gate ran nothing: ${redactSecrets(error.message)}`);
2334
2395
  const gateReplyTail = reportText.slice(-8_000);
2335
2396
  console.error(`Assignment ${taskId} agent reply tail (${reportText.length} chars total, redacted): ${redactSecrets(gateReplyTail) || "<empty>"}`);
@@ -2350,15 +2411,16 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2350
2411
  // A report that fell short of the delivery contract is named by its type and travels as the
2351
2412
  // delivery_report signal, so repair routing never reads this message (K3, debt 18 item 3c).
2352
2413
  const reportDefect = error instanceof DeliveryReportDefectError;
2353
- const classified = verificationFailed
2354
- ? { retryable: true, error: `Verification failed after the agent run.\n${message}${diffStat ? `\n\nChanged files:\n${diffStat}` : ""}` }
2355
- : reportDefect
2356
- ? { retryable: false, error: message }
2357
- // classifyFinalizeFailure: forge transport → retryable; contract/environment/unknown → fail
2358
- // closed (unknown prefixed as "Bridge finalize interrupted", not laundered as contract).
2359
- : classifyFinalizeFailure(message);
2360
- if (verificationFailed)
2361
- retainAttemptWorktree = true;
2414
+ const classified = error instanceof VerificationCommandUnavailableError
2415
+ ? { retryable: false, error: message, failure: error.failure }
2416
+ : verificationFailed
2417
+ ? { retryable: true, error: `Verification failed after the agent run.\n${message}${diffStat ? `\n\nChanged files:\n${diffStat}` : ""}` }
2418
+ : reportDefect
2419
+ ? { retryable: false, error: message }
2420
+ // classifyFinalizeFailure: forge transport → retryable; contract/environment/unknown → fail
2421
+ // closed (unknown prefixed as "Bridge finalize interrupted", not laundered as contract).
2422
+ : classifyFinalizeFailure(message);
2423
+ retainAttemptWorktree = true;
2362
2424
  const response = await queueTerminal(client, taskId, {
2363
2425
  action: "fail",
2364
2426
  body: {
@@ -2390,8 +2452,9 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2390
2452
  return;
2391
2453
  }
2392
2454
  await client.updateAttempt(taskId, { phase: "agent_finished", delivery: { spec, report } });
2455
+ retainAttemptWorktree = true;
2393
2456
  const response = await submitFinishedDelivery(client, taskId);
2394
- retainAttemptWorktree = response.retain_worktree === true;
2457
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
2395
2458
  deliverySubmitted = response.status !== "failed";
2396
2459
  if (deliverySubmitted)
2397
2460
  console.log(`Assignment ${taskId} delivered for review and acceptance.`);
@@ -3003,7 +3066,11 @@ export async function verificationScopeDryRun(input) {
3003
3066
  let redBase = null;
3004
3067
  for (const command of commands) {
3005
3068
  // A gate that cannot start says nothing about the scope; the next one still can.
3006
- const result = await run(command, input.workspace).catch(() => null);
3069
+ const result = await run(command, input.workspace).catch((error) => {
3070
+ if (error instanceof VerificationCommandUnavailableError)
3071
+ throw error;
3072
+ return null;
3073
+ });
3007
3074
  if (!result)
3008
3075
  continue;
3009
3076
  // A gate that never exited says nothing about the base commit: it is not a red base, and it
@@ -13,12 +13,12 @@ import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl } fr
13
13
  import { headCommitOrNull } from "./git.js";
14
14
  import { ConduitRequestError } from "./client.js";
15
15
  import { learnDriverFuel, resolveAssignmentModel, changedPathsSince, runDeliveryVerificationGates, discardWorktreeWrites } from "./execution.js";
16
- import { DRIVERS, execute, extractAgentReportJsonText } from "./driver.js";
16
+ import { DRIVERS, execute, extractAgentReportJsonText, untrustedCodeTempRoot, untrustedCodeSpawn, untrustedCodeEnvironment } from "./driver.js";
17
17
  import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
18
18
  import { diffStatSince } from "./git-witness.js";
19
19
  import { switchManagedWorkspace } from "./on-shift-apply.js";
20
20
  import { execFile } from "node:child_process";
21
- import { rm, writeFile } from "node:fs/promises";
21
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
22
22
  import { createRequire } from "node:module";
23
23
  import { join } from "node:path";
24
24
  import { promisify } from "node:util";
@@ -471,14 +471,6 @@ function probeTail(stderr, stdout) {
471
471
  const stderrTail = stderr.length > PROBE_TAIL_CHARS ? stderr.slice(stderr.length - PROBE_TAIL_CHARS) : stderr;
472
472
  return boundedTail(`${stderrTail}\n${stdout}`, PROBE_TAIL_CHARS);
473
473
  }
474
- function probeEnv() {
475
- const env = {};
476
- if (process.env.PATH !== undefined)
477
- env.PATH = process.env.PATH;
478
- if (process.env.HOME !== undefined)
479
- env.HOME = process.env.HOME;
480
- return env;
481
- }
482
474
  function probeInterpreter(language) {
483
475
  if (language === "ts")
484
476
  return "node --import tsx";
@@ -514,12 +506,16 @@ async function execProbe(argv, workspace) {
514
506
  const bin = argv[0];
515
507
  if (!bin)
516
508
  throw new Error("Probe command is empty");
509
+ const tempRoot = untrustedCodeTempRoot(workspace);
510
+ await mkdir(tempRoot, { recursive: true });
511
+ const scratchHome = await mkdtemp(join(tempRoot, "probe-home-"));
517
512
  try {
518
- const { stdout, stderr } = await execFileAsync(bin, argv.slice(1), {
519
- cwd: workspace,
513
+ const spawn = await untrustedCodeSpawn({ command: bin, argv: argv.slice(1), cwd: workspace, env: untrustedCodeEnvironment(scratchHome) });
514
+ const { stdout, stderr } = await execFileAsync(spawn.command, spawn.argv, {
515
+ cwd: spawn.cwd,
520
516
  timeout: PROBE_TIMEOUT_MS,
521
517
  maxBuffer: PROBE_MAX_BUFFER_BYTES,
522
- env: probeEnv(),
518
+ env: spawn.env,
523
519
  });
524
520
  return { stdout: String(stdout), stderr: String(stderr), code: 0 };
525
521
  }
@@ -535,6 +531,9 @@ async function execProbe(argv, workspace) {
535
531
  code: err.killed === true || typeof err.code !== "number" ? -1 : err.code,
536
532
  };
537
533
  }
534
+ finally {
535
+ await rm(scratchHome, { recursive: true, force: true });
536
+ }
538
537
  }
539
538
  function witnessRecord(input) {
540
539
  return {
@@ -657,14 +656,23 @@ function toStepWitness(witness) {
657
656
  /**
658
657
  * The delivered instrument runs through the agent runs' own process helper (T90): bounded output,
659
658
  * an answer on the child's exit even when a process it left behind holds the pipes, and a stop
660
- * that escalates to SIGKILL. Its environment stays the probe's own, PATH and HOME only.
659
+ * that escalates to SIGKILL. Its environment uses a disposable HOME without machine credentials or proxies.
661
660
  */
662
661
  async function execDeliveredProbeRun(argv, stdin, workspace, timeoutMs) {
663
662
  const [file, ...args] = argv;
664
663
  if (!file)
665
664
  throw new Error("delivered probe command is empty");
666
- const result = await execute(file, args, workspace, timeoutMs, undefined, "conduit", stdin, undefined, undefined, probeEnv());
667
- return { code: result.code ?? -1, stdout: result.stdout, stderr: result.stderr };
665
+ const tempRoot = untrustedCodeTempRoot(workspace);
666
+ await mkdir(tempRoot, { recursive: true });
667
+ const scratchHome = await mkdtemp(join(tempRoot, "probe-home-"));
668
+ try {
669
+ const spawn = await untrustedCodeSpawn({ command: file, argv: args, cwd: workspace, env: untrustedCodeEnvironment(scratchHome) });
670
+ const result = await execute(spawn.command, spawn.argv, spawn.cwd, timeoutMs, undefined, "conduit", stdin, undefined, undefined, spawn.env);
671
+ return { code: result.code ?? -1, stdout: result.stdout, stderr: result.stderr };
672
+ }
673
+ finally {
674
+ await rm(scratchHome, { recursive: true, force: true });
675
+ }
668
676
  }
669
677
  export async function runDeliveredProbeInstrument(input) {
670
678
  const entry = join(input.workspace, PROBE_RUN_ENTRY);
package/dist/ops.js CHANGED
@@ -9,6 +9,7 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
9
  import { stdin as input, stdout as output } from "node:process";
10
10
  import { createInterface } from "node:readline/promises";
11
11
  import { parseArgs } from "node:util";
12
+ import { probeUntrustedCodeSandbox } from "./driver.js";
12
13
  import { ConduitClient } from "./client.js";
13
14
  import { loadConfig } from "./config.js";
14
15
  import { ensureCheckout } from "./checkout.js";
@@ -355,6 +356,8 @@ export async function runOps(verb, argv = [], deps = {}) {
355
356
  const installed = await detectInstalledClients();
356
357
  const health = probeAgentHealth(installed, process.env);
357
358
  console.log(`Bridge preflight: ${report.ready ? "READY" : "BLOCKED"}`);
359
+ const isolation = await (deps.probeSandbox ?? probeUntrustedCodeSandbox)();
360
+ console.log(`Untrusted-code isolation = ${isolation.os_sandbox === "none" ? `environment only: ${isolation.reason}` : `os sandbox (${isolation.os_sandbox})`}`);
358
361
  const installedAgents = health.filter((h) => h.installed);
359
362
  if (installedAgents.length > 0) {
360
363
  console.log("\nAgent Diagnostics:");
package/dist/service.js CHANGED
@@ -198,7 +198,8 @@ export async function systemdDropInConflicts(home = homedir(), name = DEFAULT_SE
198
198
  conflicts.add(name);
199
199
  }
200
200
  }
201
- if (platform() === "linux") {
201
+ // systemd answers for this user's real unit only; another home's units are not its to report.
202
+ if (platform() === "linux" && home === homedir()) {
202
203
  const effective = spawnSync("systemctl", ["--user", "show", `${name}.service`, "--property=DropInPaths", "--value"], { encoding: "utf8" });
203
204
  if (effective.status === 0) {
204
205
  for (const path of parseSystemdDropInPaths(effective.stdout))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.174",
3
+ "version": "0.16.180",
4
4
  "description": "Conduit Bridge CLI \u2014 join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {