diff --git a/tools/floor-manifest-check.mjs b/tools/floor-manifest-check.mjs index 76650b41..aef1a6f2 100644 --- a/tools/floor-manifest-check.mjs +++ b/tools/floor-manifest-check.mjs @@ -21,9 +21,18 @@ const HOOK_EVENTS = [ 'SessionEnd', 'UserPromptSubmit', 'PreCompact', 'Notification', ]; -// Минимально-обязательный набор для «пол подтверждён» — несущий пол-хук. Живая обёртка/ -// владелец может передать расширенный защитный набор; контракт checkManifest — по requiredHooks. -const DEFAULT_REQUIRED_HOOKS = ['enforce-floor.mjs']; +// Security-load-bearing набор для «пол подтверждён» (F-3, аудит 2026-06-07): не один пол, а +// весь защитный контур — несущий пол (вето-до-плана) + верховная стена (вне-плана) + стражи +// нормативки/read-exfil/egress-exfil. Раньше проверялся только enforce-floor → owner мог +// забыть стену/стражей и получить зелёный «protected». Живая обёртка/владелец может передать +// иной набор; контракт checkManifest — по requiredHooks (DEFAULT — фолбэк main()). WARN-only. +const DEFAULT_REQUIRED_HOOKS = [ + 'enforce-floor.mjs', // несущий пол: вето-до-плана на необратимое + 'enforce-supreme-gate.mjs', // верховная стена: default-deny вне плана (нужны ОБА) + 'enforce-normative-content-rules.mjs', // защита нормативки/памяти от подмены + 'enforce-read-path-deny.mjs', // read-exfil (transcript/runtime/secrets) + 'enforce-mcp-classification.mjs', // egress-exfil (исходящий MCP-payload) +]; /** Собрать все command-строки хуков из settings (битые/пустые секции игнорируются, не бросает). */ export function collectHookCommands(settings) { diff --git a/tools/floor-manifest-check.test.mjs b/tools/floor-manifest-check.test.mjs index 26a423d9..b33ee9bb 100644 --- a/tools/floor-manifest-check.test.mjs +++ b/tools/floor-manifest-check.test.mjs @@ -57,3 +57,27 @@ describe('checkManifest (6.5, Δ8): WARN при отсутствии регис expect(r.block).toBeUndefined(); }); }); + +// F-3 (аудит 2026-06-07): DEFAULT_REQUIRED_HOOKS манифеста floor-manifest-check проверял только +// enforce-floor — owner мог зарегистрировать пол, забыть стену/exfil-стражей и получить зелёный +// «protected». Расширяем DEFAULT до security-load-bearing набора (пол + стена + normative/read/mcp +// стражи), чтобы «пол подтверждён» означал весь защитный контур, а не один хук. WARN-only. +describe('floor-manifest-check — F-3: DEFAULT защитный набор шире одного enforce-floor', () => { + it('settings с одним enforce-floor (без стены) → cry (DEFAULT теперь требует защитный набор)', () => { + const onlyFloor = { hooks: { PreToolUse: [{ matcher: '*', hooks: [{ type: 'command', command: 'node tools/enforce-floor.mjs' }] }] } }; + const r = checkManifest({ settings: onlyFloor }); // без requiredHooks → DEFAULT + expect(r.cry).toBe(true); + expect(r.missing).toContain('enforce-supreme-gate.mjs'); + }); + it('settings с полным защитным набором → ok (не кричит)', () => { + const full = { hooks: { PreToolUse: [{ matcher: '*', hooks: [ + { type: 'command', command: 'node tools/enforce-floor.mjs' }, + { type: 'command', command: 'node tools/enforce-supreme-gate.mjs' }, + { type: 'command', command: 'node tools/enforce-normative-content-rules.mjs' }, + { type: 'command', command: 'node tools/enforce-read-path-deny.mjs' }, + { type: 'command', command: 'node tools/enforce-mcp-classification.mjs' }, + ] }] } }; + const r = checkManifest({ settings: full }); // без requiredHooks → DEFAULT + expect(r.ok).toBe(true); + }); +});