import { describe, it, expect } from 'vitest'; import { decide } from './enforce-read-path-deny.mjs'; describe('enforce-read-path-deny decide()', () => { it('allows Read on normal project file', () => { const r = decide({ toolName: 'Read', filePath: 'docs/observer/STATUS.md' }); expect(r.block).toBe(false); }); it('blocks Read on ~/.claude/projects/*.jsonl transcript', () => { const r = decide({ toolName: 'Read', filePath: '~/.claude/projects/abc-session.jsonl' }); expect(r.block).toBe(true); expect(r.reason).toMatch(/protected/i); }); it('blocks Read on absolute /c/Users/.../.claude/projects/x.jsonl', () => { const r = decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/proj/session.jsonl' }); expect(r.block).toBe(true); }); it('blocks Read on ~/.claude/runtime/*.json (runtime artifacts)', () => { const r = decide({ toolName: 'Read', filePath: '~/.claude/runtime/router-state-x.json' }); expect(r.block).toBe(true); }); it('blocks Read on .env', () => { const r = decide({ toolName: 'Read', filePath: '.env' }); expect(r.block).toBe(true); }); it('allows non-Read tool calls (no-op)', () => { const r = decide({ toolName: 'Bash', filePath: 'whatever' }); expect(r.block).toBe(false); }); }); // Over-block fix (2026-05-31): Smoke 5 added CLAUDE.md + memory/ + normative // docs to the Read-deny set, which broke the legit claude-md-management / // memory-sync workflow (Edit requires a prior Read). Read of CLAUDE.md / memory // / Pravila has no exfil value (public-in-repo / own memory index). The genuine // Read-exfil targets — cross-session transcripts (.jsonl) and ~/.claude/runtime // — MUST stay blocked. Bash/PowerShell/Write protections (DEFAULT_PROTECTED_PATTERNS) // are unchanged. describe('enforce-read-path-deny — CLAUDE.md / memory readable (over-block fix 2026-05-31)', () => { it('allows Read on CLAUDE.md (public-in-repo, no exfil value)', () => { expect(decide({ toolName: 'Read', filePath: 'CLAUDE.md' }).block).toBe(false); expect(decide({ toolName: 'Read', filePath: '/c/моя/проекты/портал crm/Документация/CLAUDE.md' }).block).toBe(false); }); it('allows Read on MEMORY.md (own memory index under .claude/projects//memory)', () => { expect(decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/crm/memory/MEMORY.md' }).block).toBe(false); }); it('allows Read on a memory/*.md feedback file', () => { expect(decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/crm/memory/feedback_read_path_deny.md' }).block).toBe(false); }); it('allows Read on a normative doc (Pravila) — needed for claude-md-management', () => { expect(decide({ toolName: 'Read', filePath: 'docs/Pravila_raboty_Claude_v1_1.md' }).block).toBe(false); }); it('STILL blocks Read on transcript JSONL under .claude/projects', () => { expect(decide({ toolName: 'Read', filePath: '/c/Users/Administrator/.claude/projects/crm/session.jsonl' }).block).toBe(true); expect(decide({ toolName: 'Read', filePath: '~/.claude/projects/abc-session.jsonl' }).block).toBe(true); }); it('STILL blocks Read on ~/.claude/runtime artifacts', () => { expect(decide({ toolName: 'Read', filePath: '~/.claude/runtime/router-state-x.json' }).block).toBe(true); }); }); // Impl completion (2026-05-31, this session): exfil-pattern boundaries. describe('enforce-read-path-deny — exfil-pattern boundaries (impl completion 2026-05-31)', () => { it('STILL blocks Read on .env.production (secrets variant)', () => { expect(decide({ toolName: 'Read', filePath: '.env.production' }).block).toBe(true); }); it('allows Read on a Tooling normative doc (needed for normative sync)', () => { expect(decide({ toolName: 'Read', filePath: 'docs/Tooling_v8_3.md' }).block).toBe(false); }); }); // F-1 (аудит 2026-06-07): контент-скан Read был МЁРТВ в проде — main() в // enforce-read-path-deny не передавал content, а это PreToolUse(Read)-хук (контента // до чтения нет). Ветка убрана: decide() гейтит ТОЛЬКО по пути. Реальный exfil // (исходящий payload) закрыт живым enforce-mcp-classification.scanEgress; чтение // секрета ≠ вынос. Тесты кодируют новый контракт «content игнорируется». describe('enforce-read-path-deny — F-1: decide() гейтит только по пути, content игнорируется', () => { it('Read обычного пути с секрет-контентом → allow (контент-скан убран; exfil ловит egress)', () => { const r = decide({ toolName: 'Read', filePath: 'app/config.php', content: 'KEY=AKIAIOSFODNN7EXAMPLE' }); expect(r.block).toBe(false); }); it('Read защищённого пути блокируется независимо от наличия content', () => { const r = decide({ toolName: 'Read', filePath: '.env', content: 'irrelevant' }); expect(r.block).toBe(true); }); }); import { canonicalAction } from './escape-grant.mjs'; describe('enforce-read-path-deny — escape-honor (M7 Фаза 2, правило 7в)', () => { const now = 1_000_000; it('защищённый путь БЕЗ escape → block (регресс)', () => { expect(decide({ toolName: 'Read', filePath: '.env' }).block).toBe(true); }); it('защищённый путь + матч escape-грант владельца → block:false', () => { const action = canonicalAction('Read', { file_path: '.env' }); const r = decide({ toolName: 'Read', filePath: '.env', escapeGrants: [{ action, ts: now - 1000 }], escapeConsumed: [], now }); expect(r.block).toBe(false); }); });