diff --git a/cspell-words.txt b/cspell-words.txt index a1cb0308..6061b872 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1878,3 +1878,9 @@ ambig deplo обнулился Ревьюер + +# Router-gate v3.2 (2026-05-29) — adversarial audit closure +уйте +инкрементирован +матчащий +неверифицирована diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 771aa39b..ccfa280b 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -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-сессий. diff --git a/docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md b/docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md index 3c9cd084..d5a33cf0 100644 --- a/docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md +++ b/docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md @@ -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 добавляется и `делай ` проходит. (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--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_router_state_path": "~/.claude/runtime/router-state-.json", - "parent_chain_state_path": "~/.claude/runtime/chain-state-.json", "allowed_actions": ["Skill(matching)", "Task(subagent_type=matching)", ...], "created_at": "" } ``` + **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=` - `CLAUDE_GATE_INHERIT=true` - `CLAUDE_INHERITANCE_FILE=~/.claude/runtime/subagent-inheritance-.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:]`. 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: "", 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 по реестру | -| `делай ` | `делай subagent-driven-development` | Literal имя из реестра (точное совпадение с node `name` или `slug`) | +| `(вызови\|примени)\s+Skill\([^)]+\)` | `вызови Skill(superpowers:writing-plans)` | Literal call-syntax | +| `использ(уй\|уйте)\s+#\d+` | `используй #19` / `Используйте #19` | Literal по реестру | +| `(делай\|сделай\|вызови\|примени\|используй)\s+` | `делай 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: ` для 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-.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-.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 `, без `--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-.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 `` в 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 ` / `node ` содержит 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'а**: