@agent-native/core 0.70.3 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +51 -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/docs/content/multi-app-workspace.md +2 -2
  6. package/corpus/core/docs/design/durable-agent-runs.md +458 -4
  7. package/corpus/core/package.json +2 -1
  8. package/corpus/core/src/action.ts +80 -1
  9. package/corpus/core/src/agent/durable-background.ts +192 -0
  10. package/corpus/core/src/agent/production-agent.ts +357 -13
  11. package/corpus/core/src/agent/run-manager.ts +66 -3
  12. package/corpus/core/src/agent/run-store.ts +143 -23
  13. package/corpus/core/src/agent/types.ts +20 -0
  14. package/corpus/core/src/application-state/store.ts +5 -0
  15. package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
  16. package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
  17. package/corpus/core/src/audit/cleanup-job.ts +100 -0
  18. package/corpus/core/src/audit/config.ts +91 -0
  19. package/corpus/core/src/audit/index.ts +43 -0
  20. package/corpus/core/src/audit/record.ts +143 -0
  21. package/corpus/core/src/audit/redact.ts +109 -0
  22. package/corpus/core/src/audit/store.ts +244 -0
  23. package/corpus/core/src/audit/types.ts +125 -0
  24. package/corpus/core/src/chat-threads/store.ts +10 -0
  25. package/corpus/core/src/client/AgentPanel.tsx +12 -9
  26. package/corpus/core/src/client/AssistantChat.tsx +6 -0
  27. package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
  28. package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
  29. package/corpus/core/src/db/client.ts +4 -0
  30. package/corpus/core/src/db/widen-columns.ts +75 -0
  31. package/corpus/core/src/deploy/build.ts +96 -0
  32. package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
  33. package/corpus/core/src/mcp/build-server.ts +23 -13
  34. package/corpus/core/src/oauth-tokens/store.ts +6 -0
  35. package/corpus/core/src/provider-api/custom-registry.ts +8 -0
  36. package/corpus/core/src/resources/store.ts +10 -0
  37. package/corpus/core/src/scripts/runner.ts +6 -3
  38. package/corpus/core/src/server/action-discovery.ts +6 -0
  39. package/corpus/core/src/server/action-routes.ts +1 -0
  40. package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
  41. package/corpus/core/src/server/auth.ts +5 -0
  42. package/corpus/core/src/server/core-routes-plugin.ts +14 -0
  43. package/corpus/core/src/server/security-headers.ts +9 -6
  44. package/corpus/core/src/settings/store.ts +6 -0
  45. package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  46. package/corpus/core/src/usage/store.ts +6 -0
  47. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
  48. package/corpus/templates/analytics/AGENTS.md +8 -0
  49. package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
  50. package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
  51. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
  52. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
  53. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
  54. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
  55. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
  56. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
  57. package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
  59. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
  60. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
  61. package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
  62. package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
  63. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
  64. package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
  65. package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
  66. package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
  67. package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
  68. package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
  69. package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
  70. package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
  71. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
  73. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
  74. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
  75. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
  76. package/corpus/templates/clips/desktop/src/styles.css +33 -5
  77. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  79. package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
  80. package/corpus/templates/clips/shared/recording-audio.ts +62 -0
  81. package/corpus/templates/clips/shared/recording-core.ts +94 -0
  82. package/dist/action.d.ts +31 -0
  83. package/dist/action.d.ts.map +1 -1
  84. package/dist/action.js +45 -1
  85. package/dist/action.js.map +1 -1
  86. package/dist/agent/context-xray/schema.d.ts +1 -1
  87. package/dist/agent/durable-background.d.ts +60 -0
  88. package/dist/agent/durable-background.d.ts.map +1 -0
  89. package/dist/agent/durable-background.js +144 -0
  90. package/dist/agent/durable-background.js.map +1 -0
  91. package/dist/agent/observational-memory/schema.d.ts +1 -1
  92. package/dist/agent/production-agent.d.ts +20 -0
  93. package/dist/agent/production-agent.d.ts.map +1 -1
  94. package/dist/agent/production-agent.js +320 -14
  95. package/dist/agent/production-agent.js.map +1 -1
  96. package/dist/agent/run-manager.d.ts +48 -0
  97. package/dist/agent/run-manager.d.ts.map +1 -1
  98. package/dist/agent/run-manager.js +45 -3
  99. package/dist/agent/run-manager.js.map +1 -1
  100. package/dist/agent/run-store.d.ts +30 -1
  101. package/dist/agent/run-store.d.ts.map +1 -1
  102. package/dist/agent/run-store.js +138 -24
  103. package/dist/agent/run-store.js.map +1 -1
  104. package/dist/agent/types.d.ts +20 -0
  105. package/dist/agent/types.d.ts.map +1 -1
  106. package/dist/agent/types.js.map +1 -1
  107. package/dist/application-state/store.d.ts.map +1 -1
  108. package/dist/application-state/store.js +5 -0
  109. package/dist/application-state/store.js.map +1 -1
  110. package/dist/audit/actions/get-audit-event.d.ts +11 -0
  111. package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
  112. package/dist/audit/actions/get-audit-event.js +22 -0
  113. package/dist/audit/actions/get-audit-event.js.map +1 -0
  114. package/dist/audit/actions/list-audit-events.d.ts +22 -0
  115. package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
  116. package/dist/audit/actions/list-audit-events.js +61 -0
  117. package/dist/audit/actions/list-audit-events.js.map +1 -0
  118. package/dist/audit/cleanup-job.d.ts +12 -0
  119. package/dist/audit/cleanup-job.d.ts.map +1 -0
  120. package/dist/audit/cleanup-job.js +93 -0
  121. package/dist/audit/cleanup-job.js.map +1 -0
  122. package/dist/audit/config.d.ts +30 -0
  123. package/dist/audit/config.d.ts.map +1 -0
  124. package/dist/audit/config.js +65 -0
  125. package/dist/audit/config.js.map +1 -0
  126. package/dist/audit/index.d.ts +13 -0
  127. package/dist/audit/index.d.ts.map +1 -0
  128. package/dist/audit/index.js +6 -0
  129. package/dist/audit/index.js.map +1 -0
  130. package/dist/audit/record.d.ts +24 -0
  131. package/dist/audit/record.d.ts.map +1 -0
  132. package/dist/audit/record.js +106 -0
  133. package/dist/audit/record.js.map +1 -0
  134. package/dist/audit/redact.d.ts +28 -0
  135. package/dist/audit/redact.d.ts.map +1 -0
  136. package/dist/audit/redact.js +109 -0
  137. package/dist/audit/redact.js.map +1 -0
  138. package/dist/audit/store.d.ts +14 -0
  139. package/dist/audit/store.d.ts.map +1 -0
  140. package/dist/audit/store.js +219 -0
  141. package/dist/audit/store.js.map +1 -0
  142. package/dist/audit/types.d.ts +114 -0
  143. package/dist/audit/types.d.ts.map +1 -0
  144. package/dist/audit/types.js +15 -0
  145. package/dist/audit/types.js.map +1 -0
  146. package/dist/chat-threads/store.d.ts.map +1 -1
  147. package/dist/chat-threads/store.js +10 -0
  148. package/dist/chat-threads/store.js.map +1 -1
  149. package/dist/client/AgentPanel.d.ts.map +1 -1
  150. package/dist/client/AgentPanel.js +2 -2
  151. package/dist/client/AgentPanel.js.map +1 -1
  152. package/dist/client/AssistantChat.d.ts.map +1 -1
  153. package/dist/client/AssistantChat.js +6 -1
  154. package/dist/client/AssistantChat.js.map +1 -1
  155. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  156. package/dist/client/blocks/library/FileTreeBlock.js +37 -4
  157. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  158. package/dist/client/chat/repo-helpers.d.ts +16 -0
  159. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  160. package/dist/client/chat/repo-helpers.js +40 -0
  161. package/dist/client/chat/repo-helpers.js.map +1 -1
  162. package/dist/db/client.d.ts.map +1 -1
  163. package/dist/db/client.js +3 -0
  164. package/dist/db/client.js.map +1 -1
  165. package/dist/db/widen-columns.d.ts +39 -0
  166. package/dist/db/widen-columns.d.ts.map +1 -0
  167. package/dist/db/widen-columns.js +73 -0
  168. package/dist/db/widen-columns.js.map +1 -0
  169. package/dist/deploy/build.d.ts +29 -0
  170. package/dist/deploy/build.d.ts.map +1 -1
  171. package/dist/deploy/build.js +85 -0
  172. package/dist/deploy/build.js.map +1 -1
  173. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  174. package/dist/deploy/workspace-deploy.js +108 -0
  175. package/dist/deploy/workspace-deploy.js.map +1 -1
  176. package/dist/mcp/build-server.d.ts.map +1 -1
  177. package/dist/mcp/build-server.js +24 -9
  178. package/dist/mcp/build-server.js.map +1 -1
  179. package/dist/oauth-tokens/store.d.ts.map +1 -1
  180. package/dist/oauth-tokens/store.js +6 -0
  181. package/dist/oauth-tokens/store.js.map +1 -1
  182. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  183. package/dist/provider-api/custom-registry.js +8 -0
  184. package/dist/provider-api/custom-registry.js.map +1 -1
  185. package/dist/resources/store.d.ts.map +1 -1
  186. package/dist/resources/store.js +9 -0
  187. package/dist/resources/store.js.map +1 -1
  188. package/dist/scripts/runner.js +4 -3
  189. package/dist/scripts/runner.js.map +1 -1
  190. package/dist/server/action-discovery.d.ts.map +1 -1
  191. package/dist/server/action-discovery.js +6 -0
  192. package/dist/server/action-discovery.js.map +1 -1
  193. package/dist/server/action-routes.d.ts.map +1 -1
  194. package/dist/server/action-routes.js +1 -0
  195. package/dist/server/action-routes.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +68 -13
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/auth.d.ts.map +1 -1
  200. package/dist/server/auth.js +5 -0
  201. package/dist/server/auth.js.map +1 -1
  202. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  203. package/dist/server/core-routes-plugin.js +13 -0
  204. package/dist/server/core-routes-plugin.js.map +1 -1
  205. package/dist/server/security-headers.d.ts +8 -5
  206. package/dist/server/security-headers.d.ts.map +1 -1
  207. package/dist/server/security-headers.js +9 -6
  208. package/dist/server/security-headers.js.map +1 -1
  209. package/dist/settings/store.d.ts.map +1 -1
  210. package/dist/settings/store.js +6 -0
  211. package/dist/settings/store.js.map +1 -1
  212. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  213. package/dist/usage/store.d.ts.map +1 -1
  214. package/dist/usage/store.js +5 -0
  215. package/dist/usage/store.js.map +1 -1
  216. package/docs/content/actions.md +23 -0
  217. package/docs/content/audit-log.md +111 -0
  218. package/docs/content/multi-app-workspace.md +2 -2
  219. package/docs/design/durable-agent-runs.md +458 -4
  220. package/package.json +2 -1
  221. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
