@miraland-labs/conduit-bridge 0.16.138 → 0.16.141
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/cli.js +2 -1
- package/dist/config.js +13 -14
- package/dist/execution.js +49 -12
- package/dist/investigation.js +21 -4
- package/dist/secret-redaction.js +35 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { redactSecrets } from "./secret-redaction.js";
|
|
2
3
|
import { parseArgs } from "node:util";
|
|
3
4
|
import { resolve, dirname, join as pathJoin } from "node:path";
|
|
4
5
|
import { hostname, homedir, platform, userInfo } from "node:os";
|
|
@@ -8,7 +9,7 @@ import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
|
8
9
|
import { stdin as input, stdout as output } from "node:process";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { assignmentPollBehindServer, ConduitClient, ConduitRequestError } from "./client.js";
|
|
11
|
-
import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection,
|
|
12
|
+
import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, saveConfig, saveConfigPrefs, saveDriverQuota, savePendingConnection, suggestMachineName, } from "./config.js";
|
|
12
13
|
import { runMcp } from "./mcp.js";
|
|
13
14
|
import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from "./detect.js";
|
|
14
15
|
import { DRIVERS, MODEL_TIERS } from "./driver.js";
|
package/dist/config.js
CHANGED
|
@@ -77,8 +77,10 @@ export async function loadConfigIfPresent() {
|
|
|
77
77
|
// → config — so a new lane field has to be added in both, and forgetting here is silent: the
|
|
78
78
|
// value survives in memory, then vanishes on the next load. `quota` was lost exactly that way.
|
|
79
79
|
const quota = plainQuota(lane.quota);
|
|
80
|
+
const outcome = plainOutcome(lane.outcome);
|
|
80
81
|
const models = plainTierModels(lane.models, `drivers.${id}.models`);
|
|
81
82
|
cleaned[id] = { state, ...(modelsPolicy ? { models_policy: modelsPolicy } : {}), ...(fuel ? { fuel } : {}), ...(quota ? { quota } : {}),
|
|
83
|
+
...(outcome ? { outcome } : {}),
|
|
82
84
|
...(models && Object.keys(models).length ? { models } : {}) };
|
|
83
85
|
}
|
|
84
86
|
config.drivers = cleaned;
|
|
@@ -136,6 +138,14 @@ function plainQuota(raw) {
|
|
|
136
138
|
const vendor = typeof raw.vendor === "string" && raw.vendor.trim() ? raw.vendor : undefined;
|
|
137
139
|
return { exhausted: raw.exhausted, resets_at: raw.resets_at, observed_at: raw.observed_at, ...(vendor ? { vendor } : {}) };
|
|
138
140
|
}
|
|
141
|
+
/** The lane's timeout record. Dropped here, the count never passed 1 and a lane that kept timing out was never demoted. */
|
|
142
|
+
function plainOutcome(raw) {
|
|
143
|
+
if (!raw || typeof raw !== "object")
|
|
144
|
+
return undefined;
|
|
145
|
+
if (!Number.isFinite(raw.consecutive_timeouts) || typeof raw.last_timeout_at !== "string")
|
|
146
|
+
return undefined;
|
|
147
|
+
return { consecutive_timeouts: Math.max(0, Math.floor(raw.consecutive_timeouts)), last_timeout_at: raw.last_timeout_at };
|
|
148
|
+
}
|
|
139
149
|
async function loadRuntime() {
|
|
140
150
|
try {
|
|
141
151
|
const raw = JSON.parse(await readFile(runtimePath, "utf8"));
|
|
@@ -224,9 +234,10 @@ function mergeCurrentDriverQuota(disk, snapshot) {
|
|
|
224
234
|
return snapshot;
|
|
225
235
|
const drivers = {};
|
|
226
236
|
for (const [id, lane] of Object.entries(snapshot)) {
|
|
227
|
-
const { quota: _staleQuota, ...prefs } = lane;
|
|
237
|
+
const { quota: _staleQuota, outcome: _staleOutcome, ...prefs } = lane;
|
|
228
238
|
const quota = plainQuota(disk.drivers?.[id]?.quota);
|
|
229
|
-
drivers[id]
|
|
239
|
+
const outcome = plainOutcome(disk.drivers?.[id]?.outcome);
|
|
240
|
+
drivers[id] = { ...prefs, ...(quota ? { quota } : {}), ...(outcome ? { outcome } : {}) };
|
|
230
241
|
}
|
|
231
242
|
return drivers;
|
|
232
243
|
}
|
|
@@ -409,15 +420,3 @@ export function suggestMachineName(host, installationId) {
|
|
|
409
420
|
const suffix = installationId.slice(-5).toLowerCase();
|
|
410
421
|
return `${base.slice(0, 199 - suffix.length)}-${suffix}`;
|
|
411
422
|
}
|
|
412
|
-
export function redactSecrets(value) {
|
|
413
|
-
// Conduit's own key classes, then the provider and forge shapes an agent's own environment
|
|
414
|
-
// carries (ANTHROPIC_API_KEY, XAI_API_KEY, GEMINI_API_KEY, a GitHub token, a bearer header) —
|
|
415
|
-
// an agent that prints its environment on failure must not leak them through this terminal.
|
|
416
|
-
return value
|
|
417
|
-
.replace(/(runner_sk_|gateway_sk_|lease_|connect_)[A-Za-z0-9-]+/g, "$1[redacted]")
|
|
418
|
-
.replace(/sk-[A-Za-z0-9_-]{8,}/g, "sk-[redacted]")
|
|
419
|
-
.replace(/xai-[A-Za-z0-9]+/g, "xai-[redacted]")
|
|
420
|
-
.replace(/AIza[0-9A-Za-z_-]{35}/g, "AIza[redacted]")
|
|
421
|
-
.replace(/(ghp_|gho_|github_pat_|glpat-)[A-Za-z0-9_-]+/g, "$1[redacted]")
|
|
422
|
-
.replace(/Bearer\s+\S+/g, "Bearer [redacted]");
|
|
423
|
-
}
|
package/dist/execution.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { redactSecrets, redactSecretsDeep } from "./secret-redaction.js";
|
|
1
3
|
import { spawn } from "node:child_process";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
3
5
|
import { z } from "zod";
|
|
@@ -5,7 +7,7 @@ import { resolveAgentTimeout } from "./execution-budget.js";
|
|
|
5
7
|
import { AttemptStalledError, createAttemptWatchdog, withTimeout } from "./attempt-watchdog.js";
|
|
6
8
|
import { workPackageSchema } from "./work-package-schema.js";
|
|
7
9
|
import { ConduitRequestError, reportAssignmentPollShapeMismatch } from "./client.js";
|
|
8
|
-
import {
|
|
10
|
+
import { saveDriverOutcome, saveDriverQuota, tierCandidates } from "./config.js";
|
|
9
11
|
import { DRIVERS, acceptanceCriteriaLines, acceptanceReportSkeleton, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, isSafeModelName, driverExecutable, requireStampedExecutionClass, tierForRisk } from "./driver.js";
|
|
10
12
|
import { assertClassFloor } from "./execution-class.js";
|
|
11
13
|
import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, resolveDriverFuelProvenance, supportsReadOnlyDiagnosis, clearDriverOutcome, recordDriverTimeout } from "./drivers.js";
|
|
@@ -184,8 +186,22 @@ function timeoutFailureBody(resolution, attemptId) {
|
|
|
184
186
|
}
|
|
185
187
|
class AgentTurnTimeoutError extends Error {
|
|
186
188
|
}
|
|
187
|
-
/**
|
|
188
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Computer faults found while finishing a run. Each Holds on its own named cause: without an
|
|
191
|
+
* envelope the control plane read them as unclassifiable and stopped the task instead (K3/K4).
|
|
192
|
+
* Order matters: the first match names the cause.
|
|
193
|
+
*/
|
|
194
|
+
const FINALIZE_ENVIRONMENT_CAUSES = [
|
|
195
|
+
[/base[_ ]not[_ ]ancestor|required base commit is not available/i, "base_not_ancestor"],
|
|
196
|
+
[/source[_ ]workspace[_ ]dirty|uncommitted changes|dirty workspace/i, "workspace_dirty"],
|
|
197
|
+
[/workspace[_ ]head[_ ]changed/i, "workspace_head_changed"],
|
|
198
|
+
[/workspace[_ ]repository/i, "workspace_repository_mismatch"],
|
|
199
|
+
[/workspace[_ ]unavailable/i, "workspace_unavailable"],
|
|
200
|
+
[/driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required/i, "driver_not_authenticated"],
|
|
201
|
+
[/no[_ ]online[_ ]driver/i, "no_online_driver"],
|
|
202
|
+
[/bridge[_ ]preflight/i, "bridge_preflight_missing"],
|
|
203
|
+
[/stale bridge|bridge version/i, "stale_bridge"],
|
|
204
|
+
];
|
|
189
205
|
/** Delivery-report / grant / evidence contract defects (non-retryable rework). */
|
|
190
206
|
/** Two finalize messages with builders of their own in the twin and the cause table. */
|
|
191
207
|
const CONTRACT_SCOPE_CONFLICT_MESSAGE = /Agent reported changes outside the approved scope:/i;
|
|
@@ -204,8 +220,9 @@ export function classifyFinalizeFailure(message) {
|
|
|
204
220
|
// the owner reads the refusal rather than that Conduit could not classify the failure.
|
|
205
221
|
if (message.startsWith(PULL_REQUEST_OPEN_FAILURE_PREFIX))
|
|
206
222
|
return { retryable: false, error: message, failure: bridgeCause("pull_request_open_refused", message) };
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
const environment = FINALIZE_ENVIRONMENT_CAUSES.find(([pattern]) => pattern.test(message))?.[1];
|
|
224
|
+
if (environment)
|
|
225
|
+
return { retryable: false, error: message, failure: bridgeCause(environment, message) };
|
|
209
226
|
// Bridge names the contract defect it found (K3); an unknown throw below stays prose until its
|
|
210
227
|
// own site names a code.
|
|
211
228
|
if (FINALIZE_CONTRACT_PATTERN.test(message)) {
|
|
@@ -755,7 +772,8 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
|
|
|
755
772
|
console.error(`Slot recovery failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
756
773
|
})
|
|
757
774
|
.then(() => undefined)
|
|
758
|
-
.finally(() => { running.
|
|
775
|
+
.finally(() => { if (running.get(id) === slot)
|
|
776
|
+
running.delete(id); });
|
|
759
777
|
running.set(id, slot);
|
|
760
778
|
}
|
|
761
779
|
while (running.size < config.leaseCapacity) {
|
|
@@ -790,7 +808,8 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
|
|
|
790
808
|
console.error(`Slot execution failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
791
809
|
})
|
|
792
810
|
.then(() => undefined)
|
|
793
|
-
.finally(() => { running.
|
|
811
|
+
.finally(() => { if (running.get(taskId) === slot)
|
|
812
|
+
running.delete(taskId); });
|
|
794
813
|
running.set(taskId, slot);
|
|
795
814
|
}
|
|
796
815
|
return progressed;
|
|
@@ -955,6 +974,13 @@ export async function executeNextAssignment(client, config, driver, workspace, b
|
|
|
955
974
|
* hung: keep it instead of clearing it as an ordinary finished attempt.
|
|
956
975
|
*/
|
|
957
976
|
const stalledAttempts = new Set();
|
|
977
|
+
/**
|
|
978
|
+
* The attempt the current run serves. The maps above are keyed by task, and an abandoned run (after
|
|
979
|
+
* a stall or a lost lease) keeps running while the task's next attempt starts on this machine — a
|
|
980
|
+
* repair diagnosis always does. Without this, the old run reported with the new attempt's lease and
|
|
981
|
+
* removed the new run's handles.
|
|
982
|
+
*/
|
|
983
|
+
const runningAttempt = new AsyncLocalStorage();
|
|
958
984
|
/** T90: the timers and the sleep guard of a running attempt, for a stall report to stop. */
|
|
959
985
|
const attemptDisposers = new Map();
|
|
960
986
|
/** T90: the watchdog of a running attempt, so any terminal path can name the phase it ends in. */
|
|
@@ -973,7 +999,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
973
999
|
turnBudgetMs: resolveAgentTimeout({ machineCeilingMs: timeoutMs }).timeoutMs,
|
|
974
1000
|
});
|
|
975
1001
|
attemptWatchdogs.set(taskId, watchdog);
|
|
976
|
-
const attempt = runAttempt(client, config, driver, workspace, brief, taskId, watchdog, timeoutMs, supervision, options);
|
|
1002
|
+
const attempt = runningAttempt.run(client.attempt(taskId).attemptId, () => runAttempt(client, config, driver, workspace, brief, taskId, watchdog, timeoutMs, supervision, options));
|
|
977
1003
|
try {
|
|
978
1004
|
await Promise.race([attempt, watchdog.stalled]);
|
|
979
1005
|
}
|
|
@@ -2296,9 +2322,12 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
2296
2322
|
// that never answered.
|
|
2297
2323
|
if (stalledAttempts.delete(active.attemptId))
|
|
2298
2324
|
retainAttemptWorktree = true;
|
|
2299
|
-
|
|
2325
|
+
// Only this run's own handles: the task's next attempt may already have set its own.
|
|
2326
|
+
if (attemptDisposers.get(taskId) === disposeTimers)
|
|
2327
|
+
attemptDisposers.delete(taskId);
|
|
2300
2328
|
disposeTimers();
|
|
2301
|
-
slotAborts.
|
|
2329
|
+
if (slotAborts.get(taskId) === abort)
|
|
2330
|
+
slotAborts.delete(taskId);
|
|
2302
2331
|
if (retainAttemptWorktree) {
|
|
2303
2332
|
console.log(`Retained failed-attempt worktree for Conductor diagnosis at ${attemptWorkspace}`);
|
|
2304
2333
|
}
|
|
@@ -3092,11 +3121,19 @@ async function endAttemptForChangedFact(client, workspace, taskId, fact, observe
|
|
|
3092
3121
|
}
|
|
3093
3122
|
console.error(`Assignment ${taskId} ended: ${redactSecrets(detail)}`);
|
|
3094
3123
|
}
|
|
3095
|
-
async function queueTerminal(client, taskId,
|
|
3124
|
+
async function queueTerminal(client, taskId, reported) {
|
|
3125
|
+
// Every terminal of every path passes here: its prose, envelope, signal tails and delivery packet
|
|
3126
|
+
// carry agent and gate output, so the whole report is redacted once, before it is stored or sent.
|
|
3127
|
+
const terminal = redactSecretsDeep(reported);
|
|
3128
|
+
const active = client.attempt(taskId);
|
|
3129
|
+
const owner = runningAttempt.getStore();
|
|
3130
|
+
if (owner && owner !== active.attemptId) {
|
|
3131
|
+
console.error(`Assignment ${taskId}: a run for attempt ${owner} answered after attempt ${active.attemptId} took the task; its report is dropped.`);
|
|
3132
|
+
return { accepted: false, status: "superseded" };
|
|
3133
|
+
}
|
|
3096
3134
|
// Every terminal of every path passes here, so this is where the attempt's clock learns that the
|
|
3097
3135
|
// work is over and only the report is left.
|
|
3098
3136
|
attemptWatchdogs.get(taskId)?.phase("terminal");
|
|
3099
|
-
const active = client.attempt(taskId);
|
|
3100
3137
|
let executionFacts = active.executionFacts;
|
|
3101
3138
|
if (executionFacts && executionFacts.final_commit === null && active.worktreePath) {
|
|
3102
3139
|
const finalCommit = await headCommitOrNull(active.worktreePath);
|
package/dist/investigation.js
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* held to repo_read + test_run by the same driver projection that enforces read-only diagnosis, and
|
|
6
6
|
* the worktree it reads is deleted whichever way the run ends.
|
|
7
7
|
*/
|
|
8
|
+
import { redactSecrets } from "./secret-redaction.js";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import { boundedTail, runBoundedVerificationCommand } from "./ensure-test-evidence.js";
|
|
10
11
|
import { createAttemptWorktree, removeAttemptWorktree } from "./attempt-worktree.js";
|
|
11
12
|
import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl } from "./brief.js";
|
|
12
13
|
import { headCommitOrNull } from "./git.js";
|
|
13
|
-
import { redactSecrets } from "./config.js";
|
|
14
14
|
import { learnDriverFuel, resolveAssignmentModel, changedPathsSince, runDeliveryVerificationGates, discardWorktreeWrites } from "./execution.js";
|
|
15
15
|
import { DRIVERS, extractAgentReportJsonText, parseJsonObjectCandidate } from "./driver.js";
|
|
16
16
|
import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
@@ -52,6 +52,8 @@ export const investigationAssignmentSchema = z.object({
|
|
|
52
52
|
acceptance: z.array(z.string().trim().min(1).max(4_000)).max(20).optional().default([]),
|
|
53
53
|
/** Delivery verification: the brief's `[sN]` / `[kN]` lines. Absent or [] on a plain investigation. */
|
|
54
54
|
brief_keys: z.array(z.string().trim().min(1).max(4_000)).max(40).optional().default([]),
|
|
55
|
+
/** Delivery verification: the owner's returns on this delivery, which amend the brief. */
|
|
56
|
+
owner_returns: z.array(z.string().trim().min(1).max(4_000)).max(10).optional().default([]),
|
|
55
57
|
});
|
|
56
58
|
/**
|
|
57
59
|
* Enforcement is a vendor's claim; verification is ours. Both run on every investigation — this only
|
|
@@ -93,15 +95,20 @@ const investigationBriefSchema = z.object({
|
|
|
93
95
|
source: z.string().max(8_000).optional(),
|
|
94
96
|
checks: z.string().max(500).optional().default(""),
|
|
95
97
|
})).max(20).default([]),
|
|
98
|
+
/** K12: brief keys the lane names as stating no behaviour, so no probe is owed. Passed through, never run. */
|
|
99
|
+
no_behaviour: z.array(z.object({
|
|
100
|
+
key: z.string().trim().min(1).max(40),
|
|
101
|
+
reason: z.string().trim().min(1).max(500),
|
|
102
|
+
})).max(40).default([]),
|
|
96
103
|
});
|
|
97
104
|
/** T122: a delivery verification is an investigation whose question carries this header. */
|
|
98
105
|
export const DELIVERY_VERIFICATION_HEADER = "DELIVERY VERIFICATION";
|
|
99
106
|
/** Owner-verbatim: the lane writes probe source; Bridge executes. Shown only when BRIEF KEYS are present. */
|
|
100
|
-
export const DELIVERY_VERIFICATION_PROBE_INSTRUCTION = 'For every BRIEF KEY that states behaviour, add one entry to probes: either {"key":"[sN]","test":"<registered command that asserts it>","checks":"<one line: what it proves>"} or {"key":"[sN]","lang":"ts|py|sh","source":"<a short script run from the repository root that prints PROBE HOLDS as its last line and exits 0 when the key holds at this commit and prints PROBE FAILS as its last line and exits 1 when it does not; any other ending is recorded as broken; it may import repository modules by paths relative to the repository root>","checks":"<one line>"}. You do not run probes; Bridge runs them after you finish.
|
|
107
|
+
export const DELIVERY_VERIFICATION_PROBE_INSTRUCTION = 'For every BRIEF KEY that states behaviour, add one entry to probes: either {"key":"[sN]","test":"<registered command that asserts it>","checks":"<one line: what it proves>"} or {"key":"[sN]","lang":"ts|py|sh","source":"<a short script run from the repository root that prints PROBE HOLDS as its last line and exits 0 when the key holds at this commit and prints PROBE FAILS as its last line and exits 1 when it does not; any other ending is recorded as broken; it may import repository modules by paths relative to the repository root>","checks":"<one line>"}. You do not run probes; Bridge runs them after you finish. Every other BRIEF KEY states no behaviour (wording, scope, docs): add it to no_behaviour as {"key":"[sN]","reason":"<one line: why it states no behaviour>"}. Name every BRIEF KEY once, in probes or in no_behaviour.';
|
|
101
108
|
const BRIEF_JSON_EXAMPLE = '{"summary":"one paragraph: what the change does and whether it meets the criteria","findings":["grounded fact with file/symbol"],"verification":["command — result"],"limitations":["what you could not settle"],"criteria":[{"criterion":"c1","assessment":"supported|unsupported|unclear","reason":"what you saw"}],"command_failures":["command that exited non-zero — the decisive output line"],"files_read":0,"bytes_read":0,"commands_run":["command you actually ran"]}';
|
|
102
|
-
const BRIEF_JSON_EXAMPLE_WITH_PROBES = '{"summary":"one paragraph: what the change does and whether it meets the criteria","findings":["grounded fact with file/symbol"],"verification":["command — result"],"limitations":["what you could not settle"],"criteria":[{"criterion":"c1","assessment":"supported|unsupported|unclear","reason":"what you saw"}],"command_failures":["command that exited non-zero — the decisive output line"],"probes":[],"files_read":0,"bytes_read":0,"commands_run":["command you actually ran"]}';
|
|
109
|
+
const BRIEF_JSON_EXAMPLE_WITH_PROBES = '{"summary":"one paragraph: what the change does and whether it meets the criteria","findings":["grounded fact with file/symbol"],"verification":["command — result"],"limitations":["what you could not settle"],"criteria":[{"criterion":"c1","assessment":"supported|unsupported|unclear","reason":"what you saw"}],"command_failures":["command that exited non-zero — the decisive output line"],"probes":[],"no_behaviour":[],"files_read":0,"bytes_read":0,"commands_run":["command you actually ran"]}';
|
|
103
110
|
/** Named in both prompts so the lane knows each list's maximum before it writes. */
|
|
104
|
-
export const BRIEF_LIMITS_SENTENCE = "Limits: findings at most 12, verification at most 8, limitations at most 8, criteria at most 40, command_failures at most 10, commands_run at most 10, probes at most 20; each text entry at most 1000 characters, commands_run entries at most 200 characters. Keep the most decisive entries.";
|
|
111
|
+
export const BRIEF_LIMITS_SENTENCE = "Limits: findings at most 12, verification at most 8, limitations at most 8, criteria at most 40, command_failures at most 10, commands_run at most 10, probes at most 20, no_behaviour at most 40; each text entry at most 1000 characters, commands_run entries at most 200 characters. Keep the most decisive entries.";
|
|
105
112
|
export function isDeliveryVerification(assignment) {
|
|
106
113
|
return assignment.question.trimStart().startsWith(DELIVERY_VERIFICATION_HEADER);
|
|
107
114
|
}
|
|
@@ -145,6 +152,10 @@ export function buildDeliveryVerificationPrompt(assignment, commit, context) {
|
|
|
145
152
|
: "REGISTERED COMMANDS\n- none: this repository registers no verification command, so judge from the files only";
|
|
146
153
|
const keys = (assignment.brief_keys ?? []).map((key) => key.trim()).filter(Boolean);
|
|
147
154
|
const briefKeysBlock = keys.length ? `BRIEF KEYS\n${keys.join("\n")}` : "";
|
|
155
|
+
const returns = (assignment.owner_returns ?? []).map((text) => text.trim()).filter(Boolean);
|
|
156
|
+
const ownerReturnsBlock = returns.length
|
|
157
|
+
? `OWNER RETURNS — the product owner's decisions on this delivery, in order. They amend the criteria and the brief: where a return changes a rule, judge and probe the rule as returned, never the original wording.\n${returns.map((text, index) => `${index + 1}. ${text}`).join("\n")}`
|
|
158
|
+
: "";
|
|
148
159
|
const probeBlock = keys.length ? DELIVERY_VERIFICATION_PROBE_INSTRUCTION : "";
|
|
149
160
|
return [
|
|
150
161
|
"You are Conductor's independent delivery verification. A coding agent delivered this commit against approved acceptance criteria; you check it the way a careful reviewer would, and you never take the agent's word for anything.",
|
|
@@ -161,6 +172,7 @@ export function buildDeliveryVerificationPrompt(assignment, commit, context) {
|
|
|
161
172
|
`PINNED COMMIT ${commit}`,
|
|
162
173
|
commands,
|
|
163
174
|
assignment.question,
|
|
175
|
+
...(ownerReturnsBlock ? [ownerReturnsBlock] : []),
|
|
164
176
|
...(briefKeysBlock ? [briefKeysBlock] : []),
|
|
165
177
|
...(probeBlock ? [probeBlock] : []),
|
|
166
178
|
`BUDGET\nAt most ${assignment.budget.max_files} files read in full; the change summary and command output are not counted.`,
|
|
@@ -225,6 +237,7 @@ const BRIEF_LIST_MAX = {
|
|
|
225
237
|
command_failures: 10,
|
|
226
238
|
commands_run: 10,
|
|
227
239
|
probes: 20,
|
|
240
|
+
no_behaviour: 40,
|
|
228
241
|
};
|
|
229
242
|
function clipBriefText(value, max) {
|
|
230
243
|
return value.length > max ? value.slice(0, max) : value;
|
|
@@ -274,6 +287,9 @@ export function fitBriefToContract(candidate) {
|
|
|
274
287
|
fitNamedList("command_failures", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
|
|
275
288
|
fitNamedList("commands_run", (items) => fitBriefStrings(items, BRIEF_COMMAND_RUN_MAX));
|
|
276
289
|
fitNamedList("probes", (items) => items);
|
|
290
|
+
fitNamedList("no_behaviour", (items) => items.map((item) => item && typeof item === "object" && typeof item.reason === "string"
|
|
291
|
+
? { ...item, reason: clipBriefText(item.reason, 500) }
|
|
292
|
+
: item));
|
|
277
293
|
clipBriefStringField(brief, "summary", BRIEF_SUMMARY_MAX);
|
|
278
294
|
if (typeof brief.files_read === "number" && brief.files_read > BRIEF_FILES_READ_MAX) {
|
|
279
295
|
brief.files_read = BRIEF_FILES_READ_MAX;
|
|
@@ -909,6 +925,7 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
909
925
|
? gatesRun.filter((gate) => gate.exitCode !== 0).map(gateFailureFact).slice(0, 10)
|
|
910
926
|
: observed.command_failures,
|
|
911
927
|
step_witness: stepWitness,
|
|
928
|
+
no_behaviour: deliveryVerification ? observed.no_behaviour : [],
|
|
912
929
|
witness: {
|
|
913
930
|
repository_fingerprint: assignment.repository_fingerprint ?? liveRepository ?? "unknown",
|
|
914
931
|
commit,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one list of secret shapes, for the Bridge and the control plane.
|
|
3
|
+
*
|
|
4
|
+
* The Bridge source is packages/conduit-bridge/src/secret-redaction.ts. The control-plane copy at
|
|
5
|
+
* src/conductor/secret-redaction.ts is generated by `npm run sync:twins`. Do not edit that copy.
|
|
6
|
+
* Bridge cannot import from the control plane, and the other way round.
|
|
7
|
+
*
|
|
8
|
+
* Three lists used to exist (Bridge terminal, control-plane logs, forge diffs), and each missed
|
|
9
|
+
* shapes another caught. An agent that prints its environment on failure must not leak a provider,
|
|
10
|
+
* forge, cloud or Conduit credential through any of them.
|
|
11
|
+
*/
|
|
12
|
+
export function redactSecrets(value) {
|
|
13
|
+
return value
|
|
14
|
+
.replace(/-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g, "[redacted private key]")
|
|
15
|
+
.replace(/(authorization\s*:\s*bearer\s+)[^\s]+/gi, "$1[redacted]")
|
|
16
|
+
.replace(/Bearer\s+\S+/g, "Bearer [redacted]")
|
|
17
|
+
.replace(/((?:api[_-]?key|access[_-]?token|client[_-]?secret|password|secret)\s*[:=]\s*)["']?[^\s"']+/gi, "$1[redacted]")
|
|
18
|
+
.replace(/(runner_sk_|gateway_sk_|lease_|connect_)[A-Za-z0-9-]+/g, "$1[redacted]")
|
|
19
|
+
.replace(/sk-[A-Za-z0-9_-]{8,}/g, "sk-[redacted]")
|
|
20
|
+
.replace(/xai-[A-Za-z0-9]+/g, "xai-[redacted]")
|
|
21
|
+
.replace(/AIza[0-9A-Za-z_-]{35}/g, "AIza[redacted]")
|
|
22
|
+
.replace(/(gh[pousr]_|github_pat_|glpat-|xox[baprs]-)[A-Za-z0-9_-]+/g, "$1[redacted]")
|
|
23
|
+
.replace(/AKIA[0-9A-Z]{16}/g, "AKIA[redacted]");
|
|
24
|
+
}
|
|
25
|
+
/** Every string inside a JSON-shaped value, redacted; the shape is kept. */
|
|
26
|
+
export function redactSecretsDeep(value) {
|
|
27
|
+
if (typeof value === "string")
|
|
28
|
+
return redactSecrets(value);
|
|
29
|
+
if (Array.isArray(value))
|
|
30
|
+
return value.map((item) => redactSecretsDeep(item));
|
|
31
|
+
if (value && typeof value === "object") {
|
|
32
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactSecretsDeep(item)]));
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.141",
|
|
4
4
|
"description": "Conduit Bridge CLI \u2014 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": {
|