Skill Tracer
Skills › gstack › browse › audit.ts

audit.ts

69 lines · browse/src/audit.ts at 3f79d81

1/**
2 * Persistent command audit log — forensic trail for all browse server commands.
3 *
4 * Writes append-only JSONL to .gstack/browse-audit.jsonl. Unlike the in-memory
5 * ring buffers (console, network, dialog), the audit log persists across server
6 * restarts and is never truncated by the server. Each entry records:
7 *
8 * - timestamp, command, args (truncated), page origin
9 * - duration, status (ok/error), error message if any
10 * - whether cookies were imported (elevated security context)
11 * - connection mode (headless/headed)
12 *
13 * All writes are best-effort — audit failures never cause command failures.
14 */
16import { appendSecureFile } from './file-permissions';
18export interface AuditEntry {
19 ts: string;
20 cmd: string;
21 /** If the agent typed an alias (e.g. 'setcontent'), the raw input is preserved here
22 * while `cmd` holds the canonical name ('load-html'). Omitted when cmd === rawCmd. */
23 aliasOf?: string;
24 args: string;
25 origin: string;
26 durationMs: number;
27 status: 'ok' | 'error';
28 error?: string;
29 hasCookies: boolean;
30 mode: 'launched' | 'headed';
33const MAX_ARGS_LENGTH = 200;
34const MAX_ERROR_LENGTH = 300;
36let auditPath: string | null = null;
38export function initAuditLog(logPath: string): void {
39 auditPath = logPath;
42export function writeAuditEntry(entry: AuditEntry): void {
43 if (!auditPath) return;
44 try {
45 const truncatedArgs = entry.args.length > MAX_ARGS_LENGTH
46 ? entry.args.slice(0, MAX_ARGS_LENGTH) + '…'
47 : entry.args;
48 const truncatedError = entry.error && entry.error.length > MAX_ERROR_LENGTH
49 ? entry.error.slice(0, MAX_ERROR_LENGTH) + '…'
50 : entry.error;
52 const record: Record<string, unknown> = {
53 ts: entry.ts,
54 cmd: entry.cmd,
55 args: truncatedArgs,
56 origin: entry.origin,
57 durationMs: entry.durationMs,
58 status: entry.status,
59 hasCookies: entry.hasCookies,
60 mode: entry.mode,
61 };
62 if (entry.aliasOf) record.aliasOf = entry.aliasOf;
63 if (truncatedError) record.error = truncatedError;
65 appendSecureFile(auditPath, JSON.stringify(record) + '\n');
66 } catch {
67 // Audit write failures are silent — never block command execution
68 }