@bridge4dev/runner 0.68.1 → 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 +52 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/regex-guard.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* ВРЕМЕННАЯ МЕРА. Это обход чужого дефекта, а не его исправление. Когда движок
|
|
17
17
|
* починят — снять целиком (`claude-code-regex-guard.md` §3.3).
|
|
18
18
|
*/
|
|
19
|
+
import { commandMentionsSecretPath, gitRunsAProgram, isSecretPath } from './policy.js';
|
|
19
20
|
/**
|
|
20
21
|
* Граница числового счётчика, начиная с которой шаблон считается опасным.
|
|
21
22
|
*
|
|
@@ -303,6 +304,61 @@ export function inspectBashCommand(command) {
|
|
|
303
304
|
}
|
|
304
305
|
return { dangerous: false };
|
|
305
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Второй жилец этого привратника: защищённые пути в командах (#399).
|
|
309
|
+
*
|
|
310
|
+
* Почему здесь, а не в политике. Политика (`evaluateToolUse`) в режиме
|
|
311
|
+
* «Полный доступ» не вызывается ВООБЩЕ: раннер запускает CLI с
|
|
312
|
+
* `bypassPermissions`, и `canUseTool` из него не приходит ни разу. Именно в
|
|
313
|
+
* этом режиме идут этапы прогонов. Хук `PreToolUse` переживает любой режим —
|
|
314
|
+
* проверено живьём 08.09.2026, когда он остановил опасный `grep` в сессии
|
|
315
|
+
* режима `full`.
|
|
316
|
+
*
|
|
317
|
+
* Список и функция — те же, что у политики (`commandMentionsSecretPath`), а не
|
|
318
|
+
* вторая копия: два списка разошлись бы, и разошлись бы молча.
|
|
319
|
+
*
|
|
320
|
+
* Случай, который это закрывает, стоил 47 агентов и 4 сессий за две секунды.
|
|
321
|
+
* Агент скопировал `/root/.claude/.credentials.json` во временный `HOME` и
|
|
322
|
+
* запустил там свой `claude`; тот упреждающе обновил токен, записал новую пару
|
|
323
|
+
* в копию, и refresh-токен машины обесценился. Запрет на этот файл существовал
|
|
324
|
+
* и тогда — но в том режиме его никто не спрашивал.
|
|
325
|
+
*
|
|
326
|
+
* Объём — только `Bash`, осознанно: расширять матчер хука до `Read`/`Write`
|
|
327
|
+
* значит вставать на пути каждого чтения и записи, а в обычных режимах эти
|
|
328
|
+
* инструменты и так закрыты политикой.
|
|
329
|
+
*/
|
|
330
|
+
function inspectSecretPaths(command) {
|
|
331
|
+
if (!commandMentionsSecretPath(command))
|
|
332
|
+
return { dangerous: false };
|
|
333
|
+
return {
|
|
334
|
+
dangerous: true,
|
|
335
|
+
reason: 'Blocked: this command touches a protected path — a machine login, a key, or an agent ' +
|
|
336
|
+
'session journal. A copy of a login file is the dangerous one: the token in it is ' +
|
|
337
|
+
'single-use, so the copy that refreshes it signs every session on this machine out at ' +
|
|
338
|
+
'once. If you need an isolated run, sign in inside that HOME of its own ' +
|
|
339
|
+
'(`claude auth login` / `codex login`), or give the run an ANTHROPIC_API_KEY. If you ' +
|
|
340
|
+
'were after your own notes, those live beside the journals and are open: ' +
|
|
341
|
+
'`~/.claude/projects/<project>/memory/`.',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Третий жилец: флаг git, называющий программу для запуска (#443).
|
|
346
|
+
*
|
|
347
|
+
* Дыру закрывает `evaluateGitPolicy`, а её в режиме «Полный доступ» никто не
|
|
348
|
+
* зовёт — в том самом режиме, в котором идут этапы прогонов. Сторож зовёт
|
|
349
|
+
* только УЗКУЮ половину правила (`gitRunsAProgram`): проектные настройки git
|
|
350
|
+
* — запрет push, защищённые ветки, `git clean` — в этом режиме намеренно не
|
|
351
|
+
* действуют, и тащить их сюда значило бы запрещать работу, ради которой режим
|
|
352
|
+
* и существует. «Какая программа запустится на чужой машине» к этим
|
|
353
|
+
* настройкам никогда не относилась: у правила рядом с ним, про
|
|
354
|
+
* `--receive-pack`, переключателя тоже нет.
|
|
355
|
+
*/
|
|
356
|
+
function inspectGitProgramFlags(command) {
|
|
357
|
+
const refusal = gitRunsAProgram(command);
|
|
358
|
+
if (refusal === null)
|
|
359
|
+
return { dangerous: false };
|
|
360
|
+
return { dangerous: true, reason: `Blocked: ${refusal.reason}` };
|
|
361
|
+
}
|
|
306
362
|
/**
|
|
307
363
|
* Решение по тому, что прислал Claude Code в хук `PreToolUse`.
|
|
308
364
|
*
|
|
@@ -316,14 +372,39 @@ export function decideForTool(toolName, toolInput) {
|
|
|
316
372
|
return { dangerous: false };
|
|
317
373
|
const input = toolInput;
|
|
318
374
|
if (toolName === 'Bash') {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
375
|
+
if (typeof input['command'] !== 'string')
|
|
376
|
+
return { dangerous: false };
|
|
377
|
+
const secret = inspectSecretPaths(input['command']);
|
|
378
|
+
if (secret.dangerous)
|
|
379
|
+
return secret;
|
|
380
|
+
const runsProgram = inspectGitProgramFlags(input['command']);
|
|
381
|
+
if (runsProgram.dangerous)
|
|
382
|
+
return runsProgram;
|
|
383
|
+
return inspectBashCommand(input['command']);
|
|
322
384
|
}
|
|
323
385
|
// Инструмент Grep проверяется тем же правилом намеренно, хотя движок под ним
|
|
324
386
|
// может быть другим: цена перестраховки — один круг диалога, и она измерена
|
|
325
387
|
// как нулевая на повседневных шаблонах.
|
|
326
388
|
if (toolName === 'Grep') {
|
|
389
|
+
/**
|
|
390
|
+
* Независимая проверка этапа: правило по расширению `.jsonl` не может
|
|
391
|
+
* совпасть с КАТАЛОГОМ, а `Grep` с `path` каталога и `output_mode:
|
|
392
|
+
* 'content'` отдаёт содержимое всех журналов под ним одним вызовом. В
|
|
393
|
+
* политике это закрыто правилом на каталог; здесь — той же функцией.
|
|
394
|
+
*
|
|
395
|
+
* Матчер хука это НЕ расширяет: `Grep` стоит в нём с самого начала
|
|
396
|
+
* (`Bash|Grep`), и решение тикета «не доводить сторож до `Read`/`Write`»
|
|
397
|
+
* остаётся в силе.
|
|
398
|
+
*/
|
|
399
|
+
const target = input['path'];
|
|
400
|
+
if (typeof target === 'string' && target !== '' && isSecretPath(target)) {
|
|
401
|
+
return {
|
|
402
|
+
dangerous: true,
|
|
403
|
+
reason: 'Blocked: that path is protected — it holds machine logins, keys, or agent session ' +
|
|
404
|
+
'journals. Your own notes are open and live beside them: ' +
|
|
405
|
+
'`~/.claude/projects/<project>/memory/`.',
|
|
406
|
+
};
|
|
407
|
+
}
|
|
327
408
|
return typeof input['pattern'] === 'string'
|
|
328
409
|
? inspectPattern(input['pattern'])
|
|
329
410
|
: { dangerous: false };
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -1698,6 +1698,21 @@ export declare class Supervisor {
|
|
|
1698
1698
|
*/
|
|
1699
1699
|
private stopCages;
|
|
1700
1700
|
}
|
|
1701
|
+
/**
|
|
1702
|
+
* What the runner says to the agent when it carries a conversation on.
|
|
1703
|
+
*
|
|
1704
|
+
* Exported and pure because the alternative is untestable: the sentence is
|
|
1705
|
+
* chosen inside a timer that waits ten to thirty seconds, and an integration
|
|
1706
|
+
* test that waited for it would be slow and flaky. Its own QA found this
|
|
1707
|
+
* branch uncovered, and «uncovered» is how a sentence goes back to being wrong.
|
|
1708
|
+
*
|
|
1709
|
+
* And what it says has to be TRUE, or it costs the agent a search (#446 п. 6).
|
|
1710
|
+
* «Your answer was cut off part-way, check what you had already finished»
|
|
1711
|
+
* is right after a dropped stream and wrong after a request the provider
|
|
1712
|
+
* refused whole, where nothing of it ever reached a model: the agent goes
|
|
1713
|
+
* looking for half-done work that does not exist.
|
|
1714
|
+
*/
|
|
1715
|
+
export declare function carryOnMessage(refusedWhole: boolean): string;
|
|
1701
1716
|
/**
|
|
1702
1717
|
* The first message the agent gets.
|
|
1703
1718
|
*
|
package/dist/supervisor.js
CHANGED
|
@@ -3245,6 +3245,11 @@ export class Supervisor {
|
|
|
3245
3245
|
// emits (#300) resets the «three limit pauses in a row» counter one second
|
|
3246
3246
|
// after the third one, and «woke → refused → slept» never stops.
|
|
3247
3247
|
...(event.limitBlocked ? { limitBlocked: true } : {}),
|
|
3248
|
+
// #435 п. 1: the category the provider's filter named. Absent means the
|
|
3249
|
+
// CLI sent no such frame — an older one, or a turn nobody refused. The
|
|
3250
|
+
// API keeps the whole payload as it comes, so nothing there needs to
|
|
3251
|
+
// know this field exists for it to be stored and streamed.
|
|
3252
|
+
...(event.refusalCategory !== undefined ? { refusalCategory: event.refusalCategory } : {}),
|
|
3248
3253
|
// ALWAYS present, true or false — the absence of this field is what the
|
|
3249
3254
|
// API reads as «this runner is older than the fix», and a new runner that
|
|
3250
3255
|
// sometimes omitted it would be indistinguishable from one.
|
|
@@ -3336,6 +3341,17 @@ export class Supervisor {
|
|
|
3336
3341
|
costUsd: running.costUsd,
|
|
3337
3342
|
activeMs: Supervisor.spentMs(running),
|
|
3338
3343
|
errorMessage: event.errorMessage ?? 'Agent turn failed',
|
|
3344
|
+
// #435 п. 1. The session still FAILS — stopping instead of failing is
|
|
3345
|
+
// the rest of that ticket and needs two buttons on screen that do not
|
|
3346
|
+
// exist yet. What it stops doing is failing ANONYMOUSLY: the reason is
|
|
3347
|
+
// written down, so the platform can tell a filter refusal from a
|
|
3348
|
+
// broken connection without reading anybody's prose.
|
|
3349
|
+
// Spelled out rather than imported: the runner is published as its own
|
|
3350
|
+
// npm package and cannot depend on `@devbridge/shared`. The value is
|
|
3351
|
+
// mirrored there in `DEV_SESSION_END_REASONS.modelRefusal`, and the
|
|
3352
|
+
// API's Zod enum has `.catch(undefined)` — so an API older than this
|
|
3353
|
+
// runner hears no reason rather than dropping the whole frame.
|
|
3354
|
+
...(event.refusalCategory !== undefined ? { endReason: 'MODEL_REFUSAL' } : {}),
|
|
3339
3355
|
});
|
|
3340
3356
|
return;
|
|
3341
3357
|
}
|
|
@@ -4632,7 +4648,7 @@ export class Supervisor {
|
|
|
4632
4648
|
const delay = retryDelayMs(decision.backoff, attempt);
|
|
4633
4649
|
const at = new Date(Date.now() + delay);
|
|
4634
4650
|
const timer = setTimeout(() => {
|
|
4635
|
-
this.runApiRetry(running, bucket);
|
|
4651
|
+
this.runApiRetry(running, bucket, decision.refusedWhole);
|
|
4636
4652
|
}, delay);
|
|
4637
4653
|
timer.unref();
|
|
4638
4654
|
running.apiRetry = { timer, attempt, ruleId: decision.ruleId };
|
|
@@ -4640,9 +4656,18 @@ export class Supervisor {
|
|
|
4640
4656
|
// Supervisor-minted rather than an adapter notice: adapter notices are
|
|
4641
4657
|
// de-duplicated for the session's whole life, so «attempt 2» would appear
|
|
4642
4658
|
// once and never again (gotcha #148).
|
|
4659
|
+
// Three sentences, not two (#446 п. 6). «The answer was cut off part-way»
|
|
4660
|
+
// is true of a dropped stream and false of a request the provider threw
|
|
4661
|
+
// away whole — and telling a person the connection failed when it did not
|
|
4662
|
+
// is what cost an hour of looking in the wrong place.
|
|
4663
|
+
const why = decision.refusedWhole
|
|
4664
|
+
? 'The provider rejected the request outright'
|
|
4665
|
+
: bucket === 'continue'
|
|
4666
|
+
? 'The answer was cut off part-way'
|
|
4667
|
+
: 'The provider could not be reached';
|
|
4643
4668
|
this.sendEvent(running, 'system_note', {
|
|
4644
4669
|
code: 'agent_retry',
|
|
4645
|
-
text: `${
|
|
4670
|
+
text: `${why} — ` +
|
|
4646
4671
|
`attempt ${attempt} of ${decision.attempts}, next at ${at.toISOString().slice(11, 16)} UTC. ` +
|
|
4647
4672
|
'This is a fault on the provider’s side, not in the task.',
|
|
4648
4673
|
});
|
|
@@ -4650,7 +4675,7 @@ export class Supervisor {
|
|
|
4650
4675
|
return true;
|
|
4651
4676
|
}
|
|
4652
4677
|
/** Fire an armed retry — re-checking, at FIRE time, everything that could have changed. */
|
|
4653
|
-
runApiRetry(running, bucket) {
|
|
4678
|
+
runApiRetry(running, bucket, refusedWhole = false) {
|
|
4654
4679
|
const armed = running.apiRetry;
|
|
4655
4680
|
if (!armed)
|
|
4656
4681
|
return;
|
|
@@ -4682,9 +4707,7 @@ export class Supervisor {
|
|
|
4682
4707
|
// owner used to type by hand. `auto-resume.ts` records the cost of getting
|
|
4683
4708
|
// this backwards — a re-sent prompt ran a fifteen-minute command twice.
|
|
4684
4709
|
const text = bucket === 'continue'
|
|
4685
|
-
?
|
|
4686
|
-
'anything you did, and nothing is wrong with the work. Check what you had already ' +
|
|
4687
|
-
'finished before redoing any of it, then continue from where you stopped.'
|
|
4710
|
+
? carryOnMessage(refusedWhole)
|
|
4688
4711
|
: (running.lastTurnPrompt ?? 'Continue.');
|
|
4689
4712
|
running.session.send(text);
|
|
4690
4713
|
this.reportStatus(running.descriptor.id, 'RUNNING', {});
|
|
@@ -8153,6 +8176,29 @@ function isTerminal(status) {
|
|
|
8153
8176
|
function isSettled(status) {
|
|
8154
8177
|
return isTerminal(status) || status === 'REVIEW';
|
|
8155
8178
|
}
|
|
8179
|
+
/**
|
|
8180
|
+
* What the runner says to the agent when it carries a conversation on.
|
|
8181
|
+
*
|
|
8182
|
+
* Exported and pure because the alternative is untestable: the sentence is
|
|
8183
|
+
* chosen inside a timer that waits ten to thirty seconds, and an integration
|
|
8184
|
+
* test that waited for it would be slow and flaky. Its own QA found this
|
|
8185
|
+
* branch uncovered, and «uncovered» is how a sentence goes back to being wrong.
|
|
8186
|
+
*
|
|
8187
|
+
* And what it says has to be TRUE, or it costs the agent a search (#446 п. 6).
|
|
8188
|
+
* «Your answer was cut off part-way, check what you had already finished»
|
|
8189
|
+
* is right after a dropped stream and wrong after a request the provider
|
|
8190
|
+
* refused whole, where nothing of it ever reached a model: the agent goes
|
|
8191
|
+
* looking for half-done work that does not exist.
|
|
8192
|
+
*/
|
|
8193
|
+
export function carryOnMessage(refusedWhole) {
|
|
8194
|
+
return refusedWhole
|
|
8195
|
+
? 'The provider rejected your previous request outright — nothing of it ran, and nothing is ' +
|
|
8196
|
+
'wrong with your work or with the task. Carry on from where you were; there is no ' +
|
|
8197
|
+
'half-finished step to look for.'
|
|
8198
|
+
: 'The connection to the model dropped part-way through your previous answer — not by ' +
|
|
8199
|
+
'anything you did, and nothing is wrong with the work. Check what you had already ' +
|
|
8200
|
+
'finished before redoing any of it, then continue from where you stopped.';
|
|
8201
|
+
}
|
|
8156
8202
|
/**
|
|
8157
8203
|
* The first message the agent gets.
|
|
8158
8204
|
*
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.69.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED