@miraland-labs/conduit-bridge 0.15.0 → 0.16.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.
@@ -11,6 +11,10 @@ import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl } fr
11
11
  import { redactSecrets } from "./config.js";
12
12
  import { DRIVERS, extractAgentReportJsonText, parseJsonObjectCandidate } from "./driver.js";
13
13
  import { pickDriverForClaim, supportsReadOnlyDiagnosis } from "./drivers.js";
14
+ import { changedPathsSince } from "./execution.js";
15
+ import { execFile } from "node:child_process";
16
+ import { promisify } from "node:util";
17
+ const execFileAsync = promisify(execFile);
14
18
  const budgetSchema = z.object({
15
19
  max_files: z.number().int().min(1).max(24).optional().default(8),
16
20
  max_duration_ms: z.number().int().min(10_000).max(15 * 60_000).optional().default(5 * 60_000),
@@ -25,6 +29,18 @@ export const investigationAssignmentSchema = z.object({
25
29
  commit: z.string().nullable().optional().default(null),
26
30
  source_attempt_id: z.string().uuid().nullable().optional().default(null),
27
31
  });
32
+ /**
33
+ * Enforcement is a vendor's claim; verification is ours. Both run on every investigation — this only
34
+ * records which enforcement, if any, was also in force. Lanes with no read-only mode still qualify:
35
+ * the run happens in a disposable worktree with no remote credential, and it is checked afterwards.
36
+ */
37
+ export function boundedByForDriver(driverId) {
38
+ if (driverId === "codex")
39
+ return "os_sandbox";
40
+ if (driverId === "claude-code" || driverId === "cursor")
41
+ return "tool_allowlist";
42
+ return "observed_clean";
43
+ }
28
44
  /** Mirrors the control plane's bounded result contract; anything else is refused before settlement. */
29
45
  const investigationBriefSchema = z.object({
30
46
  summary: z.string().trim().min(1).max(4_000),
@@ -68,6 +84,42 @@ export function parseInvestigationBrief(text) {
68
84
  throw new Error(UNUSABLE_BRIEF);
69
85
  return parsed.data;
70
86
  }
87
+ /**
88
+ * Identity of the source workspace: HEAD plus its dirty set. Compared before and after the run so an
89
+ * observer that reached outside its disposable worktree is caught, not assumed away.
90
+ */
91
+ async function workspaceFingerprint(workspace) {
92
+ try {
93
+ const head = await execFileAsync("git", ["-C", workspace, "rev-parse", "HEAD"], { timeout: 30_000 });
94
+ const dirty = await execFileAsync("git", ["-C", workspace, "status", "--porcelain"], { timeout: 30_000, maxBuffer: 8_000_000 });
95
+ return `${head.stdout.trim()}\n${dirty.stdout.trim()}`;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * Prove the run only looked. A write inside the disposable worktree is discarded anyway, but it
103
+ * means the answer came from a tree that no longer matches the pinned commit — so the brief is not
104
+ * evidence about that commit and must not be settled as if it were.
105
+ */
106
+ export async function verifyObservationOnly(input) {
107
+ const changed = await changedPathsSince(input.attemptWorkspace, input.commit);
108
+ // Git could not answer. An unverified read-only claim is not a read-only claim.
109
+ if (changed === null)
110
+ return { ok: false, error: "observer_unverifiable", detail: "the worktree could not be compared with the pinned commit" };
111
+ if (changed.length) {
112
+ return { ok: false, error: "observer_wrote", detail: `${changed.length} path(s) changed under the pinned commit, starting with ${changed.slice(0, 3).join(", ")}` };
113
+ }
114
+ const after = await workspaceFingerprint(input.workspace);
115
+ if (input.fingerprintBefore === null || after === null) {
116
+ return { ok: false, error: "observer_unverifiable", detail: "the source workspace could not be fingerprinted" };
117
+ }
118
+ if (after !== input.fingerprintBefore) {
119
+ return { ok: false, error: "observer_wrote", detail: "the source workspace changed during the run" };
120
+ }
121
+ return { ok: true };
122
+ }
71
123
  async function settle(client, investigationId, body) {
72
124
  await client.request(`/runner/v1/investigations/${investigationId}/settle`, {
73
125
  method: "POST",
@@ -84,12 +136,14 @@ export async function executeNextInvestigation(client, config, workspace, brief,
84
136
  const assignment = assignments[0];
85
137
  if (!assignment)
86
138
  return false;
87
- const driverId = pickDriverForClaim(config, null, supportsReadOnlyDiagnosis);
139
+ // Prefer a lane that enforces read-only, but do not require one. Enforcement is a vendor claim;
140
+ // the guarantee this lane actually rests on is a disposable worktree plus verification afterwards,
141
+ // which works the same on every agent — including ones that ship no read-only mode at all.
142
+ const driverId = pickDriverForClaim(config, null, supportsReadOnlyDiagnosis)
143
+ ?? pickDriverForClaim(config, null);
88
144
  const driver = driverId ? DRIVERS[driverId] : null;
89
145
  if (!driver) {
90
- // Nothing local can hold the run read-only. Leave it claimable by a capable machine instead of
91
- // taking the lease and failing under it.
92
- console.error(`Investigation ${assignment.id} skipped: no online lane can enforce read-only access.`);
146
+ console.error(`Investigation ${assignment.id} skipped: no online lane on this computer.`);
93
147
  return false;
94
148
  }
95
149
  const liveBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
@@ -118,7 +172,8 @@ export async function executeNextInvestigation(client, config, workspace, brief,
118
172
  method: "POST",
119
173
  body: JSON.stringify({ lease_token: leaseToken }),
120
174
  });
121
- console.log(`Claiming investigation ${assignment.id} at ${commit}`);
175
+ console.log(`Claiming investigation ${assignment.id} at ${commit} on ${driverId ?? "the default lane"}`);
176
+ const fingerprintBefore = await workspaceFingerprint(workspace);
122
177
  let attemptWorkspace = null;
123
178
  try {
124
179
  attemptWorkspace = await createAttemptWorktree({
@@ -144,6 +199,16 @@ export async function executeNextInvestigation(client, config, workspace, brief,
144
199
  return true;
145
200
  }
146
201
  const observed = parseInvestigationBrief(result.resultText ?? "");
202
+ const check = await verifyObservationOnly({ attemptWorkspace, workspace, commit, fingerprintBefore });
203
+ if (!check.ok) {
204
+ // The answer may be perfectly good, but it is no longer an answer about the pinned commit.
205
+ // Report what happened rather than settling a brief whose ground moved under it.
206
+ await settle(client, assignment.id, {
207
+ lease_token: leaseToken, status: "failed", error: `${check.error}: ${check.detail}`.slice(0, 2_000), retryable: true,
208
+ });
209
+ console.error(`Investigation ${assignment.id} discarded — ${check.error}: ${check.detail}`);
210
+ return true;
211
+ }
147
212
  await settle(client, assignment.id, {
148
213
  lease_token: leaseToken,
149
214
  status: "completed",
@@ -156,6 +221,7 @@ export async function executeNextInvestigation(client, config, workspace, brief,
156
221
  repository_fingerprint: assignment.repository_fingerprint ?? liveRepository ?? "unknown",
157
222
  commit,
158
223
  observed_at: new Date().toISOString(),
224
+ bounded_by: boundedByForDriver(driverId),
159
225
  files_read: observed.files_read,
160
226
  bytes_read: observed.bytes_read,
161
227
  commands_run: observed.commands_run,
package/dist/preflight.js CHANGED
@@ -18,7 +18,7 @@ function modelsFingerprint(config) {
18
18
  }
19
19
  // Protocol 5 makes the lease-token claim contract explicit. Older Bridges may report a green
20
20
  // preflight but cannot claim against the current control plane, so they must be admitted as stale.
21
- export const BRIDGE_PROTOCOL_VERSION = 6;
21
+ export const BRIDGE_PROTOCOL_VERSION = 7;
22
22
  const execFileAsync = promisify(execFile);
23
23
  async function defaultCommandRunner(command, args, cwd) {
24
24
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {