@miraland-labs/conduit-bridge 0.16.185 → 0.16.187
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/dist/cli.js +6 -1
- package/dist/ensure-test-evidence.js +38 -1
- package/dist/failure-signal.js +3 -1
- package/dist/preflight.js +41 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -780,7 +780,12 @@ function startGateRehearsal(workspace, plan) {
|
|
|
780
780
|
const rehearsalPath = rehearsalWorktreePath(workspace);
|
|
781
781
|
console.log(`Rehearsing the gate in ${workspace}: ${plan.commands.join(", ")}`);
|
|
782
782
|
void createRehearsalWorktree(workspace, plan.commit)
|
|
783
|
-
.then((path) => runGateRehearsal(path, plan
|
|
783
|
+
.then((path) => runGateRehearsal(path, plan, {
|
|
784
|
+
freshWorkspace: (warmUpSeconds) => {
|
|
785
|
+
console.log(`Gate warm-up took ${warmUpSeconds} s; measuring again in a fresh worktree`);
|
|
786
|
+
return createRehearsalWorktree(workspace, plan.commit);
|
|
787
|
+
},
|
|
788
|
+
}))
|
|
784
789
|
.then((rehearsal) => { if (rehearsal === "busy")
|
|
785
790
|
console.log("Gate rehearsal deferred: another runner on this computer is rehearsing"); })
|
|
786
791
|
.catch((error) => console.error(`Gate rehearsal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown error")}`))
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* it never substitutes for it.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
-
import {
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { mkdir, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { untrustedCodeTempRoot, untrustedCodeSpawn, untrustedCodeEnvironment, criterionKey, resolveCriterionReference, signalGroup, STDIO_DRAIN_MS, CHILD_STOP_GRACE_MS, OWN_PROCESS_GROUP, trackProcessGroup, untrackProcessGroup } from "./driver.js";
|
|
10
11
|
import { isBoundedVerificationCommand, isRunnableVerificationCommand } from "./execution-class.js";
|
|
11
12
|
import { workspaceDependenciesMissingFailure } from "./failure-signal.js";
|
|
13
|
+
import { redactSecrets } from "./secret-redaction.js";
|
|
12
14
|
/** A typed spawn refusal, distinct from a command that ran and failed. */
|
|
13
15
|
export class VerificationCommandUnavailableError extends Error {
|
|
14
16
|
failure;
|
|
@@ -381,6 +383,40 @@ export function boundedFailureDetail(command, lines, code, maxChars = FAILURE_DE
|
|
|
381
383
|
const middle = dropped > 0 ? [...head, marker(dropped), ...tail] : [...head, ...tail];
|
|
382
384
|
return [first, ...middle, last].join("\n");
|
|
383
385
|
}
|
|
386
|
+
const FETCH_STDERR_TAIL_BYTES = 64 * 1024;
|
|
387
|
+
/** Workspace realpath + Cargo.lock sha256 already fetched by this process. */
|
|
388
|
+
const fetchedLocks = new Set();
|
|
389
|
+
/** The runner's own environment: network, the real CARGO_HOME and the PATH that finds cargo. */
|
|
390
|
+
const runFetchOutsideSandbox = (command, argv, cwd) => new Promise((resolve) => {
|
|
391
|
+
const child = spawn(command, argv, { cwd, env: process.env, stdio: ["ignore", "ignore", "pipe"] });
|
|
392
|
+
let stderr = "";
|
|
393
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), VERIFICATION_TIMEOUT_MS);
|
|
394
|
+
child.stderr?.setEncoding("utf8");
|
|
395
|
+
child.stderr?.on("data", (chunk) => { stderr = `${stderr}${chunk}`.slice(-FETCH_STDERR_TAIL_BYTES); });
|
|
396
|
+
child.on("error", (error) => { clearTimeout(timer); resolve({ code: 127, stderr: error.message }); });
|
|
397
|
+
child.on("close", (code) => { clearTimeout(timer); resolve({ code: code ?? 1, stderr }); });
|
|
398
|
+
});
|
|
399
|
+
/**
|
|
400
|
+
* CR19 (2026-10-22): the gate runs without network, so every crate the Cargo.lock pins must already
|
|
401
|
+
* be in CARGO_HOME. `cargo fetch --locked` fills it outside the sandbox; it writes only to CARGO_HOME
|
|
402
|
+
* and refuses to change the lock. Once per lock file per process: an agent that adds a dependency
|
|
403
|
+
* changes the lock, and the next gate fetches again. A failed fetch does not stop the gate: its own
|
|
404
|
+
* output names the cause.
|
|
405
|
+
*/
|
|
406
|
+
export async function fetchLockedDependencies(workspace, options = {}) {
|
|
407
|
+
const lock = await readFile(join(workspace, "Cargo.lock")).catch(() => null);
|
|
408
|
+
if (!lock)
|
|
409
|
+
return { ran: false };
|
|
410
|
+
const key = `${await realpath(workspace)}\0${createHash("sha256").update(lock).digest("hex")}`;
|
|
411
|
+
if (fetchedLocks.has(key))
|
|
412
|
+
return { ran: false };
|
|
413
|
+
const result = await (options.run ?? runFetchOutsideSandbox)("cargo", ["fetch", "--locked"], workspace);
|
|
414
|
+
if (result.code === 0)
|
|
415
|
+
fetchedLocks.add(key);
|
|
416
|
+
else
|
|
417
|
+
console.error(redactSecrets(`[bridge] cargo fetch --locked exited ${result.code} in ${workspace}: ${result.stderr.trim().split("\n").slice(-10).join("\n")}`));
|
|
418
|
+
return { ran: true, code: result.code };
|
|
419
|
+
}
|
|
384
420
|
/**
|
|
385
421
|
* The bounded runner every Bridge-run verification uses: the declared command, in the given
|
|
386
422
|
* workspace, with the verification timeout and buffer. A non-zero exit is a result, not a throw.
|
|
@@ -449,6 +485,7 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
|
|
|
449
485
|
partial[stream].append(parts.at(-1));
|
|
450
486
|
};
|
|
451
487
|
const output = () => [lines.text().replace(/\n$/, ""), partial.stdout.text(), partial.stderr.text()].filter(Boolean).join("\n");
|
|
488
|
+
await fetchLockedDependencies(workspace);
|
|
452
489
|
const tempRoot = untrustedCodeTempRoot(workspace);
|
|
453
490
|
await mkdir(tempRoot, { recursive: true });
|
|
454
491
|
const scratchHome = await mkdtemp(join(tempRoot, "gate-home-"));
|
package/dist/failure-signal.js
CHANGED
|
@@ -374,6 +374,8 @@ export const VERIFICATION_FAILS_ON_BASE = "verification fails on the base commit
|
|
|
374
374
|
export const WORKSPACE_DEPENDENCIES_MISSING_PATTERN = /(?:^|\n)\s*(?:\/(?:usr\/)?bin\/)?(?:ba|z|da)?sh:\s*(?:(?:\d+:\s*)?[^:\n]{1,120}:\s*(?:command\s+)?not found|command not found:\s*[^\n]+)\s*(?=\n|$)|(?:^|\n)\s*'[^'\n]+' is not recognized as an internal or external command/im;
|
|
375
375
|
/** A write the toolchain needs that the computer refused before any test ran: the sandbox or disk, not the base commit. */
|
|
376
376
|
export const WORKSPACE_WRITE_REFUSED_PATTERN = /\b(?:EROFS|ENOSPC)\b|read-only file system|no space left on device/i;
|
|
377
|
+
/** CR19 (2026-10-22): cargo inside the no-network sandbox tried to download a crate the shared cache lacks; the gate never tested the base. */
|
|
378
|
+
export const WORKSPACE_FETCH_REFUSED_PATTERN = /Could not resolve host|failed to download from|Temporary failure in name resolution|Network is unreachable|unable to get packages from source/i;
|
|
377
379
|
/** CR17e (2026-10-22): vitest could not write its config bundle under the sandboxed install; Bridge held the owner for its own mount. */
|
|
378
380
|
export function workspaceWriteRefusedFailure(detail) {
|
|
379
381
|
return {
|
|
@@ -613,7 +615,7 @@ export function classifyFailureSignal(signal) {
|
|
|
613
615
|
// base commit. It is the computer's checkout that lacks a tool — an environment Hold that
|
|
614
616
|
// clears when the workspace is bootstrapped, not a contract Hold on the author that never
|
|
615
617
|
// clears. runner-2's first claim (2026-09-15) sat on the author for a missing `node_modules`.
|
|
616
|
-
if (signal.exit_code === 127 || WORKSPACE_DEPENDENCIES_MISSING_PATTERN.test(output)) {
|
|
618
|
+
if (signal.exit_code === 127 || WORKSPACE_DEPENDENCIES_MISSING_PATTERN.test(output) || WORKSPACE_FETCH_REFUSED_PATTERN.test(output)) {
|
|
617
619
|
return workspaceDependenciesMissingFailure([`$ ${command}`, `exit ${signal.exit_code ?? "unknown"}`, output].filter(Boolean).join("\n"));
|
|
618
620
|
}
|
|
619
621
|
if (WORKSPACE_WRITE_REFUSED_PATTERN.test(output)) {
|
package/dist/preflight.js
CHANGED
|
@@ -7,7 +7,8 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
|
|
9
9
|
import { bridgeConfigDirectory } from "./config.js";
|
|
10
|
-
import { runBoundedVerificationCommand, verificationEvidenceCommands } from "./ensure-test-evidence.js";
|
|
10
|
+
import { runBoundedVerificationCommand, VERIFICATION_TIMEOUT_MS, verificationEvidenceCommands } from "./ensure-test-evidence.js";
|
|
11
|
+
import { lockfileDigest } from "./execution-facts.js";
|
|
11
12
|
import { resolveAgentTimeout, resolveMaxTurns } from "./execution-budget.js";
|
|
12
13
|
import { hasAntigravityLogin, hasClaudeLogin, hasGrokLogin, hasOpenAiLogin, hasOpenCodeLogin, driverExecutable, readSignInStatus, } from "./driver.js";
|
|
13
14
|
import { laneQuota, laneTierBindings, localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
@@ -315,18 +316,29 @@ async function readGateRehearsals(statePath) {
|
|
|
315
316
|
return {};
|
|
316
317
|
}
|
|
317
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* The rehearsal's own limit. The first run after a dependency change can compile a whole dependency
|
|
321
|
+
* tree: on 2026-10-22 acme-sim's registry crates took more than ten minutes, the attempt limit
|
|
322
|
+
* stopped the rehearsal mid-compile, the shared target directory stayed cold, and the next attempt
|
|
323
|
+
* stopped at the same limit before any agent started. The rehearsal absorbs that warm-up.
|
|
324
|
+
*/
|
|
325
|
+
export const REHEARSAL_TIMEOUT_MS = 3_600_000;
|
|
318
326
|
/** UTC calendar day: a push to main costs no run, and a new day measures the cache again. */
|
|
319
327
|
function rehearsalDay(at) {
|
|
320
328
|
return new Date(at).toISOString().slice(0, 10);
|
|
321
329
|
}
|
|
322
|
-
/**
|
|
330
|
+
/**
|
|
331
|
+
* The last rehearsal of this workspace's repository, and the gate to run when one is due: once a
|
|
332
|
+
* day, and again when the workspace's lockfiles change, because new dependencies need a new warm-up.
|
|
333
|
+
*/
|
|
323
334
|
export async function planGateRehearsal(brief, options = {}) {
|
|
324
335
|
const commands = verificationEvidenceCommands(brief.verification, brief.declared_verification, null);
|
|
325
336
|
if (!brief.repository || !brief.base_commit || !commands.length)
|
|
326
337
|
return { report: null, due: null };
|
|
327
338
|
const repository = normalizeRepositoryUrl(brief.repository);
|
|
328
339
|
const stored = (await readGateRehearsals(options.statePath ?? gateRehearsalStatePath()))[repository] ?? null;
|
|
329
|
-
const
|
|
340
|
+
const lockfiles = options.workspace ? await lockfileDigest(options.workspace) : null;
|
|
341
|
+
const digest = createHash("sha256").update(commands.join("\n")).update(lockfiles ?? "").digest("hex").slice(0, 16);
|
|
330
342
|
const day = rehearsalDay((options.now ?? Date.now)());
|
|
331
343
|
if (stored && stored.day === day && stored.digest === digest)
|
|
332
344
|
return { report: stored.rehearsal, due: null };
|
|
@@ -342,7 +354,7 @@ export async function planGateRehearsals(checkouts, options = {}) {
|
|
|
342
354
|
const seen = new Set();
|
|
343
355
|
let due = null;
|
|
344
356
|
for (const checkout of checkouts) {
|
|
345
|
-
const planned = await planGateRehearsal(checkout.brief, options);
|
|
357
|
+
const planned = await planGateRehearsal(checkout.brief, { ...options, workspace: checkout.workspace });
|
|
346
358
|
const repository = planned.report?.repository ?? planned.due?.repository;
|
|
347
359
|
if (!repository || seen.has(repository))
|
|
348
360
|
continue;
|
|
@@ -429,13 +441,36 @@ async function rehearseGate(workspace, plan, options) {
|
|
|
429
441
|
const now = options.now ?? Date.now;
|
|
430
442
|
const run = options.runCommand ?? runBoundedVerificationCommand;
|
|
431
443
|
const commands = [];
|
|
444
|
+
let cwd = workspace;
|
|
432
445
|
for (const command of plan.commands) {
|
|
433
446
|
const started = now();
|
|
434
447
|
// A command that cannot start measured nothing; the dry run skips it the same way.
|
|
435
|
-
const result = await run(command,
|
|
448
|
+
const result = await run(command, cwd, REHEARSAL_TIMEOUT_MS).catch(() => null);
|
|
436
449
|
if (!result)
|
|
437
450
|
continue;
|
|
438
|
-
|
|
451
|
+
const elapsed = now() - started;
|
|
452
|
+
const seconds = Math.round(elapsed / 1_000);
|
|
453
|
+
if (result.code !== 0 || result.timedOut === true || elapsed <= VERIFICATION_TIMEOUT_MS) {
|
|
454
|
+
commands.push({ command, seconds, exit_code: result.code, timed_out: result.timedOut === true });
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
// The pass fit only the rehearsal's limit: it was a warm-up. Measure once more, in a fresh
|
|
458
|
+
// checkout with the attempt's limit, so the report shows what an attempt will see.
|
|
459
|
+
if (options.freshWorkspace)
|
|
460
|
+
cwd = await options.freshWorkspace(seconds);
|
|
461
|
+
const again = now();
|
|
462
|
+
const second = await run(command, cwd, VERIFICATION_TIMEOUT_MS).catch(() => null);
|
|
463
|
+
if (!second) {
|
|
464
|
+
commands.push({ command, seconds, exit_code: result.code, timed_out: false });
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
commands.push({
|
|
468
|
+
command,
|
|
469
|
+
seconds: Math.round((now() - again) / 1_000),
|
|
470
|
+
exit_code: second.code,
|
|
471
|
+
timed_out: second.timedOut === true,
|
|
472
|
+
warm_up_seconds: seconds,
|
|
473
|
+
});
|
|
439
474
|
}
|
|
440
475
|
const at = now();
|
|
441
476
|
const rehearsal = commands.length
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.187",
|
|
4
4
|
"description": "Conduit Bridge CLI — 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": {
|