spec(router-gate): v3.2 closes 18 holes from v4 adversarial audit

7 секций фиксов A-G закрывают 16 из 18 новых holes (2 → §9 acceptable residual):

- A §3.1 protected paths +7 (registry, helpers, prompt-prefix, gate-config, package.json, composer.json) — закрывает C1-C5
- B §3 chain-state: user-content-only substring (H1), keyword list trimmed (S7), PostToolUse chain_step++ (S3), transcript SoT clarified (S2)
- C §5.1 Bash: tokenizer +& (H3), session-scoped file-watcher (C6), git --output blacklist (C8), path-deny на все read commands (C9/S4), node -r blacklist (C10). §5.2 glob-aware (C11) + 1-level imports (E1)
- D §4.5 default-CLOSE: unmatched answer → gate remains locked (H4)
- E §3.2.0 smoke-test env propagation pre-impl (H2) + §3.2 path hardening derive from session-id (C7) + §3.4 BLOCKED-protocol enforced parent-side (S5)
- F §4 Поведение 1 case-insensitive + morphology (E3) + stale-registry fall-through (S1). §4.7 length-ratio 4× (E2)
- G §9 open questions: S6 (2-AskUser limit) UX-tradeoff acceptable, S8 (BRANCH-SWITCH controller-writable) → follow-up эпик

Implementation budget: 13.5-20h → 18-27h (+5-7h за smoke-test, PostToolUse migration, Bash hardening, path-args overlay).

Audit methodology: audit-context-building skill + ручной adversarial разбор по 13 attack-зонам. Brainstorming через superpowers:brainstorming для дизайна правок (scope=all, H4=default-CLOSE, H2=smoke-test через AskUserQuestion).

cspell-words.txt sync: +4 валидных терминов (уйте/инкрементирован/матчащий/неверифицирована).

