From b632bcbae65c4c70f0765b2c905d94aa4aca38fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 29 May 2026 06:30:07 +0300 Subject: [PATCH] spec(router-gate): v3 closes 10 holes from v2 adversarial audit V2 audit found 10 new bypasses: - Fatal: subagent inheritance via text-prefix (no enforcement) - Critical: hook race conditions / DoS timeout / Bash script execution - Serious: path-deny symlinks/case / AskUser fatigue / silence off-topic - Edge cases: parallel subagents / coverage-verify interaction / sub-shells V3 closes all 10: - 3.2 rewritten: env-based subagent inheritance (env vars + inheritance file), gate-on-subagent reads parent state via CLAUDE_GATE_INHERIT env - 3.4 new: subagent constraints (no AskUser, no recursive Task, max 3 parallel) - 3.5 new: atomic writes (tmp+rename) + proper-lockfile for race-free state - 3.6 new: gate budget 2s + state cache TTL 5s + lazy transcript parsing - 3.1 extended: path normalization (resolve + realpath + case-fold + env) - 4.5 extended: max 2 AskUserQuestion per turn (fatigue exploit closed) - 4.7 extended: off-topic at silence uses task_classification - 5.1 extended: file-watcher for script execution + broad sweep sub-shell blacklist (backticks, command sub, process sub, heredocs) - 5.2 new: static content scan for node/python/vitest scripts before exec - 7.1 new: coverage-hint coordination layer between gate and coverage-verify Implementation cost: 8.5-12h (v2) to 13.5-20h (v3). +5-8h for architectural fixes (env-inheritance, atomic writes, static scan) + infrastructure (gate budget, path norm, askuser counter, coverage-hint). V2 baseline preserved as commit b510a758. V1 as 7a43c175. cspell-words.txt += shutil / rmtree. Co-Authored-By: Claude Opus 4.7 (1M context) --- cspell-words.txt | 2 + docs/observer/STATUS.md | 20 +- ...2026-05-28-router-gate-hard-wall-design.md | 210 +++++++++++++++--- 3 files changed, 197 insertions(+), 35 deletions(-) diff --git a/cspell-words.txt b/cspell-words.txt index 4863469f..e3d478d6 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1869,3 +1869,5 @@ sess детектирован fgrep chgrp +shutil +rmtree diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 4070bd93..a2c2bf7f 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-29T02:42:37.928Z +Last updated: 2026-05-29T03:24:37.955Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,13 +8,13 @@ Last updated: 2026-05-29T02:42:37.928Z | 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 | ⚠️ | 664 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro | +| C5 Observer-coverage | ⚠️ | 666 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: 664 episodes this month, 0 observer_error markers, 131 PII matches before filter -- Legacy v1 episodes (not in factor analysis): 525 +- Observer evidence: 666 episodes this month, 0 observer_error markers, 131 PII matches before filter +- Legacy v1 episodes (not in factor analysis): 527 - 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). @@ -31,9 +31,9 @@ Baseline дисциплины роутера (этап 2 router discipline overh | cleanup | 6 | 0.0% | 0.0% | | refactor | 1 | 0.0% | 0.0% | -Router step distribution: 1: 296, 2: 236, 3: 63, 5: 60 +Router step distribution: 1: 298, 2: 236, 3: 63, 5: 60 -Boundaries applied (ADR / границы): 75 of 655 эпизодов (11.5%). +Boundaries applied (ADR / границы): 75 of 657 эпизодов (11.4%). ## Активные многоэтапные проекты @@ -51,10 +51,10 @@ Boundaries applied (ADR / границы): 75 of 655 эпизодов (11.5%). | Компонент | Токены (in/out) | USD | |---|---|---| -| Classifier (Sonnet 4.6) | 3513/46753 | $0.71 | +| Classifier (Sonnet 4.6) | 3553/47294 | $0.72 | | Self-assessment (Sonnet 4.6) | 0/0 | $0.00 | | Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 | -| **Итого** | | **$0.71** | +| **Итого** | | **$0.72** | ## Аномалии классификатора @@ -67,7 +67,7 @@ Episodes since last run: 542 / threshold: 10 ## Reviewer: субагент vs fallback -0 эпизодов проверено из 664. +0 эпизодов проверено из 666. ## Reviewer findings @@ -110,7 +110,7 @@ Episodes since last run: 542 / threshold: 10 | Фраза | За всё время | За сегодня | |---|---|---| | `recovery` | 914 | 17 ⚠️ | -| `ремонт инфраструктуры` | 221 | 36 ⚠️ | +| `ремонт инфраструктуры` | 229 | 44 ⚠️ | | `без скилов` | 201 | 23 ⚠️ | | `срочно` | 93 | 0 | | `memory dump` | 17 | 0 | 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 4a98021b..4e9ccf41 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,13 +1,15 @@ -# Router-gate hard wall — Дизайн-спецификация (Уровень 4) v2 +# Router-gate hard wall — Дизайн-спецификация (Уровень 4) v3 **Дата:** 2026-05-28 -**Версия:** v2 (closes 10 holes found in v1 adversarial audit) +**Версия:** v3 (closes 10 holes found in v2 adversarial audit) **Автор:** Claude (controller Opus 4.7) под руководством заказчика Дмитрия **Статус:** Approved by owner — готов к плану implementation **Тип:** feature — enforcement architecture rewrite **Предшественник:** [docs/superpowers/plans/2026-05-28-router-discipline-level-1-2.md](../plans/2026-05-28-router-discipline-level-1-2.md) (Уровни 1+2, merged ранее в день) -**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. §8 пересчитан (7-9 часов). §9 закрыт большинство ранее открытых вопросов. +**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. --- @@ -108,11 +110,11 @@ Self-retrospect #2 (28.05 вечер) подтвердил: 5 commitments утр Это **explicit acknowledged risk** — заказчик соглашается что починка любого из 7 preserved хуков требует его ручного вмешательства. -### 3.1. Protected paths (hard-deny) +### 3.1. Protected paths (hard-deny) + path normalization Список путей, **никогда** не разрешённых для Edit / Write / MultiEdit / NotebookEdit и Bash-команд rm / mv / cp / redirects (`>` / `>>`): -- `~/.claude/runtime/*` — все gate state-файлы (router-state, chain-state, askuser-decisions, gate-decisions, gate-errors) +- `~/.claude/runtime/*` — все gate state-файлы (router-state, chain-state, askuser-decisions, gate-decisions, gate-errors, subagent-inheritance, coverage-hint) - `.claude/settings.json` / `.claude/settings.local.json` - `.claude/skills/**/SKILL.md` — skill definitions - `tools/enforce-*.mjs` — все enforcement-хуки (включая `enforce-router-gate.mjs`) @@ -121,22 +123,102 @@ Self-retrospect #2 (28.05 вечер) подтвердил: 5 commitments утр Hard-deny check выполняется **ПЕРВЫМ** в decision flow, до любой recommendation logic. Никакой unlock (Skill match / AskUserQuestion / direct invocation) не отменяет hard-deny. Изменения этих файлов — только через заказчика снаружи Claude. -**Закрывает Дыру 4** (Edit/Write state-файлов после unlock). +**Path normalization (closes Дыра 15 в v2 audit):** перед сравнением с deny-list gate **канонизирует** путь: -### 3.2. Subagent gate inheritance +1. `path.resolve(target)` — разрешает относительные пути и `..`. +2. `fs.realpathSync(resolved)` — разрешает symlinks (если файл существует; если нет — используется resolved). +3. Expand `~` через `os.homedir()` и env vars через `process.env`. +4. Case-fold на Windows (`.toLowerCase()` для сравнения; на Linux/Mac case-sensitive). -Task tool автоматически инжектит в начало prompt'а субагента (через существующий `tools/subagent-prompt-prefix.mjs` хук, расширенный): +Если canonicalization падает (file ENOENT) — fail-CLOSE для unknown-state файлов, fail-OPEN для проверок прав на чтение существующих. -- `[PARENT_SESSION_ID: ]` — ID родительской сессии -- `[PARENT_ROUTER_STATE_PATH: ~/.claude/runtime/router-state-.json]` -- `[PARENT_CHAIN_STATE_PATH: ~/.claude/runtime/chain-state-.json]` -- Инструкция: «Inherit parent's gate semantics. On lockout, return BLOCKED status to parent — don't attempt to bypass. Read parent state-files but don't write to them.» +**Закрывает Дыру 4 (v1)** и **Дыру 15 (v2)**. -Субагент при запуске читает parent state и работает под теми же ограничениями. Гарантирует что субагент не становится lateral bypass channel. +### 3.2. Subagent gate inheritance (env-based, NOT text-prefix) -Свой собственный router-state субагента **не создаётся** — gate использует parent's state. +V2 полагалась на text-prefix через `subagent-prompt-prefix.mjs` — субагент читает инструкцию и **должен** наследовать. V3 audit выявил что это **не enforcement**, а просьба. Sonnet субагенты могут проигнорировать (Phase 4 Task A прецедент). -**Закрывает Дыру 5** (subagent inheritance). +V3 переходит на **environment-based inheritance**: + +При вызове Task tool gate (PreToolUse): + +1. Пишет `~/.claude/runtime/subagent-inheritance-.json`: + + ``` + { + "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": "" + } + ``` + +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`. + - Свой собственный router-state субагента игнорируется (пустой при старте — silence-режим был бы deadlock). + - Decisions субагента логируются в **parent's** `router-gate-decisions.jsonl` с префиксом `[subagent:]`. + +4. Inheritance-файл удаляется когда Task tool возвращает результат родителю. + +**Закрывает Дыру 5 (v1 — undefined behavior)** и **Дыру 11 (v2 — prefix без enforcement)**. + +### 3.4. Subagent constraints + +Субагент работает с **ограниченным** set'ом возможностей по сравнению с parent: + +- **`AskUserQuestion` НЕ в baseline субагента.** При попытке вызвать — gate субагента блокирует с reason: «Subagents cannot escalate to user — return BLOCKED status to parent». +- **Recursive Task **запрещён**.** При попытке вызвать Task внутри Task — gate блокирует. Глубина max 1. +- **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**. + +### 3.5. Atomic state writes / file locking + +Все state-файлы (`router-state-*.json`, `chain-state-*.json`, `askuser-decisions-*.jsonl`, `router-gate-decisions.jsonl`, `subagent-inheritance-*.json`) пишутся atomically: + +**JSON state-файлы (one-shot writes):** + +- Запись через `write-to-tmp + rename` pattern: `fs.writeFileSync(target + '.tmp', data)` затем `fs.renameSync(target + '.tmp', target)`. POSIX `rename` атомарен; на Windows — близко к атомарному. + +**JSONL append-only логи:** + +- `fs.appendFileSync(target, line)` — атомарен для writes <`PIPE_BUF` (4096 bytes на POSIX) если flag `O_APPEND` установлен. Каждая строка <300 bytes — fits. +- На Windows — appendFileSync через `O_APPEND` тоже atomic для small writes. + +**File locking при параллельных tool calls:** + +- Gate использует `proper-lockfile` npm package (cross-platform file locks). +- Lock acquired на entry в decide function, released на exit. +- Lock timeout 1s — если другой gate-call держит lock дольше → fail-CLOSE (assume contention означает что-то странное). + +**Закрывает Дыру 12** (race conditions при parallel tool calls). + +### 3.6. Gate budget / timeout / state cache + +Gate имеет жёсткий бюджет времени **2 секунды** на одну decision. Если превышен — fail-CLOSE (block с reason «gate budget exceeded»). + +**Оптимизации в gate чтобы укладываться в budget:** + +- **In-memory state cache** (per gate-process) с TTL 5s: router-state и chain-state читаются раз, кэшируются. Следующий tool call в той же сессии в течение 5s — из кэша. +- **Lazy transcript parsing**: gate читает только last 5 turns transcript (не весь файл). Этого достаточно для проверки `ask_user_called_this_turn`, `skill_invoked_matching`, `is_direct_invocation`. +- **Configurable budget** через `~/.claude/runtime/gate-config.json`: + + ``` + { + "max_decision_time_ms": 2000, + "state_cache_ttl_ms": 5000, + "transcript_lookback_turns": 5 + } + ``` + +**Закрывает Дыру 13** (hook timeout = no enforcement DoS). ### 3.3. Failure modes (fail-CLOSE) @@ -299,6 +381,8 @@ Gate не unlock'ает turn просто по факту вызова AskUserQu Это **критическое отличие v2 от v1**. В v1 AskUserQuestion был универсальным unlock'ом — в v2 он только канал явного одобрения конкретного действия. +**AskUserQuestion counter (closes Дыра 16):** gate ведёт счётчик `askuser_count_this_turn` в state-файле. Лимит **2 AskUserQuestion за один turn**. 3-я попытка → gate **fail-CLOSE** с reason: «AskUserQuestion ambiguity loop detected (2/2 limit exhausted) — заказчик пусть переформулирует prompt с явным указанием что делать». Счётчик сбрасывается при новом prompt'е. + ### 4.6. Post-skill partial unlock (closes Дыра 3) После вызова matching Skill (или Task subagent) gate **частично** unlock'ает turn: @@ -328,7 +412,9 @@ Soft-mitigation: rationalization-audit детектит pattern «Skill X called - Лейбл одной из опций содержит «(рекомендую)» / «(recommended)» / «(safe)» / «(quick)» — потенциальный leading question. Flag. - Сильная разница в длине опций: одна > 30 слов, остальные < 5 → leading. Flag. -- Вопрос не относится к active recommendation / current task (text similarity check) — потенциальный bypass attempt. 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). Flags попадают в `rationalization-flags-.jsonl` и сюрфейсятся в next prompt через `enforce-prompt-injection`. Этого достаточно — закрытие 100% невозможно (creative leading questions неискоренимы), но 90% реальных случаев ловятся. @@ -388,6 +474,15 @@ Gate инспектирует **content** Bash-команды (string в `tool_i - `curl -X POST/PUT/DELETE/PATCH` / `wget --post-data` — outbound mutations - Любые pipe-команды (`|`) где receiver — mutating command +**Sub-shell / heredoc broad sweep (closes Дыра 20):** команды содержащие любой из следующих токенов — hard-blocked всегда (даже после unlock): + +- `` ` `` (backtick — command substitution) +- `$(...)` — POSIX command substitution +- `<(...)` / `>(...)` — process substitution +- `<<` / `<<-` — heredoc + +Это broad sweep — некоторые legitimate uses (`git log $(echo HEAD)`) ломаются. Принимаем как acceptable friction для закрытия sub-shell bypass attack surface. + **Conditional whitelist (разрешено только после явного AskUserQuestion approval per §4.5):** - Specific commands из ответа заказчика (например если он выбрал «выполнить `npx vitest run`» — тогда разрешена эта exact command). @@ -395,8 +490,24 @@ Gate инспектирует **content** Bash-команды (string в `tool_i **Path-deny overlay (§3.1):** даже разрешённый `cat` блокируется если target path — в protected list (`cat ~/.claude/runtime/router-state-*.json` — block, leaks state). +**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». + Default = block. Reason возвращается в виде «Bash command «{command}» не в whitelist; либо запроси конкретное разрешение через AskUserQuestion, либо используй read-only альтернативу». +### 5.2. Static content scan for executable scripts (closes Дыра 14 part 2) + +Перед разрешением `node