@agent-native/core 0.70.3 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +51 -0
- package/corpus/core/docs/content/actions.md +23 -0
- package/corpus/core/docs/content/audit-log.md +111 -0
- package/corpus/core/docs/content/multi-app-workspace.md +2 -2
- package/corpus/core/docs/design/durable-agent-runs.md +458 -4
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/action.ts +80 -1
- package/corpus/core/src/agent/durable-background.ts +192 -0
- package/corpus/core/src/agent/production-agent.ts +357 -13
- package/corpus/core/src/agent/run-manager.ts +66 -3
- package/corpus/core/src/agent/run-store.ts +143 -23
- package/corpus/core/src/agent/types.ts +20 -0
- package/corpus/core/src/application-state/store.ts +5 -0
- package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
- package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
- package/corpus/core/src/audit/cleanup-job.ts +100 -0
- package/corpus/core/src/audit/config.ts +91 -0
- package/corpus/core/src/audit/index.ts +43 -0
- package/corpus/core/src/audit/record.ts +143 -0
- package/corpus/core/src/audit/redact.ts +109 -0
- package/corpus/core/src/audit/store.ts +244 -0
- package/corpus/core/src/audit/types.ts +125 -0
- package/corpus/core/src/chat-threads/store.ts +10 -0
- package/corpus/core/src/client/AgentPanel.tsx +12 -9
- package/corpus/core/src/client/AssistantChat.tsx +6 -0
- package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
- package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
- package/corpus/core/src/db/client.ts +4 -0
- package/corpus/core/src/db/widen-columns.ts +75 -0
- package/corpus/core/src/deploy/build.ts +96 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
- package/corpus/core/src/mcp/build-server.ts +23 -13
- package/corpus/core/src/oauth-tokens/store.ts +6 -0
- package/corpus/core/src/provider-api/custom-registry.ts +8 -0
- package/corpus/core/src/resources/store.ts +10 -0
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/server/action-discovery.ts +6 -0
- package/corpus/core/src/server/action-routes.ts +1 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
- package/corpus/core/src/server/auth.ts +5 -0
- package/corpus/core/src/server/core-routes-plugin.ts +14 -0
- package/corpus/core/src/server/security-headers.ts +9 -6
- package/corpus/core/src/settings/store.ts +6 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/corpus/core/src/usage/store.ts +6 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
- package/corpus/templates/analytics/AGENTS.md +8 -0
- package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
- package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
- package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
- package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
- package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
- package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
- package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
- package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
- package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
- package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
- package/corpus/templates/clips/desktop/src/styles.css +33 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
- package/corpus/templates/clips/shared/recording-audio.ts +62 -0
- package/corpus/templates/clips/shared/recording-core.ts +94 -0
- package/dist/action.d.ts +31 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +45 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/context-xray/schema.d.ts +1 -1
- package/dist/agent/durable-background.d.ts +60 -0
- package/dist/agent/durable-background.d.ts.map +1 -0
- package/dist/agent/durable-background.js +144 -0
- package/dist/agent/durable-background.js.map +1 -0
- package/dist/agent/observational-memory/schema.d.ts +1 -1
- package/dist/agent/production-agent.d.ts +20 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +320 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +48 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +45 -3
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +30 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +138 -24
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +20 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +5 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/actions/get-audit-event.d.ts +11 -0
- package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
- package/dist/audit/actions/get-audit-event.js +22 -0
- package/dist/audit/actions/get-audit-event.js.map +1 -0
- package/dist/audit/actions/list-audit-events.d.ts +22 -0
- package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
- package/dist/audit/actions/list-audit-events.js +61 -0
- package/dist/audit/actions/list-audit-events.js.map +1 -0
- package/dist/audit/cleanup-job.d.ts +12 -0
- package/dist/audit/cleanup-job.d.ts.map +1 -0
- package/dist/audit/cleanup-job.js +93 -0
- package/dist/audit/cleanup-job.js.map +1 -0
- package/dist/audit/config.d.ts +30 -0
- package/dist/audit/config.d.ts.map +1 -0
- package/dist/audit/config.js +65 -0
- package/dist/audit/config.js.map +1 -0
- package/dist/audit/index.d.ts +13 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/record.d.ts +24 -0
- package/dist/audit/record.d.ts.map +1 -0
- package/dist/audit/record.js +106 -0
- package/dist/audit/record.js.map +1 -0
- package/dist/audit/redact.d.ts +28 -0
- package/dist/audit/redact.d.ts.map +1 -0
- package/dist/audit/redact.js +109 -0
- package/dist/audit/redact.js.map +1 -0
- package/dist/audit/store.d.ts +14 -0
- package/dist/audit/store.d.ts.map +1 -0
- package/dist/audit/store.js +219 -0
- package/dist/audit/store.js.map +1 -0
- package/dist/audit/types.d.ts +114 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +15 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +10 -0
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +37 -4
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/client/chat/repo-helpers.d.ts +16 -0
- package/dist/client/chat/repo-helpers.d.ts.map +1 -1
- package/dist/client/chat/repo-helpers.js +40 -0
- package/dist/client/chat/repo-helpers.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +3 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/widen-columns.d.ts +39 -0
- package/dist/db/widen-columns.d.ts.map +1 -0
- package/dist/db/widen-columns.js +73 -0
- package/dist/db/widen-columns.js.map +1 -0
- package/dist/deploy/build.d.ts +29 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +85 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +108 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +24 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +6 -0
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/provider-api/custom-registry.d.ts.map +1 -1
- package/dist/provider-api/custom-registry.js +8 -0
- package/dist/provider-api/custom-registry.js.map +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +9 -0
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.js +4 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +6 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +1 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +68 -13
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +13 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/security-headers.d.ts +8 -5
- package/dist/server/security-headers.d.ts.map +1 -1
- package/dist/server/security-headers.js +9 -6
- package/dist/server/security-headers.js.map +1 -1
- package/dist/settings/store.d.ts.map +1 -1
- package/dist/settings/store.js +6 -0
- package/dist/settings/store.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +5 -0
- package/dist/usage/store.js.map +1 -1
- package/docs/content/actions.md +23 -0
- package/docs/content/audit-log.md +111 -0
- package/docs/content/multi-app-workspace.md +2 -2
- package/docs/design/durable-agent-runs.md +458 -4
- package/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit capture entry point, called from the `defineAction` audit wrapper after
|
|
3
|
+
* an action runs (success or error). Best-effort: any failure here is swallowed
|
|
4
|
+
* so auditing never breaks the action it observes.
|
|
5
|
+
*
|
|
6
|
+
* This module touches the DB (`store.js`), so `action.ts` loads it lazily via
|
|
7
|
+
* dynamic import on the first audited call — keeping the DB client out of every
|
|
8
|
+
* bundle that merely defines actions.
|
|
9
|
+
*/
|
|
10
|
+
import { deriveActorKind, isAuditDisabled, shouldRecordAudit, } from "./config.js";
|
|
11
|
+
import { redactArgsToJson } from "./redact.js";
|
|
12
|
+
import { insertAuditEvent } from "./store.js";
|
|
13
|
+
function errorCode(error) {
|
|
14
|
+
if (!error)
|
|
15
|
+
return null;
|
|
16
|
+
if (typeof error === "object") {
|
|
17
|
+
const e = error;
|
|
18
|
+
if (typeof e.errorCode === "string")
|
|
19
|
+
return e.errorCode;
|
|
20
|
+
if (typeof e.code === "string")
|
|
21
|
+
return e.code;
|
|
22
|
+
if (typeof e.name === "string")
|
|
23
|
+
return e.name;
|
|
24
|
+
}
|
|
25
|
+
return "error";
|
|
26
|
+
}
|
|
27
|
+
function safeTarget(config, args, result, meta) {
|
|
28
|
+
if (!config?.target)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
return config.target(args, result, meta) ?? null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function safeSummary(config, args, result, meta) {
|
|
38
|
+
if (!config?.summary)
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const s = config.summary(args, result, meta);
|
|
42
|
+
return typeof s === "string" ? s.slice(0, 500) : null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Record one audit event. Resolves the actor, target, ownership (for scoped
|
|
50
|
+
* reads), and redacted inputs, then appends a row. Never throws.
|
|
51
|
+
*/
|
|
52
|
+
export async function recordActionAudit(input) {
|
|
53
|
+
try {
|
|
54
|
+
if (isAuditDisabled())
|
|
55
|
+
return;
|
|
56
|
+
const ctx = input.ctx;
|
|
57
|
+
const actionName = ctx?.actionName;
|
|
58
|
+
// No name → an internal/programmatic run() with no dispatch context. Skip
|
|
59
|
+
// rather than write a nameless row.
|
|
60
|
+
if (!actionName)
|
|
61
|
+
return;
|
|
62
|
+
if (!shouldRecordAudit(input.config, actionName))
|
|
63
|
+
return;
|
|
64
|
+
const caller = ctx?.caller ?? "http";
|
|
65
|
+
const actorEmail = ctx?.userEmail ?? null;
|
|
66
|
+
const meta = {
|
|
67
|
+
status: input.status,
|
|
68
|
+
caller,
|
|
69
|
+
userEmail: ctx?.userEmail,
|
|
70
|
+
orgId: ctx?.orgId ?? null,
|
|
71
|
+
};
|
|
72
|
+
const target = safeTarget(input.config, input.args, input.result, meta);
|
|
73
|
+
const summary = safeSummary(input.config, input.args, input.result, meta);
|
|
74
|
+
const recordInputs = input.config?.recordInputs !== false;
|
|
75
|
+
const inputJson = recordInputs ? redactArgsToJson(input.args) : null;
|
|
76
|
+
const event = {
|
|
77
|
+
id: crypto.randomUUID(),
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
action: actionName,
|
|
80
|
+
caller,
|
|
81
|
+
actorKind: deriveActorKind(caller, actorEmail),
|
|
82
|
+
actorEmail,
|
|
83
|
+
orgId: ctx?.orgId ?? null,
|
|
84
|
+
threadId: ctx?.threadId ?? null,
|
|
85
|
+
turnId: ctx?.turnId ?? null,
|
|
86
|
+
targetType: target?.type ?? null,
|
|
87
|
+
targetId: target?.id ?? null,
|
|
88
|
+
status: input.status,
|
|
89
|
+
summary,
|
|
90
|
+
input: inputJson,
|
|
91
|
+
errorCode: input.status === "error" ? errorCode(input.error) : null,
|
|
92
|
+
// Scope reads to the resource owner when the action declares one,
|
|
93
|
+
// otherwise to the actor (the common self-mutation case).
|
|
94
|
+
ownerEmail: target?.ownerEmail ?? actorEmail,
|
|
95
|
+
visibility: target?.visibility ?? "private",
|
|
96
|
+
};
|
|
97
|
+
// org_id used for scoping defaults to the target's, else the actor's org.
|
|
98
|
+
if (target?.orgId !== undefined)
|
|
99
|
+
event.orgId = target.orgId;
|
|
100
|
+
await insertAuditEvent(event);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Best-effort — auditing must never break the audited action.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/audit/record.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Audit capture entry point, called from the `defineAction` audit wrapper after\n * an action runs (success or error). Best-effort: any failure here is swallowed\n * so auditing never breaks the action it observes.\n *\n * This module touches the DB (`store.js`), so `action.ts` loads it lazily via\n * dynamic import on the first audited call — keeping the DB client out of every\n * bundle that merely defines actions.\n */\nimport {\n deriveActorKind,\n isAuditDisabled,\n shouldRecordAudit,\n} from \"./config.js\";\nimport { redactArgsToJson } from \"./redact.js\";\nimport { insertAuditEvent } from \"./store.js\";\nimport type {\n ActionAuditConfig,\n AuditCallMeta,\n AuditEvent,\n AuditStatus,\n AuditTarget,\n} from \"./types.js\";\n\n/** Minimal view of the action run context the recorder needs. */\nexport interface AuditRunContextLike {\n actionName?: string;\n caller?: string;\n userEmail?: string;\n orgId?: string | null;\n threadId?: string;\n turnId?: string;\n}\n\nexport interface RecordActionAuditInput {\n config: ActionAuditConfig | undefined;\n args: unknown;\n ctx: AuditRunContextLike | undefined;\n status: AuditStatus;\n result?: unknown;\n error?: unknown;\n}\n\nfunction errorCode(error: unknown): string | null {\n if (!error) return null;\n if (typeof error === \"object\") {\n const e = error as { errorCode?: unknown; code?: unknown; name?: unknown };\n if (typeof e.errorCode === \"string\") return e.errorCode;\n if (typeof e.code === \"string\") return e.code;\n if (typeof e.name === \"string\") return e.name;\n }\n return \"error\";\n}\n\nfunction safeTarget(\n config: ActionAuditConfig | undefined,\n args: unknown,\n result: unknown,\n meta: AuditCallMeta,\n): AuditTarget | null {\n if (!config?.target) return null;\n try {\n return config.target(args, result, meta) ?? null;\n } catch {\n return null;\n }\n}\n\nfunction safeSummary(\n config: ActionAuditConfig | undefined,\n args: unknown,\n result: unknown,\n meta: AuditCallMeta,\n): string | null {\n if (!config?.summary) return null;\n try {\n const s = config.summary(args, result, meta);\n return typeof s === \"string\" ? s.slice(0, 500) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Record one audit event. Resolves the actor, target, ownership (for scoped\n * reads), and redacted inputs, then appends a row. Never throws.\n */\nexport async function recordActionAudit(\n input: RecordActionAuditInput,\n): Promise<void> {\n try {\n if (isAuditDisabled()) return;\n const ctx = input.ctx;\n const actionName = ctx?.actionName;\n // No name → an internal/programmatic run() with no dispatch context. Skip\n // rather than write a nameless row.\n if (!actionName) return;\n if (!shouldRecordAudit(input.config, actionName)) return;\n\n const caller = ctx?.caller ?? \"http\";\n const actorEmail = ctx?.userEmail ?? null;\n const meta: AuditCallMeta = {\n status: input.status,\n caller,\n userEmail: ctx?.userEmail,\n orgId: ctx?.orgId ?? null,\n };\n\n const target = safeTarget(input.config, input.args, input.result, meta);\n const summary = safeSummary(input.config, input.args, input.result, meta);\n\n const recordInputs = input.config?.recordInputs !== false;\n const inputJson = recordInputs ? redactArgsToJson(input.args) : null;\n\n const event: AuditEvent = {\n id: crypto.randomUUID(),\n createdAt: Date.now(),\n action: actionName,\n caller,\n actorKind: deriveActorKind(caller, actorEmail),\n actorEmail,\n orgId: ctx?.orgId ?? null,\n threadId: ctx?.threadId ?? null,\n turnId: ctx?.turnId ?? null,\n targetType: target?.type ?? null,\n targetId: target?.id ?? null,\n status: input.status,\n summary,\n input: inputJson,\n errorCode: input.status === \"error\" ? errorCode(input.error) : null,\n // Scope reads to the resource owner when the action declares one,\n // otherwise to the actor (the common self-mutation case).\n ownerEmail: target?.ownerEmail ?? actorEmail,\n visibility: target?.visibility ?? \"private\",\n };\n // org_id used for scoping defaults to the target's, else the actor's org.\n if (target?.orgId !== undefined) event.orgId = target.orgId;\n\n await insertAuditEvent(event);\n } catch {\n // Best-effort — auditing must never break the audited action.\n }\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction for audit-captured arguments.
|
|
3
|
+
*
|
|
4
|
+
* The audit log must never become a secondary store of secrets. Before any
|
|
5
|
+
* call arguments are persisted we:
|
|
6
|
+
* - drop values under credential-looking keys (token, secret, password, …),
|
|
7
|
+
* - redact string values that look like bearer tokens / long opaque keys,
|
|
8
|
+
* - truncate oversized strings and cap the serialized payload size.
|
|
9
|
+
*
|
|
10
|
+
* This mirrors the framework's standing rule that credential-looking literals
|
|
11
|
+
* never land in source, logs, or fixtures.
|
|
12
|
+
*/
|
|
13
|
+
/** Heuristic: does a bare string value look like a secret? */
|
|
14
|
+
declare function looksSecret(value: string): boolean;
|
|
15
|
+
declare function redact(value: unknown, depth: number): unknown;
|
|
16
|
+
/**
|
|
17
|
+
* Redact and serialize call arguments to a capped JSON string, or `null` when
|
|
18
|
+
* there is nothing to record. Never throws.
|
|
19
|
+
*/
|
|
20
|
+
export declare function redactArgsToJson(args: unknown): string | null;
|
|
21
|
+
/** Exposed for tests. */
|
|
22
|
+
export declare const __test: {
|
|
23
|
+
looksSecret: typeof looksSecret;
|
|
24
|
+
redact: typeof redact;
|
|
25
|
+
SENSITIVE_KEY: RegExp;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/audit/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAYH,8DAA8D;AAC9D,iBAAS,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAkB3C;AAUD,iBAAS,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CA0BtD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAqB7D;AAED,yBAAyB;AACzB,eAAO,MAAM,MAAM;;;;CAAyC,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction for audit-captured arguments.
|
|
3
|
+
*
|
|
4
|
+
* The audit log must never become a secondary store of secrets. Before any
|
|
5
|
+
* call arguments are persisted we:
|
|
6
|
+
* - drop values under credential-looking keys (token, secret, password, …),
|
|
7
|
+
* - redact string values that look like bearer tokens / long opaque keys,
|
|
8
|
+
* - truncate oversized strings and cap the serialized payload size.
|
|
9
|
+
*
|
|
10
|
+
* This mirrors the framework's standing rule that credential-looking literals
|
|
11
|
+
* never land in source, logs, or fixtures.
|
|
12
|
+
*/
|
|
13
|
+
const SENSITIVE_KEY = /(pass(word|phrase)?|secret|token|api[_-]?key|apikey|authorization|bearer|credential|cookie|session[_-]?(id|token)|private[_-]?key|client[_-]?secret|signing[_-]?secret|access[_-]?key|refresh[_-]?token|webhook[_-]?(url|secret))/i;
|
|
14
|
+
const REDACTED = "[redacted]";
|
|
15
|
+
const MAX_STRING = 2000;
|
|
16
|
+
const MAX_DEPTH = 6;
|
|
17
|
+
const MAX_KEYS = 100;
|
|
18
|
+
const MAX_ARRAY = 100;
|
|
19
|
+
const MAX_JSON = 8000;
|
|
20
|
+
/** Heuristic: does a bare string value look like a secret? */
|
|
21
|
+
function looksSecret(value) {
|
|
22
|
+
if (/^bearer\s+\S/i.test(value))
|
|
23
|
+
return true;
|
|
24
|
+
// Long, unbroken, high-entropy-ish opaque token (hex/base64url, no spaces).
|
|
25
|
+
if (value.length >= 32 && /^[A-Za-z0-9_\-+/=.]+$/.test(value))
|
|
26
|
+
return true;
|
|
27
|
+
// Common secret prefixes (Stripe, GitHub, OpenAI, Slack, AWS, …).
|
|
28
|
+
if (/^(sk|pk|rk|ghp|gho|xox[baprs]|AKIA|AIza|ya29)[-_]/i.test(value)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
// Webhook URLs carry their secret in the path — redact regardless of the key
|
|
32
|
+
// they arrive under (e.g. a generic `value` field holding a Slack webhook).
|
|
33
|
+
if (/^https?:\/\/(hooks\.slack\.com\/|[^/]*\.webhook\.office\.com\/|(canary\.|ptb\.)?discord(app)?\.com\/api\/webhooks\/|hooks\.zapier\.com\/|maker\.ifttt\.com\/|discord\.com\/api\/webhooks\/)/i.test(value)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function redactString(value) {
|
|
39
|
+
if (looksSecret(value))
|
|
40
|
+
return REDACTED;
|
|
41
|
+
if (value.length > MAX_STRING) {
|
|
42
|
+
return `${value.slice(0, MAX_STRING)}…(${value.length - MAX_STRING} more chars)`;
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function redact(value, depth) {
|
|
47
|
+
if (value == null)
|
|
48
|
+
return value;
|
|
49
|
+
if (typeof value === "string")
|
|
50
|
+
return redactString(value);
|
|
51
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
52
|
+
return value;
|
|
53
|
+
if (depth >= MAX_DEPTH)
|
|
54
|
+
return "[…]";
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
const out = value.slice(0, MAX_ARRAY).map((v) => redact(v, depth + 1));
|
|
57
|
+
if (value.length > MAX_ARRAY)
|
|
58
|
+
out.push(`…(${value.length - MAX_ARRAY} more)`);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === "object") {
|
|
62
|
+
const out = {};
|
|
63
|
+
let n = 0;
|
|
64
|
+
for (const [k, v] of Object.entries(value)) {
|
|
65
|
+
if (n >= MAX_KEYS) {
|
|
66
|
+
out["…"] = "(truncated)";
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
n += 1;
|
|
70
|
+
out[k] = SENSITIVE_KEY.test(k) ? REDACTED : redact(v, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
// Functions, symbols, bigint, etc. — not serializable / not interesting.
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Redact and serialize call arguments to a capped JSON string, or `null` when
|
|
79
|
+
* there is nothing to record. Never throws.
|
|
80
|
+
*/
|
|
81
|
+
export function redactArgsToJson(args) {
|
|
82
|
+
try {
|
|
83
|
+
if (args == null)
|
|
84
|
+
return null;
|
|
85
|
+
const redacted = redact(args, 0);
|
|
86
|
+
if (redacted === undefined)
|
|
87
|
+
return null;
|
|
88
|
+
const json = JSON.stringify(redacted);
|
|
89
|
+
if (json == null)
|
|
90
|
+
return null;
|
|
91
|
+
if (json.length > MAX_JSON) {
|
|
92
|
+
// Slicing the serialized JSON would yield an unparseable string. Wrap a
|
|
93
|
+
// preview in a valid envelope so `get-audit-event` can always JSON.parse
|
|
94
|
+
// the stored `input`.
|
|
95
|
+
return JSON.stringify({
|
|
96
|
+
_auditTruncated: true,
|
|
97
|
+
originalBytes: json.length,
|
|
98
|
+
preview: json.slice(0, MAX_JSON),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return json;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Exposed for tests. */
|
|
108
|
+
export const __test = { looksSecret, redact, SENSITIVE_KEY };
|
|
109
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/audit/redact.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Redaction for audit-captured arguments.\n *\n * The audit log must never become a secondary store of secrets. Before any\n * call arguments are persisted we:\n * - drop values under credential-looking keys (token, secret, password, …),\n * - redact string values that look like bearer tokens / long opaque keys,\n * - truncate oversized strings and cap the serialized payload size.\n *\n * This mirrors the framework's standing rule that credential-looking literals\n * never land in source, logs, or fixtures.\n */\n\nconst SENSITIVE_KEY =\n /(pass(word|phrase)?|secret|token|api[_-]?key|apikey|authorization|bearer|credential|cookie|session[_-]?(id|token)|private[_-]?key|client[_-]?secret|signing[_-]?secret|access[_-]?key|refresh[_-]?token|webhook[_-]?(url|secret))/i;\n\nconst REDACTED = \"[redacted]\";\nconst MAX_STRING = 2000;\nconst MAX_DEPTH = 6;\nconst MAX_KEYS = 100;\nconst MAX_ARRAY = 100;\nconst MAX_JSON = 8000;\n\n/** Heuristic: does a bare string value look like a secret? */\nfunction looksSecret(value: string): boolean {\n if (/^bearer\\s+\\S/i.test(value)) return true;\n // Long, unbroken, high-entropy-ish opaque token (hex/base64url, no spaces).\n if (value.length >= 32 && /^[A-Za-z0-9_\\-+/=.]+$/.test(value)) return true;\n // Common secret prefixes (Stripe, GitHub, OpenAI, Slack, AWS, …).\n if (/^(sk|pk|rk|ghp|gho|xox[baprs]|AKIA|AIza|ya29)[-_]/i.test(value)) {\n return true;\n }\n // Webhook URLs carry their secret in the path — redact regardless of the key\n // they arrive under (e.g. a generic `value` field holding a Slack webhook).\n if (\n /^https?:\\/\\/(hooks\\.slack\\.com\\/|[^/]*\\.webhook\\.office\\.com\\/|(canary\\.|ptb\\.)?discord(app)?\\.com\\/api\\/webhooks\\/|hooks\\.zapier\\.com\\/|maker\\.ifttt\\.com\\/|discord\\.com\\/api\\/webhooks\\/)/i.test(\n value,\n )\n ) {\n return true;\n }\n return false;\n}\n\nfunction redactString(value: string): string {\n if (looksSecret(value)) return REDACTED;\n if (value.length > MAX_STRING) {\n return `${value.slice(0, MAX_STRING)}…(${value.length - MAX_STRING} more chars)`;\n }\n return value;\n}\n\nfunction redact(value: unknown, depth: number): unknown {\n if (value == null) return value;\n if (typeof value === \"string\") return redactString(value);\n if (typeof value === \"number\" || typeof value === \"boolean\") return value;\n if (depth >= MAX_DEPTH) return \"[…]\";\n if (Array.isArray(value)) {\n const out = value.slice(0, MAX_ARRAY).map((v) => redact(v, depth + 1));\n if (value.length > MAX_ARRAY)\n out.push(`…(${value.length - MAX_ARRAY} more)`);\n return out;\n }\n if (typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n let n = 0;\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (n >= MAX_KEYS) {\n out[\"…\"] = \"(truncated)\";\n break;\n }\n n += 1;\n out[k] = SENSITIVE_KEY.test(k) ? REDACTED : redact(v, depth + 1);\n }\n return out;\n }\n // Functions, symbols, bigint, etc. — not serializable / not interesting.\n return undefined;\n}\n\n/**\n * Redact and serialize call arguments to a capped JSON string, or `null` when\n * there is nothing to record. Never throws.\n */\nexport function redactArgsToJson(args: unknown): string | null {\n try {\n if (args == null) return null;\n const redacted = redact(args, 0);\n if (redacted === undefined) return null;\n const json = JSON.stringify(redacted);\n if (json == null) return null;\n if (json.length > MAX_JSON) {\n // Slicing the serialized JSON would yield an unparseable string. Wrap a\n // preview in a valid envelope so `get-audit-event` can always JSON.parse\n // the stored `input`.\n return JSON.stringify({\n _auditTruncated: true,\n originalBytes: json.length,\n preview: json.slice(0, MAX_JSON),\n });\n }\n return json;\n } catch {\n return null;\n }\n}\n\n/** Exposed for tests. */\nexport const __test = { looksSecret, redact, SENSITIVE_KEY };\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuditEvent, AuditQueryFilters } from "./types.js";
|
|
2
|
+
export declare function ensureAuditTables(): Promise<void>;
|
|
3
|
+
export declare function insertAuditEvent(event: AuditEvent): Promise<void>;
|
|
4
|
+
export interface AuditReadScope {
|
|
5
|
+
userEmail?: string;
|
|
6
|
+
orgId?: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function queryAuditEvents(scope: AuditReadScope, filters?: AuditQueryFilters): Promise<AuditEvent[]>;
|
|
9
|
+
export declare function getAuditEventById(id: string, scope: AuditReadScope): Promise<AuditEvent | null>;
|
|
10
|
+
/** Purge audit rows older than `cutoffMs`. Returns the deleted row count. */
|
|
11
|
+
export declare function deleteOldAuditEvents(cutoffMs: number): Promise<number>;
|
|
12
|
+
/** Test-only: reset the cached init promise so a fresh DB re-creates tables. */
|
|
13
|
+
export declare function __resetAuditInitForTests(): void;
|
|
14
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/audit/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAIpB,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAmDvD;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BvE;AAwBD,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AA2CD,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,cAAc,EACrB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,UAAU,EAAE,CAAC,CAsCvB;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAW5B;AAED,6EAA6E;AAC7E,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ5E;AAED,gFAAgF;AAChF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL persistence for the framework audit log.
|
|
3
|
+
*
|
|
4
|
+
* Follows the same raw-SQL, provider-agnostic pattern as observability/store.ts
|
|
5
|
+
* and usage/store.ts — framework tables use `getDbExec()` + `intType()` rather
|
|
6
|
+
* than Drizzle ORM (which is for template-level schemas). One append-only table
|
|
7
|
+
* `agent_audit_log`; reads are scoped to the caller's identity in SQL (no
|
|
8
|
+
* shares table — audit rows are never individually shared).
|
|
9
|
+
*/
|
|
10
|
+
import { getDbExec, intType, retryOnDdlRace } from "../db/client.js";
|
|
11
|
+
let _initPromise;
|
|
12
|
+
export async function ensureAuditTables() {
|
|
13
|
+
if (!_initPromise) {
|
|
14
|
+
_initPromise = (async () => {
|
|
15
|
+
const client = getDbExec();
|
|
16
|
+
await retryOnDdlRace(() => client.execute(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS agent_audit_log (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
created_at ${intType()} NOT NULL,
|
|
20
|
+
action TEXT NOT NULL,
|
|
21
|
+
caller TEXT NOT NULL,
|
|
22
|
+
actor_kind TEXT NOT NULL,
|
|
23
|
+
actor_email TEXT,
|
|
24
|
+
org_id TEXT,
|
|
25
|
+
thread_id TEXT,
|
|
26
|
+
turn_id TEXT,
|
|
27
|
+
target_type TEXT,
|
|
28
|
+
target_id TEXT,
|
|
29
|
+
status TEXT NOT NULL DEFAULT 'success',
|
|
30
|
+
summary TEXT,
|
|
31
|
+
input TEXT,
|
|
32
|
+
error_code TEXT,
|
|
33
|
+
owner_email TEXT,
|
|
34
|
+
visibility TEXT NOT NULL DEFAULT 'private'
|
|
35
|
+
)
|
|
36
|
+
`));
|
|
37
|
+
const indexes = [
|
|
38
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_owner ON agent_audit_log (owner_email, created_at)`,
|
|
39
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_org ON agent_audit_log (org_id, created_at)`,
|
|
40
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_target ON agent_audit_log (target_type, target_id, created_at)`,
|
|
41
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_turn ON agent_audit_log (turn_id)`,
|
|
42
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_actor ON agent_audit_log (actor_email, created_at)`,
|
|
43
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_created ON agent_audit_log (created_at)`,
|
|
44
|
+
];
|
|
45
|
+
for (const sql of indexes) {
|
|
46
|
+
try {
|
|
47
|
+
await client.execute(sql);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Index creation is best-effort; a racing boot may have created it.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})().catch((err) => {
|
|
54
|
+
// Allow a later call to retry if the first init failed.
|
|
55
|
+
_initPromise = undefined;
|
|
56
|
+
throw err;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return _initPromise;
|
|
60
|
+
}
|
|
61
|
+
export async function insertAuditEvent(event) {
|
|
62
|
+
await ensureAuditTables();
|
|
63
|
+
const client = getDbExec();
|
|
64
|
+
await client.execute({
|
|
65
|
+
sql: `INSERT INTO agent_audit_log
|
|
66
|
+
(id, created_at, action, caller, actor_kind, actor_email, org_id,
|
|
67
|
+
thread_id, turn_id, target_type, target_id, status, summary, input,
|
|
68
|
+
error_code, owner_email, visibility)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
70
|
+
args: [
|
|
71
|
+
event.id,
|
|
72
|
+
event.createdAt,
|
|
73
|
+
event.action,
|
|
74
|
+
event.caller,
|
|
75
|
+
event.actorKind,
|
|
76
|
+
event.actorEmail,
|
|
77
|
+
event.orgId,
|
|
78
|
+
event.threadId,
|
|
79
|
+
event.turnId,
|
|
80
|
+
event.targetType,
|
|
81
|
+
event.targetId,
|
|
82
|
+
event.status,
|
|
83
|
+
event.summary,
|
|
84
|
+
event.input,
|
|
85
|
+
event.errorCode,
|
|
86
|
+
event.ownerEmail,
|
|
87
|
+
event.visibility,
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function mapRow(row) {
|
|
92
|
+
return {
|
|
93
|
+
id: String(row.id),
|
|
94
|
+
createdAt: Number(row.created_at),
|
|
95
|
+
action: String(row.action),
|
|
96
|
+
caller: String(row.caller),
|
|
97
|
+
actorKind: row.actor_kind,
|
|
98
|
+
actorEmail: row.actor_email ?? null,
|
|
99
|
+
orgId: row.org_id ?? null,
|
|
100
|
+
threadId: row.thread_id ?? null,
|
|
101
|
+
turnId: row.turn_id ?? null,
|
|
102
|
+
targetType: row.target_type ?? null,
|
|
103
|
+
targetId: row.target_id ?? null,
|
|
104
|
+
status: row.status,
|
|
105
|
+
summary: row.summary ?? null,
|
|
106
|
+
input: row.input ?? null,
|
|
107
|
+
errorCode: row.error_code ?? null,
|
|
108
|
+
ownerEmail: row.owner_email ?? null,
|
|
109
|
+
visibility: (row.visibility ?? "private"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build the access-scoping WHERE fragment + args. A caller sees audit rows they
|
|
114
|
+
* own, plus org-visible rows in their org. With no identity, nothing matches —
|
|
115
|
+
* the audit log never leaks cross-tenant. Mirrors the core ownership clause of
|
|
116
|
+
* `accessFilter` (minus shares, which audit rows don't have).
|
|
117
|
+
*/
|
|
118
|
+
function scopeClause(scope) {
|
|
119
|
+
const clauses = [];
|
|
120
|
+
const args = [];
|
|
121
|
+
if (scope.userEmail) {
|
|
122
|
+
if (scope.orgId) {
|
|
123
|
+
// Constrain the owner's rows to the active org — plus legacy/solo rows
|
|
124
|
+
// that predate org-scoping (org_id IS NULL) — mirroring sharing's
|
|
125
|
+
// `ownerScopeFilter`, so switching orgs doesn't surface another org's
|
|
126
|
+
// trail.
|
|
127
|
+
clauses.push("(owner_email = ? AND (org_id = ? OR org_id IS NULL))");
|
|
128
|
+
args.push(scope.userEmail, scope.orgId);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
clauses.push("owner_email = ?");
|
|
132
|
+
args.push(scope.userEmail);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (scope.orgId) {
|
|
136
|
+
clauses.push("(visibility = 'org' AND org_id = ?)");
|
|
137
|
+
args.push(scope.orgId);
|
|
138
|
+
}
|
|
139
|
+
if (clauses.length === 0)
|
|
140
|
+
return { sql: "1=0", args };
|
|
141
|
+
return { sql: `(${clauses.join(" OR ")})`, args };
|
|
142
|
+
}
|
|
143
|
+
const MAX_LIMIT = 500;
|
|
144
|
+
const DEFAULT_LIMIT = 100;
|
|
145
|
+
// Columns returned by the list surface — deliberately EXCLUDES `input` so a
|
|
146
|
+
// timeline query never streams every event's (redacted) request body in bulk.
|
|
147
|
+
// Fetch the full payload one event at a time via `getAuditEventById`.
|
|
148
|
+
const LIST_COLUMNS = "id, created_at, action, caller, actor_kind, actor_email, org_id, " +
|
|
149
|
+
"thread_id, turn_id, target_type, target_id, status, summary, " +
|
|
150
|
+
"error_code, owner_email, visibility";
|
|
151
|
+
export async function queryAuditEvents(scope, filters = {}) {
|
|
152
|
+
await ensureAuditTables();
|
|
153
|
+
if (!scope.userEmail && !scope.orgId)
|
|
154
|
+
return [];
|
|
155
|
+
const client = getDbExec();
|
|
156
|
+
const scoped = scopeClause(scope);
|
|
157
|
+
const where = [scoped.sql];
|
|
158
|
+
const args = [...scoped.args];
|
|
159
|
+
const push = (clause, value) => {
|
|
160
|
+
where.push(clause);
|
|
161
|
+
args.push(value);
|
|
162
|
+
};
|
|
163
|
+
if (filters.targetType)
|
|
164
|
+
push("target_type = ?", filters.targetType);
|
|
165
|
+
if (filters.targetId)
|
|
166
|
+
push("target_id = ?", filters.targetId);
|
|
167
|
+
if (filters.actorKind)
|
|
168
|
+
push("actor_kind = ?", filters.actorKind);
|
|
169
|
+
if (filters.actorEmail)
|
|
170
|
+
push("actor_email = ?", filters.actorEmail);
|
|
171
|
+
if (filters.status)
|
|
172
|
+
push("status = ?", filters.status);
|
|
173
|
+
if (filters.threadId)
|
|
174
|
+
push("thread_id = ?", filters.threadId);
|
|
175
|
+
if (filters.turnId)
|
|
176
|
+
push("turn_id = ?", filters.turnId);
|
|
177
|
+
if (filters.action)
|
|
178
|
+
push("action = ?", filters.action);
|
|
179
|
+
if (typeof filters.sinceMs === "number") {
|
|
180
|
+
push("created_at >= ?", Math.floor(filters.sinceMs));
|
|
181
|
+
}
|
|
182
|
+
const limit = Math.min(Math.max(1, Math.floor(filters.limit ?? DEFAULT_LIMIT)), MAX_LIMIT);
|
|
183
|
+
const result = await client.execute({
|
|
184
|
+
sql: `SELECT ${LIST_COLUMNS} FROM agent_audit_log
|
|
185
|
+
WHERE ${where.join(" AND ")}
|
|
186
|
+
ORDER BY created_at DESC
|
|
187
|
+
LIMIT ?`,
|
|
188
|
+
args: [...args, limit],
|
|
189
|
+
});
|
|
190
|
+
return (result.rows ?? []).map(mapRow);
|
|
191
|
+
}
|
|
192
|
+
export async function getAuditEventById(id, scope) {
|
|
193
|
+
await ensureAuditTables();
|
|
194
|
+
if (!scope.userEmail && !scope.orgId)
|
|
195
|
+
return null;
|
|
196
|
+
const client = getDbExec();
|
|
197
|
+
const scoped = scopeClause(scope);
|
|
198
|
+
const result = await client.execute({
|
|
199
|
+
sql: `SELECT * FROM agent_audit_log WHERE id = ? AND ${scoped.sql} LIMIT 1`,
|
|
200
|
+
args: [id, ...scoped.args],
|
|
201
|
+
});
|
|
202
|
+
const row = (result.rows ?? [])[0];
|
|
203
|
+
return row ? mapRow(row) : null;
|
|
204
|
+
}
|
|
205
|
+
/** Purge audit rows older than `cutoffMs`. Returns the deleted row count. */
|
|
206
|
+
export async function deleteOldAuditEvents(cutoffMs) {
|
|
207
|
+
await ensureAuditTables();
|
|
208
|
+
const client = getDbExec();
|
|
209
|
+
const result = await client.execute({
|
|
210
|
+
sql: `DELETE FROM agent_audit_log WHERE created_at < ?`,
|
|
211
|
+
args: [Math.floor(cutoffMs)],
|
|
212
|
+
});
|
|
213
|
+
return Number(result.rowsAffected ?? 0);
|
|
214
|
+
}
|
|
215
|
+
/** Test-only: reset the cached init promise so a fresh DB re-creates tables. */
|
|
216
|
+
export function __resetAuditInitForTests() {
|
|
217
|
+
_initPromise = undefined;
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/audit/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOrE,IAAI,YAAuC,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,YAAY,GAAG,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;YAE3B,MAAM,cAAc,CAAC,GAAG,EAAE,CACxB,MAAM,CAAC,OAAO,CAAC;;;uBAGA,OAAO,EAAE;;;;;;;;;;;;;;;;;OAiBzB,CAAC,CACD,CAAC;YAEF,MAAM,OAAO,GAAG;gBACd,yFAAyF;gBACzF,kFAAkF;gBAClF,qGAAqG;gBACrG,wEAAwE;gBACxE,yFAAyF;gBACzF,8EAA8E;aAC/E,CAAC;YACF,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;gBAC1B,IAAI,CAAC;oBACH,MAAM,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAC5B,CAAC;gBAAC,MAAM,CAAC;oBACP,oEAAoE;gBACtE,CAAC;YACH,CAAC;QACH,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACjB,wDAAwD;YACxD,YAAY,GAAG,SAAS,CAAC;YACzB,MAAM,GAAG,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAiB;IACtD,MAAM,iBAAiB,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,MAAM,CAAC,OAAO,CAAC;QACnB,GAAG,EAAE;;;;iEAIwD;QAC7D,IAAI,EAAE;YACJ,KAAK,CAAC,EAAE;YACR,KAAK,CAAC,SAAS;YACf,KAAK,CAAC,MAAM;YACZ,KAAK,CAAC,MAAM;YACZ,KAAK,CAAC,SAAS;YACf,KAAK,CAAC,UAAU;YAChB,KAAK,CAAC,KAAK;YACX,KAAK,CAAC,QAAQ;YACd,KAAK,CAAC,MAAM;YACZ,KAAK,CAAC,UAAU;YAChB,KAAK,CAAC,QAAQ;YACd,KAAK,CAAC,MAAM;YACZ,KAAK,CAAC,OAAO;YACb,KAAK,CAAC,KAAK;YACX,KAAK,CAAC,SAAS;YACf,KAAK,CAAC,UAAU;YAChB,KAAK,CAAC,UAAU;SACjB;KACF,CAAC,CAAC;AACL,CAAC;AAED,SAAS,MAAM,CAAC,GAAQ;IACtB,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;QACjC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QAC1B,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QAC1B,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;QACnC,KAAK,EAAE,GAAG,CAAC,MAAM,IAAI,IAAI;QACzB,QAAQ,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI;QAC/B,MAAM,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;QAC3B,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;QACnC,QAAQ,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI;QAC/B,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,IAAI;QAC5B,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,IAAI;QACxB,SAAS,EAAE,GAAG,CAAC,UAAU,IAAI,IAAI;QACjC,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;QACnC,UAAU,EAAE,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS,CAAoB;KAC7D,CAAC;AACJ,CAAC;AAOD;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAqB;IACxC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAU,EAAE,CAAC;IACvB,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,uEAAuE;YACvE,kEAAkE;YAClE,sEAAsE;YACtE,SAAS;YACT,OAAO,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;YACrE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACtD,OAAO,EAAE,GAAG,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,SAAS,GAAG,GAAG,CAAC;AACtB,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B,4EAA4E;AAC5E,8EAA8E;AAC9E,sEAAsE;AACtE,MAAM,YAAY,GAChB,mEAAmE;IACnE,+DAA+D;IAC/D,qCAAqC,CAAC;AAExC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAqB,EACrB,UAA6B,EAAE;IAE/B,MAAM,iBAAiB,EAAE,CAAC;IAC1B,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IAChD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAE3B,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,KAAK,GAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,IAAI,GAAU,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAErC,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,KAAU,EAAE,EAAE;QAC1C,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC,CAAC;IACF,IAAI,OAAO,CAAC,UAAU;QAAE,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IACpE,IAAI,OAAO,CAAC,QAAQ;QAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9D,IAAI,OAAO,CAAC,SAAS;QAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACjE,IAAI,OAAO,CAAC,UAAU;QAAE,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IACpE,IAAI,OAAO,CAAC,MAAM;QAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,OAAO,CAAC,QAAQ;QAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9D,IAAI,OAAO,CAAC,MAAM;QAAE,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,MAAM;QAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACxC,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACpB,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC,CAAC,EACvD,SAAS,CACV,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QAClC,GAAG,EAAE,UAAU,YAAY;kBACb,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;;kBAEnB;QACd,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC;KACvB,CAAC,CAAC;IACH,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,EAAU,EACV,KAAqB;IAErB,MAAM,iBAAiB,EAAE,CAAC;IAC1B,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAClD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QAClC,GAAG,EAAE,kDAAkD,MAAM,CAAC,GAAG,UAAU;QAC3E,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,OAAO,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAgB;IACzD,MAAM,iBAAiB,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QAClC,GAAG,EAAE,kDAAkD;QACvD,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;KAC7B,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,wBAAwB;IACtC,YAAY,GAAG,SAAS,CAAC;AAC3B,CAAC","sourcesContent":["/**\n * SQL persistence for the framework audit log.\n *\n * Follows the same raw-SQL, provider-agnostic pattern as observability/store.ts\n * and usage/store.ts — framework tables use `getDbExec()` + `intType()` rather\n * than Drizzle ORM (which is for template-level schemas). One append-only table\n * `agent_audit_log`; reads are scoped to the caller's identity in SQL (no\n * shares table — audit rows are never individually shared).\n */\nimport { getDbExec, intType, retryOnDdlRace } from \"../db/client.js\";\nimport type {\n AuditEvent,\n AuditQueryFilters,\n AuditVisibility,\n} from \"./types.js\";\n\nlet _initPromise: Promise<void> | undefined;\n\nexport async function ensureAuditTables(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n\n await retryOnDdlRace(() =>\n client.execute(`\n CREATE TABLE IF NOT EXISTS agent_audit_log (\n id TEXT PRIMARY KEY,\n created_at ${intType()} NOT NULL,\n action TEXT NOT NULL,\n caller TEXT NOT NULL,\n actor_kind TEXT NOT NULL,\n actor_email TEXT,\n org_id TEXT,\n thread_id TEXT,\n turn_id TEXT,\n target_type TEXT,\n target_id TEXT,\n status TEXT NOT NULL DEFAULT 'success',\n summary TEXT,\n input TEXT,\n error_code TEXT,\n owner_email TEXT,\n visibility TEXT NOT NULL DEFAULT 'private'\n )\n `),\n );\n\n const indexes = [\n `CREATE INDEX IF NOT EXISTS idx_audit_owner ON agent_audit_log (owner_email, created_at)`,\n `CREATE INDEX IF NOT EXISTS idx_audit_org ON agent_audit_log (org_id, created_at)`,\n `CREATE INDEX IF NOT EXISTS idx_audit_target ON agent_audit_log (target_type, target_id, created_at)`,\n `CREATE INDEX IF NOT EXISTS idx_audit_turn ON agent_audit_log (turn_id)`,\n `CREATE INDEX IF NOT EXISTS idx_audit_actor ON agent_audit_log (actor_email, created_at)`,\n `CREATE INDEX IF NOT EXISTS idx_audit_created ON agent_audit_log (created_at)`,\n ];\n for (const sql of indexes) {\n try {\n await client.execute(sql);\n } catch {\n // Index creation is best-effort; a racing boot may have created it.\n }\n }\n })().catch((err) => {\n // Allow a later call to retry if the first init failed.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\nexport async function insertAuditEvent(event: AuditEvent): Promise<void> {\n await ensureAuditTables();\n const client = getDbExec();\n await client.execute({\n sql: `INSERT INTO agent_audit_log\n (id, created_at, action, caller, actor_kind, actor_email, org_id,\n thread_id, turn_id, target_type, target_id, status, summary, input,\n error_code, owner_email, visibility)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n event.id,\n event.createdAt,\n event.action,\n event.caller,\n event.actorKind,\n event.actorEmail,\n event.orgId,\n event.threadId,\n event.turnId,\n event.targetType,\n event.targetId,\n event.status,\n event.summary,\n event.input,\n event.errorCode,\n event.ownerEmail,\n event.visibility,\n ],\n });\n}\n\nfunction mapRow(row: any): AuditEvent {\n return {\n id: String(row.id),\n createdAt: Number(row.created_at),\n action: String(row.action),\n caller: String(row.caller),\n actorKind: row.actor_kind,\n actorEmail: row.actor_email ?? null,\n orgId: row.org_id ?? null,\n threadId: row.thread_id ?? null,\n turnId: row.turn_id ?? null,\n targetType: row.target_type ?? null,\n targetId: row.target_id ?? null,\n status: row.status,\n summary: row.summary ?? null,\n input: row.input ?? null,\n errorCode: row.error_code ?? null,\n ownerEmail: row.owner_email ?? null,\n visibility: (row.visibility ?? \"private\") as AuditVisibility,\n };\n}\n\nexport interface AuditReadScope {\n userEmail?: string;\n orgId?: string | null;\n}\n\n/**\n * Build the access-scoping WHERE fragment + args. A caller sees audit rows they\n * own, plus org-visible rows in their org. With no identity, nothing matches —\n * the audit log never leaks cross-tenant. Mirrors the core ownership clause of\n * `accessFilter` (minus shares, which audit rows don't have).\n */\nfunction scopeClause(scope: AuditReadScope): { sql: string; args: any[] } {\n const clauses: string[] = [];\n const args: any[] = [];\n if (scope.userEmail) {\n if (scope.orgId) {\n // Constrain the owner's rows to the active org — plus legacy/solo rows\n // that predate org-scoping (org_id IS NULL) — mirroring sharing's\n // `ownerScopeFilter`, so switching orgs doesn't surface another org's\n // trail.\n clauses.push(\"(owner_email = ? AND (org_id = ? OR org_id IS NULL))\");\n args.push(scope.userEmail, scope.orgId);\n } else {\n clauses.push(\"owner_email = ?\");\n args.push(scope.userEmail);\n }\n }\n if (scope.orgId) {\n clauses.push(\"(visibility = 'org' AND org_id = ?)\");\n args.push(scope.orgId);\n }\n if (clauses.length === 0) return { sql: \"1=0\", args };\n return { sql: `(${clauses.join(\" OR \")})`, args };\n}\n\nconst MAX_LIMIT = 500;\nconst DEFAULT_LIMIT = 100;\n\n// Columns returned by the list surface — deliberately EXCLUDES `input` so a\n// timeline query never streams every event's (redacted) request body in bulk.\n// Fetch the full payload one event at a time via `getAuditEventById`.\nconst LIST_COLUMNS =\n \"id, created_at, action, caller, actor_kind, actor_email, org_id, \" +\n \"thread_id, turn_id, target_type, target_id, status, summary, \" +\n \"error_code, owner_email, visibility\";\n\nexport async function queryAuditEvents(\n scope: AuditReadScope,\n filters: AuditQueryFilters = {},\n): Promise<AuditEvent[]> {\n await ensureAuditTables();\n if (!scope.userEmail && !scope.orgId) return [];\n const client = getDbExec();\n\n const scoped = scopeClause(scope);\n const where: string[] = [scoped.sql];\n const args: any[] = [...scoped.args];\n\n const push = (clause: string, value: any) => {\n where.push(clause);\n args.push(value);\n };\n if (filters.targetType) push(\"target_type = ?\", filters.targetType);\n if (filters.targetId) push(\"target_id = ?\", filters.targetId);\n if (filters.actorKind) push(\"actor_kind = ?\", filters.actorKind);\n if (filters.actorEmail) push(\"actor_email = ?\", filters.actorEmail);\n if (filters.status) push(\"status = ?\", filters.status);\n if (filters.threadId) push(\"thread_id = ?\", filters.threadId);\n if (filters.turnId) push(\"turn_id = ?\", filters.turnId);\n if (filters.action) push(\"action = ?\", filters.action);\n if (typeof filters.sinceMs === \"number\") {\n push(\"created_at >= ?\", Math.floor(filters.sinceMs));\n }\n\n const limit = Math.min(\n Math.max(1, Math.floor(filters.limit ?? DEFAULT_LIMIT)),\n MAX_LIMIT,\n );\n\n const result = await client.execute({\n sql: `SELECT ${LIST_COLUMNS} FROM agent_audit_log\n WHERE ${where.join(\" AND \")}\n ORDER BY created_at DESC\n LIMIT ?`,\n args: [...args, limit],\n });\n return (result.rows ?? []).map(mapRow);\n}\n\nexport async function getAuditEventById(\n id: string,\n scope: AuditReadScope,\n): Promise<AuditEvent | null> {\n await ensureAuditTables();\n if (!scope.userEmail && !scope.orgId) return null;\n const client = getDbExec();\n const scoped = scopeClause(scope);\n const result = await client.execute({\n sql: `SELECT * FROM agent_audit_log WHERE id = ? AND ${scoped.sql} LIMIT 1`,\n args: [id, ...scoped.args],\n });\n const row = (result.rows ?? [])[0];\n return row ? mapRow(row) : null;\n}\n\n/** Purge audit rows older than `cutoffMs`. Returns the deleted row count. */\nexport async function deleteOldAuditEvents(cutoffMs: number): Promise<number> {\n await ensureAuditTables();\n const client = getDbExec();\n const result = await client.execute({\n sql: `DELETE FROM agent_audit_log WHERE created_at < ?`,\n args: [Math.floor(cutoffMs)],\n });\n return Number(result.rowsAffected ?? 0);\n}\n\n/** Test-only: reset the cached init promise so a fresh DB re-creates tables. */\nexport function __resetAuditInitForTests(): void {\n _initPromise = undefined;\n}\n"]}
|