Verify-sentinel: 1179/1179 vitest tools-only GREEN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-29 07:31:27 +03:00
parent bd8ec88e9f
commit 832fadbcc3
3 changed files with 127 additions and 45 deletions
+6
View File
@@ -1878,3 +1878,9 @@ ambig
deplo
обнулился
Ревьюер
# Router-gate v3.2 (2026-05-29) — adversarial audit closure
уйте
инкрементирован
матчащий
неверифицирована
+14 -14
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-05-29T03:45:53.725Z
Last updated: 2026-05-29T04:10:14.576Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -8,13 +8,13 @@ Last updated: 2026-05-29T03:45:53.725Z
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
| C4 Сигнальный статус | ✅ | This file (self-reference) |
| C5 Observer-coverage | ⚠️ | 672 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
| C5 Observer-coverage | ⚠️ | 680 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
## Метрики (информационные, не алерты)
- Observer evidence: 672 episodes this month, 0 observer_error markers, 131 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 533
- Observer evidence: 680 episodes this month, 0 observer_error markers, 134 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 541
- Last /brain-retro: 2 day(s) ago
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 20. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
@@ -24,16 +24,16 @@ Baseline дисциплины роутера (этап 2 router discipline overh
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|---|---|---|---|
| analysis | 30 | 26.7% | 13.3% |
| analysis | 31 | 25.8% | 16.1% |
| bugfix | 18 | 22.2% | 27.8% |
| planning | 17 | 17.6% | 17.6% |
| feature | 16 | 12.5% | 0.0% |
| cleanup | 6 | 0.0% | 0.0% |
| refactor | 1 | 0.0% | 0.0% |
Router step distribution: 1: 303, 2: 237, 3: 63, 5: 60
Router step distribution: 1: 307, 2: 238, 3: 63, 5: 61
Boundaries applied (ADR / границы): 75 of 663 эпизодов (11.3%).
Boundaries applied (ADR / границы): 75 of 669 эпизодов (11.2%).
## Активные многоэтапные проекты
@@ -51,10 +51,10 @@ Boundaries applied (ADR / границы): 75 of 663 эпизодов (11.3%).
| Компонент | Токены (in/out) | USD |
|---|---|---|
| Classifier (Sonnet 4.6) | 3660/49643 | $0.76 |
| Classifier (Sonnet 4.6) | 3964/54203 | $0.82 |
| Self-assessment (Sonnet 4.6) | 0/0 | $0.00 |
| Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 |
| **Итого** | | **$0.76** |
| **Итого** | | **$0.82** |
## Аномалии классификатора
@@ -67,7 +67,7 @@ Episodes since last run: 542 / threshold: 10
## Reviewer: субагент vs fallback
0 эпизодов проверено из 672.
0 эпизодов проверено из 680.
## Reviewer findings
@@ -109,10 +109,10 @@ Episodes since last run: 542 / threshold: 10
| Фраза | За всё время | За сегодня |
|---|---|---|
| `recovery` | 914 | 17 ⚠️ |
| `recovery` | 917 | 20 ⚠️ |
| `ремонт инфраструктуры` | 229 | 44 ⚠️ |
| `без скилов` | 201 | 23 ⚠️ |
| `срочно` | 141 | 48 ⚠️ |
| `без скилов` | 223 | 45 ⚠️ |
| `срочно` | 144 | 51 ⚠️ |
| `memory dump` | 17 | 0 |
| `direct ok` | 6 | 0 |
| `быстрый коммит` | 3 | 0 |
@@ -123,7 +123,7 @@ Episodes since last run: 542 / threshold: 10
| PID | Имя | CPU-время | Возраст |
|---|---|---|---|
| 3464 | MsMpEng | 1.09ч | 0.0ч |
| 3464 | MsMpEng | 1.14ч | NaNч |
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
@@ -1,7 +1,7 @@
# Router-gate hard wall — Дизайн-спецификация (Уровень 4) v3.1
# Router-gate hard wall — Дизайн-спецификация (Уровень 4) v3.2
**Дата:** 2026-05-28
**Версия:** v3.1 (clarification pass — TL;DR, schemas, test strategy, success metrics, cross-refs)
**Дата:** 2026-05-29
**Версия:** v3.2 (adversarial audit v4 closure — 18 новых holes закрыто через 7 секций фиксов A-G)
**Автор:** Claude (controller Opus 4.7) под руководством заказчика Дмитрия
**Статус:** Approved by owner — готов к плану implementation
**Тип:** feature — enforcement architecture rewrite
@@ -32,7 +32,7 @@
- Gate budget 2s + fail-CLOSE при таймаутах.
- Bash blocks sub-shells (`backticks`, `$()`, `<()`, `<<` heredocs) + file-watcher для script execution + static content scan.
**Цена:** 13.5-20 часов implementation в 6 этапов через subagent-driven-development. **Закрыто 20 holes** через 3 раунда adversarial audit (v1 → v2 → v3).
**Цена:** 18-27 часов implementation в 6 этапов через subagent-driven-development. **Закрыто 38 holes** через 4 раунда adversarial audit (v1 → v2 → v3 → v3.2).
**Recovery:** заказчик соглашается быть recovery-каналом ручной правкой `.claude/settings.json` / state-файлов при ошибочном lockout.
@@ -40,6 +40,18 @@
**Сохраняется:** 7 preserved хуков (tdd-gate / coverage-verify / memory-coverage / verify-before-push / rationalization-audit / prompt-injection / branch-switch) — у них своя семантика, не про router. 6 из них теряют свои findOverride escape-фразы — становятся hard-walls тоже.
**Changes v3.1 → v3.2:** adversarial audit v4 контроллера выявил 18 новых holes (4 fatal + 11 critical + 8 serious + 3 edge через v3.1 архитектуру). Закрытие через 7 секций фиксов A-G:
- **A** §3.1 protected paths расширен на 7 позиций (registry, helpers, prompt-prefix, gate-config explicit, package.json, composer.json) — закрывает C1-C5 (вся защита gate'а через single-list).
- **B** §3 chain-state TTL substring-match restricted to user-content only (H1) + keyword list trimmed (S7 false-positive `другое`) + chain_step++ на PostToolUse success (S3) + transcript-lookback role clarified (S2).
- **C** §5.1 Bash hardening: tokenizer +`&` background (H3), file-watcher session-scoped (C6), git `--output`/`-o` blacklisted (C8), path-deny overlay для всех read commands (C9, S4), node `-r`/`--require`/`--import` blacklisted (C10). §5.2 glob-aware: vitest glob targets → AskUser required (C11) + scan follows direct imports 1-level (E1).
- **D** §4.5 default-CLOSE: любой unmatched AskUser answer → gate остаётся locked + новая enum-value `no_match_remain_blocked` (H4 keyword fragility).
- **E** §3.2.0 (новая) — smoke-test env propagation до Этапа 2.1 с pass/fail criteria (H2). §3.2 path hardening — parent state path derived из session-id, not arbitrary (C7). §3.4 BLOCKED-protocol enforced parser parent-side (S5).
- **F** §4 Поведение 1 case-insensitive Cyrillic + morphology `(делай|сделай|вызови|примени|используй)` (E3) + fall-through silent при stale registry (S1). §4.7 length-bias threshold 4× (E2).
- **G** §9 open questions: S6 (2-AskUser limit) acceptable UX-tradeoff, S8 (BRANCH-SWITCH-CONFIRMED controller-writable) → отдельный follow-up эпик (known residual risk).
Budget impact: +5-7h to §8 implementation estimate.
**Changes v2 → v3:** §3.2 переписан под env-based subagent inheritance (вместо text-prefix). Добавлены §3.4 Subagent constraints (no AskUser, no recursive Task), §3.5 Atomic state writes / file locking, §3.6 Gate budget / timeout (2s hard limit, fail-CLOSE), §5.2 Static content scan для node/python/vitest. §3.1 расширен path normalization (resolve + realpath + case-fold). §4.5 ограничен max 2 AskUserQuestion per turn. §4.7 при silence использует task_classification вместо recommendation. §5.1 добавлены file-watcher + broad sweep hard-blacklist для sub-shells. §7 добавлен coverage-hint coordination layer. §8 пересчитан (13.5-20h эпик).
**Changes v1 → v2:** добавлены §3.1 (protected paths), §3.2 (subagent inheritance), §3.3 (failure modes), §4.5 (AskUserQuestion answer parsing), §4.6 (post-skill partial unlock), §4.7 (question quality detector), §5.1 (Bash content rules). §4 Поведение 1 переписан со strict whitelist.
@@ -147,12 +159,17 @@ Self-retrospect #2 (28.05 вечер) подтвердил: 5 commitments утр
Список путей, **никогда** не разрешённых для Edit / Write / MultiEdit / NotebookEdit и Bash-команд rm / mv / cp / redirects (`>` / `>>`):
- `~/.claude/runtime/*` — все gate state-файлы (router-state, chain-state, askuser-decisions, gate-decisions, gate-errors, subagent-inheritance, coverage-hint)
- `~/.claude/runtime/*` — все gate state-файлы (router-state, chain-state, askuser-decisions, gate-decisions, gate-errors, subagent-inheritance, coverage-hint) **и `gate-config.json` тоже** (explicit-mention — закрывает C4 из v4-аудита, чтобы glob-интерпретация не разошлась).
- `.claude/settings.json` / `.claude/settings.local.json`
- `.claude/skills/**/SKILL.md` — skill definitions
- `tools/enforce-*.mjs` — все enforcement-хуки (включая `enforce-router-gate.mjs`)
- `tools/router-classifier.mjs` + dependents в `tools/router-*.mjs`
- `tools/router-gate-decide.mjs` + dependents (новые модули gate'а)
- **`tools/enforce-hook-helpers.mjs`** — содержит `sessionToolUses` / `findOverride` stubs / `logHookOutcome` — gate читает transcript через них. Если patched — falsify «askuser_called=true». (C2 v4-audit)
- **`tools/subagent-prompt-prefix.mjs`** — устанавливает env vars для субагента (§3.2). Если patched — субагент стартует без inheritance flags → undefined behavior. (C3 v4-audit)
- **`tools/registry-load.mjs`** — gate использует для alias resolution (skill ↔ #N ↔ slug). Если patched — direct-invocation strict whitelist матчит fake aliases. (C1 v4-audit)
- **`docs/registry/nodes.yaml`** — Single Source of Truth для skill/node names. Если patched — fake skill добавляется и `делай <fake>` проходит. (C1 v4-audit)
- **`package.json`** / **`composer.json`** — npm-scripts (`npm test`, `npm run lint:*`) и composer-команды в Bash-whitelist. Если patched — `"test": "rm -rf ..."` runs evil под whitelist. (C5 v4-audit)
Hard-deny check выполняется **ПЕРВЫМ** в decision flow, до любой recommendation logic. Никакой unlock (Skill match / AskUserQuestion / direct invocation) не отменяет hard-deny. Изменения этих файлов — только через заказчика снаружи Claude.
@@ -167,11 +184,17 @@ Hard-deny check выполняется **ПЕРВЫМ** в decision flow, до
**Закрывает Дыру 4 (v1)** и **Дыру 15 (v2)**.
### 3.2.0. Smoke-test env propagation (closes H2 v4-audit) — REQUIRED BEFORE §3.2 IMPLEMENTATION
**Pre-flight task Этапа 2.1 в плане implementation.** Acceptance criteria: minimal Claude session с тестовым parent prompt'ом ставит `process.env.CLAUDE_TEST_PROBE='42'`, спавнит субагент через Task tool, субагент в первом же tool_use возвращает значение `process.env.CLAUDE_TEST_PROBE`. **Pass:** `42` → env propagation работает, §3.2 реализуется по env-based design. **Fail:** undefined / другое значение → §3.2 design не работоспособен, **stop эпик** и открыть отдельную задачу «subagent inheritance через file-based handshake» (Plan B: `subagent-prompt-prefix.mjs` пишет inheritance-файл по детерминированному имени `~/.claude/runtime/subagent-inheritance-<parent-sess>-LATEST.json` + субагент читает по convention с mtime check + flock для concurrent Task'ов).
Budget: 0.5h на smoke-test setup + verification. Without this — §3.2 architectural assumption неверифицирована → риск 5-10h rework после полной реализации.
### 3.2. Subagent gate inheritance (env-based, NOT text-prefix)
V2 полагалась на text-prefix через `subagent-prompt-prefix.mjs` — субагент читает инструкцию и **должен** наследовать. V3 audit выявил что это **не enforcement**, а просьба. Sonnet субагенты могут проигнорировать (Phase 4 Task A прецедент).
V3 переходит на **environment-based inheritance**:
V3 переходит на **environment-based inheritance** (условно — pending §3.2.0 smoke-test pass):
При вызове Task tool gate (PreToolUse):
@@ -180,21 +203,22 @@ V3 переходит на **environment-based inheritance**:
```
{
"parent_session_id": "<parent-id>",
"parent_router_state_path": "~/.claude/runtime/router-state-<parent-id>.json",
"parent_chain_state_path": "~/.claude/runtime/chain-state-<parent-id>.json",
"allowed_actions": ["Skill(matching)", "Task(subagent_type=matching)", ...],
"created_at": "<iso-ts>"
}
```
**NB (closes C7 v4-audit — path-injection):** поля `parent_router_state_path` / `parent_chain_state_path` **удалены** из schema. Субагент gate **derives** оба пути **hardcoded pattern** из `parent_session_id`: `path.join(os.homedir(), '.claude', 'runtime',`router-state-${parent_session_id}.json`)` и аналогично для chain-state. Принимать произвольные absolute paths из inheritance-файла нельзя — контроллер мог бы записать фейковый router-state в `/tmp/fake-state.json` и указать его → субагент читает фейк → bypass.
2. Хук `subagent-prompt-prefix.mjs` устанавливает env vars для процесса субагента:
- `CLAUDE_PARENT_SESSION_ID=<parent-id>`
- `CLAUDE_GATE_INHERIT=true`
- `CLAUDE_INHERITANCE_FILE=~/.claude/runtime/subagent-inheritance-<task-id>.json`
3. Gate-хук **тот же** `enforce-router-gate.mjs` (тот же `.claude/settings.json` наследуется субагентом) при старте проверяет:
- Если `CLAUDE_GATE_INHERIT=true` И inheritance-файл существует и валиден → читает parent's state files (router-state, chain-state) из `parent_router_state_path` и `parent_chain_state_path`.
- Если `CLAUDE_GATE_INHERIT=true` И inheritance-файл существует и валиден → derives parent state paths hardcoded из `parent_session_id` (см. C7-fix выше) и читает parent's router-state, chain-state.
- Свой собственный router-state субагента игнорируется (пустой при старте — silence-режим был бы deadlock).
- **Если `CLAUDE_GATE_INHERIT` undefined / inheritance-файл missing / malformed → fail-CLOSE** (block all mutating tools, reason: «subagent inheritance missing — escalate to parent»). Эта политика symmetric с §3.3 «fail-CLOSE при missing state».
- Decisions субагента логируются в **parent's** `router-gate-decisions.jsonl` с префиксом `[subagent:<task-id>]`.
4. Inheritance-файл удаляется когда Task tool возвращает результат родителю.
@@ -210,7 +234,15 @@ V3 переходит на **environment-based inheritance**:
- **Limit parallel Task**: max 3 параллельных Task tool calls в одном turn'е родителя. 4-й и далее → block с reason «parallel subagent limit reached».
- При lockout субагент возвращает Task tool response: `{status: "BLOCKED", reason: "<why>", parent_should_askuser: true}`. Родитель видит → сам эскалирует к заказчику через свой AskUserQuestion.
**Закрывает Дыру 18** (multiple subagents fan-out) и **завершает закрытие Дыры 11**.
**BLOCKED-protocol enforcement parent-side (closes S5 v4-audit):** parent gate при PreToolUse следующего tool-use **после** Task return парсит last 200 chars subagent response на JSON marker `{"status":"DONE"|"BLOCKED",...}`. Возможные случаи:
- Marker `{"status":"DONE",...}` найден → parent продолжает по обычному flow (subagent отработал).
- Marker `{"status":"BLOCKED",...}` найден → parent **обязан** вызвать AskUserQuestion в next response (gate блокирует любые mutating tools пока askuser не вызван).
- Marker **отсутствует или malformed** → assume BLOCKED (defensive). Reason: «subagent did not return status marker — treat as blocked, escalate». Закрывает S5: текстовая конвенция без enforcement позволяла субагенту просто промолчать о lockout и parent продолжал бы дальше.
Парсер ищет JSON object в last 200 chars regex'ом `\{[^{}]*"status"\s*:\s*"(DONE|BLOCKED)"[^{}]*\}`. Простая heuristic — false-positive (legitimate text упоминающий status в last 200 chars) минимален, false-negative (subagent забыл marker) корректно обрабатывается как defensive-BLOCKED.
**Закрывает Дыру 18** (multiple subagents fan-out), **завершает закрытие Дыры 11**, и **закрывает S5 v4-audit**.
### 3.5. Atomic state writes / file locking
@@ -282,15 +314,19 @@ Gate имеет жёсткий бюджет времени **2 секунды**
}
```
Инициализируется когда router выдаёт chain. Обновляется при matching skill invocation (chain_step++). Очищается когда `chain_step >= chain_active.length` или router даёт другую цепочку (обнуляется и переинициализируется).
Инициализируется когда router выдаёт chain. Обновляется PostToolUse-handler'ом при **success** matching skill invocation chain_step++ (см. v3.2/S3 fix ниже: НЕ на PreToolUse `tool_input` match чтобы skill упавший mid-execution не прогрессировал chain без работы). Очищается когда `chain_step >= chain_active.length` или router даёт другую цепочку (обнуляется и переинициализируется).
**Chain-state TTL (closes Дыра 10):** chain-state считается expired и удаляется если:
**Chain-state TTL (closes Дыра 10 + H1/S7 v4-audit):** chain-state считается expired и удаляется если:
- `initialized_at + 24h < now` — цепочка протухла.
- User prompt содержит явные маркеры новой задачи: `новая задача` / `забудь` / `другое` / `новая работа` / `сброс` (case-insensitive substring).
- **`transcript[-1].user.content`** (only — НЕ assistant turns; **закрывает H1 v4-audit** — controller не может self-clear chain упоминанием маркера в собственном response) содержит явные explicit-reset фразы: `новая задача`, `сброс контекста`, `забудь предыдущее` (case-insensitive substring, точный match без частичных вхождений). **Удалены из keyword list:** `другое` (false-positive на «попробуем другое решение»), `новая работа` (false-positive на «новая работа в проекте»), `забудь` без суффикса (слишком broad) — **закрывает S7 v4-audit.**
При expiry — chain-state удаляется, gate переходит в Поведение 4 (silence) если новый router-state не даёт recommendation.
**Chain progression semantics (closes S3 v4-audit):** `chain_step++` инкрементируется НЕ при PreToolUse `tool_input` match, а при **PostToolUse success** matching skill/Task. Это требует регистрации gate ещё на PostToolUse event (lightweight handler — только инкремент chain_step при `tool_use_id` соответствующем skill, который PreToolUse разрешил по chain_step). Иначе skill упавший mid-execution прогрессирует chain без работы. Реализация: PostToolUse-handler читает `chain-state.json`, проверяет `tool_use_id` против `expected_node`, инкрементирует если success (PostToolUse fires только на success).
**Chain SoT clarification (closes S2 v4-audit):** для определения `chain_step` gate использует **`chain-state.json::chain_step` как Single Source of Truth**, НЕ transcript-lookback. Transcript-lookback (§3.6, 5 turns) используется только для current-turn флагов `askuser_called_this_turn` / `skill_invoked_matching` / `is_direct_invocation` — они per-turn, помещаются в окно. Это закрывает S2 (длинные chain >5 turns теряли step-detection через transcript-only).
### Новый файл логирования
`~/.claude/runtime/router-gate-decisions.jsonl`:
@@ -309,14 +345,14 @@ Gate определяет одно из 4 поведений на основе s
### Поведение 1 — Прямое поручение заказчика (Direct invocation) — STRICT v2
**Детектор: strict whitelist (никаких fuzzy regex).** Direct invocation **только** через literal pattern match:
**Детектор: strict whitelist (никаких fuzzy regex).** Direct invocation **только** через literal pattern match. **Все regex case-insensitive Cyrillic-aware (E3 v4-audit)** через `/.../iu` flags:
| Pattern | Пример | Что разрешает |
|---|---|---|
| `^/[a-z0-9_-]+($\|\s)` | `/brain-retro` / `/code-review --ultra` | Slash-команда в начале prompt'а |
| `вызови Skill\([^)]+\)` | `вызови Skill(superpowers:writing-plans)` | Literal call-syntax |
| `используй #\d+` | `используй #19` | Literal по реестру |
| `делай <exact-skill-name>` | `делай subagent-driven-development` | Literal имя из реестра (точное совпадение с node `name` или `slug`) |
| `(вызови\|примени)\s+Skill\([^)]+\)` | `вызови Skill(superpowers:writing-plans)` | Literal call-syntax |
| `использ(уй\|уйте)\s+#\d+` | `используй #19` / `Используйте #19` | Literal по реестру |
| `(делай\|сделай\|вызови\|примени\|используй)\s+<exact-skill-name>` | `делай subagent-driven-development` / `Сделай writing-plans` | Literal имя из реестра (точное совпадение с node `name` или `slug`). **Morphology variants (E3 v4-audit)** — 5 префиксов, capture точного имени после whitespace. |
**НЕ детектится как direct invocation:**
@@ -331,6 +367,8 @@ Gate определяет одно из 4 поведений на основе s
- Tool-call НЕ матчит → **block** с reason: «заказчик указал X через direct invocation, текущий tool — другое».
- Read-only baseline (Read/Grep/Glob/LS/TodoWrite/AskUserQuestion) разрешены всегда (как в обычном flow).
**Stale registry fall-through (closes S1 v4-audit):** если префикс-pattern matched (например `делай foo-bar`), но `foo-bar` **не находится** в registry (`docs/registry/nodes.yaml` через `tools/registry-load.mjs`) — gate **НЕ** блокирует с ошибкой «unknown skill». Вместо этого treat как **отсутствие direct invocation** и fall through к Поведениям 2/3/4 (router-driven flow). Reason: реестр может быть stale (skill переименован), user-intent не должен застревать на lookup-miss. Логируется в decisions.jsonl как `direct_invocation_attempted_unmatched_registry: <name>` для surface в brain-retro.
**Multiple direct invocations в одном prompt'е (closes Дыра 8):**
Если в prompt'е несколько direct invocations (например `вызови writing-plans, потом делай subagent-driven`):
@@ -364,10 +402,10 @@ Gate инициализирует chain-state (если не инициализ
| Состояние turn'а | Tool | Решение |
|---|---|---|
| askuser_called=false, expected_skill_invoked=false | Safe base | allow |
| askuser_called=false, expected_skill_invoked=false | Skill matching expected_node / Task subagent_type matching | allow + unlock turn + chain_step++ |
| askuser_called=false, expected_skill_invoked=false | Skill matching expected_node / Task subagent_type matching | allow + unlock turn (chain_step++ происходит на PostToolUse success — см. §3 chain-state v3.2/S3) |
| askuser_called=false, expected_skill_invoked=false | Любой mutating tool / Skill non-matching | block — «Цепочка [...], сейчас ждём шаг N (X). Вызови AskUserQuestion для одобрения» |
| askuser_called=true | Любой tool | allow |
| expected_skill_invoked=true | Любой tool | allow + chain_step++ |
| expected_skill_invoked=true | Любой tool | allow (chain_step уже инкрементирован PostToolUse-handler'ом при success предыдущего skill) |
| chain_step >= chain_active.length | Любой tool | allow (chain complete, clear chain-state) |
### Поведение 4 — Роутер молчит
@@ -404,6 +442,7 @@ Gate не unlock'ает turn просто по факту вызова AskUserQu
| Выбранная опция матчит конкретный skill/tool/action в формуле «делать X» / «выполнить X» / «использовать X» | Gate unlock'ает **только** этот конкретный skill/tool. Остальные mutating tools всё ещё блокируются (требуется новый AskUserQuestion для них) |
| Выбранная опция явно говорит «direct, без скила» / «продолжить без skill'а» / «делать напрямую» | Gate unlock'ает Edit/Write/MultiEdit (но не Bash mutating и не Skill/Task non-baseline). Bash требует whitelist (§5.1) |
| Свободный ответ (заказчик выбрал «Other» и написал текст) | Gate анализирует free-form text по тем же правилам выше. Если интерпретация неоднозначна — gate требует ещё одно AskUserQuestion для уточнения |
| **Любой ответ не матчащий ни один из 4 классов выше** (`stop` / `approve_specific_tool` / `approve_direct_no_skill` / `freeform_clear`) — **default-CLOSE (H4 v4-audit)** | Gate **остаётся заблокированным**. Записывается `gate_interpretation: "no_match_remain_blocked"`. Контроллер обязан вызвать второй AskUserQuestion с переформулированными опциями (включая явные keyword-маркеры stop/approve). Если и второй ответ unmatched → 2/2 limit достигнут → fail-CLOSE: «AskUserQuestion ambiguity loop — заказчик пусть переформулирует prompt с явным указанием действия». Закрывает H4 — keyword stop-list не покрывает естественные фразы типа «подожди» / «обдумаем», теперь это default-CLOSE а не silent-approve. |
| AskUserQuestion вызван, но заказчик ещё не ответил (in-flight) | Gate ждёт ответ — pre-answer state, никакого unlock |
Все решения логируются в новый файл `~/.claude/runtime/askuser-decisions-<sess>.jsonl`:
@@ -443,8 +482,8 @@ Soft-mitigation: rationalization-audit детектит pattern «Skill X called
**Soft warnings (surface в next turn):**
- Лейбл одной из опций содержит «(рекомендую)» / «(recommended)» / «(safe)» / «(quick)» — потенциальный leading question. Flag.
- Сильная разница в длине опций: одна > 30 слов, остальные < 5 → leading. Flag.
- Лейбл одной из опций содержит «(рекомендую)» / «(recommended)» / «(safe)» / «(quick)» / «(подходит лучше всего)» / «(оптимально)» — потенциальный leading question. Flag.
- Сильная разница в длине опций: **ratio > 4× между самой длинной и самой короткой** (E2 v4-audit — был threshold «one > 30 words, others < 5» обходился формулировкой ~10/40/20/15 слов). Flag.
- Вопрос не относится к active recommendation / current task — потенциальный bypass attempt. Flag.
**Off-topic detection при silence (closes Дыра 17):** когда recommendation отсутствует (Поведение 4), off-topic check сравнивает вопрос AskUser не с recommendation (её нет), а с **`task_classification`** field из router-state. Если classification = `bugfix`, а вопрос про дизайн / погоду / неотносящееся к bugfix — soft flag. Использует keyword-overlap heuristic (simple — без LLM call).
@@ -471,7 +510,9 @@ Flags попадают в `rationalization-flags-<sess>.jsonl` и сюрфейс
### 5.1. Bash content rules (closes Дыра 2)
Gate инспектирует **content** Bash-команды (string в `tool_input.command`). Парсит через простой shell-aware tokenizer (split по `;`, `&&`, `||`, `|` — далее извлекает first token каждой ветви как command).
Gate инспектирует **content** Bash-команды (string в `tool_input.command`). Парсит через простой shell-aware tokenizer (split по `;`, `&&`, `||`, `|`, `&` (background) — далее извлекает first token каждой ветви как command). **Tokenizer rule:** `&` split применяется только если **не** часть `&&` (lookahead `&(?!&)`). **Закрывает H3 v4-audit**`git status & mv evil good` теперь токенизируется на 2 команды, `mv` ловится hard-blacklist'ом.
**Tokenizer limitation (acknowledged):** простой split не handle'ит quoted args / escaped chars (`git log --grep='foo;bar'` ломается на quoted `;`). Принимается как acceptable friction (E1 v4-audit) — false-positive блок проще обойти через AskUser, чем риск bypass через quote-escaping.
**Default-deny** — если command не в whitelist и не в conditional-whitelist, gate блокирует.
@@ -480,14 +521,14 @@ Gate инспектирует **content** Bash-команды (string в `tool_i
| Команда | Допустимые args |
|---|---|
| `git status` | без mutating флагов |
| `git log` / `git show` / `git diff` / `git blame` | без `--exec` |
| `git log` / `git show` / `git diff` / `git blame` / `git format-patch` | без `--exec`, **без `--output=*`, без `-o <path>`, без `--file=*` (C8 v4-audit — write to file через whitelisted command)** |
| `git rev-parse` / `git merge-base` | любые |
| `git branch --show-current` | (только этот sub-command для `git branch`) |
| `git remote -v` / `git remote show` | без add/set-url |
| `ls` / `pwd` / `wc` / `head` / `tail` / `file` / `stat` | без `>` redirect |
| `grep` / `egrep` / `fgrep` | без `--exec`, без `-l` после `>` |
| `cat` / `less` / `more` | без `>` redirect |
| `node` | без `-e` / `--eval` / `-p` / `--print` |
| `ls` / `pwd` / `wc` / `head` / `tail` / `file` / `stat` | без `>` redirect, **path-deny overlay на ВСЕ path-args (C9/S4 v4-audit)** |
| `grep` / `egrep` / `fgrep` | без `--exec`, без `-l` после `>`, **path-deny overlay на ВСЕ path-args (C9/S4)** |
| `cat` / `less` / `more` | без `>` redirect, **path-deny overlay на ВСЕ path-args (C9/S4)** |
| `node` | без `-e` / `--eval` / `-p` / `--print`, **без `-r` / `--require` / `--import` / `--experimental-loader` (C10 v4-audit — preload arbitrary module before main script)** |
| `npx vitest run ...` / `npx vitest --version` | любые run-args, не `--reporter` с suspicious patterns |
| `npm test` / `npm run test` / `npm run lint:*` (read-only scripts) | любые |
| `php artisan` (read-only commands только: `list`, `route:list`, `migrate:status`) | по whitelist |
@@ -521,9 +562,9 @@ Gate инспектирует **content** Bash-команды (string в `tool_i
- Specific commands из ответа заказчика (например если он выбрал «выполнить `npx vitest run`» — тогда разрешена эта exact command).
- После Skill match для конкретной семантики — например `superpowers:test-driven-development` skill разрешает Bash для запуска тестов, но не для commit'ов.
**Path-deny overlay (§3.1):** даже разрешённый `cat` блокируется если target path — в protected list (`cat ~/.claude/runtime/router-state-*.json`block, leaks state).
**Path-deny overlay (§3.1) — applied to ALL path-args (C9/S4 v4-audit):** для **каждого** path-args whitelisted reading commands (`cat`/`tail`/`head`/`grep`/`less`/`more`/`file`/`stat`/`wc`/`ls`) gate нормализует path (§3.1 path normalization: resolve + realpath + case-fold + env expansion) и проверяет против protected list. Пример: `tail -n 100 ~/.claude/runtime/router-state-abc.json``tail` whitelisted, но `~/.claude/runtime/*` в protected → block. Раньше overlay упоминался только для `cat` — это была дыра C9: контроллер мог использовать `tail` / `head` / `grep -r` / `less` / `stat` для чтения protected state-файлов (knowledge advantage для подбора bypass). Теперь все reading commands проверяются единообразно.
**File-watcher для script execution (closes Дыра 14 part 1):** gate tracks `edited_files_this_turn` в state. Если в текущем turn'е был Edit/Write на `*.js` / `*.test.js` / `*.mjs` / `*.py` / `*.test.mjs` файл, последующие `node X`, `npx vitest run X`, `python X` блокируются если target path в `edited_files_this_turn`. Reason: «recently-edited script cannot be executed without explicit approval — request via AskUserQuestion».
**File-watcher для script execution (closes Дыра 14 part 1 + C6 v4-audit) — session-scoped, NOT turn-scoped:** gate tracks `edited_files_this_session` в `~/.claude/runtime/edited-files-<sess>.json` (session-wide persistence). Если в session был Edit/Write на `*.js` / `*.test.js` / `*.mjs` / `*.py` / `*.test.mjs` файл, последующие `node X`, `npx vitest run X`, `python X` блокируются если target path в session-list. Reset происходит **только** на: (a) explicit `git commit` whitelisted-команды (file перешёл в репо, проверен gitleaks/regression); (b) явный markup `<!-- edited-files-clear -->` в user-prompt; (c) новый session-id. Reason: «recently-edited script cannot be executed without explicit approval — request via AskUserQuestion». **C6 v4-audit:** turn-scoped tracking позволял bypass — Edit в turn N, run в turn N+1, watcher state очищен. Session-scoped закрывает.
Default = block. Reason возвращается в виде «Bash command «{command}» не в whitelist; либо запроси конкретное разрешение через AskUserQuestion, либо используй read-only альтернативу».
@@ -541,6 +582,10 @@ Default = block. Reason возвращается в виде «Bash command «{c
**Limitations:** статический скан ловит direct calls, но не creative indirection (например `fs[String.fromCharCode(...)]`). 95% real cases caught. Полное решение требовало бы sandbox runtime — выходит за scope этой итерации.
**Glob-aware scan (closes C11 v4-audit):** если target argument в `npx vitest run <pattern>` / `node <pattern>` содержит glob-метасимволы (`*`, `?`, `[`, `]`, `{`, `}`) — gate **не** может статически выбрать файл для сканирования (vitest auto-discovers config + setup-hooks + transitively-imported tests). В этом случае gate требует **AskUserQuestion approval** с явным указанием pattern и ожидаемого scope. Conditional whitelist (§5.1) применяется к exact-pattern approval — заказчик пишет «approve `npx vitest run tools/*.test.mjs`» → gate unlocks exactly эту команду.
**Direct imports follow (1-level deep, E1/C11 v4-audit partial):** для single-target скриптов (`node tools/foo.mjs`) gate **дополнительно** сканит файлы, импортируемые из target напрямую (parse `import ... from './X'` / `require('./X')` regex, resolve to absolute, 1 level deep, без следования транзитивно). Если imported file тоже matches forbidden patterns — block с reason: «target script imports forbidden module». Закрывает базовый case «target innocent.mjs но импортирует ./evil.mjs». Транзитивная глубина оставлена как acknowledged limitation (deep imports → require sandboxing). Сканирование не следует за dynamic imports (`import()` / `require(variable)`) — известная edge.
---
## 6. Recovery при lockout
@@ -654,7 +699,9 @@ Hint удаляется при Stop event (cleanup).
| 4 — Документация Recovery | Памятка для заказчика по 3 уровням recovery + v3 update | 1 |
| 5 — Прогон в реальной работе | Несколько дней наблюдения, brain-retro #11 | (не таск) |
| 6 — Brain-retro adaptation | Обновить `brain-retro-analyzer.mjs` + SKILL.md mandatory tables 11→13 | 1.5-2 |
| **Итого** | | **13.5-20** |
| **2.1.0 — Smoke-test env propagation (v3.2/H2)** | Pre-flight task до 2.1: minimal parent→Task→subagent env probe. Pass → продолжить §3.2 env-based. Fail → stop эпик, открыть отдельную задачу file-handshake | 0.5 |
| **+v3.2 fixes inline** | Section A protected paths (+0.3h), B chain PostToolUse migration (+1-2h), C Bash hardening (~2.4h), D default-CLOSE (+0.3h), E path-hardening + BLOCKED-protocol (+0.8h), F morphology + length-ratio (+0.5h) | +5-7 |
| **Итого** | | **18-27** |
### Риски миграции
@@ -673,6 +720,12 @@ Hint удаляется при Stop event (cleanup).
- **Logging granularity** — все decision events в одном файле или раздельно (router-gate / askuser-decisions / gate-errors)? Сейчас разделено на 3 файла — может слишком много.
- **Bash whitelist completeness** — список в §5.1 покрывает основные read-only operations, но возможно есть пропуски (например `tree`, `du`, `find` с right args). Расширяется итеративно по требованию из реальной работы.
### Acceptable residual risks (S6, S8 v4-audit)
- **S6 — max 2 AskUserQuestion per turn limit** (§4.5): chain'ы с many mutating tools (>2 шагов с Edit/Write/Bash per ход) НЕ помещаются в один turn — заказчик вынужден re-prompt'ить или делить задачу. **Acceptable UX-tradeoff** — это design choice §4.5 (anti-fatigue), не дыра. Multi-step chains реально работают через переspawning context per next prompt. Документировать в README/onboarding для заказчика.
- **S8 — `BRANCH-SWITCH-CONFIRMED` controller-writable marker** (preserved `enforce-branch-switch` hook): после удаления `recovery` override этот хук требует explicit маркер в каждом ответе на dangerous git-операцию. **Маркер пишется в controller response** — controller может self-write его, что есть pre-existing bypass-pattern (зафиксирован в memory `feedback_hook_bypass_pattern_2026-05-28.md`). **Известный residual risk v3.2** — не закрывается в этом эпике. **Follow-up эпик:** «Move BRANCH-SWITCH-CONFIRMED source from response to user-prompt» (заказчик должен явно писать фразу в prompt, аналогично direct-invocation). Budget ~1h на отдельный мини-эпик. Surface как **known-residual-risk** в §11 history v3.2 entry и в STATUS.md C5.
### Закрыты в v2
- ✅ Direct-invocation regex (§4 Поведение 1 — strict whitelist).
@@ -752,7 +805,7 @@ Hint удаляется при Stop event (cleanup).
"options": ["string"],
"chosen_label": "string",
"chosen_text": "string",
"gate_interpretation": "stop_remain_locked|approve_specific_tool|approve_direct_no_skill|ambiguous_requires_followup",
"gate_interpretation": "stop_remain_locked|approve_specific_tool|approve_direct_no_skill|ambiguous_requires_followup|no_match_remain_blocked",
"approved_tool": "Skill|Edit|Bash|null",
"approved_action_pattern": "string|null"
}
@@ -959,6 +1012,29 @@ Berkeley-style: use v2 spec, новый план implementation.
## 11. История версий
### v3.2 (2026-05-29, ночь, adversarial audit v4 closure)
Adversarial audit спека v3.1 от controller'а (через chain `audit-context-building` → ручной адверсариальный разбор по 13 attack-зонам) выявил **18 новых holes**, из них:
- ☠️ **4 fatal:** H1 chain-state self-clear через controller-response substring / H2 env-vars subagent inheritance unverified assumption / H3 Bash `&` background missing from tokenizer split / H4 AskUser keyword stop-list тривиально обходится natural-language ответом.
- 🔴 **11 critical:** C1 registry+registry-loader не в protected → fake-skill через Edit nodes.yaml; C2 enforce-hook-helpers не в protected → falsify sessionToolUses; C3 subagent-prompt-prefix не в protected → strip env-setting; C4 gate-config.json двусмысленность protected status → tune fatigue/timeout; C5 package.json/composer.json не в protected → evil npm-script через whitelist; C6 file-watcher turn-scoped → cross-turn bypass; C7 subagent inheritance path-injection через arbitrary parent_router_state_path; C8 git `--output` пишет файл через whitelist; C9 tail/grep/less/head/stat читают protected paths (overlay только на cat); C10 node `-r` preload не блокирован; C11 vitest glob + auto-discovery не покрываются static scan.
- 🟠 **3 serious:** S1 direct-invocation stale-registry blocks; S2 transcript lookback 5 turns теряет chain progression >5 turns; S3 chain_step++ на tool_input match без проверки success; S4 path-deny overlay нечётко прописано «ко всем args»; S5 subagent BLOCKED-protocol = текстовая конвенция без enforcement; S7 chain-clear keyword `другое` false-positive; **S6** (UX-tradeoff max 2 AskUser/turn) и **S8** (BRANCH-SWITCH-CONFIRMED controller-writable marker) — задокументированы как acceptable residual risks в §9.
- 🟡 **3 edge:** E1 Bash tokenizer без quote-handling; E2 length-bias detector обходится semantic framing; E3 direct-invocation regex не case-insensitive Cyrillic + morphology.
V3.2 закрывает **16 из 18** (S6/S8 → open questions §9). Ключевые изменения по 7 секциям A-G:
- **A** §3.1 protected paths **расширен** на 7 позиций: `docs/registry/nodes.yaml`, `tools/registry-load.mjs`, `tools/enforce-hook-helpers.mjs`, `tools/subagent-prompt-prefix.mjs`, `~/.claude/runtime/gate-config.json` explicit, `package.json`, `composer.json` — single-list defense-in-depth, закрывает 5 critical через одну правку.
- **B** §3 chain-state TTL: substring-match restricted to `transcript[-1].user.content` (H1); keyword list trimmed — `другое`/`забудь`/`новая работа` removed, оставлены только explicit-reset `новая задача`/`сброс контекста`/`забудь предыдущее` (S7); `chain_step++` мигрирует с PreToolUse `tool_input` match на PostToolUse success (S3 — требует регистрации gate на PostToolUse event); §3.6 transcript-lookback role clarified — chain_step SoT = chain-state.json (S2 — длинные chain >5 turns).
- **C** §5.1 Bash hardening: tokenizer split-list +`&` background с lookahead `(?!&)` (H3); file-watcher session-scoped vs turn-scoped + reset on git commit / explicit markup / new session (C6); git `--output=*`/`-o`/`--file=*` blacklisted (C8); path-deny overlay явно «для каждого path-args» расширен на tail/head/grep/less/more/file/stat/wc/ls (C9, S4); node `-r`/`--require`/`--import`/`--experimental-loader` blacklisted (C10). §5.2 glob-aware: vitest/node glob targets → AskUser required (C11); scan follows direct imports 1-level deep (C11 partial, E1 partial). Tokenizer quote-handling limitation acknowledged как acceptable friction (E1).
- **D** §4.5 default-CLOSE: новый row в таблице ответов «любой unmatched ответ → gate остаётся locked, требуется 2-й AskUser, 2/2 → fail-CLOSE ambiguity loop». schema askuser-decisions.jsonl: `gate_interpretation` enum +`no_match_remain_blocked` (H4).
- **E** §3.2.0 (новая) smoke-test env propagation REQUIRED BEFORE Этапа 2.1 — minimal parent→Task→subagent probe `CLAUDE_TEST_PROBE='42'` через `process.env`. Pass → §3.2 env-based реализуется. Fail → stop эпик, открыть отдельную задачу file-based handshake (H2). §3.2 path hardening: parent state paths **derived** hardcoded из parent_session_id, **не** принимаются как произвольный path в inheritance-файле (C7). §3.2 missing env vars / malformed inheritance → fail-CLOSE (H2 defensive). §3.4 BLOCKED-protocol enforced parent-side: parse last 200 chars subagent response regex'ом `\{[^{}]*"status"\s*:\s*"(DONE|BLOCKED)"[^{}]*\}` — missing marker = assume BLOCKED (S5).
- **F** §4 Поведение 1 strict whitelist: regex case-insensitive Cyrillic-aware (`/iu` flags); morphology variants `(делай|сделай|вызови|примени|используй)` для slug-trigger (E3); stale-registry fall-through — unknown skill name treated как «no direct invocation», fall through к Поведениям 2/3/4 без error-block (S1). §4.7 length-bias threshold tightened — ratio > 4× между longest/shortest option (E2); расширен leading-keyword list `(подходит лучше всего)/(оптимально)`.
- **G** §9 open questions: S6 (max 2 AskUser/turn) задокументирован как acceptable UX-tradeoff; S8 (BRANCH-SWITCH-CONFIRMED controller-writable) → known-residual-risk + follow-up эпик «move marker source from response to user-prompt» (~1h).
Implementation time: 13.5-20h (v3.1) → **18-27h** (v3.2). +5-7h за: smoke-test (0.5h), chain PostToolUse миграция (1-2h non-trivial), Bash hardening (~2.4h), path-args overlay (0.5h), file-watcher session-scope (0.5h), node `-r` blacklist (0.2h), glob-aware scan (0.5h), BLOCKED-protocol enforcement (0.5h), morphology regex (0.5h), default-CLOSE (0.3h), registry/helpers/prompt-prefix в protected (0.3h).
Audit-методология: `audit-context-building` skill (context layer) → ручной adversarial разбор по 13 attack-зонам (direct invocation / AskUser parser / chain-state / protected paths / Bash whitelist / subagent inheritance / gate budget / static scan / sub-shell sweep / coverage-hint / recovery / preserved hooks / cross-file). Brainstorming через `superpowers:brainstorming` skill для дизайна правок: 3 ключевых развилки решены через AskUserQuestion (scope=all, H4=default-CLOSE, H2=smoke-test).
### v3.1 (2026-05-28, поздний вечер, после комплексного анализа)
Clarification pass — не новые архитектурные решения, а **уточнения для implementer'а**: