feat(audit): Add core audit logging infrastructure

- Add AuditLogger class with rotation support
- Add audit configuration to config schema
- Instrument tool execution with full audit logging
- Instrument session lifecycle (create, message, delete, transfer, compact)
- Add audit logger initialization in daemon
- Add cron scheduler audit logging

Audit events captured:
- tool.start/success/error/denied
- session.create/message/delete/transfer/compact
- cron.trigger/add/remove

All logs go to ~/.local/share/flynn/audit.log (JSON lines)
with rotation (10MB files, 30-day retention)
This commit is contained in:
William Valentin
2026-02-11 15:58:07 -08:00
parent fae3565480
commit d62e836b5d
12 changed files with 732 additions and 1 deletions
+50 -1
View File
@@ -2,6 +2,7 @@ import type { ToolResult } from './types.js';
import type { ToolRegistry } from './registry.js';
import type { HookEngine } from '../hooks/engine.js';
import type { ToolPolicyContext } from './policy.js';
import { auditLogger } from '../audit/index.js';
export interface ToolExecutorConfig {
defaultTimeoutMs?: number;
@@ -24,6 +25,12 @@ export class ToolExecutor {
async execute(toolName: string, args: unknown, context?: ToolPolicyContext): Promise<ToolResult> {
const tool = this.registry.getByApiName(toolName);
if (!tool) {
auditLogger?.toolDenied({
tool_name: toolName,
reason: 'Tool not found',
denial_type: 'not_found',
session_id: context?.sessionId,
});
return { success: false, output: '', error: `Tool '${toolName}' not found` };
}
@@ -32,6 +39,12 @@ export class ToolExecutor {
if (policy) {
const allNames = this.registry.list().map(t => t.name);
if (!policy.isAllowed(toolName, allNames, context)) {
auditLogger?.toolDenied({
tool_name: toolName,
reason: 'Tool not allowed by policy',
denial_type: 'policy',
session_id: context?.sessionId,
});
return {
success: false,
output: '',
@@ -48,6 +61,12 @@ export class ToolExecutor {
args as Record<string, unknown>,
);
if (!hookResult.approved) {
auditLogger?.toolDenied({
tool_name: toolName,
reason: hookResult.reason ?? 'no reason',
denial_type: 'hook',
session_id: context?.sessionId,
});
return {
success: false,
output: '',
@@ -57,6 +76,17 @@ export class ToolExecutor {
}
// Execute with timeout
const startTime = Date.now();
auditLogger?.toolStart({
tool_name: toolName,
tool_args: args,
session_id: context?.sessionId,
channel: context?.channel,
sender: context?.sender,
agent_tier: context?.tier,
});
try {
const result = await Promise.race([
tool.execute(args),
@@ -65,17 +95,36 @@ export class ToolExecutor {
),
]);
const duration = Date.now() - startTime;
// Truncate output if too large
if (result.output.length > this.maxOutputBytes) {
result.output = result.output.slice(0, this.maxOutputBytes) + '\n[truncated]';
}
auditLogger?.toolSuccess({
tool_name: toolName,
result: result,
duration_ms: duration,
session_id: context?.sessionId,
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
auditLogger?.toolError({
tool_name: toolName,
error: errorMessage,
duration_ms: duration,
session_id: context?.sessionId,
});
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
error: errorMessage,
};
}
}
+8
View File
@@ -132,6 +132,14 @@ export interface ToolPolicyContext {
agent?: string;
/** Provider name (e.g. 'ollama', 'anthropic'). */
provider?: string;
/** Session ID for audit logging. */
sessionId?: string;
/** Channel name for audit logging. */
channel?: string;
/** Sender ID for audit logging. */
sender?: string;
/** Model tier for audit logging. */
tier?: string;
}
// ── ToolPolicy engine ───────────────────────────────────────────────