@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +38 -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 +14 -0
- package/corpus/core/src/application-state/store.ts +5 -0
- package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
- package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
- package/corpus/core/src/audit/cleanup-job.ts +100 -0
- package/corpus/core/src/audit/config.ts +91 -0
- package/corpus/core/src/audit/index.ts +43 -0
- package/corpus/core/src/audit/record.ts +143 -0
- package/corpus/core/src/audit/redact.ts +109 -0
- package/corpus/core/src/audit/store.ts +244 -0
- package/corpus/core/src/audit/types.ts +125 -0
- package/corpus/core/src/chat-threads/store.ts +10 -0
- package/corpus/core/src/client/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/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 +14 -0
- 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
|
@@ -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(
|
|
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
|
[
|
|
@@ -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
|
|
23
|
-
* screen-wake-lock=()` — allows
|
|
24
|
-
* for
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
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)`,
|