@@ -32,6 +32,7 @@ import {
32
32
  collectImmutableAssetPaths,
33
33
  IMMUTABLE_ASSET_CACHE_HEADERS,
34
34
  } from "./immutable-assets.js";
35
+ import { AGENT_CHAT_PROCESS_RUN_PATH } from "../agent/durable-background.js";
35
36
 
36
37
  export type WorkspaceDeployPreset = "cloudflare_pages" | "netlify" | "vercel";
37
38
 
@@ -665,6 +666,126 @@ function copyNetlifyFunctionIntoWorkspace(
665
666
  fs.rmSync(dest, { recursive: true, force: true });
666
667
  copyDir(src, dest);
667
668
  patchNetlifyFunctionEntry(dest, app, workspaceApps, staticDir);
669
+
670
+ // Durable background agent runs (off by default). Additive ONLY: when the
671
+ // flag is off this emits nothing and the single-function deploy is unchanged.
672
+ if (isDurableBackgroundDeployEnabled()) {
673
+ emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Deploy-time gate for emitting the second `-background` Netlify function. Reads
679
+ * the same env flag the runtime gate uses (`AGENT_CHAT_DURABLE_BACKGROUND`).
680
+ * Off by default — when off, the deploy emits exactly one function per app
681
+ * (today's behavior, byte-for-byte).
682
+ */
683
+ function isDurableBackgroundDeployEnabled(): boolean {
684
+ const raw = process.env.AGENT_CHAT_DURABLE_BACKGROUND;
685
+ if (raw == null) return false;
686
+ const v = raw.trim().toLowerCase();
687
+ return v === "1" || v === "true" || v === "yes" || v === "on";
688
+ }
689
+
690
+ /**
691
+ * Emit a SECOND Netlify function for `app` whose name ends in `-background`,
692
+ * re-exporting the SAME `main.mjs` handler bundle. Netlify invokes any function
693
+ * whose deployed name ends in `-background` asynchronously (202 immediately, up
694
+ * to 15-min budget), which is exactly what the durable-background chat
695
+ * dispatch (`fireInternalDispatch` → AGENT_CHAT_PROCESS_RUN_PATH) needs.
696
+ *
697
+ * The function is given a `config.path` of the chat process-run route so
698
+ * Netlify routes that POST to this async function instead of the synchronous
699
+ * `<app>-server` function. It shares the same bundle (`includedFiles: ["**"]`)
700
+ * so `A2A_SECRET`, the DB URL, and the rest of the env/bundle are present.
701
+ *
702
+ * ⚠️ REAL-DEPLOY VERIFICATION REQUIRED. The exact Netlify routing/precedence
703
+ * between this `-background` function's `config.path` and the synchronous
704
+ * `<app>-server` function's broader `config.path` (which also matches
705
+ * `/_agent-native/*` for the dispatch app, or `/<app>/*` otherwise) cannot be
706
+ * verified in this environment. It is emitted additively and only under the
707
+ * flag; if Netlify resolves the overlap to the wrong function, the dispatch
708
+ * would land on the synchronous function (no 15-min budget) — the run would
709
+ * still work via the existing 40s soft-timeout path, just without the durable
710
+ * win. See packages/core/docs/design/durable-agent-runs.md.
711
+ */
712
+ function emitNetlifyBackgroundFunction(
713
+ workspaceRoot: string,
714
+ app: string,
715
+ srcServerDir: string,
716
+ workspaceApps: WorkspaceAppManifestEntry[],
717
+ ): void {
718
+ // Name MUST end in `-background` for Netlify async invocation.
719
+ const backgroundName = `${app}-agent-background`;
720
+ const dest = path.join(netlifyFunctionsDir(workspaceRoot), backgroundName);
721
+ fs.rmSync(dest, { recursive: true, force: true });
722
+ copyDir(srcServerDir, dest);
723
+
724
+ const basePath = `/${app}`;
725
+ const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
726
+ const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
727
+ workspaceApps,
728
+ app,
729
+ );
730
+ // Only the chat process-run route needs the async function. Keeping the path
731
+ // narrow avoids stealing any other traffic from the synchronous function.
732
+ // The dispatch URL is app-base-path-prefixed (same as the agent-teams
733
+ // processor dispatch), so the route Netlify must map to this async function
734
+ // is `/<app>/_agent-native/agent-chat/_process-run`.
735
+ const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
736
+ const pathConfig = [processRunPath];
737
+ const server = `const basePath = ${JSON.stringify(basePath)};
738
+
739
+ function setBasePathEnv() {
740
+ const processRef = globalThis.process ??= { env: {} };
741
+ processRef.env ??= {};
742
+ Object.assign(processRef.env, {
743
+ AGENT_NATIVE_WORKSPACE: "1",
744
+ AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
745
+ APP_BASE_PATH: basePath,
746
+ AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
747
+ AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
748
+ AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
749
+ VITE_AGENT_NATIVE_WORKSPACE: "1",
750
+ VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
751
+ VITE_APP_BASE_PATH: basePath,
752
+ VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
753
+ VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
754
+ VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
755
+ VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
756
+ ${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
757
+ });
758
+ }
759
+
760
+ setBasePathEnv();
761
+
762
+ let cachedHandler;
763
+
764
+ export default async function handler(...args) {
765
+ setBasePathEnv();
766
+ cachedHandler ??= (await import("./main.mjs")).default;
767
+ return cachedHandler(...args);
768
+ }
769
+
770
+ export const config = {
771
+ name: ${JSON.stringify(`${app} agent background handler`)},
772
+ generator: "agent-native workspace deploy",
773
+ path: ${JSON.stringify(pathConfig)},
774
+ nodeBundler: "none",
775
+ includedFiles: ["**"],
776
+ preferStatic: false,
777
+ };
778
+ `;
779
+ // Remove the original Nitro entry (server.mjs) so only our background entry
780
+ // is the function entrypoint, mirroring patchNetlifyFunctionEntry.
781
+ fs.rmSync(path.join(dest, "server.mjs"), { force: true });
782
+ fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), server);
783
+ console.log(
784
+ `[workspace-deploy] Emitted durable-background function "${backgroundName}" ` +
785
+ `for app "${app}" (path ${processRunPath}). ` +
786
+ `REQUIRES real-deploy verification of Netlify async routing — see ` +
787
+ `docs/design/durable-agent-runs.md.`,
788
+ );
668
789
  }
