@miraland-labs/conduit-bridge 0.11.2 → 0.11.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/README.md +1 -1
- package/dist/cli.js +27 -48
- package/dist/config.js +27 -5
- package/dist/driver.js +4 -6
- package/dist/drivers.js +1 -1
- package/dist/execution-class.js +1 -28
- package/dist/execution.js +40 -5
- package/dist/preflight.js +15 -0
- package/dist/service.js +0 -2
- package/package.json +1 -1
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.11.
|
|
5
|
+
**Package version:** `0.11.4` — finalize transport flakes stay **retryable**; stamped **ExecutionClass** is required (fail closed if missing). Heartbeat requires **protocol 2 + preflight** (protocol 1 rejected). Bring lanes online with `drivers online`, then `runner --workspace` or `install-service` (no `--agent` process override). Prefs commands (`fuel`, `drivers online|offline`) do not heartbeat a fake not-ready preflight over a live runner. In-flight claims prefer `runtime.json`; a one-time migrate still adopts legacy `config.json` claims when runtime is absent. Ordinary grants derive server-side as classMax ∩ project ceiling; landing is package `lands`.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ensureCheckout } from "./checkout.js";
|
|
|
18
18
|
import { pumpExecutionSlots, renewLeases } from "./execution.js";
|
|
19
19
|
import { installRunnerService, uninstallRunnerService } from "./service.js";
|
|
20
20
|
import { OPS_VERBS, runOps } from "./ops.js";
|
|
21
|
-
import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight } from "./preflight.js";
|
|
21
|
+
import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, unavailableWorkspacePreflight } from "./preflight.js";
|
|
22
22
|
const [command] = process.argv.slice(2);
|
|
23
23
|
/** Read our own package version so every runner start logs exactly which build is live. */
|
|
24
24
|
function bridgeVersion() {
|
|
@@ -35,7 +35,7 @@ const BRIDGE_NPX = "npx @miraland-labs/conduit-bridge";
|
|
|
35
35
|
function bridgeUsage(...args) {
|
|
36
36
|
return `${BRIDGE_NPX} ${args.join(" ")}`;
|
|
37
37
|
}
|
|
38
|
-
/** Single source for the
|
|
38
|
+
/** Single source for the driver-id placeholder so CLI help never drifts from DRIVERS. */
|
|
39
39
|
const AGENT_PLACEHOLDER = `<${Object.keys(DRIVERS).join("|")}>`;
|
|
40
40
|
function parseFuelSource(value) {
|
|
41
41
|
if (value === undefined)
|
|
@@ -231,7 +231,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
231
231
|
const client = new ConduitClient(config);
|
|
232
232
|
let heartbeatOk = true;
|
|
233
233
|
try {
|
|
234
|
-
await heartbeat(client, config, null);
|
|
234
|
+
await heartbeat(client, config, null, unavailableWorkspacePreflight({ config }));
|
|
235
235
|
}
|
|
236
236
|
catch {
|
|
237
237
|
heartbeatOk = false;
|
|
@@ -254,7 +254,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
254
254
|
console.log("Heartbeats report capabilities for diagnostics only; matching uses the Connect confirmation. Detected clients never receive grants automatically.");
|
|
255
255
|
console.log(`MCP setup: {"mcpServers":{"conduit":{"command":"npx","args":["-y","@miraland-labs/conduit-bridge","mcp"]}}}`);
|
|
256
256
|
console.log(`Bring a lane online: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
|
|
257
|
-
console.log(`Execute work: ${bridgeUsage("runner", "--workspace", "<repo>")}
|
|
257
|
+
console.log(`Execute work: ${bridgeUsage("runner", "--workspace", "<repo>")}`);
|
|
258
258
|
console.log(`Keep on shift (macOS/Linux): ${bridgeUsage("install-service", "--workspace", "<repo>")}`);
|
|
259
259
|
console.log(`Flip machine fuel later: ${bridgeUsage("fuel", "local|conduit")} (per-lane override: ${bridgeUsage("drivers", "fuel", "<id>", "local|conduit")})`);
|
|
260
260
|
console.log("Windows: keep the runner terminal open — install-service is macOS/Linux only.");
|
|
@@ -281,24 +281,15 @@ function openUrl(url) {
|
|
|
281
281
|
async function installService() {
|
|
282
282
|
let config = await loadConfig();
|
|
283
283
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
284
|
-
|
|
284
|
+
workspace: { type: "string" }, interval: { type: "string" }, "agent-timeout-minutes": { type: "string" },
|
|
285
285
|
"ensure-checkout": { type: "string" },
|
|
286
286
|
} });
|
|
287
287
|
if (!values.workspace) {
|
|
288
|
-
throw new Error(`Usage: ${bridgeUsage("install-service", "--workspace", "<repository-path>",
|
|
288
|
+
throw new Error(`Usage: ${bridgeUsage("install-service", "--workspace", "<repository-path>", "[--ensure-checkout <repository-url>]")} (workspace required; uses online driver lanes)`);
|
|
289
289
|
}
|
|
290
290
|
config = (await seedDriversFromDetection(config)).config;
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
throw new Error(`Unknown agent driver: ${values.agent}. Available: ${Object.keys(DRIVERS).join(", ")}`);
|
|
294
|
-
}
|
|
295
|
-
config = seedDriverLanes(config, [values.agent]).config;
|
|
296
|
-
config = setDriversOnline(config, [values.agent], true);
|
|
297
|
-
await saveConfigPrefs(config);
|
|
298
|
-
console.log(`Brought ${values.agent} online for this computer (shared capacity ${config.leaseCapacity}).`);
|
|
299
|
-
}
|
|
300
|
-
else if (!onlineDriverIds(config).length) {
|
|
301
|
-
throw new Error(`No online driver lanes. Run \`${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}\` first, or pass --agent to bring one online.`);
|
|
291
|
+
if (!onlineDriverIds(config).length) {
|
|
292
|
+
throw new Error(`No online driver lanes. Run \`${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}\` first.`);
|
|
302
293
|
}
|
|
303
294
|
const workspace = resolve(values.workspace);
|
|
304
295
|
if (values["ensure-checkout"]) {
|
|
@@ -307,7 +298,6 @@ async function installService() {
|
|
|
307
298
|
? `Cloned ${values["ensure-checkout"]} into ${workspace}`
|
|
308
299
|
: `Workspace already matches ${values["ensure-checkout"]}`);
|
|
309
300
|
}
|
|
310
|
-
// Supervisor service: no baked --agent; honors config drivers online set.
|
|
311
301
|
const result = await installRunnerService({
|
|
312
302
|
workspace,
|
|
313
303
|
interval: values.interval,
|
|
@@ -380,9 +370,8 @@ async function fuelCommand() {
|
|
|
380
370
|
const config = await loadConfig();
|
|
381
371
|
config.fuelSource = mode;
|
|
382
372
|
await saveConfigPrefs(config);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
console.log(`Machine fuel source set to ${mode === "local" ? "local subscription" : "Conduit pump"} and reported on heartbeat.`);
|
|
373
|
+
// Do not heartbeat here: a workspace-less preflight would overwrite a live runner's ready report.
|
|
374
|
+
console.log(`Machine fuel source set to ${mode === "local" ? "local subscription" : "Conduit pump"}. The runner reports it on the next heartbeat.`);
|
|
386
375
|
}
|
|
387
376
|
async function driversCommand() {
|
|
388
377
|
const sub = process.argv[3];
|
|
@@ -416,8 +405,7 @@ async function driversCommand() {
|
|
|
416
405
|
config = seedDriverLanes(config, ids).config;
|
|
417
406
|
config = setDriversOnline(config, ids, sub === "online");
|
|
418
407
|
await saveConfigPrefs(config);
|
|
419
|
-
|
|
420
|
-
await heartbeat(client, config, null).catch(() => undefined);
|
|
408
|
+
// Prefs only — a not-ready preflight here would clear a live runner's dispatch readiness.
|
|
421
409
|
console.log(`${sub === "online" ? "Online" : "Offline"}: ${ids.join(", ")}`);
|
|
422
410
|
console.log(`Now online: ${onlineDriverIds(config).join(", ") || "(none)"}`);
|
|
423
411
|
return;
|
|
@@ -438,7 +426,7 @@ async function driversCommand() {
|
|
|
438
426
|
}
|
|
439
427
|
async function runner() {
|
|
440
428
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
441
|
-
|
|
429
|
+
workspace: { type: "string" }, interval: { type: "string" }, once: { type: "boolean" },
|
|
442
430
|
"agent-timeout-minutes": { type: "string" }, fuel: { type: "string" }, "ensure-checkout": { type: "string" },
|
|
443
431
|
} });
|
|
444
432
|
let config = await loadConfig();
|
|
@@ -450,16 +438,6 @@ async function runner() {
|
|
|
450
438
|
config = (await seedDriversFromDetection(config)).config;
|
|
451
439
|
await saveConfigPrefs(config);
|
|
452
440
|
const client = new ConduitClient(config);
|
|
453
|
-
let processDriver = null;
|
|
454
|
-
let processOnlineIds = null;
|
|
455
|
-
if (values.agent) {
|
|
456
|
-
processDriver = DRIVERS[values.agent] ?? null;
|
|
457
|
-
if (!processDriver)
|
|
458
|
-
throw new Error(`Unknown agent driver: ${values.agent}. Available: ${Object.keys(DRIVERS).join(", ")}`);
|
|
459
|
-
if (!values.workspace)
|
|
460
|
-
throw new Error("--workspace <repository-path> is required with --agent");
|
|
461
|
-
processOnlineIds = [values.agent];
|
|
462
|
-
}
|
|
463
441
|
if (values["ensure-checkout"] && !values.workspace) {
|
|
464
442
|
throw new Error("--ensure-checkout requires --workspace <repository-path>");
|
|
465
443
|
}
|
|
@@ -473,14 +451,14 @@ async function runner() {
|
|
|
473
451
|
const brief = workspace ? await buildWorkspaceBrief(workspace) : null;
|
|
474
452
|
const intervalMs = values.interval ? Math.max(5_000, Number(values.interval)) : 15_000;
|
|
475
453
|
const timeoutMs = values["agent-timeout-minutes"] ? Number(values["agent-timeout-minutes"]) * 60_000 : undefined;
|
|
476
|
-
const canExecute = Boolean(workspace) &&
|
|
454
|
+
const canExecute = Boolean(workspace) && onlineDriverIds(config).length > 0;
|
|
477
455
|
if (!workspace) {
|
|
478
456
|
console.warn(`WARNING: heartbeat only — pass --workspace <repo>. Lanes: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
|
|
479
457
|
}
|
|
480
458
|
else if (!canExecute) {
|
|
481
|
-
console.warn(`WARNING: no online driver lanes — ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}
|
|
459
|
+
console.warn(`WARNING: no online driver lanes — ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
|
|
482
460
|
}
|
|
483
|
-
const onlineLabel =
|
|
461
|
+
const onlineLabel = onlineDriverIds(config).join(", ") || "(none)";
|
|
484
462
|
console.log(`Conduit runner (bridge v${bridgeVersion()}) connected to ${config.baseUrl}${workspace ? ` — workspace ${workspace}` : " — heartbeat only"} (lanes: ${onlineLabel}; machine fuel: ${config.fuelSource === "local" ? "local" : "conduit"}; slots: ${config.leaseCapacity})`);
|
|
485
463
|
const running = new Map();
|
|
486
464
|
for (;;) {
|
|
@@ -492,18 +470,20 @@ async function runner() {
|
|
|
492
470
|
config.fuelSource = latest.fuelSource;
|
|
493
471
|
config.leaseCapacity = latest.leaseCapacity;
|
|
494
472
|
const currentBrief = workspace ? await buildWorkspaceBrief(workspace).catch(() => brief) : null;
|
|
495
|
-
const preflight = workspace
|
|
496
|
-
|
|
473
|
+
const preflight = workspace
|
|
474
|
+
? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined })
|
|
475
|
+
: unavailableWorkspacePreflight({ config });
|
|
476
|
+
await heartbeat(client, config, currentBrief, preflight);
|
|
497
477
|
await renewLeases(client, config);
|
|
498
|
-
if (workspace &&
|
|
478
|
+
if (workspace && onlineDriverIds(config).length) {
|
|
499
479
|
progressed = await pumpExecutionSlots(client, config, workspace, brief, timeoutMs, {
|
|
500
480
|
heartbeat: async () => {
|
|
501
481
|
const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
|
|
502
|
-
const preflight = await cachedBridgePreflight({ config, workspace,
|
|
503
|
-
await heartbeat(client, config, currentBrief,
|
|
482
|
+
const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined });
|
|
483
|
+
await heartbeat(client, config, currentBrief, preflight);
|
|
504
484
|
},
|
|
505
485
|
heartbeatIntervalMs: intervalMs,
|
|
506
|
-
}, running, {
|
|
486
|
+
}, running, {});
|
|
507
487
|
}
|
|
508
488
|
}
|
|
509
489
|
catch (error) {
|
|
@@ -517,9 +497,8 @@ async function runner() {
|
|
|
517
497
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
|
|
518
498
|
}
|
|
519
499
|
}
|
|
520
|
-
async function heartbeat(client, config, brief,
|
|
521
|
-
|
|
522
|
-
const online = processOnlineIds?.length ? processOnlineIds : onlineDriverIds(config);
|
|
500
|
+
async function heartbeat(client, config, brief, preflight) {
|
|
501
|
+
const online = onlineDriverIds(config);
|
|
523
502
|
const status = heartbeatStatusForDrivers(online);
|
|
524
503
|
await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
|
|
525
504
|
status,
|
|
@@ -528,8 +507,8 @@ async function heartbeat(client, config, brief, processOnlineIds, preflight) {
|
|
|
528
507
|
fuel_source: config.fuelSource === "local" ? "local" : "conduit",
|
|
529
508
|
bridge_version: bridgeVersion(),
|
|
530
509
|
bridge_protocol: BRIDGE_PROTOCOL_VERSION,
|
|
531
|
-
drivers: driversHeartbeatReport(config, config.activeAttempts
|
|
532
|
-
|
|
510
|
+
drivers: driversHeartbeatReport(config, config.activeAttempts),
|
|
511
|
+
preflight,
|
|
533
512
|
...(brief ? { workspace_brief: brief } : {}),
|
|
534
513
|
}) });
|
|
535
514
|
}
|
package/dist/config.js
CHANGED
|
@@ -23,6 +23,22 @@ const runtimePath = join(directory, "runtime.json");
|
|
|
23
23
|
const lockPath = join(directory, "config.lock");
|
|
24
24
|
const pendingPath = join(directory, "pending-connect.json");
|
|
25
25
|
const installationPath = join(directory, "installation.json");
|
|
26
|
+
/**
|
|
27
|
+
* Prefer runtime.json; when absent, adopt legacy activeAttempts/sessions still on config.json
|
|
28
|
+
* so one Bridge upgrade does not strand in-flight claims.
|
|
29
|
+
*/
|
|
30
|
+
export function adoptLegacyRuntimeClaims(input) {
|
|
31
|
+
if (input.runtime) {
|
|
32
|
+
return {
|
|
33
|
+
activeAttempts: input.runtime.activeAttempts ?? {},
|
|
34
|
+
sessions: input.runtime.sessions,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
activeAttempts: input.legacyAttempts,
|
|
39
|
+
sessions: input.legacySessions,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
26
42
|
export async function loadConfig() {
|
|
27
43
|
const config = await loadConfigIfPresent();
|
|
28
44
|
if (!config)
|
|
@@ -49,9 +65,13 @@ export async function loadConfigIfPresent() {
|
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
const runtime = await loadRuntime();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
const adopted = adoptLegacyRuntimeClaims({
|
|
69
|
+
runtime,
|
|
70
|
+
legacyAttempts: config.activeAttempts && typeof config.activeAttempts === "object" ? config.activeAttempts : {},
|
|
71
|
+
legacySessions: config.sessions && typeof config.sessions === "object" ? config.sessions : undefined,
|
|
72
|
+
});
|
|
73
|
+
config.activeAttempts = adopted.activeAttempts;
|
|
74
|
+
config.sessions = adopted.sessions;
|
|
55
75
|
for (const active of Object.values(config.activeAttempts))
|
|
56
76
|
active.phase ??= "agent_running";
|
|
57
77
|
if (config.fuelSource !== "local" && config.fuelSource !== "conduit")
|
|
@@ -165,10 +185,12 @@ export async function saveConfig(config, options = {}) {
|
|
|
165
185
|
let legacySessions;
|
|
166
186
|
try {
|
|
167
187
|
const legacy = JSON.parse(await readFile(path, "utf8"));
|
|
168
|
-
if (legacy.activeAttempts && typeof legacy.activeAttempts === "object")
|
|
188
|
+
if (legacy.activeAttempts && typeof legacy.activeAttempts === "object") {
|
|
169
189
|
legacyAttempts = legacy.activeAttempts;
|
|
170
|
-
|
|
190
|
+
}
|
|
191
|
+
if (legacy.sessions && typeof legacy.sessions === "object") {
|
|
171
192
|
legacySessions = legacy.sessions;
|
|
193
|
+
}
|
|
172
194
|
}
|
|
173
195
|
catch { /* no prior config */ }
|
|
174
196
|
if (Object.keys(legacyAttempts).length || legacySessions) {
|
package/dist/driver.js
CHANGED
|
@@ -3,8 +3,8 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectKiroTools, projectPi, requireStampedExecutionClass,
|
|
7
|
-
export { branchCreateCommands, deniedCommands, isBoundedVerificationCommand, prCreateCommands, requireStampedExecutionClass,
|
|
6
|
+
import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
|
|
7
|
+
export { branchCreateCommands, deniedCommands, isBoundedVerificationCommand, prCreateCommands, requireStampedExecutionClass, } from "./execution-class.js";
|
|
8
8
|
function deliveryLanguageRule(language) {
|
|
9
9
|
if (language === "zh") {
|
|
10
10
|
return "- Write every human-facing delivery packet string (outcome, changes, verification, assumptions, risks, limitations, criterion text) in Chinese (Simplified). Keep JSON keys and enum token values in English.";
|
|
@@ -424,10 +424,8 @@ export const codexDriver = {
|
|
|
424
424
|
* publish shell under open class policy. Research-only networked assignments
|
|
425
425
|
* deny Shell(*) and Write(**).
|
|
426
426
|
*/
|
|
427
|
-
export function cursorPermissionsForGrants(grants, verificationCommands
|
|
428
|
-
const
|
|
429
|
-
?? resolveExecutionClass({ grants, capabilities, deliverable, workRole });
|
|
430
|
-
const { allow, deny } = projectCursor(cls, { grants, verificationCommands, capabilities });
|
|
427
|
+
export function cursorPermissionsForGrants(grants, verificationCommands, capabilities, executionClass) {
|
|
428
|
+
const { allow, deny } = projectCursor(executionClass, { grants, verificationCommands, capabilities });
|
|
431
429
|
return { allow, deny };
|
|
432
430
|
}
|
|
433
431
|
/**
|
package/dist/drivers.js
CHANGED
|
@@ -146,7 +146,7 @@ export function driversHeartbeatReport(config, activeAttempts, processOnlineIds)
|
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
148
|
* Pick an online driver for a new claim: least loaded among online (shared machine pool).
|
|
149
|
-
* `processOnlineIds` overrides config
|
|
149
|
+
* `processOnlineIds` overrides config online lanes (tests / harness).
|
|
150
150
|
*/
|
|
151
151
|
export function pickDriverForClaim(config, processOnlineIds) {
|
|
152
152
|
const online = (processOnlineIds?.length ? processOnlineIds : onlineDriverIds(config))
|
package/dist/execution-class.js
CHANGED
|
@@ -27,31 +27,6 @@ export const EXECUTION_CLASSES = [
|
|
|
27
27
|
"verify",
|
|
28
28
|
"mutate_repo",
|
|
29
29
|
];
|
|
30
|
-
/**
|
|
31
|
-
* Migrator / test helper only. Launch must use the control-plane stamped class
|
|
32
|
-
* via `requireStampedExecutionClass` — do not re-derive as source of truth.
|
|
33
|
-
*/
|
|
34
|
-
export function resolveExecutionClass(input) {
|
|
35
|
-
const grants = input.grants;
|
|
36
|
-
const capabilities = input.capabilities ?? [];
|
|
37
|
-
const deliverable = input.deliverable ?? "repository";
|
|
38
|
-
const workRole = input.workRole ?? null;
|
|
39
|
-
const hasWrite = grants.includes("repo_write");
|
|
40
|
-
const hasLand = grants.includes("branch_create") || grants.includes("pr_create");
|
|
41
|
-
const hasTest = grants.includes("test_run");
|
|
42
|
-
const hasRead = grants.includes("repo_read");
|
|
43
|
-
const external = capabilities.includes("external_network");
|
|
44
|
-
if (deliverable === "artifact" && workRole !== "research" && hasWrite)
|
|
45
|
-
return "publish_artifact";
|
|
46
|
-
if (external && !hasWrite && !hasLand && !hasTest)
|
|
47
|
-
return "observe_network";
|
|
48
|
-
// verify before observe: repo_read+test_run is the usual verify shape and must keep shell.
|
|
49
|
-
if (hasTest && !hasWrite && !hasLand)
|
|
50
|
-
return "verify";
|
|
51
|
-
if (!hasWrite && !hasLand && !hasTest && (hasRead || workRole === "research"))
|
|
52
|
-
return "observe";
|
|
53
|
-
return "mutate_repo";
|
|
54
|
-
}
|
|
55
30
|
const STAMPED_CLASSES = new Set(EXECUTION_CLASSES);
|
|
56
31
|
/** Fail closed when the assignment has no stamped class (upgrade Bridge / redeploy CP). */
|
|
57
32
|
export function requireStampedExecutionClass(stamped) {
|
|
@@ -89,9 +64,7 @@ export function assertClassFloor(executionClass, ordinaryGrants) {
|
|
|
89
64
|
* with `external_network` was therefore told to fetch by a prompt no driver could honour. One
|
|
90
65
|
* predicate, read by all of them, is what keeps that from recurring for the next class added.
|
|
91
66
|
*
|
|
92
|
-
* `observe_network` implies the capability by construction
|
|
93
|
-
* when `external_network` is present); it stays in the test so a spec that loses its capability list
|
|
94
|
-
* in transit still fails toward the class it was stamped with.
|
|
67
|
+
* `observe_network` implies the capability by construction.
|
|
95
68
|
*/
|
|
96
69
|
export function allowsExternalFetch(executionClass, capabilities = []) {
|
|
97
70
|
return executionClass === "observe_network" || capabilities.includes("external_network");
|
package/dist/execution.js
CHANGED
|
@@ -22,6 +22,34 @@ function changesRequestedFeedback(summary) {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* A network fault reaching the code host. Mirrors FORGE_TRANSPORT_PATTERN in the control plane's
|
|
27
|
+
* failures.ts — Bridge cannot import from there, so test/failure-classification.test.ts pins the two
|
|
28
|
+
* together rather than trusting them to stay in step.
|
|
29
|
+
*/
|
|
30
|
+
export const FORGE_TRANSPORT_PATTERN = /unable to access '?https?:\/\/|error in the http2 framing layer|could not resolve host|connection (?:reset|timed out|refused)|\bcurl\b.*\b(?:52|55|56|92)\b|remote end hung up unexpectedly|\brpc failed\b|tls handshake|network is unreachable|operation timed out/i;
|
|
31
|
+
export function forgeTransportFailure(message) {
|
|
32
|
+
return FORGE_TRANSPORT_PATTERN.test(message);
|
|
33
|
+
}
|
|
34
|
+
/** Environment faults that must Hold — mirror CP ENVIRONMENT_FAILURES message patterns. */
|
|
35
|
+
const FINALIZE_ENVIRONMENT_PATTERN = /base[_ ]not[_ ]ancestor|required base commit is not available|source[_ ]workspace[_ ]dirty|uncommitted changes|dirty workspace|workspace[_ ]head[_ ]changed|workspace[_ ]repository|workspace[_ ]unavailable|driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required|no[_ ]online[_ ]driver|bridge[_ ]preflight|stale bridge|bridge version/i;
|
|
36
|
+
/** Delivery-report / grant / evidence contract defects (non-retryable rework). */
|
|
37
|
+
const FINALIZE_CONTRACT_PATTERN = /missing required evidence|without the repo_write grant|pull_request_url|Artifact delivery requires|outside the approved scope|head commit|test evidence must include|Met acceptance criteria|Agent report|Agent reported|Delivery report/i;
|
|
38
|
+
/**
|
|
39
|
+
* Classify throws from ensureDeliveryPullRequest + validateDeliveryReport.
|
|
40
|
+
* Unknown non-contract faults must not be laundered as execution_contract_failed.
|
|
41
|
+
*/
|
|
42
|
+
export function classifyFinalizeFailure(message) {
|
|
43
|
+
if (forgeTransportFailure(message))
|
|
44
|
+
return { retryable: true, error: message };
|
|
45
|
+
if (FINALIZE_ENVIRONMENT_PATTERN.test(message))
|
|
46
|
+
return { retryable: false, error: message };
|
|
47
|
+
if (FINALIZE_CONTRACT_PATTERN.test(message))
|
|
48
|
+
return { retryable: false, error: message };
|
|
49
|
+
// Unknown finalize faults fail closed once — retrying burns the attempt budget (Invariant 22).
|
|
50
|
+
// Distinct from execution_contract_failed so owners see a Bridge fault, not a contract lie.
|
|
51
|
+
return { retryable: false, error: `Bridge finalize interrupted: ${message}` };
|
|
52
|
+
}
|
|
25
53
|
/** Failures that require an operator/configuration change must never burn the remaining attempts. */
|
|
26
54
|
export function retryableAgentFailure(message) {
|
|
27
55
|
// `cannot enforce` is a driver projection refusing a class it structurally cannot bound (Pi and
|
|
@@ -188,7 +216,6 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
|
|
|
188
216
|
let laneDriver = null;
|
|
189
217
|
let driverId = null;
|
|
190
218
|
if (fallbackDriver) {
|
|
191
|
-
// Legacy `--agent` / tests: one fixed driver for every new claim.
|
|
192
219
|
laneDriver = fallbackDriver;
|
|
193
220
|
driverId = processOnlineIds?.[0]
|
|
194
221
|
?? Object.entries(DRIVERS).find(([, d]) => d === fallbackDriver)?.[0]
|
|
@@ -521,10 +548,18 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
521
548
|
}
|
|
522
549
|
catch (error) {
|
|
523
550
|
const message = error instanceof Error ? error.message : "Agent delivery report was invalid";
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
551
|
+
// classifyFinalizeFailure: contract/environment → non-retryable; forge transport → retryable;
|
|
552
|
+
// unknown faults stay retryable as "Bridge finalize interrupted" (not laundered as contract).
|
|
553
|
+
const classified = classifyFinalizeFailure(message);
|
|
554
|
+
await queueTerminal(client, taskId, {
|
|
555
|
+
action: "fail",
|
|
556
|
+
body: {
|
|
557
|
+
error: classified.error,
|
|
558
|
+
retryable: classified.retryable,
|
|
559
|
+
idempotency_key: `bridge:invalid-delivery:${active.attemptId}`,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
console.error(`Assignment ${taskId} could not produce a valid Delivery: ${redactSecrets(classified.error)}`);
|
|
528
563
|
const replyTail = reportText.slice(-8_000);
|
|
529
564
|
console.error(`Assignment ${taskId} agent reply tail (${reportText.length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
|
|
530
565
|
return;
|
package/dist/preflight.js
CHANGED
|
@@ -110,6 +110,21 @@ export async function cachedBridgePreflight(input) {
|
|
|
110
110
|
cached = { key, at: Date.now(), report };
|
|
111
111
|
return report;
|
|
112
112
|
}
|
|
113
|
+
/** Heartbeat-only / missing workspace still reports protocol 2 preflight (not ready). */
|
|
114
|
+
export function unavailableWorkspacePreflight(input) {
|
|
115
|
+
const online = input.processOnlineIds?.length ? input.processOnlineIds : onlineDriverIds(input.config);
|
|
116
|
+
const issues = [];
|
|
117
|
+
if (!online.length)
|
|
118
|
+
issues.push({ code: "no_online_driver" });
|
|
119
|
+
issues.push({ code: "workspace_unavailable" });
|
|
120
|
+
return {
|
|
121
|
+
ready: false,
|
|
122
|
+
checked_at: new Date().toISOString(),
|
|
123
|
+
workspace_clean: false,
|
|
124
|
+
drivers: online.map((id) => ({ id, version: null, ready: false })),
|
|
125
|
+
issues,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
113
128
|
export function describePreflightIssue(issue) {
|
|
114
129
|
const lane = issue.driver ? ` (${issue.driver})` : "";
|
|
115
130
|
if (issue.code === "no_online_driver")
|
package/dist/service.js
CHANGED
|
@@ -22,8 +22,6 @@ export function runnerProgramArguments(options = {}) {
|
|
|
22
22
|
if (!cli)
|
|
23
23
|
throw new Error("Unable to resolve the conduit CLI path");
|
|
24
24
|
const args = [process.execPath, cli, "runner"];
|
|
25
|
-
if (options.agent)
|
|
26
|
-
args.push("--agent", options.agent);
|
|
27
25
|
if (options.workspace)
|
|
28
26
|
args.push("--workspace", options.workspace);
|
|
29
27
|
if (options.ensureCheckout)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.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": {
|