@miraland-labs/conduit-bridge 0.13.1 → 0.14.4

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/client.js CHANGED
@@ -40,11 +40,15 @@ export class ConduitClient {
40
40
  return data;
41
41
  }
42
42
  async claim(taskId, attemptId, extras) {
43
- const data = await this.request(`/runner/v1/tasks/${taskId}/claim`, { method: "POST", body: JSON.stringify({ attempt_id: attemptId, idempotency_key: `bridge:claim:${attemptId}` }) });
43
+ // The lease token is this machine's bearer credential for the attempt. The server stores only
44
+ // its hash, so the client mints it and re-sends the identical token if it ever retries the
45
+ // claim (the idempotency key is deterministic per attempt exactly so that replay works).
46
+ const leaseToken = `lease_${crypto.randomUUID()}${crypto.randomUUID()}`;
47
+ const data = await this.request(`/runner/v1/tasks/${taskId}/claim`, { method: "POST", body: JSON.stringify({ attempt_id: attemptId, lease_token: leaseToken, idempotency_key: `bridge:claim:${attemptId}` }) });
44
48
  const active = {
45
49
  taskId,
46
50
  attemptId,
47
- leaseToken: String(data.lease_token),
51
+ leaseToken,
48
52
  leaseExpiresAt: String(data.lease_expires_at),
49
53
  phase: "claimed",
50
54
  ...(extras?.driverId ? { driverId: extras.driverId } : {}),
package/dist/execution.js CHANGED
@@ -463,13 +463,23 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
463
463
  });
464
464
  return { taskId: assignment.id, driverId: selectedDriverId };
465
465
  }
466
+ export function claimedAssignmentDriver(fallback, driverId) {
467
+ if (!driverId)
468
+ return fallback;
469
+ const selected = DRIVERS[driverId];
470
+ if (!selected)
471
+ throw new Error(`Claimed assignment selected unavailable driver ${driverId}`);
472
+ return selected;
473
+ }
466
474
  export async function executeNextAssignment(client, config, driver, workspace, brief, timeoutMs, supervision) {
467
475
  if (Object.keys(config.activeAttempts).length)
468
476
  return recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision);
469
- const claimed = await claimNextAssignment(client, config, workspace, brief);
477
+ const preferredDriverId = Object.entries(DRIVERS).find(([, candidate]) => candidate === driver)?.[0] ?? null;
478
+ const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId);
470
479
  if (!claimed)
471
480
  return false;
472
- await runClaimedAssignment(client, config, driver, workspace, brief, claimed.taskId, timeoutMs, supervision);
481
+ const selectedDriver = claimedAssignmentDriver(driver, claimed.driverId);
482
+ await runClaimedAssignment(client, config, selectedDriver, workspace, brief, claimed.taskId, timeoutMs, supervision);
473
483
  return true;
474
484
  }
475
485
  async function runClaimedAssignment(client, config, driver, workspace, brief, taskId, timeoutMs, supervision, options = {}) {
@@ -1071,7 +1081,7 @@ export async function conduitAliases(fuel) {
1071
1081
  return null;
1072
1082
  }
1073
1083
  }
