Wave-1 order 4 (P7, P12, the P9 test lock, the P3 plan-approval guard). All command hooks; none runs a model. - P12: pre-bash-executor answers permissionDecision "ask" before merge into main, push, tag/publish and destructive git (holds in auto mode). post-approval-log writes exactly what the person was asked to the K2 approval log: merge/push to main as `merge`, tag/publish as `release`, with a one-hour expiry; nothing when no person was asked. - P3: pre-plan-gate runs the K3 plan gate on a Voyage plan leaving plan mode and sends a refused plan back; "not measured" asks, never passes. - P9: lib/util/test-lock.mjs (K8). pre-write-executor refuses edits to locked test files; stop-test-lock refuses "done" for the locking session (stop_hook_active guarded). - P7: post-edit-check runs the target repo's own formatter/linter per edit, only during a Voyage plan run or after opt-in (.claude/voyage-guards.json); lib/verification/step-check.mjs (K9) fails the step on findings; no tool reads "not measured", never green. - STORM cap matcher also names `Agent` (the tool's name since 2.1.63). - Mod precondition stated in README, SECURITY.md and every PreToolUse stop guard: Voyage's guards are deterministic only when no installed mod handles `tool.check`. K2, K3 and K5 are stubbed in tests until orders 2 and 3 are merged. Two independent review rounds; every finding has a regression test. Suite 1600 (1598 pass / 0 fail / 2 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
2.1 KiB
JavaScript
60 lines
2.1 KiB
JavaScript
// hooks/lib/approval-record.mjs
|
|
// What the person was asked. pre-bash-executor.mjs writes one record per
|
|
// asked call, keyed by session_id + tool_use_id (both in PreToolUse and
|
|
// PostToolUse input); post-approval-log.mjs takes it after the call ran and
|
|
// logs exactly that -- never a re-reading of the command afterwards, which
|
|
// can see another branch than the one the question named. No record = the
|
|
// person was not asked = nothing is logged.
|
|
//
|
|
// Records live in the OS temp dir (voyage-approvals-<uid>/), are removed when taken,
|
|
// and anything older than a day is swept on the next write.
|
|
|
|
import { mkdirSync, writeFileSync, readFileSync, rmSync, readdirSync, statSync } from 'node:fs';
|
|
import { createHash } from 'node:crypto';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
// One directory per user: another user's directory in a shared /tmp is never ours.
|
|
const DIR = join(tmpdir(), `voyage-approvals-${process.getuid ? process.getuid() : 'u'}`);
|
|
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
|
|
function pathFor(sessionId, toolUseId) {
|
|
const key = createHash('sha256').update(`${sessionId || ''}\n${toolUseId}`).digest('hex').slice(0, 32);
|
|
return join(DIR, `${key}.json`);
|
|
}
|
|
|
|
function sweep() {
|
|
try {
|
|
const now = Date.now();
|
|
for (const f of readdirSync(DIR)) {
|
|
const p = join(DIR, f);
|
|
try { if (now - statSync(p).mtimeMs > MAX_AGE_MS) rmSync(p, { force: true }); } catch { /* raced */ }
|
|
}
|
|
} catch { /* no dir yet */ }
|
|
}
|
|
|
|
/** @returns {boolean} written */
|
|
export function writeApprovalRecord(sessionId, toolUseId, record) {
|
|
if (!toolUseId) return false;
|
|
try {
|
|
mkdirSync(DIR, { recursive: true });
|
|
sweep();
|
|
writeFileSync(pathFor(sessionId, toolUseId), JSON.stringify(record));
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Read and remove the record; null when there is none. */
|
|
export function takeApprovalRecord(sessionId, toolUseId) {
|
|
if (!toolUseId) return null;
|
|
const p = pathFor(sessionId, toolUseId);
|
|
try {
|
|
const rec = JSON.parse(readFileSync(p, 'utf8'));
|
|
rmSync(p, { force: true });
|
|
return rec;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|