@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.
Files changed (165) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +60 -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 +18 -1
  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/desktop/src-tauri/src/native_screen.rs +43 -3
  54. package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
  55. package/corpus/templates/clips/shared/recording-audio.ts +62 -0
  56. package/corpus/templates/clips/shared/recording-core.ts +94 -0
  57. package/dist/action.d.ts +31 -0
  58. package/dist/action.d.ts.map +1 -1
  59. package/dist/action.js +45 -1
  60. package/dist/action.js.map +1 -1
  61. package/dist/agent/context-xray/schema.d.ts +1 -1
  62. package/dist/agent/observational-memory/schema.d.ts +1 -1
  63. package/dist/agent/production-agent.d.ts.map +1 -1
  64. package/dist/agent/production-agent.js +28 -0
  65. package/dist/agent/production-agent.js.map +1 -1
  66. package/dist/agent/run-store.d.ts.map +1 -1
  67. package/dist/agent/run-store.js +18 -1
  68. package/dist/agent/run-store.js.map +1 -1
  69. package/dist/application-state/store.d.ts.map +1 -1
  70. package/dist/application-state/store.js +5 -0
  71. package/dist/application-state/store.js.map +1 -1
  72. package/dist/audit/actions/get-audit-event.d.ts +11 -0
  73. package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
  74. package/dist/audit/actions/get-audit-event.js +22 -0
  75. package/dist/audit/actions/get-audit-event.js.map +1 -0
  76. package/dist/audit/actions/list-audit-events.d.ts +22 -0
  77. package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
  78. package/dist/audit/actions/list-audit-events.js +61 -0
  79. package/dist/audit/actions/list-audit-events.js.map +1 -0
  80. package/dist/audit/cleanup-job.d.ts +12 -0
  81. package/dist/audit/cleanup-job.d.ts.map +1 -0
  82. package/dist/audit/cleanup-job.js +93 -0
  83. package/dist/audit/cleanup-job.js.map +1 -0
  84. package/dist/audit/config.d.ts +30 -0
  85. package/dist/audit/config.d.ts.map +1 -0
  86. package/dist/audit/config.js +65 -0
  87. package/dist/audit/config.js.map +1 -0
  88. package/dist/audit/index.d.ts +13 -0
  89. package/dist/audit/index.d.ts.map +1 -0
  90. package/dist/audit/index.js +6 -0
  91. package/dist/audit/index.js.map +1 -0
  92. package/dist/audit/record.d.ts +24 -0
  93. package/dist/audit/record.d.ts.map +1 -0
  94. package/dist/audit/record.js +106 -0
  95. package/dist/audit/record.js.map +1 -0
  96. package/dist/audit/redact.d.ts +28 -0
  97. package/dist/audit/redact.d.ts.map +1 -0
  98. package/dist/audit/redact.js +109 -0
  99. package/dist/audit/redact.js.map +1 -0
  100. package/dist/audit/store.d.ts +14 -0
  101. package/dist/audit/store.d.ts.map +1 -0
  102. package/dist/audit/store.js +219 -0
  103. package/dist/audit/store.js.map +1 -0
  104. package/dist/audit/types.d.ts +114 -0
  105. package/dist/audit/types.d.ts.map +1 -0
  106. package/dist/audit/types.js +15 -0
  107. package/dist/audit/types.js.map +1 -0
  108. package/dist/chat-threads/store.d.ts.map +1 -1
  109. package/dist/chat-threads/store.js +10 -0
  110. package/dist/chat-threads/store.js.map +1 -1
  111. package/dist/client/AssistantChat.d.ts.map +1 -1
  112. package/dist/client/AssistantChat.js +6 -1
  113. package/dist/client/AssistantChat.js.map +1 -1
  114. package/dist/client/chat/repo-helpers.d.ts +16 -0
  115. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  116. package/dist/client/chat/repo-helpers.js +40 -0
  117. package/dist/client/chat/repo-helpers.js.map +1 -1
  118. package/dist/db/client.d.ts.map +1 -1
  119. package/dist/db/client.js +3 -0
  120. package/dist/db/client.js.map +1 -1
  121. package/dist/db/widen-columns.d.ts +39 -0
  122. package/dist/db/widen-columns.d.ts.map +1 -0
  123. package/dist/db/widen-columns.js +73 -0
  124. package/dist/db/widen-columns.js.map +1 -0
  125. package/dist/mcp/build-server.d.ts.map +1 -1
  126. package/dist/mcp/build-server.js +1 -0
  127. package/dist/mcp/build-server.js.map +1 -1
  128. package/dist/oauth-tokens/store.d.ts.map +1 -1
  129. package/dist/oauth-tokens/store.js +6 -0
  130. package/dist/oauth-tokens/store.js.map +1 -1
  131. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  132. package/dist/provider-api/custom-registry.js +8 -0
  133. package/dist/provider-api/custom-registry.js.map +1 -1
  134. package/dist/resources/store.d.ts.map +1 -1
  135. package/dist/resources/store.js +9 -0
  136. package/dist/resources/store.js.map +1 -1
  137. package/dist/scripts/runner.js +4 -3
  138. package/dist/scripts/runner.js.map +1 -1
  139. package/dist/server/action-discovery.d.ts.map +1 -1
  140. package/dist/server/action-discovery.js +6 -0
  141. package/dist/server/action-discovery.js.map +1 -1
  142. package/dist/server/action-routes.d.ts.map +1 -1
  143. package/dist/server/action-routes.js +1 -0
  144. package/dist/server/action-routes.js.map +1 -1
  145. package/dist/server/auth.d.ts.map +1 -1
  146. package/dist/server/auth.js +5 -0
  147. package/dist/server/auth.js.map +1 -1
  148. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  149. package/dist/server/core-routes-plugin.js +13 -0
  150. package/dist/server/core-routes-plugin.js.map +1 -1
  151. package/dist/server/security-headers.d.ts +8 -5
  152. package/dist/server/security-headers.d.ts.map +1 -1
  153. package/dist/server/security-headers.js +9 -6
  154. package/dist/server/security-headers.js.map +1 -1
  155. package/dist/settings/store.d.ts.map +1 -1
  156. package/dist/settings/store.js +6 -0
  157. package/dist/settings/store.js.map +1 -1
  158. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  159. package/dist/usage/store.d.ts.map +1 -1
  160. package/dist/usage/store.js +5 -0
  161. package/dist/usage/store.js.map +1 -1
  162. package/docs/content/actions.md +23 -0
  163. package/docs/content/audit-log.md +111 -0
  164. package/package.json +2 -1
  165. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Types for the framework audit log.
