@miraland-labs/conduit-bridge 0.16.32 → 0.16.38
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/driver.js +233 -14
- package/dist/execution.js +62 -4
- package/package.json +1 -1
package/dist/driver.js
CHANGED
|
@@ -3,9 +3,9 @@ import { boundedTail } from "./ensure-test-evidence.js";
|
|
|
3
3
|
import { resolveAgentTimeout } from "./execution-budget.js";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectGrok, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
|
|
8
|
+
import { allowsExternalFetch, deniedCommands, executionClassPromptRules, isRunnableVerificationCommand, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectGrok, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
|
|
9
9
|
/**
|
|
10
10
|
* The bound for this turn.
|
|
11
11
|
*
|
|
@@ -995,10 +995,16 @@ export const kiroDriver = {
|
|
|
995
995
|
},
|
|
996
996
|
};
|
|
997
997
|
/**
|
|
998
|
-
* Antigravity's `agy` CLI (verified against agy 1.1.
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
998
|
+
* Antigravity's `agy` CLI (verified against agy 1.1.24): non-interactive runs use
|
|
999
|
+
* `--mode plan` (read-only) or `--mode accept-edits`. `-p`/`--print` is a Go-style
|
|
1000
|
+
* flag that consumes its value from the next argument, so the prompt must be the
|
|
1001
|
+
* last argument attached to `-p` — putting other flags after it (as earlier builds
|
|
1002
|
+
* of this driver did) makes agy swallow one of them as the prompt and silently
|
|
1003
|
+
* drop the real prompt. `--print-timeout` is set from the turn budget so agy's
|
|
1004
|
+
* 5-minute default cannot cut a longer turn short. Its enforcement is mode-level,
|
|
1005
|
+
* not per-tool — coarser than Claude, the Cursor tier. Write mode adds `--sandbox`
|
|
1006
|
+
* for terminal restrictions. `--output-format json` returns one envelope, parsed by
|
|
1007
|
+
* parseAntigravityOutput.
|
|
1002
1008
|
*/
|
|
1003
1009
|
export function antigravityModeForGrants(grants, executionClass) {
|
|
1004
1010
|
if (!hasMappedRepoAccess(grants))
|
|
@@ -1011,15 +1017,207 @@ export function antigravityModeForGrants(grants, executionClass) {
|
|
|
1011
1017
|
return "plan";
|
|
1012
1018
|
return null;
|
|
1013
1019
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1020
|
+
/** agy's `--print-timeout` takes a Go duration; ceil to whole minutes (agy's own default floor is 5m). */
|
|
1021
|
+
export function antigravityPrintTimeout(timeoutMs) {
|
|
1022
|
+
return `${Math.max(5, Math.ceil(timeoutMs / 60_000))}m0s`;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Class + grants → agy permission allow-rules (probed against agy 1.1.24).
|
|
1026
|
+
*
|
|
1027
|
+
* Headless `agy -p` soft-denies every tool it was not granted: the run returns an empty response
|
|
1028
|
+
* with a stderr notice and nothing is written. Only wildcard rules ever matched in the probes —
|
|
1029
|
+
* bounded ones (`command(git add)`, `command(git *)`, and in the field `unsandboxed(git push)`)
|
|
1030
|
+
* were auto-denied, because the agent picks its own shell spellings and stops at the first denial.
|
|
1031
|
+
* So the enforceable unit is the coarse rule plus `--sandbox`, the same boundary the Codex driver
|
|
1032
|
+
* leans on with `--sandbox workspace-write`. The grammar agy accepts is `command(<cmd>)`,
|
|
1033
|
+
* `unsandboxed(<cmd>)`, `write_file(<glob>)`, `read_file(<glob>)`, `read_url(<target>)`.
|
|
1034
|
+
*
|
|
1035
|
+
* `deniedCommands` cannot be expressed here: agy has no deny grammar. Antigravity is the coarse
|
|
1036
|
+
* tier the product spec already assigns it — enforcement is the sandbox, the prompt rule that names
|
|
1037
|
+
* the hard-denied commands (executionClassPromptRules), and the delivery gate.
|
|
1038
|
+
*/
|
|
1039
|
+
export function antigravityPermissionProjection(executionClass, grants, options = {}) {
|
|
1040
|
+
const allow = ["read_file(*)"];
|
|
1041
|
+
// Live HTTP is the assignment's capability, read the one way every driver reads it. agy aborts the
|
|
1042
|
+
// whole turn on a denial rather than skipping the tool, so an assignment that may fetch must carry
|
|
1043
|
+
// the rule; one that may not never gets it.
|
|
1044
|
+
if (allowsExternalFetch(executionClass, options.capabilities))
|
|
1045
|
+
allow.push("read_url(*)");
|
|
1046
|
+
const runnable = (options.verificationCommands ?? []).some(isRunnableVerificationCommand);
|
|
1047
|
+
if (executionClass === "observe" || executionClass === "observe_network" || options.diagnosis) {
|
|
1048
|
+
// Diagnosis has to run its bounded checks; a plain observe class gets no shell at all.
|
|
1049
|
+
if (runnable && (options.diagnosis || grants.includes("test_run")))
|
|
1050
|
+
allow.push("command(*)");
|
|
1051
|
+
return allow;
|
|
1052
|
+
}
|
|
1053
|
+
if (executionClass === "verify") {
|
|
1054
|
+
allow.push("command(*)");
|
|
1055
|
+
return allow;
|
|
1056
|
+
}
|
|
1057
|
+
allow.push("write_file(*)", "command(*)");
|
|
1058
|
+
// Land work needs the sandbox escape hatch, and only a wildcard reaches it: a real mutate_repo
|
|
1059
|
+
// assignment carrying `pr_create` was auto-denied on exact `unsandboxed(git push)` with
|
|
1060
|
+
// "a tool required the \"unsandboxed\" permission that headless mode cannot prompt for". So for
|
|
1061
|
+
// the land class the sandbox boundary no longer covers the escape hatch, and enforcement there is
|
|
1062
|
+
// the prompt's hard-deny list, the delivery gate (one PR, a scoped diff, Bridge-witnessed
|
|
1063
|
+
// commands) and the project's grants — the coarse tier. Without `pr_create` no `unsandboxed(...)`
|
|
1064
|
+
// rule is emitted at all, and `--sandbox` stays on either way.
|
|
1065
|
+
if (grants.includes("pr_create"))
|
|
1066
|
+
allow.push("unsandboxed(*)");
|
|
1067
|
+
return allow;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* The one driver note agy needs beside the class rules. It aborts the whole turn on any denied tool
|
|
1071
|
+
* — Claude Code merely skips one — and a run that had already committed died opening its own PR URL
|
|
1072
|
+
* with `read_url`, which no assignment without external_network may hold.
|
|
1073
|
+
*/
|
|
1074
|
+
export const antigravityPromptRule = "Antigravity: do not use read_url, browser, or web tools unless this assignment lists external_network; do not open the pull request URL after creating it — report the URL from the `gh pr create` output. A denied tool ends the whole run, so use only the tools this assignment allows.";
|
|
1075
|
+
export function antigravityRunArgs(input, mode, timeoutMs) {
|
|
1076
|
+
const args = ["--mode", mode];
|
|
1016
1077
|
if (mode === "accept-edits")
|
|
1017
1078
|
args.push("--sandbox");
|
|
1018
|
-
args.push(
|
|
1079
|
+
args.push("--output-format", "json");
|
|
1080
|
+
args.push("--print-timeout", antigravityPrintTimeout(timeoutMs));
|
|
1081
|
+
// agy's CLI default model is one account lane among several; `--model` selects the lane this
|
|
1082
|
+
// machine's tier mapping names, which is how a spent Gemini window stops the whole driver.
|
|
1083
|
+
if (input.model)
|
|
1084
|
+
args.push("--model", input.model);
|
|
1085
|
+
if (input.resumeSessionId)
|
|
1086
|
+
args.push("--conversation", input.resumeSessionId);
|
|
1087
|
+
args.push("-p", input.prompt);
|
|
1019
1088
|
return args;
|
|
1020
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Parse `agy models`: one model per line, the id first and the display name after it
|
|
1092
|
+
* ("claude-sonnet-4-6 Claude Sonnet 4.6 (Thinking)"). Two or more spaces separate the two
|
|
1093
|
+
* columns, which is what tells a model line from a banner line ("Available models:").
|
|
1094
|
+
*/
|
|
1095
|
+
export function parseAntigravityModelList(stdout) {
|
|
1096
|
+
const models = new Set();
|
|
1097
|
+
for (const line of stdout.split("\n")) {
|
|
1098
|
+
// A terminal shows the two columns padded with spaces; a pipe gets one tab between them.
|
|
1099
|
+
const match = /^\s*([A-Za-z0-9][A-Za-z0-9._/-]*)(?:\t+| {2,})\S/.exec(line);
|
|
1100
|
+
if (match)
|
|
1101
|
+
models.add(match[1]);
|
|
1102
|
+
}
|
|
1103
|
+
return models;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* `agy --output-format json` prints one envelope:
|
|
1107
|
+
* `{"conversation_id","status","response","duration_seconds","num_turns","usage"}`.
|
|
1108
|
+
* Tolerant like the Cursor parser: a build that answers in plain text still delivers its report.
|
|
1109
|
+
*
|
|
1110
|
+
* A refused run keeps the same envelope with `status: "ERROR"`, an empty `response`, and the reason
|
|
1111
|
+
* in `error` ("Individual quota reached ... Resets in 165h48m43s."), and prints **nothing** on
|
|
1112
|
+
* stderr. Reading only `response` reported that run as "agy exited with code 1", which says nothing
|
|
1113
|
+
* the owner can act on — so `status` and `error` are parsed here and named by the driver.
|
|
1114
|
+
*/
|
|
1115
|
+
export function parseAntigravityOutput(stdout) {
|
|
1116
|
+
try {
|
|
1117
|
+
const parsed = JSON.parse(stdout.trim());
|
|
1118
|
+
return {
|
|
1119
|
+
resultText: typeof parsed.response === "string" ? parsed.response : null,
|
|
1120
|
+
sessionId: typeof parsed.conversation_id === "string" ? parsed.conversation_id : null,
|
|
1121
|
+
status: typeof parsed.status === "string" ? parsed.status : null,
|
|
1122
|
+
error: typeof parsed.error === "string" && parsed.error.trim() ? parsed.error : null,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
return { resultText: stdout || null, sessionId: null, status: null, error: null };
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* The stderr line agy prints when a permission it was not granted ended the turn. An empty response
|
|
1131
|
+
* beside such a line is a denial, not an agent with nothing to say, and the control plane has to see
|
|
1132
|
+
* the difference.
|
|
1133
|
+
*/
|
|
1134
|
+
export function antigravityDenialNotice(stderr) {
|
|
1135
|
+
for (const line of stderr.split("\n")) {
|
|
1136
|
+
const trimmed = line.trim();
|
|
1137
|
+
if (trimmed && /no output produced|permission/i.test(trimmed))
|
|
1138
|
+
return trimmed;
|
|
1139
|
+
}
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
/** agy reads one global settings file, so concurrent runs are serialized rather than racing on it. */
|
|
1143
|
+
let antigravitySettingsLock = Promise.resolve();
|
|
1144
|
+
/**
|
|
1145
|
+
* Write the run's permission contract to agy's global settings file, restoring it byte-exactly after.
|
|
1146
|
+
*
|
|
1147
|
+
* Probed against agy 1.1.24: this is the only placement a headless run honours — a workspace-local
|
|
1148
|
+
* `.antigravity/settings.json` and `~/.gemini/config/projects/<uuid>.json` `permissionGrants` were
|
|
1149
|
+
* both ignored, and without `trustedWorkspaces` naming the workspace every tool is soft-denied. The
|
|
1150
|
+
* file belongs to the machine owner and is global, so the original bytes are held in memory and
|
|
1151
|
+
* beside it as `settings.json.conduit-backup` for the length of the run, unparsable JSON fails the
|
|
1152
|
+
* run instead of being overwritten, and every other key is preserved.
|
|
1153
|
+
*/
|
|
1154
|
+
async function withAntigravityPermissions(workspace, allow, run) {
|
|
1155
|
+
const previousRun = antigravitySettingsLock;
|
|
1156
|
+
let release = () => undefined;
|
|
1157
|
+
antigravitySettingsLock = new Promise((settle) => { release = settle; });
|
|
1158
|
+
await previousRun.catch(() => undefined);
|
|
1159
|
+
try {
|
|
1160
|
+
const home = process.env.HOME;
|
|
1161
|
+
if (!home)
|
|
1162
|
+
throw new Error("antigravity needs HOME to write ~/.gemini/antigravity-cli/settings.json");
|
|
1163
|
+
const settingsDir = join(home, ".gemini", "antigravity-cli");
|
|
1164
|
+
const settingsPath = join(settingsDir, "settings.json");
|
|
1165
|
+
const backupPath = `${settingsPath}.conduit-backup`;
|
|
1166
|
+
const settingsDirExisted = existsSync(settingsDir);
|
|
1167
|
+
let previous = null;
|
|
1168
|
+
try {
|
|
1169
|
+
previous = await readFile(settingsPath, "utf8");
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
previous = null;
|
|
1173
|
+
}
|
|
1174
|
+
let settings = {};
|
|
1175
|
+
if (previous !== null) {
|
|
1176
|
+
try {
|
|
1177
|
+
settings = { ...JSON.parse(previous) };
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
throw new Error(`antigravity settings at ${settingsPath} are not valid JSON; refusing to overwrite them`);
|
|
1181
|
+
}
|
|
1182
|
+
await writeFile(backupPath, previous, "utf8");
|
|
1183
|
+
}
|
|
1184
|
+
const existingPermissions = (settings.permissions && typeof settings.permissions === "object" && !Array.isArray(settings.permissions))
|
|
1185
|
+
? { ...settings.permissions }
|
|
1186
|
+
: {};
|
|
1187
|
+
const trusted = Array.isArray(settings.trustedWorkspaces)
|
|
1188
|
+
? settings.trustedWorkspaces.filter((entry) => typeof entry === "string")
|
|
1189
|
+
: [];
|
|
1190
|
+
const absolute = resolve(workspace);
|
|
1191
|
+
settings.permissions = { ...existingPermissions, allow };
|
|
1192
|
+
settings.trustedWorkspaces = trusted.includes(absolute) ? trusted : [...trusted, absolute];
|
|
1193
|
+
await mkdir(settingsDir, { recursive: true });
|
|
1194
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
1195
|
+
try {
|
|
1196
|
+
return await run();
|
|
1197
|
+
}
|
|
1198
|
+
finally {
|
|
1199
|
+
if (previous === null)
|
|
1200
|
+
await rm(settingsPath, { force: true });
|
|
1201
|
+
else
|
|
1202
|
+
await writeFile(settingsPath, previous, "utf8");
|
|
1203
|
+
await rm(backupPath, { force: true });
|
|
1204
|
+
if (!settingsDirExisted)
|
|
1205
|
+
await rmdir(settingsDir).catch(() => undefined);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
finally {
|
|
1209
|
+
release();
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1021
1212
|
export const antigravityDriver = {
|
|
1022
1213
|
name: "antigravity",
|
|
1214
|
+
async listModels(executable = "agy", workspace = process.cwd()) {
|
|
1215
|
+
const result = await execute(executable, ["models"], workspace, 30_000, undefined, "local");
|
|
1216
|
+
if (result.code !== 0)
|
|
1217
|
+
return null;
|
|
1218
|
+
const models = parseAntigravityModelList(result.stdout);
|
|
1219
|
+
return models.size ? models : null;
|
|
1220
|
+
},
|
|
1023
1221
|
async run(input) {
|
|
1024
1222
|
const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
|
|
1025
1223
|
// Antigravity authenticates with a Google login / GEMINI_API_KEY — not Conduit /v1 shims.
|
|
@@ -1034,12 +1232,33 @@ export const antigravityDriver = {
|
|
|
1034
1232
|
if (!mode) {
|
|
1035
1233
|
return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped Antigravity mode for active grants; refusing to start agent" };
|
|
1036
1234
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1235
|
+
const allow = antigravityPermissionProjection(executionClass, input.grants, {
|
|
1236
|
+
verificationCommands: input.verificationCommands,
|
|
1237
|
+
capabilities: input.capabilities ?? [],
|
|
1238
|
+
diagnosis: input.workRole === "diagnose",
|
|
1239
|
+
});
|
|
1240
|
+
const timeoutMs = agentTurnTimeoutMs(input);
|
|
1241
|
+
let executed;
|
|
1242
|
+
try {
|
|
1243
|
+
executed = await withAntigravityPermissions(input.workspace, allow, () => execute(input.executable ?? "agy", antigravityRunArgs({ prompt: `${input.prompt}\n\n${antigravityPromptRule}`, grants: input.grants, resumeSessionId: input.resumeSessionId, model: input.model }, mode, timeoutMs), input.workspace, timeoutMs, undefined, "local"));
|
|
1041
1244
|
}
|
|
1042
|
-
|
|
1245
|
+
catch (error) {
|
|
1246
|
+
return { status: "failed", resultText: null, sessionId: null, error: error instanceof Error ? error.message : String(error) };
|
|
1247
|
+
}
|
|
1248
|
+
const { code, stdout, stderr } = executed;
|
|
1249
|
+
const parsed = parseAntigravityOutput(stdout);
|
|
1250
|
+
// A refused run says so in the envelope, not on stderr. Take the envelope's own reason first —
|
|
1251
|
+
// it is the only text that names the cause (a spent allowance and when it refills).
|
|
1252
|
+
const refused = code !== 0 || (parsed.status !== null && parsed.status.toUpperCase() !== "SUCCESS");
|
|
1253
|
+
if (refused) {
|
|
1254
|
+
return { status: "failed", resultText: parsed.resultText, sessionId: parsed.sessionId, error: boundedTail(parsed.error || stderr || parsed.resultText || `agy exited with code ${code}`, 20_000) };
|
|
1255
|
+
}
|
|
1256
|
+
// An empty response with a denial on stderr is a refused run: report it instead of an empty reply.
|
|
1257
|
+
const denial = parsed.resultText?.trim() ? null : antigravityDenialNotice(stderr);
|
|
1258
|
+
if (denial) {
|
|
1259
|
+
return { status: "failed", resultText: parsed.resultText, sessionId: parsed.sessionId, error: boundedTail(`agy returned no response: ${denial}`, 20_000) };
|
|
1260
|
+
}
|
|
1261
|
+
return { status: "completed", resultText: parsed.resultText, sessionId: parsed.sessionId };
|
|
1043
1262
|
},
|
|
1044
1263
|
};
|
|
1045
1264
|
/**
|
package/dist/execution.js
CHANGED
|
@@ -74,6 +74,40 @@ function changesRequestedFeedback(summary) {
|
|
|
74
74
|
* failure, which is exactly today's behaviour.
|
|
75
75
|
*/
|
|
76
76
|
export const SUBSCRIPTION_EXHAUSTED_PATTERN = /\b(?:usage|rate|session)[ _-]?limit(?:ed|s)?\b|\b(?:daily|weekly|monthly) limit\b|\bquota (?:exceeded|exhausted|reached)\b|\b(?:http|status|code)\W{0,3}429\b|\b429\b(?=\W{0,3}too many)|too many requests|out of (?:credits|usage)/i;
|
|
77
|
+
/**
|
|
78
|
+
* A vendor naming the account's own allowance as the reason it refused this run.
|
|
79
|
+
*
|
|
80
|
+
* Narrower and louder than SUBSCRIPTION_EXHAUSTED_PATTERN, which decides whether to dark a lane:
|
|
81
|
+
* this one decides what the owner is told. agy 1.1.24 returns `{"status":"ERROR","error":
|
|
82
|
+
* "Individual quota reached. Please upgrade your subscription ... Resets in 165h48m43s."}` with an
|
|
83
|
+
* empty stderr, which had no pattern at all and retried three times in a minute while the owner was
|
|
84
|
+
* told nothing. A spent allowance is a wait, never a retry.
|
|
85
|
+
*
|
|
86
|
+
* Kept identical to the control-plane copy in src/conductor/failures.ts, pinned by
|
|
87
|
+
* test/failure-classification.test.ts — the two ship on different clocks.
|
|
88
|
+
*/
|
|
89
|
+
export const PROVIDER_QUOTA_PATTERN = /individual quota reached|quota reached|upgrade your subscription|resets in \d/i;
|
|
90
|
+
export function providerQuotaRefusal(message) {
|
|
91
|
+
return PROVIDER_QUOTA_PATTERN.test(message);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* What the owner sees when the account's allowance, not Conduit, ended the run. The reset time is
|
|
95
|
+
* quoted from the vendor's own text when it gave one; nothing is invented when it did not.
|
|
96
|
+
*/
|
|
97
|
+
export function providerQuotaFailure(detail) {
|
|
98
|
+
const resets = /resets? in ([^.\n]{1,40})/i.exec(detail);
|
|
99
|
+
return {
|
|
100
|
+
code: "driver_quota_exhausted",
|
|
101
|
+
class: "environment",
|
|
102
|
+
disposition: "hold",
|
|
103
|
+
responsible_party: "computer_operator",
|
|
104
|
+
message: resets
|
|
105
|
+
? `The agent lane on this computer has spent its subscription allowance. It refills in ${resets[1].trim()}.`
|
|
106
|
+
: "The agent lane on this computer has spent its subscription allowance.",
|
|
107
|
+
next_action: "Wait for the allowance to refill, bring another agent lane online to start sooner, or point this lane's model settings at a model that still has allowance.",
|
|
108
|
+
diagnostic_detail: detail,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
77
111
|
/**
|
|
78
112
|
* Does this failure mean the plan is spent, rather than the prompt being wrong?
|
|
79
113
|
*
|
|
@@ -271,10 +305,20 @@ export function observeDriverFuel(config, driverId, fuelSource, result, now = Da
|
|
|
271
305
|
}
|
|
272
306
|
/** Failures that require an operator/configuration change must never burn the remaining attempts. */
|
|
273
307
|
export function retryableAgentFailure(message) {
|
|
308
|
+
// A spent allowance is the account's window, not a flaky run: every retry inside it fails the
|
|
309
|
+
// same way and burns the attempt budget while the owner is told nothing.
|
|
310
|
+
if (providerQuotaRefusal(message))
|
|
311
|
+
return false;
|
|
274
312
|
// `cannot enforce` is a driver projection refusing a class it structurally cannot bound (Pi and
|
|
275
313
|
// observe_network). No number of retries on that lane changes it, and retrying an immutable input
|
|
276
314
|
// is how one assignment burned ~80 attempts. Fail closed; the message names the drivers that work.
|
|
277
|
-
|
|
315
|
+
// `took "..." as its prompt` / `Usage of agy` is agy's Go-style flag-parsing refusal (e.g. a
|
|
316
|
+
// driver flag ordering bug feeding it the wrong prompt argument) — an identical arg list fails
|
|
317
|
+
// identically on every retry.
|
|
318
|
+
// `headless mode cannot prompt for` / `was auto-denied` is a soft denial: the CLI refused a tool
|
|
319
|
+
// the run's permission rules did not match. The same rules produce the same denial next time, so
|
|
320
|
+
// it is a driver/permission defect to surface, never a transient to burn attempts on.
|
|
321
|
+
return !/unknown (?:option|argument)|unrecognized (?:option|argument)|not logged in|no login|not authenticated|login required|\bENOENT\b|could not verify the installed CLI|requires local fuel|No Bridge-mapped|cannot enforce|preflight|took "[^"]*" as its prompt|Usage of agy|headless mode cannot prompt for|was auto-denied/i.test(message);
|
|
278
322
|
}
|
|
279
323
|
const assignmentSchema = z.object({
|
|
280
324
|
id: z.string().uuid(),
|
|
@@ -1246,7 +1290,12 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
1246
1290
|
// Keep the tree until the replay-safe terminal response explicitly says whether diagnosis was
|
|
1247
1291
|
// queued. A network fault here must not delete the evidence before Bridge can replay terminal.
|
|
1248
1292
|
retainAttemptWorktree = true;
|
|
1249
|
-
const response = await queueTerminal(client, taskId, { action: "fail", body: {
|
|
1293
|
+
const response = await queueTerminal(client, taskId, { action: "fail", body: {
|
|
1294
|
+
error: message,
|
|
1295
|
+
retryable: retryableAgentFailure(agentMessage),
|
|
1296
|
+
...(providerQuotaRefusal(agentMessage) ? { failure: providerQuotaFailure(agentMessage) } : {}),
|
|
1297
|
+
idempotency_key: `bridge:fail:${active.attemptId}`,
|
|
1298
|
+
} });
|
|
1250
1299
|
retainAttemptWorktree = response.retain_worktree === true;
|
|
1251
1300
|
console.error(`Assignment ${taskId} failed: ${redactSecrets(message)}`);
|
|
1252
1301
|
return;
|
|
@@ -1414,7 +1463,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
1414
1463
|
const message = repairError instanceof Error ? repairError.message : "Delivery report repair failed";
|
|
1415
1464
|
const detail = `Delivery report repair failed: ${message}`;
|
|
1416
1465
|
const retryable = retryableAgentFailure(message);
|
|
1417
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: detail, retryable, failure:
|
|
1466
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: detail, retryable, failure: repairTurnFailure(detail, retryable), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
1418
1467
|
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
1419
1468
|
return;
|
|
1420
1469
|
}
|
|
@@ -1427,7 +1476,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
1427
1476
|
if (repaired.status === "failed") {
|
|
1428
1477
|
const message = repaired.error ?? "Delivery report repair failed";
|
|
1429
1478
|
const retryable = retryableAgentFailure(message);
|
|
1430
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable, failure:
|
|
1479
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable, failure: repairTurnFailure(message, retryable), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
1431
1480
|
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
1432
1481
|
return;
|
|
1433
1482
|
}
|
|
@@ -2083,6 +2132,15 @@ function retainDiagnosticWorktree(response) {
|
|
|
2083
2132
|
// receive that decision, so fail safe by retaining the only copy of the failed code.
|
|
2084
2133
|
return response.retain_worktree === true || response.status === "invalid_lease";
|
|
2085
2134
|
}
|
|
2135
|
+
/**
|
|
2136
|
+
* A repair turn that died on the account's allowance is not a delivery-report defect. Naming it one
|
|
2137
|
+
* would send the owner to rework a report the agent never got to write.
|
|
2138
|
+
*/
|
|
2139
|
+
function repairTurnFailure(detail, retryable) {
|
|
2140
|
+
if (providerQuotaRefusal(detail))
|
|
2141
|
+
return providerQuotaFailure(detail);
|
|
2142
|
+
return retryable ? undefined : deliveryReportRepairFailure(detail);
|
|
2143
|
+
}
|
|
2086
2144
|
/**
|
|
2087
2145
|
* The envelope for a delivery report the Bridge could not repair.
|
|
2088
2146
|
*
|
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.38",
|
|
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": {
|