669
790
 
670
791
  function patchNetlifyFunctionEntry(
@@ -952,12 +952,13 @@ async function resolveMcpAppResource(
952
952
  ): Promise<ResolvedMcpAppResource | null> {
953
953
  const resource = entry.mcpApp?.resource;
954
954
  if (!resource) return null;
955
- // Inline MCP App embeds are gated behind the deploy-toggleable kill switch
956
- // (default off). When disabled, advertise no resource and attach no embed
957
- // reference so hosts fall back to the tool's deep-link text instead of an
958
- // iframe. This is the single chokepoint for every embed surface: tools/list
959
- // descriptor meta, tools/call result meta, resources/list, resources/read.
960
- if (!requestMeta?.inlineMcpApps) return null;
955
+ // NB: the inline kill switch is intentionally NOT enforced here. This
956
+ // resolver also backs `resources/read`, which must keep serving the shell
957
+ // for a URI the host already holds (e.g. a cached descriptor) so it degrades
958
+ // gracefully instead of throwing a hard `-32603`. The switch is enforced at
959
+ // the *advertisement/render* sites (`tools/list` descriptor meta,
960
+ // `tools/call` result meta, `resources/list`) so disabled embeds never get
961
+ // advertised in the first place.
961
962
  const resolvedUri = getMcpAppResourceUri(config, actionName, entry);
962
963
  if (!resolvedUri) return null;
963
964
  const description = resource.description ?? entry.tool.description;
@@ -1006,6 +1007,9 @@ async function getMcpAppResources(
1006
1007
  actions: Record<string, ActionEntry>,
1007
1008
  requestMeta?: MCPRequestMeta,
1008
1009
  ): Promise<ResolvedMcpAppResource[]> {
1010
+ // Advertisement path (resources/list + resources/templates/list): suppressed
1011
+ // by the inline kill switch so disabled embeds are never listed.
1012
+ if (!requestMeta?.inlineMcpApps) return [];
1009
1013
  const resources = await Promise.all(
1010
1014
  Object.entries(actions).map(([name, entry]) =>
1011
1015
  resolveMcpAppResourceSafely(config, name, entry, requestMeta),
@@ -1383,7 +1387,10 @@ export async function createMCPServerForRequest(
1383
1387
  : {};
1384
1388
  const toolMeta = {
1385
1389
  ...rawToolMeta,
1386
- ...(mcpAppResource
1390
+ // Advertisement path: only tag the tool with its inline-embed
1391
+ // descriptor when the kill switch is on, so disabled embeds never
1392
+ // prompt a host to render/read the `ui://` resource.
1393
+ ...(mcpAppResource && requestMeta?.inlineMcpApps
1387
1394
  ? {
1388
1395
  ...openAiToolDescriptorMeta(mcpAppResource),
1389
1396
  [MCP_APP_RESOURCE_URI_META_KEY]: mcpAppResource.uri,
@@ -1524,6 +1531,7 @@ export async function createMCPServerForRequest(
1524
1531
  userEmail: getRequestUserEmail(),
1525
1532
  orgId: getRequestOrgId() ?? null,
1526
1533
  caller: "mcp",
1534
+ actionName: name,
1527
1535
  });
1528
1536
  const mcpResult = isMcpActionResult(result) ? result : null;
1529
1537
  const rawResult = mcpResult ? mcpResult.raw : result;
@@ -1533,12 +1541,14 @@ export async function createMCPServerForRequest(
1533
1541
  !!mcpResult.raw &&
1534
1542
  typeof mcpResult.raw === "object" &&
1535
1543
  (mcpResult.raw as Record<string, unknown>).isError === true;
1536
- const mcpAppResource = await resolveMcpAppResourceSafely(
1537
- config,
1538
- name,
1539
- entry,
1540
- requestMeta,
1541
- );
1544
+ // Render path: only treat the result as an inline embed when the kill
1545
+ // switch is on. When off, `mcpAppResource` is null so every embed
1546
+ // branch below degrades to the plain deep-link artifacts the tool would
1547
+ // otherwise return — no `openai/outputTemplate`, no minted embed-start,
1548
+ // no embed structuredContent — so the host shows a link, not an iframe.
1549
+ const mcpAppResource = requestMeta?.inlineMcpApps
1550
+ ? await resolveMcpAppResourceSafely(config, name, entry, requestMeta)
1551
+ : null;
1542
1552
  const rawResultForClient = mcpAppResource
1543
1553
  ? await withServerMintedMcpAppEmbedStart(rawResult, requestMeta)
1544
1554
  : rawResult;
@@ -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
@@ -150,6 +150,10 @@ import {
150
150
  hasConfiguredA2ASecret,
151
151
  isA2AProductionRuntime,
152
152
  } from "../a2a/auth-policy.js";
153
+ import {
154
+ AGENT_CHAT_PROCESS_RUN_PATH,
155
+ prepareProcessRunRequest,
156
+ } from "../agent/durable-background.js";
153
157
  import {
154
158
  getBuilderBrowserConnectUrlForOwner,
155
159
  resolveBuilderBranchProjectId,
@@ -7695,6 +7699,134 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7695
7699
  }),
7696
7700
  );
7697
7701
 
7702
+ // Shared per-request invocation: resolve auth/org/timezone context, then
7703
+ // pick the dev/prod/anonymous handler and run it inside the request
7704
+ // context. Used by the main chat POST and by the durable-background
7705
+ // `_process-run` processor route (which re-enters the same handler set as
7706
+ // the background worker), so both go through identical context + handler
7707
+ // selection.
7708
+ const invokeAgentChatHandler = async (event: any) => {
7709
+ // Resolve per-request auth context
7710
+ const ownerContext = await resolveOwnerContext(event);
7711
+ const owner = ownerContext.owner;
7712
+
7713
+ // Resolve org ID: explicit callback > session.orgId from Better Auth
7714
+ // > implicit org membership. Better Auth leaves session.orgId null
7715
+ // until the user explicitly switches orgs, so a fresh signup with
7716
+ // implicit membership (e.g. domain-matched org) would otherwise see
7717
+ // no org-scoped credentials. getOrgContext() does the same DB lookup
7718
+ // the /builder/status endpoint uses to decide "Connected".
7719
+ let resolvedOrgId: string | undefined;
7720
+ if (options?.resolveOrgId) {
7721
+ resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
7722
+ } else {
7723
+ try {
7724
+ const session = await getSession(event);
7725
+ resolvedOrgId = session?.orgId ?? undefined;
7726
+ } catch {
7727
+ // Session not available
7728
+ }
7729
+ if (!resolvedOrgId) {
7730
+ try {
7731
+ const { getOrgContext } = await import("../org/context.js");
7732
+ const ctx = await getOrgContext(event);
7733
+ resolvedOrgId = ctx.orgId ?? undefined;
7734
+ } catch {
7735
+ // org_members table may not exist yet on first boot
7736
+ }
7737
+ }
7738
+ }
7739
+
7740
+ // Propagate the caller's IANA timezone from `x-user-timezone` so that
7741
+ // tool calls made by the agent (e.g. log-meal with no explicit date)
7742
+ // resolve "today" in the user's local timezone instead of server UTC.
7743
+ const tzRaw = getHeader(event, "x-user-timezone");
7744
+ const timezone =
7745
+ typeof tzRaw === "string" &&
7746
+ tzRaw.trim().length > 0 &&
7747
+ tzRaw.trim().length < 64
7748
+ ? tzRaw.trim()
7749
+ : undefined;
7750
+
7751
+ return runWithRequestContext(
7752
+ {
7753
+ userEmail: owner,
7754
+ userName: ownerContext.name,
7755
+ orgId: resolvedOrgId,
7756
+ timezone,
7757
+ },
7758
+ () => {
7759
+ // App-rendered chat can't host direct code edits — HMR/full
7760
+ // reloads would kill the same chat surface mid-run. Force the
7761
+ // prod handler (no shell / no fs); the prompt block injected by
7762
+ // `prodHandler.systemPrompt` then steers source changes to a
7763
+ // separate agent surface such as Builder or the dev frame.
7764
+ const blockInProductCodeEditing =
7765
+ shouldBlockInProductCodeEditing(event);
7766
+ const handler =
7767
+ ownerContext.anonymous && anonymousHandler
7768
+ ? anonymousHandler
7769
+ : !blockInProductCodeEditing && currentDevMode && devHandler
7770
+ ? devHandler
7771
+ : prodHandler;
7772
+ return handler(event);
7773
+ },
7774
+ );
7775
+ };
7776
+
7777
+ // ─── Durable background agent-chat run processor ──────────────────────
7778
+ // Self-fire target for a long chat turn. The foreground POST claims the
7779
+ // run slot, inserts the run row, and `fireInternalDispatch`es here; this
7780
+ // route runs INSIDE the Netlify background function (15-min budget). It
7781
+ // HMAC-verifies the dispatch (same internal-token scheme as the agent-
7782
+ // teams / A2A / webhook processors), injects the background-run marker,
7783
+ // and re-enters the SAME agent-chat handler as the background worker,
7784
+ // which runs the full multi-step turn inline with the ~13min soft
7785
+ // timeout. With AGENT_CHAT_DURABLE_BACKGROUND off, the foreground never
7786
+ // dispatches here, so this route is never exercised.
7787
+ getH3App(nitroApp).use(
7788
+ AGENT_CHAT_PROCESS_RUN_PATH,
7789
+ defineEventHandler(async (event) => {
7790
+ if (getMethod(event) !== "POST") {
7791
+ setResponseStatus(event, 405);
7792
+ return { error: "Method not allowed" };
7793
+ }
7794
+ // Consume the body ONCE (h3 v2's web Request stream is single-use).
7795
+ let processBody: any;
7796
+ try {
7797
+ processBody = await readBody(event);
7798
+ } catch {
7799
+ setResponseStatus(event, 400);
7800
+ return { error: "Invalid request body" };
7801
+ }
7802
+
7803
+ // Validate + HMAC-authenticate the self-dispatch and prepare the
7804
+ // background-worker body. Pure decision (unit-tested in
7805
+ // durable-background.spec.ts); the route only wires it to h3.
7806
+ const prepared = prepareProcessRunRequest(
7807
+ processBody,
7808
+ getHeader(event, "authorization"),
7809
+ );
7810
+ if (!prepared.ok) {
7811
+ setResponseStatus(event, prepared.status);
7812
+ return { error: prepared.error };
7813
+ }
7814
+
7815
+ // Stash the verified+augmented body for the handler — the body stream
7816
+ // is already consumed, so the handler reads this instead.
7817
+ (event as any).context = (event as any).context ?? {};
7818
+ (event as any).context.__agentChatBackgroundBody = prepared.body;
7819
+
7820
+ try {
7821
+ return await invokeAgentChatHandler(event);
7822
+ } catch (err: any) {
7823
+ console.error("[agent-chat] _process-run failed:", err);
7824
+ setResponseStatus(event, 500);
7825
+ return { error: "process-run failed" };
7826
+ }
7827
+ }),
7828
+ );
7829
+
7698
7830
  // Mount the main chat handler — delegates to dev or prod handler based on current mode.
7699
7831
  // This is mounted last because h3's use() is prefix-based, meaning /_agent-native/agent-chat
7700
7832
  // also matches /_agent-native/agent-chat/threads/... — we skip sub-path requests here so the
@@ -7713,72 +7845,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
7713
7845
  return { error: "Not found" };
7714
7846
  }
7715
7847
 
7716
- // Resolve per-request auth context
7717
- const ownerContext = await resolveOwnerContext(event);
7718
- const owner = ownerContext.owner;
7719
-
7720
- // Resolve org ID: explicit callback > session.orgId from Better Auth
7721
- // > implicit org membership. Better Auth leaves session.orgId null
7722
- // until the user explicitly switches orgs, so a fresh signup with
7723
- // implicit membership (e.g. domain-matched org) would otherwise see
7724
- // no org-scoped credentials. getOrgContext() does the same DB lookup
7725
- // the /builder/status endpoint uses to decide "Connected".
7726
- let resolvedOrgId: string | undefined;
7727
- if (options?.resolveOrgId) {
7728
- resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
7729
- } else {
7730
- try {
7731
- const session = await getSession(event);
7732
- resolvedOrgId = session?.orgId ?? undefined;
7733
- } catch {
7734
- // Session not available
7735
- }
7736
- if (!resolvedOrgId) {
7737
- try {
7738
- const { getOrgContext } = await import("../org/context.js");
7739
- const ctx = await getOrgContext(event);
7740
- resolvedOrgId = ctx.orgId ?? undefined;
7741
- } catch {
7742
- // org_members table may not exist yet on first boot
7743
- }
7744
- }
7745
- }
7746
-
7747
- // Propagate the caller's IANA timezone from `x-user-timezone` so that
7748
- // tool calls made by the agent (e.g. log-meal with no explicit date)
7749
- // resolve "today" in the user's local timezone instead of server UTC.
7750
- const tzRaw = getHeader(event, "x-user-timezone");
7751
- const timezone =
7752
- typeof tzRaw === "string" &&
7753
- tzRaw.trim().length > 0 &&
7754
- tzRaw.trim().length < 64
7755
- ? tzRaw.trim()
7756
- : undefined;
7757
-
7758
- return runWithRequestContext(
7759
- {
7760
- userEmail: owner,
7761
- userName: ownerContext.name,
7762
- orgId: resolvedOrgId,
7763
- timezone,
7764
- },
7765
- () => {
7766
- // App-rendered chat can't host direct code edits — HMR/full
7767
- // reloads would kill the same chat surface mid-run. Force the
7768
- // prod handler (no shell / no fs); the prompt block injected by
7769
- // `prodHandler.systemPrompt` then steers source changes to a
7770
- // separate agent surface such as Builder or the dev frame.
7771
- const blockInProductCodeEditing =
7772
- shouldBlockInProductCodeEditing(event);
7773
- const handler =
7774
- ownerContext.anonymous && anonymousHandler
7775
- ? anonymousHandler
7776
- : !blockInProductCodeEditing && currentDevMode && devHandler
7777
- ? devHandler
7778
- : prodHandler;
7779
- return handler(event);
7780
- },
7781
- );
7848
+ return invokeAgentChatHandler(event);
7782
7849
  }),
7783
7850
  );
7784
7851
 
@@ -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.