From 3d7690650ecd933162745e8a62f8e2225c24a274 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: Tue, 16 Jun 2026 09:41:58 +0300 Subject: [PATCH] =?UTF-8?q?feat(brain-config):=20shell-content=20=D0=B7?= =?UTF-8?q?=D0=B0=D1=89=D0=B8=D1=82=D0=B0=20config-driven=20(greenfield=20?= =?UTF-8?q?#3=20shell)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildProtectedPatterns 2-й параметр normativeFiles даёт anchored .md stem-паттерны; оба гейта в main строят protectedPaths из loadConfig (try/catch fallback DEFAULT). DEFAULT 32-34 сохранён (backward-compat); augment только добавляет защиту. shell-content-rules импортирует docStem из cross-ref-checker. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...2026-06-16-greenfield-shell-config-plan.md | 150 ++++++++++++++++++ tools/enforce-powershell-gate.mjs | 9 +- tools/enforce-router-gate.mjs | 9 +- tools/shell-content-rules.mjs | 16 +- tools/shell-content-rules.test.mjs | 10 ++ 5 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-16-greenfield-shell-config-plan.md diff --git a/docs/superpowers/plans/2026-06-16-greenfield-shell-config-plan.md b/docs/superpowers/plans/2026-06-16-greenfield-shell-config-plan.md new file mode 100644 index 0000000..20eae99 --- /dev/null +++ b/docs/superpowers/plans/2026-06-16-greenfield-shell-config-plan.md @@ -0,0 +1,150 @@ +# greenfield shell-content config-driven защита — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (инлайн под стеной; production-правки под TDD-gate override + floor-escape). Steps — checkbox (`- [ ]`). + +**Goal:** Подключить config-augment защиты нормативки (`buildProtectedPatterns`) в живые shell-гейты, чтобы greenfield-проект защищал свои нормативные доки от shell-чтения/перезаписи; claude-brain — без регресса. + +**Architecture:** Refines design `2026-06-16-greenfield-regex-names-config-design.md` §4 (вместо «убрать 32-34» — **оставить DEFAULT + augment**, т.к. тест 281 пинит DEFAULT-защиту, а шов был не подключён). `pathDenyOverlay` зовут `enforce-powershell-gate` (живой) и `enforce-router-gate` (увольняется, но wire future-proof); оба строят `ctx.protectedPaths = DEFAULT_PROTECTED_PATTERNS` (хардкод). Меняем: `buildProtectedPatterns(configPaths, normativeFiles)` добавляет filename-anchored стем-паттерны `(^|/)[^/]*\.md` (`docStem` экспортирован из cross-ref-checker.mjs, deb5049); оба гейта в `main()` строят `protectedPaths` из `loadConfig()` (try/catch → DEFAULT fallback). DEFAULT 32-34 не трогаем → backward-compat. + +**Tech Stack:** Node ESM (.mjs), vitest. + +--- + +## File Structure + +- Modify: `tools/shell-content-rules.mjs` (+ `.test.mjs`) — import `docStem`; `buildProtectedPatterns` 2-й параметр `normativeFiles` + stem-паттерны. +- Modify: `tools/enforce-powershell-gate.mjs` — import `buildProtectedPatterns`; wire `main()` (стр. 102). +- Modify: `tools/enforce-router-gate.mjs` — import `buildProtectedPatterns`; wire `main()` (стр. 192). + +--- + +## Task 1: `buildProtectedPatterns` 2-й параметр `normativeFiles` (TDD) + +**Files:** `tools/shell-content-rules.mjs`, `tools/shell-content-rules.test.mjs` + +- [ ] **Step 1 (RED test):** в `shell-content-rules.test.mjs`, в `describe('buildProtectedPatterns augment ...')` добавить: + +```javascript + it('normativeFiles → anchored .md stem-паттерны (greenfield); DEFAULT сохранён', () => { + const pats = buildProtectedPatterns([], ['docs/MyRules_v2.md']); + expect(isProtectedPath('docs/MyRules_v2.md', defaultPathNormalize, pats)).toBe(true); + expect(isProtectedPath('proj/MyRules-notes.md', defaultPathNormalize, pats)).toBe(true); + expect(isProtectedPath('app/Models/Deal.php', defaultPathNormalize, pats)).toBe(false); + expect(isProtectedPath('CLAUDE.md', defaultPathNormalize, pats)).toBe(true); + }); + it('пустой normativeFiles → DEFAULT + configPaths (backward-compat)', () => { + expect(buildProtectedPatterns([], [])).toEqual(DEFAULT_PROTECTED_PATTERNS); + }); +``` + +- [ ] **Step 2:** Run RED — `npx vitest run --config vitest.config.tools.mjs tools/shell-content-rules.test.mjs` → FAIL (2-й аргумент игнорируется; MyRules не защищён). + +- [ ] **Step 3 (impl):** в `shell-content-rules.mjs` добавить импорт (после существующих import-ов вверху): + +```javascript +import { docStem } from './cross-ref-checker.mjs'; +``` + +и заменить `buildProtectedPatterns` (текущие строки 46-54): + +```javascript +export function buildProtectedPatterns(configPaths = [], normativeFiles = []) { + const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const extra = Array.isArray(configPaths) + ? configPaths.map((p) => String(p || '').replace(/\\/g, '/').trim()).filter((p) => p.length > 0) + .map((p) => new RegExp('(^|/)' + esc(p), 'i')) + : []; + const stems = Array.isArray(normativeFiles) + ? normativeFiles.map((f) => docStem(f)).filter((s) => s.length > 0) + .map((s) => new RegExp('(^|/)' + esc(s) + '[^/]*\\.md', 'i')) + : []; + return [...DEFAULT_PROTECTED_PATTERNS, ...extra, ...stems]; +} +``` + +- [ ] **Step 4:** Run GREEN — тот же прогон → PASS (новые + прежние 380-393: `()`/`([])`/`(null)` → DEFAULT, configPaths augment). + +--- + +## Task 2: wire `enforce-powershell-gate` main() + +**Files:** `tools/enforce-powershell-gate.mjs` + +- [ ] **Step 1 (import):** в import-блоке из `./shell-content-rules.mjs` (стр. 10-11, рядом с `DEFAULT_PROTECTED_PATTERNS`/`pathDenyOverlay`) добавить строку: + +```javascript + buildProtectedPatterns, +``` + +- [ ] **Step 2 (wire main):** заменить блок построения `ctx` (стр. 99-104): + +```javascript + let protectedPaths = DEFAULT_PROTECTED_PATTERNS; + try { + const { loadConfig } = await import('./brain-config.mjs'); + const cfg = loadConfig(); + protectedPaths = buildProtectedPatterns(cfg.protected_paths, cfg.normative_files); + } catch { /* дефолт DEFAULT_PROTECTED_PATTERNS */ } + const ctx = { + approvedGitOps: loadApprovedGitOps(sessionId), + pathNormalize: await resolvePathNormalize(), + protectedPaths, + now: Date.now(), + }; +``` + +- [ ] **Step 3 (verify):** владелец: `npx vitest run --config vitest.config.tools.mjs tools/enforce-powershell-gate.test.mjs` → PASS (gate-тесты используют DEFAULT через свой ctx/инъекцию; live main читает config с fallback). + +--- + +## Task 3: wire `enforce-router-gate` main() + +**Files:** `tools/enforce-router-gate.mjs` + +- [ ] **Step 1 (import):** в import-блоке из `./shell-content-rules.mjs` (стр. 15-16) добавить: + +```javascript + buildProtectedPatterns, +``` + +- [ ] **Step 2 (wire main):** заменить блок построения `ctx` (стр. 188-194): + +```javascript + let protectedPaths = DEFAULT_PROTECTED_PATTERNS; + try { + const { loadConfig } = await import('./brain-config.mjs'); + const cfg = loadConfig(); + protectedPaths = buildProtectedPatterns(cfg.protected_paths, cfg.normative_files); + } catch { /* дефолт DEFAULT_PROTECTED_PATTERNS */ } + const ctx = { + approvedGitOps: loadApprovedGitOps(sessionId), + editedFiles: readEditedFiles(sessionId), + pathNormalize, + protectedPaths, + now: Date.now(), + }; +``` + +- [ ] **Step 3 (verify):** владелец: `npx vitest run --config vitest.config.tools.mjs tools/enforce-router-gate.test.mjs` → PASS. + +--- + +## Task 4: Полный свод + коммит (терминал владельца) + +- [ ] **Step 1:** `npx vitest run --config vitest.config.tools.mjs` → зелёный, новых падений нет. +- [ ] **Step 2:** commit: + +```bash +git add tools/shell-content-rules.mjs tools/shell-content-rules.test.mjs tools/enforce-powershell-gate.mjs tools/enforce-router-gate.mjs docs/superpowers/plans/2026-06-16-greenfield-shell-config-plan.md +git commit -m "feat(brain-config): shell-content защита config-driven (greenfield #3 shell)" +``` + +--- + +## Self-Review + +- **Spec coverage:** §4 (refined) — Task 1 (buildProtectedPatterns normativeFiles + keep DEFAULT), Task 2/3 (wire оба гейта). §5 fail-safe: пустой normative_files → stems=[] → DEFAULT защищает universal (Task 1 тест). Граница §4: 32-34 НЕ убраны (backward-compat, тест 281). +- **Placeholders:** нет; весь код реальный (точные строки 10/99-104, 15/188-194). +- **Type consistency:** `buildProtectedPatterns(configPaths, normativeFiles)` — единая сигнатура; `docStem` импортируется из cross-ref-checker.mjs (deb5049). Оба гейта — одинаковый wiring-блок. +- **Стена:** production-правки (3 файла: shell-content-rules/powershell-gate/router-gate) под TDD-gate override + floor-escape per Edit; `.test.mjs` — gate молчит. ~7 правок. Полный свод + коммит — терминал владельца. +- **Security-note:** оба гейта — fail-CLOSE при ошибке (catch → block); config-чтение в try/catch → при сбое падаем на DEFAULT (защита не слабеет). Augment только ДОБАВЛЯет паттерны (никогда не убирает) → защита монотонно растёт. diff --git a/tools/enforce-powershell-gate.mjs b/tools/enforce-powershell-gate.mjs index 524c75e..874e61d 100644 --- a/tools/enforce-powershell-gate.mjs +++ b/tools/enforce-powershell-gate.mjs @@ -9,6 +9,7 @@ import { defaultPathNormalize, DEFAULT_PROTECTED_PATTERNS, pathDenyOverlay, + buildProtectedPatterns, classifyGitCommand, loadApprovedGitOps, matchPsHardBlacklist, @@ -96,10 +97,16 @@ async function main() { if (event.tool_name !== 'PowerShell') { exitDecision({ block: false }); return; } const command = (event.tool_input && event.tool_input.command) || ''; const sessionId = event.session_id || 'unknown'; + let protectedPaths = DEFAULT_PROTECTED_PATTERNS; + try { + const { loadConfig } = await import('./brain-config.mjs'); + const cfg = loadConfig(); + protectedPaths = buildProtectedPatterns(cfg.protected_paths, cfg.normative_files); + } catch { /* дефолт DEFAULT_PROTECTED_PATTERNS */ } const ctx = { approvedGitOps: loadApprovedGitOps(sessionId), pathNormalize: await resolvePathNormalize(), - protectedPaths: DEFAULT_PROTECTED_PATTERNS, + protectedPaths, now: Date.now(), }; const verdict = classifyPowerShellCommand(command, ctx); diff --git a/tools/enforce-router-gate.mjs b/tools/enforce-router-gate.mjs index dfad92d..8559fa8 100644 --- a/tools/enforce-router-gate.mjs +++ b/tools/enforce-router-gate.mjs @@ -14,6 +14,7 @@ import { defaultPathNormalize, DEFAULT_PROTECTED_PATTERNS, pathDenyOverlay, + buildProtectedPatterns, extractPathArgs, matchBashHardBlacklist, BASH_HARD_BLACKLIST, @@ -185,11 +186,17 @@ async function main() { const command = (event.tool_input && event.tool_input.command) || ''; const sessionId = event.session_id || 'unknown'; const pathNormalize = await resolvePathNormalize(); + let protectedPaths = DEFAULT_PROTECTED_PATTERNS; + try { + const { loadConfig } = await import('./brain-config.mjs'); + const cfg = loadConfig(); + protectedPaths = buildProtectedPatterns(cfg.protected_paths, cfg.normative_files); + } catch { /* дефолт DEFAULT_PROTECTED_PATTERNS */ } const ctx = { approvedGitOps: loadApprovedGitOps(sessionId), editedFiles: readEditedFiles(sessionId), pathNormalize, - protectedPaths: DEFAULT_PROTECTED_PATTERNS, + protectedPaths, now: Date.now(), }; const verdict = classifyBashCommand(command, ctx); diff --git a/tools/shell-content-rules.mjs b/tools/shell-content-rules.mjs index f126fbe..759ee53 100644 --- a/tools/shell-content-rules.mjs +++ b/tools/shell-content-rules.mjs @@ -8,6 +8,7 @@ import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; +import { docStem } from './cross-ref-checker.mjs'; // ── Path normalization (Stream A заглушка; реальная — path-normalization.mjs) ── export function defaultPathNormalize(target) { @@ -43,14 +44,23 @@ export const DEFAULT_PROTECTED_PATTERNS = [ /** fail-CLOSED augment (§D2): UNION базовых DEFAULT_PROTECTED_PATTERNS с config-путями. * База всегда первая и не удаляется; пусто / не-массив → только база (защита байт-в-байт). * Каждый config-путь экранируется и матчится по сегменту пути (^|/)… (case-insensitive). */ -export function buildProtectedPatterns(configPaths = []) { +export function buildProtectedPatterns(configPaths = [], normativeFiles = []) { + const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const extra = Array.isArray(configPaths) ? configPaths .map((p) => String(p || '').replace(/\\/g, '/').trim()) .filter((p) => p.length > 0) - .map((p) => new RegExp('(^|/)' + p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')) + .map((p) => new RegExp('(^|/)' + esc(p), 'i')) : []; - return [...DEFAULT_PROTECTED_PATTERNS, ...extra]; + // #3-shell: нормативные доки из config → filename-anchored stem-паттерны (greenfield). + // DEFAULT (32-34) сохраняется; augment только ДОБАВЛЯет (защита монотонно растёт). + const stems = Array.isArray(normativeFiles) + ? normativeFiles + .map((f) => docStem(f)) + .filter((s) => s.length > 0) + .map((s) => new RegExp('(^|/)' + esc(s) + '[^/]*\\.md', 'i')) + : []; + return [...DEFAULT_PROTECTED_PATTERNS, ...extra, ...stems]; } // Read-tool deny list — narrower than DEFAULT_PROTECTED_PATTERNS (over-block fix 2026-05-31). diff --git a/tools/shell-content-rules.test.mjs b/tools/shell-content-rules.test.mjs index 79e84f3..793d981 100644 --- a/tools/shell-content-rules.test.mjs +++ b/tools/shell-content-rules.test.mjs @@ -392,4 +392,14 @@ describe('buildProtectedPatterns augment (Task 4 security, §D2 fail-CLOSED)', ( expect(isProtectedPath('app/secrets/keys.txt', defaultPathNormalize, pats)).toBe(true); expect(isProtectedPath('app/Models/Deal.php', defaultPathNormalize, pats)).toBe(false); }); + it('normativeFiles → anchored .md stem-паттерны (greenfield); DEFAULT сохранён', () => { + const pats = buildProtectedPatterns([], ['docs/MyRules_v2.md']); + expect(isProtectedPath('docs/MyRules_v2.md', defaultPathNormalize, pats)).toBe(true); + expect(isProtectedPath('proj/MyRules-notes.md', defaultPathNormalize, pats)).toBe(true); + expect(isProtectedPath('app/Models/Deal.php', defaultPathNormalize, pats)).toBe(false); + expect(isProtectedPath('CLAUDE.md', defaultPathNormalize, pats)).toBe(true); + }); + it('пустой normativeFiles → DEFAULT + configPaths (backward-compat)', () => { + expect(buildProtectedPatterns([], [])).toEqual(DEFAULT_PROTECTED_PATTERNS); + }); });