1074
- async function resolveAssignmentModel(driver, config, spec, taskId, workspace, fuel) {
1084
+ export async function resolveAssignmentModel(driver, config, spec, taskId, workspace, fuel) {
1075
1085
  const tier = tierForRisk(spec.risk_level);
1076
1086
  const configured = config.models?.[tier];
1077
1087
  const risk = spec.risk_level === "low" || spec.risk_level === "medium" || spec.risk_level === "high" ? spec.risk_level : "unset";
@@ -1094,7 +1104,18 @@ async function resolveAssignmentModel(driver, config, spec, taskId, workspace, f
1094
1104
  return { risk, tier };
1095
1105
  }
1096
1106
  const candidates = Array.isArray(configured) ? configured : [configured];
1097
- const { model, skipped } = pickModelCandidate(candidates, await list());
1107
+ const available = await list();
1108
+ // Model names are driver-specific, but the tier -> model map is machine-wide, so switching the
1109
+ // online lane silently invalidates it. Under local fuel the lane's own default is authenticated
1110
+ // and works; a configured name we cannot confirm it supports hard-fails ("No API key found for
1111
+ // <vendor>") — Pi resolved a Claude-era "opus" to amazon-bedrock and Held. When we cannot verify a
1112
+ // local model, prefer the authenticated default over a guess that breaks the run. Under Conduit
1113
+ // fuel the CLI default cannot resolve, so the mapped alias is still the right bet even unverified.
1114
+ if (!fuel && available === null) {
1115
+ console.error(`Assignment ${taskId} intelligence tier ${tier} (risk ${risk}): configured model not confirmed available on ${driver.name}; using its authenticated default. Set this lane's own default model to pin a specific one.`);
1116
+ return { risk, tier };
1117
+ }
1118
+ const { model, skipped } = pickModelCandidate(candidates, available);
1098
1119
  const skippedNote = skipped.length ? ` (skipped unavailable/unsafe: ${skipped.join(", ")})` : "";
1099
1120
  if (!model) {
1100
1121
  const none = `Assignment ${taskId} intelligence tier ${tier} (risk ${risk}): no configured candidate available — CLI default${skippedNote}`;
package/dist/preflight.js CHANGED
@@ -1,10 +1,22 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { promisify } from "node:util";
3
4
  import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
4
5
  import { hasAntigravityLogin, hasClaudeLogin, hasOpenAiLogin, hasOpenCodeLogin, resolveCodexExecutable, } from "./driver.js";
5
- import { localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel } from "./drivers.js";
6
+ import { localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
6
7
  /** Protocol 3: read-only diagnosis can reuse a retained failed-attempt worktree. */
7
- export const BRIDGE_PROTOCOL_VERSION = 3;
8
+ // 4: each driver snapshot reports diagnosis_read_only, so the control plane can require a
9
+ // capable diagnostic lane *before* dispatching instead of learning it from a failed attempt.
10
+ /** Stable digest of the tier -> model mapping, so a model change moves the readiness fingerprint. */
11
+ function modelsFingerprint(config) {
12
+ const models = config.models ?? {};
13
+ const canonical = Object.keys(models).sort().map((tier) => {
14
+ const value = models[tier];
15
+ return `${tier}=${(Array.isArray(value) ? value : [value]).join("|")}`;
16
+ }).join(";");
17
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
18
+ }
19
+ export const BRIDGE_PROTOCOL_VERSION = 4;
8
20
  const execFileAsync = promisify(execFile);
9
21
  async function defaultCommandRunner(command, args, cwd) {
10
22
  try {
@@ -83,22 +95,27 @@ export async function runBridgePreflight(input, deps = {}) {
83
95
  issues.push({ code: status.code === 0 ? "workspace_dirty" : "workspace_unavailable" });
84
96
  }
85
97
  const driverChecks = await Promise.all(online.map(async (driver) => {
98
+ // Whether this lane can combine bounded test execution with enforced read-only repository
99
+ // access. Reported here so dispatch can require it; the control plane must not keep its own
100
+ // copy of driver-name policy.
101
+ const diagnosisReadOnly = supportsReadOnlyDiagnosis(driver);
86
102
  const fuel = resolveDriverFuel(input.config, driver);
87
103
  if (localFuelOnlyDriver(driver) && fuel !== "local") {
88
- return { issue: { code: "driver_fuel_mismatch", driver }, snapshot: { id: driver, version: null, ready: false } };
104
+ return { issue: { code: "driver_fuel_mismatch", driver }, snapshot: { id: driver, version: null, ready: false, diagnosis_read_only: diagnosisReadOnly } };
89
105
  }
90
106
  const version = await run(executableFor(driver), ["--version"], input.workspace);
91
107
  const versionText = `${version.stdout}${version.stderr}`.trim().slice(0, 200) || null;
92
108
  if (version.code !== 0 || !`${version.stdout}${version.stderr}`.trim()) {
93
- return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version: versionText, ready: false } };
109
+ return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
94
110
  }
95
111
  if (fuel === "local" && !await localAuthenticationReady(driver, input.workspace, run)) {
96
- return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false } };
112
+ return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
97
113
  }
98
- return { issue: null, snapshot: { id: driver, version: versionText, ready: true } };
114
+ return { issue: null, snapshot: { id: driver, version: versionText, ready: true, diagnosis_read_only: diagnosisReadOnly } };
99
115
  }));
100
116
  issues.push(...driverChecks.map((check) => check.issue).filter((issue) => issue !== null));
101
- return { ready: issues.length === 0, checked_at: new Date().toISOString(), workspace_clean: workspaceClean, drivers: driverChecks.map((check) => check.snapshot), issues };
117
+ return { ready: issues.length === 0, checked_at: new Date().toISOString(), workspace_clean: workspaceClean,
118
+ drivers: driverChecks.map((check) => check.snapshot), models_fingerprint: modelsFingerprint(input.config), issues };
102
119
  }
103
120
  let cached = null;
104
121
  /** Keep auth probes off the 15-second heartbeat hot path while still expiring readiness promptly. */
@@ -122,7 +139,8 @@ export function unavailableWorkspacePreflight(input) {
122
139
  ready: false,
123
140
  checked_at: new Date().toISOString(),
124
141
  workspace_clean: false,
125
- drivers: online.map((id) => ({ id, version: null, ready: false })),
142
+ drivers: online.map((id) => ({ id, version: null, ready: false, diagnosis_read_only: supportsReadOnlyDiagnosis(id) })),
143
+ models_fingerprint: modelsFingerprint(input.config),
126
144
  issues,
127
145
  };
128
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.13.1",
3
+ "version": "0.14.4",
4
4
  "description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {