dec0ed502a
Несомненный канал согласия для тяжёлого (Поза 1): floor_escape с origin:owner-terminal, подписанный ключом keychain. Скрипт владельца tools/owner-consent.mjs строит+подписывает+ пишет грант (запускает владелец; контроллер не может — нет ключа + floor режет запись runtime). Читатель loadTerminalGrants принимает ТОЛЬКО origin-помеченные валидно-подписанные гранты, ключ обязателен (fail-closed, #KEY). Нулевой радиус: живые потребители НЕ тронуты — подключение (owner-seal/ops-runbook/commit/plan-done/gate3/bash) отдельными заходами B2+. Спека: specs/2026-06-18-consent-forgery-fix-design.md §B/§KEY. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
42 lines
2.1 KiB
JavaScript
42 lines
2.1 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { buildTerminalGrant, writeTerminalGrant } from './owner-consent.mjs';
|
|
import { OWNER_TERMINAL_ORIGIN, loadTerminalGrants } from './escape-grant.mjs';
|
|
import { verifyFloorEscapeRecord } from './askuser-answer-parser.mjs';
|
|
|
|
const KEY = 'oc-key';
|
|
|
|
// in-memory fs (как escape-grant.test): пишем в Map, читаем обратно
|
|
function memFs() {
|
|
const s = new Map(); const norm = (p) => String(p).replace(/\\/g, '/');
|
|
return { s, norm,
|
|
appendFileSync: (p, d) => { const n = norm(p); s.set(n, (s.get(n) || '') + d); },
|
|
mkdirSync: () => {},
|
|
existsSync: (p) => s.has(norm(p)),
|
|
readFileSync: (p) => s.get(norm(p)) || '' };
|
|
}
|
|
|
|
describe('owner-consent — терминальный грант владельца (Часть B)', () => {
|
|
it('buildTerminalGrant ставит origin и тип', () => {
|
|
expect(buildTerminalGrant('owner-seal:abc', 7)).toEqual({
|
|
type: 'floor_escape', action: 'owner-seal:abc', origin: OWNER_TERMINAL_ORIGIN, ts: 7 });
|
|
});
|
|
|
|
it('writeTerminalGrant пишет подписанный грант, который читает loadTerminalGrants', () => {
|
|
const fs = memFs();
|
|
const r = writeTerminalGrant({ sessionId: 's1', action: 'owner-seal:abc', nowMs: 100, key: KEY, runtimeDir: '/rt', fsImpl: fs });
|
|
expect(r.signed).toBe(true);
|
|
const raw = JSON.parse((fs.s.get('/rt/askuser-decisions-s1.jsonl') || '').trim());
|
|
expect(raw.origin).toBe(OWNER_TERMINAL_ORIGIN);
|
|
expect(verifyFloorEscapeRecord(raw, KEY)).toBe(true);
|
|
expect(loadTerminalGrants('s1', 100, { keyImpl: () => KEY, fsImpl: fs, runtimeDir: '/rt' }))
|
|
.toEqual([{ action: 'owner-seal:abc', ts: 100 }]);
|
|
});
|
|
|
|
it('без ключа грант не подписан → loadTerminalGrants его отвергает', () => {
|
|
const fs = memFs();
|
|
const r = writeTerminalGrant({ sessionId: 's2', action: 'commit:xyz', nowMs: 100, key: null, runtimeDir: '/rt', fsImpl: fs });
|
|
expect(r.signed).toBe(false);
|
|
expect(loadTerminalGrants('s2', 100, { keyImpl: () => KEY, fsImpl: fs, runtimeDir: '/rt' })).toEqual([]);
|
|
});
|
|
});
|