feat(brain-config): protected_paths fail-CLOSED augment (Фаза 1 Task 4 security)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-15 13:17:28 +03:00
parent 4ff8f2b418
commit bcd55abbc9
6 changed files with 76 additions and 3 deletions
+1
View File
@@ -46,6 +46,7 @@ const DEFAULTS = Object.freeze({
project_url_whitelist: [],
classifier_context: 'generic project (no profile configured)',
economy_default: '100',
protected_paths: [],
});
/** Наложить дефолты + fail-safe направления (§D3). Чистая. */
+10
View File
@@ -67,3 +67,13 @@ describe('resolveConfig fail-safe (§D3)', () => {
expect(r.registry_path).toBe('docs/registry/nodes.yaml');
});
});
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']);
});
});
+11 -3
View File
@@ -17,11 +17,19 @@ const NORMATIVE_PATTERNS = [
/(^|\/)docs\/Tooling_[^/]*\.md$/,
];
/** True if the file path is a protected normative document (§3.6.1). */
export function isNormativePath(filePath) {
/** 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, '/');
return NORMATIVE_PATTERNS.some((re) => re.test(n));
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);
});
}
// 7.2 (H3, Блок 4.2) — наиболее защищаемое подмножество нормативки: CLAUDE.md / память /
@@ -362,3 +362,23 @@ describe('Ф8 cherry-pick d1ad4e85 — слияние сохранило ОБЕ
expect(src).toContain("logGuardBlock(event, 'М1/М5 Нормативный'");
});
});
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);
});
});
+13
View File
@@ -40,6 +40,19 @@ export const DEFAULT_PROTECTED_PATTERNS = [
/(^|\/)\.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];
}
// Read-tool deny list — narrower than DEFAULT_PROTECTED_PATTERNS (over-block fix 2026-05-31).
// Smoke 5 reused the full protected-list for the Read tool, which blocked Read of
// CLAUDE.md, the normative docs and the memory/ index — breaking the legit
+21
View File
@@ -372,3 +372,24 @@ describe('matchPsHardBlacklist hosted in shell-content-rules (M7 PS single-sourc
expect(matchPsHardBlacklist('rm x')).toBeTruthy();
});
});
import { buildProtectedPatterns } from './shell-content-rules.mjs';
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);
});
});