Files
brain/docs/superpowers/plans/2026-06-15-task4-security-protected-paths-plan-v2.md
T

217 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task 4 security — config-augment protected_paths (fail-CLOSED) — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (инлайн под стеной — субагенты запрещены, VA-4). Steps — checkbox (`- [ ]`).
**Goal:** Добавить config-управляемый fail-CLOSED augment `protected_paths` в `enforce-normative-content-rules` (`isNormativePath`) и `shell-content-rules` (`buildProtectedPatterns`) + дефолтный ключ в `brain-config`, не меняя поведение claude-brain (backward-compat: дефолт = только база).
**Architecture:** Чистый seam — новые параметры со значением по умолчанию; база хардкод и неизменна, конфиг только UNION (никогда не убирает). Подключение значений в `main()` хуков — Задача 7.
**Tech Stack:** Node.js ESM (`tools/`), vitest (`vitest.config.tools.mjs`).
**Спек:** `docs/superpowers/specs/2026-06-15-task4-security-protected-paths-spec-v2.md` (§D1 контракт, §D2 fail-CLOSED union, §D3 крайние случаи + критерий).
## Цель
Вынести два захардкоженных защитных списка движка в config-управляемое расширение `protected_paths`
(fail-CLOSED union): база неизменна, конфиг только добавляет пути под защиту. claude-brain с
`protected_paths: []` ведёт себя байт-в-байт как сейчас.
## Переговоры
Позиция контроллера по типовым замечаниям ревью к правке существующих файлов:
1. **Edit, не Write-overwrite.** Все три исходника прочитаны целиком в этой сессии; `old_string`
каждого Edit — байт-точная подстрока текущего состояния. Правки аддитивны (новый параметр со
значением по умолчанию / новый экспорт) — существующие сигнатуры и тела не ломаются.
2. **Бэкап — git.** Файлы зафиксированы (`b9730af` / `97985b4`); откат любой правки —
`git restore` / `git show HEAD:<путь>`. Отдельный `.bak` избыточен.
3. **Направление изменения — fail-CLOSED.** Добавляемый ключ только РАСШИРЯЕТ защиту; пусто /
невалидно → база неизменна. Конфигом ослабить защиту нельзя.
4. **Обратная совместимость — тестами.** Каждая задача: RED→GREEN + кейс «один аргумент →
байт-в-байт» + полный регресс. Авторитетный полный свод — в терминале владельца.
```skills-json
["test-driven-development"]
```
```steps-json
[
{"op":"Edit","object":"tools/brain-config.test.mjs","ref":"D1"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/brain-config.mjs","ref":"D1"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/enforce-normative-content-rules.test.mjs","ref":"D2"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/enforce-normative-content-rules.mjs","ref":"D2"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/shell-content-rules.test.mjs","ref":"D3"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/shell-content-rules.mjs","ref":"D2"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"}
]
```
```verified-context-json
[
{"id":"D1","kind":"EXTRACTED","ref":"tools/brain-config.mjs","anchor":"const DEFAULTS = Object.freeze({"},
{"id":"D2","kind":"EXTRACTED","ref":"tools/enforce-normative-content-rules.mjs","anchor":"const NORMATIVE_PATTERNS = ["},
{"id":"D3","kind":"EXTRACTED","ref":"tools/shell-content-rules.mjs","anchor":"export const DEFAULT_PROTECTED_PATTERNS = ["}
]
```
---
## Task A: brain-config — дефолтный ключ `protected_paths`
**Files:** Modify `tools/brain-config.mjs`, `tools/brain-config.test.mjs`
- [ ] **Step 1 (Edit test, RED):** в `tools/brain-config.test.mjs` добавить describe-блок (импорт `resolveConfig` уже есть в файле — не дублировать):
```javascript
describe('resolveConfig protected_paths (Task 4 security, §D1/§D2)', () => {
it('отсутствует → [] (fail-CLOSED augment пуст = база защищает полностью)', () => {
expect(resolveConfig({}).protected_paths).toEqual([]);
});
it('список пробрасывается как есть', () => {
expect(resolveConfig({ protected_paths: ['secrets/keys'] }).protected_paths)
.toEqual(['secrets/keys']);
});
});
```
- [ ] **Step 2 (Bash, verify FAIL):** `npx vitest run --config vitest.config.tools.mjs`
Expected: новый тест FAIL (`protected_paths` is undefined). NB: авторитетный прогон — в терминале владельца.
- [ ] **Step 3 (Edit impl):** в `tools/brain-config.mjs`, в объекте `DEFAULTS`, добавить ключ после `economy_default`:
```javascript
classifier_context: 'generic project (no profile configured)',
economy_default: '100',
protected_paths: [],
});
```
(`old_string` = три текущие строки `classifier_context … economy_default … });`; добавляется строка `protected_paths: [],`.)
- [ ] **Step 4 (Bash, verify PASS):** `npx vitest run --config vitest.config.tools.mjs`
Expected: PASS (новые + все существующие).
---
## Task B: `enforce-normative-content-rules` — augment `isNormativePath`
**Files:** Modify `tools/enforce-normative-content-rules.mjs`, `tools/enforce-normative-content-rules.test.mjs`
- [ ] **Step 5 (Edit test, RED):** в `tools/enforce-normative-content-rules.test.mjs` добавить describe-блок (импорт `isNormativePath` уже есть):
```javascript
describe('isNormativePath augment (Task 4 security, §D2 fail-CLOSED)', () => {
it('backward-compat: один аргумент → только база', () => {
expect(isNormativePath('CLAUDE.md')).toBe(true);
expect(isNormativePath('app/secret/keys.md')).toBe(false);
});
it('extraProtectedPaths добавляет путь под гейт', () => {
expect(isNormativePath('app/secret/keys.md', ['secret/keys'])).toBe(true);
});
it('база сохраняется при непустом augment', () => {
expect(isNormativePath('CLAUDE.md', ['secret/keys'])).toBe(true);
});
it('пусто / не-массив → только база (fail-CLOSED)', () => {
expect(isNormativePath('app/secret/keys.md', [])).toBe(false);
expect(isNormativePath('app/secret/keys.md', null)).toBe(false);
});
it('пустые строки в списке отбрасываются', () => {
expect(isNormativePath('app/x.md', [' ', ''])).toBe(false);
});
});
```
- [ ] **Step 6 (Bash, verify FAIL):** `npx vitest run --config vitest.config.tools.mjs`
Expected: FAIL (augment-кейсы не проходят — функция игнорирует второй аргумент).
- [ ] **Step 7 (Edit impl):** в `tools/enforce-normative-content-rules.mjs` заменить функцию `isNormativePath` (`old_string` = текущие строки 20–25, комментарий + функция) на:
```javascript
/** True if the file path is a protected normative document (§3.6.1).
* @param {string} filePath
* @param {string[]} [extraProtectedPaths] — fail-CLOSED augment (§D2): config protected_paths
* ТОЛЬКО добавляет пути под гейт; пусто / не-массив → защищает только база. */
export function isNormativePath(filePath, extraProtectedPaths = []) {
if (typeof filePath !== 'string') return false;
const n = filePath.replace(/\\/g, '/');
if (NORMATIVE_PATTERNS.some((re) => re.test(n))) return true;
if (!Array.isArray(extraProtectedPaths)) return false;
return extraProtectedPaths.some((p) => {
const e = String(p || '').replace(/\\/g, '/').trim();
return e.length > 0 && n.includes(e);
});
}
```
- [ ] **Step 8 (Bash, verify PASS):** `npx vitest run --config vitest.config.tools.mjs`
Expected: PASS (новые + все существующие — `main()` зовёт `isNormativePath(filePath)` с одним аргументом → база, поведение не изменилось).
---
## Task C: `shell-content-rules` — `buildProtectedPatterns`
**Files:** Modify `tools/shell-content-rules.mjs`, `tools/shell-content-rules.test.mjs`
- [ ] **Step 9 (Edit test, RED):** в `tools/shell-content-rules.test.mjs` (1) добавить `buildProtectedPatterns` в существующий импорт из `./shell-content-rules.mjs`; (2) добавить describe-блок:
```javascript
describe('buildProtectedPatterns augment (Task 4 security, §D2 fail-CLOSED)', () => {
it('пусто / без аргумента → база байт-в-байт', () => {
expect(buildProtectedPatterns()).toEqual(DEFAULT_PROTECTED_PATTERNS);
expect(buildProtectedPatterns([])).toEqual(DEFAULT_PROTECTED_PATTERNS);
});
it('не-массив → только база (fail-CLOSED)', () => {
expect(buildProtectedPatterns(null)).toEqual(DEFAULT_PROTECTED_PATTERNS);
});
it('пустые строки отбрасываются', () => {
expect(buildProtectedPatterns(['', ' '])).toEqual(DEFAULT_PROTECTED_PATTERNS);
});
it('добавляет config-путь, база сохранена', () => {
const pats = buildProtectedPatterns(['secrets/keys']);
expect(isProtectedPath('CLAUDE.md', defaultPathNormalize, pats)).toBe(true);
expect(isProtectedPath('app/secrets/keys.txt', defaultPathNormalize, pats)).toBe(true);
expect(isProtectedPath('app/Models/Deal.php', defaultPathNormalize, pats)).toBe(false);
});
});
```
- [ ] **Step 10 (Bash, verify FAIL):** `npx vitest run --config vitest.config.tools.mjs`
Expected: FAIL (`buildProtectedPatterns` не существует).
- [ ] **Step 11 (Edit impl):** в `tools/shell-content-rules.mjs` вставить новую функцию сразу после массива `DEFAULT_PROTECTED_PATTERNS` (`old_string` = последняя строка массива `/(^|\/)\.npmrc$/i,` + закрывающая `];`):
```javascript
/(^|\/)\.npmrc$/i,
];
/** fail-CLOSED augment (§D2): UNION базовых DEFAULT_PROTECTED_PATTERNS с config-путями.
* База всегда первая и не удаляется; пусто / не-массив → только база (защита байт-в-байт).
* Каждый config-путь экранируется и матчится по сегменту пути (^|/)… (case-insensitive). */
export function buildProtectedPatterns(configPaths = []) {
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'))
: [];
return [...DEFAULT_PROTECTED_PATTERNS, ...extra];
}
```
- [ ] **Step 12 (Bash, verify PASS + полный регресс):** `npx vitest run --config vitest.config.tools.mjs`
Expected: PASS весь свод (backward-compat дефолты сохранили поведение). Авторитетный прогон — в терминале владельца.
---
## Self-Review
- **Покрытие спека:** §D1 — Task A (ключ) + B (isNormativePath) + C (buildProtectedPatterns); §D2 fail-CLOSED — тесты «пусто/не-массив→база» в каждой задаче; §D3 крайние случаи — backward-compat (один аргумент), null, пустые строки, база-сохранена — все в тестах; критерий — Step 12 полный свод.
- **Заглушек нет:** каждый impl-шаг несёт полный код; тесты — полные; команды vitest точные.
- **Согласованность имён:** `protected_paths` (config-ключ) → `extraProtectedPaths` (isNormativePath) / `configPaths` (buildProtectedPatterns); дефолт всюду = только база (backward-compat инвариант).
- **Стена:** все три исходника + 2 из 3 тестов — discipline-source; под ЗАПЕЧАТАННЫМ планом проходят как CARD (build-loop §6). `brain-config.{mjs,test.mjs}` — не нормативный/не-discipline путь → гейт не engage. Bash-шаги floor-safe (`npx vitest`, без node -e/install/rm). MultiEdit не используется (недоступен). Wiring в `main()` — Задача 7, вне scope.