@miraland-labs/conduit-bridge 0.9.12 → 0.10.0
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 +10 -6
- package/dist/cli.js +4 -4
- package/dist/detect.js +3 -2
- package/dist/driver.js +149 -122
- package/dist/drivers.js +2 -1
- package/dist/execution-class.js +278 -0
- package/dist/execution.js +30 -2
- package/dist/ops.js +1 -1
- package/dist/preflight.js +4 -0
- package/package.json +2 -2
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.10.0` — **ExecutionClass** permission policy (`observe` | `observe_network` | `publish_artifact` | `verify` | `mutate_repo`) compiled once from the assignment contract and projected into every driver, so prompts match enforcement without a growing publish-command allow-list. Artifact evidence digests still normalize bare 64-hex to `sha256:<hex>`. New **Experimental Pi** lane (`pi` from `@earendil-works/pi-coding-agent`, local fuel); `observe_network` is refused on Pi (no fetch-without-bash). Heartbeat protocol 2 requires a clean workspace plus a ready, versioned driver lane before dispatch. Attempt worktrees fetch/start from the initiative base when the source tip is behind or history was rewritten, without resetting the source checkout. Also includes **ops** helpers for macOS / Linux / Windows, LaunchAgent/systemd **PATH** for `~/.local/bin`, disconnect/disengage, multi-driver lanes, shared slots **1–4**, worktrees, and optional `--ensure-checkout`. Protocol 1 heartbeats remain compatible for presence but cannot receive work.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
|
@@ -12,6 +12,7 @@ Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connect
|
|
|
12
12
|
- **Codex** — `codex`
|
|
13
13
|
- **OpenCode** — `opencode`
|
|
14
14
|
- **Cursor Agent** — `agent` (local fuel)
|
|
15
|
+
- **Pi** — `pi` (local fuel, Experimental; pin `@earendil-works/pi-coding-agent`)
|
|
15
16
|
- **Kiro CLI** — `kiro-cli` (local fuel)
|
|
16
17
|
- **Antigravity** — `agy` (local fuel)
|
|
17
18
|
|
|
@@ -77,13 +78,16 @@ repository delivery with PR/evidence and a successful rework cycle. All other la
|
|
|
77
78
|
|
|
78
79
|
## Grant enforcement
|
|
79
80
|
|
|
81
|
+
Assignments compile to an **ExecutionClass**, then each driver projects that class:
|
|
82
|
+
|
|
80
83
|
| Driver | Fuel | Mechanism |
|
|
81
84
|
| --- | --- | --- |
|
|
82
|
-
| `claude-code` | pump or local | `--allowedTools` / `--disallowedTools` per
|
|
83
|
-
| `codex` | pump or local | sandbox `read-only` / `workspace-write` |
|
|
84
|
-
| `opencode` | pump or local | per-run
|
|
85
|
-
| `cursor` | local only | per-run `.cursor/cli.json`
|
|
85
|
+
| `claude-code` | pump or local | `--allowedTools` / `--disallowedTools` per class |
|
|
86
|
+
| `codex` | pump or local | sandbox `read-only` / `workspace-write` (+ network when class requires) |
|
|
87
|
+
| `opencode` | pump or local | plan/build agent + per-run bash deny list |
|
|
88
|
+
| `cursor` | local only | per-run `.cursor/cli.json` + `--force` when class needs open tools |
|
|
89
|
+
| `pi` | local only | `--tools` class map (`observe_network` refused) |
|
|
86
90
|
| `kiro` | local only | `--trust-tools` allowlist |
|
|
87
91
|
| `antigravity` | local only | `-p --mode plan` / `accept-edits` |
|
|
88
92
|
|
|
89
|
-
Cursor/Kiro/Antigravity require local fuel (`ops install` sets this for you).
|
|
93
|
+
Cursor/Pi/Kiro/Antigravity require local fuel (`ops install` sets this for you).
|
package/dist/cli.js
CHANGED
|
@@ -131,8 +131,8 @@ async function join() {
|
|
|
131
131
|
console.log(`Proposed roles: ${capabilities.join(", ")}`);
|
|
132
132
|
console.log(`Assignments at once: ${leaseCapacity}`);
|
|
133
133
|
console.log(`Fuel source: ${plannedFuel === "local" ? "local subscription" : "Conduit pump"}`
|
|
134
|
-
+ (!fuelSource && plannedFuel === "local" ? " (auto: only
|
|
135
|
-
console.log(`Execution drivers: ${Object.keys(DRIVERS).join(", ")} (cursor/kiro/antigravity require local fuel).`);
|
|
134
|
+
+ (!fuelSource && plannedFuel === "local" ? " (auto: only local-fuel agents detected)" : ""));
|
|
135
|
+
console.log(`Execution drivers: ${Object.keys(DRIVERS).join(", ")} (cursor/pi/kiro/antigravity require local fuel).`);
|
|
136
136
|
console.log("Use --machine <name> to override the computer label.\n");
|
|
137
137
|
const response = await fetch(`${baseUrl}/runner/v1/connect/requests`, {
|
|
138
138
|
method: "POST",
|
|
@@ -246,10 +246,10 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
246
246
|
console.log(`Confirmed capabilities (matching): ${config.capabilities.join(", ") || "none"}`);
|
|
247
247
|
console.log(`Allowed actions: ${config.grants.join(", ") || "none"}`);
|
|
248
248
|
console.log(`Fuel source: ${config.fuelSource === "local" ? "local subscription" : "Conduit /v1 (project owner BYOK on open orgs; org credentials on invite-only)"}`
|
|
249
|
-
+ (fuelAutoLocal ? " (auto: only
|
|
249
|
+
+ (fuelAutoLocal ? " (auto: only local-fuel agents detected)" : ""));
|
|
250
250
|
const localOnly = localFuelOnlyClients(detected);
|
|
251
251
|
if (config.fuelSource === "conduit" && localOnly.length) {
|
|
252
|
-
console.log(`Note: ${localOnly.join(", ")} require local fuel per lane (set automatically for cursor/kiro/antigravity).`);
|
|
252
|
+
console.log(`Note: ${localOnly.join(", ")} require local fuel per lane (set automatically for cursor/pi/kiro/antigravity).`);
|
|
253
253
|
}
|
|
254
254
|
console.log("Heartbeats report capabilities for diagnostics only; matching uses the Connect confirmation. Detected clients never receive grants automatically.");
|
|
255
255
|
console.log(`MCP setup: {"mcpServers":{"conduit":{"command":"npx","args":["-y","@miraland-labs/conduit-bridge","mcp"]}}}`);
|
package/dist/detect.js
CHANGED
|
@@ -7,6 +7,7 @@ const CLIENTS = [
|
|
|
7
7
|
{ command: "opencode", label: "OpenCode" },
|
|
8
8
|
{ command: "agent", label: "Cursor Agent" },
|
|
9
9
|
{ command: "cursor", label: "Cursor" },
|
|
10
|
+
{ command: "pi", label: "Pi" },
|
|
10
11
|
{ command: "kiro-cli", label: "Kiro CLI" },
|
|
11
12
|
{ command: "agy", label: "Antigravity" },
|
|
12
13
|
{ command: "code", label: "Visual Studio Code" },
|
|
@@ -14,13 +15,13 @@ const CLIENTS = [
|
|
|
14
15
|
/** Agents Conduit /v1 pump fuel can drive today. */
|
|
15
16
|
const PUMP_CAPABLE_LABELS = new Set(["Claude Code", "Codex CLI", "OpenCode"]);
|
|
16
17
|
/** Agents that only accept vendor login — Bridge refuses Conduit pump for these drivers. */
|
|
17
|
-
const LOCAL_FUEL_ONLY_LABELS = new Set(["Cursor Agent", "Cursor", "Kiro CLI", "Antigravity"]);
|
|
18
|
+
const LOCAL_FUEL_ONLY_LABELS = new Set(["Cursor Agent", "Cursor", "Pi", "Kiro CLI", "Antigravity"]);
|
|
18
19
|
export function localFuelOnlyClients(detected) {
|
|
19
20
|
return detected.filter((label) => LOCAL_FUEL_ONLY_LABELS.has(label));
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* Default join fuel when `--fuel` is omitted.
|
|
23
|
-
* Pump when any pump-capable client is present; local when only
|
|
24
|
+
* Pump when any pump-capable client is present; local when only local-fuel agents are.
|
|
24
25
|
*/
|
|
25
26
|
export function suggestFuelSource(detected) {
|
|
26
27
|
const hasPumpCapable = detected.some((label) => PUMP_CAPABLE_LABELS.has(label));
|
package/dist/driver.js
CHANGED
|
@@ -3,6 +3,8 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectKiroTools, projectPi, resolveExecutionClass, } from "./execution-class.js";
|
|
7
|
+
export { branchCreateCommands, deniedCommands, isBoundedVerificationCommand, prCreateCommands, resolveExecutionClass, } from "./execution-class.js";
|
|
6
8
|
function deliveryLanguageRule(language) {
|
|
7
9
|
if (language === "zh") {
|
|
8
10
|
return "- Write every human-facing delivery packet string (outcome, changes, verification, assumptions, risks, limitations, criterion text) in Chinese (Simplified). Keep JSON keys and enum token values in English.";
|
|
@@ -45,39 +47,20 @@ export function tierForRisk(risk) {
|
|
|
45
47
|
export function isSafeModelName(value) {
|
|
46
48
|
return /^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$/.test(value);
|
|
47
49
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Shell commands each grant authorizes — one source of truth mapped per driver
|
|
50
|
-
* (`Bash(cmd:*)` for Claude, `Shell(cmd)` for Cursor) so the two cannot drift.
|
|
51
|
-
* rev-parse/log are read-only but required: the delivery contract reports the
|
|
52
|
-
* committed head sha, so a committing agent must be able to read it.
|
|
53
|
-
*/
|
|
54
|
-
export const branchCreateCommands = ["git status", "git diff", "git add", "git commit", "git branch", "git switch", "git rev-parse", "git log"];
|
|
55
|
-
export const prCreateCommands = ["git push", "gh pr create"];
|
|
56
|
-
/** Grants → Claude Code tool allow-list. Privileged grants are never mapped. */
|
|
50
|
+
/** Grants → Claude Code tool allow-list (mutate_repo mapping). Privileged grants are never mapped. */
|
|
57
51
|
export function claudeToolsForGrants(grants, verificationCommands = []) {
|
|
58
|
-
|
|
59
|
-
if (grants.includes("repo_read"))
|
|
60
|
-
tools.push("Read", "Glob", "Grep");
|
|
61
|
-
if (grants.includes("repo_write"))
|
|
62
|
-
tools.push("Edit", "Write");
|
|
63
|
-
if (grants.includes("test_run")) {
|
|
64
|
-
tools.push(...verificationCommands.filter(isBoundedVerificationCommand).map((command) => `Bash(${command}:*)`));
|
|
65
|
-
}
|
|
66
|
-
if (grants.includes("branch_create"))
|
|
67
|
-
tools.push(...branchCreateCommands.map((command) => `Bash(${command}:*)`));
|
|
68
|
-
if (grants.includes("pr_create"))
|
|
69
|
-
tools.push(...prCreateCommands.map((command) => `Bash(${command}:*)`));
|
|
70
|
-
return tools;
|
|
52
|
+
return projectClaude("mutate_repo", { grants, verificationCommands }).allowedTools;
|
|
71
53
|
}
|
|
72
|
-
/** Commands no driver may ever run, regardless of grants — one source for every driver's deny list. */
|
|
73
|
-
export const deniedCommands = [
|
|
74
|
-
"git merge", "git rebase", "git reset --hard", "git push --force",
|
|
75
|
-
"npm run deploy", "pnpm deploy", "yarn deploy", "wrangler deploy",
|
|
76
|
-
"kubectl", "terraform apply",
|
|
77
|
-
];
|
|
78
54
|
export const claudeDeniedTools = deniedCommands.map((command) => `Bash(${command}:*)`);
|
|
79
|
-
function
|
|
80
|
-
|
|
55
|
+
function resolveRunClass(input) {
|
|
56
|
+
if (input.executionClass)
|
|
57
|
+
return input.executionClass;
|
|
58
|
+
return resolveExecutionClass({
|
|
59
|
+
grants: input.grants,
|
|
60
|
+
capabilities: input.capabilities,
|
|
61
|
+
deliverable: input.deliverable,
|
|
62
|
+
workRole: input.workRole,
|
|
63
|
+
});
|
|
81
64
|
}
|
|
82
65
|
export function buildAssignmentPrompt(context) {
|
|
83
66
|
const packageContext = context.workPackage;
|
|
@@ -90,9 +73,6 @@ export function buildAssignmentPrompt(context) {
|
|
|
90
73
|
const changeScope = packageContext?.change_scope?.length ? packageContext.change_scope : spec.change_scope;
|
|
91
74
|
const evidence = packageContext?.required_evidence?.length ? packageContext.required_evidence : spec.required_evidence;
|
|
92
75
|
const rework = packageContext?.rework_feedback ?? context.reworkFeedback;
|
|
93
|
-
const labeledArtifact = (packageContext?.deliverable ?? spec.deliverable) === "artifact";
|
|
94
|
-
// Research is discovery in the delivery report — never a published content-pack artifact.
|
|
95
|
-
const artifact = labeledArtifact && role !== "research";
|
|
96
76
|
const lines = [
|
|
97
77
|
`You are completing one delegated Conduit assignment as the ${role} role. Work only inside the current workspace.`,
|
|
98
78
|
"",
|
|
@@ -135,48 +115,22 @@ export function buildAssignmentPrompt(context) {
|
|
|
135
115
|
lines.push("", `REWORK FEEDBACK — an independent review returned this delivery; address every point\n${rework}`);
|
|
136
116
|
const mustCommit = context.grants.includes("repo_write") && context.grants.includes("branch_create");
|
|
137
117
|
const mustOpenPr = mustCommit && context.grants.includes("pr_create") && Boolean(changeScope?.length);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
];
|
|
145
|
-
const hasExternalNetwork = (spec.required_capabilities ?? []).includes("external_network");
|
|
118
|
+
const executionClass = context.executionClass ?? resolveExecutionClass({
|
|
119
|
+
grants: context.grants,
|
|
120
|
+
capabilities: spec.required_capabilities,
|
|
121
|
+
deliverable: packageContext?.deliverable ?? spec.deliverable,
|
|
122
|
+
workRole: role,
|
|
123
|
+
});
|
|
146
124
|
const languageRule = deliveryLanguageRule(packageContext?.working_language ?? spec.working_language);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
: ["- You have no shell authority for this assignment. Do not attempt shell commands; verify by reading files and report what you could not verify as unknown."]),
|
|
157
|
-
// Mirror .cursor/cli.json WebFetch(*) — without naming it, agents fall back to WebSearch (not
|
|
158
|
-
// allow-listable) or invent status/title after "User Rejected" on unlisted fetches.
|
|
159
|
-
...(hasExternalNetwork
|
|
160
|
-
? ["- This assignment includes external_network: WebFetch is allow-listed. Use WebFetch for live HTTP status and page content; do not invent origin responses. Prefer WebFetch over WebSearch."]
|
|
161
|
-
: []), ...(mustCommit
|
|
162
|
-
? [
|
|
163
|
-
"- Your working branch is already checked out. Commit your repository changes on the current branch (git add + git commit) — do not create a new branch — and report the resulting sha as head_commit. A delivery that changed files without a commit is rejected.",
|
|
164
|
-
...(mustOpenPr
|
|
165
|
-
? [
|
|
166
|
-
"- This assignment includes pr_create: after committing, `git push -u origin HEAD` and `gh pr create` against the repository default branch, then set pull_request_url to that PR URL. Conduit merge-on-accept lands the PR when the owner Accepts — a head_commit without a PR leaves main unchanged and breaks later packages.",
|
|
167
|
-
]
|
|
168
|
-
: ["- You may commit locally; opening a pull request is outside this assignment's grants."]),
|
|
169
|
-
]
|
|
170
|
-
: artifact
|
|
171
|
-
? [
|
|
172
|
-
// repo_write without branch_create: the agent may write, but the output is not repository
|
|
173
|
-
// content. `.conduit/` is already in .git/info/exclude, so this path is ignored by git and
|
|
174
|
-
// `git status` stays clean — which is the point for a content pack, and mandatory for
|
|
175
|
-
// anything binary.
|
|
176
|
-
"- This assignment delivers an artifact, not repository content. Write your output under `.conduit/artifacts/` — that path is excluded from git, so do not commit anything and do not report a head_commit.",
|
|
177
|
-
"- Publish the artifact to its destination and report each output in your evidence with the published HTTPS URL and digest exactly as `sha256:` plus the 64-character hex hash (bare hex from shasum is normalized, but prefer the prefixed form). An artifact nobody can fetch is not a delivery.",
|
|
178
|
-
]
|
|
179
|
-
: ["- You have no repository write authority for this assignment. Do not create, modify, or commit any file; a delivery reporting repository changes will be rejected. Record all findings, verification output, and conclusions in your final report instead."]), "- Never merge, deploy, force-push, push to protected branches, or touch production.", `- Hard-denied commands (all drivers): ${deniedCommands.join("; ")}.`, "- Do not invent evidence. Report unknown when you could not verify a criterion.",
|
|
125
|
+
const classRules = executionClassPromptRules({
|
|
126
|
+
executionClass,
|
|
127
|
+
grants: context.grants,
|
|
128
|
+
verificationCommands: context.verificationCommands,
|
|
129
|
+
capabilities: spec.required_capabilities,
|
|
130
|
+
mustCommit,
|
|
131
|
+
mustOpenPr,
|
|
132
|
+
});
|
|
133
|
+
lines.push("", "RULES", "- Stay within the change scope and boundaries.", ...(languageRule ? [languageRule] : []), ...classRules, "- Never merge, deploy, force-push, push to protected branches, or touch production.", `- Hard-denied commands (all drivers): ${deniedCommands.join("; ")}.`, "- Do not invent evidence. Report unknown when you could not verify a criterion.",
|
|
180
134
|
// A met claim with nothing backing it is rejected server-side ("Met acceptance criteria require
|
|
181
135
|
// mapped evidence"). Observed live: six criteria marked met, evidence mapped to four, whole
|
|
182
136
|
// delivery lost. Say it here rather than let the agent discover it by failing.
|
|
@@ -320,10 +274,14 @@ export const claudeCodeDriver = {
|
|
|
320
274
|
error: "machine set to local fuel but claude-code has no login (ANTHROPIC_* or ~/.claude)",
|
|
321
275
|
};
|
|
322
276
|
}
|
|
323
|
-
const
|
|
277
|
+
const executionClass = resolveRunClass(input);
|
|
278
|
+
const projected = projectClaude(executionClass, {
|
|
279
|
+
grants: input.grants,
|
|
280
|
+
verificationCommands: input.verificationCommands,
|
|
281
|
+
});
|
|
324
282
|
// Fail closed: omitting --allowedTools would leave only the deny-list and
|
|
325
283
|
// broaden the agent beyond the grant contract (privileged grants map to none).
|
|
326
|
-
if (!
|
|
284
|
+
if (!projected.allowedTools.length) {
|
|
327
285
|
return {
|
|
328
286
|
status: "failed",
|
|
329
287
|
resultText: null,
|
|
@@ -336,9 +294,9 @@ export const claudeCodeDriver = {
|
|
|
336
294
|
args.push("--model", input.model);
|
|
337
295
|
if (input.resumeSessionId)
|
|
338
296
|
args.push("--resume", input.resumeSessionId);
|
|
339
|
-
args.push("--allowedTools",
|
|
340
|
-
args.push("--disallowedTools",
|
|
341
|
-
if (
|
|
297
|
+
args.push("--allowedTools", projected.allowedTools.join(","));
|
|
298
|
+
args.push("--disallowedTools", projected.disallowedTools.join(","));
|
|
299
|
+
if (projected.acceptEdits)
|
|
342
300
|
args.push("--permission-mode", "acceptEdits");
|
|
343
301
|
const { code, stdout, stderr } = await execute(input.executable ?? "claude", args, input.workspace, input.timeoutMs ?? 20 * 60_000, fuelSource === "conduit" ? input.fuel : undefined, fuelSource);
|
|
344
302
|
let message = null;
|
|
@@ -366,7 +324,7 @@ export function codexSandboxForGrants(grants) {
|
|
|
366
324
|
}
|
|
367
325
|
/**
|
|
368
326
|
* Codex exec arguments. The sandbox is the enforcement boundary: network stays
|
|
369
|
-
* off inside workspace-write unless the
|
|
327
|
+
* off inside workspace-write unless the class/capabilities require it.
|
|
370
328
|
*/
|
|
371
329
|
export function codexExecArgs(input, sandbox) {
|
|
372
330
|
// exec is non-interactive by design (no approval flag), and `exec resume`
|
|
@@ -376,7 +334,9 @@ export function codexExecArgs(input, sandbox) {
|
|
|
376
334
|
: ["exec", "--json", "--sandbox", sandbox];
|
|
377
335
|
if (input.model)
|
|
378
336
|
args.push("-m", input.model);
|
|
379
|
-
|
|
337
|
+
const networkAccess = input.networkAccess
|
|
338
|
+
?? (sandbox === "workspace-write" && input.capabilities?.includes("external_network"));
|
|
339
|
+
if (sandbox === "workspace-write" && networkAccess) {
|
|
380
340
|
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
381
341
|
}
|
|
382
342
|
if (input.resumeSessionId)
|
|
@@ -431,8 +391,7 @@ export const codexDriver = {
|
|
|
431
391
|
error: "machine set to local fuel but codex has no login (OPENAI_API_KEY / CODEX_API_KEY / ~/.codex)",
|
|
432
392
|
};
|
|
433
393
|
}
|
|
434
|
-
|
|
435
|
-
if (!sandbox) {
|
|
394
|
+
if (!hasMappedRepoAccess(input.grants)) {
|
|
436
395
|
return {
|
|
437
396
|
status: "failed",
|
|
438
397
|
resultText: null,
|
|
@@ -440,8 +399,10 @@ export const codexDriver = {
|
|
|
440
399
|
error: "No Bridge-mapped Codex sandbox for active grants; refusing to start agent",
|
|
441
400
|
};
|
|
442
401
|
}
|
|
402
|
+
const executionClass = resolveRunClass(input);
|
|
403
|
+
const projected = projectCodex(executionClass, { capabilities: input.capabilities });
|
|
443
404
|
// Prompt via stdin avoids ARG_MAX limits on large assignment contracts.
|
|
444
|
-
const { code, stdout, stderr } = await execute(input.executable ?? resolveCodexExecutable(), codexExecArgs(input, sandbox), input.workspace, input.timeoutMs ?? 20 * 60_000, fuelSource === "conduit" ? input.fuel : undefined, fuelSource, input.prompt);
|
|
405
|
+
const { code, stdout, stderr } = await execute(input.executable ?? resolveCodexExecutable(), codexExecArgs({ ...input, networkAccess: projected.networkAccess }, projected.sandbox), input.workspace, input.timeoutMs ?? 20 * 60_000, fuelSource === "conduit" ? input.fuel : undefined, fuelSource, input.prompt);
|
|
445
406
|
const parsed = parseCodexJsonl(stdout);
|
|
446
407
|
const resultText = parsed.resultText ?? (stdout || null);
|
|
447
408
|
if (code !== 0) {
|
|
@@ -451,38 +412,19 @@ export const codexDriver = {
|
|
|
451
412
|
},
|
|
452
413
|
};
|
|
453
414
|
/**
|
|
454
|
-
* Grants + capabilities → Cursor CLI permissions
|
|
415
|
+
* Grants + capabilities → Cursor CLI permissions via ExecutionClass.
|
|
455
416
|
* Current Cursor Agent schema accepts only `permissions.{allow,deny}` (no
|
|
456
417
|
* `version` / `approvalMode`). Outside the allow list is denied without --force.
|
|
457
418
|
*
|
|
458
|
-
* `
|
|
419
|
+
* `observe_network` / networked mutate / publish_artifact use `--force` (see
|
|
459
420
|
* cursorRunArgs). Field evidence: allow-listing WebFetch alone still yields
|
|
460
|
-
* "User Rejected" under headless `-p`; `--force` is required for fetch
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
* sandbox_workspace_write.network_access=true.
|
|
421
|
+
* "User Rejected" under headless `-p`; `--force` is required for fetch and for
|
|
422
|
+
* publish shell under open class policy. Research-only networked assignments
|
|
423
|
+
* deny Shell(*) and Write(**).
|
|
464
424
|
*/
|
|
465
|
-
export function cursorPermissionsForGrants(grants, verificationCommands = [], capabilities = []) {
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
allow.push(...verificationCommands.filter(isBoundedVerificationCommand).map((command) => `Shell(${command})`));
|
|
469
|
-
}
|
|
470
|
-
if (grants.includes("branch_create"))
|
|
471
|
-
allow.push(...branchCreateCommands.map((command) => `Shell(${command})`));
|
|
472
|
-
if (grants.includes("pr_create"))
|
|
473
|
-
allow.push(...prCreateCommands.map((command) => `Shell(${command})`));
|
|
474
|
-
// Cursor docs: WebFetch(domainOrPattern); WebFetch(*) auto-approves any domain.
|
|
475
|
-
if (capabilities.includes("external_network"))
|
|
476
|
-
allow.push("WebFetch(*)");
|
|
477
|
-
const deny = deniedCommands.map((command) => `Shell(${command})`);
|
|
478
|
-
// --force opens anything not denied; lock shell/write when the assignment has no mutate/shell grants.
|
|
479
|
-
if (capabilities.includes("external_network")
|
|
480
|
-
&& !grants.includes("repo_write")
|
|
481
|
-
&& !grants.includes("test_run")
|
|
482
|
-
&& !grants.includes("branch_create")
|
|
483
|
-
&& !grants.includes("pr_create")) {
|
|
484
|
-
deny.push("Shell(*)", "Write(**)");
|
|
485
|
-
}
|
|
425
|
+
export function cursorPermissionsForGrants(grants, verificationCommands = [], capabilities = [], deliverable = "repository", workRole) {
|
|
426
|
+
const executionClass = resolveExecutionClass({ grants, capabilities, deliverable, workRole });
|
|
427
|
+
const { allow, deny } = projectCursor(executionClass, { grants, verificationCommands, capabilities });
|
|
486
428
|
return { allow, deny };
|
|
487
429
|
}
|
|
488
430
|
/**
|
|
@@ -501,8 +443,13 @@ export function cursorRunArgs(input) {
|
|
|
501
443
|
const args = ["-p", "--output-format", "json", "--workspace", input.workspace];
|
|
502
444
|
if (input.trustWorkspace)
|
|
503
445
|
args.push("--trust");
|
|
504
|
-
|
|
505
|
-
|
|
446
|
+
const executionClass = resolveRunClass(input);
|
|
447
|
+
const force = input.force ?? projectCursor(executionClass, {
|
|
448
|
+
grants: input.grants,
|
|
449
|
+
capabilities: input.capabilities,
|
|
450
|
+
}).force;
|
|
451
|
+
// Headless WebFetch / open publish shell require --force (Cursor CLI 2026.07+).
|
|
452
|
+
if (force)
|
|
506
453
|
args.push("--force");
|
|
507
454
|
if (input.model)
|
|
508
455
|
args.push("--model", input.model);
|
|
@@ -581,7 +528,13 @@ export const cursorDriver = {
|
|
|
581
528
|
// local contract instead of making either version fail every attempt worktree.
|
|
582
529
|
const help = await execute(executable, ["--help"], input.workspace, 15_000, undefined, "local");
|
|
583
530
|
const trustWorkspace = help.code === 0 && cursorSupportsWorkspaceTrust(`${help.stdout}\n${help.stderr}`);
|
|
584
|
-
const
|
|
531
|
+
const executionClass = resolveRunClass(input);
|
|
532
|
+
const projected = projectCursor(executionClass, {
|
|
533
|
+
grants: input.grants,
|
|
534
|
+
verificationCommands: input.verificationCommands,
|
|
535
|
+
capabilities: input.capabilities ?? [],
|
|
536
|
+
});
|
|
537
|
+
const configured = await withCursorPermissions(input.workspace, { allow: projected.allow, deny: projected.deny }, () => execute(executable, cursorRunArgs({ ...input, trustWorkspace, executionClass, force: projected.force }), input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local"));
|
|
585
538
|
const { code, stdout, stderr } = configured;
|
|
586
539
|
const parsed = parseCursorOutput(stdout);
|
|
587
540
|
if (code !== 0 || parsed.isError) {
|
|
@@ -617,10 +570,17 @@ async function withCursorPermissions(workspace, permissions, run) {
|
|
|
617
570
|
}
|
|
618
571
|
/**
|
|
619
572
|
* OpenCode routes through any OpenAI/Anthropic-compatible endpoint, so it runs
|
|
620
|
-
* on Conduit pump fuel or a local login.
|
|
621
|
-
* agent (no edit/bash tools); write
|
|
573
|
+
* on Conduit pump fuel or a local login. Observe classes use its `plan`
|
|
574
|
+
* agent (no edit/bash tools); write/publish/verify use `build`.
|
|
622
575
|
*/
|
|
623
|
-
|
|
576
|
+
function hasMappedRepoAccess(grants) {
|
|
577
|
+
return ["repo_read", "repo_write", "test_run", "branch_create", "pr_create"].some((grant) => grants.includes(grant));
|
|
578
|
+
}
|
|
579
|
+
export function openCodeAgentForGrants(grants, executionClass) {
|
|
580
|
+
if (!hasMappedRepoAccess(grants))
|
|
581
|
+
return null;
|
|
582
|
+
if (executionClass)
|
|
583
|
+
return projectCoarseMode(executionClass) === "plan" ? "plan" : "build";
|
|
624
584
|
if (["repo_write", "test_run", "branch_create", "pr_create"].some((grant) => grants.includes(grant)))
|
|
625
585
|
return "build";
|
|
626
586
|
if (grants.includes("repo_read"))
|
|
@@ -729,7 +689,8 @@ export const openCodeDriver = {
|
|
|
729
689
|
if (fuelSource === "local" && !hasOpenCodeLogin()) {
|
|
730
690
|
return { status: "failed", resultText: null, sessionId: null, error: "machine set to local fuel but opencode has no login (auth.json / provider key in ~/.local/share/opencode or ~/.config/opencode)" };
|
|
731
691
|
}
|
|
732
|
-
const
|
|
692
|
+
const executionClass = resolveRunClass(input);
|
|
693
|
+
const agent = openCodeAgentForGrants(input.grants, executionClass);
|
|
733
694
|
if (!agent) {
|
|
734
695
|
return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped OpenCode agent for active grants; refusing to start agent" };
|
|
735
696
|
}
|
|
@@ -753,7 +714,11 @@ export const openCodeDriver = {
|
|
|
753
714
|
* (Antigravity-tier, not Claude-tier). Fine-grained command allowlisting would
|
|
754
715
|
* need a kiro agent config file — deferred until it can be verified with a login.
|
|
755
716
|
*/
|
|
756
|
-
export function kiroTrustedTools(grants) {
|
|
717
|
+
export function kiroTrustedTools(grants, executionClass) {
|
|
718
|
+
if (!hasMappedRepoAccess(grants))
|
|
719
|
+
return [];
|
|
720
|
+
if (executionClass)
|
|
721
|
+
return projectKiroTools(executionClass);
|
|
757
722
|
const tools = [];
|
|
758
723
|
if (grants.includes("repo_read"))
|
|
759
724
|
tools.push("fs_read");
|
|
@@ -780,7 +745,8 @@ export const kiroDriver = {
|
|
|
780
745
|
if (fuelSource === "conduit") {
|
|
781
746
|
return { status: "failed", resultText: null, sessionId: null, error: "kiro driver requires local fuel (KIRO_API_KEY or `kiro-cli login`). Run: npx @miraland-labs/conduit-bridge fuel local" };
|
|
782
747
|
}
|
|
783
|
-
const
|
|
748
|
+
const executionClass = resolveRunClass(input);
|
|
749
|
+
const trusted = kiroTrustedTools(input.grants, executionClass);
|
|
784
750
|
if (!trusted.length) {
|
|
785
751
|
return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped Kiro tools for active grants; refusing to start agent" };
|
|
786
752
|
}
|
|
@@ -809,7 +775,11 @@ export const kiroDriver = {
|
|
|
809
775
|
* enforcement is mode-level, not per-tool — coarser than Claude, the Cursor tier.
|
|
810
776
|
* Write mode adds `--sandbox` for terminal restrictions. Output is plain text.
|
|
811
777
|
*/
|
|
812
|
-
export function antigravityModeForGrants(grants) {
|
|
778
|
+
export function antigravityModeForGrants(grants, executionClass) {
|
|
779
|
+
if (!hasMappedRepoAccess(grants))
|
|
780
|
+
return null;
|
|
781
|
+
if (executionClass)
|
|
782
|
+
return projectCoarseMode(executionClass) === "plan" ? "plan" : "accept-edits";
|
|
813
783
|
if (["repo_write", "test_run", "branch_create", "pr_create"].some((grant) => grants.includes(grant)))
|
|
814
784
|
return "accept-edits";
|
|
815
785
|
if (grants.includes("repo_read"))
|
|
@@ -834,7 +804,8 @@ export const antigravityDriver = {
|
|
|
834
804
|
if (!hasAntigravityLogin()) {
|
|
835
805
|
return { status: "failed", resultText: null, sessionId: null, error: "antigravity has no login (set GEMINI_API_KEY or sign in with `agy`)" };
|
|
836
806
|
}
|
|
837
|
-
const
|
|
807
|
+
const executionClass = resolveRunClass(input);
|
|
808
|
+
const mode = antigravityModeForGrants(input.grants, executionClass);
|
|
838
809
|
if (!mode) {
|
|
839
810
|
return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped Antigravity mode for active grants; refusing to start agent" };
|
|
840
811
|
}
|
|
@@ -846,12 +817,67 @@ export const antigravityDriver = {
|
|
|
846
817
|
return { status: "completed", resultText: stdout, sessionId: null };
|
|
847
818
|
},
|
|
848
819
|
};
|
|
820
|
+
/**
|
|
821
|
+
* Pi (`@earendil-works/pi-coding-agent`) — Experimental local-fuel lane.
|
|
822
|
+
* Class tools via `--tools`; observe_network fails closed (no fetch-without-bash).
|
|
823
|
+
*/
|
|
824
|
+
export function piRunArgs(input) {
|
|
825
|
+
const args = ["--mode", "json", "-p", "--no-session", "--tools", input.tools];
|
|
826
|
+
if (input.model)
|
|
827
|
+
args.push("--model", input.model);
|
|
828
|
+
args.push(input.prompt);
|
|
829
|
+
return args;
|
|
830
|
+
}
|
|
831
|
+
export const piDriver = {
|
|
832
|
+
name: "pi",
|
|
833
|
+
async run(input) {
|
|
834
|
+
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
835
|
+
if (fuelSource === "conduit") {
|
|
836
|
+
return {
|
|
837
|
+
status: "failed",
|
|
838
|
+
resultText: null,
|
|
839
|
+
sessionId: null,
|
|
840
|
+
error: "pi driver requires local fuel (provider API key or Pi login). Run: npx @miraland-labs/conduit-bridge fuel local",
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
if (!hasMappedRepoAccess(input.grants)) {
|
|
844
|
+
return {
|
|
845
|
+
status: "failed",
|
|
846
|
+
resultText: null,
|
|
847
|
+
sessionId: null,
|
|
848
|
+
error: "No Bridge-mapped Pi access for active grants; refusing to start agent",
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
const executionClass = resolveRunClass(input);
|
|
852
|
+
const projected = projectPi(executionClass);
|
|
853
|
+
if ("error" in projected) {
|
|
854
|
+
return { status: "failed", resultText: null, sessionId: null, error: projected.error };
|
|
855
|
+
}
|
|
856
|
+
const executable = input.executable ?? "pi";
|
|
857
|
+
const version = await execute(executable, ["--version"], input.workspace, 15_000, undefined, "local");
|
|
858
|
+
if (version.code !== 0 || !(version.stdout || version.stderr).trim()) {
|
|
859
|
+
return { status: "failed", resultText: null, sessionId: null, error: "pi preflight could not verify the installed CLI version" };
|
|
860
|
+
}
|
|
861
|
+
const { code, stdout, stderr } = await execute(executable, piRunArgs({ prompt: input.prompt, tools: projected.tools, model: input.model }), input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local");
|
|
862
|
+
const parsed = parsePiJsonl(stdout);
|
|
863
|
+
if (code !== 0) {
|
|
864
|
+
return {
|
|
865
|
+
status: "failed",
|
|
866
|
+
resultText: parsed.resultText,
|
|
867
|
+
sessionId: parsed.sessionId,
|
|
868
|
+
error: (stderr || parsed.resultText || `pi exited with code ${code}`).slice(0, 20_000),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
return { status: "completed", resultText: parsed.resultText, sessionId: parsed.sessionId };
|
|
872
|
+
},
|
|
873
|
+
};
|
|
849
874
|
/** Stable driver ids shown in Connect / CLI. Order is product preference, not exclusivity. */
|
|
850
875
|
export const SUPPORTED_AGENTS = [
|
|
851
876
|
{ id: "claude-code", label: "Claude Code", executableHint: "claude" },
|
|
852
877
|
{ id: "codex", label: "Codex (ChatGPT)", executableHint: "codex" },
|
|
853
878
|
{ id: "cursor", label: "Cursor Agent", executableHint: "agent" },
|
|
854
879
|
{ id: "opencode", label: "OpenCode", executableHint: "opencode" },
|
|
880
|
+
{ id: "pi", label: "Pi", executableHint: "pi" },
|
|
855
881
|
{ id: "kiro", label: "Kiro CLI", executableHint: "kiro-cli" },
|
|
856
882
|
{ id: "antigravity", label: "Antigravity (agy)", executableHint: "agy" },
|
|
857
883
|
];
|
|
@@ -860,6 +886,7 @@ export const DRIVERS = {
|
|
|
860
886
|
codex: codexDriver,
|
|
861
887
|
cursor: cursorDriver,
|
|
862
888
|
opencode: openCodeDriver,
|
|
889
|
+
pi: piDriver,
|
|
863
890
|
kiro: kiroDriver,
|
|
864
891
|
antigravity: antigravityDriver,
|
|
865
892
|
};
|
package/dist/drivers.js
CHANGED
|
@@ -11,10 +11,11 @@ const LABEL_TO_DRIVER = {
|
|
|
11
11
|
OpenCode: "opencode",
|
|
12
12
|
"Cursor Agent": "cursor",
|
|
13
13
|
Cursor: "cursor",
|
|
14
|
+
Pi: "pi",
|
|
14
15
|
"Kiro CLI": "kiro",
|
|
15
16
|
Antigravity: "antigravity",
|
|
16
17
|
};
|
|
17
|
-
const LOCAL_FUEL_ONLY_DRIVERS = new Set(["cursor", "kiro", "antigravity"]);
|
|
18
|
+
const LOCAL_FUEL_ONLY_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity"]);
|
|
18
19
|
export function isSupportedDriverId(id) {
|
|
19
20
|
return Object.prototype.hasOwnProperty.call(DRIVERS, id);
|
|
20
21
|
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExecutionClass — one permission policy compiled from the assignment contract,
|
|
3
|
+
* then projected into each driver. Ends per-command allow-list republish churn.
|
|
4
|
+
*/
|
|
5
|
+
/** Bounded verification commands a `test_run` grant may execute. */
|
|
6
|
+
export function isBoundedVerificationCommand(command) {
|
|
7
|
+
return /^(npm run (verify|typecheck|lint|test|build)|pnpm (verify|typecheck|lint|test|build)|yarn (verify|typecheck|lint|test|build)|cargo (test|check)|go test(?: \.\/\.\.\.)?|make (test|check))$/.test(command);
|
|
8
|
+
}
|
|
9
|
+
/** Shell commands each grant authorizes — mapped per driver so they cannot drift. */
|
|
10
|
+
export const branchCreateCommands = [
|
|
11
|
+
"git status", "git diff", "git add", "git commit", "git branch", "git switch", "git rev-parse", "git log",
|
|
12
|
+
];
|
|
13
|
+
export const prCreateCommands = ["git push", "gh pr create"];
|
|
14
|
+
/** Commands no driver may ever run, regardless of grants. */
|
|
15
|
+
export const deniedCommands = [
|
|
16
|
+
"git merge", "git rebase", "git reset --hard", "git push --force",
|
|
17
|
+
"npm run deploy", "pnpm deploy", "yarn deploy", "wrangler deploy",
|
|
18
|
+
"kubectl", "terraform apply",
|
|
19
|
+
];
|
|
20
|
+
/** Land commands denied for publish_artifact / observe* (delivery gate still enforces). */
|
|
21
|
+
export const landCommands = ["git commit", "git push", "gh pr create"];
|
|
22
|
+
export const EXECUTION_CLASSES = [
|
|
23
|
+
"observe",
|
|
24
|
+
"observe_network",
|
|
25
|
+
"publish_artifact",
|
|
26
|
+
"verify",
|
|
27
|
+
"mutate_repo",
|
|
28
|
+
];
|
|
29
|
+
export function resolveExecutionClass(input) {
|
|
30
|
+
const grants = input.grants;
|
|
31
|
+
const capabilities = input.capabilities ?? [];
|
|
32
|
+
const deliverable = input.deliverable ?? "repository";
|
|
33
|
+
const workRole = input.workRole ?? null;
|
|
34
|
+
const hasWrite = grants.includes("repo_write");
|
|
35
|
+
const hasLand = grants.includes("branch_create") || grants.includes("pr_create");
|
|
36
|
+
const hasTest = grants.includes("test_run");
|
|
37
|
+
const hasRead = grants.includes("repo_read");
|
|
38
|
+
const external = capabilities.includes("external_network");
|
|
39
|
+
if (deliverable === "artifact" && workRole !== "research" && hasWrite)
|
|
40
|
+
return "publish_artifact";
|
|
41
|
+
if (external && !hasWrite && !hasLand && !hasTest)
|
|
42
|
+
return "observe_network";
|
|
43
|
+
// verify before observe: repo_read+test_run is the usual verify shape and must keep shell.
|
|
44
|
+
if (hasTest && !hasWrite && !hasLand)
|
|
45
|
+
return "verify";
|
|
46
|
+
if (!hasWrite && !hasLand && !hasTest && (hasRead || workRole === "research"))
|
|
47
|
+
return "observe";
|
|
48
|
+
return "mutate_repo";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Live HTTP is a property of the assignment's capabilities, not of its class.
|
|
52
|
+
*
|
|
53
|
+
* Every surface used to enumerate classes here, and the lists disagreed: the prompt promised WebFetch
|
|
54
|
+
* whenever the capability was present, Cursor granted it only for `observe_network`/`publish_artifact`/
|
|
55
|
+
* `mutate_repo`, Claude never granted it, and Codex granted it by capability. A `verify` assignment
|
|
56
|
+
* with `external_network` was therefore told to fetch by a prompt no driver could honour. One
|
|
57
|
+
* predicate, read by all of them, is what keeps that from recurring for the next class added.
|
|
58
|
+
*
|
|
59
|
+
* `observe_network` implies the capability by construction (`resolveExecutionClass` only returns it
|
|
60
|
+
* when `external_network` is present); it stays in the test so a spec that loses its capability list
|
|
61
|
+
* in transit still fails toward the class it was stamped with.
|
|
62
|
+
*/
|
|
63
|
+
export function allowsExternalFetch(executionClass, capabilities = []) {
|
|
64
|
+
return executionClass === "observe_network" || capabilities.includes("external_network");
|
|
65
|
+
}
|
|
66
|
+
export function executionClassPromptRules(input) {
|
|
67
|
+
const { executionClass, grants, verificationCommands = [], capabilities = [], mustCommit, mustOpenPr, } = input;
|
|
68
|
+
// Derived, never passed in: a caller-supplied `artifact` flag let the prompt order a publish that
|
|
69
|
+
// the class had already denied. The class is the single source of truth for both halves.
|
|
70
|
+
const artifact = executionClass === "publish_artifact";
|
|
71
|
+
const rules = [];
|
|
72
|
+
const grantShell = [
|
|
73
|
+
...(grants.includes("test_run") ? verificationCommands.filter(isBoundedVerificationCommand) : []),
|
|
74
|
+
...(grants.includes("branch_create") ? branchCreateCommands : []),
|
|
75
|
+
...(grants.includes("pr_create") ? prCreateCommands : []),
|
|
76
|
+
];
|
|
77
|
+
if (executionClass === "publish_artifact") {
|
|
78
|
+
rules.push("- You have shell authority to write under `.conduit/artifacts/` and publish the artifact (HTTPS URL + sha256 digest). Do not invent command wrappers — publish with ordinary tools (for example gh gist, curl, shasum). Do not commit or open a pull request.");
|
|
79
|
+
}
|
|
80
|
+
else if (executionClass === "observe" || executionClass === "observe_network") {
|
|
81
|
+
rules.push("- You have no shell authority for this assignment. Do not attempt shell commands; verify by reading files and report what you could not verify as unknown.");
|
|
82
|
+
}
|
|
83
|
+
else if (grantShell.length) {
|
|
84
|
+
rules.push(`- These are the ONLY shell commands you may run: ${grantShell.join("; ")}. Anything else is rejected — do not try variations, wrappers, or \`echo\`. Run the relevant ones and report their real output.`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
rules.push("- You have no shell authority for this assignment. Do not attempt shell commands; verify by reading files and report what you could not verify as unknown.");
|
|
88
|
+
}
|
|
89
|
+
if (allowsExternalFetch(executionClass, capabilities)) {
|
|
90
|
+
rules.push("- This assignment includes external_network: WebFetch is allow-listed. Use WebFetch (or the driver's live-fetch tool) for live HTTP status and page content; do not invent origin responses. Prefer WebFetch over WebSearch.");
|
|
91
|
+
}
|
|
92
|
+
if (mustCommit) {
|
|
93
|
+
rules.push("- Your working branch is already checked out. Commit your repository changes on the current branch (git add + git commit) — do not create a new branch — and report the resulting sha as head_commit. A delivery that changed files without a commit is rejected.");
|
|
94
|
+
if (mustOpenPr) {
|
|
95
|
+
rules.push("- This assignment includes pr_create: after committing, `git push -u origin HEAD` and `gh pr create` against the repository default branch, then set pull_request_url to that PR URL. Conduit merge-on-accept lands the PR when the owner Accepts — a head_commit without a PR leaves main unchanged and breaks later packages.");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
rules.push("- You may commit locally; opening a pull request is outside this assignment's grants.");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (artifact) {
|
|
102
|
+
rules.push("- This assignment delivers an artifact, not repository content. Write your output under `.conduit/artifacts/` — that path is excluded from git, so do not commit anything and do not report a head_commit.", "- Publish the artifact to its destination and report each output in your evidence with the published HTTPS URL and digest exactly as `sha256:` plus the 64-character hex hash (bare hex from shasum is normalized, but prefer the prefixed form). An artifact nobody can fetch is not a delivery.");
|
|
103
|
+
}
|
|
104
|
+
else if (executionClass === "mutate_repo" && grants.includes("repo_write")) {
|
|
105
|
+
// Write without land grants: tools may edit files, but commit/PR stay outside the contract.
|
|
106
|
+
rules.push("- You may create and modify files in the workspace. Do not commit or open a pull request — this assignment has no branch_create/pr_create authority. A delivery reporting head_commit or pull_request_url will be rejected.");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
rules.push("- You have no repository write authority for this assignment. Do not create, modify, or commit any file; a delivery reporting repository changes will be rejected. Record all findings, verification output, and conclusions in your final report instead.");
|
|
110
|
+
}
|
|
111
|
+
return rules;
|
|
112
|
+
}
|
|
113
|
+
export function projectCursor(executionClass, input) {
|
|
114
|
+
const grants = input.grants;
|
|
115
|
+
const verificationCommands = input.verificationCommands ?? [];
|
|
116
|
+
const capabilities = input.capabilities ?? [];
|
|
117
|
+
const deny = [
|
|
118
|
+
...deniedCommands.map((command) => `Shell(${command})`),
|
|
119
|
+
...((executionClass === "publish_artifact" || executionClass === "observe" || executionClass === "observe_network")
|
|
120
|
+
? landCommands.map((command) => `Shell(${command})`)
|
|
121
|
+
: []),
|
|
122
|
+
];
|
|
123
|
+
if (executionClass === "observe" || executionClass === "observe_network") {
|
|
124
|
+
deny.push("Shell(*)", "Write(**)");
|
|
125
|
+
}
|
|
126
|
+
const allow = [];
|
|
127
|
+
if (allowsExternalFetch(executionClass, capabilities))
|
|
128
|
+
allow.push("WebFetch(*)");
|
|
129
|
+
if (executionClass === "mutate_repo" || executionClass === "verify") {
|
|
130
|
+
if (grants.includes("test_run")) {
|
|
131
|
+
allow.push(...verificationCommands.filter(isBoundedVerificationCommand).map((command) => `Shell(${command})`));
|
|
132
|
+
}
|
|
133
|
+
if (executionClass === "mutate_repo") {
|
|
134
|
+
if (grants.includes("branch_create"))
|
|
135
|
+
allow.push(...branchCreateCommands.map((command) => `Shell(${command})`));
|
|
136
|
+
if (grants.includes("pr_create"))
|
|
137
|
+
allow.push(...prCreateCommands.map((command) => `Shell(${command})`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Headless WebFetch and the open publish shell are the two things Cursor will not do unprompted.
|
|
141
|
+
const force = allowsExternalFetch(executionClass, capabilities) || executionClass === "publish_artifact";
|
|
142
|
+
return { allow, deny, force };
|
|
143
|
+
}
|
|
144
|
+
export function projectClaude(executionClass, input) {
|
|
145
|
+
const grants = input.grants;
|
|
146
|
+
const verificationCommands = input.verificationCommands ?? [];
|
|
147
|
+
// allowedTools is a whitelist, so an unnamed tool is a denied tool: Claude honoured no
|
|
148
|
+
// external_network assignment at all until this was added, whatever the prompt said.
|
|
149
|
+
const fetchTools = allowsExternalFetch(executionClass, input.capabilities) ? ["WebFetch"] : [];
|
|
150
|
+
const landDeny = landCommands.map((command) => `Bash(${command}:*)`);
|
|
151
|
+
const nucleusDeny = deniedCommands.map((command) => `Bash(${command}:*)`);
|
|
152
|
+
const disallowedTools = [
|
|
153
|
+
...nucleusDeny,
|
|
154
|
+
...((executionClass === "publish_artifact" || executionClass === "observe" || executionClass === "observe_network")
|
|
155
|
+
? landDeny
|
|
156
|
+
: []),
|
|
157
|
+
];
|
|
158
|
+
if (executionClass === "observe" || executionClass === "observe_network") {
|
|
159
|
+
return { allowedTools: ["Read", "Glob", "Grep", ...fetchTools], disallowedTools, acceptEdits: false };
|
|
160
|
+
}
|
|
161
|
+
if (executionClass === "publish_artifact") {
|
|
162
|
+
return {
|
|
163
|
+
allowedTools: ["Read", "Glob", "Grep", "Edit", "Write", "Bash", ...fetchTools],
|
|
164
|
+
disallowedTools,
|
|
165
|
+
acceptEdits: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (executionClass === "verify") {
|
|
169
|
+
return {
|
|
170
|
+
allowedTools: [
|
|
171
|
+
"Read",
|
|
172
|
+
...verificationCommands.filter(isBoundedVerificationCommand).map((command) => `Bash(${command}:*)`),
|
|
173
|
+
...fetchTools,
|
|
174
|
+
],
|
|
175
|
+
disallowedTools,
|
|
176
|
+
acceptEdits: false,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// mutate_repo — current grant mapping
|
|
180
|
+
const tools = [];
|
|
181
|
+
if (grants.includes("repo_read"))
|
|
182
|
+
tools.push("Read", "Glob", "Grep");
|
|
183
|
+
if (grants.includes("repo_write"))
|
|
184
|
+
tools.push("Edit", "Write");
|
|
185
|
+
if (grants.includes("test_run")) {
|
|
186
|
+
tools.push(...verificationCommands.filter(isBoundedVerificationCommand).map((command) => `Bash(${command}:*)`));
|
|
187
|
+
}
|
|
188
|
+
if (grants.includes("branch_create"))
|
|
189
|
+
tools.push(...branchCreateCommands.map((command) => `Bash(${command}:*)`));
|
|
190
|
+
if (grants.includes("pr_create"))
|
|
191
|
+
tools.push(...prCreateCommands.map((command) => `Bash(${command}:*)`));
|
|
192
|
+
tools.push(...fetchTools);
|
|
193
|
+
return {
|
|
194
|
+
allowedTools: tools,
|
|
195
|
+
disallowedTools,
|
|
196
|
+
acceptEdits: grants.includes("repo_write"),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
export function projectCodex(executionClass, input) {
|
|
200
|
+
const capabilities = input.capabilities ?? [];
|
|
201
|
+
const networkAccess = allowsExternalFetch(executionClass, capabilities);
|
|
202
|
+
// Read-only is the sandbox for the one class that may not write; everything else needs the
|
|
203
|
+
// workspace. Network is the shared predicate, so Codex cannot drift from Cursor and Claude again.
|
|
204
|
+
if (executionClass === "observe")
|
|
205
|
+
return { sandbox: "read-only", networkAccess };
|
|
206
|
+
return { sandbox: "workspace-write", networkAccess };
|
|
207
|
+
}
|
|
208
|
+
/** Coarse mode for OpenCode / Antigravity. */
|
|
209
|
+
export function projectCoarseMode(executionClass) {
|
|
210
|
+
if (executionClass === "observe" || executionClass === "observe_network")
|
|
211
|
+
return "plan";
|
|
212
|
+
return "build";
|
|
213
|
+
}
|
|
214
|
+
export function projectKiroTools(executionClass) {
|
|
215
|
+
if (executionClass === "observe" || executionClass === "observe_network")
|
|
216
|
+
return ["fs_read"];
|
|
217
|
+
if (executionClass === "verify")
|
|
218
|
+
return ["fs_read", "execute_bash"];
|
|
219
|
+
return ["fs_read", "fs_write", "execute_bash"];
|
|
220
|
+
}
|
|
221
|
+
export function projectPi(executionClass) {
|
|
222
|
+
switch (executionClass) {
|
|
223
|
+
case "observe":
|
|
224
|
+
return { tools: "read,grep,find,ls" };
|
|
225
|
+
case "observe_network":
|
|
226
|
+
return {
|
|
227
|
+
error: "Pi cannot enforce observe_network; use Cursor/Codex or add OS sandbox later",
|
|
228
|
+
};
|
|
229
|
+
case "publish_artifact":
|
|
230
|
+
case "mutate_repo":
|
|
231
|
+
return { tools: "read,write,edit,bash,grep,find,ls" };
|
|
232
|
+
case "verify":
|
|
233
|
+
return { tools: "read,bash,grep,find,ls" };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Extract final assistant text from `pi --mode json` JSONL. */
|
|
237
|
+
export function parsePiJsonl(stdout) {
|
|
238
|
+
let resultText = null;
|
|
239
|
+
let sessionId = null;
|
|
240
|
+
const texts = [];
|
|
241
|
+
for (const line of stdout.split("\n")) {
|
|
242
|
+
const trimmed = line.trim();
|
|
243
|
+
if (!trimmed.startsWith("{"))
|
|
244
|
+
continue;
|
|
245
|
+
let event;
|
|
246
|
+
try {
|
|
247
|
+
event = JSON.parse(trimmed);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const session = [event.sessionId, event.session_id].find((value) => typeof value === "string");
|
|
253
|
+
if (typeof session === "string")
|
|
254
|
+
sessionId = session;
|
|
255
|
+
if (event.type === "message_end" || event.type === "agent_end" || event.type === "assistant") {
|
|
256
|
+
const content = event.message?.content ?? event.message?.text ?? event.text ?? event.result;
|
|
257
|
+
if (typeof content === "string" && content.trim())
|
|
258
|
+
resultText = content;
|
|
259
|
+
else if (Array.isArray(content)) {
|
|
260
|
+
const joined = content
|
|
261
|
+
.map((part) => (typeof part === "string" ? part : (part && typeof part === "object" && "text" in part ? String(part.text) : "")))
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join("\n");
|
|
264
|
+
if (joined)
|
|
265
|
+
resultText = joined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (event.type === "text" && typeof event.text === "string")
|
|
269
|
+
texts.push(event.text);
|
|
270
|
+
}
|
|
271
|
+
if (!resultText && texts.length)
|
|
272
|
+
resultText = texts.join("\n");
|
|
273
|
+
if (!resultText && stdout.trim()) {
|
|
274
|
+
// Fall back to whole stdout so parseAgentReport can still find a fenced block.
|
|
275
|
+
resultText = stdout;
|
|
276
|
+
}
|
|
277
|
+
return { resultText, sessionId };
|
|
278
|
+
}
|
package/dist/execution.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { ConduitRequestError } from "./client.js";
|
|
4
4
|
import { redactSecrets } from "./config.js";
|
|
5
|
-
import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, pickModelCandidate, tierForRisk } from "./driver.js";
|
|
5
|
+
import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, pickModelCandidate, resolveExecutionClass, tierForRisk } from "./driver.js";
|
|
6
6
|
import { pickDriverForClaim, resolveDriverFuel } from "./drivers.js";
|
|
7
7
|
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
8
8
|
import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
|
|
@@ -326,7 +326,32 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
326
326
|
}
|
|
327
327
|
// Same list the driver hands to the permission contract, so the prompt states exactly what is
|
|
328
328
|
// executable rather than leaving the agent to guess and hit rejections.
|
|
329
|
-
const
|
|
329
|
+
const executionClass = resolveExecutionClass({
|
|
330
|
+
grants,
|
|
331
|
+
capabilities: spec.required_capabilities,
|
|
332
|
+
deliverable,
|
|
333
|
+
workRole: workPackage?.work_role ?? spec.work_role,
|
|
334
|
+
});
|
|
335
|
+
if (typeof spec.execution_class === "string" && spec.execution_class !== executionClass) {
|
|
336
|
+
console.error(`Assignment ${taskId}: stamped execution_class=${spec.execution_class} differs from Bridge-derived ${executionClass}; using Bridge-derived class for launch`);
|
|
337
|
+
}
|
|
338
|
+
// An artifact deliverable that does not classify as publish_artifact cannot write the file it is
|
|
339
|
+
// being asked to publish, so the run is doomed before it starts. Refuse here rather than spend an
|
|
340
|
+
// agent run and fail at the delivery gate with a missing artifact and no stated cause. Reachable
|
|
341
|
+
// when a control plane older than the artifact-grant normalization dispatches such a package —
|
|
342
|
+
// exactly the window between publishing Bridge and deploying the control plane.
|
|
343
|
+
if (artifactDelivery && executionClass !== "publish_artifact") {
|
|
344
|
+
const detail = `Artifact deliverable resolved to execution class ${executionClass}, which cannot write or publish it. The work package needs the repo_write grant.`;
|
|
345
|
+
console.error(`Assignment ${taskId}: ${detail}`);
|
|
346
|
+
// Not retryable: retrying cannot add a grant, so this must reach the owner as a plan correction
|
|
347
|
+
// rather than burn attempts. Same terminal path every other launch failure uses.
|
|
348
|
+
await queueTerminal(client, taskId, {
|
|
349
|
+
action: "fail",
|
|
350
|
+
body: { error: detail, retryable: false, idempotency_key: `bridge:fail:${active.attemptId}:artifact-class` },
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const prompt = buildAssignmentPrompt({ taskId, objective: task.objective, spec, grants, workspace: attemptWorkspace, currentHead: worktreeStart, reworkFeedback, workPackage, verificationCommands: liveBrief?.verification ?? [], executionClass });
|
|
330
355
|
const resuming = Boolean(options.forceResumeSessionId);
|
|
331
356
|
await client.attemptRequest(taskId, "progress", {
|
|
332
357
|
phase: "changing",
|
|
@@ -383,6 +408,9 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
383
408
|
grants,
|
|
384
409
|
capabilities: spec.required_capabilities ?? [],
|
|
385
410
|
verificationCommands: liveBrief?.verification ?? [],
|
|
411
|
+
deliverable,
|
|
412
|
+
workRole: workPackage?.work_role ?? spec.work_role,
|
|
413
|
+
executionClass,
|
|
386
414
|
resumeSessionId,
|
|
387
415
|
timeoutMs,
|
|
388
416
|
model: selection.model,
|
package/dist/ops.js
CHANGED
|
@@ -11,7 +11,7 @@ import { describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
|
11
11
|
export const OPS_VERBS = [
|
|
12
12
|
"connect", "install", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
13
13
|
];
|
|
14
|
-
const LOCAL_FUEL_DRIVERS = new Set(["cursor", "kiro", "antigravity"]);
|
|
14
|
+
const LOCAL_FUEL_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity"]);
|
|
15
15
|
export function defaultOpsEnvPath(home = homedir()) {
|
|
16
16
|
return join(home, ".config", "conduit", "ops.env");
|
|
17
17
|
}
|
package/dist/preflight.js
CHANGED
|
@@ -21,6 +21,7 @@ const DRIVER_COMMAND = {
|
|
|
21
21
|
codex: "codex",
|
|
22
22
|
cursor: "agent",
|
|
23
23
|
opencode: "opencode",
|
|
24
|
+
pi: "pi",
|
|
24
25
|
kiro: "kiro-cli",
|
|
25
26
|
antigravity: "agy",
|
|
26
27
|
};
|
|
@@ -48,6 +49,9 @@ async function localAuthenticationReady(driver, workspace, run) {
|
|
|
48
49
|
const status = await run(executableFor(driver), ["whoami"], workspace);
|
|
49
50
|
return status.code === 0 && !/not logged in|not authenticated|login required/i.test(`${status.stdout}\n${status.stderr}`);
|
|
50
51
|
}
|
|
52
|
+
// Pi stores provider auth in its own config; CLI has no stable whoami — version probe is enough.
|
|
53
|
+
if (driver === "pi")
|
|
54
|
+
return true;
|
|
51
55
|
return false;
|
|
52
56
|
}
|
|
53
57
|
export async function runBridgePreflight(input, deps = {}) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conduit": "dist/cli.js"
|