From 475d381e0cf170e8bd2e2372e4b496d434d98010 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: Mon, 8 Jun 2026 08:20:40 +0300 Subject: [PATCH] docs(m7): fold plan-critique findings P-1..P-8 into plan + spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Independent critical review of the implementation plan (audit-context -> sharp-edges -> variant-analysis -> systematic-debugging, grounded against real M1-M6 code). 8 findings, all code-grounded, no ghosts: - P-1 (CRIT): content-floor "full port" was a SUBSET of BASH_HARD_BLACKLIST (lost #21 env-prefix / #22 --watch / #34 echo-inj / C16 2>file / cp-mv-chmod-chown-chgrp; cp X ~/.claude/runtime = escape-grant forge) -> single source matchBashHardBlacklist relocated to shell-content-rules (Task 1.0.5) + §12 content-floor invariant proves completeness. - P-2 (CRIT): canonicalAction has no PowerShell branch -> every PS command collapses to 'write:' -> one escape unlocks ALL PS commands; Task 1.5 test was spuriously green (both sides equally broken) -> Task 1.2b + specificity. - P-3 (HIGH): PS floor branch returned block:false skipping runtime/secret guard (command field not parsed) -> Set-Content ~/.claude/runtime forge -> psProtectedWrite guard (Task 1.4). - P-4 (MED): content-block whole-string only -> bashIsContentBlock whole+per -segment parity with bashIsFloor (Task 1.3). - P-5 (MED): suspicious blind to content-danger -> suspicious |= contentBlock. - P-6 (MED): §12 CI-invariants ownerless -> assigned per phase (phase rule). - P-7 (LOW): Phase 0 fail-CLOSE "subset" unlisted -> explicit hook list. - P-8 (LOW): plan = detailed Phase 1 + scoped skeleton -> honest framing. Plan: Tasks 1.0.5/1.1/1.2b/1.3/1.4/1.5/1.6 + phase-transition rule + self-review. Spec: §5 PowerShell row, §12 M5 line, §13 addendum. No code built. commit-not-push. --- .../2026-06-08-router-mentor-machine-7.md | 188 +++++++++++++----- ...26-06-08-router-mentor-machine-7-design.md | 14 +- 2 files changed, 153 insertions(+), 49 deletions(-) diff --git a/docs/superpowers/plans/2026-06-08-router-mentor-machine-7.md b/docs/superpowers/plans/2026-06-08-router-mentor-machine-7.md index 90c91a29..03e85547 100644 --- a/docs/superpowers/plans/2026-06-08-router-mentor-machine-7.md +++ b/docs/superpowers/plans/2026-06-08-router-mentor-machine-7.md @@ -47,6 +47,14 @@ **Правило перехода между фазами:** фаза закрыта ТОЛЬКО при зелёной регрессии tools-only ≥ baseline и зелёном per-phase smoke. `audit-context-building` ОБЯЗАТЕЛЕН перед правкой каждого существующего модуля. +**§12 CI-инварианты — владельцы-фазы (P-6, иначе бесхозны).** Это анти-регресс-костяк «непробиваемости», каждый закрепляется как тест в своей фазе: +- content-floor инвариант (каждая `BASH_HARD_BLACKLIST`-запись floored даже как шаг плана) → **Фаза 1** (Task 1.6); +- escape-survivability инвариант (страж бросает на входе → escape всё равно оценён) → **Фаза 2**; +- skill-журнал инвариант (seed/ad-hoc Skill → в journal → K2 видит) → **Фаза 3**; +- structural-тест (нет `if(result.ok)` truthy, только `=== true` в дисциплине) + grep-инвариант (нет `assistantText.includes(marker) → block:false`) + escape≠protection (дисциплина не несёт override-вокабуляра) → **Фаза 4** (после поглощения дисциплины); +- манифест-тест (снятие дисциплинарного стража → CI красный, набор = полный М1–М6) → **Фаза 6**. +Фаза 8 §9.2 — это интеграционные smokes, НЕ замена этим unit-инвариантам. + **Каждая фаза 0,2–8 — отдельный детальный bite-sized план** (пишется при подходе через `superpowers:writing-plans`). Ниже — Фаза 1 в полной детализации + scoped-карты остальных. --- @@ -55,25 +63,44 @@ **Зачем:** стена М2 проверяет лишь членство-в-плане, пол М5 — лишь необратимость. Опасное-по-содержанию (произвольное исполнение `node -e`/`bash -c`, install, egress `curl`/`wget`/`nc`, redirect) как шаг плана проскальзывает (V1); PowerShell пол не смотрит вообще (V1-PS, реоткрытие v3.8 F1). Фаза 1 добавляет в пол **content-block класс**, рубящий по содержанию независимо от плана (escape по-прежнему снимает — owner-санкция). -**Дизайн-решения (locked):** -- Bash content-block regexes живут в `classify-destructive.mjs` (Δ9-б «единый источник правды по разрушительным regex» — не плодим второй модуль для Bash). Новое поле `contentBlock` рядом с `floor`/`suspicious`. -- PowerShell — отдельный модуль `tools/powershell-destructive.mjs` (PS-нативные глаголы НЕ матчат unix-regex). +**Дизайн-решения (locked, с поправками разбора плана 2026-06-08 — см. §«Поправки разбора плана»):** +- **ЕДИНЫЙ ИСТОЧНИК, НЕ перепечатка (P-1/S1, КРИТ).** Content-block НЕ переписывает regex'ы руками — он **переиспользует** реальный матчер `matchBashHardBlacklist` (он же `BASH_HARD_BLACKLIST` + `hasInjection` #34 + `stderrRedirectBlock` C16). Ручной список из прошлой редакции Task 1.1 был **подмножеством** реального blacklist'а (терял `#21` env-prefix, `#22` --watch, `#34` echo-injection, C16 `2>file`, `cp`/`mv`/`chmod`/`chown`/`chgrp`). Чтобы не зависеть от увольняемого router-gate, матчер **переносится в постоянный дом** `shell-content-rules.mjs` (там уже живёт `hasInjection`); `enforce-router-gate.mjs` импортирует оттуда (зелён до Фазы 8) — Task 1.0.5. +- `contentBlock` (whole-string `matchBashHardBlacklist`) кладётся в `classify-destructive` как поле рядом с `floor`/`suspicious`, и **`suspicious ||= contentBlock`** (P-5) — иначе голоса судьи М4 content-опасное не видят. +- **Per-segment паритет (P-4).** Фактический блок в полу идёт через `bashIsContentBlock` (whole-string + посегментно через `tokenizeBash`) — зеркало `bashIsFloor`: нейтрализует кавычки (`echo "node -e"` не должен давать ни FP, ни обход) и ловит content-сегмент за читающим. +- PowerShell — отдельный модуль `tools/powershell-destructive.mjs` (PS-нативные глаголы НЕ матчат unix-regex). **+ привязка escape (P-2, КРИТ):** `escape-grant.mjs::canonicalAction` ОБЯЗАН получить ветку PowerShell (`powershell:${normalizeCommand(command)}`) — Task 1.2b. Без неё все PS-команды схлопываются в `write:` (пусто) → один escape разблокирует ВСЕ PS-команды, а тест Task 1.5 зелёный ложно. +- **+ PS forge-страж (P-3, ВЫС):** PS-ветка пола, кроме `psContentBlock`, рубит запись в защищённый путь (`Set-Content`/`Add-Content`/`Out-File`/`Copy-Item`/`New-Item`/`Tee-Object`/`Move-Item`/`>` в `.claude/runtime`/`.env`/секрет) — иначе PS подделывает escape-грант (Task 1.4). - `floor-decide.mjs` зовёт content-block ПЕРВЫМ (до проверки необратимости), для Bash и PowerShell; escape снимает блок в обеих ветках. -- Порт из `enforce-router-gate.mjs::BASH_HARD_BLACKLIST` (стр. 45-73) — **полный набор**, минус whitelist-логика (она остаётся в router-gate до его увольнения в Фазе 8; content-block — независимый hard-deny по содержанию). ### Файловая структура Фазы 1 -- Modify: `tools/classify-destructive.mjs` (+`CONTENT_BLOCK_RE`, +`contentBlock` в выдаче) -- Test: `tools/classify-destructive.test.mjs` (+content-block кейсы) +- Modify: `tools/shell-content-rules.mjs` (постоянный дом `BASH_HARD_BLACKLIST` + `stderrRedirectBlock` + `matchBashHardBlacklist`; Task 1.0.5 — P-1/S1) +- Modify: `tools/enforce-router-gate.mjs` (импортирует матчер из shell-content-rules вместо собственного; зелён до Фазы 8) +- Modify: `tools/classify-destructive.mjs` (+`contentBlock` = `matchBashHardBlacklist` whole-string; `suspicious ||= contentBlock` — P-5) +- Test: `tools/classify-destructive.test.mjs` (+content-block кейсы, включая P-1-пробелы: env-prefix/--watch/echo-inj/`2>file`/cp/mv) +- Modify: `tools/escape-grant.mjs` (`canonicalAction` +ветка PowerShell — Task 1.2b, P-2) +- Test: `tools/escape-grant.test.mjs` (+PS canonicalAction специфичен) - Create: `tools/powershell-destructive.mjs` (`psContentBlock`) - Test: `tools/powershell-destructive.test.mjs` -- Modify: `tools/floor-decide.mjs` (content-block ветка Bash + PowerShell, escapable) -- Test: `tools/floor-decide.test.mjs` (+content-block + escape-bypass кейсы) +- Modify: `tools/floor-decide.mjs` (`bashIsContentBlock` whole+per-segment; ветка Bash + PowerShell+forge-страж, escapable — P-3/P-4) +- Test: `tools/floor-decide.test.mjs` (+content-block + escape-bypass + escape-СПЕЦИФИЧНОСТЬ кейсы) --- ### Task 1.0: audit-context-building перед правкой -- [ ] **Step 1:** Прочитать целиком `tools/classify-destructive.mjs`, `tools/floor-decide.mjs`, `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST), `tools/bash-tokenizer.mjs`, `tools/escape-grant.mjs`. Зафиксировать: текущую сигнатуру `classifyDestructive(command) → {floor, suspicious, reason}`; `floorDecide({toolUse, escapeGrants, escapeConsumed, now, normalizeImpl})`; что `bashIsFloor` уже посегментно ходит через tokenizeBash. Это контекст — без правок. +- [ ] **Step 1:** Прочитать целиком `tools/classify-destructive.mjs`, `tools/floor-decide.mjs`, `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST + `matchBashHardBlacklist` + `stderrRedirectBlock`), `tools/shell-content-rules.mjs` (где живёт `hasInjection`), `tools/bash-tokenizer.mjs`, `tools/escape-grant.mjs`. Зафиксировать: текущую сигнатуру `classifyDestructive(command) → {floor, suspicious, reason}`; `floorDecide({toolUse, escapeGrants, escapeConsumed, now, normalizeImpl})`; что `bashIsFloor` уже посегментно ходит через tokenizeBash; **что `canonicalAction` (escape-grant.mjs:25-36) НЕ имеет ветки PowerShell** (P-2) и **что `matchBashHardBlacklist` сейчас живёт в router-gate** (P-1 — переедет в Task 1.0.5). Проверить отсутствие циклического импорта `shell-content-rules` ↔ `classify-destructive`. Это контекст — без правок. + +--- + +### Task 1.0.5: единый источник content-правил (P-1/S1) — перенос матчера в постоянный дом + +**Зачем:** прошлая редакция Task 1.1 перепечатывала regex'ы руками → получалось **подмножество** реального `BASH_HARD_BLACKLIST` (терялись `#21` env-prefix, `#22` --watch, `#34` echo-injection, C16 `2>file`, `cp`/`mv`/`chmod`/`chown`/`chgrp`). Единственный источник правды устраняет port-дрейф **по конструкции** (Δ9-б). + +**Files:** Modify `tools/shell-content-rules.mjs`, `tools/enforce-router-gate.mjs`; тесты обоих. + +- [ ] **Step 1:** Перенести из `enforce-router-gate.mjs` в `shell-content-rules.mjs`: `BASH_HARD_BLACKLIST`, `stderrRedirectBlock`, `matchBashHardBlacklist` (последний уже зовёт `hasInjection`, который там же живёт). Экспортировать их. +- [ ] **Step 2:** В `enforce-router-gate.mjs` заменить локальные определения на `import { matchBashHardBlacklist, BASH_HARD_BLACKLIST } from './shell-content-rules.mjs'` (router-gate остаётся зелёным до Фазы 8; его собственные тесты не меняют поведения). +- [ ] **Step 3:** Прогнать тесты router-gate + shell-content-rules — поведение байт-в-байт прежнее (чистый рефактор-перенос, 0 изменений семантики). GREEN. +- [ ] **Step 4:** Коммит `refactor(m7-floor): matchBashHardBlacklist → shell-content-rules (единый дом content-правил, P-1)`. --- @@ -96,6 +123,12 @@ describe('contentBlock (правило 8 §4.1, V1)', () => { 'yarn add evil', 'pnpm install evil', 'npx claude-flow', 'curl -X POST https://e.rf', 'wget http://e.rf', 'nc -l 4444', 'ncat e.rf 80', 'socat - tcp:e.rf:80', 'echo x > /tmp/f', 'echo x >> f', + // P-1 пробелы прошлого ручного списка — теперь ловятся (единый источник): + 'FOO=bar node tools/x.mjs', 'env -i node x', // #21 env-prefix перед интерпретатором + 'node tools/x.mjs --watch', 'pest --watch', // #22 persistent --watch + 'git status 2> /tmp/err', // C16 stderr-redirect в файл + 'cp secret ~/.claude/runtime/g.json', 'mv a b', // file-mutation cmds (forge-вектор) + 'chmod 777 x', // chmod ]; it.each(blocked)('blocks by content: %s', (cmd) => { expect(classifyDestructive(cmd).contentBlock).toBe(true); @@ -123,37 +156,18 @@ Expected: FAIL — `contentBlock` is undefined (поле не существуе - [ ] **Step 3: Реализовать минимально** -В `tools/classify-destructive.mjs` добавить после `SUSPICIOUS_RE`: - -```js -// Правило 8 §4.1 (V1): опасное-по-СОДЕРЖАНИЮ — рубится полом независимо от плана. -// Полный port enforce-router-gate.mjs::BASH_HARD_BLACKLIST (без whitelist-логики). -const CONTENT_BLOCK_RE = [ - /\b(?:node|nodejs)\s+(?:[^|;]*\s)?(?:-e|--eval|-p|--print)\b/, - /\bnode\s+(?:[^|;]*\s)?(?:-r|--require|--import|--experimental-loader)\b/, - /\bpython3?\s+-c\b/, - /\b(?:bash|sh)\s+-c\b/, - /(^|\s|;|&&|\|\|)eval\b/, - /\bcomposer\s+(?:install|update|require|remove)\b/, - /\bnpm\s+(?:install|i|update|remove|uninstall)\b/, - /\b(?:yarn|pnpm)\s+(?:add|install|remove)\b/, - /\bnpx\s+claude-/, - /\bcurl\b[^|;]*-X\s*(?:POST|PUT|DELETE|PATCH)\b/i, - /\bwget\b/, - /(^|\s|;|&&|\|\|)(?:nc|ncat|netcat)\b/, - /(^|\s|;|&&|\|\|)socat\b/, - /(?:^|[^0-9>&])>{1,2}(?![>&])/, // stdout redirect >/>> -]; -``` - -И в `classifyDestructive` дополнить выдачу: +В `tools/classify-destructive.mjs` — **НЕ перепечатывать regex'ы** (P-1/S1), переиспользовать единый матчер из Task 1.0.5: ```js +import { matchBashHardBlacklist } from './shell-content-rules.mjs'; +// ... export function classifyDestructive(command) { const cmd = String(command || ''); const floor = rmIsFloor(cmd) || FLOOR_RE.some((re) => re.test(cmd)); - const suspicious = floor || SUSPICIOUS_RE.some((re) => re.test(cmd)); - const contentBlock = CONTENT_BLOCK_RE.some((re) => re.test(cmd)); + // Правило 8 §4.1 (V1): опасное-по-СОДЕРЖАНИЮ — единый источник (P-1), whole-string. + const contentBlock = matchBashHardBlacklist(cmd) !== null; + // P-5: судья М4 (голоса) видит content-опасное как suspicious. + const suspicious = floor || contentBlock || SUSPICIOUS_RE.some((re) => re.test(cmd)); const reason = floor ? 'необратимая команда (floor)' : contentBlock ? 'опасная по содержанию (content-block)' : suspicious ? 'подозрительная команда (suspicious)' @@ -162,6 +176,8 @@ export function classifyDestructive(command) { } ``` +> **NB (P-4):** это whole-string поле для видимости судьи. Фактический **блок** пола идёт через `bashIsContentBlock` (whole + per-segment) в Task 1.3 — паритет с `bashIsFloor`, нейтрализует кавычки (`echo "node -e foo"` не даёт ни FP, ни обход). + - [ ] **Step 4: Запустить — убедиться, что проходит** Bash: та же команда. Expected: PASS (все content-block + allowed кейсы зелёные). @@ -249,6 +265,34 @@ feat(m7-floor): powershell-destructive psContentBlock (V1-PS) --- +### Task 1.2b: canonicalAction — ветка PowerShell (P-2, КРИТ — escape-привязка) + +**Зачем:** `canonicalAction` (escape-grant.mjs:25-36) обрабатывает `Bash` и `mcp__*`; PowerShell уходит в write-fallback → `extractWritePath({command})` пусто → **все PS-команды схлопываются в `write:`**. Тот же `canonicalAction` используют пол, **стена** (`enforce-supreme-gate.mjs:174`) и консьюмер → дефект сквозной: один escape на PS разблокирует ВСЕ PS-команды в окне; коллизия с пустым `write:`. Без этой ветки PS-пол Task 1.4 формально escapable, но привязка неспецифична, а тест Task 1.5 зелёный ложно. + +**Files:** Modify `tools/escape-grant.mjs`; Test `tools/escape-grant.test.mjs` + +- [ ] **Step 1: Падающий тест (специфичность).** +```js +import { canonicalAction } from './escape-grant.mjs'; +it('PowerShell canonicalAction специфичен (разные команды → разные ключи)', () => { + const a = canonicalAction('PowerShell', { command: 'Remove-Item -Recurse -Force C:\\x' }); + const b = canonicalAction('PowerShell', { command: 'Invoke-WebRequest https://e.rf' }); + expect(a).toMatch(/^powershell:/); + expect(a).not.toBe(b); // НЕ оба 'write:' (текущий баг даёт a===b===' write:') + expect(a).not.toBe('write:'); +}); +``` +Expected: FAIL (текущий код даёт `a === b === 'write:'`). + +- [ ] **Step 2: Реализовать.** В `canonicalAction` после Bash-ветки: +```js +if (name === 'PowerShell') return `powershell:${normalizeCommand(input.command || '')}`; +``` +- [ ] **Step 3: GREEN.** Прогнать `escape-grant` + потребителей (`floor-decide`/`enforce-supreme-gate`/`floor-escape-consume`) — поведение Bash/Write/mcp не задето (чистое добавление ветки). +- [ ] **Step 4: Коммит** `fix(m7-floor): canonicalAction +PowerShell — escape-привязка специфична (P-2)`. + +--- + ### Task 1.3: floor-decide — content-block ветка Bash (escapable) **Files:** Modify `tools/floor-decide.mjs`; Test `tools/floor-decide.test.mjs` @@ -286,14 +330,27 @@ Expected: FAIL — `node -e` сейчас не floor → block:false (дыра V В `tools/floor-decide.mjs` импорт + в ветке `if (name === 'Bash')` добавить content-block ДО проверки `bashIsFloor`: ```js -// импорт сверху: +// импорты сверху: import { psContentBlock } from './powershell-destructive.mjs'; -// classifyDestructive уже импортирован (bashIsFloor его зовёт). +import { matchBashHardBlacklist } from './shell-content-rules.mjs'; +// classifyDestructive/tokenizeBash уже импортированы (bashIsFloor их зовёт). + +// P-4: паритет с bashIsFloor — whole-string И посегментно (нейтрализует кавычки, +// ловит content-сегмент за читающим). Единый матчер (P-1). +export function bashIsContentBlock(command) { + const raw = String(command || ''); + if (matchBashHardBlacklist(raw)) return true; + const tok = tokenizeBash(raw); + if (tok && tok.ok && Array.isArray(tok.segments)) + for (const s of tok.segments) + if (matchBashHardBlacklist((s.tokens || []).join(' '))) return true; + return false; +} // внутри floorDecide, в начале ветки Bash: if (name === 'Bash') { // Правило 8 (V1): content-block по содержанию — независимо от плана, escapable. - if (classifyDestructive(input.command || '').contentBlock) { + if (bashIsContentBlock(input.command || '')) { if (escaped()) return { block: false, reason: 'floor: content-block снят аварийным выходом (floor_escape)' }; return { block: true, reason: `floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: ${action}` }; } @@ -305,6 +362,8 @@ import { psContentBlock } from './powershell-destructive.mjs'; } ``` +> **NB (P-4 тест):** добавить кейс «`echo "node -e foo" && cat x` НЕ over-блокируется (кавычки нейтрализованы), а `cat x && node -e "y"` блокируется (content-сегмент за читающим)» — доказывает per-segment паритет. + - [ ] **Step 4: Запустить — убедиться, что проходит** Bash: та же команда. Expected: PASS. @@ -346,11 +405,23 @@ Expected: FAIL — PowerShell сейчас уходит в write-path-ветку - [ ] **Step 3: Реализовать минимально** -В `floor-decide.mjs` добавить ветку PowerShell ПОСЛЕ Bash-ветки, ДО OBSERVE_TOOLS: +В `floor-decide.mjs` добавить ветку PowerShell ПОСЛЕ Bash-ветки, ДО OBSERVE_TOOLS. **+P-3 forge-страж:** поле `command` не парсится `extractWritePath`/общим write-стражем (стр. 95-105), поэтому запись PS-глаголом в защищённый путь надо проверить по сырой строке (иначе `Set-Content ~/.claude/runtime/...` подделывает escape-грант): ```js +// рядом с RUNTIME_RE/SECRET_PATH_RE (переиспользуем их): +const PS_WRITE_VERB_RE = /\b(?:Set-Content|Add-Content|Out-File|Copy-Item|New-Item|Tee-Object|Move-Item)\b/i; +function psProtectedWrite(cmd) { + const s = String(cmd || ''); + const isWrite = PS_WRITE_VERB_RE.test(s) || /(?:^|[^0-9>&])>{1,2}(?![>&])/.test(s); + if (!isWrite) return false; + const slashed = s.split('\\').join('/'); + return RUNTIME_RE.test(slashed) || SECRET_PATH_RE.some((re) => re.test(slashed)); +} + +// внутри floorDecide: if (name === 'PowerShell') { - if (psContentBlock(input.command || '')) { + const cmd = input.command || ''; + if (psContentBlock(cmd) || psProtectedWrite(cmd)) { // P-3: forge-страж if (escaped()) return { block: false, reason: 'floor: PS content-block снят аварийным выходом (floor_escape)' }; return { block: true, reason: `floor: опасная PowerShell-команда без аварийного выхода — блок (правило 8, V1-PS); FLOOR-ESCAPE: ${action}` }; } @@ -358,6 +429,8 @@ Expected: FAIL — PowerShell сейчас уходит в write-path-ветку } ``` +> **NB (P-3 тест):** добавить в step 1 кейс `Set-Content -Path ~/.claude/runtime/askuser-decisions-x.jsonl -Value '{}'` → block:true (forge-вектор закрыт), и `Set-Content -Path notes.md -Value ok` → block:false (обычная запись не over-блокируется). + - [ ] **Step 4: Запустить — убедиться, что проходит** Expected: PASS. @@ -393,12 +466,21 @@ describe('escape снимает content-block (owner-санкция)', () => { const r = floorDecide({ toolUse: tu, escapeGrants: [{ action, ts: now }], escapeConsumed: [], now }); expect(r.block).toBe(false); }); + // P-2 (КРИТ): escape СПЕЦИФИЧЕН — грант на одну PS-команду НЕ разблокирует другую. + // До Task 1.2b обе схлопывались в 'write:' и этот тест проходил ложно (a===b). + it('escape на PS-команду A НЕ разблокирует PS-команду B', () => { + const now = 1_000_000; + const grantA = canonicalAction('PowerShell', { command: 'Remove-Item -Recurse -Force C:\\x' }); + const tuB = { name: 'PowerShell', input: { command: 'Invoke-WebRequest https://e.rf' } }; + const r = floorDecide({ toolUse: tuB, escapeGrants: [{ action: grantA, ts: now }], escapeConsumed: [], now }); + expect(r.block).toBe(true); // грант A не покрывает B + }); }); ``` - [ ] **Step 2: Запустить** -Expected: PASS сразу (escaped() уже в обеих ветках). Если FAIL — canonicalAction для PowerShell даёт иную строку, чем floorDecide вычисляет → проверить, что обе зовут `canonicalAction(name, input)` идентично (floorDecide: `action = canonicalAction(name, input, {normalizeImpl})`). Выровнять. +Expected: первые два кейса PASS (escaped() в обеих ветках); кейс специфичности PASS **только после Task 1.2b** (до неё `grantA === canonicalAction(B) === 'write:'` → ложно block:false). Если специфичность падает — Task 1.2b не выполнен/не выровнен: floorDecide и тест обязаны звать один `canonicalAction(name, input)`. - [ ] **Step 3: Коммит** @@ -415,17 +497,17 @@ test(m7-floor): инвариант escape снимает content-block (Bash+PS) Bash: `node "\app\node_modules\vitest\vitest.mjs" run --root "\app" --config "\app\vitest.config.tools.mjs" --reporter dot` Expected: ≥ **2843 passed + 2 skip** + новые content-block тесты (Фаза 1 добавляет ~40). 0 regressions. -- [ ] **Step 2: Smoke — дыра V1 закрыта** +- [ ] **Step 2: Smoke — дыра V1/V1-PS закрыта + §12 content-floor инвариант** -Подтвердить в выводе: `floor-decide` тесты «blocks node -e / curl / Remove-Item even with no escape» зелёные; «safe reading НЕ блокируется» зелёные. Это и есть доказательство закрытия V1/V1-PS. +Подтвердить в выводе: `floor-decide` тесты «blocks node -e / curl / Remove-Item even with no escape» зелёные; «safe reading НЕ блокируется» зелёные; P-1-пробелы (env-prefix/--watch/echo-inj/`2>file`/cp/mv) зелёные; P-2 escape-специфичность зелёная; P-3 PS-forge зелёный. **§12 content-floor инвариант:** добавить тест-генератор «КАЖДАЯ запись `BASH_HARD_BLACKLIST` рубится полом даже как валидный шаг плана» (итерация по экспортированному списку — доказывает полноту порта по конструкции, не вручную). - [ ] **Step 3: Финальный коммит фазы (если оставались несакоммиченные правки)** ``` -chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS, регрессия GREEN +chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS (P-1..P-5), регрессия GREEN ``` -**Acceptance Фазы 1:** опасное-по-содержанию (Bash + PowerShell) рубится полом независимо от плана; safe-reading не over-блокируется; escape снимает; регрессия ≥ baseline. → разблокирует увольнение router-gate/powershell-gate в Фазе 8. +**Acceptance Фазы 1:** опасное-по-содержанию (Bash + PowerShell) рубится полом независимо от плана из **единого источника** (P-1, не ручной список); content-block whole+per-segment (P-4); `suspicious` видит content-опасное (P-5); escape **специфичен** для PS (P-2) и снимает блок; PS-запись в защищённый путь зарублена (P-3); §12 content-floor инвариант зелёный; регрессия ≥ baseline. → разблокирует увольнение router-gate/powershell-gate в Фазе 8. --- @@ -433,6 +515,8 @@ chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS, регр ### Фаза 0 — fail-CLOSE-карвут + escape-survivability примитивы **Спека:** §4.1 правило 1 + правило 7(а). **Файлы:** `enforce-hook-helpers.mjs` (карвут «дисциплина fail-CLOSE, наблюдение fail-quiet» — уточнить контракт :7 + хелпер `exitDisciplineDecision` с fail-CLOSE-дефолтом), `escape-grant.mjs::canonicalAction` (обернуть `normalizeCommand` в try → тотальность), `path-normalization.mjs` (тотальность). **Acceptance:** тест «дисциплинарный хелпер на ошибке → block (не quiet)»; «canonicalAction на любом мусоре не бросает». **Ключевой риск:** не перевести наблюдательные хуки (observer/cost) в fail-CLOSE — только дисциплинарное подмножество. +**P-7 (явный список — иначе «дисциплинарное подмножество» непроверяемо):** fail-CLOSE получают **дисциплинарные/защитные** хуки — `enforce-floor`, `enforce-supreme-gate`, `enforce-judge-gate`, `enforce-snapshot`, `enforce-floor-escape-consume`, `enforce-read-path-deny`, `enforce-mcp-classification`, `enforce-normative-content-rules`, `enforce-skill-журналер` (Фаза 3), плюс поглощённая дисциплина по §4.2. **Остаются fail-quiet (наблюдение):** `observer-stop-hook`, `cost-stop-hook`. Список — константа в коде + манифест-тест (P-6/SE-B): добавление дисциплинарного хука без fail-CLOSE → CI красный. +**P-2 связка:** ветка PowerShell в `canonicalAction` живёт в Фазе 1 (Task 1.2b — там PS входит в пол); здесь — только тотальность (try вокруг `normalizeCommand`). Если Фаза 0 идёт первой, ветку PS можно поднять сюда — но тогда её escape-специфичный тест всё равно в Task 1.5. ### Фаза 2 — escape-survivability полная (SE-I/L6) **Спека:** §4.1 правило 7(б,в) + §6 escape-honor. **Файлы:** `enforce-floor.mjs`/`enforce-supreme-gate.mjs` (panic-ветка: escape оценивается до любой бросающей логики — уже почти так в supreme-gate G-1α, проверить ordering), `enforce-read-path-deny.mjs` + `enforce-mcp-classification.mjs` + `enforce-normative-content-rules.mjs` (+чтение escape-гранта `escapeGrantOpen` перед block). **Acceptance:** тест «страж бросает на входе → escape всё равно оценён → escaped-действие проходит»; «escaped-правка ЗАКОНА через normative-content-rules проходит». **Риск:** escape-honor не должен ослаблять стражей для не-escaped действий. @@ -463,6 +547,16 @@ chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS, регр **2. Placeholder scan:** Фаза 1 — полный код в каждом шаге, без TBD ✓. Фазы 0,2-8 — осознанно scoped (scope-check скила: per-subsystem планы), не placeholder, а явная декомпозиция; каждая разворачивается в bite-sized при подходе. -**3. Type consistency:** `classifyDestructive(cmd) → {floor, suspicious, contentBlock, reason}` (1.1) используется в `floor-decide` (1.3) консистентно; `psContentBlock(cmd)→bool` (1.2) в floor-decide PowerShell-ветке (1.4); `canonicalAction(name,input)` идентично в escape-grant и floor-decide (1.5). ✓ +**3. Type consistency:** `classifyDestructive(cmd) → {floor, suspicious, contentBlock, reason}` (1.1) используется в `floor-decide` (1.3) консистентно; `matchBashHardBlacklist(cmd) → reason|null` (1.0.5) единый матчер для `contentBlock` (1.1) и `bashIsContentBlock` (1.3); `psContentBlock(cmd)→bool` + `psProtectedWrite(cmd)→bool` (1.2/1.4) в floor-decide PowerShell-ветке; `canonicalAction(name,input)` теперь обрабатывает PowerShell (1.2b) → идентичный ключ в escape-grant, floor-decide, supreme-gate, consume. ✓ -**Известное допущение для исполнителя:** регексы content-block — порт из `BASH_HARD_BLACKLIST`; при увольнении router-gate (Фаза 8) `classify-destructive::CONTENT_BLOCK_RE` становится единственным домом этих правил. +**Известное допущение для исполнителя (обновлено разбором плана 2026-06-08):** content-block — **НЕ ручной порт**, а переиспользование `matchBashHardBlacklist`, перенесённого в `shell-content-rules.mjs` (Task 1.0.5) как постоянный дом (P-1/S1). При увольнении router-gate (Фаза 8) дом не меняется — router-gate уже импортирует оттуда. §12 content-floor инвариант (Task 1.6) доказывает полноту порта по конструкции (итерация по списку). + +**4. Поправки разбора плана (2026-06-08) — трассировка:** независимый критический разбор плана (цепочка `audit-context-building` → `sharp-edges` → `variant-analysis` → `superpowers:systematic-debugging`, заземление против реального кода М1–М6) нашёл 8 находок. Закрыты в этом плане: +- **P-1 (КРИТ)** «полный порт» был подмножеством `BASH_HARD_BLACKLIST` (терял #21 env-prefix / #22 --watch / #34 echo-inj / C16 `2>file` / cp/mv/chmod/chown/chgrp) → Task 1.0.5 единый источник + Task 1.1 + §12-инвариант. +- **P-2 (КРИТ)** `canonicalAction` без ветки PowerShell → все PS-команды = `write:` → escape неспецифичен (один грант открывает все PS), тест 1.5 зелёный ложно → Task 1.2b + escape-специфичность 1.5. +- **P-3 (ВЫС)** PS-ветка пола проскакивала runtime/secret-страж (`Set-Content ~/.claude/runtime/...` = forge escape-гранта) → forge-страж в Task 1.4. +- **P-4 (СРЕД)** content-block был whole-string без паритета с `bashIsFloor` → `bashIsContentBlock` whole+per-segment (Task 1.3). +- **P-5 (СРЕД)** `suspicious` не видел content-опасное (судья М4 слеп) → `suspicious ||= contentBlock` (Task 1.1). +- **P-6 (СРЕД)** §12 CI-инварианты бесхозны → закреплены за фазами (правило перехода). +- **P-7 (НИЗ)** «дисциплинарное подмножество» Фазы 0 без списка → явный перечень (Фаза 0). +- **P-8 (НИЗ)** план = детальная Фаза 1 + скелет — честная декомпозиция; scoped-фазы 0,2-8 разворачиваются через `writing-plans`, не теряя пунктов спеки (см. P-6). diff --git a/docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md b/docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md index 68ce42ee..610e8f5a 100644 --- a/docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md +++ b/docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md @@ -131,7 +131,7 @@ | Увольняемый | Карта покрытия (что доказать) | |---|---| | `router-gate` (Bash content default-deny) | **Перенос ПОЛНОГО `BASH_HARD_BLACKLIST` (`enforce-router-gate.mjs:45-73`) в content-floor М5 (правило 8), рубящий ВНЕ зависимости от плана.** Покрыть поимённо: (1) **arbitrary-exec** `node -e/-p/--eval/-r/--import`, `python -c`, `bash/sh -c`, `eval`, node-inline-`fs.*` — критично: это отмычки, способные подделать escape-грант в `~/.claude/runtime` и подорвать машины (floor-write-guard только Write-TOOL, не node-fs); (2) **install** `npm/yarn/pnpm/composer install/require/add`, `npx claude-*`; (3) **egress** `curl -X`, `wget`, `nc/ncat/netcat`, `socat`; (4) **redirect** `>`/`>>`/`2>file` (C16); (5) **env-prefix-интерпретатор**, `--watch` persistent; (6) sub-shell `$(...)`, prompt-injection `echo`. Необратимый субсет (`rm -rf`/`migrate:fresh`/`reset --hard`/force-push/`db:wipe`) уже в `FLOOR_RE`. Whitelist-подход router-gate (default-deny) снимается — content-безопасность Bash держат content-floor (всегда) + членство-в-плане (стена). | -| `powershell-gate` (PowerShell content) `[V1-PS]` | **Реоткрытие v3.8 F1 при наивном увольнении.** `floor-decide` инспектирует команду только при `name==='Bash'` (стр. 85) → PowerShell-tool пол не смотрит вообще. Перенос: content-floor правила 8 расширить на PowerShell-tool отдельным набором (`Remove-Item -Recurse -Force`, `Invoke-WebRequest`/`iwr`, `iex`, redirect `>`/`Out-File`, `Start-Process` интерпретатора) — PS-нативные глаголы НЕ матчат unix-regex, нужен свой набор. | +| `powershell-gate` (PowerShell content) `[V1-PS]` | **Реоткрытие v3.8 F1 при наивном увольнении.** `floor-decide` инспектирует команду только при `name==='Bash'` (стр. 85) → PowerShell-tool пол не смотрит вообще. Перенос: content-floor правила 8 расширить на PowerShell-tool отдельным набором (`Remove-Item -Recurse -Force`, `Invoke-WebRequest`/`iwr`, `iex`, redirect `>`/`Out-File`, `Start-Process` интерпретатора) — PS-нативные глаголы НЕ матчат unix-regex, нужен свой набор. **+ Поправки разбора плана:** (P-2) `canonicalAction` ОБЯЗАН получить ветку PowerShell (`powershell:…`) — без неё все PS-команды схлопываются в `write:`, escape неспецифичен (один грант открывает все PS); (P-3) PS-ветка пола рубит запись write-глаголом (`Set-Content`/`Add-Content`/`Copy-Item`/…) в защищённый путь — иначе PS подделывает escape-грант. См. план Task 1.2b/1.4. | | `L4` (`llm-judge-per-tool`/`response-scan`) | Тупой per-tool consistency-вопрос → покрыт надзором А2 судьи М4 (расхождение → голос «Верность намерению»). М4 шире (приёмка ТЗ/плана/результата). | | `runtime-write-deny` | Запись Write-инструментом в `~/.claude/runtime` → ветка пола М5 (RUNTIME_RE). **NB (правило 7):** после переезда обязан чтить escape (сейчас не чтит — DOC-1). | | `verify-before-push` / `verify-record` | «Свежая проверка перед push» → М5 по-критерийный подписанный GREEN + М4 Гейт-3. | @@ -239,7 +239,7 @@ L4 (`llm-judge-per-tool`/`response-scan`) ретайрится после тог **Правки существующих машин (→ обязателен `audit-context-building` перед каждой):** - М4: журнал-K2 для coverage/todowrite/decomposition (через skill-журналер); Гейт-2 план-требование; Гейт-3 verify; пост-оценщик D31 + halt-counter для rationalization; вливание self-debrief; ИИ-проводка (обёртка `enforce-judge-gate` + ключ/флаг). -- М5: мутация P18 как условие GREEN; подписанный по-критерийный GREEN; **content-floor класс (правило 8): port полного `BASH_HARD_BLACKLIST` + PowerShell-набор, блок независимо от плана** (V1/V1-PS — НЕ просто «перенос необратимого»); floor-write-guard расширить с Write-TOOL на содержимое команд node/PowerShell. +- М5: мутация P18 как условие GREEN; подписанный по-критерийный GREEN; **content-floor класс (правило 8): port полного `BASH_HARD_BLACKLIST` + PowerShell-набор, блок независимо от плана** (V1/V1-PS — НЕ просто «перенос необратимого»); floor-write-guard расширить с Write-TOOL на содержимое команд node/PowerShell. **Реализация (разбор плана 2026-06-08):** единый источник `matchBashHardBlacklist` (перенесён в `shell-content-rules.mjs`, НЕ ручной список — P-1); content-block whole+per-segment паритет с `bashIsFloor` (P-4); `suspicious ||= contentBlock` (P-5); `canonicalAction` +ветка PowerShell (P-2); PS forge-страж записи в защищённый путь (P-3). - М2: ветка-как-шаг-плана; действие-в-плане для вытесненной дисциплины; seed-allow реактивных дисциплинарных навыков (SE-K). - М3: роутинг по журналу (замена `router-tool-gate`); enforce by construction (без warn-only). - М6: escape на normative-ЗАКОН (Кусок 2) — расширение floor_escape на класс «правка закона»; **escape-survivability (правило 7): canonicalAction тотальна, panic-ветка, чтение escape остальными стражами**. @@ -281,6 +281,16 @@ L4 (`llm-judge-per-tool`/`response-scan`) ретайрится после тог **Открытый verify-item прошлого handoff закрыт фактом:** `enforce-parallel-session-lock.mjs:12` — no-op → §10 увольнение. +### Поправки разбора ПЛАНА (2026-06-08, второй проход) + +После написания плана реализации владелец заказал отдельный критический разбор **самого плана** (объектив «как будто план писал не я»; та же цепочка `audit-context-building` → `sharp-edges` → `variant-analysis` → `superpowers:systematic-debugging`, заземление против реального кода М1–М6). Найдено 8 находок (2 КРИТ / 1 ВЫС / 3 СРЕД / 2 НИЗ), все заземлены в коде, призраки отсеяны. Внесены в план (детали — план §«Поправки разбора плана» + Tasks 1.0.5/1.1/1.2b/1.3/1.4/1.5/1.6). Затрагивают и спеку: +- **P-1 (КРИТ):** заявленный «полный port `BASH_HARD_BLACKLIST`» в плане был **подмножеством** (терял #21 env-prefix / #22 --watch / #34 echo-inj / C16 `2>file` / cp-mv-chmod-chown-chgrp; `cp X ~/.claude/runtime/…` = forge escape-гранта). Закрытие: **единый источник** `matchBashHardBlacklist` (перенесён в `shell-content-rules.mjs`), не ручной список; §12 content-floor инвариант доказывает полноту по конструкции. Поэтому §5 «покрыть поимённо» теперь читается как «покрыть единым матчером + инвариант», не «перепечатать». +- **P-2 (КРИТ):** `canonicalAction` (escape-grant.mjs) не имеет ветки PowerShell → все PS-команды = `write:` → escape неспецифичен (одного гранта хватает на любую PS-команду в окне), причём план-тест escape был зелёным ложно (обе стороны равно сломаны). Закрытие — ветка PowerShell + тест специфичности. +- **P-3 (ВЫС):** PS-ветка пола возвращала `block:false` минуя runtime/secret-страж (поле `command` не парсится общим write-стражем) → PS forge escape-гранта. Закрытие — PS forge-страж. +- **P-4/P-5/P-6/P-7/P-8** — per-segment паритет content-block; `suspicious ||= contentBlock` (судья видит); §12 CI-инварианты закреплены за фазами; явный список fail-CLOSE-подмножества Фазы 0; честная пометка «план = детальная Фаза 1 + scoped-скелет». + +Промис §1 и предпосылки П1–П3 не меняются — разбор плана подтвердил их и **уточнил, что П1 (content-floor) держится только при едином источнике + PowerShell-привязке escape**, иначе остаётся дыра. + ## Само-аудит спеки (2026-06-08) Дизайн собран через цепочку `audit-context-building` (карта обходов §2, построчно с цитатами строк) + `sharp-edges` (6 находок SE1–SE6 → 6 правил ужесточения §4.1) + `variant-analysis` (sweep всех близнецов §2.1). Ядро — **принцип атрибуции §1**: непробиваемость (Кусок 1) + неприкосновенный закон (Кусок 2) + видимость (Кусок 3) ⇒ глюк машины ≠ хитрость контроллера. Технический content проверен против реального кода М1–М6; границы с машинами — в §3–§5. Остаточные пределы названы честно §11.