@agent-native/core 0.71.0 → 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.
Files changed (164) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +38 -0
  3. package/corpus/core/docs/content/actions.md +23 -0
  4. package/corpus/core/docs/content/audit-log.md +111 -0
  5. package/corpus/core/package.json +2 -1
  6. package/corpus/core/src/action.ts +80 -1
  7. package/corpus/core/src/agent/production-agent.ts +27 -0
  8. package/corpus/core/src/agent/run-store.ts +14 -0
  9. package/corpus/core/src/application-state/store.ts +5 -0
  10. package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
  11. package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
  12. package/corpus/core/src/audit/cleanup-job.ts +100 -0
  13. package/corpus/core/src/audit/config.ts +91 -0
  14. package/corpus/core/src/audit/index.ts +43 -0
  15. package/corpus/core/src/audit/record.ts +143 -0
  16. package/corpus/core/src/audit/redact.ts +109 -0
  17. package/corpus/core/src/audit/store.ts +244 -0
  18. package/corpus/core/src/audit/types.ts +125 -0
  19. package/corpus/core/src/chat-threads/store.ts +10 -0
  20. package/corpus/core/src/client/AssistantChat.tsx +6 -0
  21. package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
  22. package/corpus/core/src/db/client.ts +4 -0
  23. package/corpus/core/src/db/widen-columns.ts +75 -0
  24. package/corpus/core/src/mcp/build-server.ts +1 -0
  25. package/corpus/core/src/oauth-tokens/store.ts +6 -0
  26. package/corpus/core/src/provider-api/custom-registry.ts +8 -0
  27. package/corpus/core/src/resources/store.ts +10 -0
  28. package/corpus/core/src/scripts/runner.ts +6 -3
  29. package/corpus/core/src/server/action-discovery.ts +6 -0
  30. package/corpus/core/src/server/action-routes.ts +1 -0
  31. package/corpus/core/src/server/auth.ts +5 -0
  32. package/corpus/core/src/server/core-routes-plugin.ts +14 -0
  33. package/corpus/core/src/server/security-headers.ts +9 -6
  34. package/corpus/core/src/settings/store.ts +6 -0
  35. package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  36. package/corpus/core/src/usage/store.ts +6 -0
  37. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
  38. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
  39. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
  40. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
  41. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
  42. package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
  43. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
  44. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
  45. package/corpus/templates/clips/chrome-extension/src/background.ts +34 -1
  46. package/corpus/templates/clips/chrome-extension/src/content-script.ts +138 -11
  47. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +50 -33
  48. package/corpus/templates/clips/chrome-extension/src/overlay.css +11 -27
  49. package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
  50. package/corpus/templates/clips/chrome-extension/src/overlay.ts +33 -38
  51. package/corpus/templates/clips/chrome-extension/src/popup.ts +6 -24
  52. package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
  53. package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
  54. package/corpus/templates/clips/shared/recording-audio.ts +62 -0
  55. package/corpus/templates/clips/shared/recording-core.ts +94 -0
  56. package/dist/action.d.ts +31 -0
  57. package/dist/action.d.ts.map +1 -1
  58. package/dist/action.js +45 -1
  59. package/dist/action.js.map +1 -1
  60. package/dist/agent/context-xray/schema.d.ts +1 -1
  61. package/dist/agent/observational-memory/schema.d.ts +1 -1
  62. package/dist/agent/production-agent.d.ts.map +1 -1
  63. package/dist/agent/production-agent.js +28 -0
  64. package/dist/agent/production-agent.js.map +1 -1
  65. package/dist/agent/run-store.d.ts.map +1 -1
  66. package/dist/agent/run-store.js +14 -0
  67. package/dist/agent/run-store.js.map +1 -1
  68. package/dist/application-state/store.d.ts.map +1 -1
  69. package/dist/application-state/store.js +5 -0
  70. package/dist/application-state/store.js.map +1 -1
  71. package/dist/audit/actions/get-audit-event.d.ts +11 -0
  72. package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
  73. package/dist/audit/actions/get-audit-event.js +22 -0
  74. package/dist/audit/actions/get-audit-event.js.map +1 -0
  75. package/dist/audit/actions/list-audit-events.d.ts +22 -0
  76. package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
  77. package/dist/audit/actions/list-audit-events.js +61 -0
  78. package/dist/audit/actions/list-audit-events.js.map +1 -0
  79. package/dist/audit/cleanup-job.d.ts +12 -0
  80. package/dist/audit/cleanup-job.d.ts.map +1 -0
  81. package/dist/audit/cleanup-job.js +93 -0
  82. package/dist/audit/cleanup-job.js.map +1 -0
  83. package/dist/audit/config.d.ts +30 -0
  84. package/dist/audit/config.d.ts.map +1 -0
  85. package/dist/audit/config.js +65 -0
  86. package/dist/audit/config.js.map +1 -0
  87. package/dist/audit/index.d.ts +13 -0
  88. package/dist/audit/index.d.ts.map +1 -0
  89. package/dist/audit/index.js +6 -0
  90. package/dist/audit/index.js.map +1 -0
  91. package/dist/audit/record.d.ts +24 -0
  92. package/dist/audit/record.d.ts.map +1 -0
  93. package/dist/audit/record.js +106 -0
  94. package/dist/audit/record.js.map +1 -0
  95. package/dist/audit/redact.d.ts +28 -0
  96. package/dist/audit/redact.d.ts.map +1 -0
  97. package/dist/audit/redact.js +109 -0
  98. package/dist/audit/redact.js.map +1 -0
  99. package/dist/audit/store.d.ts +14 -0
  100. package/dist/audit/store.d.ts.map +1 -0
  101. package/dist/audit/store.js +219 -0
  102. package/dist/audit/store.js.map +1 -0
  103. package/dist/audit/types.d.ts +114 -0
  104. package/dist/audit/types.d.ts.map +1 -0
  105. package/dist/audit/types.js +15 -0
  106. package/dist/audit/types.js.map +1 -0
  107. package/dist/chat-threads/store.d.ts.map +1 -1
  108. package/dist/chat-threads/store.js +10 -0
  109. package/dist/chat-threads/store.js.map +1 -1
  110. package/dist/client/AssistantChat.d.ts.map +1 -1
  111. package/dist/client/AssistantChat.js +6 -1
  112. package/dist/client/AssistantChat.js.map +1 -1
  113. package/dist/client/chat/repo-helpers.d.ts +16 -0
  114. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  115. package/dist/client/chat/repo-helpers.js +40 -0
  116. package/dist/client/chat/repo-helpers.js.map +1 -1
  117. package/dist/db/client.d.ts.map +1 -1
  118. package/dist/db/client.js +3 -0
  119. package/dist/db/client.js.map +1 -1
  120. package/dist/db/widen-columns.d.ts +39 -0
  121. package/dist/db/widen-columns.d.ts.map +1 -0
  122. package/dist/db/widen-columns.js +73 -0
  123. package/dist/db/widen-columns.js.map +1 -0
  124. package/dist/mcp/build-server.d.ts.map +1 -1
  125. package/dist/mcp/build-server.js +1 -0
  126. package/dist/mcp/build-server.js.map +1 -1
  127. package/dist/oauth-tokens/store.d.ts.map +1 -1
  128. package/dist/oauth-tokens/store.js +6 -0
  129. package/dist/oauth-tokens/store.js.map +1 -1
  130. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  131. package/dist/provider-api/custom-registry.js +8 -0
  132. package/dist/provider-api/custom-registry.js.map +1 -1
  133. package/dist/resources/store.d.ts.map +1 -1
  134. package/dist/resources/store.js +9 -0
  135. package/dist/resources/store.js.map +1 -1
  136. package/dist/scripts/runner.js +4 -3
  137. package/dist/scripts/runner.js.map +1 -1
  138. package/dist/server/action-discovery.d.ts.map +1 -1
  139. package/dist/server/action-discovery.js +6 -0
  140. package/dist/server/action-discovery.js.map +1 -1
  141. package/dist/server/action-routes.d.ts.map +1 -1
  142. package/dist/server/action-routes.js +1 -0
  143. package/dist/server/action-routes.js.map +1 -1
  144. package/dist/server/auth.d.ts.map +1 -1
  145. package/dist/server/auth.js +5 -0
  146. package/dist/server/auth.js.map +1 -1
  147. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  148. package/dist/server/core-routes-plugin.js +13 -0
  149. package/dist/server/core-routes-plugin.js.map +1 -1
  150. package/dist/server/security-headers.d.ts +8 -5
  151. package/dist/server/security-headers.d.ts.map +1 -1
  152. package/dist/server/security-headers.js +9 -6
  153. package/dist/server/security-headers.js.map +1 -1
  154. package/dist/settings/store.d.ts.map +1 -1
  155. package/dist/settings/store.js +6 -0
  156. package/dist/settings/store.js.map +1 -1
  157. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  158. package/dist/usage/store.d.ts.map +1 -1
  159. package/dist/usage/store.js +5 -0
  160. package/dist/usage/store.js.map +1 -1
  161. package/docs/content/actions.md +23 -0
  162. package/docs/content/audit-log.md +111 -0
  163. package/package.json +2 -1
  164. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 1148
