217 lines
13 KiB
Markdown
217 lines
13 KiB
Markdown
# 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.
|