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

13 KiB
Raw Blame History

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 + кейс «один аргумент → байт-в-байт» + полный регресс. Авторитетный полный свод — в терминале владельца.
["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.mjs Expected: новый тест FAIL (protected_paths is 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.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 уже есть):
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, комментарий + функция) на:

/** 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-rulesbuildProtectedPatterns

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.mjs Expected: 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.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.