@bridge4dev/runner 0.68.0 → 0.69.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/adapters/claude.js +243 -10
- package/dist/adapters/codex.js +166 -3
- package/dist/adapters/error-policy.d.ts +17 -0
- package/dist/adapters/error-policy.js +74 -3
- package/dist/adapters/types.d.ts +12 -0
- package/dist/policy.d.ts +39 -0
- package/dist/policy.js +523 -8
- package/dist/protocol.d.ts +9 -2
- package/dist/regex-guard.js +84 -3
- package/dist/supervisor.d.ts +15 -0
- package/dist/supervisor.js +54 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -120,6 +120,21 @@ export const ERROR_RULES = [
|
|
|
120
120
|
note: 'The API refused the request itself. Deterministic — it will refuse the identical request ' +
|
|
121
121
|
'identically. Often a malformed transcript, which a repeat can only make worse.',
|
|
122
122
|
},
|
|
123
|
+
{
|
|
124
|
+
id: 'claude.model_refusal',
|
|
125
|
+
provider: 'claude',
|
|
126
|
+
match: { codeIn: ['model_refusal'] },
|
|
127
|
+
retry: false,
|
|
128
|
+
verified: true,
|
|
129
|
+
note: 'The provider’s own safety filter refused the answer and no fallback model took the turn ' +
|
|
130
|
+
'(#435, first item). `model_refusal` is OURS, not the SDK’s: the closed enum on the ' +
|
|
131
|
+
'assistant frame has no member for this, so the adapter mints the code from the machine ' +
|
|
132
|
+
'fields that do carry it — `stop_reason: "refusal"` with `stop_details.category`, or the ' +
|
|
133
|
+
'`model_refusal_no_fallback` frame with `api_refusal_category`. Listed rather than left to ' +
|
|
134
|
+
'the default for one concrete reason: without it a refusal that named no other cause fell ' +
|
|
135
|
+
'through to `server_error` and would have been REPEATED five times, and re-sending a ' +
|
|
136
|
+
'refused request is the one repeat that actively harms the account (see `codex.policy`).',
|
|
137
|
+
},
|
|
123
138
|
{
|
|
124
139
|
id: 'claude.model_not_found',
|
|
125
140
|
provider: 'claude',
|
|
@@ -218,6 +233,45 @@ export const ERROR_RULES = [
|
|
|
218
233
|
note: 'Refused on content policy — about what was asked, not about their weather. Re-sending a ' +
|
|
219
234
|
'refused request is the one repeat that actively harms the account.',
|
|
220
235
|
},
|
|
236
|
+
/**
|
|
237
|
+
* The one 4xx that is worth carrying on from — and it sits ABOVE the status
|
|
238
|
+
* range on purpose, because first match wins.
|
|
239
|
+
*
|
|
240
|
+
* 2026-09-18, 20:27:44 UTC, DEV-COMPLECTA: OpenAI answered a turn with
|
|
241
|
+
* `{"error":{"code":"unsupported_parameter","param":"access_programs.cyber"},
|
|
242
|
+
* "status":400}`. The parameter is one the Codex CLI adds by itself — it is
|
|
243
|
+
* in no request this platform writes — and the organisation is not in the
|
|
244
|
+
* programme it names. The session died, the stage stopped as «unknown» and a
|
|
245
|
+
* run with three stages ahead of it went to NEEDS_YOU. Fifteen other sessions
|
|
246
|
+
* on the same machine, the same account and the same model ran that day
|
|
247
|
+
* without it, including the neighbouring stage of the same run four minutes
|
|
248
|
+
* earlier. Known upstream: openai/codex#46398.
|
|
249
|
+
*
|
|
250
|
+
* `continue`, not `retry`: the owner pressed Retry on the live run at 21:10
|
|
251
|
+
* and it continued the SAME conversation (`resume_count = 2`) and finished
|
|
252
|
+
* `done`. What worked by hand is what the runner should do — and what it must
|
|
253
|
+
* not do is send the turn again.
|
|
254
|
+
*
|
|
255
|
+
* `attempts: 1` and not the 2 the ticket asked for. The clamp below gives a
|
|
256
|
+
* `continue` one attempt whatever the row says, and raising the clamp would
|
|
257
|
+
* widen every `continue` rule at once. It also buys nothing: the second
|
|
258
|
+
* attempt of the SAME rule is refused by `isRepeatOfSameFailure` anyway, so 2
|
|
259
|
+
* would be a number that never happened.
|
|
260
|
+
*/
|
|
261
|
+
{
|
|
262
|
+
id: 'codex.api.access_program_not_enabled',
|
|
263
|
+
provider: 'codex',
|
|
264
|
+
match: { codeIn: ['api:unsupported_parameter:access_programs'] },
|
|
265
|
+
retry: true,
|
|
266
|
+
bucket: 'continue',
|
|
267
|
+
attempts: 1,
|
|
268
|
+
backoff: 'standard',
|
|
269
|
+
refusedWhole: true,
|
|
270
|
+
verified: true,
|
|
271
|
+
note: 'The provider rejected the whole request over a parameter the CLI adds, not the person — ' +
|
|
272
|
+
'so nothing was produced and nothing is repeated by carrying the conversation on. Verified ' +
|
|
273
|
+
'on the live run of 2026-09-18 (#446), not on a guess.',
|
|
274
|
+
},
|
|
221
275
|
{
|
|
222
276
|
id: 'codex.http_4xx',
|
|
223
277
|
provider: 'codex',
|
|
@@ -233,6 +287,7 @@ const DEFAULT_DECISION = {
|
|
|
233
287
|
attempts: 0,
|
|
234
288
|
backoff: 'standard',
|
|
235
289
|
ruleId: null,
|
|
290
|
+
refusedWhole: false,
|
|
236
291
|
};
|
|
237
292
|
function matches(rule, signal) {
|
|
238
293
|
if (rule.provider !== signal.provider)
|
|
@@ -261,9 +316,24 @@ const PARTIAL_PHRASES = ['may be incomplete', 'mid-response'];
|
|
|
261
316
|
* ran a fifteen-minute command twice, four minutes apart.
|
|
262
317
|
*/
|
|
263
318
|
function refine(bucket, signal) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Something irreversible already happened this turn. Neither repeating nor
|
|
321
|
+
* resuming is safe enough to do without a person looking.
|
|
322
|
+
*
|
|
323
|
+
* **One exception, and it is the narrowest shape that works (#446 п. 5).**
|
|
324
|
+
* The rule itself has to have asked to CARRY THE CONVERSATION ON. When it
|
|
325
|
+
* has, «send the turn again after a commit» stays impossible by
|
|
326
|
+
* construction: `refine` cannot manufacture a `continue`, it only receives
|
|
327
|
+
* the row's own bucket, so nothing here can turn a `retry` row into one.
|
|
328
|
+
*
|
|
329
|
+
* Why it has to exist: the turn this came from had run `git commit` one
|
|
330
|
+
* second before the provider threw the request away, so `irreversible` was
|
|
331
|
+
* true and the answer was an unconditional `stop` — for a failure where
|
|
332
|
+
* nothing at all had run on the provider's side. Carrying the conversation
|
|
333
|
+
* on repeats nothing; it is exactly what the owner did by hand on the live
|
|
334
|
+
* run, and it finished.
|
|
335
|
+
*/
|
|
336
|
+
if (signal.irreversible === true && bucket !== 'continue')
|
|
267
337
|
return 'stop';
|
|
268
338
|
// Work was produced, so «send it again» is off the table whatever the cause says.
|
|
269
339
|
if (signal.produced === true && bucket === 'retry')
|
|
@@ -308,6 +378,7 @@ export function classifyFailure(signal) {
|
|
|
308
378
|
attempts: bucket === 'stop' ? 0 : bucket === 'continue' ? 1 : Math.max(0, rule.attempts ?? 0),
|
|
309
379
|
backoff: rule.backoff ?? 'standard',
|
|
310
380
|
ruleId: rule.id,
|
|
381
|
+
refusedWhole: rule.refusedWhole === true,
|
|
311
382
|
};
|
|
312
383
|
}
|
|
313
384
|
return DEFAULT_DECISION;
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -654,6 +654,18 @@ export type AgentEvent = {
|
|
|
654
654
|
failureCode?: string;
|
|
655
655
|
/** HTTP status, when the provider named one alongside the cause. */
|
|
656
656
|
failureStatus?: number;
|
|
657
|
+
/**
|
|
658
|
+
* The category the provider's own safety filter named when it refused to
|
|
659
|
+
* answer (#435, first item): `reasoning_extraction`, `cyber`, `bio`, …,
|
|
660
|
+
* or `unknown` when the frame named none.
|
|
661
|
+
*
|
|
662
|
+
* Separate from `failureCode` because the two answer different
|
|
663
|
+
* questions: the code decides what may happen next, the category is what
|
|
664
|
+
* a person is told and what the platform stores. Absent means the CLI
|
|
665
|
+
* sent no such frame — an older one, or a turn that was not refused —
|
|
666
|
+
* and everything behaves exactly as it did before.
|
|
667
|
+
*/
|
|
668
|
+
refusalCategory?: string;
|
|
657
669
|
/**
|
|
658
670
|
* Did this turn put anything at all on the wire before it broke — a text
|
|
659
671
|
* block, a thought, a tool call?
|
package/dist/policy.d.ts
CHANGED
|
@@ -182,6 +182,31 @@ export declare function isInsideWorktree(p: string, worktreePath: string): boole
|
|
|
182
182
|
* next git invocation. Neither is ever ordinary agent work.
|
|
183
183
|
*/
|
|
184
184
|
export declare function isGitInternalPath(p: string): boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Does this segment tell git to run a program of the caller's choosing? (#443)
|
|
187
|
+
*
|
|
188
|
+
* The hole this closes was open from 2026-08-30 to this release and survived
|
|
189
|
+
* runner 0.59 through 0.68: `git fetch --upload-pack='touch /tmp/x' /path` is
|
|
190
|
+
* on the safe list (it starts `git fetch`), carries no shell metacharacter, and
|
|
191
|
+
* so ran with no permission card at all under the DEFAULT trust level. The
|
|
192
|
+
* flag is not a niche one — git executes it even when the fetch itself then
|
|
193
|
+
* fails with code 128.
|
|
194
|
+
*
|
|
195
|
+
* Three things this must not do, each of them a mistake somebody already paid
|
|
196
|
+
* for on this file:
|
|
197
|
+
*
|
|
198
|
+
* - **no regex over the command.** `git -c` forty-four times in a row once
|
|
199
|
+
* froze the runner for minutes inside a normal turn (see `GIT_PUSH`), and
|
|
200
|
+
* this runs synchronously on every Bash call of every session on the
|
|
201
|
+
* machine. Everything below is a single pass over `words()` and string
|
|
202
|
+
* compares;
|
|
203
|
+
* - **no searching for the flag as a substring.** See `PROGRAM_FLAGS`;
|
|
204
|
+
* - **the refusal has to catch the form with no shell metacharacter in it.**
|
|
205
|
+
* The proof-of-concept in the 2026-08-30 audit used `;`, which the
|
|
206
|
+
* metacharacter filter already caught, so a test written from it would have
|
|
207
|
+
* gone green against an unfixed runner.
|
|
208
|
+
*/
|
|
209
|
+
export declare function gitRunsAProgram(command: string): PolicyDecision | null;
|
|
185
210
|
/**
|
|
186
211
|
* The project's git rules, applied to one Bash command.
|
|
187
212
|
*
|
|
@@ -200,6 +225,20 @@ export declare function isGitInternalPath(p: string): boolean;
|
|
|
200
225
|
* and the loop reads to the end.
|
|
201
226
|
*/
|
|
202
227
|
export declare function evaluateGitPolicy(command: string, ctx: PolicyContext): PolicyDecision | null;
|
|
228
|
+
/**
|
|
229
|
+
* Does this shell command name a path the agent must not touch?
|
|
230
|
+
*
|
|
231
|
+
* Exported for the `PreToolUse` guard (#399), which is the one check that
|
|
232
|
+
* survives `bypassPermissions` — where `evaluateToolUse` below is never called
|
|
233
|
+
* at all. One list, one function, two callers: a second copy of these patterns
|
|
234
|
+
* would drift, and the drift would be silent.
|
|
235
|
+
*
|
|
236
|
+
* Both spellings are tested here rather than at the call site, so a caller
|
|
237
|
+
* cannot forget the one that matters: `cat "/root/.cla""ude/…"` collapses only
|
|
238
|
+
* under `dequote`, and forgetting it is how quote-splitting got past a rule
|
|
239
|
+
* once already (QA-96 F8/F9).
|
|
240
|
+
*/
|
|
241
|
+
export declare function commandMentionsSecretPath(command: string): boolean;
|
|
203
242
|
export interface RecipeCommandContext {
|
|
204
243
|
/**
|
|
205
244
|
* The docker compose project this preview owns, from `preview.project` in the
|