Files
brain/tools/owner-consent.mjs
T
Дмитрий dec0ed502a feat: терминальный грант владельца — примитив (consent forgery B1)
Несомненный канал согласия для тяжёлого (Поза 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>
2026-06-18 18:02:28 +03:00

46 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
/**
* owner-consent — терминал ВЛАДЕЛЬЦА подписывает ТЕРМИНАЛЬНЫЙ грант согласия (Поза 1, Часть B).
* Запуск владельцем: node tools/owner-consent.mjs <sessionId> <action>
* Контроллер запустить с пользой НЕ может: его Bash не имеет доступа к keychain (ключ null →
* грант не подписан → loadTerminalGrants отвергает) и не пишет ~/.claude/runtime (floor).
* Канон <action> — как escape-grant::canonicalAction / ownerSealAction (owner-seal:<hash> и т.п.).
*/
import { appendFileSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { signFloorEscapeRecord } from './askuser-answer-parser.mjs';
import { resolveReceiptKey } from './receipt-key-config.mjs';
import { OWNER_TERMINAL_ORIGIN } from './escape-grant.mjs';
export function buildTerminalGrant(action, nowMs = Date.now()) {
return { type: 'floor_escape', action: String(action || ''), origin: OWNER_TERMINAL_ORIGIN, ts: nowMs };
}
export function writeTerminalGrant({ sessionId, action, nowMs = Date.now(), key, runtimeDir, fsImpl = { appendFileSync, mkdirSync } }) {
const rec = signFloorEscapeRecord(buildTerminalGrant(action, nowMs), key);
const dir = runtimeDir || join(homedir(), '.claude', 'runtime');
const path = join(dir, `askuser-decisions-${sessionId || 'unknown'}.jsonl`);
try { fsImpl.mkdirSync(dirname(path), { recursive: true }); } catch { /* ignore */ }
fsImpl.appendFileSync(path, JSON.stringify(rec) + '\n');
return { path, signed: !!(rec && rec.sig), action: rec.action };
}
function main() {
const argv = process.argv.slice(2);
const sessionId = argv[0];
const action = argv.slice(1).join(' ');
if (!sessionId || !action) {
console.error('usage: node tools/owner-consent.mjs <sessionId> <action>');
process.exit(2);
}
const key = resolveReceiptKey();
if (!key) console.error('[owner-consent] ВНИМАНИЕ: нет ключа keychain (router-mentor-receipts) — грант НЕ подписан, тяжёлый потребитель его отвергнет.');
const r = writeTerminalGrant({ sessionId, action, key });
console.log(`[owner-consent] ${r.signed ? 'signed' : 'UNSIGNED'} terminal grant: ${r.action} -> ${r.path}`);
}
import { fileURLToPath } from 'node:url';
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();