@miraland-labs/conduit-bridge 0.16.18 → 0.16.19
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 +16 -11
- package/dist/client.js +12 -1
- package/dist/detect.js +1 -1
- package/dist/drivers.js +22 -1
- package/dist/ensure-test-evidence.js +72 -40
- package/dist/execution.js +10 -4
- package/dist/on-shift-apply.js +28 -31
- package/dist/ops.js +83 -66
- package/dist/preflight.js +4 -4
- package/dist/service.js +70 -45
- package/ops/env.example +5 -3
- 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.
|
|
5
|
+
**Package version:** `0.16.19` — `ops.env` is enrollment/bootstrap defaults only: an explicit workspace does not inherit an old repository, and project switches do not rewrite global project state. `ops enroll` exchanges a single-use token, registers the machine, binds project affinity, provisions fuel keys, brings detected drivers online, and starts the runner in one command. On Linux, `ops install` refuses any effective systemd drop-in. A switch intent remains queued until the target runner heartbeat proves Bound. After every Bridge publish, operators must re-run `ops install` (LaunchAgent pins an absolute `cli.js`).
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ 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
17
|
import { ensureCheckout } from "./checkout.js";
|
|
18
|
-
import { maybeApplyOnShiftIntent } from "./on-shift-apply.js";
|
|
18
|
+
import { buildOnShiftIntentProof, maybeApplyOnShiftIntent } from "./on-shift-apply.js";
|
|
19
19
|
import { loadOpsEnv, OPS_VERBS, runOps } from "./ops.js";
|
|
20
20
|
import { pumpExecutionSlots, renewLeases } from "./execution.js";
|
|
21
21
|
import { applyRunnerToolPath, installRunnerService, uninstallRunnerService } from "./service.js";
|
|
@@ -123,7 +123,7 @@ async function join() {
|
|
|
123
123
|
console.log(`Detected local clients: ${detected.join(", ") || "none"}`);
|
|
124
124
|
console.log(`Proposed roles: ${capabilities.join(", ")}`);
|
|
125
125
|
console.log(`Assignments at once: ${leaseCapacity}`);
|
|
126
|
-
console.log(`Fuel source: ${plannedFuel === "local" ? "local
|
|
126
|
+
console.log(`Fuel source: ${plannedFuel === "local" ? "local agent login or environment API key" : "Conduit pump"}`
|
|
127
127
|
+ (!fuelSource && plannedFuel === "local" ? " (auto: only local-fuel agents detected)" : ""));
|
|
128
128
|
console.log(`Execution drivers: ${Object.keys(DRIVERS).join(", ")} (cursor/pi/kiro/antigravity/grok require local fuel).`);
|
|
129
129
|
console.log("Use --machine <name> to override the computer label.\n");
|
|
@@ -269,7 +269,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
269
269
|
const client = new ConduitClient(config);
|
|
270
270
|
let heartbeatOk = true;
|
|
271
271
|
try {
|
|
272
|
-
await heartbeat(client, config, null, unavailableWorkspacePreflight({ config }));
|
|
272
|
+
await heartbeat(client, config, null, null, unavailableWorkspacePreflight({ config }));
|
|
273
273
|
}
|
|
274
274
|
catch {
|
|
275
275
|
heartbeatOk = false;
|
|
@@ -283,7 +283,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
283
283
|
}
|
|
284
284
|
console.log(`Confirmed capabilities (matching): ${config.capabilities.join(", ") || "none"}`);
|
|
285
285
|
console.log(`Allowed actions: ${config.grants.join(", ") || "none"}`);
|
|
286
|
-
console.log(`Fuel source: ${config.fuelSource === "local" ? "local
|
|
286
|
+
console.log(`Fuel source: ${config.fuelSource === "local" ? "local agent login or environment API key" : "Conduit /v1 (project owner BYOK on open orgs; org credentials on invite-only)"}`
|
|
287
287
|
+ (fuelAutoLocal ? " (auto: only local-fuel agents detected)" : ""));
|
|
288
288
|
const localOnly = localFuelOnlyClients(detected);
|
|
289
289
|
if (config.fuelSource === "conduit" && localOnly.length) {
|
|
@@ -373,7 +373,7 @@ async function initOps() {
|
|
|
373
373
|
console.log("Works the same on macOS, Linux, and Windows via:");
|
|
374
374
|
console.log(` ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|disconnect|uninstall>")}`);
|
|
375
375
|
console.log("");
|
|
376
|
-
console.log("1.
|
|
376
|
+
console.log("1. Set enrollment/bootstrap defaults once:");
|
|
377
377
|
console.log(` Create folder: ${configDir}`);
|
|
378
378
|
console.log(` Copy ${pathJoin(target, "env.example")} → ${envPath}`);
|
|
379
379
|
console.log(` # edit ${envPath}`);
|
|
@@ -408,7 +408,7 @@ async function fuelCommand() {
|
|
|
408
408
|
config.fuelSource = mode;
|
|
409
409
|
await saveConfigPrefs(config);
|
|
410
410
|
// Do not heartbeat here: a workspace-less preflight would overwrite a live runner's ready report.
|
|
411
|
-
console.log(`Machine fuel source set to ${mode === "local" ? "local
|
|
411
|
+
console.log(`Machine fuel source set to ${mode === "local" ? "local agent login or environment API key" : "Conduit pump"}. The runner reports exact lane provenance on the next heartbeat.`);
|
|
412
412
|
}
|
|
413
413
|
async function driversCommand() {
|
|
414
414
|
const sub = process.argv[3];
|
|
@@ -511,14 +511,13 @@ async function runner() {
|
|
|
511
511
|
const preflight = workspace
|
|
512
512
|
? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined })
|
|
513
513
|
: unavailableWorkspacePreflight({ config });
|
|
514
|
-
const hb = await heartbeat(client, config, currentBrief, preflight);
|
|
514
|
+
const hb = await heartbeat(client, config, workspace, currentBrief, preflight);
|
|
515
515
|
if (hb.staleBridge) {
|
|
516
516
|
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
517
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, Math.max(intervalMs, 60_000)));
|
|
518
518
|
continue;
|
|
519
519
|
}
|
|
520
520
|
const applied = await maybeApplyOnShiftIntent({
|
|
521
|
-
client,
|
|
522
521
|
currentWorkspace: workspace,
|
|
523
522
|
onShift: hb.on_shift,
|
|
524
523
|
runInstall: async (nextWorkspace, repo) => {
|
|
@@ -531,7 +530,7 @@ async function runner() {
|
|
|
531
530
|
}
|
|
532
531
|
await runOps("install", [], {
|
|
533
532
|
env: env
|
|
534
|
-
? { ...env, CONDUIT_WORKSPACE: nextWorkspace, CONDUIT_REPO: repo
|
|
533
|
+
? { ...env, CONDUIT_WORKSPACE: nextWorkspace, CONDUIT_REPO: repo }
|
|
535
534
|
: undefined,
|
|
536
535
|
});
|
|
537
536
|
},
|
|
@@ -540,6 +539,10 @@ async function runner() {
|
|
|
540
539
|
console.log("On-shift workspace applied; exiting so the background service restarts on the new checkout.");
|
|
541
540
|
process.exit(0);
|
|
542
541
|
}
|
|
542
|
+
const intentProof = buildOnShiftIntentProof({ currentWorkspace: workspace, onShift: hb.on_shift });
|
|
543
|
+
if (intentProof) {
|
|
544
|
+
await heartbeat(client, config, workspace, currentBrief, preflight, intentProof);
|
|
545
|
+
}
|
|
543
546
|
await renewLeases(client, config);
|
|
544
547
|
if (workspace && onlineDriverIds(config).length) {
|
|
545
548
|
// Observation runs before authoring: an owner waiting on an answer should not queue behind
|
|
@@ -551,7 +554,7 @@ async function runner() {
|
|
|
551
554
|
heartbeat: async () => {
|
|
552
555
|
const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
|
|
553
556
|
const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined });
|
|
554
|
-
await heartbeat(client, config, currentBrief, preflight);
|
|
557
|
+
await heartbeat(client, config, workspace, currentBrief, preflight);
|
|
555
558
|
},
|
|
556
559
|
heartbeatIntervalMs: intervalMs,
|
|
557
560
|
}, running, {});
|
|
@@ -568,18 +571,20 @@ async function runner() {
|
|
|
568
571
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
|
|
569
572
|
}
|
|
570
573
|
}
|
|
571
|
-
async function heartbeat(client, config, brief, preflight) {
|
|
574
|
+
async function heartbeat(client, config, workspacePath, brief, preflight, intentProof) {
|
|
572
575
|
const online = onlineDriverIds(config);
|
|
573
576
|
const status = heartbeatStatusForDrivers(online);
|
|
574
577
|
const response = await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
|
|
575
578
|
status,
|
|
576
579
|
capabilities: config.capabilities,
|
|
577
580
|
lease_capacity: config.leaseCapacity,
|
|
581
|
+
workspace_path: workspacePath,
|
|
578
582
|
fuel_source: config.fuelSource === "local" ? "local" : "conduit",
|
|
579
583
|
bridge_version: bridgeVersion(),
|
|
580
584
|
bridge_protocol: BRIDGE_PROTOCOL_VERSION,
|
|
581
585
|
drivers: driversHeartbeatReport(config, config.activeAttempts),
|
|
582
586
|
preflight,
|
|
587
|
+
...(intentProof ? { on_shift_intent_proof: intentProof } : {}),
|
|
583
588
|
...(brief ? { workspace_brief: brief } : {}),
|
|
584
589
|
}) });
|
|
585
590
|
const compatibility = response.bridge_compatibility && typeof response.bridge_compatibility === "object"
|
package/dist/client.js
CHANGED
|
@@ -44,7 +44,17 @@ export class ConduitClient {
|
|
|
44
44
|
// its hash, so the client mints it and re-sends the identical token if it ever retries the
|
|
45
45
|
// claim (the idempotency key is deterministic per attempt exactly so that replay works).
|
|
46
46
|
const leaseToken = `lease_${crypto.randomUUID()}${crypto.randomUUID()}`;
|
|
47
|
-
const data = await this.request(`/runner/v1/tasks/${taskId}/claim`, {
|
|
47
|
+
const data = await this.request(`/runner/v1/tasks/${taskId}/claim`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
attempt_id: attemptId,
|
|
51
|
+
lease_token: leaseToken,
|
|
52
|
+
idempotency_key: `bridge:claim:${attemptId}`,
|
|
53
|
+
...(extras?.driverId ? { driver_id: extras.driverId } : {}),
|
|
54
|
+
...(extras?.fuelMode ? { fuel_mode: extras.fuelMode } : {}),
|
|
55
|
+
...(extras?.fuelProvenance ? { fuel_provenance: extras.fuelProvenance } : {}),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
48
58
|
const active = {
|
|
49
59
|
taskId,
|
|
50
60
|
attemptId,
|
|
@@ -52,6 +62,7 @@ export class ConduitClient {
|
|
|
52
62
|
leaseExpiresAt: String(data.lease_expires_at),
|
|
53
63
|
phase: "claimed",
|
|
54
64
|
...(extras?.driverId ? { driverId: extras.driverId } : {}),
|
|
65
|
+
...(extras?.fuelMode ? { fuelMode: extras.fuelMode } : {}),
|
|
55
66
|
...(extras?.executionKind ? { executionKind: extras.executionKind } : {}),
|
|
56
67
|
...(extras?.sourceAttemptId ? { sourceAttemptId: extras.sourceAttemptId } : {}),
|
|
57
68
|
};
|
package/dist/detect.js
CHANGED
|
@@ -42,7 +42,7 @@ export function probeAgentHealth(detected, env = process.env) {
|
|
|
42
42
|
label: "Claude Code",
|
|
43
43
|
installed: claudeInstalled,
|
|
44
44
|
authenticated: claudeInstalled,
|
|
45
|
-
authMethod: env.ANTHROPIC_API_KEY ? "environment_key" : claudeAuth ? "local_login" : "conduit_pump",
|
|
45
|
+
authMethod: (env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN) ? "environment_key" : claudeAuth ? "local_login" : "conduit_pump",
|
|
46
46
|
});
|
|
47
47
|
const codexInstalled = detectedSet.has("Codex CLI");
|
|
48
48
|
const codexAuth = hasOpenAiLogin(env);
|
package/dist/drivers.js
CHANGED
|
@@ -173,6 +173,26 @@ export function resolveDriverFuel(config, driverId) {
|
|
|
173
173
|
return lane.fuel;
|
|
174
174
|
return config.fuelSource === "local" ? "local" : "conduit";
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Credential class the selected lane will actually pass to its agent process. Secret values never
|
|
178
|
+
* leave the machine; only this bounded provenance is reported to the Control Plane.
|
|
179
|
+
*/
|
|
180
|
+
export function resolveDriverFuelProvenance(config, driverId, env = process.env, fuelMode = resolveDriverFuel(config, driverId)) {
|
|
181
|
+
if (fuelMode === "conduit")
|
|
182
|
+
return "conduit_gateway";
|
|
183
|
+
const apiKeys = {
|
|
184
|
+
"claude-code": ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
|
|
185
|
+
codex: ["OPENAI_API_KEY", "CODEX_API_KEY"],
|
|
186
|
+
cursor: ["CURSOR_API_KEY"],
|
|
187
|
+
opencode: ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
188
|
+
pi: ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "CODEX_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "XAI_API_KEY"],
|
|
189
|
+
kiro: ["KIRO_API_KEY"],
|
|
190
|
+
antigravity: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
191
|
+
grok: ["XAI_API_KEY"],
|
|
192
|
+
};
|
|
193
|
+
const names = isSupportedDriverId(driverId) ? apiKeys[driverId] ?? [] : [];
|
|
194
|
+
return names.some((name) => Boolean(env[name]?.trim())) ? "environment_api_key" : "local_login";
|
|
195
|
+
}
|
|
176
196
|
export function setDriverFuel(config, driverId, fuel) {
|
|
177
197
|
if (!isSupportedDriverId(driverId))
|
|
178
198
|
throw new Error(`Unknown driver: ${driverId}`);
|
|
@@ -197,7 +217,7 @@ export function advertiseLeaseCapacity(approved, onlineIds) {
|
|
|
197
217
|
return 1; // schema/heartbeat min; status standby prevents matching
|
|
198
218
|
return approved;
|
|
199
219
|
}
|
|
200
|
-
export function driversHeartbeatReport(config, activeAttempts, processOnlineIds) {
|
|
220
|
+
export function driversHeartbeatReport(config, activeAttempts, processOnlineIds, env = process.env) {
|
|
201
221
|
const processOnline = new Set(processOnlineIds ?? []);
|
|
202
222
|
const busyByDriver = new Map();
|
|
203
223
|
for (const active of Object.values(activeAttempts)) {
|
|
@@ -212,6 +232,7 @@ export function driversHeartbeatReport(config, activeAttempts, processOnlineIds)
|
|
|
212
232
|
state: processOnline.has(lane.id) ? "online" : lane.state,
|
|
213
233
|
busy: (busyByDriver.get(lane.id) ?? 0) > 0,
|
|
214
234
|
fuel: lane.fuel,
|
|
235
|
+
fuel_provenance: resolveDriverFuelProvenance(config, lane.id, env),
|
|
215
236
|
}));
|
|
216
237
|
}
|
|
217
238
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* When the contract requires test evidence, Bridge runs
|
|
3
|
-
*
|
|
2
|
+
* When the contract requires test evidence, Bridge runs the bounded verification
|
|
3
|
+
* commands and attaches their verbatim results. Agent prose may supplement those runs;
|
|
4
4
|
* it never substitutes for it.
|
|
5
5
|
*/
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
@@ -31,6 +31,14 @@ export function pickVerificationCommand(commands) {
|
|
|
31
31
|
.filter((command) => command && isBoundedVerificationCommand(command));
|
|
32
32
|
return bounded.find(isPreferentialTestCommand) ?? bounded[0] ?? null;
|
|
33
33
|
}
|
|
34
|
+
/** Prefer one project-defined aggregate; otherwise witness every distinct bounded command. */
|
|
35
|
+
export function verificationEvidenceCommands(commands) {
|
|
36
|
+
const bounded = [...new Set(commands
|
|
37
|
+
.map((command) => command.trim())
|
|
38
|
+
.filter((command) => command && isBoundedVerificationCommand(command)))];
|
|
39
|
+
const aggregate = bounded.find((command) => /^(?:npm run|pnpm|yarn) verify$|^make check$/.test(command));
|
|
40
|
+
return aggregate ? [aggregate] : bounded;
|
|
41
|
+
}
|
|
34
42
|
function argvForBoundedCommand(command) {
|
|
35
43
|
return command.trim().split(/\s+/);
|
|
36
44
|
}
|
|
@@ -118,11 +126,29 @@ async function defaultRunCommand(command, workspace) {
|
|
|
118
126
|
* Reuse only criteria the agent already mapped onto a test-kind evidence entry (including thin ones
|
|
119
127
|
* we are replacing). Empty mapping lets validateDeliveryReport reject met-without-evidence.
|
|
120
128
|
*/
|
|
121
|
-
|
|
129
|
+
function verificationCommandNames(command) {
|
|
130
|
+
const normalized = command.trim().replace(/\s+/g, " ").toLowerCase();
|
|
131
|
+
const names = new Set([normalized]);
|
|
132
|
+
if (!isBoundedVerificationCommand(normalized))
|
|
133
|
+
return names;
|
|
134
|
+
const script = normalized.match(/^(?:npm|pnpm|yarn) run ([^\s]+)(?:\s|$)/)?.[1]
|
|
135
|
+
?? normalized.match(/^(?:pnpm|yarn) ([^\s]+)(?:\s|$)/)?.[1]
|
|
136
|
+
?? normalized.match(/^npm ([^\s]+)(?:\s|$)/)?.[1];
|
|
137
|
+
if (script)
|
|
138
|
+
names.add(script);
|
|
139
|
+
return names;
|
|
140
|
+
}
|
|
141
|
+
export function criteriaForTestEvidence(report, command) {
|
|
142
|
+
const testEvidence = report.evidence.filter((item) => item.kind === "test");
|
|
143
|
+
const commandNames = command ? verificationCommandNames(command) : null;
|
|
144
|
+
const sources = commandNames
|
|
145
|
+
? testEvidence.filter((item) => {
|
|
146
|
+
const evidenceNames = verificationCommandNames(item.name);
|
|
147
|
+
return [...commandNames].some((name) => evidenceNames.has(name));
|
|
148
|
+
})
|
|
149
|
+
: testEvidence;
|
|
122
150
|
const mapped = new Set();
|
|
123
|
-
for (const item of
|
|
124
|
-
if (item.kind !== "test")
|
|
125
|
-
continue;
|
|
151
|
+
for (const item of sources) {
|
|
126
152
|
for (const criterion of item.acceptance_criteria ?? []) {
|
|
127
153
|
if (criterion)
|
|
128
154
|
mapped.add(criterion);
|
|
@@ -131,42 +157,52 @@ export function criteriaForTestEvidence(report) {
|
|
|
131
157
|
return [...mapped];
|
|
132
158
|
}
|
|
133
159
|
/**
|
|
134
|
-
* If test evidence is required, run
|
|
135
|
-
* and attach verbatim stdout/stderr with
|
|
160
|
+
* If test evidence is required, run the bounded verification commands in the attempt workspace
|
|
161
|
+
* and attach verbatim stdout/stderr with Bridge witness markers.
|
|
136
162
|
*/
|
|
137
163
|
export async function ensureTestEvidence(input) {
|
|
138
164
|
if (!needsTestEvidence(input.report, input.spec, input.grants))
|
|
139
165
|
return input.report;
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
166
|
+
const commands = verificationEvidenceCommands(input.verificationCommands);
|
|
167
|
+
if (commands.length === 0) {
|
|
142
168
|
throw new Error("Agent report is missing required evidence: test (no bounded verification command available for Bridge to run)");
|
|
143
169
|
}
|
|
144
170
|
const run = input.runCommand ?? defaultRunCommand;
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
171
|
+
const witnessed = [];
|
|
172
|
+
for (const command of commands) {
|
|
173
|
+
const result = await run(command, input.workspace);
|
|
174
|
+
const stdoutLines = result.stdout.trim() ? result.stdout.trim().split("\n") : [];
|
|
175
|
+
const stderrLines = result.stderr.trim() ? result.stderr.trim().split("\n") : [];
|
|
176
|
+
if (result.code !== 0) {
|
|
177
|
+
// Prefix must match FINALIZE_CONTRACT_PATTERN ("Agent report") — not Bridge-fault laundering.
|
|
178
|
+
const failedDetails = [
|
|
179
|
+
`$ ${command}`,
|
|
180
|
+
...stdoutLines,
|
|
181
|
+
...stderrLines,
|
|
182
|
+
`exit ${result.code}`,
|
|
183
|
+
].map((line) => line.slice(0, 4_000)).slice(0, 100);
|
|
184
|
+
throw new Error(`Agent report: Verification failed (${command}): ${failedDetails.join("\n").slice(0, 2_000)}`);
|
|
185
|
+
}
|
|
186
|
+
// Empty exit-0 is not proof — a no-op recipe (`@true`) must not clear required_evidence: test.
|
|
187
|
+
if (stdoutLines.length === 0 && stderrLines.length === 0) {
|
|
188
|
+
throw new Error(`Agent report: Verification produced no output for test evidence (${command}). A bounded test command must print observable stdout or stderr.`);
|
|
189
|
+
}
|
|
190
|
+
const details = [
|
|
151
191
|
`$ ${command}`,
|
|
152
192
|
...stdoutLines,
|
|
153
193
|
...stderrLines,
|
|
154
194
|
`exit ${result.code}`,
|
|
155
195
|
].map((line) => line.slice(0, 4_000)).slice(0, 100);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`exit ${result.code}`,
|
|
167
|
-
].map((line) => line.slice(0, 4_000)).slice(0, 100);
|
|
168
|
-
if (details.join("\n").trim().length < TEST_EVIDENCE_DETAILS_MIN) {
|
|
169
|
-
throw new Error(`Agent report: Verification produced insufficient output for test evidence (${command})`);
|
|
196
|
+
if (details.join("\n").trim().length < TEST_EVIDENCE_DETAILS_MIN) {
|
|
197
|
+
throw new Error(`Agent report: Verification produced insufficient output for test evidence (${command})`);
|
|
198
|
+
}
|
|
199
|
+
witnessed.push({
|
|
200
|
+
kind: "test",
|
|
201
|
+
name: command,
|
|
202
|
+
details,
|
|
203
|
+
acceptance_criteria: criteriaForTestEvidence(input.report, command),
|
|
204
|
+
witness: "bridge",
|
|
205
|
+
});
|
|
170
206
|
}
|
|
171
207
|
// Keep agent test prose as supplement; Bridge witness is the authoritative entry.
|
|
172
208
|
const withoutThinTest = input.report.evidence.filter((item) => {
|
|
@@ -176,18 +212,14 @@ export async function ensureTestEvidence(input) {
|
|
|
176
212
|
});
|
|
177
213
|
return {
|
|
178
214
|
...input.report,
|
|
179
|
-
verification:
|
|
180
|
-
|
|
181
|
-
|
|
215
|
+
verification: [
|
|
216
|
+
...input.report.verification,
|
|
217
|
+
...commands.filter((command) => !input.report.verification.some((item) => item.startsWith(`${command} —`)))
|
|
218
|
+
.map((command) => `${command} — exit 0`),
|
|
219
|
+
],
|
|
182
220
|
evidence: [
|
|
183
221
|
...withoutThinTest,
|
|
184
|
-
|
|
185
|
-
kind: "test",
|
|
186
|
-
name: command,
|
|
187
|
-
details,
|
|
188
|
-
acceptance_criteria: criteriaForTestEvidence(input.report),
|
|
189
|
-
witness: "bridge",
|
|
190
|
-
},
|
|
222
|
+
...witnessed,
|
|
191
223
|
],
|
|
192
224
|
};
|
|
193
225
|
}
|
package/dist/execution.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ConduitRequestError } from "./client.js";
|
|
|
5
5
|
import { redactSecrets, saveDriverQuota } from "./config.js";
|
|
6
6
|
import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, requireStampedExecutionClass, tierForRisk } from "./driver.js";
|
|
7
7
|
import { assertClassFloor } from "./execution-class.js";
|
|
8
|
-
import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
8
|
+
import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, resolveDriverFuelProvenance, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
9
9
|
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { promisify } from "node:util";
|
|
@@ -608,8 +608,12 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
|
|
|
608
608
|
return null;
|
|
609
609
|
}
|
|
610
610
|
console.log(`Claiming ${assignment.execution_kind} assignment ${assignment.id} (attempt ${assignment.attempt_id})`);
|
|
611
|
+
const fuelMode = selectedDriverId ? resolveDriverFuel(config, selectedDriverId) : undefined;
|
|
612
|
+
const fuelProvenance = selectedDriverId ? resolveDriverFuelProvenance(config, selectedDriverId) : undefined;
|
|
611
613
|
await client.claim(assignment.id, assignment.attempt_id, {
|
|
612
614
|
...(selectedDriverId ? { driverId: selectedDriverId } : {}),
|
|
615
|
+
...(fuelMode ? { fuelMode } : {}),
|
|
616
|
+
...(fuelProvenance ? { fuelProvenance } : {}),
|
|
613
617
|
executionKind: assignment.execution_kind,
|
|
614
618
|
...(assignment.source_attempt_id ? { sourceAttemptId: assignment.source_attempt_id } : {}),
|
|
615
619
|
});
|
|
@@ -909,7 +913,8 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
909
913
|
?? "unknown";
|
|
910
914
|
if (!active.driverId && driverId !== "unknown")
|
|
911
915
|
await client.updateAttempt(taskId, { driverId });
|
|
912
|
-
const fuelSource = resolveDriverFuel(config, driverId);
|
|
916
|
+
const fuelSource = active.fuelMode ?? resolveDriverFuel(config, driverId);
|
|
917
|
+
const fuelProvenance = resolveDriverFuelProvenance(config, driverId, process.env, fuelSource);
|
|
913
918
|
let fuel;
|
|
914
919
|
if (fuelSource === "conduit") {
|
|
915
920
|
const gatewayKey = await client.ensureFuel(task.project_id);
|
|
@@ -918,6 +923,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
918
923
|
const selection = await resolveAssignmentModel(driver, config, spec, taskId, attemptWorkspace, fuel);
|
|
919
924
|
await client.attemptRequest(taskId, "model-selection", {
|
|
920
925
|
risk: selection.risk, tier: selection.tier, model: selection.model ?? null, driver: driver.name,
|
|
926
|
+
fuel_mode: fuelSource, fuel_provenance: fuelProvenance,
|
|
921
927
|
...(selection.model ? {} : { note: "cli-default" }), idempotency_key: `model:${active.attemptId}`,
|
|
922
928
|
});
|
|
923
929
|
// Persist the start *after* model selection, so audit history proves the agent ran with the model
|
|
@@ -1683,7 +1689,7 @@ function parseTaskSpec(value) {
|
|
|
1683
1689
|
}
|
|
1684
1690
|
}
|
|
1685
1691
|
async function prepareDelivery(client, attemptId, taskId, report) {
|
|
1686
|
-
const fuelMode = client.attempt(taskId).fuelMode
|
|
1692
|
+
const fuelMode = client.attempt(taskId).fuelMode;
|
|
1687
1693
|
const evidence = [];
|
|
1688
1694
|
const evidenceByCriterion = new Map();
|
|
1689
1695
|
for (const [index, item] of report.evidence.entries()) {
|
|
@@ -1717,7 +1723,7 @@ async function prepareDelivery(client, attemptId, taskId, report) {
|
|
|
1717
1723
|
return {
|
|
1718
1724
|
action: "complete",
|
|
1719
1725
|
body: {
|
|
1720
|
-
delivery_packet: { ...body, acceptance_results: acceptanceResults, ...(head_commit ? { head_commit } : {}), ...(pull_request_url ? { pull_request_url } : {}), references, fuel_mode: fuelMode },
|
|
1726
|
+
delivery_packet: { ...body, acceptance_results: acceptanceResults, ...(head_commit ? { head_commit } : {}), ...(pull_request_url ? { pull_request_url } : {}), references, ...(fuelMode ? { fuel_mode: fuelMode } : {}) },
|
|
1721
1727
|
evidence,
|
|
1722
1728
|
idempotency_key: `bridge:complete:${attemptId}`,
|
|
1723
1729
|
},
|
package/dist/on-shift-apply.js
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* After a successful restart, currentWorkspace matches the target so we do not re-apply.
|
|
2
|
+
* When Connect queues a workspace intent, Bridge applies it without rewriting the
|
|
3
|
+
* machine-wide ops.env. The rebound runner's matching heartbeat clears the intent;
|
|
4
|
+
* installation alone is not proof that the new process is actually bound.
|
|
6
5
|
*/
|
|
7
6
|
import { resolve } from "node:path";
|
|
8
|
-
import {
|
|
9
|
-
import { defaultOpsEnvPath, expandOpsValue, loadOpsEnv, writeOpsEnvFile } from "./ops.js";
|
|
7
|
+
import { expandOpsValue } from "./ops.js";
|
|
10
8
|
function pathsEqual(left, right) {
|
|
11
9
|
return resolve(expandOpsValue(left)) === resolve(expandOpsValue(right));
|
|
12
10
|
}
|
|
11
|
+
/** Prove a historical path alias only after this Bridge resolves it to its current workspace. */
|
|
12
|
+
export function buildOnShiftIntentProof(input) {
|
|
13
|
+
const intent = input.onShift?.on_shift_intent;
|
|
14
|
+
if (input.onShift?.binding_status !== "bound"
|
|
15
|
+
|| !input.currentWorkspace
|
|
16
|
+
|| !intent?.workspace_path?.trim()
|
|
17
|
+
|| !intent.intent_at)
|
|
18
|
+
return null;
|
|
19
|
+
const current = resolve(expandOpsValue(input.currentWorkspace));
|
|
20
|
+
if (!pathsEqual(current, intent.workspace_path))
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
workspace_path: intent.workspace_path,
|
|
24
|
+
resolved_workspace_path: current,
|
|
25
|
+
repository_url: intent.repository_url,
|
|
26
|
+
intent_at: intent.intent_at,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
13
29
|
/**
|
|
14
30
|
* If control-plane intent names a different workspace than this runner, apply and return true
|
|
15
31
|
* (caller should exit so install-service / LaunchAgent restarts).
|
|
@@ -20,36 +36,17 @@ export async function maybeApplyOnShiftIntent(input) {
|
|
|
20
36
|
return false;
|
|
21
37
|
const target = resolve(expandOpsValue(intentPath));
|
|
22
38
|
const current = input.currentWorkspace ? resolve(expandOpsValue(input.currentWorkspace)) : null;
|
|
23
|
-
if (current && pathsEqual(current, target)) {
|
|
24
|
-
// Already on the declared path — leave intent for Bound clear on heartbeat.
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
39
|
const repo = (input.onShift?.on_shift_intent?.repository_url
|
|
28
40
|
?? input.onShift?.expected_repository
|
|
29
41
|
?? "").trim();
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
envPath = loadOpsEnv().loadedFrom ?? envPath;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
// First apply may create ops.env at the default path.
|
|
40
|
-
}
|
|
41
|
-
writeOpsEnvFile(envPath, {
|
|
42
|
-
CONDUIT_WORKSPACE: target,
|
|
43
|
-
...(repo ? { CONDUIT_REPO: repo } : {}),
|
|
44
|
-
});
|
|
42
|
+
if (current && pathsEqual(current, target)) {
|
|
43
|
+
// A matching path is sufficient only when there is no repository target, or when the
|
|
44
|
+
// control plane already observed the target fingerprint. Same path + different repo must
|
|
45
|
+
// still run checkout/install; path-only equality caused stale-project loops.
|
|
46
|
+
if (!repo || input.onShift?.binding_status === "bound")
|
|
47
|
+
return false;
|
|
45
48
|
}
|
|
46
49
|
console.log(`Applying on-shift workspace intent: ${target}${repo ? ` (${repo})` : ""}`);
|
|
47
50
|
await input.runInstall(target, repo);
|
|
48
|
-
// Ack only after install succeeds. A rebound service that already matches the target
|
|
49
|
-
// short-circuits above and will not re-apply; Bound clears any leftover intent.
|
|
50
|
-
await input.client.request("/runner/v1/machine/ack-on-shift-intent", {
|
|
51
|
-
method: "POST",
|
|
52
|
-
body: "{}",
|
|
53
|
-
});
|
|
54
51
|
return true;
|
|
55
52
|
}
|
package/dist/ops.js
CHANGED
|
@@ -13,7 +13,7 @@ import { ensureCheckout } from "./checkout.js";
|
|
|
13
13
|
import { detectInstalledClients, probeAgentHealth } from "./detect.js";
|
|
14
14
|
import { driverIdsFromDetectedLabels } from "./drivers.js";
|
|
15
15
|
import { BRIDGE_PROTOCOL_VERSION, describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
16
|
-
import { applyRunnerToolPath, runnerServiceWorkspaceWarnings } from "./service.js";
|
|
16
|
+
import { applyRunnerToolPath, readActiveRunnerServiceWorkspace, readStoredRunnerServiceOptions, runnerServiceWorkspaceWarnings } from "./service.js";
|
|
17
17
|
import { bridgeVersion } from "./version.js";
|
|
18
18
|
import { bootstrapManagedWorkspace } from "./workspace-bootstrap.js";
|
|
19
19
|
export const OPS_VERBS = [
|
|
@@ -88,7 +88,10 @@ export function loadOpsEnv(home = homedir(), cwd = process.cwd()) {
|
|
|
88
88
|
}
|
|
89
89
|
const pick = (key, fallback) => {
|
|
90
90
|
const fromEnv = process.env[key];
|
|
91
|
-
|
|
91
|
+
// An explicit empty value clears a stale file default (notably CONDUIT_REPO when a new
|
|
92
|
+
// project has no declared repository yet). Process authority must not resurrect old project
|
|
93
|
+
// context from ops.env.
|
|
94
|
+
if (fromEnv !== undefined)
|
|
92
95
|
return expandOpsValue(fromEnv, home);
|
|
93
96
|
if (fileValues[key])
|
|
94
97
|
return fileValues[key];
|
|
@@ -236,37 +239,49 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
236
239
|
if (env.loadedFrom) {
|
|
237
240
|
console.log(`Env: ${env.loadedFrom}`);
|
|
238
241
|
console.log(`URL: ${env.CONDUIT_URL || "(unset)"}`);
|
|
239
|
-
console.log(`
|
|
240
|
-
console.log(`
|
|
242
|
+
console.log(`Bootstrap workspace: ${env.CONDUIT_WORKSPACE || "(unset)"}`);
|
|
243
|
+
console.log(`Bootstrap repo: ${env.CONDUIT_REPO || "(none)"}`);
|
|
241
244
|
console.log(`Drivers: ${env.CONDUIT_DRIVERS || "(auto-detect installed agents)"}`);
|
|
242
245
|
console.log(`Roles: ${env.CONDUIT_ROLES}`);
|
|
243
|
-
if (env.CONDUIT_WORKSPACE) {
|
|
244
|
-
for (const warning of runnerServiceWorkspaceWarnings(resolve(expandOpsValue(env.CONDUIT_WORKSPACE)))) {
|
|
245
|
-
console.warn(`WARNING: ${warning}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
246
|
console.log("");
|
|
249
247
|
}
|
|
250
248
|
else {
|
|
251
249
|
console.log("No ops.env loaded (showing drivers only).\n");
|
|
252
250
|
}
|
|
251
|
+
console.log(`Active runner workspace: ${await (deps.readActiveRunnerServiceWorkspace ?? readActiveRunnerServiceWorkspace)() ?? "(not installed)"}`);
|
|
253
252
|
runBridge(["drivers"]);
|
|
254
253
|
return;
|
|
255
254
|
}
|
|
256
255
|
if (verb === "doctor") {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
const { values } = parseArgs({
|
|
257
|
+
args: argv,
|
|
258
|
+
options: {
|
|
259
|
+
workspace: { type: "string" },
|
|
260
|
+
repo: { type: "string" },
|
|
261
|
+
"skip-service-alignment": { type: "boolean" },
|
|
262
|
+
},
|
|
263
|
+
allowPositionals: false,
|
|
264
|
+
});
|
|
265
|
+
const stored = await (deps.readStoredRunnerServiceOptions ?? readStoredRunnerServiceOptions)();
|
|
266
|
+
const workspaceRaw = values.workspace?.trim() || stored?.workspace || env.CONDUIT_WORKSPACE;
|
|
267
|
+
const expectedRepository = values.repo !== undefined
|
|
268
|
+
? values.repo.trim()
|
|
269
|
+
: stored?.ensureCheckout ?? env.CONDUIT_REPO;
|
|
270
|
+
requireOpsEnv({ ...env, CONDUIT_WORKSPACE: workspaceRaw });
|
|
271
|
+
if (!workspaceRaw)
|
|
272
|
+
throw new Error(`Install a runner service or set a bootstrap CONDUIT_WORKSPACE in ${defaultOpsEnvPath()}`);
|
|
260
273
|
applyRunnerToolPath();
|
|
261
274
|
const config = await (deps.loadBridgeConfig ?? loadConfig)();
|
|
262
|
-
const workspace = resolve(expandOpsValue(
|
|
263
|
-
|
|
264
|
-
|
|
275
|
+
const workspace = resolve(expandOpsValue(workspaceRaw));
|
|
276
|
+
if (!values["skip-service-alignment"]) {
|
|
277
|
+
for (const warning of runnerServiceWorkspaceWarnings(workspace)) {
|
|
278
|
+
console.warn(`WARNING: ${warning}`);
|
|
279
|
+
}
|
|
265
280
|
}
|
|
266
281
|
const report = await (deps.preflight ?? runBridgePreflight)({
|
|
267
282
|
config,
|
|
268
283
|
workspace,
|
|
269
|
-
expectedRepository:
|
|
284
|
+
expectedRepository: expectedRepository || undefined,
|
|
270
285
|
});
|
|
271
286
|
const installed = await detectInstalledClients();
|
|
272
287
|
const health = probeAgentHealth(installed, process.env);
|
|
@@ -287,8 +302,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
287
302
|
console.log("");
|
|
288
303
|
}
|
|
289
304
|
if (report.ready) {
|
|
290
|
-
const serviceWarnings = runnerServiceWorkspaceWarnings(workspace);
|
|
291
|
-
if (serviceWarnings.length) {
|
|
305
|
+
const serviceWarnings = values["skip-service-alignment"] ? [] : runnerServiceWorkspaceWarnings(workspace);
|
|
306
|
+
if (serviceWarnings.length > 0) {
|
|
292
307
|
for (const warning of serviceWarnings)
|
|
293
308
|
console.warn(`WARNING: ${warning}`);
|
|
294
309
|
throw new Error("Background runner service workspace does not match ops.env");
|
|
@@ -306,8 +321,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
306
321
|
runBridge(["drivers", verb, ...drivers]);
|
|
307
322
|
return;
|
|
308
323
|
}
|
|
309
|
-
//
|
|
310
|
-
//
|
|
324
|
+
// Explicit install arguments are active-run inputs, not machine-wide defaults. Enrollment may
|
|
325
|
+
// seed ops.env once; later project changes must not mutate it.
|
|
311
326
|
let installEnv = env;
|
|
312
327
|
let installArgv = argv;
|
|
313
328
|
if (verb === "install") {
|
|
@@ -323,20 +338,14 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
323
338
|
const declaredWorkspace = values.workspace?.trim();
|
|
324
339
|
const declaredRepo = values.repo?.trim();
|
|
325
340
|
if (declaredWorkspace || declaredRepo) {
|
|
326
|
-
const envPath = env.loadedFrom ?? defaultOpsEnvPath();
|
|
327
|
-
writeOpsEnvFile(envPath, {
|
|
328
|
-
...(declaredWorkspace ? { CONDUIT_WORKSPACE: declaredWorkspace } : {}),
|
|
329
|
-
...(declaredRepo ? { CONDUIT_REPO: declaredRepo } : {}),
|
|
330
|
-
});
|
|
331
|
-
console.log(`Saved ${[
|
|
332
|
-
declaredWorkspace ? `workspace ${declaredWorkspace}` : "",
|
|
333
|
-
declaredRepo ? `repo ${declaredRepo}` : "",
|
|
334
|
-
].filter(Boolean).join(" and ")} to ${envPath}`);
|
|
335
341
|
installEnv = {
|
|
336
342
|
...env,
|
|
337
|
-
...(declaredWorkspace ? {
|
|
343
|
+
...(declaredWorkspace ? {
|
|
344
|
+
CONDUIT_WORKSPACE: declaredWorkspace,
|
|
345
|
+
// A new explicit workspace must not inherit the prior bootstrap repository.
|
|
346
|
+
CONDUIT_REPO: declaredRepo ?? "",
|
|
347
|
+
} : {}),
|
|
338
348
|
...(declaredRepo ? { CONDUIT_REPO: declaredRepo } : {}),
|
|
339
|
-
loadedFrom: env.loadedFrom ?? envPath,
|
|
340
349
|
};
|
|
341
350
|
}
|
|
342
351
|
}
|
|
@@ -355,13 +364,26 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
355
364
|
if (!projectId) {
|
|
356
365
|
throw new Error("Usage: ops switch --project <project-id> [--workspace <path>] [--repo <url>]");
|
|
357
366
|
}
|
|
358
|
-
const
|
|
367
|
+
const declaredWorkspace = values.workspace?.trim();
|
|
368
|
+
const declaredRepo = values.repo?.trim();
|
|
369
|
+
const [activeWorkspace, stored] = await Promise.all([
|
|
370
|
+
(deps.readActiveRunnerServiceWorkspace ?? readActiveRunnerServiceWorkspace)(),
|
|
371
|
+
(deps.readStoredRunnerServiceOptions ?? readStoredRunnerServiceOptions)(),
|
|
372
|
+
]);
|
|
373
|
+
const workspaceRaw = declaredWorkspace || activeWorkspace || stored?.workspace || env.CONDUIT_WORKSPACE;
|
|
359
374
|
if (!workspaceRaw)
|
|
360
375
|
throw new Error(`Set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()} or pass --workspace`);
|
|
361
376
|
const workspace = resolve(expandOpsValue(workspaceRaw));
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
377
|
+
const storedWorkspace = stored?.workspace ? resolve(expandOpsValue(stored.workspace)) : null;
|
|
378
|
+
const repo = (declaredRepo ?? (declaredWorkspace
|
|
379
|
+
? ""
|
|
380
|
+
: storedWorkspace === workspace
|
|
381
|
+
? stored?.ensureCheckout ?? ""
|
|
382
|
+
: activeWorkspace || stored?.workspace
|
|
383
|
+
? ""
|
|
384
|
+
: env.CONDUIT_REPO)).trim();
|
|
385
|
+
// Control plane first. Its intent is the target authority; the installed runner command is
|
|
386
|
+
// the local runtime authority. ops.env remains enrollment/bootstrap input only.
|
|
365
387
|
const config = await (deps.loadBridgeConfig ?? loadConfig)();
|
|
366
388
|
const client = new ConduitClient(config);
|
|
367
389
|
const result = await client.request("/runner/v1/machine/switch-project", {
|
|
@@ -373,29 +395,17 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
373
395
|
}),
|
|
374
396
|
});
|
|
375
397
|
console.log(`Control plane on-shift: ${String(result.project_id ?? projectId)} (${String(result.binding_status ?? "pending")})`);
|
|
376
|
-
writeOpsEnvFile(envPath, {
|
|
377
|
-
CONDUIT_WORKSPACE: workspace,
|
|
378
|
-
...(repo ? { CONDUIT_REPO: repo } : {}),
|
|
379
|
-
});
|
|
380
|
-
console.log(`Updated ${envPath}`);
|
|
381
398
|
console.log(`On-shift workspace: ${workspace}${repo ? ` (${repo})` : ""}`);
|
|
382
399
|
// Same install path as ops install — restart runner against the new checkout.
|
|
383
|
-
const switched = loadOpsEnv();
|
|
384
400
|
await runOps("install", [], {
|
|
385
401
|
...deps,
|
|
386
402
|
env: {
|
|
387
|
-
...
|
|
403
|
+
...env,
|
|
388
404
|
CONDUIT_WORKSPACE: workspace,
|
|
389
|
-
CONDUIT_REPO: repo
|
|
405
|
+
CONDUIT_REPO: repo,
|
|
390
406
|
},
|
|
391
407
|
});
|
|
392
|
-
//
|
|
393
|
-
try {
|
|
394
|
-
await client.request("/runner/v1/machine/ack-on-shift-intent", { method: "POST", body: "{}" });
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
// Older control planes omit ack — local ops.env still wins.
|
|
398
|
-
}
|
|
408
|
+
// The target runner heartbeat clears the queued intent only after it proves Bound.
|
|
399
409
|
return;
|
|
400
410
|
}
|
|
401
411
|
if (verb === "connect") {
|
|
@@ -466,29 +476,32 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
466
476
|
* intent and Bridge clones into <root>/<owner>/<repo> and restarts on it.
|
|
467
477
|
*/
|
|
468
478
|
const managed = values.managed !== undefined;
|
|
469
|
-
const managedRoot = managed ? (values.managed?.trim() || DEFAULT_MANAGED_ROOT) : "";
|
|
470
|
-
const
|
|
471
|
-
const
|
|
479
|
+
const managedRoot = managed ? resolve(expandOpsValue(values.managed?.trim() || DEFAULT_MANAGED_ROOT)) : "";
|
|
480
|
+
const declaredWorkspace = values.workspace?.trim();
|
|
481
|
+
const declaredRepo = values.repo?.trim();
|
|
482
|
+
const workspace = managed ? managedRoot : (declaredWorkspace || env.CONDUIT_WORKSPACE);
|
|
483
|
+
const repo = managed ? "" : (declaredRepo ?? (declaredWorkspace ? "" : env.CONDUIT_REPO));
|
|
472
484
|
const machine = values.machine?.trim();
|
|
473
485
|
const envPath = env.loadedFrom ?? defaultOpsEnvPath();
|
|
474
486
|
writeOpsEnvFile(envPath, {
|
|
475
487
|
CONDUIT_URL: url,
|
|
476
488
|
...(workspace ? { CONDUIT_WORKSPACE: workspace } : {}),
|
|
477
|
-
|
|
478
|
-
|
|
489
|
+
CONDUIT_REPO: repo,
|
|
490
|
+
CONDUIT_MANAGED_ROOT: managedRoot,
|
|
479
491
|
});
|
|
492
|
+
const enrollmentEnv = {
|
|
493
|
+
...env,
|
|
494
|
+
CONDUIT_URL: url,
|
|
495
|
+
CONDUIT_WORKSPACE: workspace,
|
|
496
|
+
CONDUIT_REPO: repo,
|
|
497
|
+
CONDUIT_MANAGED_ROOT: managedRoot,
|
|
498
|
+
loadedFrom: envPath,
|
|
499
|
+
};
|
|
480
500
|
// Resolve the local lane before redeeming the single-use token. A missing coding agent is a
|
|
481
501
|
// local prerequisite, not an enrollment failure, and must leave the token available to retry.
|
|
482
502
|
// `--no-install` remains the explicit credential-only escape hatch.
|
|
483
503
|
const installDrivers = !values["no-install"] && workspace
|
|
484
|
-
? await (deps.resolveInstallDrivers ?? resolveInstallDrivers)(
|
|
485
|
-
...env,
|
|
486
|
-
CONDUIT_URL: url,
|
|
487
|
-
CONDUIT_WORKSPACE: workspace,
|
|
488
|
-
CONDUIT_REPO: repo,
|
|
489
|
-
CONDUIT_MANAGED_ROOT: managedRoot,
|
|
490
|
-
loadedFrom: envPath,
|
|
491
|
-
}, [])
|
|
504
|
+
? await (deps.resolveInstallDrivers ?? resolveInstallDrivers)(enrollmentEnv, [])
|
|
492
505
|
: [];
|
|
493
506
|
if (managed)
|
|
494
507
|
mkdirSync(resolve(expandOpsValue(managedRoot)), { recursive: true });
|
|
@@ -519,8 +532,7 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
519
532
|
return;
|
|
520
533
|
}
|
|
521
534
|
if (workspace) {
|
|
522
|
-
|
|
523
|
-
await runOps("install", installDrivers, { ...deps, env: refreshedEnv });
|
|
535
|
+
await runOps("install", installDrivers, { ...deps, env: enrollmentEnv });
|
|
524
536
|
}
|
|
525
537
|
return;
|
|
526
538
|
}
|
|
@@ -566,7 +578,12 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
566
578
|
&& !installEnv.CONDUIT_REPO);
|
|
567
579
|
if (!waitingOnManagedAssignment) {
|
|
568
580
|
// Prove the exact local environment before installing a service that advertises availability.
|
|
569
|
-
runBridge([
|
|
581
|
+
runBridge([
|
|
582
|
+
"ops", "doctor",
|
|
583
|
+
"--workspace", workspace,
|
|
584
|
+
"--repo", installEnv.CONDUIT_REPO,
|
|
585
|
+
"--skip-service-alignment",
|
|
586
|
+
]);
|
|
570
587
|
}
|
|
571
588
|
const installArgs = ["install-service", "--workspace", workspace];
|
|
572
589
|
if (installEnv.CONDUIT_REPO)
|
package/dist/preflight.js
CHANGED
|
@@ -16,9 +16,9 @@ function modelsFingerprint(config) {
|
|
|
16
16
|
}).join(";");
|
|
17
17
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
18
18
|
}
|
|
19
|
-
// Protocol
|
|
20
|
-
//
|
|
21
|
-
export const BRIDGE_PROTOCOL_VERSION =
|
|
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;
|
|
22
22
|
const execFileAsync = promisify(execFile);
|
|
23
23
|
async function defaultCommandRunner(command, args, cwd) {
|
|
24
24
|
try {
|
|
@@ -181,7 +181,7 @@ export function describePreflightIssue(issue) {
|
|
|
181
181
|
// Named before the fallback: an exhausted lane fell through to "requires local fuel", which sends
|
|
182
182
|
// the operator to change a fuel setting when the lane is configured correctly and simply spent.
|
|
183
183
|
if (issue.code === "driver_quota_exhausted") {
|
|
184
|
-
return "Every online
|
|
184
|
+
return "Every online local-fuel lane is unavailable; work resumes when a reported provider allowance recovers, or bring another lane online";
|
|
185
185
|
}
|
|
186
186
|
return `Agent lane requires local fuel${lane}`;
|
|
187
187
|
}
|
package/dist/service.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir, platform } from "node:os";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
export const SERVICE_LABEL = "io.miraland.conduit-runner";
|
|
@@ -95,7 +95,7 @@ ${argsXml}
|
|
|
95
95
|
`;
|
|
96
96
|
}
|
|
97
97
|
export function systemdUserUnit(programArguments, workingDirectory, pathEnv = runnerServicePath()) {
|
|
98
|
-
const execStart = programArguments.map(
|
|
98
|
+
const execStart = programArguments.map(escapeSystemdExecArgument).join(" ");
|
|
99
99
|
return `[Unit]
|
|
100
100
|
Description=Conduit Bridge runner
|
|
101
101
|
After=network-online.target
|
|
@@ -103,8 +103,9 @@ Wants=network-online.target
|
|
|
103
103
|
|
|
104
104
|
[Service]
|
|
105
105
|
Type=simple
|
|
106
|
-
WorkingDirectory=${workingDirectory}
|
|
107
|
-
Environment=PATH=${
|
|
106
|
+
WorkingDirectory=${escapeSystemdUnitValue(workingDirectory)}
|
|
107
|
+
Environment=PATH=${escapeSystemdUnitValue(pathEnv)}
|
|
108
|
+
Environment=CONDUIT_RUNNER_SERVICE=systemd
|
|
108
109
|
ExecStart=${execStart}
|
|
109
110
|
Restart=always
|
|
110
111
|
RestartSec=5
|
|
@@ -116,10 +117,16 @@ WantedBy=default.target
|
|
|
116
117
|
function escapeXml(value) {
|
|
117
118
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
118
119
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
function escapeSystemdUnitValue(value) {
|
|
121
|
+
// Unit values expand % specifiers. Environment= and WorkingDirectory= keep $ literal.
|
|
122
|
+
const literal = value.replaceAll("%", "%%");
|
|
123
|
+
if (/[\s"'\\]/.test(literal))
|
|
124
|
+
return `"${literal.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
125
|
+
return literal;
|
|
126
|
+
}
|
|
127
|
+
function escapeSystemdExecArgument(value) {
|
|
128
|
+
// Exec*= additionally expands $ environment references; $$ passes one literal dollar.
|
|
129
|
+
return escapeSystemdUnitValue(value.replaceAll("$", () => "$$"));
|
|
123
130
|
}
|
|
124
131
|
function launchAgentsPath() {
|
|
125
132
|
return join(homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
@@ -137,52 +144,53 @@ export function parseRunnerWorkspaceFromProgramArguments(args) {
|
|
|
137
144
|
}
|
|
138
145
|
export function parseRunnerWorkspaceFromExecStart(execStart) {
|
|
139
146
|
const match = /--workspace(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s]+))/.exec(execStart);
|
|
140
|
-
|
|
147
|
+
const value = match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
|
|
148
|
+
return value?.replaceAll("%%", "%").replaceAll("$$", "$") ?? null;
|
|
141
149
|
}
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
/** Parse the effective drop-ins reported by systemd without reading their contents. */
|
|
151
|
+
export function parseSystemdDropInPaths(value) {
|
|
152
|
+
return value.trim().split(/\s+/).filter((path) => path.endsWith(".conf"));
|
|
144
153
|
}
|
|
145
|
-
/**
|
|
146
|
-
export async function
|
|
154
|
+
/** Any effective drop-in can change workspace, identity, fuel, PATH, or restart behaviour. */
|
|
155
|
+
export async function systemdDropInConflicts(home = homedir()) {
|
|
156
|
+
const conflicts = new Set();
|
|
147
157
|
const dir = systemdDropInDir(home);
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (!name.endsWith(".conf"))
|
|
153
|
-
continue;
|
|
154
|
-
const path = join(dir, name);
|
|
155
|
-
const content = await readFile(path, "utf8");
|
|
156
|
-
if (!dropInOverridesExecStart(content))
|
|
157
|
-
continue;
|
|
158
|
-
const kept = content.split(/\r?\n/).filter((line) => {
|
|
159
|
-
const trimmed = line.trim();
|
|
160
|
-
return trimmed && !trimmed.startsWith("#") && !/^ExecStart\s*=/.test(trimmed);
|
|
161
|
-
});
|
|
162
|
-
if (kept.length === 0) {
|
|
163
|
-
await unlink(path);
|
|
158
|
+
if (existsSync(dir)) {
|
|
159
|
+
for (const name of await readdir(dir)) {
|
|
160
|
+
if (name.endsWith(".conf"))
|
|
161
|
+
conflicts.add(name);
|
|
164
162
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
}
|
|
164
|
+
if (platform() === "linux") {
|
|
165
|
+
const effective = spawnSync("systemctl", ["--user", "show", "conduit-runner.service", "--property=DropInPaths", "--value"], { encoding: "utf8" });
|
|
166
|
+
if (effective.status === 0) {
|
|
167
|
+
for (const path of parseSystemdDropInPaths(effective.stdout))
|
|
168
|
+
conflicts.add(basename(path));
|
|
170
169
|
}
|
|
171
|
-
removed.push(name);
|
|
172
170
|
}
|
|
173
|
-
return
|
|
171
|
+
return [...conflicts].sort();
|
|
174
172
|
}
|
|
175
|
-
export async function
|
|
173
|
+
export async function readStoredRunnerServiceOptions() {
|
|
176
174
|
try {
|
|
177
175
|
const stored = JSON.parse(await readFile(serviceStatePath(), "utf8"));
|
|
178
|
-
return
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
return {
|
|
177
|
+
...stored.options,
|
|
178
|
+
workspace: stored.options.workspace
|
|
179
|
+
?? parseRunnerWorkspaceFromProgramArguments(stored.programArguments)
|
|
180
|
+
?? undefined,
|
|
181
|
+
};
|
|
181
182
|
}
|
|
182
183
|
catch {
|
|
183
184
|
return null;
|
|
184
185
|
}
|
|
185
186
|
}
|
|
187
|
+
export async function readStoredRunnerServiceWorkspace() {
|
|
188
|
+
return (await readStoredRunnerServiceOptions())?.workspace ?? null;
|
|
189
|
+
}
|
|
190
|
+
/** Installed supervisor command is the active workspace authority; service.json is fallback. */
|
|
191
|
+
export async function readActiveRunnerServiceWorkspace(home = homedir()) {
|
|
192
|
+
return readEffectiveLinuxRunnerWorkspace(home) ?? await readStoredRunnerServiceWorkspace();
|
|
193
|
+
}
|
|
186
194
|
/** Effective workspace from the installed supervisor (systemd user unit on Linux). */
|
|
187
195
|
export function readEffectiveLinuxRunnerWorkspace(home = homedir()) {
|
|
188
196
|
if (platform() !== "linux")
|
|
@@ -264,6 +272,21 @@ function sleepMs(ms) {
|
|
|
264
272
|
export function runningInsideRunnerLaunchAgent() {
|
|
265
273
|
return process.env.XPC_SERVICE_NAME === SERVICE_LABEL;
|
|
266
274
|
}
|
|
275
|
+
/** True when install-service was invoked by the systemd runner it is replacing. */
|
|
276
|
+
export function runningInsideRunnerSystemd(env = process.env) {
|
|
277
|
+
return env.CONDUIT_RUNNER_SERVICE === "systemd";
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Reloading a unit does not replace an already-running ExecStart. Interactive installs restart
|
|
281
|
+
* explicitly; the resident runner must return to its caller and exit normally, after which
|
|
282
|
+
* Restart=always starts the rewritten unit.
|
|
283
|
+
*/
|
|
284
|
+
export function systemdActivationCommands(insideRunner) {
|
|
285
|
+
return [
|
|
286
|
+
["--user", "enable", "conduit-runner.service"],
|
|
287
|
+
...(!insideRunner ? [["--user", "restart", "conduit-runner.service"]] : []),
|
|
288
|
+
];
|
|
289
|
+
}
|
|
267
290
|
/**
|
|
268
291
|
* Boot out (ignore miss), bootstrap with retries, then enable + kickstart.
|
|
269
292
|
* Returns whether `launchctl print` sees the service.
|
|
@@ -336,16 +359,18 @@ export async function installRunnerService(options = {}) {
|
|
|
336
359
|
return { path: plistPath, platform: "darwin" };
|
|
337
360
|
}
|
|
338
361
|
const unitPath = systemdUnitPath();
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
"
|
|
362
|
+
const dropIns = await systemdDropInConflicts();
|
|
363
|
+
if (dropIns.length) {
|
|
364
|
+
throw new Error(`Refusing to install with conduit-runner.service drop-in(s): ${dropIns.join(", ")}. ` +
|
|
365
|
+
"Review and migrate or remove them, then run ops install again.");
|
|
343
366
|
}
|
|
344
367
|
await mkdir(dirname(unitPath), { recursive: true });
|
|
345
368
|
await mkdir(linuxLogDir(), { recursive: true });
|
|
346
369
|
await writeFile(unitPath, systemdUserUnit(programArguments, configDir()), { mode: 0o644 });
|
|
347
370
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
348
|
-
|
|
371
|
+
for (const args of systemdActivationCommands(runningInsideRunnerSystemd())) {
|
|
372
|
+
run("systemctl", args);
|
|
373
|
+
}
|
|
349
374
|
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "linux", programArguments, options, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
350
375
|
return { path: unitPath, platform: "linux" };
|
|
351
376
|
}
|
package/ops/env.example
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Enrollment/bootstrap defaults. Copy to ~/.config/conduit/ops.env and edit once.
|
|
2
|
+
# Project switches come from Conduit and the installed runner command; do not edit this file per project.
|
|
2
3
|
# Same file works on macOS, Linux, and Windows ($HOME / %USERPROFILE% / ~ are expanded).
|
|
3
4
|
#
|
|
4
5
|
# Cross-platform commands (any OS):
|
|
@@ -6,13 +7,14 @@
|
|
|
6
7
|
# npx @miraland-labs/conduit-bridge@latest ops install
|
|
7
8
|
# Local shortcuts after init-ops: ~/conduit/ops/*.sh (Mac/Linux) or *.cmd (Windows)
|
|
8
9
|
#
|
|
9
|
-
# Replace the placeholders below with
|
|
10
|
+
# Replace the placeholders below with initial connection defaults.
|
|
10
11
|
CONDUIT_URL=https://api.conduit.miraland.io
|
|
11
12
|
# Leave CONDUIT_ORG unset so `ops connect` asks "Join which org?".
|
|
12
13
|
# Or pass once: ops connect --organization your-org-slug
|
|
13
14
|
# Prefer the Connect UI join command (includes the org you are signed into).
|
|
14
15
|
# CONDUIT_ORG=your-org-slug
|
|
15
|
-
|
|
16
|
+
# Optional bootstrap checkout for the first install. Later switches do not rewrite these values.
|
|
17
|
+
# CONDUIT_WORKSPACE=$HOME/path/to/your-repo
|
|
16
18
|
# CONDUIT_REPO=https://github.com/your-org/your-repo.git
|
|
17
19
|
# Leave CONDUIT_DRIVERS unset to auto-detect the coding agents installed on this computer.
|
|
18
20
|
# Pin explicitly only when you want a subset, e.g.:
|
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.19",
|
|
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": {
|