@miraland-labs/conduit-bridge 0.16.18 → 0.16.20
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 +17 -12
- 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 +113 -68
- package/dist/preflight.js +4 -4
- package/dist/service.js +70 -45
- package/ops/conduit-ops.cmd +1 -1
- package/ops/connect.cmd +1 -1
- package/ops/disconnect.cmd +1 -1
- package/ops/doctor.cmd +1 -1
- package/ops/env.example +5 -3
- package/ops/install.cmd +1 -1
- package/ops/offline.cmd +1 -1
- package/ops/online.cmd +1 -1
- package/ops/status.cmd +1 -1
- package/ops/uninstall.cmd +1 -1
- 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.20` — `ops disconnect` now asks before it removes the runner service and clears credentials; pass `--yes` to skip the question. `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) {
|
|
@@ -371,9 +371,9 @@ async function initOps() {
|
|
|
371
371
|
console.log(`Config file (separate): ${envPath}`);
|
|
372
372
|
console.log("");
|
|
373
373
|
console.log("Works the same on macOS, Linux, and Windows via:");
|
|
374
|
-
console.log(` ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|disconnect|uninstall>")}`);
|
|
374
|
+
console.log(` ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|doctor|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
|
@@ -6,6 +6,8 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
9
11
|
import { parseArgs } from "node:util";
|
|
10
12
|
import { ConduitClient } from "./client.js";
|
|
11
13
|
import { loadConfig } from "./config.js";
|
|
@@ -13,7 +15,7 @@ import { ensureCheckout } from "./checkout.js";
|
|
|
13
15
|
import { detectInstalledClients, probeAgentHealth } from "./detect.js";
|
|
14
16
|
import { driverIdsFromDetectedLabels } from "./drivers.js";
|
|
15
17
|
import { BRIDGE_PROTOCOL_VERSION, describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
16
|
-
import { applyRunnerToolPath, runnerServiceWorkspaceWarnings } from "./service.js";
|
|
18
|
+
import { applyRunnerToolPath, readActiveRunnerServiceWorkspace, readStoredRunnerServiceOptions, runnerServiceWorkspaceWarnings } from "./service.js";
|
|
17
19
|
import { bridgeVersion } from "./version.js";
|
|
18
20
|
import { bootstrapManagedWorkspace } from "./workspace-bootstrap.js";
|
|
19
21
|
export const OPS_VERBS = [
|
|
@@ -51,7 +53,7 @@ export function expandOpsValue(value, home = homedir()) {
|
|
|
51
53
|
}
|
|
52
54
|
next = next.replace(/%USERPROFILE%/gi, home).replace(/%HOME%/gi, home);
|
|
53
55
|
next = next.replace(/\$HOME\b/g, home).replace(/\$\{HOME\}/g, home);
|
|
54
|
-
if (next.startsWith("~/") || next === "~")
|
|
56
|
+
if (next.startsWith("~/") || next.startsWith("~\\") || next === "~")
|
|
55
57
|
next = join(home, next.slice(2));
|
|
56
58
|
return next;
|
|
57
59
|
}
|
|
@@ -67,7 +69,12 @@ export function parseOpsEnvFile(text, home = homedir()) {
|
|
|
67
69
|
const key = line.slice(0, eq).trim();
|
|
68
70
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
69
71
|
continue;
|
|
70
|
-
|
|
72
|
+
// Drop a trailing annotation such as `CONDUIT_WORKSPACE=$HOME/repo # laptop`, so it does not
|
|
73
|
+
// become part of the path. A quoted value keeps everything inside its quotes, and a `#` with no
|
|
74
|
+
// space before it stays in the value.
|
|
75
|
+
const rawValue = line.slice(eq + 1).trim();
|
|
76
|
+
const quoted = rawValue.startsWith('"') || rawValue.startsWith("'");
|
|
77
|
+
out[key] = expandOpsValue(quoted ? rawValue : rawValue.replace(/\s+#.*$/, ""), home);
|
|
71
78
|
}
|
|
72
79
|
return out;
|
|
73
80
|
}
|
|
@@ -88,7 +95,10 @@ export function loadOpsEnv(home = homedir(), cwd = process.cwd()) {
|
|
|
88
95
|
}
|
|
89
96
|
const pick = (key, fallback) => {
|
|
90
97
|
const fromEnv = process.env[key];
|
|
91
|
-
|
|
98
|
+
// An explicit empty value clears a stale file default (notably CONDUIT_REPO when a new
|
|
99
|
+
// project has no declared repository yet). Process authority must not resurrect old project
|
|
100
|
+
// context from ops.env.
|
|
101
|
+
if (fromEnv !== undefined)
|
|
92
102
|
return expandOpsValue(fromEnv, home);
|
|
93
103
|
if (fileValues[key])
|
|
94
104
|
return fileValues[key];
|
|
@@ -182,6 +192,20 @@ export function shellQuoteArgs(args) {
|
|
|
182
192
|
return `"${arg.replaceAll('"', '\\"')}"`;
|
|
183
193
|
}).join(" ");
|
|
184
194
|
}
|
|
195
|
+
/** Ask the operator to approve a step that cannot be undone. Non-interactive shells must pass --yes. */
|
|
196
|
+
async function confirmOps(question) {
|
|
197
|
+
if (!input.isTTY || !output.isTTY) {
|
|
198
|
+
throw new Error("This step needs confirmation. Re-run with --yes to approve it without a prompt.");
|
|
199
|
+
}
|
|
200
|
+
const rl = createInterface({ input, output });
|
|
201
|
+
try {
|
|
202
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
203
|
+
return answer === "y" || answer === "yes";
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
rl.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
185
209
|
function defaultRunBridge(args, options = {}) {
|
|
186
210
|
// Re-enter this package's CLI in a child so ops stays a thin orchestrator.
|
|
187
211
|
const cli = process.argv[1];
|
|
@@ -202,6 +226,13 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
202
226
|
const runBridge = deps.runBridge ?? defaultRunBridge;
|
|
203
227
|
const env = deps.env ?? loadOpsEnv();
|
|
204
228
|
if (verb === "disconnect") {
|
|
229
|
+
// Ask here, not in the child command: this verb also removes the runner service, and the
|
|
230
|
+
// child's own prompt would come too late to stop that.
|
|
231
|
+
const approved = argv.includes("--yes") || argv.includes("-y") || await (deps.confirm ?? confirmOps)("Disengage this computer from the organization, remove the runner service, and clear local credentials? [y/N] ");
|
|
232
|
+
if (!approved) {
|
|
233
|
+
console.log("Cancelled.");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
205
236
|
runBridge(["uninstall-service"], { allowFail: true });
|
|
206
237
|
runBridge(["disconnect", "--yes"]);
|
|
207
238
|
console.log("Disconnected. Run: npx @miraland-labs/conduit-bridge@latest ops connect");
|
|
@@ -236,37 +267,49 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
236
267
|
if (env.loadedFrom) {
|
|
237
268
|
console.log(`Env: ${env.loadedFrom}`);
|
|
238
269
|
console.log(`URL: ${env.CONDUIT_URL || "(unset)"}`);
|
|
239
|
-
console.log(`
|
|
240
|
-
console.log(`
|
|
270
|
+
console.log(`Bootstrap workspace: ${env.CONDUIT_WORKSPACE || "(unset)"}`);
|
|
271
|
+
console.log(`Bootstrap repo: ${env.CONDUIT_REPO || "(none)"}`);
|
|
241
272
|
console.log(`Drivers: ${env.CONDUIT_DRIVERS || "(auto-detect installed agents)"}`);
|
|
242
273
|
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
274
|
console.log("");
|
|
249
275
|
}
|
|
250
276
|
else {
|
|
251
277
|
console.log("No ops.env loaded (showing drivers only).\n");
|
|
252
278
|
}
|
|
279
|
+
console.log(`Active runner workspace: ${await (deps.readActiveRunnerServiceWorkspace ?? readActiveRunnerServiceWorkspace)() ?? "(not installed)"}`);
|
|
253
280
|
runBridge(["drivers"]);
|
|
254
281
|
return;
|
|
255
282
|
}
|
|
256
283
|
if (verb === "doctor") {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
284
|
+
const { values } = parseArgs({
|
|
285
|
+
args: argv,
|
|
286
|
+
options: {
|
|
287
|
+
workspace: { type: "string" },
|
|
288
|
+
repo: { type: "string" },
|
|
289
|
+
"skip-service-alignment": { type: "boolean" },
|
|
290
|
+
},
|
|
291
|
+
allowPositionals: false,
|
|
292
|
+
});
|
|
293
|
+
const stored = await (deps.readStoredRunnerServiceOptions ?? readStoredRunnerServiceOptions)();
|
|
294
|
+
const workspaceRaw = values.workspace?.trim() || stored?.workspace || env.CONDUIT_WORKSPACE;
|
|
295
|
+
const expectedRepository = values.repo !== undefined
|
|
296
|
+
? values.repo.trim()
|
|
297
|
+
: stored?.ensureCheckout ?? env.CONDUIT_REPO;
|
|
298
|
+
requireOpsEnv({ ...env, CONDUIT_WORKSPACE: workspaceRaw });
|
|
299
|
+
if (!workspaceRaw)
|
|
300
|
+
throw new Error(`Install a runner service or set a bootstrap CONDUIT_WORKSPACE in ${defaultOpsEnvPath()}`);
|
|
260
301
|
applyRunnerToolPath();
|
|
261
302
|
const config = await (deps.loadBridgeConfig ?? loadConfig)();
|
|
262
|
-
const workspace = resolve(expandOpsValue(
|
|
263
|
-
|
|
264
|
-
|
|
303
|
+
const workspace = resolve(expandOpsValue(workspaceRaw));
|
|
304
|
+
if (!values["skip-service-alignment"]) {
|
|
305
|
+
for (const warning of runnerServiceWorkspaceWarnings(workspace)) {
|
|
306
|
+
console.warn(`WARNING: ${warning}`);
|
|
307
|
+
}
|
|
265
308
|
}
|
|
266
309
|
const report = await (deps.preflight ?? runBridgePreflight)({
|
|
267
310
|
config,
|
|
268
311
|
workspace,
|
|
269
|
-
expectedRepository:
|
|
312
|
+
expectedRepository: expectedRepository || undefined,
|
|
270
313
|
});
|
|
271
314
|
const installed = await detectInstalledClients();
|
|
272
315
|
const health = probeAgentHealth(installed, process.env);
|
|
@@ -287,8 +330,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
287
330
|
console.log("");
|
|
288
331
|
}
|
|
289
332
|
if (report.ready) {
|
|
290
|
-
const serviceWarnings = runnerServiceWorkspaceWarnings(workspace);
|
|
291
|
-
if (serviceWarnings.length) {
|
|
333
|
+
const serviceWarnings = values["skip-service-alignment"] ? [] : runnerServiceWorkspaceWarnings(workspace);
|
|
334
|
+
if (serviceWarnings.length > 0) {
|
|
292
335
|
for (const warning of serviceWarnings)
|
|
293
336
|
console.warn(`WARNING: ${warning}`);
|
|
294
337
|
throw new Error("Background runner service workspace does not match ops.env");
|
|
@@ -306,8 +349,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
306
349
|
runBridge(["drivers", verb, ...drivers]);
|
|
307
350
|
return;
|
|
308
351
|
}
|
|
309
|
-
//
|
|
310
|
-
//
|
|
352
|
+
// Explicit install arguments are active-run inputs, not machine-wide defaults. Enrollment may
|
|
353
|
+
// seed ops.env once; later project changes must not mutate it.
|
|
311
354
|
let installEnv = env;
|
|
312
355
|
let installArgv = argv;
|
|
313
356
|
if (verb === "install") {
|
|
@@ -323,20 +366,14 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
323
366
|
const declaredWorkspace = values.workspace?.trim();
|
|
324
367
|
const declaredRepo = values.repo?.trim();
|
|
325
368
|
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
369
|
installEnv = {
|
|
336
370
|
...env,
|
|
337
|
-
...(declaredWorkspace ? {
|
|
371
|
+
...(declaredWorkspace ? {
|
|
372
|
+
CONDUIT_WORKSPACE: declaredWorkspace,
|
|
373
|
+
// A new explicit workspace must not inherit the prior bootstrap repository.
|
|
374
|
+
CONDUIT_REPO: declaredRepo ?? "",
|
|
375
|
+
} : {}),
|
|
338
376
|
...(declaredRepo ? { CONDUIT_REPO: declaredRepo } : {}),
|
|
339
|
-
loadedFrom: env.loadedFrom ?? envPath,
|
|
340
377
|
};
|
|
341
378
|
}
|
|
342
379
|
}
|
|
@@ -355,13 +392,26 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
355
392
|
if (!projectId) {
|
|
356
393
|
throw new Error("Usage: ops switch --project <project-id> [--workspace <path>] [--repo <url>]");
|
|
357
394
|
}
|
|
358
|
-
const
|
|
395
|
+
const declaredWorkspace = values.workspace?.trim();
|
|
396
|
+
const declaredRepo = values.repo?.trim();
|
|
397
|
+
const [activeWorkspace, stored] = await Promise.all([
|
|
398
|
+
(deps.readActiveRunnerServiceWorkspace ?? readActiveRunnerServiceWorkspace)(),
|
|
399
|
+
(deps.readStoredRunnerServiceOptions ?? readStoredRunnerServiceOptions)(),
|
|
400
|
+
]);
|
|
401
|
+
const workspaceRaw = declaredWorkspace || activeWorkspace || stored?.workspace || env.CONDUIT_WORKSPACE;
|
|
359
402
|
if (!workspaceRaw)
|
|
360
403
|
throw new Error(`Set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()} or pass --workspace`);
|
|
361
404
|
const workspace = resolve(expandOpsValue(workspaceRaw));
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
405
|
+
const storedWorkspace = stored?.workspace ? resolve(expandOpsValue(stored.workspace)) : null;
|
|
406
|
+
const repo = (declaredRepo ?? (declaredWorkspace
|
|
407
|
+
? ""
|
|
408
|
+
: storedWorkspace === workspace
|
|
409
|
+
? stored?.ensureCheckout ?? ""
|
|
410
|
+
: activeWorkspace || stored?.workspace
|
|
411
|
+
? ""
|
|
412
|
+
: env.CONDUIT_REPO)).trim();
|
|
413
|
+
// Control plane first. Its intent is the target authority; the installed runner command is
|
|
414
|
+
// the local runtime authority. ops.env remains enrollment/bootstrap input only.
|
|
365
415
|
const config = await (deps.loadBridgeConfig ?? loadConfig)();
|
|
366
416
|
const client = new ConduitClient(config);
|
|
367
417
|
const result = await client.request("/runner/v1/machine/switch-project", {
|
|
@@ -373,29 +423,17 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
373
423
|
}),
|
|
374
424
|
});
|
|
375
425
|
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
426
|
console.log(`On-shift workspace: ${workspace}${repo ? ` (${repo})` : ""}`);
|
|
382
427
|
// Same install path as ops install — restart runner against the new checkout.
|
|
383
|
-
const switched = loadOpsEnv();
|
|
384
428
|
await runOps("install", [], {
|
|
385
429
|
...deps,
|
|
386
430
|
env: {
|
|
387
|
-
...
|
|
431
|
+
...env,
|
|
388
432
|
CONDUIT_WORKSPACE: workspace,
|
|
389
|
-
CONDUIT_REPO: repo
|
|
433
|
+
CONDUIT_REPO: repo,
|
|
390
434
|
},
|
|
391
435
|
});
|
|
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
|
-
}
|
|
436
|
+
// The target runner heartbeat clears the queued intent only after it proves Bound.
|
|
399
437
|
return;
|
|
400
438
|
}
|
|
401
439
|
if (verb === "connect") {
|
|
@@ -466,29 +504,32 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
466
504
|
* intent and Bridge clones into <root>/<owner>/<repo> and restarts on it.
|
|
467
505
|
*/
|
|
468
506
|
const managed = values.managed !== undefined;
|
|
469
|
-
const managedRoot = managed ? (values.managed?.trim() || DEFAULT_MANAGED_ROOT) : "";
|
|
470
|
-
const
|
|
471
|
-
const
|
|
507
|
+
const managedRoot = managed ? resolve(expandOpsValue(values.managed?.trim() || DEFAULT_MANAGED_ROOT)) : "";
|
|
508
|
+
const declaredWorkspace = values.workspace?.trim();
|
|
509
|
+
const declaredRepo = values.repo?.trim();
|
|
510
|
+
const workspace = managed ? managedRoot : (declaredWorkspace || env.CONDUIT_WORKSPACE);
|
|
511
|
+
const repo = managed ? "" : (declaredRepo ?? (declaredWorkspace ? "" : env.CONDUIT_REPO));
|
|
472
512
|
const machine = values.machine?.trim();
|
|
473
513
|
const envPath = env.loadedFrom ?? defaultOpsEnvPath();
|
|
474
514
|
writeOpsEnvFile(envPath, {
|
|
475
515
|
CONDUIT_URL: url,
|
|
476
516
|
...(workspace ? { CONDUIT_WORKSPACE: workspace } : {}),
|
|
477
|
-
|
|
478
|
-
|
|
517
|
+
CONDUIT_REPO: repo,
|
|
518
|
+
CONDUIT_MANAGED_ROOT: managedRoot,
|
|
479
519
|
});
|
|
520
|
+
const enrollmentEnv = {
|
|
521
|
+
...env,
|
|
522
|
+
CONDUIT_URL: url,
|
|
523
|
+
CONDUIT_WORKSPACE: workspace,
|
|
524
|
+
CONDUIT_REPO: repo,
|
|
525
|
+
CONDUIT_MANAGED_ROOT: managedRoot,
|
|
526
|
+
loadedFrom: envPath,
|
|
527
|
+
};
|
|
480
528
|
// Resolve the local lane before redeeming the single-use token. A missing coding agent is a
|
|
481
529
|
// local prerequisite, not an enrollment failure, and must leave the token available to retry.
|
|
482
530
|
// `--no-install` remains the explicit credential-only escape hatch.
|
|
483
531
|
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
|
-
}, [])
|
|
532
|
+
? await (deps.resolveInstallDrivers ?? resolveInstallDrivers)(enrollmentEnv, [])
|
|
492
533
|
: [];
|
|
493
534
|
if (managed)
|
|
494
535
|
mkdirSync(resolve(expandOpsValue(managedRoot)), { recursive: true });
|
|
@@ -519,8 +560,7 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
519
560
|
return;
|
|
520
561
|
}
|
|
521
562
|
if (workspace) {
|
|
522
|
-
|
|
523
|
-
await runOps("install", installDrivers, { ...deps, env: refreshedEnv });
|
|
563
|
+
await runOps("install", installDrivers, { ...deps, env: enrollmentEnv });
|
|
524
564
|
}
|
|
525
565
|
return;
|
|
526
566
|
}
|
|
@@ -566,7 +606,12 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
566
606
|
&& !installEnv.CONDUIT_REPO);
|
|
567
607
|
if (!waitingOnManagedAssignment) {
|
|
568
608
|
// Prove the exact local environment before installing a service that advertises availability.
|
|
569
|
-
runBridge([
|
|
609
|
+
runBridge([
|
|
610
|
+
"ops", "doctor",
|
|
611
|
+
"--workspace", workspace,
|
|
612
|
+
"--repo", installEnv.CONDUIT_REPO,
|
|
613
|
+
"--skip-service-alignment",
|
|
614
|
+
]);
|
|
570
615
|
}
|
|
571
616
|
const installArgs = ["install-service", "--workspace", workspace];
|
|
572
617
|
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/conduit-ops.cmd
CHANGED
|
@@ -4,5 +4,5 @@ if "%~1"=="" (
|
|
|
4
4
|
echo Usage: %~nx0 ^<connect^|install^|online^|offline^|status^|doctor^|disconnect^|uninstall^> [args…]
|
|
5
5
|
exit /b 1
|
|
6
6
|
)
|
|
7
|
-
npx --yes @miraland-labs/conduit-bridge@latest ops %*
|
|
7
|
+
call npx --yes @miraland-labs/conduit-bridge@latest ops %*
|
|
8
8
|
if errorlevel 1 exit /b %errorlevel%
|
package/ops/connect.cmd
CHANGED
package/ops/disconnect.cmd
CHANGED
package/ops/doctor.cmd
CHANGED
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/ops/install.cmd
CHANGED
package/ops/offline.cmd
CHANGED
package/ops/online.cmd
CHANGED
package/ops/status.cmd
CHANGED
package/ops/uninstall.cmd
CHANGED
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.20",
|
|
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": {
|