@miraland-labs/conduit-bridge 0.16.22 → 0.16.24

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 CHANGED
@@ -14,6 +14,7 @@ import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from
14
14
  import { DRIVERS } from "./driver.js";
15
15
  import { driversHeartbeatReport, driverLabel, heartbeatStatusForDrivers, isSupportedDriverId, listDriverLanes, onlineDriverIds, seedDriversFromDetection, seedDriverLanes, setDriverFuel, setDriversOnline, } from "./drivers.js";
16
16
  import { buildWorkspaceBrief } from "./brief.js";
17
+ import { BOOTSTRAP_RESULTS, buildSovereignExecutionFacts } from "./execution-facts.js";
17
18
  import { ensureCheckout } from "./checkout.js";
18
19
  import { buildOnShiftIntentProof, maybeApplyOnShiftIntent } from "./on-shift-apply.js";
19
20
  import { loadOpsEnv, OPS_VERBS, runOps } from "./ops.js";
@@ -37,6 +38,12 @@ function parseFuelSource(value) {
37
38
  return value;
38
39
  throw new Error("Fuel source must be conduit or local");
39
40
  }
41
+ function parseBootstrapResult(value) {
42
+ const result = BOOTSTRAP_RESULTS.find((candidate) => candidate === value);
43
+ if (!result)
44
+ throw new Error(`Bootstrap result must be one of: ${BOOTSTRAP_RESULTS.join(", ")}`);
45
+ return result;
46
+ }
40
47
  async function connect() {
41
48
  const { values } = parseArgs({ args: process.argv.slice(3), options: { url: { type: "string" }, code: { type: "string" }, fuel: { type: "string" } } });
42
49
  if (!values.url || !values.code)
@@ -319,7 +326,7 @@ async function installService() {
319
326
  let config = await loadConfig();
320
327
  const { values } = parseArgs({ args: process.argv.slice(3), options: {
321
328
  workspace: { type: "string" }, interval: { type: "string" }, "agent-timeout-minutes": { type: "string" },
322
- "ensure-checkout": { type: "string" },
329
+ "ensure-checkout": { type: "string" }, "bootstrap-result": { type: "string" },
323
330
  } });
324
331
  if (!values.workspace) {
325
332
  throw new Error(`Usage: ${bridgeUsage("install-service", "--workspace", "<repository-path>", "[--ensure-checkout <repository-url>]")} (workspace required; uses online driver lanes)`);
@@ -329,6 +336,9 @@ async function installService() {
329
336
  throw new Error(`No online driver lanes. Run \`${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}\` first.`);
330
337
  }
331
338
  const workspace = resolve(values.workspace);
339
+ const bootstrapResult = values["bootstrap-result"]
340
+ ? parseBootstrapResult(values["bootstrap-result"])
341
+ : undefined;
332
342
  if (values["ensure-checkout"]) {
333
343
  const result = await ensureCheckout(workspace, values["ensure-checkout"]);
334
344
  console.log(result === "cloned"
@@ -340,6 +350,7 @@ async function installService() {
340
350
  interval: values.interval,
341
351
  agentTimeoutMinutes: values["agent-timeout-minutes"],
342
352
  ensureCheckout: values["ensure-checkout"],
353
+ bootstrapResult,
343
354
  });
344
355
  console.log(`Installed Conduit Bridge v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION}) runner service (${result.platform}): ${result.path}`);
345
356
  console.log(`Online lanes: ${onlineDriverIds(config).join(", ")}. Toggle with \`${bridgeUsage("drivers", "online|offline", "…")}\`.`);
@@ -466,9 +477,13 @@ async function runner() {
466
477
  const { values } = parseArgs({ args: process.argv.slice(3), options: {
467
478
  workspace: { type: "string" }, interval: { type: "string" }, once: { type: "boolean" },
468
479
  "agent-timeout-minutes": { type: "string" }, fuel: { type: "string" }, "ensure-checkout": { type: "string" },
480
+ "bootstrap-result": { type: "string" },
469
481
  } });
470
482
  let config = await loadConfig();
471
483
  const fuelOverride = parseFuelSource(values.fuel);
484
+ const bootstrapResult = values["bootstrap-result"]
485
+ ? parseBootstrapResult(values["bootstrap-result"])
486
+ : undefined;
472
487
  if (fuelOverride) {
473
488
  config.fuelSource = fuelOverride;
474
489
  await saveConfigPrefs(config);
@@ -511,7 +526,7 @@ async function runner() {
511
526
  const preflight = workspace
512
527
  ? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined })
513
528
  : unavailableWorkspacePreflight({ config });
514
- const hb = await heartbeat(client, config, workspace, currentBrief, preflight);
529
+ const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
515
530
  if (hb.staleBridge) {
516
531
  console.error(`Bridge update required: running protocol ${BRIDGE_PROTOCOL_VERSION}; control plane requires protocol ${hb.requiredProtocol ?? "unknown"} (minimum Bridge ${hb.minimumVersion ?? "unknown"}). Reinstall the latest Bridge, then restart this runner.`);
517
532
  await new Promise((resolveSleep) => setTimeout(resolveSleep, Math.max(intervalMs, 60_000)));
@@ -548,7 +563,7 @@ async function runner() {
548
563
  }
549
564
  const intentProof = buildOnShiftIntentProof({ currentWorkspace: workspace, onShift: hb.on_shift });
550
565
  if (intentProof) {
551
- await heartbeat(client, config, workspace, currentBrief, preflight, intentProof);
566
+ await heartbeat(client, config, workspace, currentBrief, preflight, intentProof, bootstrapResult);
552
567
  }
553
568
  await renewLeases(client, config);
554
569
  if (workspace && onlineDriverIds(config).length) {
@@ -561,7 +576,19 @@ async function runner() {
561
576
  heartbeat: async () => {
562
577
  const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
563
578
  const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined });
564
- await heartbeat(client, config, workspace, currentBrief, preflight);
579
+ await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
580
+ },
581
+ beforeClaim: async (driverId) => {
582
+ // Never let the five-minute heartbeat cache span a CLI upgrade into a certified claim.
583
+ // Publish the fresh probe first; the Control Plane then remains the authority for the
584
+ // exact version frozen onto the claim response and event.
585
+ const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
586
+ const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined }, { force: true });
587
+ const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
588
+ if (hb.staleBridge)
589
+ return false;
590
+ const lane = preflight.drivers.find((candidate) => candidate.id === driverId);
591
+ return preflight.ready && lane?.ready === true && typeof lane.version === "string" && lane.version.length > 0;
565
592
  },
566
593
  heartbeatIntervalMs: intervalMs,
567
594
  }, running, {});
@@ -578,19 +605,29 @@ async function runner() {
578
605
  await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
579
606
  }
580
607
  }
581
- async function heartbeat(client, config, workspacePath, brief, preflight, intentProof) {
608
+ async function heartbeat(client, config, workspacePath, brief, preflight, intentProof, bootstrapResult) {
582
609
  const online = onlineDriverIds(config);
583
610
  const status = heartbeatStatusForDrivers(online);
611
+ const version = bridgeVersion();
612
+ const executionFacts = await buildSovereignExecutionFacts({
613
+ workspace: workspacePath,
614
+ brief,
615
+ workspaceClean: preflight.workspace_clean,
616
+ bridgeVersion: version,
617
+ bridgeProtocol: BRIDGE_PROTOCOL_VERSION,
618
+ bootstrapResult,
619
+ });
584
620
  const response = await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
585
621
  status,
586
622
  capabilities: config.capabilities,
587
623
  lease_capacity: config.leaseCapacity,
588
624
  workspace_path: workspacePath,
589
625
  fuel_source: config.fuelSource === "local" ? "local" : "conduit",
590
- bridge_version: bridgeVersion(),
626
+ bridge_version: version,
591
627
  bridge_protocol: BRIDGE_PROTOCOL_VERSION,
592
628
  drivers: driversHeartbeatReport(config, config.activeAttempts),
593
629
  preflight,
630
+ execution_facts: executionFacts,
594
631
  ...(intentProof ? { on_shift_intent_proof: intentProof } : {}),
595
632
  ...(brief ? { workspace_brief: brief } : {}),
596
633
  }) });