31
- - template files: 4000
30
+ - core files: 1160
31
+ - template files: 4005
@@ -1,5 +1,43 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.72.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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).
8
+
9
+ ### Patch Changes
10
+
11
+ - 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.
12
+ - 9a984f2: Fix a chat crash where the agent transcript could fail to render with
13
+ "MessageRepository(performOp/link): A message with the same id already exists in
14
+ the parent tree". Thread repositories whose message list contained a repeated id
15
+ (from optimistic+echo races, streaming reconnect replays, or multi-tab merges)
16
+ were imported into assistant-ui verbatim, and its `MessageRepository` throws on a
17
+ duplicate id. The import path now collapses duplicate ids to their most recent
18
+ copy before handing the repository to assistant-ui, so the throw can't occur. The
19
+ no-duplicate case is an exact no-op, leaving normal threads unchanged.
20
+ - 9a984f2: Fix `value "<ms epoch>" is out of range for type integer` on long-lived
21
+ Postgres/Neon databases — most visibly, agent chat failing on **every** prompt.
22
+ Millisecond `Date.now()` timestamps are written into columns that, on databases
23
+ created before the Postgres BIGINT-compatibility shim, are physically 32-bit
24
+ `INTEGER` (int4, max 2,147,483,647); a millisecond epoch like `1782269273204`
25
+ overflows. The source had since switched to `BIGINT`, but `CREATE TABLE IF NOT
26
+ EXISTS` can't re-type an existing column, so those databases kept the int4
27
+ column and writes kept failing (`insertRun()` runs at the start of every turn,
28
+ so the agent chat aborted as a `connection_error`).
29
+
30
+ Adds a `widenIntColumnsToBigInt()` helper (new module
31
+ `@agent-native/core/db/widen-columns`) that, on Postgres only, widens such
32
+ columns in place to `BIGINT` once via each store's existing `ensureTable()`
33
+ bootstrap. It is idempotent (only ALTERs columns still typed `integer`, so
34
+ already-bigint tables are never rewritten), non-destructive (int4 → int8
35
+ widening), and a no-op on SQLite. Applied to the millisecond-timestamp columns
36
+ of `agent_runs`, `agent_tool_ledger`, `chat_threads`, `application_state`,
37
+ `token_usage`, `settings`, `oauth_tokens`, `resources`, `sessions`, and
38
+ `custom_api_providers`. (`staged_datasets` already self-heals via its own
39
+ widener.)
40
+
3
41
  ## 0.71.0
4
42
 
5
43
  ### 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")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
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;
@@ -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
+ }