@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 +6 -2
- package/dist/execution.js +25 -4
- package/dist/preflight.js +26 -8
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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": {
|