@@ -0,0 +1,127 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, readFile, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { normalizeRepositoryUrl } from "./brief.js";
7
+ const execFileAsync = promisify(execFile);
8
+ export const EXECUTION_CLASSES = ["observe", "observe_network", "publish_artifact", "verify", "mutate_repo"];
9
+ export const BOOTSTRAP_RESULTS = ["not_applicable", "not_run", "installed", "failed"];
10
+ /**
11
+ * Lockfiles are hashed, never uploaded. The set is intentionally explicit and root-scoped so a
12
+ * dependency change becomes a durable identity without moving repository contents onto Conduit.
13
+ */
14
+ const LOCKFILE_NAMES = [
15
+ "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml",
16
+ "Cargo.lock", "poetry.lock", "Pipfile.lock", "go.sum", "Gemfile.lock", "composer.lock",
17
+ ];
18
+ /** Stable identity for the exact bounded verification command list used by Bridge. */
19
+ export function verificationCommandsDigest(commands) {
20
+ const normalized = [...new Set(commands.map((command) => command.trim()).filter(Boolean))];
21
+ if (!normalized.length)
22
+ return null;
23
+ return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
24
+ }
25
+ /**
26
+ * Hash the root lockfiles in deterministic name order. Missing or unreadable lockfiles are simply
27
+ * absent from the identity; a workspace with no readable lockfile reports null rather than an
28
+ * invented digest.
29
+ */
30
+ export async function lockfileDigest(workspace) {
31
+ const hash = createHash("sha256");
32
+ let found = false;
33
+ for (const name of LOCKFILE_NAMES) {
34
+ try {
35
+ const contents = await readFile(join(workspace, name));
36
+ hash.update(name);
37
+ hash.update("\0");
38
+ hash.update(contents);
39
+ hash.update("\0");
40
+ found = true;
41
+ }
42
+ catch {
43
+ // A missing or unreadable optional lockfile contributes no claim.
44
+ }
45
+ }
46
+ return found ? hash.digest("hex") : null;
47
+ }
48
+ /**
49
+ * Report whether a JavaScript workspace has dependencies installed. This is an observed state, not
50
+ * a promise that Bridge will run a package manager during execution. Managed installs already run
51
+ * `npm ci` before the service is advertised, so a present node_modules directory is the durable
52
+ * local evidence available on later heartbeats.
53
+ */
54
+ export async function bootstrapResultForWorkspace(workspace) {
55
+ if (!workspace)
56
+ return "not_applicable";
57
+ try {
58
+ await access(join(workspace, "package.json"));
59
+ }
60
+ catch {
61
+ return "not_applicable";
62
+ }
63
+ try {
64
+ const lockfile = await Promise.any(LOCKFILE_NAMES.map(async (name) => {
65
+ await access(join(workspace, name));
66
+ return true;
67
+ }));
68
+ void lockfile;
69
+ }
70
+ catch {
71
+ return "not_run";
72
+ }
73
+ try {
74
+ const dependencies = await stat(join(workspace, "node_modules"));
75
+ return dependencies.isDirectory() ? "installed" : "not_run";
76
+ }
77
+ catch {
78
+ return "not_run";
79
+ }
80
+ }
81
+ /** Read the source/attempt tree's Git cleanliness without returning its contents. */
82
+ export async function workspaceIsClean(workspace) {
83
+ try {
84
+ const result = await execFileAsync("git", ["-C", workspace, "status", "--porcelain", "--untracked-files=normal"], {
85
+ timeout: 15_000,
86
+ windowsHide: true,
87
+ });
88
+ return result.stdout.trim().length === 0;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ /** Read only the bounded HEAD identity retained when an attempt terminates. */
95
+ export async function finalCommitForWorkspace(workspace) {
96
+ try {
97
+ const result = await execFileAsync("git", ["-C", workspace, "rev-parse", "HEAD"], {
98
+ timeout: 15_000,
99
+ windowsHide: true,
100
+ });
101
+ return commitOrNull(result.stdout.trim());
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ function commitOrNull(value) {
108
+ return value && /^[0-9a-f]{40,64}$/i.test(value) ? value.toLowerCase() : null;
109
+ }
110
+ /** Build one self-contained heartbeat/attempt snapshot from existing Bridge observations. */
111
+ export async function buildSovereignExecutionFacts(input) {
112
+ const repository = input.brief?.repository?.trim();
113
+ return {
114
+ os: process.platform,
115
+ architecture: process.arch,
116
+ repository_fingerprint: repository ? normalizeRepositoryUrl(repository) : null,
117
+ claimed_head: commitOrNull(input.claimedHead ?? input.brief?.base_commit),
118
+ lockfile_digest: input.workspace ? await lockfileDigest(input.workspace) : null,
119
+ bootstrap_result: input.bootstrapResult ?? await bootstrapResultForWorkspace(input.workspace),
120
+ bridge_version: input.bridgeVersion,
121
+ bridge_protocol: input.bridgeProtocol,
122
+ execution_class: input.executionClass ?? null,
123
+ verification_commands_digest: verificationCommandsDigest(input.brief?.verification ?? []),
124
+ workspace_clean: input.workspaceClean,
125
+ final_commit: commitOrNull(input.finalCommit),
126
+ };
127
+ }
package/dist/execution.js CHANGED
@@ -10,6 +10,9 @@ import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quaran
10
10
  import { execFile } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
12
  import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
13
+ import { buildSovereignExecutionFacts, bootstrapResultForWorkspace, finalCommitForWorkspace, workspaceIsClean } from "./execution-facts.js";
14
+ import { BRIDGE_PROTOCOL_VERSION } from "./preflight.js";
15
+ import { bridgeVersion } from "./version.js";
13
16
  import { ensureDeliveryPullRequest } from "./ensure-pull-request.js";
14
17
  import { ensureLandCommit } from "./ensure-land-commit.js";
15
18
  import { AgentNoLandCommitError, agentNoLandCommitMessage, requiresLandCommit } from "./land-contract.js";
@@ -538,7 +541,9 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
538
541
  }
539
542
  if (!laneDriver || !driverId)
540
543
  break;
541
- const claimed = await claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds);
544
+ const claimed = await claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds, {
545
+ beforeClaim: supervision?.beforeClaim,
546
+ });
542
547
  if (!claimed)
543
548
  break;
544
549
  const taskId = claimed.taskId;
@@ -557,7 +562,7 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
557
562
  return progressed;
558
563
  }
