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:
+50
-1
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user