13 KiB
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: [] ведёт себя байт-в-байт как сейчас.
Переговоры
Позиция контроллера по типовым замечаниям ревью к правке существующих файлов:
- Edit, не Write-overwrite. Все три исходника прочитаны целиком в этой сессии;
old_stringкаждого Edit — байт-точная подстрока текущего состояния. Правки аддитивны (новый параметр со значением по умолчанию / новый экспорт) — существующие сигнатуры и тела не ломаются. - Бэкап — git. Файлы зафиксированы (
b9730af/97985b4); откат любой правки —git restore/git show HEAD:<путь>. Отдельный.bakизбыточен. - Направление изменения — fail-CLOSED. Добавляемый ключ только РАСШИРЯЕТ защиту; пусто / невалидно → база неизменна. Конфигом ослабить защиту нельзя.
- Обратная совместимость — тестами. Каждая задача: RED→GREEN + кейс «один аргумент → байт-в-байт» + полный регресс. Авторитетный полный свод — в терминале владельца.
["test-driven-development"]
[
{"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"}
]
[
{"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уже есть в файле — не дублировать):
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.mjsExpected: новый тест FAIL (protected_pathsis undefined). NB: авторитетный прогон — в терминале владельца. -
Step 3 (Edit impl): в
tools/brain-config.mjs, в объектеDEFAULTS, добавить ключ послеeconomy_default:
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.mjsExpected: 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уже есть):
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.mjsExpected: FAIL (augment-кейсы не проходят — функция игнорирует второй аргумент). -
Step 7 (Edit impl): в
tools/enforce-normative-content-rules.mjsзаменить функциюisNormativePath(old_string= текущие строки 20–25, комментарий + функция) на:
/** 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.mjsExpected: 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-блок:
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.mjsExpected: FAIL (buildProtectedPatternsне существует). -
Step 11 (Edit impl): в
tools/shell-content-rules.mjsвставить новую функцию сразу после массиваDEFAULT_PROTECTED_PATTERNS(old_string= последняя строка массива/(^|\/)\.npmrc$/i,+ закрывающая];):
/(^|\/)\.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.mjsExpected: 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.