559
564
  /** Claim one assignment when under capacity; does not start the agent (multi-slot pump does). */
560
- export async function claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds) {
565
+ export async function claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds, options = {}) {
561
566
  if (Object.keys(config.activeAttempts).length >= config.leaseCapacity)
562
567
  return null;
563
568
  const data = await client.request("/runner/v1/assignments");
@@ -607,6 +612,10 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
607
612
  console.error(`Assignment ${assignment.id} rejected before claim: ${rejection}`);
608
613
  return null;
609
614
  }
615
+ if (selectedDriverId && options.beforeClaim && !await options.beforeClaim(selectedDriverId)) {
616
+ console.warn(`Assignment ${assignment.id} held before claim: driver ${selectedDriverId} preflight is not ready`);
617
+ return null;
618
+ }
610
619
  console.log(`Claiming ${assignment.execution_kind} assignment ${assignment.id} (attempt ${assignment.attempt_id})`);
611
620
  const fuelMode = selectedDriverId ? resolveDriverFuel(config, selectedDriverId) : undefined;
612
621
  const fuelProvenance = selectedDriverId ? resolveDriverFuelProvenance(config, selectedDriverId) : undefined;
@@ -631,7 +640,9 @@ export async function executeNextAssignment(client, config, driver, workspace, b
631
640
  if (Object.keys(config.activeAttempts).length)
632
641
  return recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision);
