Skill Tracer
Skills › gstack › autoplan › guard-log.ts

guard-log.ts

40 lines · autoplan/bin/guard-log.ts at 3f79d81

1/** One content-free line per guarded /autoplan decision, so denials have a denominator. */
2import * as fs from 'node:fs';
3import * as path from 'node:path';
4import { resolveStateRoot } from '../../lib/state-root';
5
6export const GUARD_LOG_SCHEMA = 1;
7export const GUARD_LOG_KEEP_LINES = 1_000;
8export const GUARD_LOG_TRIM_BYTES = 256 * 1024;
9
10export interface GuardLogEntry {
11 decision: 'allow' | 'deny';
12 /** allow, unverified, or the denial's fallback/corrective/transient class. */
13 disposition: 'allow' | 'unverified' | 'fallback' | 'corrective' | 'transient';
14 code?: string;
15 path: 'journal' | 'payload' | 'none';
16 claudeVersion?: string;
17 /** Names only (never values) of Agent keys outside the allowlist. */
18 unknownKeys?: string[];
19 recordTypes?: string[];
22export const guardLogPath = () => path.join(resolveStateRoot(), 'analytics', 'autoplan-guard.jsonl');
24/** Append-only; past the byte bound keep the newest lines via temp file + rename. A logging failure never changes a decision. */
25export function logGuardDecision(entry: GuardLogEntry, now = new Date()): void {
26 try {
27 const file = guardLogPath();
28 fs.mkdirSync(path.dirname(file), { recursive: true });
29 fs.appendFileSync(file, JSON.stringify({ schema: GUARD_LOG_SCHEMA, ts: now.toISOString(), decision: entry.decision,
30 disposition: entry.disposition, code: entry.code ?? null, path: entry.path, claude_code_version: entry.claudeVersion ?? null,
31 ...(entry.unknownKeys?.length ? { unknown_keys: entry.unknownKeys } : {}),
32 ...(entry.recordTypes?.length ? { record_types: entry.recordTypes } : {}) }) + '\n');
33 if (fs.statSync(file).size <= GUARD_LOG_TRIM_BYTES) return;
34 const kept = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-GUARD_LOG_KEEP_LINES);
35 const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
36 fs.writeFileSync(temp, kept.join('\n') + '\n');
37 // A concurrent trim may win; losing that race only keeps a few more lines.
38 try { fs.renameSync(temp, file); } catch { fs.rmSync(temp, { force: true }); }
39 } catch { /* A logging failure never changes the verdict. */ }