@agent-native/core 0.71.0 → 0.72.1
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 +60 -0
- package/corpus/core/docs/content/actions.md +23 -0
- package/corpus/core/docs/content/audit-log.md +111 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/action.ts +80 -1
- package/corpus/core/src/agent/production-agent.ts +27 -0
- package/corpus/core/src/agent/run-store.ts +18 -1
- 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/AssistantChat.tsx +6 -0
- 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/mcp/build-server.ts +1 -0
- 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/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/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-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 +34 -1
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +138 -11
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +50 -33
- package/corpus/templates/clips/chrome-extension/src/overlay.css +11 -27
- package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +33 -38
- package/corpus/templates/clips/chrome-extension/src/popup.ts +6 -24
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +43 -3
- 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/observational-memory/schema.d.ts +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +28 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +18 -1
- package/dist/agent/run-store.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/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -1
- package/dist/client/AssistantChat.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/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +1 -0
- 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/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/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.72.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b2a5931: Fix `value "<ms epoch>" is out of range for type integer` aborting agent chat on
|
|
8
|
+
Postgres/Neon at the start of every turn.
|
|
9
|
+
|
|
10
|
+
The background-aware stale-run cutoff built SQL like
|
|
11
|
+
`COALESCE(heartbeat_at, started_at) >= (? - CASE WHEN dispatch_mode LIKE
|
|
12
|
+
'background%' THEN 90000 ELSE 15000 END)` and bound `Date.now()` to the
|
|
13
|
+
parameter. Postgres infers an untyped parameter's type from its surrounding
|
|
14
|
+
expression, and because the `15000`/`90000` window literals are `int4`, it typed
|
|
15
|
+
the parameter as `int4` too — so a millisecond epoch like `1782295529106`
|
|
16
|
+
overflowed even though every column involved is already `BIGINT`. This is a
|
|
17
|
+
query-level type-inference bug, independent of column types, which is why the
|
|
18
|
+
`widenIntColumnsToBigInt()` shim could never fix it.
|
|
19
|
+
|
|
20
|
+
The cutoff now wraps the parameter in `CAST(? AS BIGINT)`, pinning it to int8 so
|
|
21
|
+
the subtraction stays 64-bit. SQLite treats `CAST(x AS BIGINT)` as INTEGER
|
|
22
|
+
affinity, so it is a no-op there. Fixes `tryClaimRunSlot`, `reapIfStale`,
|
|
23
|
+
`reapAllStaleRuns`, and `cleanupOldRuns`, which all share the cutoff.
|
|
24
|
+
|
|
25
|
+
## 0.72.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- 9a984f2: Add a framework audit log: a durable, complete, access-scoped, append-only record of who mutated what app data, when, from where, and — when it was the agent — in which run. Capture is automatic at the `defineAction` seam (default-on for mutating actions; read-only actions opt in via `audit.onRead`), with credential redaction, agent-vs-human actor attribution, and agent thread/turn linkage. Reads go through two new core actions every app inherits — `list-audit-events` and `get-audit-event` — scoped in SQL to the caller's identity and org. Stored in `agent_audit_log` (provider-agnostic), with a retention purge configurable via `AGENT_NATIVE_AUDIT_RETENTION_DAYS` (default 365) and a global kill switch `AGENT_NATIVE_AUDIT_ENABLED=false`. Distinct from observability (sampled telemetry) and tracking (fire-and-forget analytics).
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- 9a984f2: Relax the default `Permissions-Policy` from `camera=()` to `camera=*` so media-capture UI is no longer blocked at the policy level. `camera=()` disabled the camera for the page **and every iframe inside it**, which broke same-page recording UI and the Clips browser extension's camera bubble (injected as a cross-origin iframe, so it can't be re-enabled per-frame). Microphone stays `self`, geolocation and wake-lock stay disabled, and the browser still gates actual camera/mic use behind a per-origin permission prompt — this only removes the policy-level block, not user consent.
|
|
34
|
+
- 9a984f2: Fix a chat crash where the agent transcript could fail to render with
|
|
35
|
+
"MessageRepository(performOp/link): A message with the same id already exists in
|
|
36
|
+
the parent tree". Thread repositories whose message list contained a repeated id
|
|
37
|
+
(from optimistic+echo races, streaming reconnect replays, or multi-tab merges)
|
|
38
|
+
were imported into assistant-ui verbatim, and its `MessageRepository` throws on a
|
|
39
|
+
duplicate id. The import path now collapses duplicate ids to their most recent
|
|
40
|
+
copy before handing the repository to assistant-ui, so the throw can't occur. The
|
|
41
|
+
no-duplicate case is an exact no-op, leaving normal threads unchanged.
|
|
42
|
+
- 9a984f2: Fix `value "<ms epoch>" is out of range for type integer` on long-lived
|
|
43
|
+
Postgres/Neon databases — most visibly, agent chat failing on **every** prompt.
|
|
44
|
+
Millisecond `Date.now()` timestamps are written into columns that, on databases
|
|
45
|
+
created before the Postgres BIGINT-compatibility shim, are physically 32-bit
|
|
46
|
+
`INTEGER` (int4, max 2,147,483,647); a millisecond epoch like `1782269273204`
|
|
47
|
+
overflows. The source had since switched to `BIGINT`, but `CREATE TABLE IF NOT
|
|
48
|
+
EXISTS` can't re-type an existing column, so those databases kept the int4
|
|
49
|
+
column and writes kept failing (`insertRun()` runs at the start of every turn,
|
|
50
|
+
so the agent chat aborted as a `connection_error`).
|
|
51
|
+
|
|
52
|
+
Adds a `widenIntColumnsToBigInt()` helper (new module
|
|
53
|
+
`@agent-native/core/db/widen-columns`) that, on Postgres only, widens such
|
|
54
|
+
columns in place to `BIGINT` once via each store's existing `ensureTable()`
|
|
55
|
+
bootstrap. It is idempotent (only ALTERs columns still typed `integer`, so
|
|
56
|
+
already-bigint tables are never rewritten), non-destructive (int4 → int8
|
|
57
|
+
widening), and a no-op on SQLite. Applied to the millisecond-timestamp columns
|
|
58
|
+
of `agent_runs`, `agent_tool_ledger`, `chat_threads`, `application_state`,
|
|
59
|
+
`token_usage`, `settings`, `oauth_tokens`, `resources`, `sessions`, and
|
|
60
|
+
`custom_api_providers`. (`staged_datasets` already self-heals via its own
|
|
61
|
+
widener.)
|
|
62
|
+
|
|
3
63
|
## 0.71.0
|
|
4
64
|
|
|
5
65
|
### Minor Changes
|
|
@@ -329,6 +329,28 @@ export default defineAction({
|
|
|
329
329
|
> [!WARNING]
|
|
330
330
|
> Keep approvals rare. Each gated action is a hard stop in the agent loop. The default is **off**, and almost every action should leave it off. See [Human-in-the-Loop Approvals](/docs/human-approval) for the predicate API, the `approval_required` event, and the full flow.
|
|
331
331
|
|
|
332
|
+
### Audit logging {#audit}
|
|
333
|
+
|
|
334
|
+
Every mutating action is **audited automatically** — the framework records who ran it, when, from which surface, and (when it was the agent) which thread/turn, with credential-redacted inputs. Read-only (`GET`) actions are skipped. You don't write any code for this; it happens at the `defineAction` seam.
|
|
335
|
+
|
|
336
|
+
Add an `audit` block only to _tune_ capture — most usefully to declare the resource the action changed so the change shows up in that resource's owner's trail:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
export default defineAction({
|
|
340
|
+
description: "Delete a recording.",
|
|
341
|
+
schema: z.object({ id: z.string() }),
|
|
342
|
+
audit: {
|
|
343
|
+
target: (args, result) => ({ type: "recording", id: args.id }),
|
|
344
|
+
summary: (args) => `Deleted recording ${args.id}`,
|
|
345
|
+
},
|
|
346
|
+
run: async (args, ctx) => {
|
|
347
|
+
/* ...delete... */
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Other knobs: `audit: { onRead: true }` audits a sensitive read (secret access, bulk export); `audit: { enabled: false }` opts a noisy write out; `audit: { recordInputs: false }` skips capturing arguments. Read the trail back with the built-in `list-audit-events` / `get-audit-event` actions. Full details in [Audit Log](/docs/audit-log).
|
|
353
|
+
|
|
332
354
|
## Calling it from the UI {#ui}
|
|
333
355
|
|
|
334
356
|
Two hooks, both in `@agent-native/core/client`. Types are inferred from your `defineAction` schemas — no manual type declarations.
|
|
@@ -553,6 +575,7 @@ const args = parseArgs(["--name", "Steve", "--verbose", "--count=3"]);
|
|
|
553
575
|
|
|
554
576
|
## What's next
|
|
555
577
|
|
|
578
|
+
- [**Audit Log**](/docs/audit-log) — the automatic who-changed-what trail around every action
|
|
556
579
|
- [**Human-in-the-Loop Approvals**](/docs/human-approval) — the `needsApproval` gate in depth
|
|
557
580
|
- [**Drop-in Agent**](/docs/drop-in-agent) — `useActionMutation` / `useActionQuery` in React
|
|
558
581
|
- [**Context Awareness**](/docs/context-awareness) — the `view-screen` + `navigate` pattern in depth
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Audit Log"
|
|
3
|
+
description: "A durable, append-only record of who changed what app data, when, and whether it was you or the agent — captured automatically at the action seam."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Audit Log
|
|
7
|
+
|
|
8
|
+
Every agent-native app gets an audit log out of the box: a durable, complete, access-scoped, append-only record of **who mutated what app data, when, from where, and — when it was the agent — in which run.** Capture is automatic at the action seam; you write no code for it.
|
|
9
|
+
|
|
10
|
+
Because the agent can change data on your behalf, the headline question an audit log answers here isn't just "who edited this record" — it's **"was that me or the agent, and which turn caused it?"** No other system in the framework can answer that.
|
|
11
|
+
|
|
12
|
+
## Audit vs. observability vs. tracking {#which}
|
|
13
|
+
|
|
14
|
+
Three systems record "what happened," for three different reasons. Pick by the question you're asking:
|
|
15
|
+
|
|
16
|
+
| System | The question it answers | Fidelity | Audience |
|
|
17
|
+
| ---------------------------------------- | ------------------------------------------------------ | -------------------------------- | ----------------------------- |
|
|
18
|
+
| **Audit Log** (this page) | "Who changed this record, when, and was it the agent?" | **Complete, durable, scoped** | User, admin, the agent itself |
|
|
19
|
+
| **[Observability](/docs/observability)** | "Why did the agent do that, and what did it cost?" | Sampled span telemetry | Developer |
|
|
20
|
+
| **[Tracking](/docs/tracking)** | "How are people using the product?" | Fire-and-forget to external SaaS | PM / growth |
|
|
21
|
+
|
|
22
|
+
An audit log that's sampled or shipped off to an analytics provider is useless — the whole point is that it's complete, local, and queryable. So it's its own subsystem, not a mode of the other two.
|
|
23
|
+
|
|
24
|
+
## What's captured automatically {#captured}
|
|
25
|
+
|
|
26
|
+
When any **mutating** action runs (anything that isn't a read-only `GET`), the framework appends one row to `agent_audit_log` with:
|
|
27
|
+
|
|
28
|
+
- **Action** — the action name (e.g. `delete-recording`).
|
|
29
|
+
- **Actor** — `agent`, `human`, or `system`, plus the actor's email — populated **even for agent calls**, so you get "the agent, acting for alice@, …".
|
|
30
|
+
- **Run linkage** — the agent `threadId` / `turnId` that triggered the call (a tool call), so a mutation traces back to the exact agent turn.
|
|
31
|
+
- **Surface** — `tool` (agent), `frontend`, `http`, `cli`, `mcp`, or `a2a`.
|
|
32
|
+
- **Outcome** — `success`, `error` (with an error code), or `denied` (blocked by a human-approval gate).
|
|
33
|
+
- **Inputs** — the call arguments, with credential-shaped values [redacted](#privacy).
|
|
34
|
+
- **Target & owner** — the resource the action changed, used to [scope reads](#reading).
|
|
35
|
+
|
|
36
|
+
No wiring needed — the capture hooks into `defineAction` transparently. Read-only actions are skipped, and a few high-frequency framework actions (app-state sync, context-xray, navigation) are skipped by default to avoid flooding the log.
|
|
37
|
+
|
|
38
|
+
## Declare what an action changed {#target}
|
|
39
|
+
|
|
40
|
+
By default an event is scoped to the **actor** — you see your own changes and the agent's changes on your behalf. To make a change to a _shared_ resource also appear in the **owner's** trail, and to label events by resource, declare a `target`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
export default defineAction({
|
|
44
|
+
description: "Delete a recording.",
|
|
45
|
+
schema: z.object({ id: z.string() }),
|
|
46
|
+
audit: {
|
|
47
|
+
target: (args, result) => ({
|
|
48
|
+
type: "recording",
|
|
49
|
+
id: args.id,
|
|
50
|
+
// Optional — defaults to the actor. Set when editing someone else's resource.
|
|
51
|
+
ownerEmail: result?.ownerEmail,
|
|
52
|
+
visibility: "org",
|
|
53
|
+
}),
|
|
54
|
+
summary: (args) => `Deleted recording ${args.id}`,
|
|
55
|
+
},
|
|
56
|
+
run: async (args, ctx) => {
|
|
57
|
+
/* ...delete... */
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Everything in `audit` is optional. The minimum useful addition is `target: () => ({ type, id })`.
|
|
63
|
+
|
|
64
|
+
### Tuning capture {#tuning}
|
|
65
|
+
|
|
66
|
+
| Option | Effect |
|
|
67
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
68
|
+
| `audit.target` | Label the event with the resource and scope reads to its owner. |
|
|
69
|
+
| `audit.summary` | A short human-readable line for the event. |
|
|
70
|
+
| `audit.onRead` | Audit a sensitive **read** (secret access, bulk export). |
|
|
71
|
+
| `audit.enabled` | `true` forces capture on; `false` opts a noisy mutation out. |
|
|
72
|
+
| `audit.recordInputs` | `false` skips capturing the (already redacted) arguments. |
|
|
73
|
+
|
|
74
|
+
## Reading the trail {#reading}
|
|
75
|
+
|
|
76
|
+
Two read actions are available to the agent **and** the frontend in every app, scoped in SQL to the caller — they never return another tenant's rows:
|
|
77
|
+
|
|
78
|
+
- **`list-audit-events`** — filter by `targetType` / `targetId`, `actorKind` (`agent` | `human` | `system`), `status`, `threadId` / `turnId`, `action`, `sinceMs`, and `limit`.
|
|
79
|
+
- **`get-audit-event`** — one event by id, including its redacted input payload.
|
|
80
|
+
|
|
81
|
+
Build an activity feed or a "who changed this" line by calling `list-audit-events` from the UI with `useActionQuery` — never hand-write a fetch to the audit table:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useActionQuery } from "@agent-native/core/client";
|
|
85
|
+
|
|
86
|
+
const { data } = useActionQuery("list-audit-events", {
|
|
87
|
+
targetType: "recording",
|
|
88
|
+
targetId: recordingId,
|
|
89
|
+
});
|
|
90
|
+
// data.events → [{ action, actorKind, actorEmail, turnId, status, summary, createdAt }, …]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The agent can call the same action — ask it "what did you change on this recording?" and it answers from the trail.
|
|
94
|
+
|
|
95
|
+
## Privacy & retention {#privacy}
|
|
96
|
+
|
|
97
|
+
- **Redaction** — before any inputs are stored, credential-shaped keys and values (tokens, secrets, passwords, bearer strings) are stripped and oversized payloads truncated. The audit log never becomes a secondary store of secrets. Keep `summary` text free of sensitive data too.
|
|
98
|
+
- **Append-only** — there is no update or delete action for audit rows. The only deletion is the retention purge, which makes the log trustworthy as an audit trail.
|
|
99
|
+
- **Tenant isolation** — reads are scoped to the caller's identity and org; with no identity, nothing matches.
|
|
100
|
+
|
|
101
|
+
Configure via environment:
|
|
102
|
+
|
|
103
|
+
- `AGENT_NATIVE_AUDIT_RETENTION_DAYS` — how long rows are kept (default `365`; `0` = keep forever).
|
|
104
|
+
- `AGENT_NATIVE_AUDIT_ENABLED=false` — global kill switch.
|
|
105
|
+
|
|
106
|
+
## What's next
|
|
107
|
+
|
|
108
|
+
- [**Actions**](/docs/actions) — the `defineAction` seam where capture happens
|
|
109
|
+
- [**Human-in-the-Loop Approvals**](/docs/human-approval) — gated actions, recorded as `denied`
|
|
110
|
+
- [**Security & Data Scoping**](/docs/security) — the ownership model audit reads reuse
|
|
111
|
+
- [**Observability**](/docs/observability) — agent-run telemetry (the other "what happened")
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"./org": "./dist/org/index.js",
|
|
98
98
|
"./client/org": "./dist/client/org/index.js",
|
|
99
99
|
"./client/db-admin": "./dist/client/db-admin/index.js",
|
|
100
|
+
"./audit": "./dist/audit/index.js",
|
|
100
101
|
"./sharing": "./dist/sharing/index.js",
|
|
101
102
|
"./sharing/actions/share-resource": "./dist/sharing/actions/share-resource.js",
|
|
102
103
|
"./sharing/actions/unshare-resource": "./dist/sharing/actions/unshare-resource.js",
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
normalizeActionChatUIConfig,
|
|
8
8
|
type ActionChatUIConfig,
|
|
9
9
|
} from "./action-ui.js";
|
|
10
|
+
import type { ActionAuditConfig } from "./audit/types.js";
|
|
11
|
+
import { normalizeAuditConfig, resolveAuditAttach } from "./audit/config.js";
|
|
10
12
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -75,6 +77,21 @@ export interface ActionRunContext {
|
|
|
75
77
|
* attaching an `"abort"` listener is always safe.
|
|
76
78
|
*/
|
|
77
79
|
signal?: AbortSignal;
|
|
80
|
+
/**
|
|
81
|
+
* Name of the action being invoked (the registry key, e.g.
|
|
82
|
+
* `delete-recording`). Set at each dispatch site so cross-cutting concerns —
|
|
83
|
+
* notably the audit log — can attribute the call. `undefined` for direct
|
|
84
|
+
* programmatic `run()` calls that bypass the dispatcher.
|
|
85
|
+
*/
|
|
86
|
+
actionName?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Agent conversation thread + turn that triggered this call, populated only
|
|
89
|
+
* inside the agent tool loop (`caller: "tool"`). Lets the audit log link a
|
|
90
|
+
* mutation to the specific agent run/turn that caused it. `undefined` on
|
|
91
|
+
* every human/programmatic surface.
|
|
92
|
+
*/
|
|
93
|
+
threadId?: string;
|
|
94
|
+
turnId?: string;
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
export interface AgentActionStopOptions {
|
|
@@ -378,6 +395,14 @@ interface DefineActionWithSchema<
|
|
|
378
395
|
args: StandardSchemaV1.InferOutput<TSchema>,
|
|
379
396
|
ctx?: ActionRunContext,
|
|
380
397
|
) => boolean | Promise<boolean>);
|
|
398
|
+
/**
|
|
399
|
+
* Audit-log configuration. **Default-on for mutating actions** — you only
|
|
400
|
+
* need this to tune capture: declare the mutated `target` (so the change
|
|
401
|
+
* shows up in the owner's audit trail) and/or a `summary`, opt a read-only
|
|
402
|
+
* action in via `onRead`, or opt a noisy action out via `enabled: false`.
|
|
403
|
+
* See the `audit-log` skill.
|
|
404
|
+
*/
|
|
405
|
+
audit?: ActionAuditConfig;
|
|
381
406
|
}
|
|
382
407
|
|
|
383
408
|
// ---------------------------------------------------------------------------
|
|
@@ -443,6 +468,9 @@ interface DefineActionWithParams<
|
|
|
443
468
|
args: InferParams<TParams>,
|
|
444
469
|
ctx?: ActionRunContext,
|
|
445
470
|
) => boolean | Promise<boolean>);
|
|
471
|
+
/** Audit-log configuration (default-on for mutations). See the schema
|
|
472
|
+
* overload above and the `audit-log` skill. */
|
|
473
|
+
audit?: ActionAuditConfig;
|
|
446
474
|
}
|
|
447
475
|
|
|
448
476
|
// ---------------------------------------------------------------------------
|
|
@@ -497,6 +525,10 @@ export interface ActionDefinition<TInput, TReturn> {
|
|
|
497
525
|
readonly needsApproval?:
|
|
498
526
|
| boolean
|
|
499
527
|
| ((args: TInput, ctx?: ActionRunContext) => boolean | Promise<boolean>);
|
|
528
|
+
/** Resolved audit-log configuration. Present only when the caller passed
|
|
529
|
+
* `audit`. The audit capture wrapper is baked into `run`; this field is for
|
|
530
|
+
* introspection. */
|
|
531
|
+
readonly audit?: ActionAuditConfig;
|
|
500
532
|
}
|
|
501
533
|
|
|
502
534
|
// ---------------------------------------------------------------------------
|
|
@@ -617,6 +649,16 @@ export function defineAction(options: any) {
|
|
|
617
649
|
? true
|
|
618
650
|
: undefined;
|
|
619
651
|
|
|
652
|
+
// Audit: wrap the validated run so every mutating call records an audit
|
|
653
|
+
// event (who/what/when/from-where, and for the agent which run). Default-on
|
|
654
|
+
// for mutations; read-only actions opt in via `audit.onRead`. The wrapper
|
|
655
|
+
// lazily imports the DB-touching recorder so `action.ts` keeps no static DB
|
|
656
|
+
// dependency.
|
|
657
|
+
const auditConfig = normalizeAuditConfig(options.audit);
|
|
658
|
+
const finalRun = resolveAuditAttach(auditConfig, readOnly)
|
|
659
|
+
? wrapRunWithAudit(run, auditConfig)
|
|
660
|
+
: run;
|
|
661
|
+
|
|
620
662
|
// toolCallable: thread through whatever the caller declared. We DO NOT
|
|
621
663
|
// default to `true` here — the absence of an explicit field is meaningful
|
|
622
664
|
// to the tools bridge: it lets us emit a one-shot warning when an action
|
|
@@ -675,7 +717,7 @@ export function defineAction(options: any) {
|
|
|
675
717
|
description: options.description,
|
|
676
718
|
parameters: toolParameters,
|
|
677
719
|
},
|
|
678
|
-
run,
|
|
720
|
+
run: finalRun,
|
|
679
721
|
...(hasSchema ? { schema: options.schema } : {}),
|
|
680
722
|
...(options.http !== undefined ? { http: options.http } : {}),
|
|
681
723
|
...(typeof options.requiresAuth === "boolean"
|
|
@@ -702,6 +744,43 @@ export function defineAction(options: any) {
|
|
|
702
744
|
typeof options.needsApproval === "function"
|
|
703
745
|
? { needsApproval: options.needsApproval }
|
|
704
746
|
: {}),
|
|
747
|
+
...(auditConfig ? { audit: auditConfig } : {}),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Wrap an action's (already input/output-validated) run so each call records an
|
|
753
|
+
* audit event after it resolves — on success and on error. Best-effort: the
|
|
754
|
+
* recorder swallows its own failures and the original result/throw is always
|
|
755
|
+
* preserved, so auditing can never change an action's behavior. The DB-touching
|
|
756
|
+
* recorder is imported lazily so merely defining an action pulls in no DB code.
|
|
757
|
+
*/
|
|
758
|
+
function wrapRunWithAudit(
|
|
759
|
+
run: (args: any, ctx?: ActionRunContext) => any,
|
|
760
|
+
auditConfig: ActionAuditConfig | undefined,
|
|
761
|
+
): (args: any, ctx?: ActionRunContext) => Promise<any> {
|
|
762
|
+
return async function auditedRun(args: any, ctx?: ActionRunContext) {
|
|
763
|
+
let result: any;
|
|
764
|
+
let error: unknown;
|
|
765
|
+
let threw = false;
|
|
766
|
+
try {
|
|
767
|
+
result = await run(args, ctx);
|
|
768
|
+
} catch (err) {
|
|
769
|
+
error = err;
|
|
770
|
+
threw = true;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const { recordActionAudit } = await import("./audit/record.js");
|
|
774
|
+
await recordActionAudit(
|
|
775
|
+
threw
|
|
776
|
+
? { config: auditConfig, args, ctx, status: "error", error }
|
|
777
|
+
: { config: auditConfig, args, ctx, status: "success", result },
|
|
778
|
+
);
|
|
779
|
+
} catch {
|
|
780
|
+
// Recorder failed to load/run — never affect the action.
|
|
781
|
+
}
|
|
782
|
+
if (threw) throw error;
|
|
783
|
+
return result;
|
|
705
784
|
};
|
|
706
785
|
}
|
|
707
786
|
|
|
@@ -2958,6 +2958,28 @@ export async function runAgentLoop(opts: {
|
|
|
2958
2958
|
approvalKey,
|
|
2959
2959
|
...(toolCall.id ? { toolCallId: toolCall.id } : {}),
|
|
2960
2960
|
});
|
|
2961
|
+
// Audit the blocked attempt: the action did NOT run, but "the agent
|
|
2962
|
+
// tried to do X and was gated" is itself worth recording. Best-effort,
|
|
2963
|
+
// but AWAITED (not fire-and-forget) so the row isn't lost to a
|
|
2964
|
+
// serverless freeze / request teardown when the turn pauses.
|
|
2965
|
+
try {
|
|
2966
|
+
const { recordActionAudit } = await import("../audit/record.js");
|
|
2967
|
+
await recordActionAudit({
|
|
2968
|
+
config: undefined,
|
|
2969
|
+
args: toolCall.input,
|
|
2970
|
+
ctx: {
|
|
2971
|
+
actionName: toolCall.name,
|
|
2972
|
+
caller: "tool",
|
|
2973
|
+
userEmail: getRequestUserEmail(),
|
|
2974
|
+
orgId: getRequestOrgId() ?? null,
|
|
2975
|
+
...(opts.threadId ? { threadId: opts.threadId } : {}),
|
|
2976
|
+
...(opts.turnId ? { turnId: opts.turnId } : {}),
|
|
2977
|
+
},
|
|
2978
|
+
status: "denied",
|
|
2979
|
+
});
|
|
2980
|
+
} catch {
|
|
2981
|
+
// Best-effort — auditing must never break the approval pause.
|
|
2982
|
+
}
|
|
2961
2983
|
const result =
|
|
2962
2984
|
`Awaiting human approval to run "${toolCall.name}". This action did ` +
|
|
2963
2985
|
`NOT execute — a human must approve this specific call before it ` +
|
|
@@ -3227,6 +3249,11 @@ export async function runAgentLoop(opts: {
|
|
|
3227
3249
|
caller: "tool",
|
|
3228
3250
|
attachments: opts.attachments,
|
|
3229
3251
|
signal,
|
|
3252
|
+
// Audit attribution: the action name + the agent thread/turn that
|
|
3253
|
+
// triggered this call, so a mutation can be traced to its run.
|
|
3254
|
+
actionName: toolCall.name,
|
|
3255
|
+
...(opts.threadId ? { threadId: opts.threadId } : {}),
|
|
3256
|
+
...(opts.turnId ? { turnId: opts.turnId } : {}),
|
|
3230
3257
|
}),
|
|
3231
3258
|
);
|
|
3232
3259
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* reliable reconnection after page refreshes.
|
|
5
5
|
*/
|
|
6
6
|
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
7
|
+
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
7
8
|
import { captureError } from "../server/capture-error.js";
|
|
8
9
|
import type { AgentChatEvent } from "./types.js";
|
|
9
10
|
|
|
@@ -159,6 +160,19 @@ async function ensureRunTables(): Promise<void> {
|
|
|
159
160
|
PRIMARY KEY (thread_id, tool_key)
|
|
160
161
|
)
|
|
161
162
|
`);
|
|
163
|
+
// Widen millisecond-timestamp columns that older deployments created as
|
|
164
|
+
// 32-bit `INTEGER`. `insertRun()` writes `Date.now()` into `started_at`
|
|
165
|
+
// on every turn, so an int4 column makes every agent prompt fail on
|
|
166
|
+
// Postgres with "value … is out of range for type integer". No-op once
|
|
167
|
+
// widened (and on fresh DBs that already use BIGINT). See
|
|
168
|
+
// widenIntColumnsToBigInt.
|
|
169
|
+
await widenIntColumnsToBigInt("agent_runs", [
|
|
170
|
+
"started_at",
|
|
171
|
+
"completed_at",
|
|
172
|
+
"heartbeat_at",
|
|
173
|
+
"last_progress_at",
|
|
174
|
+
]);
|
|
175
|
+
await widenIntColumnsToBigInt("agent_tool_ledger", ["completed_at"]);
|
|
162
176
|
})().catch((err) => {
|
|
163
177
|
// Retry init on the next call after a failed startup.
|
|
164
178
|
_initPromise = undefined;
|
|
@@ -292,7 +306,10 @@ export async function insertRun(
|
|
|
292
306
|
* states get the wider window via a LIKE-prefix match.
|
|
293
307
|
*/
|
|
294
308
|
function backgroundAwareStaleCutoffSql(): string {
|
|
295
|
-
|
|
309
|
+
// `CAST(? AS BIGINT)` is required: without it Postgres infers the param as
|
|
310
|
+
// int4 from the int4 window literals, so the bound `Date.now()` ms epoch
|
|
311
|
+
// overflows int4. The cast keeps the subtraction 64-bit; a no-op on SQLite.
|
|
312
|
+
return `(CAST(? AS BIGINT) - CASE WHEN dispatch_mode LIKE 'background%' THEN ${BACKGROUND_RUN_STALE_MS} ELSE ${RUN_STALE_MS} END)`;
|
|
296
313
|
}
|
|
297
314
|
|
|
298
315
|
/**
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isPostgres,
|
|
5
5
|
intType,
|
|
6
6
|
} from "../db/client.js";
|
|
7
|
+
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
7
8
|
import { emitAppStateChange, emitAppStateDelete } from "./emitter.js";
|
|
8
9
|
import type { StoreWriteOptions } from "../settings/store.js";
|
|
9
10
|
|
|
@@ -30,6 +31,10 @@ async function ensureTable(): Promise<void> {
|
|
|
30
31
|
PRIMARY KEY (session_id, key)
|
|
31
32
|
)
|
|
32
33
|
`);
|
|
34
|
+
// Older deployments created `updated_at` as 32-bit `INTEGER`; on Postgres
|
|
35
|
+
// the `Date.now()` written by appStatePut() on every turn overflows int4.
|
|
36
|
+
// Widen it in place (no-op once done / on fresh BIGINT databases).
|
|
37
|
+
await widenIntColumnsToBigInt("application_state", ["updated_at"]);
|
|
33
38
|
// Indexes for the two hot poll paths:
|
|
34
39
|
// - `SELECT … WHERE updated_at > ?` (watermark scan, every poll cycle)
|
|
35
40
|
// - `SELECT … WHERE key = ? … ORDER BY updated_at ASC` (marker lookups)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getAuditEventById } from "../store.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch a single audit event by id, including its redacted input payload.
|
|
7
|
+
* Scoped to the caller's identity — returns null if they can't access it.
|
|
8
|
+
*/
|
|
9
|
+
export default defineAction({
|
|
10
|
+
description:
|
|
11
|
+
"Get one audit-log event by id, with its full redacted input payload. Returns null if you don't have access to it.",
|
|
12
|
+
schema: z.object({
|
|
13
|
+
id: z.string().describe("The audit event id."),
|
|
14
|
+
}),
|
|
15
|
+
http: { method: "GET" },
|
|
16
|
+
run: async (args, ctx) => {
|
|
17
|
+
const event = await getAuditEventById(args.id, {
|
|
18
|
+
userEmail: ctx?.userEmail,
|
|
19
|
+
orgId: ctx?.orgId ?? null,
|
|
20
|
+
});
|
|
21
|
+
return { event };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { queryAuditEvents } from "../store.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List audit-log events the current user can see — their own actions plus the
|
|
7
|
+
* agent's actions on their behalf, scoped in SQL to the caller's identity and
|
|
8
|
+
* org. Read-only; never exposes other tenants' rows.
|
|
9
|
+
*/
|
|
10
|
+
export default defineAction({
|
|
11
|
+
description:
|
|
12
|
+
"List audit-log events (who changed what, when, and whether it was you or the agent) for resources you can access. Supports filtering by target resource, actor (agent vs human), status, agent thread/turn, and time. Use this to answer 'what did the agent change', 'who edited this record', or 'show recent changes'.",
|
|
13
|
+
schema: z.object({
|
|
14
|
+
targetType: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Filter to one resource type, e.g. 'recording'."),
|
|
18
|
+
targetId: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Filter to one resource id (pair with targetType)."),
|
|
22
|
+
actorKind: z
|
|
23
|
+
.enum(["agent", "human", "system"])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Filter to changes made by the agent, a human, or the system."),
|
|
26
|
+
actorEmail: z.string().optional().describe("Filter to one actor's email."),
|
|
27
|
+
status: z
|
|
28
|
+
.enum(["success", "error", "denied"])
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Filter by outcome."),
|
|
31
|
+
threadId: z.string().optional().describe("Filter to one agent thread."),
|
|
32
|
+
turnId: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Filter to one agent turn (a single agent response)."),
|
|
36
|
+
action: z.string().optional().describe("Filter to one action name."),
|
|
37
|
+
sinceMs: z
|
|
38
|
+
.number()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Only events at or after this Unix epoch (ms)."),
|
|
41
|
+
limit: z
|
|
42
|
+
.number()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Max events to return (default 100, max 500)."),
|
|
45
|
+
}),
|
|
46
|
+
http: { method: "GET" },
|
|
47
|
+
run: async (args, ctx) => {
|
|
48
|
+
const events = await queryAuditEvents(
|
|
49
|
+
{ userEmail: ctx?.userEmail, orgId: ctx?.orgId ?? null },
|
|
50
|
+
{
|
|
51
|
+
...(args.targetType ? { targetType: args.targetType } : {}),
|
|
52
|
+
...(args.targetId ? { targetId: args.targetId } : {}),
|
|
53
|
+
...(args.actorKind ? { actorKind: args.actorKind } : {}),
|
|
54
|
+
...(args.actorEmail ? { actorEmail: args.actorEmail } : {}),
|
|
55
|
+
...(args.status ? { status: args.status } : {}),
|
|
56
|
+
...(args.threadId ? { threadId: args.threadId } : {}),
|
|
57
|
+
...(args.turnId ? { turnId: args.turnId } : {}),
|
|
58
|
+
...(args.action ? { action: args.action } : {}),
|
|
59
|
+
...(typeof args.sinceMs === "number" ? { sinceMs: args.sinceMs } : {}),
|
|
60
|
+
...(typeof args.limit === "number" ? { limit: args.limit } : {}),
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
return { events, count: events.length };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-log retention job.
|
|
3
|
+
*
|
|
4
|
+
* Periodically purges `agent_audit_log` rows older than the configured horizon
|
|
5
|
+
* so the log doesn't grow unbounded. Retention is configurable via
|
|
6
|
+
* `AGENT_NATIVE_AUDIT_RETENTION_DAYS` (default: 365 days — audit trails are kept
|
|
7
|
+
* far longer than sampled traces). Setting it to `0` disables the purge (keep
|
|
8
|
+
* forever). Mirrors observability/cleanup-job.ts.
|
|
9
|
+
*
|
|
10
|
+
* Runs once on startup after a short delay, then on a 24-hour interval. Timers
|
|
11
|
+
* are unref'd so they never keep the process alive on their own.
|
|
12
|
+
*/
|
|
13
|
+
import { deleteOldAuditEvents } from "./store.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_RETENTION_DAYS = 365;
|
|
16
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const STARTUP_DELAY_MS = 5 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
let _cleanupTimer: NodeJS.Timeout | null = null;
|
|
20
|
+
let _intervalTimer: NodeJS.Timeout | null = null;
|
|
21
|
+
|
|
22
|
+
function resolveRetentionDays(): number {
|
|
23
|
+
const raw = process.env.AGENT_NATIVE_AUDIT_RETENTION_DAYS;
|
|
24
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
25
|
+
return DEFAULT_RETENTION_DAYS;
|
|
26
|
+
}
|
|
27
|
+
const parsed = Number.parseInt(raw, 10);
|
|
28
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_RETENTION_DAYS;
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run the audit cleanup once. Returns the deleted row count, or null when
|
|
34
|
+
* retention is disabled (`AGENT_NATIVE_AUDIT_RETENTION_DAYS=0`).
|
|
35
|
+
*/
|
|
36
|
+
export async function runAuditCleanupOnce(): Promise<number | null> {
|
|
37
|
+
const days = resolveRetentionDays();
|
|
38
|
+
if (days === 0) return null;
|
|
39
|
+
const cutoff = Date.now() - days * ONE_DAY_MS;
|
|
40
|
+
return deleteOldAuditEvents(cutoff);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start the recurring audit-cleanup job. Idempotent — calling more than once is
|
|
45
|
+
* a no-op while a previous schedule is active. Returns a stop function.
|
|
46
|
+
*/
|
|
47
|
+
export function startAuditCleanupJob(): () => void {
|
|
48
|
+
if (_cleanupTimer || _intervalTimer) return stopAuditCleanupJob;
|
|
49
|
+
const days = resolveRetentionDays();
|
|
50
|
+
if (days === 0) {
|
|
51
|
+
if (process.env.DEBUG)
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.log(
|
|
54
|
+
"[audit] Audit cleanup disabled (AGENT_NATIVE_AUDIT_RETENTION_DAYS=0)",
|
|
55
|
+
);
|
|
56
|
+
return () => {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tick = () => {
|
|
60
|
+
runAuditCleanupOnce()
|
|
61
|
+
.then((deleted) => {
|
|
62
|
+
if (deleted == null) return;
|
|
63
|
+
if (process.env.DEBUG) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log(
|
|
66
|
+
`[audit] Audit cleanup purged ${deleted} rows (retention=${days}d)`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.catch((err) => {
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.error("[audit] Audit cleanup failed:", err?.message ?? err);
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
_cleanupTimer = setTimeout(() => {
|
|
77
|
+
_cleanupTimer = null;
|
|
78
|
+
tick();
|
|
79
|
+
_intervalTimer = setInterval(tick, ONE_DAY_MS);
|
|
80
|
+
if (typeof _intervalTimer.unref === "function") _intervalTimer.unref();
|
|
81
|
+
}, STARTUP_DELAY_MS);
|
|
82
|
+
if (typeof _cleanupTimer.unref === "function") _cleanupTimer.unref();
|
|
83
|
+
|
|
84
|
+
if (process.env.DEBUG)
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log(`[audit] Audit cleanup scheduled (retention=${days}d, daily)`);
|
|
87
|
+
|
|
88
|
+
return stopAuditCleanupJob;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function stopAuditCleanupJob(): void {
|
|
92
|
+
if (_cleanupTimer) {
|
|
93
|
+
clearTimeout(_cleanupTimer);
|
|
94
|
+
_cleanupTimer = null;
|
|
95
|
+
}
|
|
96
|
+
if (_intervalTimer) {
|
|
97
|
+
clearInterval(_intervalTimer);
|
|
98
|
+
_intervalTimer = null;
|
|
99
|
+
}
|
|
100
|
+
}
|