633
642
  const preferredDriverId = Object.entries(DRIVERS).find(([, candidate]) => candidate === driver)?.[0] ?? null;
634
- const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId);
643
+ const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId, null, {
644
+ beforeClaim: supervision?.beforeClaim,
645
+ });
635
646
  if (!claimed)
636
647
  return false;
637
648
  const selectedDriver = claimedAssignmentDriver(driver, claimed.driverId);
@@ -899,8 +910,20 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
899
910
  normativeRefs: normativeMaterialized,
900
911
  });
901
912
  const resuming = Boolean(options.forceResumeSessionId);
913
+ const executionFacts = await buildSovereignExecutionFacts({
914
+ workspace: attemptWorkspace,
915
+ brief: attemptBrief,
916
+ workspaceClean: await workspaceIsClean(workspace),
917
+ bridgeVersion: bridgeVersion(),
918
+ bridgeProtocol: BRIDGE_PROTOCOL_VERSION,
919
+ executionClass,
920
+ claimedHead: executionContract.claimed_head ?? startCommit,
921
+ bootstrapResult: await bootstrapResultForWorkspace(workspace),
922
+ });
923
+ await client.updateAttempt(taskId, { executionFacts });
902
924
  await client.attemptRequest(taskId, "progress", {
903
925
  phase: diagnosis ? "inspecting" : "changing",
926
+ execution_facts: executionFacts,
904
927
  message: resuming
905
928
  ? `Resuming ${driver.name} after Bridge restart with proven session and worktree.`
906
929
  : diagnosis
@@ -1587,6 +1610,10 @@ async function submitFinishedDelivery(client, taskId) {
1587
1610
  const active = client.attempt(taskId);
1588
1611
  if (!active.delivery)
1589
1612
  throw new Error("Finished agent run is missing its persisted Delivery data");
1613
+ const finalCommit = active.delivery.report.head_commit?.toLowerCase() ?? null;
1614
+ if (finalCommit && active.executionFacts) {
1615
+ await client.updateAttempt(taskId, { executionFacts: { ...active.executionFacts, final_commit: finalCommit } });
1616
+ }
1590
1617
  const terminal = await prepareDelivery(client, active.attemptId, taskId, active.delivery.report);
1591
1618
  await client.attemptRequest(taskId, "progress", { phase: "preparing_delivery", message: "Agent finished; submitting the Delivery.", idempotency_key: `bridge:progress:${active.attemptId}:delivery` });
1592
1619
  return queueTerminal(client, taskId, terminal);
@@ -1862,7 +1889,19 @@ function retainDiagnosticWorktree(response) {
1862
1889
  return response.retain_worktree === true || response.status === "invalid_lease";
1863
1890
  }
1864
1891
  async function queueTerminal(client, taskId, terminal) {
1865
- await client.updateAttempt(taskId, { phase: "terminal_pending", terminal });
1892
+ const active = client.attempt(taskId);
1893
+ let executionFacts = active.executionFacts;
1894
+ if (executionFacts && executionFacts.final_commit === null && active.worktreePath) {
1895
+ const finalCommit = await finalCommitForWorkspace(active.worktreePath);
1896
+ if (finalCommit) {
1897
+ executionFacts = { ...executionFacts, final_commit: finalCommit };
1898
+ await client.updateAttempt(taskId, { executionFacts });
1899
+ }
1900
+ }
1901
+ const body = executionFacts && !terminal.body.execution_facts
1902
+ ? { ...terminal.body, execution_facts: executionFacts }
1903
+ : terminal.body;
1904
+ await client.updateAttempt(taskId, { phase: "terminal_pending", terminal: { ...terminal, body } });
1866
1905
  return flushTerminal(client, taskId);
1867
1906
  }
1868
1907
  export async function flushTerminal(client, taskId) {
@@ -19,11 +19,6 @@ export const AGENT_NO_LAND_COMMIT_PREFIX = "Agent did not land repository change
19
19
  export function agentNoLandCommitMessage(baseCommit) {
20
20
  return `${AGENT_NO_LAND_COMMIT_PREFIX}: the attempt branch has no commit after base ${baseCommit.slice(0, 12)}.`;
21
21
  }
22
- /** Legacy ensureDeliveryPullRequest text — kept recognizable for stored failures. */
23
- export const LEGACY_NO_LAND_COMMIT_PATTERN = /no commit after base .*; this run produced no change to deliver/i;
24
- export function isAgentNoLandCommitMessage(message) {
25
- return message.startsWith(AGENT_NO_LAND_COMMIT_PREFIX) || LEGACY_NO_LAND_COMMIT_PATTERN.test(message);
26
- }
27
22
  export class AgentNoLandCommitError extends Error {
28
23
  baseCommit;
29
24
  constructor(baseCommit) {
package/dist/ops.js CHANGED
@@ -570,6 +570,7 @@ export async function runOps(verb, argv = [], deps = {}) {
570
570
  }
571
571
  const workspace = resolve(expandOpsValue(installEnv.CONDUIT_WORKSPACE));
572
572
  const drivers = await (deps.resolveInstallDrivers ?? resolveInstallDrivers)(installEnv, installArgv);
573
+ let bootstrapResult;
573
574
  if (installEnv.CONDUIT_REPO) {
574
575
  const checkout = deps.ensureCheckout ?? ensureCheckout;
575
576
  const result = await checkout(workspace, installEnv.CONDUIT_REPO);
@@ -587,7 +588,7 @@ export async function runOps(verb, argv = [], deps = {}) {
587
588
  && !isAbsolute(managedRelative));
588
589
  if (managedCheckout) {
589
590
  const bootstrap = deps.bootstrapWorkspace ?? bootstrapManagedWorkspace;
590
- const bootstrapResult = bootstrap(workspace);
591
+ bootstrapResult = bootstrap(workspace);
591
592
  if (bootstrapResult === "installed")
592
593
  console.log(`Installed locked project dependencies in ${workspace}`);
593
594
  }
@@ -616,6 +617,8 @@ export async function runOps(verb, argv = [], deps = {}) {
616
617
  const installArgs = ["install-service", "--workspace", workspace];
617
618
  if (installEnv.CONDUIT_REPO)
618
619
  installArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
620
+ if (bootstrapResult)
621
+ installArgs.push("--bootstrap-result", bootstrapResult);
619
622
  console.log(`Bridge: v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION})`);
620
623
  console.log(`Installing runner for ${workspace} (drivers: ${drivers.join(", ")})`);
621
624
  runBridge(installArgs);
package/dist/preflight.js CHANGED
@@ -16,9 +16,31 @@ function modelsFingerprint(config) {
16
16
  }).join(";");
17
17
  return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
18
18
  }
19
- // Protocol 8 reports the resolved lane fuel and credential provenance in heartbeat and claim, so
20
- // the control plane can freeze what will actually run before the agent spends tokens.
21
- export const BRIDGE_PROTOCOL_VERSION = 8;
19
+ // Protocol 9 adds the Bridge-observed driver interface cohort to every preflight lane.
20
+ export const BRIDGE_PROTOCOL_VERSION = 9;
21
+ /** Keep the version identity within the same bound accepted by the heartbeat schema. */
22
+ export const MAX_DRIVER_VERSION_LENGTH = 200;
23
+ /**
24
+ * Turn one successful CLI version probe into a stable, bounded identity.
25
+ *
26
+ * A probe is deliberately stricter than "some output": warnings, multiple lines, control
27
+ * sequences, and overlong output cannot be assigned to a particular CLI version. Returning null
28
+ * makes the lane unready, so a stale/truncated prefix can never be used for certification.
29
+ */
30
+ export function parseDriverVersion(result) {
31
+ if (result.code !== 0)
32
+ return null;
33
+ const streams = [result.stdout, result.stderr]
34
+ .filter((value) => typeof value === "string")
35
+ .map((value) => value.trim())
36
+ .filter(Boolean);
37
+ if (streams.length !== 1)
38
+ return null;
39
+ const version = streams[0];
40
+ if (version.length > MAX_DRIVER_VERSION_LENGTH || /[\r\n\u0000-\u001f\u007f]/u.test(version))
41
+ return null;
42
+ return version;
43
+ }
22
44
  const execFileAsync = promisify(execFile);
23
45
  async function defaultCommandRunner(command, args, cwd) {
24
46
  try {
@@ -44,6 +66,55 @@ const DRIVER_COMMAND = {
44
66
  function executableFor(driver) {
45
67
  return driver === "codex" ? resolveCodexExecutable() : DRIVER_COMMAND[driver] ?? driver;
46
68
  }
69
+ /**
70
+ * Primary-driver interface surfaces used by the existing AgentDriver implementations. This is
71
+ * deliberately not a hash of all help prose: vendor copy edits must not manufacture a new cohort.
72
+ */
73
+ const DRIVER_INTERFACE_CONTRACTS = {
74
+ "claude-code": {
75
+ adapter_revision: 1,
76
+ help_args: ["--help"],
77
+ required: ["outputformat", "allowedtools", "disallowedtools"],
78
+ observed: ["outputformat", "allowedtools", "disallowedtools", "permissionmode", "resume", "model"],
79
+ },
80
+ codex: {
81
+ adapter_revision: 1,
82
+ help_args: ["exec", "--help"],
83
+ required: ["json", "sandbox"],
84
+ observed: ["json", "sandbox", "model", "config"],
85
+ },
86
+ cursor: {
87
+ adapter_revision: 1,
88
+ help_args: ["--help"],
89
+ required: ["outputformat", "workspace"],
90
+ observed: ["outputformat", "workspace", "trust", "force", "resume", "model"],
91
+ },
92
+ };
93
+ function normalizedHelpSurface(value) {
94
+ return value.toLowerCase().replace(/[-_\s]/g, "");
95
+ }
96
+ /**
97
+ * Fingerprint only what a bounded, non-spending interface probe can prove. It is not a claim that
98
+ * vendor semantics or model quality stayed identical; per-attempt gates and field outcomes remain
99
+ * authoritative for those facts.
100
+ */
101
+ async function driverCompatibilityFingerprint(driver, workspace, run) {
102
+ const contract = DRIVER_INTERFACE_CONTRACTS[driver];
103
+ if (!contract)
104
+ return null;
105
+ const help = await run(executableFor(driver), contract.help_args, workspace);
106
+ if (help.code !== 0)
107
+ return null;
108
+ const surface = normalizedHelpSurface(`${help.stdout}\n${help.stderr}`);
109
+ if (!contract.required.every((signal) => surface.includes(signal)))
110
+ return null;
111
+ const features = contract.observed.map((signal) => [signal, surface.includes(signal)]);
112
+ return createHash("sha256").update(JSON.stringify({
113
+ driver,
114
+ adapter_revision: contract.adapter_revision,
115
+ features,
116
+ })).digest("hex");
117
+ }
47
118
  async function localAuthenticationReady(driver, workspace, run) {
48
119
  if (driver === "claude-code")
49
120
  return hasClaudeLogin();
@@ -106,23 +177,25 @@ export async function runBridgePreflight(input, deps = {}) {
106
177
  const diagnosisReadOnly = supportsReadOnlyDiagnosis(driver);
107
178
  const fuel = resolveDriverFuel(input.config, driver);
108
179
  if (localFuelOnlyDriver(driver) && fuel !== "local") {
109
- return { issue: { code: "driver_fuel_mismatch", driver }, snapshot: { id: driver, version: null, ready: false, diagnosis_read_only: diagnosisReadOnly } };
180
+ return { issue: { code: "driver_fuel_mismatch", driver }, snapshot: { id: driver, version: null, ready: false, diagnosis_read_only: diagnosisReadOnly, compatibility_fingerprint: null } };
110
181
  }
111
182
  const version = await run(executableFor(driver), ["--version"], input.workspace);
112
- const versionText = `${version.stdout}${version.stderr}`.trim().slice(0, 200) || null;
113
- if (version.code !== 0 || !`${version.stdout}${version.stderr}`.trim()) {
114
- return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
183
+ const versionText = parseDriverVersion(version);
184
+ if (!versionText) {
185
+ return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version: null, ready: false, diagnosis_read_only: diagnosisReadOnly, compatibility_fingerprint: null } };
115
186
  }
116
187
  if (fuel === "local" && !await localAuthenticationReady(driver, input.workspace, run)) {
117
- return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
188
+ return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly, compatibility_fingerprint: null } };
118
189
  }
119
190
  // Reported, not probed: the window state was learned from the vendor's last refusal and read
120
191
  // back through laneQuota, which retires a passed reset. Nothing is executed here, so a lane
121
192
  // running dry costs the heartbeat nothing.
122
193
  const quota = fuel === "local" ? laneQuota(input.config, driver) ?? undefined : undefined;
194
+ const compatibilityFingerprint = await driverCompatibilityFingerprint(driver, input.workspace, run);
123
195
  return {
124
196
  issue: null,
125
- snapshot: { id: driver, version: versionText, ready: quota?.exhausted !== true, diagnosis_read_only: diagnosisReadOnly, ...(quota ? { quota } : {}) },
197
+ snapshot: { id: driver, version: versionText, ready: quota?.exhausted !== true, diagnosis_read_only: diagnosisReadOnly,
198
+ compatibility_fingerprint: compatibilityFingerprint, ...(quota ? { quota } : {}) },
126
199
  };
127
200
  }));
128
201
  issues.push(...driverChecks.map((check) => check.issue).filter((issue) => issue !== null));
@@ -137,10 +210,10 @@ export async function runBridgePreflight(input, deps = {}) {
137
210
  }
138
211
  let cached = null;
139
212
  /** Keep auth probes off the 15-second heartbeat hot path while still expiring readiness promptly. */
140
- export async function cachedBridgePreflight(input) {
213
+ export async function cachedBridgePreflight(input, options = {}) {
141
214
  const online = input.processOnlineIds?.length ? input.processOnlineIds : onlineDriverIds(input.config);
142
215
  const key = JSON.stringify([input.workspace, input.expectedRepository ?? "", online, input.config.fuelSource, input.config.drivers]);
143
- if (cached?.key === key && Date.now() - cached.at < 5 * 60_000)
216
+ if (!options.force && cached?.key === key && Date.now() - cached.at < 5 * 60_000)
144
217
  return cached.report;
145
218
  const report = await runBridgePreflight(input);
146
219
  cached = { key, at: Date.now(), report };
@@ -157,7 +230,7 @@ export function unavailableWorkspacePreflight(input) {
157
230
  ready: false,
158
231
  checked_at: new Date().toISOString(),
159
232
  workspace_clean: false,
160
- drivers: online.map((id) => ({ id, version: null, ready: false, diagnosis_read_only: supportsReadOnlyDiagnosis(id) })),
233
+ drivers: online.map((id) => ({ id, version: null, ready: false, diagnosis_read_only: supportsReadOnlyDiagnosis(id), compatibility_fingerprint: null })),
161
234
  models_fingerprint: modelsFingerprint(input.config),
162
235
  issues,
163
236
  };
package/dist/service.js CHANGED
@@ -30,6 +30,8 @@ export function runnerProgramArguments(options = {}) {
30
30
  args.push("--interval", options.interval);
31
31
  if (options.agentTimeoutMinutes)
32
32
  args.push("--agent-timeout-minutes", options.agentTimeoutMinutes);
33
+ if (options.bootstrapResult)
34
+ args.push("--bootstrap-result", options.bootstrapResult);
33
35
  return args;
34
36
  }
35
37
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.22",
3
+ "version": "0.16.24",
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": {