#!/usr/bin/env node /** * owner-consent — терминал ВЛАДЕЛЬЦА подписывает ТЕРМИНАЛЬНЫЙ грант согласия (Поза 1, Часть B). * Запуск владельцем: node tools/owner-consent.mjs * Контроллер запустить с пользой НЕ может: его Bash не имеет доступа к keychain (ключ null → * грант не подписан → loadTerminalGrants отвергает) и не пишет ~/.claude/runtime (floor). * Канон — как escape-grant::canonicalAction / ownerSealAction (owner-seal: и т.п.). */ 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 '); 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();