import { createWriteStream, statSync, renameSync, type WriteStream } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import { join, dirname } from 'node:path'; import { homedir } from 'node:os'; import type { AuditEntry } from '../types.js'; const DEFAULT_AUDIT_DIR = join(homedir(), '.openclaw', 'security'); const DEFAULT_AUDIT_FILE = join(DEFAULT_AUDIT_DIR, 'audit.jsonl'); const MAX_SIZE = 10 * 1024 * 1024; // 10MB let auditStream: WriteStream | null = null; let currentPath: string = DEFAULT_AUDIT_FILE; let lastRotateCheck = 0; const ROTATE_CHECK_INTERVAL = 60_000; // check rotation every 60s /** * Resolve the audit log path. Supports env override via * OPENCLAW_SECURITY_LOG_DIR or falls back to ~/.openclaw/security/ */ function resolveLogPath(): string { const customDir = process.env.OPENCLAW_SECURITY_LOG_DIR; if (customDir) { return join(customDir, 'audit.jsonl'); } return DEFAULT_AUDIT_FILE; } function rotateIfNeeded(filePath: string): void { const now = Date.now(); if (now - lastRotateCheck < ROTATE_CHECK_INTERVAL) return; lastRotateCheck = now; try { const stat = statSync(filePath); if (stat.size > MAX_SIZE) { // Close current stream before rotating if (auditStream) { auditStream.end(); auditStream = null; } renameSync(filePath, filePath + '.1'); } } catch { // File doesn't exist yet or stat failed — that's fine } } function ensureAuditStream(): WriteStream { const logPath = resolveLogPath(); // Recreate stream if path changed or stream is destroyed if (auditStream && !auditStream.destroyed && logPath === currentPath) { return auditStream; } // Close old stream if path changed if (auditStream && !auditStream.destroyed) { auditStream.end(); } currentPath = logPath; void mkdir(dirname(logPath), { recursive: true }).catch(() => {}); rotateIfNeeded(logPath); auditStream = createWriteStream(logPath, { flags: 'a', encoding: 'utf-8', }); auditStream.on('error', (err) => { console.error('[security-guardrails:audit] WriteStream error:', err.message); auditStream = null; }); return auditStream; } export function writeAuditLog(entry: AuditEntry): void { try { const stream = ensureAuditStream(); const line = JSON.stringify({ ...entry, timestamp: entry.timestamp || new Date().toISOString(), }) + '\n'; stream.write(line); } catch (err) { console.error( '[security-guardrails:audit] Failed to write:', err instanceof Error ? err.message : String(err), ); } } // Flush on process exit process.on('beforeExit', () => { if (auditStream && !auditStream.destroyed) { auditStream.end(); auditStream = null; } });