@bridge4dev/runner 0.42.0 → 0.44.1
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-usage.d.ts +35 -4
- package/dist/adapters/claude-usage.js +138 -14
- package/dist/adapters/claude.js +218 -12
- package/dist/adapters/codex.js +46 -0
- package/dist/adapters/error-policy.d.ts +178 -0
- package/dist/adapters/error-policy.js +370 -0
- package/dist/adapters/rate-limits.d.ts +22 -0
- package/dist/adapters/rate-limits.js +24 -0
- package/dist/adapters/types.d.ts +30 -0
- package/dist/claude-settings.d.ts +107 -0
- package/dist/claude-settings.js +415 -0
- package/dist/index.js +88 -1
- package/dist/recipe-schema.d.ts +6 -6
- package/dist/regex-guard-hook.d.ts +3 -0
- package/dist/regex-guard-hook.js +48 -0
- package/dist/regex-guard.d.ts +86 -0
- package/dist/regex-guard.js +359 -0
- package/dist/supervisor.d.ts +16 -0
- package/dist/supervisor.js +151 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/recipe-schema.d.ts
CHANGED
|
@@ -51,14 +51,14 @@ export declare const ProjectRecipePreviewSchema: z.ZodObject<{
|
|
|
51
51
|
run: string;
|
|
52
52
|
url?: string | undefined;
|
|
53
53
|
project?: string | undefined;
|
|
54
|
-
timeoutSec?: number | undefined;
|
|
55
54
|
stop?: string | undefined;
|
|
55
|
+
timeoutSec?: number | undefined;
|
|
56
56
|
}, {
|
|
57
57
|
run: string;
|
|
58
58
|
url?: string | undefined;
|
|
59
59
|
project?: string | undefined;
|
|
60
|
-
timeoutSec?: number | undefined;
|
|
61
60
|
stop?: string | undefined;
|
|
61
|
+
timeoutSec?: number | undefined;
|
|
62
62
|
}>;
|
|
63
63
|
export declare const ProjectRecipeSchema: z.ZodObject<{
|
|
64
64
|
version: z.ZodDefault<z.ZodOptional<z.ZodLiteral<1>>>;
|
|
@@ -201,14 +201,14 @@ export declare const ProjectRecipeSchema: z.ZodObject<{
|
|
|
201
201
|
run: string;
|
|
202
202
|
url?: string | undefined;
|
|
203
203
|
project?: string | undefined;
|
|
204
|
-
timeoutSec?: number | undefined;
|
|
205
204
|
stop?: string | undefined;
|
|
205
|
+
timeoutSec?: number | undefined;
|
|
206
206
|
}, {
|
|
207
207
|
run: string;
|
|
208
208
|
url?: string | undefined;
|
|
209
209
|
project?: string | undefined;
|
|
210
|
-
timeoutSec?: number | undefined;
|
|
211
210
|
stop?: string | undefined;
|
|
211
|
+
timeoutSec?: number | undefined;
|
|
212
212
|
}>>;
|
|
213
213
|
notes: z.ZodOptional<z.ZodString>;
|
|
214
214
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -244,8 +244,8 @@ export declare const ProjectRecipeSchema: z.ZodObject<{
|
|
|
244
244
|
run: string;
|
|
245
245
|
url?: string | undefined;
|
|
246
246
|
project?: string | undefined;
|
|
247
|
-
timeoutSec?: number | undefined;
|
|
248
247
|
stop?: string | undefined;
|
|
248
|
+
timeoutSec?: number | undefined;
|
|
249
249
|
} | undefined;
|
|
250
250
|
notes?: string | undefined;
|
|
251
251
|
health?: {
|
|
@@ -258,8 +258,8 @@ export declare const ProjectRecipeSchema: z.ZodObject<{
|
|
|
258
258
|
run: string;
|
|
259
259
|
url?: string | undefined;
|
|
260
260
|
project?: string | undefined;
|
|
261
|
-
timeoutSec?: number | undefined;
|
|
262
261
|
stop?: string | undefined;
|
|
262
|
+
timeoutSec?: number | undefined;
|
|
263
263
|
} | undefined;
|
|
264
264
|
notes?: string | undefined;
|
|
265
265
|
steps?: {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Исполняемый привратник шаблонов поиска — точка входа для хука `PreToolUse`.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code присылает JSON в stdin и смотрит на код возврата:
|
|
6
|
+
* 0 — пропустить, 2 — заблокировать (stderr показывается модели).
|
|
7
|
+
*
|
|
8
|
+
* Здесь только ввод-вывод. Всё решение — в `hookDecision`, и оно покрыто тестами
|
|
9
|
+
* (`regex-guard.test.ts`), потому что проверять логику через запуск процесса
|
|
10
|
+
* дорого и хрупко.
|
|
11
|
+
*
|
|
12
|
+
* Зачем это существует и когда снимать — `regex-guard.ts` и
|
|
13
|
+
* `docs/plans/active/claude-code-regex-guard.md`.
|
|
14
|
+
*/
|
|
15
|
+
import { hookDecision } from './regex-guard.js';
|
|
16
|
+
/**
|
|
17
|
+
* Потолок на чтение stdin.
|
|
18
|
+
*
|
|
19
|
+
* Хук стоит на пути каждой команды агента. Если Claude Code однажды не закроет
|
|
20
|
+
* поток, зависший привратник остановит работу вернее любого шаблона — поэтому
|
|
21
|
+
* молчание тоже считается поводом пропустить.
|
|
22
|
+
*/
|
|
23
|
+
const STDIN_TIMEOUT_MS = 5_000;
|
|
24
|
+
async function readStdin() {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let data = '';
|
|
27
|
+
const done = (value) => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
resolve(value);
|
|
30
|
+
};
|
|
31
|
+
const timer = setTimeout(() => done(''), STDIN_TIMEOUT_MS);
|
|
32
|
+
process.stdin.setEncoding('utf8');
|
|
33
|
+
process.stdin.on('data', (chunk) => {
|
|
34
|
+
data += chunk;
|
|
35
|
+
});
|
|
36
|
+
process.stdin.on('end', () => done(data));
|
|
37
|
+
process.stdin.on('error', () => done(''));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function main() {
|
|
41
|
+
const decision = hookDecision(await readStdin());
|
|
42
|
+
if (decision.message)
|
|
43
|
+
process.stderr.write(`${decision.message}\n`);
|
|
44
|
+
process.exit(decision.exitCode);
|
|
45
|
+
}
|
|
46
|
+
// Любая непойманная ошибка — пропуск, а не блокировка. См. `hookDecision`.
|
|
47
|
+
main().catch(() => process.exit(0));
|
|
48
|
+
//# sourceMappingURL=regex-guard-hook.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Привратник шаблонов поиска — защита от дефекта движка внутри Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code подменяет `grep` собственным `ugrep`, вшитым в бинарь. На шаблонах
|
|
5
|
+
* определённой формы этот движок съедает ~5.4 ГБ и ~67 секунд ПРИ РАЗБОРЕ ШАБЛОНА —
|
|
6
|
+
* до чтения файла, поэтому размер и содержимое данных ни на что не влияют
|
|
7
|
+
* (проверено: файл 18 МБ и файл 6 байт дают одинаковый пик).
|
|
8
|
+
*
|
|
9
|
+
* На дев-сервере это валит машину в своп, демон раннера перестаёт успевать
|
|
10
|
+
* отвечать на ping за отведённые 60 секунд (`RUNNER_PING_INTERVAL_MS` ×
|
|
11
|
+
* `RUNNER_PONG_MAX_MISSES`), и 504 прилетают ПО ВСЕМ сессиям машины сразу.
|
|
12
|
+
*
|
|
13
|
+
* Полный разбор, таблица замеров и признак инцидента — `docs/standards/project-gotchas.md` §345.
|
|
14
|
+
* План и границы применимости — `docs/plans/active/claude-code-regex-guard.md`.
|
|
15
|
+
*
|
|
16
|
+
* ВРЕМЕННАЯ МЕРА. Это обход чужого дефекта, а не его исправление. Когда движок
|
|
17
|
+
* починят — снять целиком (`claude-code-regex-guard.md` §3.3).
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Граница числового счётчика, начиная с которой шаблон считается опасным.
|
|
21
|
+
*
|
|
22
|
+
* Измеренный порог взрыва — ровно 20, во всех сочетаниях (два счётчика, счётчик
|
|
23
|
+
* рядом с `.*`, счётчики без верхней границы, счётчик над группой). Рост
|
|
24
|
+
* экспоненциальный, примерно вчетверо на каждые +2. Замеры для двух счётчиков
|
|
25
|
+
* (`x.{0,N}y.{0,N}z`): 16 — 377 МБ, 18 — 1560 МБ, 20 — взрыв на 5.4 ГБ; для
|
|
26
|
+
* `.*` плюс один счётчик — 16 — 204 МБ, 18 — 831 МБ. Таблица целиком в §345.
|
|
27
|
+
*
|
|
28
|
+
* Берём 16 — запас в две ступени экспоненты. Ниже опускать нет смысла:
|
|
29
|
+
* повседневные шаблоны (IP, дата, semver, hex) держатся в пределах 6.
|
|
30
|
+
*/
|
|
31
|
+
export declare const QUANTIFIER_DANGER_BOUND = 16;
|
|
32
|
+
export interface PatternVerdict {
|
|
33
|
+
dangerous: boolean;
|
|
34
|
+
/** Человеческое объяснение с готовой заменой. Заполняется только при `dangerous`. */
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Взорвёт ли этот шаблон встроенный движок Claude Code?
|
|
39
|
+
*
|
|
40
|
+
* Условие (полностью выведено из замеров, см. §345):
|
|
41
|
+
* есть счётчик с границей ≥ порога, применённый к ОДИНОЧНОМУ матчеру,
|
|
42
|
+
* И в шаблоне есть ещё хотя бы один участок переменной длины.
|
|
43
|
+
*
|
|
44
|
+
* Одиночный счётчик безопасен при любом размере (`{0,5000}` — 14 МБ), шаблон
|
|
45
|
+
* без счётчиков безопасен при любом числе `.*` — взрыв даёт именно сочетание.
|
|
46
|
+
*/
|
|
47
|
+
export declare function inspectPattern(pattern: string): PatternVerdict;
|
|
48
|
+
/**
|
|
49
|
+
* Опасен ли шаблон внутри команды оболочки?
|
|
50
|
+
*
|
|
51
|
+
* Сегмент разбирается целиком, а не выделенный из него аргумент: надёжно
|
|
52
|
+
* вычленить шаблон из произвольной команды нельзя (кавычки, подстановки), а
|
|
53
|
+
* форма `{n,m}` вне регулярного выражения почти не встречается. Ошибка в сторону
|
|
54
|
+
* осторожности здесь стоит одного лишнего круга диалога, ошибка в другую
|
|
55
|
+
* сторону — лежащего дев-сервера.
|
|
56
|
+
*
|
|
57
|
+
* Чего НЕ ловит намеренно: разорванное слово (`gr""ep`). Привратник защищает от
|
|
58
|
+
* случайного самострела, а не от умышленного обхода — и второй линией остаётся
|
|
59
|
+
* клетка сессии (`runner-memory-ceiling.md` §6).
|
|
60
|
+
*/
|
|
61
|
+
export declare function inspectBashCommand(command: string): PatternVerdict;
|
|
62
|
+
/**
|
|
63
|
+
* Решение по тому, что прислал Claude Code в хук `PreToolUse`.
|
|
64
|
+
*
|
|
65
|
+
* Незнакомый инструмент и любой неожиданный вход — пропуск. Привратник обязан
|
|
66
|
+
* ошибаться в сторону «пропустить»: заблокированная по недоразумению работа
|
|
67
|
+
* агента дороже пропущенного шаблона, от которого есть вторая линия защиты
|
|
68
|
+
* (клетка сессии, `runner-memory-ceiling.md` §6).
|
|
69
|
+
*/
|
|
70
|
+
export declare function decideForTool(toolName: string, toolInput: unknown): PatternVerdict;
|
|
71
|
+
export interface HookDecision {
|
|
72
|
+
/** 0 — пропустить, 2 — заблокировать (соглашение `PreToolUse` в Claude Code). */
|
|
73
|
+
exitCode: 0 | 2;
|
|
74
|
+
/** Текст в stderr; Claude Code показывает его модели. Пуст при пропуске. */
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Превращает полезную нагрузку хука в решение.
|
|
79
|
+
*
|
|
80
|
+
* ЛЮБАЯ неожиданность — пропуск. Привратник стоит на пути каждой команды агента,
|
|
81
|
+
* и его собственная ошибка не должна останавливать работу: пропущенный шаблон
|
|
82
|
+
* прикрыт второй линией (клеткой сессии), а привратник, блокирующий всё подряд
|
|
83
|
+
* из-за смены формата входа, останавливает разработку целиком.
|
|
84
|
+
*/
|
|
85
|
+
export declare function hookDecision(payload: string): HookDecision;
|
|
86
|
+
//# sourceMappingURL=regex-guard.d.ts.map
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Привратник шаблонов поиска — защита от дефекта движка внутри Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code подменяет `grep` собственным `ugrep`, вшитым в бинарь. На шаблонах
|
|
5
|
+
* определённой формы этот движок съедает ~5.4 ГБ и ~67 секунд ПРИ РАЗБОРЕ ШАБЛОНА —
|
|
6
|
+
* до чтения файла, поэтому размер и содержимое данных ни на что не влияют
|
|
7
|
+
* (проверено: файл 18 МБ и файл 6 байт дают одинаковый пик).
|
|
8
|
+
*
|
|
9
|
+
* На дев-сервере это валит машину в своп, демон раннера перестаёт успевать
|
|
10
|
+
* отвечать на ping за отведённые 60 секунд (`RUNNER_PING_INTERVAL_MS` ×
|
|
11
|
+
* `RUNNER_PONG_MAX_MISSES`), и 504 прилетают ПО ВСЕМ сессиям машины сразу.
|
|
12
|
+
*
|
|
13
|
+
* Полный разбор, таблица замеров и признак инцидента — `docs/standards/project-gotchas.md` §345.
|
|
14
|
+
* План и границы применимости — `docs/plans/active/claude-code-regex-guard.md`.
|
|
15
|
+
*
|
|
16
|
+
* ВРЕМЕННАЯ МЕРА. Это обход чужого дефекта, а не его исправление. Когда движок
|
|
17
|
+
* починят — снять целиком (`claude-code-regex-guard.md` §3.3).
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Граница числового счётчика, начиная с которой шаблон считается опасным.
|
|
21
|
+
*
|
|
22
|
+
* Измеренный порог взрыва — ровно 20, во всех сочетаниях (два счётчика, счётчик
|
|
23
|
+
* рядом с `.*`, счётчики без верхней границы, счётчик над группой). Рост
|
|
24
|
+
* экспоненциальный, примерно вчетверо на каждые +2. Замеры для двух счётчиков
|
|
25
|
+
* (`x.{0,N}y.{0,N}z`): 16 — 377 МБ, 18 — 1560 МБ, 20 — взрыв на 5.4 ГБ; для
|
|
26
|
+
* `.*` плюс один счётчик — 16 — 204 МБ, 18 — 831 МБ. Таблица целиком в §345.
|
|
27
|
+
*
|
|
28
|
+
* Берём 16 — запас в две ступени экспоненты. Ниже опускать нет смысла:
|
|
29
|
+
* повседневные шаблоны (IP, дата, semver, hex) держатся в пределах 6.
|
|
30
|
+
*/
|
|
31
|
+
export const QUANTIFIER_DANGER_BOUND = 16;
|
|
32
|
+
/**
|
|
33
|
+
* Разбирает шаблон и собирает участки переменной длины.
|
|
34
|
+
*
|
|
35
|
+
* Синтаксис не уточняется намеренно: `-G` (basic) и `-E` (extended) отличаются
|
|
36
|
+
* тем, где стоит обратный слэш, а взрывается движок одинаково в обоих. Поэтому
|
|
37
|
+
* и `\{n,m\}`, и `{n,m}` считаются счётчиком — в сторону осторожности.
|
|
38
|
+
*
|
|
39
|
+
* Группы отслеживаются стеком: группа считается широкой, если широкий матчер
|
|
40
|
+
* встретился где-то в её теле, и эта ширина поднимается наружу — счётчик над
|
|
41
|
+
* группой наследует её.
|
|
42
|
+
*/
|
|
43
|
+
function scan(pattern) {
|
|
44
|
+
const parts = [];
|
|
45
|
+
/** Широк ли атом непосредственно слева — то, к чему относится следующий счётчик. */
|
|
46
|
+
let lastWide = false;
|
|
47
|
+
/** По одному флагу на открытую группу: встретился ли внутри широкий матчер. */
|
|
48
|
+
const groups = [];
|
|
49
|
+
let i = 0;
|
|
50
|
+
const markWide = () => {
|
|
51
|
+
lastWide = true;
|
|
52
|
+
if (groups.length > 0)
|
|
53
|
+
groups[groups.length - 1] = true;
|
|
54
|
+
};
|
|
55
|
+
const openGroup = () => {
|
|
56
|
+
groups.push(false);
|
|
57
|
+
lastWide = false;
|
|
58
|
+
};
|
|
59
|
+
const closeGroup = () => {
|
|
60
|
+
const wide = groups.pop() ?? false;
|
|
61
|
+
lastWide = wide;
|
|
62
|
+
if (wide && groups.length > 0)
|
|
63
|
+
groups[groups.length - 1] = true;
|
|
64
|
+
};
|
|
65
|
+
while (i < pattern.length) {
|
|
66
|
+
const ch = pattern[i];
|
|
67
|
+
// Класс символов — проглатываем целиком, иначе его содержимое
|
|
68
|
+
// (`[{}]`, `[*+]`) прочтётся как синтаксис. Класс всегда широкий.
|
|
69
|
+
if (ch === '[') {
|
|
70
|
+
const end = classEnd(pattern, i);
|
|
71
|
+
if (end === null) {
|
|
72
|
+
// Незакрытая `[` — это литерал, а не класс. Проглотить остаток строки
|
|
73
|
+
// значило бы ослепить детектор на всё, что правее (QA MEDIUM-5:
|
|
74
|
+
// `awk -F'[' … | grep 'опасное'` проходил мимо).
|
|
75
|
+
lastWide = false;
|
|
76
|
+
i += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
markWide();
|
|
80
|
+
i = end;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === '\\') {
|
|
84
|
+
const next = pattern[i + 1];
|
|
85
|
+
if (next === undefined)
|
|
86
|
+
break;
|
|
87
|
+
if (next === 'Q') {
|
|
88
|
+
// `\Q…\E` — всё внутри литерал, синтаксиса там нет.
|
|
89
|
+
const stop = pattern.indexOf('\\E', i + 2);
|
|
90
|
+
i = stop < 0 ? pattern.length : stop + 2;
|
|
91
|
+
lastWide = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (next === '{') {
|
|
95
|
+
i = readQuantifier(pattern, i + 2, parts, lastWide, true);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (next === '+') {
|
|
99
|
+
parts.push({ bound: null, onWide: lastWide });
|
|
100
|
+
i += 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (next === ')') {
|
|
104
|
+
closeGroup();
|
|
105
|
+
i += 2;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (next === '(') {
|
|
109
|
+
openGroup();
|
|
110
|
+
i += 2;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (next === 'w' ||
|
|
114
|
+
next === 's' ||
|
|
115
|
+
next === 'd' ||
|
|
116
|
+
next === 'W' ||
|
|
117
|
+
next === 'S' ||
|
|
118
|
+
next === 'D') {
|
|
119
|
+
markWide();
|
|
120
|
+
i += 2;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Любой другой экранированный символ — обычный узкий атом.
|
|
124
|
+
lastWide = false;
|
|
125
|
+
i += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '{') {
|
|
129
|
+
const after = readQuantifier(pattern, i + 1, parts, lastWide, false);
|
|
130
|
+
// `readQuantifier` вернёт исходную позицию, если это не счётчик,
|
|
131
|
+
// а обычная фигурная скобка (например `function.*{.*}`).
|
|
132
|
+
if (after === i + 1) {
|
|
133
|
+
lastWide = false;
|
|
134
|
+
i += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
i = after;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (ch === '*' || ch === '+') {
|
|
141
|
+
parts.push({ bound: null, onWide: lastWide });
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ch === '(') {
|
|
146
|
+
openGroup();
|
|
147
|
+
i += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ch === ')') {
|
|
151
|
+
closeGroup();
|
|
152
|
+
i += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ch === '.') {
|
|
156
|
+
markWide();
|
|
157
|
+
i += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
lastWide = false;
|
|
161
|
+
i += 1;
|
|
162
|
+
}
|
|
163
|
+
return parts;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Конец класса символов `[...]`, с учётом `[]...]` и `[^]...]`.
|
|
167
|
+
* `null` — закрывающей скобки нет, то есть это была не скобка класса, а литерал.
|
|
168
|
+
*/
|
|
169
|
+
function classEnd(pattern, start) {
|
|
170
|
+
let i = start + 1;
|
|
171
|
+
if (pattern[i] === '^')
|
|
172
|
+
i += 1;
|
|
173
|
+
if (pattern[i] === ']')
|
|
174
|
+
i += 1; // первый `]` — литерал
|
|
175
|
+
while (i < pattern.length && pattern[i] !== ']') {
|
|
176
|
+
if (pattern[i] === '\\')
|
|
177
|
+
i += 1;
|
|
178
|
+
i += 1;
|
|
179
|
+
}
|
|
180
|
+
return i < pattern.length ? i + 1 : null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Читает тело счётчика начиная с позиции после открывающей скобки.
|
|
184
|
+
* Возвращает позицию после закрывающей — или `start`, если это не счётчик.
|
|
185
|
+
*/
|
|
186
|
+
function readQuantifier(pattern, start, parts, onWide, escaped) {
|
|
187
|
+
let i = start;
|
|
188
|
+
let body = '';
|
|
189
|
+
while (i < pattern.length) {
|
|
190
|
+
if (escaped && pattern[i] === '\\' && pattern[i + 1] === '}')
|
|
191
|
+
break;
|
|
192
|
+
if (!escaped && pattern[i] === '}')
|
|
193
|
+
break;
|
|
194
|
+
body += pattern[i];
|
|
195
|
+
i += 1;
|
|
196
|
+
}
|
|
197
|
+
// Незакрытый счётчик — шаблон всё равно не скомпилируется, разбор не роняем.
|
|
198
|
+
if (i >= pattern.length)
|
|
199
|
+
return start;
|
|
200
|
+
// Только цифры и запятая. `{.*}` в `function.*{.*}` — не счётчик, а скобка.
|
|
201
|
+
if (!/^\d+(,\d*)?$/.test(body))
|
|
202
|
+
return start;
|
|
203
|
+
const [minText, maxText] = body.split(',');
|
|
204
|
+
const min = Number.parseInt(minText ?? '', 10);
|
|
205
|
+
// `{n,}` — верхней границы нет, но состояний всё равно не меньше n:
|
|
206
|
+
// измерено, `x.{20,}y.{20,}z` взрывается так же, как `{0,20}`.
|
|
207
|
+
const bound = maxText === undefined || maxText === '' ? min : Number.parseInt(maxText, 10);
|
|
208
|
+
parts.push({ bound: Number.isFinite(bound) ? bound : min, onWide });
|
|
209
|
+
return i + (escaped ? 2 : 1);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Взорвёт ли этот шаблон встроенный движок Claude Code?
|
|
213
|
+
*
|
|
214
|
+
* Условие (полностью выведено из замеров, см. §345):
|
|
215
|
+
* есть счётчик с границей ≥ порога, применённый к ОДИНОЧНОМУ матчеру,
|
|
216
|
+
* И в шаблоне есть ещё хотя бы один участок переменной длины.
|
|
217
|
+
*
|
|
218
|
+
* Одиночный счётчик безопасен при любом размере (`{0,5000}` — 14 МБ), шаблон
|
|
219
|
+
* без счётчиков безопасен при любом числе `.*` — взрыв даёт именно сочетание.
|
|
220
|
+
*/
|
|
221
|
+
export function inspectPattern(pattern) {
|
|
222
|
+
if (typeof pattern !== 'string')
|
|
223
|
+
return { dangerous: false };
|
|
224
|
+
const parts = scan(pattern);
|
|
225
|
+
const heavy = parts.filter((p) => p.onWide && p.bound !== null && p.bound >= QUANTIFIER_DANGER_BOUND);
|
|
226
|
+
if (heavy.length === 0 || parts.length < 2)
|
|
227
|
+
return { dangerous: false };
|
|
228
|
+
const worst = Math.max(...heavy.map((p) => p.bound ?? 0));
|
|
229
|
+
return {
|
|
230
|
+
dangerous: true,
|
|
231
|
+
reason: `Поиск остановлен: счётчик {…,${worst}} рядом с ещё одним участком переменной длины. ` +
|
|
232
|
+
`Встроенный в Claude Code движок на такой форме занимает ~5.4 ГБ и ~67 секунд ещё ` +
|
|
233
|
+
`на разборе шаблона — это роняет дев-сервер и рвёт связь по всем сессиям машины ` +
|
|
234
|
+
`(подробности: docs/standards/project-gotchas.md §345). ` +
|
|
235
|
+
`Повтори через «command grep» — системный grep обрабатывает этот шаблон нормально. ` +
|
|
236
|
+
`Либо уменьши границу счётчика ниже ${QUANTIFIER_DANGER_BOUND}.`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* `grep` как отдельное слово — то есть вызов, который перехватит функция-обёртка
|
|
241
|
+
* Claude Code.
|
|
242
|
+
*
|
|
243
|
+
* В класс-разделитель входят кавычки и обратный слэш. Это не педантизм: `\grep`,
|
|
244
|
+
* `'grep'` и `"grep"` подавляют раскрытие АЛИАСОВ, но функцию bash не минуют —
|
|
245
|
+
* проверено стендом с локальной `function grep`, вызов происходит во всех трёх
|
|
246
|
+
* случаях. А `\grep` — общеизвестный приём «обойти обёртку», и модель, только что
|
|
247
|
+
* получившая отказ, попробует именно его.
|
|
248
|
+
*
|
|
249
|
+
* `pgrep`, `zgrep` и `/usr/bin/grep` сюда НЕ попадают: обёртка подменяет только
|
|
250
|
+
* имя команды, а полный путь и другие имена уходят мимо неё.
|
|
251
|
+
*/
|
|
252
|
+
const BARE_GREP = /(^|[\s;&|(`$'"\\])grep\b/;
|
|
253
|
+
/**
|
|
254
|
+
* Вызовы, которые до подменённой функции не доходят.
|
|
255
|
+
*
|
|
256
|
+
* Подмена в снимке оболочки Claude Code — это функция bash с именем `grep`
|
|
257
|
+
* (`function grep { … ARGV0=ugrep … }`). Её обходит всё, что запускает бинарь
|
|
258
|
+
* напрямую, минуя разрешение имён оболочки:
|
|
259
|
+
*
|
|
260
|
+
* - `command grep` — ровно то, что привратник советует в отказе; не пропускать
|
|
261
|
+
* его значило бы завести совет в тупик;
|
|
262
|
+
* - `git grep` — своя реализация внутри git, к движку Claude Code отношения не имеет;
|
|
263
|
+
* - `xargs`, `sudo`, `exec`, `find -exec` — исполняют файл, а не функцию.
|
|
264
|
+
*
|
|
265
|
+
* Блокировать их незачем: дефекта там нет, а совет «повтори через command grep»
|
|
266
|
+
* был бы просто непонятным.
|
|
267
|
+
*/
|
|
268
|
+
const BYPASS = /(^|[\s;&|(`$'"\\])(command|git|sudo|xargs|exec)\s+grep\b|-exec\s+grep\b/g;
|
|
269
|
+
/**
|
|
270
|
+
* Разделители команд в оболочке.
|
|
271
|
+
*
|
|
272
|
+
* Разбор посегментно, а не по всей строке целиком, решает сразу три беды:
|
|
273
|
+
* шаблон одной команды не приписывается другой; безобидный `| grep -c ERROR`
|
|
274
|
+
* после обойдённого `command grep` не возвращает модели тот же отказ, которому
|
|
275
|
+
* она уже последовала; и синтаксический мусор из соседней команды (незакрытая
|
|
276
|
+
* скобка в аргументе `awk`) не ослепляет разбор дальше по строке.
|
|
277
|
+
*/
|
|
278
|
+
const SEGMENT_SPLIT = /\|\||&&|[|;\n]/;
|
|
279
|
+
/**
|
|
280
|
+
* Опасен ли шаблон внутри команды оболочки?
|
|
281
|
+
*
|
|
282
|
+
* Сегмент разбирается целиком, а не выделенный из него аргумент: надёжно
|
|
283
|
+
* вычленить шаблон из произвольной команды нельзя (кавычки, подстановки), а
|
|
284
|
+
* форма `{n,m}` вне регулярного выражения почти не встречается. Ошибка в сторону
|
|
285
|
+
* осторожности здесь стоит одного лишнего круга диалога, ошибка в другую
|
|
286
|
+
* сторону — лежащего дев-сервера.
|
|
287
|
+
*
|
|
288
|
+
* Чего НЕ ловит намеренно: разорванное слово (`gr""ep`). Привратник защищает от
|
|
289
|
+
* случайного самострела, а не от умышленного обхода — и второй линией остаётся
|
|
290
|
+
* клетка сессии (`runner-memory-ceiling.md` §6).
|
|
291
|
+
*/
|
|
292
|
+
export function inspectBashCommand(command) {
|
|
293
|
+
if (typeof command !== 'string')
|
|
294
|
+
return { dangerous: false };
|
|
295
|
+
for (const segment of command.split(SEGMENT_SPLIT)) {
|
|
296
|
+
// `eval "grep …"` исполняет содержимое строки как команду — заглядываем внутрь.
|
|
297
|
+
const remaining = segment.replace(BYPASS, '$1__bypassed__');
|
|
298
|
+
if (!BARE_GREP.test(remaining))
|
|
299
|
+
continue;
|
|
300
|
+
const verdict = inspectPattern(segment);
|
|
301
|
+
if (verdict.dangerous)
|
|
302
|
+
return verdict;
|
|
303
|
+
}
|
|
304
|
+
return { dangerous: false };
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Решение по тому, что прислал Claude Code в хук `PreToolUse`.
|
|
308
|
+
*
|
|
309
|
+
* Незнакомый инструмент и любой неожиданный вход — пропуск. Привратник обязан
|
|
310
|
+
* ошибаться в сторону «пропустить»: заблокированная по недоразумению работа
|
|
311
|
+
* агента дороже пропущенного шаблона, от которого есть вторая линия защиты
|
|
312
|
+
* (клетка сессии, `runner-memory-ceiling.md` §6).
|
|
313
|
+
*/
|
|
314
|
+
export function decideForTool(toolName, toolInput) {
|
|
315
|
+
if (!toolInput || typeof toolInput !== 'object')
|
|
316
|
+
return { dangerous: false };
|
|
317
|
+
const input = toolInput;
|
|
318
|
+
if (toolName === 'Bash') {
|
|
319
|
+
return typeof input['command'] === 'string'
|
|
320
|
+
? inspectBashCommand(input['command'])
|
|
321
|
+
: { dangerous: false };
|
|
322
|
+
}
|
|
323
|
+
// Инструмент Grep проверяется тем же правилом намеренно, хотя движок под ним
|
|
324
|
+
// может быть другим: цена перестраховки — один круг диалога, и она измерена
|
|
325
|
+
// как нулевая на повседневных шаблонах.
|
|
326
|
+
if (toolName === 'Grep') {
|
|
327
|
+
return typeof input['pattern'] === 'string'
|
|
328
|
+
? inspectPattern(input['pattern'])
|
|
329
|
+
: { dangerous: false };
|
|
330
|
+
}
|
|
331
|
+
return { dangerous: false };
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Превращает полезную нагрузку хука в решение.
|
|
335
|
+
*
|
|
336
|
+
* ЛЮБАЯ неожиданность — пропуск. Привратник стоит на пути каждой команды агента,
|
|
337
|
+
* и его собственная ошибка не должна останавливать работу: пропущенный шаблон
|
|
338
|
+
* прикрыт второй линией (клеткой сессии), а привратник, блокирующий всё подряд
|
|
339
|
+
* из-за смены формата входа, останавливает разработку целиком.
|
|
340
|
+
*/
|
|
341
|
+
export function hookDecision(payload) {
|
|
342
|
+
let parsed;
|
|
343
|
+
try {
|
|
344
|
+
parsed = JSON.parse(payload);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return { exitCode: 0, message: '' };
|
|
348
|
+
}
|
|
349
|
+
if (!parsed || typeof parsed !== 'object')
|
|
350
|
+
return { exitCode: 0, message: '' };
|
|
351
|
+
const { tool_name: toolName, tool_input: toolInput } = parsed;
|
|
352
|
+
if (typeof toolName !== 'string')
|
|
353
|
+
return { exitCode: 0, message: '' };
|
|
354
|
+
const verdict = decideForTool(toolName, toolInput);
|
|
355
|
+
return verdict.dangerous
|
|
356
|
+
? { exitCode: 2, message: verdict.reason ?? '' }
|
|
357
|
+
: { exitCode: 0, message: '' };
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=regex-guard.js.map
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -377,6 +377,22 @@ export declare class Supervisor {
|
|
|
377
377
|
* resolves them and a refusal puts exactly those records back in the queue —
|
|
378
378
|
* the message is retired from disk only once an agent has it.
|
|
379
379
|
*/
|
|
380
|
+
/**
|
|
381
|
+
* Should this failed turn be retried by itself, and if so, arm it (#252, #257).
|
|
382
|
+
*
|
|
383
|
+
* Returns `true` when a retry is armed, and the caller must then emit NOTHING —
|
|
384
|
+
* a `turn_end{ok:false}` files the session as FAILED, which is terminal, and a
|
|
385
|
+
* session we intend to retry cannot be told it has ended.
|
|
386
|
+
*
|
|
387
|
+
* The owner's standing priority governs every branch here: «лучше, чтобы ретрай
|
|
388
|
+
* не сработал, чем сработал там, где не нужно». So this reads as a list of
|
|
389
|
+
* reasons to decline, and the permission is the last thing it reaches.
|
|
390
|
+
*/
|
|
391
|
+
private armApiRetry;
|
|
392
|
+
/** Fire an armed retry — re-checking, at FIRE time, everything that could have changed. */
|
|
393
|
+
private runApiRetry;
|
|
394
|
+
/** Disarm a pending retry — always through here, so no timer is ever orphaned. */
|
|
395
|
+
private clearApiRetry;
|
|
380
396
|
private deliverMessage;
|
|
381
397
|
/**
|
|
382
398
|
* Nothing could take the message: put it back where it came from.
|