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:
@@ -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). Чистая. */
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user