@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.
- package/dist/investigation.js +71 -5
- package/dist/preflight.js +1 -1
- package/package.json +1 -1
package/dist/investigation.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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": {
|