3
+ *
4
+ * The audit log is a durable, complete, access-scoped, append-only record of
5
+ * *who mutated what app data, when, from where, and — when it was the agent —
6
+ * in which run*. It is deliberately distinct from observability (sampled
7
+ * agent-run telemetry) and tracking (fire-and-forget product analytics): audit
8
+ * rows are complete, locally queryable, and scoped to the data they describe.
9
+ *
10
+ * Capture happens automatically at the action-execution seam (`defineAction`):
11
+ * every mutating action records an audit event after it runs. Read-only (GET /
12
+ * `readOnly`) actions are skipped unless they opt in via `audit.onRead`.
13
+ */
14
+
15
+ /** Outcome of an audited action call. */
16
+ export type AuditStatus = "success" | "error" | "denied";
17
+
18
+ /**
19
+ * Who performed the mutation.
20
+ * - `agent` — the in-app assistant / sub-agents / A2A (caller `"tool"`).
21
+ * - `human` — a person via the UI, HTTP, CLI, or an external MCP client.
22
+ * - `system` — no resolved identity (background jobs, schedules).
23
+ */
24
+ export type AuditActorKind = "agent" | "human" | "system";
25
+
26
+ export type AuditVisibility = "private" | "org" | "public";
27
+
28
+ /**
29
+ * The resource an action mutated, plus the ownership used to scope who can
30
+ * read the resulting audit event. Returned by an action's `audit.target`.
31
+ *
32
+ * `ownerEmail` / `orgId` / `visibility` default to the actor's identity when
33
+ * omitted, which is correct for the common case where a user mutates their own
34
+ * data. Provide them explicitly when a user edits a resource owned by someone
35
+ * else so the *owner* sees the change in their audit trail.
36
+ */
37
+ export interface AuditTarget {
38
+ type?: string;
39
+ id?: string;
40
+ ownerEmail?: string | null;
41
+ orgId?: string | null;
42
+ visibility?: AuditVisibility;
43
+ }
44
+
45
+ /** Metadata about the call, passed to `audit.target` / `audit.summary`. */
46
+ export interface AuditCallMeta {
47
+ status: AuditStatus;
48
+ caller: string;
49
+ userEmail?: string;
50
+ orgId?: string | null;
51
+ }
52
+
53
+ /**
54
+ * Per-action audit configuration, set on `defineAction({ audit })`.
55
+ *
56
+ * Audit is **default-on for mutating actions** — you only need this object to
57
+ * *tune* capture (declare the target, customize the summary) or to opt a
58
+ * read-only action in / a noisy action out.
59
+ */
60
+ export interface ActionAuditConfig {
61
+ /**
62
+ * Force audit on (`true`) or off (`false`). Overrides every default,
63
+ * including the read-only skip and the high-frequency denylist. When
64
+ * omitted, mutating actions are audited and read-only actions are not.
65
+ */
66
+ enabled?: boolean;
67
+ /**
68
+ * Audit this action even though it is read-only (GET). Use for sensitive
69
+ * reads worth recording — secret access, bulk export. Ignored when
70
+ * `enabled` is set explicitly.
71
+ */
72
+ onRead?: boolean;
73
+ /** Capture the (redacted) call arguments. Default `true`. */
74
+ recordInputs?: boolean;
75
+ /** Resolve the mutated resource + its ownership for scoped reads. */
76
+ target?: (
77
+ args: any,
78
+ result: unknown,
79
+ meta: AuditCallMeta,
80
+ ) => AuditTarget | null | undefined;
81
+ /** Build a short human-readable summary line for the event. */
82
+ summary?: (args: any, result: unknown, meta: AuditCallMeta) => string;
83
+ }
84
+
85
+ /** A persisted audit event. */
86
+ export interface AuditEvent {
87
+ id: string;
88
+ createdAt: number;
89
+ /** Action name (the registry key, e.g. `delete-recording`). */
90
+ action: string;
91
+ /** Invocation surface: tool | frontend | http | cli | mcp | a2a. */
92
+ caller: string;
93
+ actorKind: AuditActorKind;
94
+ /** Human on whose behalf it ran — populated even for agent calls. */
95
+ actorEmail: string | null;
96
+ orgId: string | null;
97
+ /** Agent conversation thread, when caller is the agent loop. */
98
+ threadId: string | null;
99
+ /** Agent turn that produced the mutation — the unit of one agent response. */
100
+ turnId: string | null;
101
+ targetType: string | null;
102
+ targetId: string | null;
103
+ status: AuditStatus;
104
+ summary: string | null;
105
+ /** Redacted JSON of the call arguments, or null. */
106
+ input: string | null;
107
+ errorCode: string | null;
108
+ /** Denormalized owner used to scope reads (defaults to the actor). */
109
+ ownerEmail: string | null;
110
+ visibility: AuditVisibility;
111
+ }
112
+
113
+ /** Filters for `queryAuditEvents`. */
114
+ export interface AuditQueryFilters {
115
+ targetType?: string;
116
+ targetId?: string;
117
+ actorKind?: AuditActorKind;
118
+ actorEmail?: string;
119
+ status?: AuditStatus;
120
+ threadId?: string;
121
+ turnId?: string;
122
+ action?: string;
123
+ sinceMs?: number;
124
+ limit?: number;
125
+ }
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import { getDbExec, intType } from "../db/client.js";
3
+ import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
3
4
  import {
4
5
  mergeThreadDataForClientSave,
5
6
  normalizeThreadRepository,
@@ -87,6 +88,15 @@ async function ensureTable(): Promise<void> {
87
88
  // Column already exists.
88
89
  }
89
90
  }
