@miraland-labs/conduit-bridge 0.16.0 → 0.16.2
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 +3 -1
- package/dist/cli.js +46 -7
- package/dist/detect.js +83 -1
- package/dist/driver.js +144 -2
- package/dist/drivers.js +3 -2
- package/dist/ensure-test-evidence.js +8 -3
- package/dist/execution-class.js +62 -0
- package/dist/execution.js +1 -1
- package/dist/investigation.js +1 -1
- package/dist/ops.js +59 -17
- package/dist/preflight.js +4 -1
- package/dist/service.js +52 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connect
|
|
|
15
15
|
- **Pi** — `pi` (local fuel, Experimental; pin `@earendil-works/pi-coding-agent`)
|
|
16
16
|
- **Kiro CLI** — `kiro-cli` (local fuel)
|
|
17
17
|
- **Antigravity** — `agy` (local fuel)
|
|
18
|
+
- **Grok Build** — `grok` (local fuel)
|
|
18
19
|
|
|
19
20
|
## Easy path (all platforms)
|
|
20
21
|
|
|
@@ -97,5 +98,6 @@ The control plane authors **ExecutionClass**; Bridge requires the stamped class
|
|
|
97
98
|
| `pi` | local only | `--tools` class map (`observe_network` refused) |
|
|
98
99
|
| `kiro` | local only | `--trust-tools` allowlist |
|
|
99
100
|
| `antigravity` | local only | `-p --mode plan` / `accept-edits` |
|
|
101
|
+
| `grok` | local only | `--sandbox` + `--allow`/`--deny` (deny wins under `--always-approve`) |
|
|
100
102
|
|
|
101
|
-
Cursor/Pi/Kiro/Antigravity require local fuel (`ops install` sets this for you).
|
|
103
|
+
Cursor/Pi/Kiro/Antigravity/Grok require local fuel (`ops install` sets this for you).
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ensureCheckout } from "./checkout.js";
|
|
|
18
18
|
import { 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
|
-
import { installRunnerService, uninstallRunnerService } from "./service.js";
|
|
21
|
+
import { applyRunnerToolPath, installRunnerService, uninstallRunnerService } from "./service.js";
|
|
22
22
|
import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, unavailableWorkspacePreflight } from "./preflight.js";
|
|
23
23
|
import { executeNextInvestigation } from "./investigation.js";
|
|
24
24
|
import { bridgeVersion } from "./version.js";
|
|
@@ -125,7 +125,7 @@ async function join() {
|
|
|
125
125
|
console.log(`Assignments at once: ${leaseCapacity}`);
|
|
126
126
|
console.log(`Fuel source: ${plannedFuel === "local" ? "local subscription" : "Conduit pump"}`
|
|
127
127
|
+ (!fuelSource && plannedFuel === "local" ? " (auto: only local-fuel agents detected)" : ""));
|
|
128
|
-
console.log(`Execution drivers: ${Object.keys(DRIVERS).join(", ")} (cursor/pi/kiro/antigravity require local fuel).`);
|
|
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");
|
|
130
130
|
const response = await fetch(`${baseUrl}/runner/v1/connect/requests`, {
|
|
131
131
|
method: "POST",
|
|
@@ -197,6 +197,43 @@ async function waitForConnection(pending, fuelSource) {
|
|
|
197
197
|
await clearPendingConnection();
|
|
198
198
|
throw new Error("Connection request expired. Run `conduit join --url <worker-url>` again.");
|
|
199
199
|
}
|
|
200
|
+
async function enroll() {
|
|
201
|
+
const { values } = parseArgs({
|
|
202
|
+
args: process.argv.slice(3),
|
|
203
|
+
options: {
|
|
204
|
+
url: { type: "string" },
|
|
205
|
+
token: { type: "string" },
|
|
206
|
+
fuel: { type: "string" },
|
|
207
|
+
machine: { type: "string" },
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
if (!values.url || !values.token) {
|
|
211
|
+
throw new Error(`Usage: ${BRIDGE_NPX} enroll --url <url> --token <token> [--machine <name>]`);
|
|
212
|
+
}
|
|
213
|
+
const baseUrl = normalizeBaseUrl(values.url);
|
|
214
|
+
const installationId = await loadOrCreateInstallationId();
|
|
215
|
+
const machineName = values.machine?.trim() || suggestMachineName(hostname(), installationId);
|
|
216
|
+
const response = await fetch(`${baseUrl}/runner/v1/connect/enroll-fast`, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: { "content-type": "application/json" },
|
|
219
|
+
body: JSON.stringify({ code: values.token, installation_id: installationId, machine_name: machineName }),
|
|
220
|
+
});
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const errorText = await response.text();
|
|
223
|
+
let message = errorText;
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(errorText);
|
|
226
|
+
if (parsed.message)
|
|
227
|
+
message = parsed.message;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// keep raw text
|
|
231
|
+
}
|
|
232
|
+
throw new Error(`Enrollment failed (${response.status}): ${message}`);
|
|
233
|
+
}
|
|
234
|
+
const data = (await response.json());
|
|
235
|
+
await finishConnection(baseUrl, data, parseFuelSource(values.fuel));
|
|
236
|
+
}
|
|
200
237
|
async function finishConnection(baseUrl, data, fuelSource) {
|
|
201
238
|
const fuelList = Array.isArray(data.fuel) ? data.fuel : [];
|
|
202
239
|
const fuel = {};
|
|
@@ -242,15 +279,14 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
242
279
|
+ (fuelAutoLocal ? " (auto: only local-fuel agents detected)" : ""));
|
|
243
280
|
const localOnly = localFuelOnlyClients(detected);
|
|
244
281
|
if (config.fuelSource === "conduit" && localOnly.length) {
|
|
245
|
-
console.log(`Note: ${localOnly.join(", ")} require local fuel per lane (set automatically for cursor/pi/kiro/antigravity).`);
|
|
282
|
+
console.log(`Note: ${localOnly.join(", ")} require local fuel per lane (set automatically for cursor/pi/kiro/antigravity/grok).`);
|
|
246
283
|
}
|
|
247
284
|
console.log("Heartbeats report capabilities for diagnostics only; matching uses the Connect confirmation. Detected clients never receive grants automatically.");
|
|
248
285
|
console.log(`MCP setup: {"mcpServers":{"conduit":{"command":"npx","args":["-y","@miraland-labs/conduit-bridge","mcp"]}}}`);
|
|
249
286
|
console.log(`Bring a lane online: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
|
|
250
287
|
console.log(`Execute work: ${bridgeUsage("runner", "--workspace", "<repo>")}`);
|
|
251
|
-
console.log(`Keep on shift
|
|
288
|
+
console.log(`Keep on shift: ${bridgeUsage("install-service", "--workspace", "<repo>")}`);
|
|
252
289
|
console.log(`Flip machine fuel later: ${bridgeUsage("fuel", "local|conduit")} (per-lane override: ${bridgeUsage("drivers", "fuel", "<id>", "local|conduit")})`);
|
|
253
|
-
console.log("Windows: keep the runner terminal open — install-service is macOS/Linux only.");
|
|
254
290
|
if (Object.keys(fuel).length) {
|
|
255
291
|
console.log(`Fleet fueling: ${Object.keys(fuel).length} project key(s) configured for Conduit /v1`);
|
|
256
292
|
}
|
|
@@ -259,7 +295,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
259
295
|
}
|
|
260
296
|
}
|
|
261
297
|
function openUrl(url) {
|
|
262
|
-
const command = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
|
|
298
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : process.platform === "win32" ? "explorer" : null;
|
|
263
299
|
if (!command)
|
|
264
300
|
return;
|
|
265
301
|
try {
|
|
@@ -418,6 +454,7 @@ async function driversCommand() {
|
|
|
418
454
|
throw new Error(`Usage: ${bridgeUsage("drivers", "[list|online|offline|fuel]", "…")}`);
|
|
419
455
|
}
|
|
420
456
|
async function runner() {
|
|
457
|
+
applyRunnerToolPath();
|
|
421
458
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
422
459
|
workspace: { type: "string" }, interval: { type: "string" }, once: { type: "boolean" },
|
|
423
460
|
"agent-timeout-minutes": { type: "string" }, fuel: { type: "string" }, "ensure-checkout": { type: "string" },
|
|
@@ -587,6 +624,8 @@ try {
|
|
|
587
624
|
await connect();
|
|
588
625
|
else if (command === "join")
|
|
589
626
|
await join();
|
|
627
|
+
else if (command === "enroll")
|
|
628
|
+
await enroll();
|
|
590
629
|
else if (command === "disconnect")
|
|
591
630
|
await disconnect();
|
|
592
631
|
else if (command === "fuel")
|
|
@@ -606,7 +645,7 @@ try {
|
|
|
606
645
|
else if (command === "ops")
|
|
607
646
|
await opsCommand();
|
|
608
647
|
else
|
|
609
|
-
throw new Error(`Usage: ${BRIDGE_NPX} <join|disconnect|connect|fuel|drivers|mcp|runner|install-service|uninstall-service|init-ops|ops>`);
|
|
648
|
+
throw new Error(`Usage: ${BRIDGE_NPX} <join|enroll|disconnect|connect|fuel|drivers|mcp|runner|install-service|uninstall-service|init-ops|ops>`);
|
|
610
649
|
}
|
|
611
650
|
catch (error) {
|
|
612
651
|
console.error(error instanceof Error ? redactSecrets(error.message) : "Conduit command failed");
|
package/dist/detect.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, constants } from "node:fs/promises";
|
|
2
2
|
import { delimiter, join } from "node:path";
|
|
3
|
+
import { hasAntigravityLogin, hasClaudeLogin, hasCursorLogin, hasGrokLogin, hasKiroLogin, hasOpenAiLogin, hasOpenCodeLogin, } from "./driver.js";
|
|
3
4
|
/** PATH probes for Connect diagnostics — not proof of a Bridge driver. */
|
|
4
5
|
const CLIENTS = [
|
|
5
6
|
{ command: "claude", label: "Claude Code" },
|
|
@@ -10,12 +11,13 @@ const CLIENTS = [
|
|
|
10
11
|
{ command: "pi", label: "Pi" },
|
|
11
12
|
{ command: "kiro-cli", label: "Kiro CLI" },
|
|
12
13
|
{ command: "agy", label: "Antigravity" },
|
|
14
|
+
{ command: "grok", label: "Grok Build" },
|
|
13
15
|
{ command: "code", label: "Visual Studio Code" },
|
|
14
16
|
];
|
|
15
17
|
/** Agents Conduit /v1 pump fuel can drive today. */
|
|
16
18
|
const PUMP_CAPABLE_LABELS = new Set(["Claude Code", "Codex CLI", "OpenCode"]);
|
|
17
19
|
/** Agents that only accept vendor login — Bridge refuses Conduit pump for these drivers. */
|
|
18
|
-
const LOCAL_FUEL_ONLY_LABELS = new Set(["Cursor Agent", "Cursor", "Pi", "Kiro CLI", "Antigravity"]);
|
|
20
|
+
const LOCAL_FUEL_ONLY_LABELS = new Set(["Cursor Agent", "Cursor", "Pi", "Kiro CLI", "Antigravity", "Grok Build"]);
|
|
19
21
|
export function localFuelOnlyClients(detected) {
|
|
20
22
|
return detected.filter((label) => LOCAL_FUEL_ONLY_LABELS.has(label));
|
|
21
23
|
}
|
|
@@ -30,6 +32,86 @@ export function suggestFuelSource(detected) {
|
|
|
30
32
|
return "local";
|
|
31
33
|
return "conduit";
|
|
32
34
|
}
|
|
35
|
+
export function probeAgentHealth(detected, env = process.env) {
|
|
36
|
+
const detectedSet = new Set(detected);
|
|
37
|
+
const health = [];
|
|
38
|
+
const claudeInstalled = detectedSet.has("Claude Code");
|
|
39
|
+
const claudeAuth = hasClaudeLogin(env);
|
|
40
|
+
health.push({
|
|
41
|
+
driverId: "claude",
|
|
42
|
+
label: "Claude Code",
|
|
43
|
+
installed: claudeInstalled,
|
|
44
|
+
authenticated: claudeInstalled,
|
|
45
|
+
authMethod: env.ANTHROPIC_API_KEY ? "environment_key" : claudeAuth ? "local_login" : "conduit_pump",
|
|
46
|
+
});
|
|
47
|
+
const codexInstalled = detectedSet.has("Codex CLI");
|
|
48
|
+
const codexAuth = hasOpenAiLogin(env);
|
|
49
|
+
health.push({
|
|
50
|
+
driverId: "codex",
|
|
51
|
+
label: "Codex CLI",
|
|
52
|
+
installed: codexInstalled,
|
|
53
|
+
authenticated: codexInstalled,
|
|
54
|
+
authMethod: (env.OPENAI_API_KEY || env.CODEX_API_KEY) ? "environment_key" : codexAuth ? "local_login" : "conduit_pump",
|
|
55
|
+
});
|
|
56
|
+
const openCodeInstalled = detectedSet.has("OpenCode");
|
|
57
|
+
const openCodeAuth = hasOpenCodeLogin(env);
|
|
58
|
+
health.push({
|
|
59
|
+
driverId: "opencode",
|
|
60
|
+
label: "OpenCode",
|
|
61
|
+
installed: openCodeInstalled,
|
|
62
|
+
authenticated: openCodeInstalled,
|
|
63
|
+
authMethod: openCodeAuth ? "local_login" : "conduit_pump",
|
|
64
|
+
});
|
|
65
|
+
const cursorInstalled = detectedSet.has("Cursor Agent") || detectedSet.has("Cursor");
|
|
66
|
+
const cursorAuth = hasCursorLogin(env);
|
|
67
|
+
health.push({
|
|
68
|
+
driverId: "cursor",
|
|
69
|
+
label: "Cursor Agent",
|
|
70
|
+
installed: cursorInstalled,
|
|
71
|
+
authenticated: cursorAuth,
|
|
72
|
+
authMethod: env.CURSOR_API_KEY ? "environment_key" : cursorAuth ? "local_login" : undefined,
|
|
73
|
+
missingAuthAdvice: cursorInstalled && !cursorAuth ? "Set CURSOR_API_KEY or log in via Cursor IDE" : undefined,
|
|
74
|
+
});
|
|
75
|
+
const agyInstalled = detectedSet.has("Antigravity");
|
|
76
|
+
const agyAuth = hasAntigravityLogin(env);
|
|
77
|
+
health.push({
|
|
78
|
+
driverId: "antigravity",
|
|
79
|
+
label: "Antigravity",
|
|
80
|
+
installed: agyInstalled,
|
|
81
|
+
authenticated: agyAuth,
|
|
82
|
+
authMethod: (env.GEMINI_API_KEY || env.GOOGLE_API_KEY) ? "environment_key" : agyAuth ? "local_login" : undefined,
|
|
83
|
+
missingAuthAdvice: agyInstalled && !agyAuth ? "Run `agy` to log in or set GEMINI_API_KEY" : undefined,
|
|
84
|
+
});
|
|
85
|
+
const kiroInstalled = detectedSet.has("Kiro CLI");
|
|
86
|
+
const kiroAuth = hasKiroLogin(env);
|
|
87
|
+
health.push({
|
|
88
|
+
driverId: "kiro",
|
|
89
|
+
label: "Kiro CLI",
|
|
90
|
+
installed: kiroInstalled,
|
|
91
|
+
authenticated: kiroAuth,
|
|
92
|
+
authMethod: env.KIRO_API_KEY ? "environment_key" : undefined,
|
|
93
|
+
missingAuthAdvice: kiroInstalled && !kiroAuth ? "Set KIRO_API_KEY or run `kiro-cli login`" : undefined,
|
|
94
|
+
});
|
|
95
|
+
const piInstalled = detectedSet.has("Pi");
|
|
96
|
+
health.push({
|
|
97
|
+
driverId: "pi",
|
|
98
|
+
label: "Pi",
|
|
99
|
+
installed: piInstalled,
|
|
100
|
+
authenticated: piInstalled,
|
|
101
|
+
authMethod: piInstalled ? "local_login" : undefined,
|
|
102
|
+
});
|
|
103
|
+
const grokInstalled = detectedSet.has("Grok Build");
|
|
104
|
+
const grokAuth = hasGrokLogin(env);
|
|
105
|
+
health.push({
|
|
106
|
+
driverId: "grok",
|
|
107
|
+
label: "Grok Build",
|
|
108
|
+
installed: grokInstalled,
|
|
109
|
+
authenticated: grokAuth,
|
|
110
|
+
authMethod: env.XAI_API_KEY ? "environment_key" : grokAuth ? "local_login" : undefined,
|
|
111
|
+
missingAuthAdvice: grokInstalled && !grokAuth ? "Set XAI_API_KEY or run `grok login`" : undefined,
|
|
112
|
+
});
|
|
113
|
+
return health;
|
|
114
|
+
}
|
|
33
115
|
export async function detectInstalledClients(pathValue = process.env.PATH ?? "", platform = process.platform) {
|
|
34
116
|
const directories = pathValue.split(delimiter).filter(Boolean);
|
|
35
117
|
const extensions = platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
package/dist/driver.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
|
|
6
|
+
import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectGrok, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
|
|
7
7
|
export { branchCreateCommands, deniedCommands, isBoundedVerificationCommand, prCreateCommands, requireStampedExecutionClass, } from "./execution-class.js";
|
|
8
8
|
function deliveryLanguageRule(language) {
|
|
9
9
|
if (language === "zh") {
|
|
@@ -1054,6 +1054,137 @@ export const piDriver = {
|
|
|
1054
1054
|
return { status: "completed", resultText: parsed.resultText, sessionId: parsed.sessionId };
|
|
1055
1055
|
},
|
|
1056
1056
|
};
|
|
1057
|
+
/**
|
|
1058
|
+
* Grok Build (`grok`) — local-fuel lane. Unattended flags are `--always-approve` plus deny/sandbox;
|
|
1059
|
+
* never `--dangerously-skip-permissions`, never plan mode (same Cursor lesson: it is not an
|
|
1060
|
+
* enforcement boundary and it suppresses the delivery JSON). `--worktree` is omitted: Bridge already
|
|
1061
|
+
* creates the attempt worktree.
|
|
1062
|
+
*/
|
|
1063
|
+
export function grokRunArgs(input) {
|
|
1064
|
+
const executionClass = resolveRunClass(input);
|
|
1065
|
+
const projected = projectGrok(executionClass, {
|
|
1066
|
+
grants: input.grants,
|
|
1067
|
+
verificationCommands: input.verificationCommands,
|
|
1068
|
+
capabilities: input.capabilities ?? [],
|
|
1069
|
+
diagnosis: input.workRole === "diagnose",
|
|
1070
|
+
});
|
|
1071
|
+
const args = [
|
|
1072
|
+
"--no-auto-update",
|
|
1073
|
+
"--no-alt-screen",
|
|
1074
|
+
"--no-plan",
|
|
1075
|
+
"--no-subagents",
|
|
1076
|
+
"--always-approve",
|
|
1077
|
+
"--output-format", "json",
|
|
1078
|
+
"--cwd", input.workspace,
|
|
1079
|
+
"--sandbox", projected.sandbox,
|
|
1080
|
+
];
|
|
1081
|
+
if (projected.disableWebSearch)
|
|
1082
|
+
args.push("--disable-web-search");
|
|
1083
|
+
args.push("--disallowed-tools", projected.disallowedTools.join(","));
|
|
1084
|
+
for (const rule of projected.deny)
|
|
1085
|
+
args.push("--deny", rule);
|
|
1086
|
+
for (const rule of projected.allow)
|
|
1087
|
+
args.push("--allow", rule);
|
|
1088
|
+
if (input.model)
|
|
1089
|
+
args.push("--model", input.model);
|
|
1090
|
+
if (input.resumeSessionId)
|
|
1091
|
+
args.push("--resume", input.resumeSessionId);
|
|
1092
|
+
args.push("-p", input.prompt);
|
|
1093
|
+
return args;
|
|
1094
|
+
}
|
|
1095
|
+
/** Last JSON object in `--output-format json` stdout (`text` / `sessionId`; Claude-shaped fallbacks). */
|
|
1096
|
+
export function parseGrokOutput(stdout) {
|
|
1097
|
+
const parsed = parseTrailingJsonObject(stdout);
|
|
1098
|
+
if (!parsed)
|
|
1099
|
+
return { resultText: stdout || null, sessionId: null, isError: false };
|
|
1100
|
+
const resultText = typeof parsed.text === "string" ? parsed.text
|
|
1101
|
+
: typeof parsed.result === "string" ? parsed.result
|
|
1102
|
+
: stdout;
|
|
1103
|
+
const sessionCandidate = [parsed.sessionId, parsed.session_id].find((value) => typeof value === "string");
|
|
1104
|
+
const stop = typeof parsed.stopReason === "string" ? parsed.stopReason
|
|
1105
|
+
: typeof parsed.stop_reason === "string" ? parsed.stop_reason
|
|
1106
|
+
: "";
|
|
1107
|
+
const isError = parsed.is_error === true
|
|
1108
|
+
|| parsed.subtype === "error"
|
|
1109
|
+
|| typeof parsed.error === "string"
|
|
1110
|
+
|| /^(error|refused)/i.test(stop);
|
|
1111
|
+
return { resultText, sessionId: sessionCandidate ?? null, isError };
|
|
1112
|
+
}
|
|
1113
|
+
function parseTrailingJsonObject(stdout) {
|
|
1114
|
+
const trimmed = stdout.trim();
|
|
1115
|
+
if (!trimmed)
|
|
1116
|
+
return null;
|
|
1117
|
+
try {
|
|
1118
|
+
const parsed = JSON.parse(trimmed);
|
|
1119
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
1120
|
+
return parsed;
|
|
1121
|
+
}
|
|
1122
|
+
catch {
|
|
1123
|
+
// Headless json is "one object at the end"; ignore preamble and take the trailing object.
|
|
1124
|
+
}
|
|
1125
|
+
const end = trimmed.lastIndexOf("}");
|
|
1126
|
+
if (end < 0)
|
|
1127
|
+
return null;
|
|
1128
|
+
let depth = 0;
|
|
1129
|
+
for (let index = end; index >= 0; index -= 1) {
|
|
1130
|
+
const ch = trimmed[index];
|
|
1131
|
+
if (ch === "}")
|
|
1132
|
+
depth += 1;
|
|
1133
|
+
else if (ch === "{") {
|
|
1134
|
+
depth -= 1;
|
|
1135
|
+
if (depth === 0) {
|
|
1136
|
+
try {
|
|
1137
|
+
const parsed = JSON.parse(trimmed.slice(index, end + 1));
|
|
1138
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
1139
|
+
return parsed;
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
export const grokDriver = {
|
|
1150
|
+
name: "grok",
|
|
1151
|
+
async run(input) {
|
|
1152
|
+
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
1153
|
+
if (fuelSource === "conduit") {
|
|
1154
|
+
return {
|
|
1155
|
+
status: "failed",
|
|
1156
|
+
resultText: null,
|
|
1157
|
+
sessionId: null,
|
|
1158
|
+
error: "grok driver requires local fuel (XAI_API_KEY or `grok login`). Run: npx @miraland-labs/conduit-bridge fuel local",
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
if (!hasMappedRepoAccess(input.grants)) {
|
|
1162
|
+
return {
|
|
1163
|
+
status: "failed",
|
|
1164
|
+
resultText: null,
|
|
1165
|
+
sessionId: null,
|
|
1166
|
+
error: "No Bridge-mapped Grok access for active grants; refusing to start agent",
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
if (!hasGrokLogin()) {
|
|
1170
|
+
return { status: "failed", resultText: null, sessionId: null, error: "grok has no login (set XAI_API_KEY or run `grok login`)" };
|
|
1171
|
+
}
|
|
1172
|
+
const executable = input.executable ?? "grok";
|
|
1173
|
+
const version = await execute(executable, ["--version"], input.workspace, 15_000, undefined, "local");
|
|
1174
|
+
if (version.code !== 0 || !(version.stdout || version.stderr).trim()) {
|
|
1175
|
+
return { status: "failed", resultText: null, sessionId: null, error: "grok preflight could not verify the installed CLI version" };
|
|
1176
|
+
}
|
|
1177
|
+
if (input.grants.includes("test_run") && !(input.verificationCommands?.length)) {
|
|
1178
|
+
return { status: "failed", resultText: null, sessionId: null, error: "grok preflight found no bounded verification command for the test_run grant" };
|
|
1179
|
+
}
|
|
1180
|
+
const { code, stdout, stderr } = await execute(executable, grokRunArgs(input), input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local");
|
|
1181
|
+
const parsed = parseGrokOutput(stdout);
|
|
1182
|
+
if (code !== 0 || parsed.isError) {
|
|
1183
|
+
return { status: "failed", resultText: parsed.resultText, sessionId: parsed.sessionId, error: (stderr || parsed.resultText || `grok exited with code ${code}`).slice(0, 20_000) };
|
|
1184
|
+
}
|
|
1185
|
+
return { status: "completed", resultText: parsed.resultText, sessionId: parsed.sessionId };
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1057
1188
|
/** Stable driver ids shown in Connect / CLI. Order is product preference, not exclusivity. */
|
|
1058
1189
|
export const SUPPORTED_AGENTS = [
|
|
1059
1190
|
{ id: "claude-code", label: "Claude Code", executableHint: "claude" },
|
|
@@ -1063,6 +1194,7 @@ export const SUPPORTED_AGENTS = [
|
|
|
1063
1194
|
{ id: "pi", label: "Pi", executableHint: "pi" },
|
|
1064
1195
|
{ id: "kiro", label: "Kiro CLI", executableHint: "kiro-cli" },
|
|
1065
1196
|
{ id: "antigravity", label: "Antigravity (agy)", executableHint: "agy" },
|
|
1197
|
+
{ id: "grok", label: "Grok Build", executableHint: "grok" },
|
|
1066
1198
|
];
|
|
1067
1199
|
export const DRIVERS = {
|
|
1068
1200
|
"claude-code": claudeCodeDriver,
|
|
@@ -1072,6 +1204,7 @@ export const DRIVERS = {
|
|
|
1072
1204
|
pi: piDriver,
|
|
1073
1205
|
kiro: kiroDriver,
|
|
1074
1206
|
antigravity: antigravityDriver,
|
|
1207
|
+
grok: grokDriver,
|
|
1075
1208
|
};
|
|
1076
1209
|
/** Most an agent may write to a pipe before the runner starts discarding the head. */
|
|
1077
1210
|
const MAX_AGENT_OUTPUT = 8 * 1024 * 1024;
|
|
@@ -1124,6 +1257,7 @@ const LOCAL_VENDOR_ENV = [
|
|
|
1124
1257
|
"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL",
|
|
1125
1258
|
"OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "CODEX_API_KEY",
|
|
1126
1259
|
"CURSOR_API_KEY", "KIRO_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY",
|
|
1260
|
+
"XAI_API_KEY",
|
|
1127
1261
|
];
|
|
1128
1262
|
function boundedEnvironment(fuel, fuelSource = "conduit") {
|
|
1129
1263
|
// Proxy and TLS variables stay: fueled agents must reach Conduit's /v1 on
|
|
@@ -1185,10 +1319,18 @@ export function hasAntigravityLogin(env = process.env) {
|
|
|
1185
1319
|
// agy 1.1.3 stores its Google session state under ~/.gemini (shared with the IDE).
|
|
1186
1320
|
return Boolean(home && (existsSync(join(home, ".gemini")) || existsSync(join(home, ".antigravity"))));
|
|
1187
1321
|
}
|
|
1322
|
+
export function hasGrokLogin(env = process.env) {
|
|
1323
|
+
if (env.XAI_API_KEY)
|
|
1324
|
+
return true;
|
|
1325
|
+
const home = env.HOME;
|
|
1326
|
+
// `~/.grok` exists after install; auth.json is the `grok login` / cached_token file.
|
|
1327
|
+
return Boolean(home && existsSync(join(home, ".grok", "auth.json")));
|
|
1328
|
+
}
|
|
1188
1329
|
/** True when local fuel can use a host vendor login for at least one supported agent. */
|
|
1189
1330
|
export function hasLocalVendorLogin(env = process.env) {
|
|
1190
1331
|
return hasClaudeLogin(env) || hasOpenAiLogin(env) || hasCursorLogin(env)
|
|
1191
|
-
|| hasOpenCodeLogin(env) || hasKiroLogin(env) || hasAntigravityLogin(env)
|
|
1332
|
+
|| hasOpenCodeLogin(env) || hasKiroLogin(env) || hasAntigravityLogin(env)
|
|
1333
|
+
|| hasGrokLogin(env);
|
|
1192
1334
|
}
|
|
1193
1335
|
/** Exported for tests — builds the stripped process env with optional Conduit fuel. */
|
|
1194
1336
|
export function agentProcessEnv(fuel, fuelSource = "conduit") {
|
package/dist/drivers.js
CHANGED
|
@@ -14,14 +14,15 @@ const LABEL_TO_DRIVER = {
|
|
|
14
14
|
Pi: "pi",
|
|
15
15
|
"Kiro CLI": "kiro",
|
|
16
16
|
Antigravity: "antigravity",
|
|
17
|
+
"Grok Build": "grok",
|
|
17
18
|
};
|
|
18
|
-
const LOCAL_FUEL_ONLY_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity"]);
|
|
19
|
+
const LOCAL_FUEL_ONLY_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity", "grok"]);
|
|
19
20
|
/**
|
|
20
21
|
* Drivers that can combine repository reads with bounded verification while denying source edits.
|
|
21
22
|
* Coarse build/bash modes are not enough for Invariant 23: a diagnosis carries `test_run`, but it
|
|
22
23
|
* must never acquire `repo_write` as an implementation detail of the selected lane.
|
|
23
24
|
*/
|
|
24
|
-
const READ_ONLY_DIAGNOSIS_DRIVERS = new Set(["claude-code", "codex", "cursor"]);
|
|
25
|
+
const READ_ONLY_DIAGNOSIS_DRIVERS = new Set(["claude-code", "codex", "cursor", "grok"]);
|
|
25
26
|
export function isSupportedDriverId(id) {
|
|
26
27
|
return Object.prototype.hasOwnProperty.call(DRIVERS, id);
|
|
27
28
|
}
|
|
@@ -8,6 +8,10 @@ import { promisify } from "node:util";
|
|
|
8
8
|
import { isBoundedVerificationCommand } from "./execution-class.js";
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
const TEST_EVIDENCE_DETAILS_MIN = 32;
|
|
11
|
+
/** Cold `cargo test` in an attempt worktree routinely exceeds two minutes of compile. */
|
|
12
|
+
export const VERIFICATION_TIMEOUT_MS = 600_000;
|
|
13
|
+
/** rustc + cargo logs routinely exceed 2 MiB; Node kills the child when maxBuffer is hit. */
|
|
14
|
+
export const VERIFICATION_MAX_BUFFER_BYTES = 32_000_000;
|
|
11
15
|
/** Contract requires test + test_run — Bridge always runs; agent paste is not a substitute. */
|
|
12
16
|
export function needsTestEvidence(_report, spec, grants) {
|
|
13
17
|
if (!grants.includes("test_run"))
|
|
@@ -93,17 +97,18 @@ async function defaultRunCommand(command, workspace) {
|
|
|
93
97
|
try {
|
|
94
98
|
const { stdout, stderr } = await execFileAsync(bin, argv.slice(1), {
|
|
95
99
|
cwd: workspace,
|
|
96
|
-
timeout:
|
|
97
|
-
maxBuffer:
|
|
100
|
+
timeout: VERIFICATION_TIMEOUT_MS,
|
|
101
|
+
maxBuffer: VERIFICATION_MAX_BUFFER_BYTES,
|
|
98
102
|
env: process.env,
|
|
99
103
|
});
|
|
100
104
|
return { stdout: String(stdout), stderr: String(stderr), code: 0 };
|
|
101
105
|
}
|
|
102
106
|
catch (error) {
|
|
103
107
|
const err = error;
|
|
108
|
+
const stderr = String(err.stderr || err.message || "verification failed");
|
|
104
109
|
return {
|
|
105
110
|
stdout: String(err.stdout ?? ""),
|
|
106
|
-
stderr
|
|
111
|
+
stderr,
|
|
107
112
|
code: typeof err.code === "number" ? err.code : 1,
|
|
108
113
|
};
|
|
109
114
|
}
|
package/dist/execution-class.js
CHANGED
|
@@ -217,6 +217,68 @@ export function projectCodex(executionClass, input) {
|
|
|
217
217
|
return { sandbox: "read-only", networkAccess };
|
|
218
218
|
return { sandbox: "workspace-write", networkAccess };
|
|
219
219
|
}
|
|
220
|
+
function grokBash(command) {
|
|
221
|
+
return `Bash(${command}*)`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Grok Build (`grok`) — Claude-style `--allow`/`--deny` plus Codex-style `--sandbox`.
|
|
225
|
+
*
|
|
226
|
+
* `--always-approve` is required for unattended runs (otherwise unmatched tools prompt and hang).
|
|
227
|
+
* Deny always wins over allow, including under always-approve, so restriction is deny + sandbox +
|
|
228
|
+
* `--disallowed-tools` + `--disable-web-search`. Allow entries document the intended surface; they
|
|
229
|
+
* are not a whitelist under always-approve. Do not deny `Bash(*)` on diagnosis: that would also
|
|
230
|
+
* block bounded verification because deny wins. OS `read-only` is what keeps diagnosis from writing
|
|
231
|
+
* the worktree. In-process WebFetch still works under `read-only` (child-process network does not).
|
|
232
|
+
*/
|
|
233
|
+
export function projectGrok(executionClass, input) {
|
|
234
|
+
const grants = input.grants;
|
|
235
|
+
const verificationCommands = input.verificationCommands ?? [];
|
|
236
|
+
const capabilities = input.capabilities ?? [];
|
|
237
|
+
const fetch = allowsExternalFetch(executionClass, capabilities);
|
|
238
|
+
const landDeny = executionClass === "publish_artifact" || executionClass === "observe" || executionClass === "observe_network";
|
|
239
|
+
const deny = [
|
|
240
|
+
...deniedCommands.map(grokBash),
|
|
241
|
+
"MCPTool(*)",
|
|
242
|
+
...(landDeny ? landCommands.map(grokBash) : []),
|
|
243
|
+
// always-approve: omit-from-allow is not a gate. In-process WebFetch ignores read-only.
|
|
244
|
+
...(!fetch ? ["WebFetch(*)", "WebSearch(*)"] : []),
|
|
245
|
+
];
|
|
246
|
+
const disallowedTools = fetch ? ["Agent"] : ["Agent", "web_search", "web_fetch"];
|
|
247
|
+
const allow = [];
|
|
248
|
+
if (fetch)
|
|
249
|
+
allow.push("WebFetch(*)");
|
|
250
|
+
if (executionClass === "observe" || executionClass === "observe_network") {
|
|
251
|
+
allow.push("Read(*)", "Grep(*)");
|
|
252
|
+
deny.push("Edit(*)", "Write(*)", "Bash(*)");
|
|
253
|
+
return { sandbox: "read-only", allow, deny, disableWebSearch: !fetch, disallowedTools };
|
|
254
|
+
}
|
|
255
|
+
if (input.diagnosis) {
|
|
256
|
+
allow.push("Read(*)", "Grep(*)", ...verificationCommands.filter(isBoundedVerificationCommand).map(grokBash));
|
|
257
|
+
deny.push("Edit(*)", "Write(*)");
|
|
258
|
+
return { sandbox: "read-only", allow, deny, disableWebSearch: !fetch, disallowedTools };
|
|
259
|
+
}
|
|
260
|
+
if (executionClass === "verify") {
|
|
261
|
+
allow.push("Read(*)", "Grep(*)", ...verificationCommands.filter(isBoundedVerificationCommand).map(grokBash));
|
|
262
|
+
deny.push("Edit(*)", "Write(*)");
|
|
263
|
+
return { sandbox: "workspace", allow, deny, disableWebSearch: !fetch, disallowedTools };
|
|
264
|
+
}
|
|
265
|
+
if (executionClass === "publish_artifact") {
|
|
266
|
+
allow.push("Read(*)", "Grep(*)", "Edit(*)", "Write(*)", "Bash(*)");
|
|
267
|
+
return { sandbox: "workspace", allow, deny, disableWebSearch: !fetch, disallowedTools };
|
|
268
|
+
}
|
|
269
|
+
if (grants.includes("repo_read"))
|
|
270
|
+
allow.push("Read(*)", "Grep(*)");
|
|
271
|
+
if (grants.includes("repo_write"))
|
|
272
|
+
allow.push("Edit(*)", "Write(*)");
|
|
273
|
+
if (grants.includes("test_run")) {
|
|
274
|
+
allow.push(...verificationCommands.filter(isBoundedVerificationCommand).map(grokBash));
|
|
275
|
+
}
|
|
276
|
+
if (grants.includes("branch_create"))
|
|
277
|
+
allow.push(...branchCreateCommands.map(grokBash));
|
|
278
|
+
if (grants.includes("pr_create"))
|
|
279
|
+
allow.push(...prCreateCommands.map(grokBash));
|
|
280
|
+
return { sandbox: "workspace", allow, deny, disableWebSearch: !fetch, disallowedTools };
|
|
281
|
+
}
|
|
220
282
|
/** Coarse mode for OpenCode / Antigravity. */
|
|
221
283
|
export function projectCoarseMode(executionClass) {
|
|
222
284
|
if (executionClass === "observe" || executionClass === "observe_network")
|
package/dist/execution.js
CHANGED
|
@@ -670,7 +670,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
670
670
|
disposition: "hold",
|
|
671
671
|
responsible_party: "computer_operator",
|
|
672
672
|
message: "Conductor is waiting for a diagnostic lane that can enforce read-only access.",
|
|
673
|
-
next_action: "Bring a Claude Code, Codex, or
|
|
673
|
+
next_action: "Bring a Claude Code, Codex, Cursor, or Grok Build lane online on this computer, then Recheck. Do not author compiler constraints for the agent.",
|
|
674
674
|
diagnostic_detail: detail,
|
|
675
675
|
},
|
|
676
676
|
error: detail,
|
package/dist/investigation.js
CHANGED
|
@@ -35,7 +35,7 @@ export const investigationAssignmentSchema = z.object({
|
|
|
35
35
|
* the run happens in a disposable worktree with no remote credential, and it is checked afterwards.
|
|
36
36
|
*/
|
|
37
37
|
export function boundedByForDriver(driverId) {
|
|
38
|
-
if (driverId === "codex")
|
|
38
|
+
if (driverId === "codex" || driverId === "grok")
|
|
39
39
|
return "os_sandbox";
|
|
40
40
|
if (driverId === "claude-code" || driverId === "cursor")
|
|
41
41
|
return "tool_allowlist";
|
package/dist/ops.js
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { homedir
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
import { ConduitClient } from "./client.js";
|
|
11
11
|
import { loadConfig } from "./config.js";
|
|
12
|
-
import { detectInstalledClients } from "./detect.js";
|
|
12
|
+
import { detectInstalledClients, probeAgentHealth } from "./detect.js";
|
|
13
13
|
import { driverIdsFromDetectedLabels } from "./drivers.js";
|
|
14
14
|
import { BRIDGE_PROTOCOL_VERSION, describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
15
15
|
import { bridgeVersion } from "./version.js";
|
|
16
16
|
export const OPS_VERBS = [
|
|
17
|
-
"connect", "install", "switch", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
17
|
+
"connect", "enroll", "install", "switch", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
18
18
|
];
|
|
19
|
-
const LOCAL_FUEL_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity"]);
|
|
19
|
+
const LOCAL_FUEL_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity", "grok"]);
|
|
20
20
|
function bridgeVersionAtLeast(value, minimum) {
|
|
21
21
|
const parse = (input) => {
|
|
22
22
|
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(input);
|
|
@@ -151,7 +151,7 @@ export async function resolveDrivers(env, argv, detect = detectInstalledClients)
|
|
|
151
151
|
console.log(`CONDUIT_DRIVERS not set — using detected agents: ${detected.join(", ")}`);
|
|
152
152
|
return detected;
|
|
153
153
|
}
|
|
154
|
-
throw new Error("No coding agent found on this computer. Install one (Claude Code, Codex, Cursor, OpenCode, Pi, Kiro, Antigravity), " +
|
|
154
|
+
throw new Error("No coding agent found on this computer. Install one (Claude Code, Codex, Cursor, OpenCode, Pi, Kiro, Antigravity, Grok Build), " +
|
|
155
155
|
"then retry — or set CONDUIT_DRIVERS / pass driver ids explicitly.");
|
|
156
156
|
}
|
|
157
157
|
/** Quote args for a copy-pasteable shell/cmd line (paths with spaces). */
|
|
@@ -180,7 +180,6 @@ function defaultRunBridge(args, options = {}) {
|
|
|
180
180
|
}
|
|
181
181
|
export async function runOps(verb, argv = [], deps = {}) {
|
|
182
182
|
const runBridge = deps.runBridge ?? defaultRunBridge;
|
|
183
|
-
const host = deps.host ?? platform();
|
|
184
183
|
const env = deps.env ?? loadOpsEnv();
|
|
185
184
|
if (verb === "disconnect") {
|
|
186
185
|
runBridge(["uninstall-service"], { allowFail: true });
|
|
@@ -239,7 +238,24 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
239
238
|
workspace: resolve(expandOpsValue(env.CONDUIT_WORKSPACE)),
|
|
240
239
|
expectedRepository: env.CONDUIT_REPO || undefined,
|
|
241
240
|
});
|
|
241
|
+
const installed = await detectInstalledClients();
|
|
242
|
+
const health = probeAgentHealth(installed, process.env);
|
|
242
243
|
console.log(`Bridge preflight: ${report.ready ? "READY" : "BLOCKED"}`);
|
|
244
|
+
const installedAgents = health.filter((h) => h.installed);
|
|
245
|
+
if (installedAgents.length > 0) {
|
|
246
|
+
console.log("\nAgent Diagnostics:");
|
|
247
|
+
for (const h of installedAgents) {
|
|
248
|
+
const method = h.authMethod === "conduit_pump" ? "Conduit pump"
|
|
249
|
+
: h.authMethod === "environment_key" ? "environment key"
|
|
250
|
+
: h.authMethod === "local_login" ? "local login"
|
|
251
|
+
: h.authMethod;
|
|
252
|
+
console.log(`- ${h.label}: ${h.authenticated ? "Ready" : "Needs local login"}${method ? ` (${method})` : ""}`);
|
|
253
|
+
if (h.missingAuthAdvice) {
|
|
254
|
+
console.log(` Advice: ${h.missingAuthAdvice}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
console.log("");
|
|
258
|
+
}
|
|
243
259
|
if (report.ready) {
|
|
244
260
|
console.log("Workspace, online lanes, local fuel, CLI availability, and authentication are ready.");
|
|
245
261
|
return;
|
|
@@ -388,6 +404,43 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
388
404
|
runBridge(args);
|
|
389
405
|
return;
|
|
390
406
|
}
|
|
407
|
+
if (verb === "enroll") {
|
|
408
|
+
const { values } = parseArgs({
|
|
409
|
+
args: argv,
|
|
410
|
+
options: {
|
|
411
|
+
url: { type: "string" },
|
|
412
|
+
token: { type: "string" },
|
|
413
|
+
workspace: { type: "string" },
|
|
414
|
+
repo: { type: "string" },
|
|
415
|
+
machine: { type: "string" },
|
|
416
|
+
"no-install": { type: "boolean" },
|
|
417
|
+
},
|
|
418
|
+
allowPositionals: true,
|
|
419
|
+
});
|
|
420
|
+
const url = (values.url?.trim() || env.CONDUIT_URL).trim();
|
|
421
|
+
const token = values.token?.trim();
|
|
422
|
+
if (!url || !token) {
|
|
423
|
+
throw new Error("Usage: ops enroll --url <url> --token <token> [--workspace <path>] [--repo <url>] [--machine <name>]");
|
|
424
|
+
}
|
|
425
|
+
const workspace = values.workspace?.trim() || env.CONDUIT_WORKSPACE;
|
|
426
|
+
const repo = values.repo?.trim() || env.CONDUIT_REPO;
|
|
427
|
+
const machine = values.machine?.trim();
|
|
428
|
+
const envPath = env.loadedFrom ?? defaultOpsEnvPath();
|
|
429
|
+
writeOpsEnvFile(envPath, {
|
|
430
|
+
CONDUIT_URL: url,
|
|
431
|
+
...(workspace ? { CONDUIT_WORKSPACE: workspace } : {}),
|
|
432
|
+
...(repo ? { CONDUIT_REPO: repo } : {}),
|
|
433
|
+
});
|
|
434
|
+
const args = ["enroll", "--url", url, "--token", token];
|
|
435
|
+
if (machine)
|
|
436
|
+
args.push("--machine", machine);
|
|
437
|
+
runBridge(args);
|
|
438
|
+
if (!values["no-install"] && workspace) {
|
|
439
|
+
const refreshedEnv = loadOpsEnv();
|
|
440
|
+
await runOps("install", [], { ...deps, env: refreshedEnv });
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
391
444
|
if (verb === "install") {
|
|
392
445
|
if (!installEnv.CONDUIT_WORKSPACE) {
|
|
393
446
|
throw new Error(`No workspace declared. Rerun with --workspace /path/to/repo, or set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()}`);
|
|
@@ -401,17 +454,6 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
401
454
|
runBridge(["drivers", "online", ...drivers]);
|
|
402
455
|
// Prove the exact local environment before installing a service that advertises availability.
|
|
403
456
|
runBridge(["ops", "doctor"]);
|
|
404
|
-
if (host === "win32") {
|
|
405
|
-
const runnerArgs = ["runner", "--workspace", workspace];
|
|
406
|
-
if (installEnv.CONDUIT_REPO)
|
|
407
|
-
runnerArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
|
|
408
|
-
console.log(`Bridge: v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION})`);
|
|
409
|
-
console.log("Windows: background install-service is not available.");
|
|
410
|
-
console.log("Keep a terminal open and run:");
|
|
411
|
-
console.log(` npx @miraland-labs/conduit-bridge@latest ${shellQuoteArgs(runnerArgs)}`);
|
|
412
|
-
console.log(`Drivers online: ${drivers.join(", ")}`);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
457
|
const installArgs = ["install-service", "--workspace", workspace];
|
|
416
458
|
if (installEnv.CONDUIT_REPO)
|
|
417
459
|
installArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
|
package/dist/preflight.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
|
|
5
|
-
import { hasAntigravityLogin, hasClaudeLogin, hasOpenAiLogin, hasOpenCodeLogin, resolveCodexExecutable, } from "./driver.js";
|
|
5
|
+
import { hasAntigravityLogin, hasClaudeLogin, hasGrokLogin, hasOpenAiLogin, hasOpenCodeLogin, resolveCodexExecutable, } from "./driver.js";
|
|
6
6
|
import { localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
7
7
|
/** Protocol 3: read-only diagnosis can reuse a retained failed-attempt worktree. */
|
|
8
8
|
// 4: each driver snapshot reports diagnosis_read_only, so the control plane can require a
|
|
@@ -39,6 +39,7 @@ const DRIVER_COMMAND = {
|
|
|
39
39
|
pi: "pi",
|
|
40
40
|
kiro: "kiro-cli",
|
|
41
41
|
antigravity: "agy",
|
|
42
|
+
grok: "grok",
|
|
42
43
|
};
|
|
43
44
|
function executableFor(driver) {
|
|
44
45
|
return driver === "codex" ? resolveCodexExecutable() : DRIVER_COMMAND[driver] ?? driver;
|
|
@@ -52,6 +53,8 @@ async function localAuthenticationReady(driver, workspace, run) {
|
|
|
52
53
|
return hasOpenCodeLogin();
|
|
53
54
|
if (driver === "antigravity")
|
|
54
55
|
return hasAntigravityLogin();
|
|
56
|
+
if (driver === "grok")
|
|
57
|
+
return hasGrokLogin();
|
|
55
58
|
if (driver === "cursor") {
|
|
56
59
|
if (process.env.CURSOR_API_KEY)
|
|
57
60
|
return true;
|
package/dist/service.js
CHANGED
|
@@ -33,13 +33,14 @@ export function runnerProgramArguments(options = {}) {
|
|
|
33
33
|
return args;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* PATH for LaunchAgent/systemd so agent CLIs in ~/.local/bin
|
|
37
|
-
* without the operator hand-editing the unit.
|
|
36
|
+
* PATH for LaunchAgent/systemd so agent CLIs in ~/.local/bin, rustup cargo,
|
|
37
|
+
* and Homebrew resolve without the operator hand-editing the unit.
|
|
38
38
|
*/
|
|
39
39
|
export function runnerServicePath(home = homedir(), nodeBin = dirname(process.execPath)) {
|
|
40
40
|
const parts = [
|
|
41
41
|
nodeBin,
|
|
42
42
|
join(home, ".local", "bin"),
|
|
43
|
+
join(home, ".cargo", "bin"),
|
|
43
44
|
"/opt/homebrew/bin",
|
|
44
45
|
"/usr/local/bin",
|
|
45
46
|
"/usr/bin",
|
|
@@ -49,6 +50,21 @@ export function runnerServicePath(home = homedir(), nodeBin = dirname(process.ex
|
|
|
49
50
|
];
|
|
50
51
|
return [...new Set(parts.filter(Boolean))].join(":");
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Merge the install-service PATH into this process so `cargo` / agent CLIs resolve
|
|
55
|
+
* even when a stale LaunchAgent plist omitted them. Call at runner boot — not a
|
|
56
|
+
* per-machine hand edit.
|
|
57
|
+
*/
|
|
58
|
+
export function applyRunnerToolPath(env = process.env, home = homedir(), nodeBin = dirname(process.execPath)) {
|
|
59
|
+
if (platform() === "win32")
|
|
60
|
+
return env.PATH ?? env.Path ?? "";
|
|
61
|
+
const current = env.PATH ?? "";
|
|
62
|
+
const seen = new Set(current.split(":").filter(Boolean));
|
|
63
|
+
const prefix = runnerServicePath(home, nodeBin).split(":").filter((dir) => dir && !seen.has(dir));
|
|
64
|
+
const next = [...prefix, ...current.split(":").filter(Boolean)].join(":");
|
|
65
|
+
env.PATH = next;
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
52
68
|
export function launchdPlist(programArguments, stdoutPath, pathEnv = runnerServicePath()) {
|
|
53
69
|
const argsXml = programArguments.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
|
|
54
70
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -111,6 +127,18 @@ function launchAgentsPath() {
|
|
|
111
127
|
function systemdUnitPath() {
|
|
112
128
|
return join(homedir(), ".config", "systemd", "user", "conduit-runner.service");
|
|
113
129
|
}
|
|
130
|
+
export const WINDOWS_TASK_NAME = "ConduitBridgeRunner";
|
|
131
|
+
/** Quote one argv token for a Windows Task Scheduler /TR command line. */
|
|
132
|
+
export function quoteWindowsTaskArg(value) {
|
|
133
|
+
if (!/[\s&<>|^()"]/.test(value))
|
|
134
|
+
return value;
|
|
135
|
+
return `"${value.replaceAll('"', '\\"')}"`;
|
|
136
|
+
}
|
|
137
|
+
/** cmd.exe line that keeps Node on PATH and starts the runner. */
|
|
138
|
+
export function windowsTaskCommandLine(programArguments, nodeBin = dirname(process.execPath)) {
|
|
139
|
+
const inner = programArguments.map(quoteWindowsTaskArg).join(" ");
|
|
140
|
+
return `cmd.exe /c set "PATH=${nodeBin};%PATH%"&& ${inner}`;
|
|
141
|
+
}
|
|
114
142
|
function run(command, args) {
|
|
115
143
|
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
116
144
|
if (result.status !== 0) {
|
|
@@ -180,10 +208,23 @@ export function reloadLaunchdRunnerSync(domain, plistPath) {
|
|
|
180
208
|
}
|
|
181
209
|
export async function installRunnerService(options = {}) {
|
|
182
210
|
const host = platform();
|
|
183
|
-
if (host !== "darwin" && host !== "linux") {
|
|
184
|
-
throw new Error(`install-service is supported on macOS and
|
|
211
|
+
if (host !== "darwin" && host !== "linux" && host !== "win32") {
|
|
212
|
+
throw new Error(`install-service is supported on macOS, Linux, and Windows only (got ${host})`);
|
|
185
213
|
}
|
|
186
214
|
const programArguments = runnerProgramArguments(options);
|
|
215
|
+
if (host === "win32") {
|
|
216
|
+
const tr = windowsTaskCommandLine(programArguments);
|
|
217
|
+
run("schtasks", ["/Create", "/TN", WINDOWS_TASK_NAME, "/TR", tr, "/SC", "ONLOGON", "/RL", "LIMITED", "/F"]);
|
|
218
|
+
try {
|
|
219
|
+
run("schtasks", ["/Run", "/TN", WINDOWS_TASK_NAME]);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Task is registered for next logon. /Run needs an interactive session; enroll already succeeded.
|
|
223
|
+
}
|
|
224
|
+
await mkdir(configDir(), { recursive: true });
|
|
225
|
+
await writeFile(serviceStatePath(), `${JSON.stringify({ platform: "win32", programArguments, options, installed_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
226
|
+
return { path: WINDOWS_TASK_NAME, platform: "win32" };
|
|
227
|
+
}
|
|
187
228
|
if (host === "darwin") {
|
|
188
229
|
const plistPath = launchAgentsPath();
|
|
189
230
|
await mkdir(dirname(plistPath), { recursive: true });
|
|
@@ -225,8 +266,8 @@ export async function installRunnerService(options = {}) {
|
|
|
225
266
|
}
|
|
226
267
|
export async function uninstallRunnerService() {
|
|
227
268
|
const host = platform();
|
|
228
|
-
if (host !== "darwin" && host !== "linux") {
|
|
229
|
-
throw new Error(`uninstall-service is supported on macOS and
|
|
269
|
+
if (host !== "darwin" && host !== "linux" && host !== "win32") {
|
|
270
|
+
throw new Error(`uninstall-service is supported on macOS, Linux, and Windows only (got ${host})`);
|
|
230
271
|
}
|
|
231
272
|
let stored = null;
|
|
232
273
|
try {
|
|
@@ -235,6 +276,11 @@ export async function uninstallRunnerService() {
|
|
|
235
276
|
catch {
|
|
236
277
|
stored = null;
|
|
237
278
|
}
|
|
279
|
+
if (host === "win32") {
|
|
280
|
+
spawnSync("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"], { encoding: "utf8" });
|
|
281
|
+
await unlink(serviceStatePath()).catch(() => undefined);
|
|
282
|
+
return { path: WINDOWS_TASK_NAME, platform: "win32" };
|
|
283
|
+
}
|
|
238
284
|
if (host === "darwin") {
|
|
239
285
|
const plistPath = launchAgentsPath();
|
|
240
286
|
spawnSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${SERVICE_LABEL}`], { encoding: "utf8" });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.16.
|
|
4
|
-
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
|
|
3
|
+
"version": "0.16.2",
|
|
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": {
|
|
7
7
|
"conduit": "dist/cli.js"
|