91
+ // Widen millisecond-timestamp columns that older deployments created as
92
+ // 32-bit `INTEGER`; on Postgres the `Date.now()` written on every turn
93
+ // overflows int4. No-op once widened / on fresh BIGINT databases.
94
+ await widenIntColumnsToBigInt("chat_threads", [
95
+ "created_at",
96
+ "updated_at",
97
+ "pinned_at",
98
+ "archived_at",
99
+ ]);
90
100
  // Indexes for the hot read paths. Both the sidebar list and the
91
101
  // scoped/per-resource list filter on owner_email (and optionally
92
102
  // scope) and sort by updated_at. Keep these dialect-agnostic (no
@@ -151,6 +151,7 @@ import {
151
151
  getRepoMessages,
152
152
  getRepoMessage,
153
153
  shouldImportServerThreadData,
154
+ dedupeRepoMessagesById,
154
155
  } from "./chat/repo-helpers.js";
155
156
 
156
157
  export {
@@ -829,6 +830,11 @@ export function clearChatStorage(tabId?: string) {
829
830
  * messages missing these fields crash.
830
831
  */
831
832
  function ensureMessageMetadata(repo: any): any {
833
+ // Drop duplicate message ids before import — assistant-ui's MessageRepository
834
+ // throws "performOp/link: A message with the same id already exists in the
835
+ // parent tree" (Sentry AGENT-NATIVE-BROWSER-2Q) when fed repeated ids. No-op
836
+ // for the normal no-duplicate case. See dedupeRepoMessagesById.
837
+ repo = dedupeRepoMessagesById(repo);
832
838
  if (!repo?.messages || !Array.isArray(repo.messages)) return repo;
833
839
  for (const entry of repo.messages) {
834
840
  // Handle both wrapped ({ message: { ... } }) and flat ({ role, ... }) formats
@@ -56,6 +56,44 @@ export function getRepoMessage(entry: RepoEntry): RepoMessage | null {
56
56
  return (entry?.message ?? entry) as RepoMessage | null;
57
57
  }
58
58
 
59
+ /**
60
+ * Collapse duplicate message ids before a repository is handed to
61
+ * `threadRuntime.import()`. assistant-ui's `MessageRepository` throws
62
+ * "MessageRepository(performOp/link): A message with the same id already exists
63
+ * in the parent tree" when the imported messages contain the same id more than
64
+ * once (Sentry AGENT-NATIVE-BROWSER-2Q). Duplicate ids are never valid thread
65
+ * data — they come from optimistic+echo races, streaming reconnect replays, or
66
+ * multi-tab merges — so keep only the LAST occurrence of each id (the most
67
+ * recent, most complete copy). parentId references stay valid because the
68
+ * surviving entry keeps the same id.
69
+ *
70
+ * Returns the input unchanged (same reference) when there are no duplicates, so
71
+ * the overwhelmingly common no-dupe case is a cheap no-op with zero behavioural
72
+ * change for normal threads.
73
+ */
74
+ export function dedupeRepoMessagesById<T extends NormalizedRepo>(
75
+ repo: T | null | undefined,
76
+ ): T | null | undefined {
77
+ if (!repo || !Array.isArray(repo.messages)) return repo;
78
+ const entries = repo.messages;
79
+ const lastIndexById = new Map<string, number>();
80
+ let hasDuplicate = false;
81
+ entries.forEach((entry, index) => {
82
+ const id = getRepoMessage(entry)?.id;
83
+ if (typeof id !== "string" || !id) return;
84
+ if (lastIndexById.has(id)) hasDuplicate = true;
85
+ lastIndexById.set(id, index);
86
+ });
87
+ if (!hasDuplicate) return repo;
88
+ const deduped = entries.filter((entry, index) => {
89
+ const id = getRepoMessage(entry)?.id;
90
+ // Keep id-less entries untouched; for duplicated ids keep only the last.
91
+ if (typeof id !== "string" || !id) return true;
92
+ return lastIndexById.get(id) === index;
93
+ });
94
+ return { ...repo, messages: deduped };
95
+ }
96
+
59
97
  export function isAssistantMessageTerminal(
60
98
  message: RepoMessage | null,
61
99
  ): boolean {
@@ -318,6 +318,10 @@ export function intType(): string {
318
318
  return isPostgres() ? "BIGINT" : "INTEGER";
319
319
  }
320
320
 
321
+ // `widenIntColumnsToBigInt` lives in `./widen-columns.js` (it depends only on
322
+ // `isPostgres`/`getDbExec` from here) so stores can import it without every
323
+ // `vi.mock("./client.js")` test having to stub the export.
324
+
321
325
  // ---------------------------------------------------------------------------
322
326
  // Parameter conversion: ? -> $1, $2, $3
323
327
  // ---------------------------------------------------------------------------
@@ -0,0 +1,75 @@
1
+ /**
2
+ * In-place widening of legacy 32-bit `integer` columns to 64-bit `BIGINT` on
3
+ * Postgres.
4
+ *
5
+ * Lives in its own module (rather than `client.js`) so that stores can import
6
+ * it without every `vi.mock("../db/client.js")` test needing to stub it: the
7
+ * helper resolves `isPostgres()` / `getDbExec()` through `client.js`, so a test
8
+ * that mocks the client to SQLite (`isPostgres: () => false`) makes this a
9
+ * no-op automatically.
10
+ */
11
+
12
+ import { isPostgres, getDbExec, type DbExec } from "./client.js";
13
+
14
+ const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
15
+
16
+ /**
17
+ * Widen pre-existing 32-bit `integer` columns to 64-bit `BIGINT` in place on
18
+ * Postgres. No-op on SQLite, whose `INTEGER` is already 64-bit.
19
+ *
20
+ * Several stores historically created millisecond-timestamp columns (e.g.
21
+ * `agent_runs.started_at`, `application_state.updated_at`) as a literal
22
+ * `INTEGER`. On Postgres that is int4 (max 2,147,483,647), so a millisecond
23
+ * epoch such as 1782269273204 overflows with:
24
+ * `value "1782269273204" is out of range for type integer`
25
+ * The CREATE TABLE source was later switched to `intType()` (BIGINT on PG),
26
+ * but `CREATE TABLE IF NOT EXISTS` cannot re-type a column that already
27
+ * exists, so long-lived Neon databases keep the int4 column and every write
28
+ * into it fails. (Migrations don't hit this — the migration runner rewrites
29
+ * `INTEGER` → `BIGINT` for Postgres; only raw `ensureTable()` CREATE strings
30
+ * that predate `intType()` are affected.)
31
+ *
32
+ * This widens such columns once, then no-ops: it only ALTERs columns whose
33
+ * current type is `integer`, so already-bigint tables are never rewritten.
34
+ * Widening int4 → int8 is additive and non-destructive (no data loss, no
35
+ * narrowing), so it is safe to run unconditionally on every boot.
36
+ *
37
+ * Call from a store's `ensureTable()` right after its `CREATE TABLE IF NOT
38
+ * EXISTS`, passing the millisecond-timestamp columns for that table. Pass the
39
+ * UNQUALIFIED table name (the lookup is scoped to the `public` schema); the
40
+ * `ALTER` resolves the table through the search path.
41
+ */
42
+ export async function widenIntColumnsToBigInt(
43
+ table: string,
44
+ columns: string[],
45
+ // Injectable for tests; production callers use the configured client.
46
+ injectedClient?: DbExec,
47
+ ): Promise<void> {
48
+ if (!isPostgres() || columns.length === 0) return;
49
+ if (!PLAIN_IDENTIFIER.test(table)) return;
50
+ const client = injectedClient ?? getDbExec();
51
+ let int4Columns: Set<string>;
52
+ try {
53
+ const { rows } = await client.execute({
54
+ sql: `SELECT column_name FROM information_schema.columns
55
+ WHERE table_schema = 'public' AND table_name = ? AND data_type = 'integer'`,
56
+ args: [table],
57
+ });
58
+ int4Columns = new Set(rows.map((r) => String(r.column_name)));
59
+ } catch {
60
+ // information_schema unreadable (permissions / non-standard backend) —
61
+ // skip silently and leave the pre-existing behaviour unchanged.
62
+ return;
63
+ }
64
+ for (const col of columns) {
65
+ if (!int4Columns.has(col) || !PLAIN_IDENTIFIER.test(col)) continue;
66
+ try {
67
+ await client.execute(
68
+ `ALTER TABLE ${table} ALTER COLUMN ${col} TYPE BIGINT`,
69
+ );
70
+ } catch {
71
+ // A concurrent boot already widened it, or the role lacks ALTER — both
72
+ // are safe to ignore; a later boot retries if still needed.
73
+ }
74
+ }
75
+ }
@@ -1531,6 +1531,7 @@ export async function createMCPServerForRequest(
1531
1531
  userEmail: getRequestUserEmail(),
1532
1532
  orgId: getRequestOrgId() ?? null,
1533
1533
  caller: "mcp",
1534
+ actionName: name,
1534
1535
  });
1535
1536
  const mcpResult = isMcpActionResult(result) ? result : null;
1536
1537
  const rawResult = mcpResult ? mcpResult.raw : result;
@@ -1,4 +1,5 @@
1
1
  import { getDbExec, isPostgres, intType } from "../db/client.js";
2
+ import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
2
3
  import {
3
4
  encryptSecretValue,
4
5
  decryptSecretValue,
@@ -81,6 +82,11 @@ async function ensureTable(): Promise<void> {
81
82
  await client.execute(
82
83
  `UPDATE ${table} SET owner = account_id WHERE owner IS NULL`,
83
84
  );
85
+ // Older deployments have a 32-bit `updated_at`; on Postgres the
86
+ // `Date.now()` written on every token save overflows int4. Widen in place
87
+ // (no-op once done / on fresh DBs). Unqualified name — the helper scopes
88
+ // to the `public` schema.
89
+ await widenIntColumnsToBigInt("oauth_tokens", ["updated_at"]);
84
90
  })().catch((err) => {
85
91
  // Retry init on the next call after a failed startup.
86
92
  _initPromise = undefined;
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { getDbExec, isPostgres } from "../db/client.js";
24
+ import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
24
25
  import { isBlockedExtensionUrlWithDns } from "../extensions/url-safety.js";
25
26
 
26
27
  // ---------------------------------------------------------------------------
@@ -94,6 +95,13 @@ async function ensureTable(): Promise<void> {
94
95
  ? CREATE_SQL.replace(/\bINTEGER\b/g, "BIGINT")
95
96
  : CREATE_SQL;
96
97
  await client.execute(sql);
98
+ // Fresh Postgres tables get BIGINT via the replace above, but tables
99
+ // created before that compat existed kept int4 timestamp columns; widen
100
+ // them so `Date.now()` writes don't overflow. No-op once done.
101
+ await widenIntColumnsToBigInt("custom_api_providers", [
102
+ "created_at",
103
+ "updated_at",
104
+ ]);
97
105
  })().catch((err) => {
98
106
  _initPromise = undefined;
99
107
  throw err;
@@ -5,6 +5,7 @@ import {
5
5
  retryOnDdlRace,
6
6
  type DbExec,
7
7
  } from "../db/client.js";
8
+ import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
8
9
  import { emitResourceChange, emitResourceDelete } from "./emitter.js";
9
10
  import type { StoreWriteOptions } from "../settings/store.js";
10
11
  import {
@@ -781,6 +782,15 @@ async function _doEnsureTable(): Promise<void> {
781
782
  await ensureResourceColumn(client, `expires_at ${intType()}`);
782
783
  await ensureResourceColumn(client, "metadata TEXT");
783
784
 
785
+ // Older deployments have 32-bit timestamp columns; on Postgres the
786
+ // `Date.now()` written on insert/update overflows int4. Widen in place
787
+ // (no-op once done / on fresh DBs).
788
+ await widenIntColumnsToBigInt("resources", [
789
+ "created_at",
790
+ "updated_at",
791
+ "expires_at",
792
+ ]);
793
+
784
794
  // Seed default shared resources if they don't exist (INSERT OR IGNORE to avoid race conditions)
785
795
  const now = Date.now();
786
796
  const seedSql = isPostgres()
@@ -228,11 +228,14 @@ function parsePositionalJsonArg(args: string[]): Record<string, unknown> {
228
228
  * resolves `AGENT_USER_EMAIL` / the dev session); we never inject a dev
229
229
  * identity here beyond what that wrap already established.
230
230
  */
231
- function cliActionCtx(): import("../action.js").ActionRunContext {
231
+ function cliActionCtx(
232
+ actionName: string,
233
+ ): import("../action.js").ActionRunContext {
232
234
  return {
233
235
  userEmail: getRequestUserEmail(),
234
236
  orgId: getRequestOrgId() ?? null,
235
237
  caller: "cli",
238
+ actionName,
236
239
  };
237
240
  }
238
241
 
@@ -268,7 +271,7 @@ async function dispatchAction(
268
271
  typeof handler.run === "function"
269
272
  ) {
270
273
  const parsed = parseActionArgs(args, { coerceBooleans: true });
271
- const result = await handler.run(parsed, cliActionCtx());
274
+ const result = await handler.run(parsed, cliActionCtx(actionName));
272
275
  if (handler.readOnly !== true) {
273
276
  await notifyActionChange({ actionName }).catch(() => {});
274
277
  }
@@ -298,7 +301,7 @@ async function dispatchAction(
298
301
  const parsed = parseActionArgs(args, { coerceBooleans: true });
299
302
  const result = await packageAction.run(
300
303
  parsed as Record<string, string>,
301
- cliActionCtx(),
304
+ cliActionCtx(actionName),
302
305
  );
303
306
  if (packageAction.readOnly !== true) {
304
307
  await notifyActionChange({ actionName }).catch(() => {});
@@ -580,6 +580,12 @@ export async function mergeCoreSharingActions(
580
580
  () => import("../appearance/actions/change-appearance.js"),
581
581
  ],
582
582
  ["toggle-demo-mode", () => import("../demo/actions/toggle-demo-mode.js")],
583
+ // Audit log — read surface (who changed what, when, agent vs human).
584
+ [
585
+ "list-audit-events",
586
+ () => import("../audit/actions/list-audit-events.js"),
587
+ ],
588
+ ["get-audit-event", () => import("../audit/actions/get-audit-event.js")],
583
589
  // Org service tokens (CI credentials, e.g. PLAN_RECAP_TOKEN). Mint/revoke
584
590
  // are toolCallable:false — preserved via preserveActionFlags below.
585
591
  [
@@ -327,6 +327,7 @@ export function mountActionRoutes(
327
327
  userEmail,
328
328
  orgId: orgId ?? null,
329
329
  caller,
330
+ actionName: name,
330
331
  });
331
332
 
332
333
  // Auto-refresh the UI after a successful mutating action. GET
@@ -70,6 +70,7 @@ import {
70
70
  retryOnDdlRace,
71
71
  describeDbError,
72
72
  } from "../db/client.js";
73
+ import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
73
74
  import { getBetterAuth, getBetterAuthSync } from "./better-auth-instance.js";
74
75
  import type { BetterAuthConfig } from "./better-auth-instance.js";
75
76
  import { resolveGoogleSignInCredentials } from "./google-oauth-credentials.js";
@@ -869,6 +870,10 @@ async function ensureSessionTable(): Promise<void> {
869
870
  } catch {
870
871
  // Column already exists
871
872
  }
873
+ // Older deployments have a 32-bit `created_at`; on Postgres the
874
+ // `Date.now()` written on session create overflows int4. Widen in place
875
+ // (no-op once done / on fresh DBs).
876
+ await widenIntColumnsToBigInt("sessions", ["created_at"]);
872
877
  })().catch((err) => {
873
878
  // Don't cache the rejection — let the next caller retry a fresh init.
874
879
  _sessionInitPromise = undefined;
@@ -757,6 +757,20 @@ export function createCoreRoutesPlugin(
757
757
  // Observability module not available — skip
758
758
  }
759
759
 
760
+ // Audit log — durable, append-only record of who mutated what app data,
761
+ // when, and (for the agent) in which run. Capture is automatic at the
762
+ // action seam; here we just ensure the table exists and start the
763
+ // retention purge. Best-effort so a missing DB never crashes boot.
764
+ try {
765
+ const { ensureAuditTables } = await import("../audit/store.js");
766
+ const { startAuditCleanupJob } =
767
+ await import("../audit/cleanup-job.js");
768
+ ensureAuditTables().catch(() => {});
769
+ startAuditCleanupJob();
770
+ } catch {
771
+ // Audit module not available — skip
772
+ }
773
+
760
774
  const P = FRAMEWORK_ROUTE_PREFIX;
761
775
 
762
776
  // Security response headers — emitted on every framework response.
@@ -19,11 +19,14 @@
19
19
  * - `Referrer-Policy: strict-origin-when-cross-origin` — strips path/query
20
20
  * from outbound Referer headers when the request crosses origin, so a
21
21
  * public-share viewer's outbound link clicks never leak the share token.
22
- * - `Permissions-Policy: camera=(), microphone=(self), geolocation=(),
23
- * screen-wake-lock=()` — allows the app shell to request microphone access
24
- * for composer dictation while keeping camera/location/wake-lock blocked
25
- * by default. Templates that need broader media capture for recording UI
26
- * override this on their own routes.
22
+ * - `Permissions-Policy: camera=*, microphone=(self), geolocation=(),
23
+ * screen-wake-lock=()` — allows microphone access for composer dictation
24
+ * and camera access for media-capture UI (the Clips recorder, and the
25
+ * Clips browser extension's camera bubble, which is injected as a
26
+ * cross-origin iframe and is therefore blocked outright by `camera=()`).
27
+ * Location and wake-lock stay disabled. The browser still gates actual
28
+ * camera/mic use behind a per-origin permission prompt, so `camera=*` only
29
+ * removes the policy-level block, not the user consent.
27
30
  * - `Cross-Origin-Opener-Policy: same-origin` — isolates window.opener so
28
31
  * a popup-window opener reference can't read or modify our document.
29
32
  * - `Cross-Origin-Embedder-Policy: require-corp` — emitted only for
@@ -69,7 +72,7 @@ export function computeInlineScriptHash(scriptContent: string): string {
69
72
 
70
73
  const HSTS = "max-age=31536000; includeSubDomains; preload";
71
74
  const PERMISSIONS_POLICY =
72
- "camera=(), microphone=(self), geolocation=(), screen-wake-lock=()";
75
+ "camera=*, microphone=(self), geolocation=(), screen-wake-lock=()";
73
76
 
74
77
  /**
75
78
  * Returns true when the request was received over HTTPS. We trust both the
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "events";
2
2
  import { getDbExec, isPostgres, intType } from "../db/client.js";
3
+ import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
3
4
 
4
5
  let _initPromise: Promise<void> | undefined;
5
6
 
@@ -25,6 +26,11 @@ async function ensureTable(): Promise<void> {
25
26
  updated_at ${intType()} NOT NULL
26
27
  )
27
28
  `);
29
+ // Older deployments (pre BIGINT-compat) have a 32-bit `updated_at`; on
30
+ // Postgres the `Date.now()` written on every setSetting overflows int4.
31
+ // Widen in place (no-op once done / on fresh DBs). Pass the unqualified
32
+ // name — the helper scopes to the `public` schema.
33
+ await widenIntColumnsToBigInt("settings", ["updated_at"]);
28
34
  // Index for the poll watermark query: `SELECT MAX(updated_at) FROM settings`.
29
35
  // MAX on an indexed column avoids a full-table scan on every poll cycle.
30
36
  // IF NOT EXISTS makes it idempotent on existing databases.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: audit-log
3
+ description: >-
4
+ Durable, access-scoped, append-only record of who changed what app data,
5
+ when, and whether it was the agent or a human. Use when adding an activity
6
+ feed or change history, declaring what a mutating action targets, auditing
7
+ sensitive reads, or answering "what did the agent change / who edited this".
8
+ scope: dev
9
+ metadata:
10
+ internal: true
11
+ ---
12
+
13
+ # Audit Log
14
+
15
+ ## Rule
16
+
17
+ Every mutating action automatically records an audit event — **no wiring
18
+ needed**. The framework captures who/what/when/from-where at the `defineAction`
19
+ seam, redacts credentials, and attributes the change to a human or the agent
20
+ (with the agent thread/turn that caused it). You only touch `audit` config to
21
+ make events *more useful* (declare the target) or to opt a read in / a noisy
22
+ write out.
23
+
24
+ This is distinct from:
25
+
26
+ - **observability** — sampled agent-run telemetry (traces, evals), developer-facing.
27
+ - **tracking** — fire-and-forget product analytics to external SaaS.
28
+
29
+ Audit is complete, durable, locally queryable, and scoped to the data it
30
+ describes.
31
+
32
+ ## Declare the target so it lands in the owner's trail
33
+
34
+ By default an event is scoped to the **actor** (you see your own changes and the
35
+ agent's changes on your behalf). To make a change to a *shared* resource show up
36
+ in the **owner's** audit trail, declare the target:
37
+
38
+ ```ts
39
+ defineAction({
40
+ description: "Delete a recording",
41
+ schema: z.object({ id: z.string() }),
42
+ audit: {
43
+ // type + id label the event; ownerEmail/orgId/visibility scope who can read it.
44
+ target: (args, result, meta) => ({
45
+ type: "recording",
46
+ id: args.id,
47
+ // Optional — defaults to the actor. Set when editing someone else's resource.
48
+ ownerEmail: result?.ownerEmail,
49
+ visibility: "org",
50
+ }),
51
+ summary: (args) => `Deleted recording ${args.id}`,
52
+ },
53
+ run: async (args, ctx) => { /* ... */ },
54
+ });
55
+ ```
56
+
57
+ `target`, `ownerEmail`, `visibility`, and `summary` are all optional. The
58
+ minimum useful addition is `target: () => ({ type, id })`.
59
+
60
+ ## Defaults and how to override them
61
+
62
+ - **Mutations** (anything not GET / `readOnly`) are audited automatically.
63
+ - **Read-only** actions are skipped. Audit a sensitive read (secret access, bulk
64
+ export) with `audit: { onRead: true }`.
65
+ - **High-frequency framework actions** (app-state sync, context-xray, navigate,
66
+ appearance) are skipped by default. Force one on with `audit: { enabled: true }`.
67
+ - **Opt a noisy write out** with `audit: { enabled: false }`.
68
+ - **Skip capturing arguments** (large/sensitive payloads) with
69
+ `audit: { recordInputs: false }`. Inputs are credential-redacted regardless.
70
+
71
+ ## Reading the log
72
+
73
+ Two actions are available to the agent and the frontend in every app, scoped in
74
+ SQL to the caller — they never leak another tenant's rows:
75
+
76
+ - `list-audit-events` — filter by `targetType`/`targetId`, `actorKind`
77
+ (`agent` | `human` | `system`), `status`, `threadId`/`turnId`, `action`,
78
+ `sinceMs`, `limit`.
79
+ - `get-audit-event` — one event by id, with its redacted input payload.
80
+
81
+ Call them from the UI with `useActionQuery` to build an activity feed or a
82
+ "who changed this" line — never hand-write a fetch to the audit table.
83
+
84
+ ## Never
85
+
86
+ - Don't write a parallel "history" table for a resource — declare an `audit.target`
87
+ and read it back instead.
88
+ - Don't put secrets in `summary` or rely on inputs being safe — redaction covers
89
+ credential-shaped values, but keep summaries free of sensitive data.
90
+ - Don't expose an update/delete path for audit rows. The log is append-only; the
91
+ only deletion is the retention purge (`AGENT_NATIVE_AUDIT_RETENTION_DAYS`,
92
+ default 365 days; `0` = keep forever). Global kill switch:
93
+ `AGENT_NATIVE_AUDIT_ENABLED=false`.
@@ -8,6 +8,7 @@
8
8
  * Cost is stored as "centicents" (1/100th of a cent) for integer precision.
9
9
  */
10
10
  import { getDbExec, intType, isPostgres } from "../db/client.js";
11
+ import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
11
12
 
12
13
  /**
13
14
  * Per-million-token pricing in cents. Cache read is typically ~10% of
@@ -233,6 +234,11 @@ async function ensureUsageTable(): Promise<void> {
233
234
  }
234
235
  }
235
236
 
237
+ // Older deployments created `created_at` as 32-bit `INTEGER`; on Postgres
238
+ // the `Date.now()` written per run by recordUsage() overflows int4. Widen
239
+ // it in place (no-op once done / on fresh BIGINT databases).
240
+ await widenIntColumnsToBigInt("token_usage", ["created_at"]);
241
+
236
242
  try {
237
243
  await client.execute(
238
244
  `CREATE INDEX IF NOT EXISTS idx_token_usage_owner_created ON token_usage (owner_email, created_at)`,