@agent-native/core 0.144.3 → 0.145.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 (71) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/changelog/2026-08-06-desktop-full-screen-recordings-now-normalize-microphone-leve.md +6 -0
  3. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +185 -69
  4. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +47 -4
  5. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  6. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  7. package/dist/agent/production-agent.d.ts +6 -0
  8. package/dist/agent/run-manager.d.ts +44 -1
  9. package/dist/agent/run-manager.js +70 -8
  10. package/dist/chat-threads/store.js +6 -4
  11. package/dist/client/use-db-sync.js +9 -1
  12. package/dist/collab/awareness.d.ts +2 -2
  13. package/dist/collab/struct-routes.d.ts +1 -1
  14. package/dist/db/ddl-guard.d.ts +8 -0
  15. package/dist/db/ddl-guard.js +148 -0
  16. package/dist/deploy/build.d.ts +36 -0
  17. package/dist/deploy/build.js +78 -5
  18. package/dist/framework-tools.d.ts +126 -0
  19. package/dist/framework-tools.js +114 -0
  20. package/dist/notifications/routes.d.ts +3 -3
  21. package/dist/org/accept-pending.js +2 -2
  22. package/dist/org/auth-policy.js +4 -0
  23. package/dist/org/auto-join-domain.d.ts +9 -0
  24. package/dist/org/auto-join-domain.js +53 -4
  25. package/dist/org/context.js +12 -3
  26. package/dist/org/handlers.js +17 -5
  27. package/dist/org/request-org-cache.d.ts +24 -2
  28. package/dist/org/request-org-cache.js +57 -2
  29. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  30. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  31. package/dist/resources/store.d.ts +1 -1
  32. package/dist/resources/store.js +96 -7
  33. package/dist/secrets/routes.d.ts +9 -9
  34. package/dist/server/action-discovery.d.ts +27 -0
  35. package/dist/server/action-discovery.js +89 -0
  36. package/dist/server/agent-chat/action-filters-a2a.d.ts +11 -0
  37. package/dist/server/agent-chat/action-filters-a2a.js +17 -1
  38. package/dist/server/agent-chat/framework-prompts.d.ts +2 -0
  39. package/dist/server/agent-chat/plugin-options.d.ts +30 -0
  40. package/dist/server/agent-chat/prompt-resources.d.ts +4 -1
  41. package/dist/server/agent-chat/prompt-resources.js +8 -2
  42. package/dist/server/agent-chat-plugin.js +138 -75
  43. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  44. package/dist/server/auth.d.ts +3 -0
  45. package/dist/server/auth.js +22 -1
  46. package/dist/server/collab-plugin.js +10 -4
  47. package/dist/server/core-routes-plugin.js +3 -0
  48. package/dist/server/prompts/framework-core-compact.d.ts +5 -0
  49. package/dist/server/prompts/framework-core-compact.js +28 -12
  50. package/dist/server/prompts/framework-core.d.ts +5 -0
  51. package/dist/server/prompts/framework-core.js +33 -16
  52. package/dist/server/prompts/index.d.ts +1 -1
  53. package/dist/server/prompts/index.js +1 -1
  54. package/dist/server/prompts/shared-rules.d.ts +12 -4
  55. package/dist/server/prompts/shared-rules.js +14 -4
  56. package/dist/server/session-email-cache.d.ts +14 -0
  57. package/dist/server/session-email-cache.js +48 -0
  58. package/dist/server/sweep-runtime.d.ts +22 -0
  59. package/dist/server/sweep-runtime.js +28 -0
  60. package/dist/settings/store.js +63 -7
  61. package/dist/shared/ttl-cache.d.ts +59 -0
  62. package/dist/shared/ttl-cache.js +43 -0
  63. package/dist/sharing/actions/list-resource-shares.d.ts +3 -3
  64. package/dist/sharing/actions/list-resource-shares.js +4 -1
  65. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
  66. package/dist/triggers/condition-evaluator.js +11 -27
  67. package/docs/content/deployment.mdx +95 -13
  68. package/docs/content/environment-variables.mdx +5 -0
  69. package/docs/content/key-concepts.mdx +13 -6
  70. package/package.json +1 -1
  71. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
@@ -24,6 +24,7 @@ import { adoptThreadScopeIfUnscoped, createThread, forkThread, getThread, regist
24
24
  import { isCheckpointRestorePath } from "../checkpoints/route-match.js";
25
25
  import { createDbAdminAgentTools } from "../db-admin/agent-tools.js";
26
26
  import { isTransientDatabaseError } from "../db/client.js";
27
+ import { filterFrameworkToolGroups, resolveFrameworkTools, } from "../framework-tools.js";
27
28
  import { verifyInternalToken, extractBearerToken, } from "../integrations/internal-token.js";
28
29
  import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
29
30
  import { McpClientManager, loadMcpConfig, autoDetectMcpConfig, mcpToolsToActionEntries, syncMcpActionEntries, mountMcpServersRoutes, mountMcpHubRoutes, buildMergedConfig, startMcpConfigRefresh, getHubStatus, isHubServeEnabled, } from "../mcp-client/index.js";
@@ -116,6 +117,7 @@ import { isLocalhost, shouldBlockInProductCodeEditingSurface, } from "./agent-ch
116
117
  import { loadRunCodeToolEntries } from "./agent-chat/run-code-tools.js";
117
118
  import { createAgentEngineScriptEntries, createAgentLoopSettingsScriptEntries, createCallAgentScriptEntry, createChatScriptEntries, createDbScriptEntries, createDocsScriptEntries, createResourceScriptEntries, } from "./agent-chat/script-entries.js";
118
119
  import { parseSkillFrontmatter } from "./agent-chat/skill-frontmatter.js";
120
+ import { shouldDisableInProcessSweeps } from "./sweep-runtime.js";
119
121
  export { loadResourcesForPrompt };
120
122
  export { _agentChatPromptSectionsForTests };
121
123
  export { buildPublicAgentA2ASkills };
@@ -277,12 +279,19 @@ export function createAgentChatPlugin(options) {
277
279
  // `canToggle` means "this environment allows toggling" (static); this
278
280
  // function means "the user currently has dev mode ON" (live).
279
281
  const isDevMode = () => currentDevMode;
282
+ // Resolve the framework's own tool surface once, folding the deprecated
283
+ // `databaseTools` / `extensionTools` into `frameworkTools` so every
284
+ // consumer below reads a single shape. Conflicting old/new values throw
285
+ // here rather than booting with a surface nobody chose.
286
+ const frameworkTools = resolveFrameworkTools(options);
287
+ const disabledFrameworkGroups = frameworkTools.disabledGroups;
280
288
  // Build the four assembled system prompt strings. These are static for the
281
289
  // lifetime of this plugin instance — examples come from options once at
282
290
  // startup, not per-request.
283
291
  const { PROD_FRAMEWORK_PROMPT, DEV_FRAMEWORK_PROMPT, PROD_FRAMEWORK_PROMPT_COMPACT, DEV_FRAMEWORK_PROMPT_COMPACT, } = buildFrameworkPrompts(options?.promptExamples, {
284
- databaseTools: options?.databaseTools,
285
- extensionTools: options?.extensionTools,
292
+ databaseTools: frameworkTools.database,
293
+ extensionTools: frameworkTools.extensions,
294
+ disabledFrameworkGroups,
286
295
  });
287
296
  // Route readiness must not wait on settings scans, remote hub fetches, or
288
297
  // third-party MCP handshakes. Build the action surface against an empty
@@ -397,13 +406,23 @@ export function createAgentChatPlugin(options) {
397
406
  catch {
398
407
  // Package action registration is optional.
399
408
  }
400
- // Resource, chat, docs, db, and cross-agent scripts are available in both prod and dev modes
401
- const resourceScripts = await createResourceScriptEntries();
402
- const docsScripts = await createDocsScriptEntries();
403
- const databaseToolsMode = normalizeDatabaseToolsMode(options?.databaseTools);
409
+ // Resource, chat, docs, db, and cross-agent scripts are available in both
410
+ // prod and dev modes, unless the app switched the group off through
411
+ // `frameworkTools`. Gating at construction is deliberate: an empty map
412
+ // flows through all thirteen registry composition sites below, whereas a
413
+ // condition at each spread site is the same omission waiting to happen
414
+ // thirteen times. Matching HTTP routes are mounted from `httpActions`,
415
+ // which is built from the ungated sets so the UI keeps working.
416
+ const resourceScripts = frameworkTools.isEnabled("resources")
417
+ ? await createResourceScriptEntries()
418
+ : {};
419
+ const docsScripts = frameworkTools.isEnabled("docs")
420
+ ? await createDocsScriptEntries()
421
+ : {};
422
+ const databaseToolsMode = normalizeDatabaseToolsMode(frameworkTools.database);
404
423
  const databaseToolsEnabled = databaseToolsMode !== "off";
405
424
  const databaseWriteToolsEnabled = databaseToolsMode === "write";
406
- const extensionToolsEnabled = options?.extensionTools === true;
425
+ const extensionToolsEnabled = frameworkTools.extensions;
407
426
  const dbScripts = databaseToolsEnabled
408
427
  ? await createDbScriptEntries(databaseToolsMode, {
409
428
  extensionTools: extensionToolsEnabled,
@@ -417,12 +436,20 @@ export function createAgentChatPlugin(options) {
417
436
  const urlTools = createUrlTools();
418
437
  const engineScripts = await createAgentEngineScriptEntries(options?.appId);
419
438
  const loopSettingsScripts = await createAgentLoopSettingsScriptEntries();
420
- const chatScripts = {
421
- ...(await createChatScriptEntries()),
422
- ...engineScripts,
423
- ...loopSettingsScripts,
424
- };
425
- const callAgentScript = await createCallAgentScriptEntry(options?.appId);
439
+ // `httpActions` spreads engineScripts/loopSettingsScripts directly, so
440
+ // gating here removes them from the agent's tools without unmounting the
441
+ // routes the settings UI calls.
442
+ const chatScripts = frameworkTools.isEnabled("chat")
443
+ ? {
444
+ ...(await createChatScriptEntries()),
445
+ ...engineScripts,
446
+ ...loopSettingsScripts,
447
+ }
448
+ : {};
449
+ // Both `call-agent` and `describe-workspace-apps` come from this factory.
450
+ const callAgentScript = frameworkTools.isEnabled("workspaceApps")
451
+ ? await createCallAgentScriptEntry(options?.appId)
452
+ : {};
426
453
  let runNowSchedulerDeps = null;
427
454
  const browserTools = createBuilderBrowserTool({
428
455
  getOrigin: () => getRequestRunContext()?.requestOrigin ?? "http://localhost:3000",
@@ -582,7 +609,13 @@ export function createAgentChatPlugin(options) {
582
609
  // surface, prompt, MCP/A2A list, and job/trigger runner below. The full
583
610
  // `*All` sets are reserved for `httpActions`, which keeps agent-hidden
584
611
  // actions reachable from the frontend / HTTP.
585
- const templateScripts = filterAgentTools(templateScriptsAll);
612
+ //
613
+ // The framework's own action kits (sharing, review, history, flags, …)
614
+ // arrive in this same registry through `autoDiscoverActions`, tagged with
615
+ // `frameworkGroup`. Subtracting disabled groups HERE is what removes them
616
+ // from every downstream agent surface at once; `httpActions` re-merges
617
+ // them ungated, so the share dialog and review threads keep their routes.
618
+ const templateScripts = filterFrameworkToolGroups(filterAgentTools(templateScriptsAll), disabledFrameworkGroups);
586
619
  // Compact is the safe default for every app, including generated and
587
620
  // third-party apps that have not curated a starter list yet. Keep the
588
621
  // app's own action surface immediately callable; framework, provider,
@@ -592,7 +625,7 @@ export function createAgentChatPlugin(options) {
592
625
  // (below, after the run-code tools are built) additionally folds in
593
626
  // whatever `generateCorpusToolsPrompt` teaches by name for this request.
594
627
  const templateInitialToolNames = resolveInitialToolNames(templateScripts, options?.initialToolNames);
595
- const discoveredActions = filterAgentTools(discoveredActionsAll);
628
+ const discoveredActions = filterFrameworkToolGroups(filterAgentTools(discoveredActionsAll), disabledFrameworkGroups);
596
629
  // Per-request owner is read from the AsyncLocalStorage run context
597
630
  // (populated by prepareRun). Module-scope `let` would race across
598
631
  // concurrent requests on a long-lived Node process — overlapping
@@ -628,22 +661,29 @@ export function createAgentChatPlugin(options) {
628
661
  // Automation tools + fetch tool — depend on owner via callback.
629
662
  // Each callback short-circuits with a clear error when the run context
630
663
  // has no authenticated owner (see SECURITY note on getCurrentRunOwner).
664
+ const automationGroupEnabled = frameworkTools.isEnabled("automation");
631
665
  let automationTools = {};
632
666
  try {
633
- const { createAutomationToolEntries } = await import("../triggers/actions.js");
634
- automationTools = createAutomationToolEntries(() => requireCurrentRunOwner("manage automations"));
667
+ if (automationGroupEnabled) {
668
+ const { createAutomationToolEntries } = await import("../triggers/actions.js");
669
+ automationTools = createAutomationToolEntries(() => requireCurrentRunOwner("manage automations"));
670
+ }
635
671
  }
636
672
  catch { }
637
673
  let notificationTools = {};
638
674
  try {
639
- const { createNotificationToolEntries } = await import("../notifications/actions.js");
640
- notificationTools = createNotificationToolEntries(() => requireCurrentRunOwner("manage notifications"));
675
+ if (automationGroupEnabled) {
676
+ const { createNotificationToolEntries } = await import("../notifications/actions.js");
677
+ notificationTools = createNotificationToolEntries(() => requireCurrentRunOwner("manage notifications"));
678
+ }
641
679
  }
642
680
  catch { }
643
681
  let progressTools = {};
644
682
  try {
645
- const { createProgressToolEntries } = await import("../progress/actions.js");
646
- progressTools = createProgressToolEntries(() => requireCurrentRunOwner("manage progress"));
683
+ if (automationGroupEnabled) {
684
+ const { createProgressToolEntries } = await import("../progress/actions.js");
685
+ progressTools = createProgressToolEntries(() => requireCurrentRunOwner("manage progress"));
686
+ }
647
687
  }
648
688
  catch { }
649
689
  let githubRepoTools = {};
@@ -661,48 +701,53 @@ export function createAgentChatPlugin(options) {
661
701
  });
662
702
  }
663
703
  catch { }
704
+ const webGroupEnabled = frameworkTools.isEnabled("web");
664
705
  let fetchTool = {};
665
706
  try {
666
- const { createFetchToolEntry } = await import("../extensions/fetch-tool.js");
667
- // Resolve `${keys.NAME}` through the same request-scope cascade
668
- // already used by extension fetches (extensions/routes.ts) and
669
- // automation connector headers (automation/index.ts): user scope
670
- // first (personal overrides win), then the active org scope (the
671
- // Dispatch vault syncs workspace secrets here), then workspace
672
- // scope. Org/workspace vault rows are write-gated (org-admin +
673
- // Dispatch vault UI), so this is safe to read by default — unlike
674
- // the opt-in-only user→workspace fallback in resolveKeyReferences
675
- // (see audit 05 H2 in secrets/substitution.ts), which stays off.
676
- // Previously this tool only looked at scope "user", so a
677
- // ${keys.NAME} reference to a key synced into the org/workspace
678
- // vault could never resolve here even though the same key already
679
- // worked for extension fetches and automations.
680
- const { resolveKeyReferencesWithRequestScopes, validateUrlAllowlist, getKeyAllowlist, getResolvedKeyAllowlist, } = await import("../secrets/substitution.js");
681
- fetchTool = createFetchToolEntry({
682
- resolveKeys: async (text) => resolveKeyReferencesWithRequestScopes(text, requireCurrentRunOwner("resolve key references")),
683
- validateUrl: async (url, usedKeys, resolvedKeys) => {
684
- for (const keyName of usedKeys) {
685
- const allowlist = await resolveFetchToolKeyAllowlist(keyName, resolvedKeys, requireCurrentRunOwner("validate URL allowlist"), { getKeyAllowlist, getResolvedKeyAllowlist });
686
- if (allowlist && !validateUrlAllowlist(url, allowlist)) {
687
- return false;
707
+ if (webGroupEnabled) {
708
+ const { createFetchToolEntry } = await import("../extensions/fetch-tool.js");
709
+ // Resolve `${keys.NAME}` through the same request-scope cascade
710
+ // already used by extension fetches (extensions/routes.ts) and
711
+ // automation connector headers (automation/index.ts): user scope
712
+ // first (personal overrides win), then the active org scope (the
713
+ // Dispatch vault syncs workspace secrets here), then workspace
714
+ // scope. Org/workspace vault rows are write-gated (org-admin +
715
+ // Dispatch vault UI), so this is safe to read by default — unlike
716
+ // the opt-in-only user→workspace fallback in resolveKeyReferences
717
+ // (see audit 05 H2 in secrets/substitution.ts), which stays off.
718
+ // Previously this tool only looked at scope "user", so a
719
+ // ${keys.NAME} reference to a key synced into the org/workspace
720
+ // vault could never resolve here even though the same key already
721
+ // worked for extension fetches and automations.
722
+ const { resolveKeyReferencesWithRequestScopes, validateUrlAllowlist, getKeyAllowlist, getResolvedKeyAllowlist, } = await import("../secrets/substitution.js");
723
+ fetchTool = createFetchToolEntry({
724
+ resolveKeys: async (text) => resolveKeyReferencesWithRequestScopes(text, requireCurrentRunOwner("resolve key references")),
725
+ validateUrl: async (url, usedKeys, resolvedKeys) => {
726
+ for (const keyName of usedKeys) {
727
+ const allowlist = await resolveFetchToolKeyAllowlist(keyName, resolvedKeys, requireCurrentRunOwner("validate URL allowlist"), { getKeyAllowlist, getResolvedKeyAllowlist });
728
+ if (allowlist && !validateUrlAllowlist(url, allowlist)) {
729
+ return false;
730
+ }
688
731
  }
689
- }
690
- return true;
691
- },
692
- });
732
+ return true;
733
+ },
734
+ });
735
+ }
693
736
  }
694
737
  catch { }
695
738
  let webSearchTool = {};
696
739
  try {
697
- const { createWebSearchToolEntry } = await import("../extensions/web-search-tool.js");
698
- const { getBuilderWebSearchBaseUrl, resolveBuilderCredentials, resolveSecret, } = await import("./credential-provider.js");
699
- const { getBuilderGatewayRequestHeaders } = await import("../agent/engine/builder-gateway-headers.js");
700
- webSearchTool = createWebSearchToolEntry({
701
- resolveSecret,
702
- resolveBuilderCredentials,
703
- getBuilderWebSearchBaseUrl,
704
- getBuilderRequestHeaders: getBuilderGatewayRequestHeaders,
705
- });
740
+ if (webGroupEnabled) {
741
+ const { createWebSearchToolEntry } = await import("../extensions/web-search-tool.js");
742
+ const { getBuilderWebSearchBaseUrl, resolveBuilderCredentials, resolveSecret, } = await import("./credential-provider.js");
743
+ const { getBuilderGatewayRequestHeaders } = await import("../agent/engine/builder-gateway-headers.js");
744
+ webSearchTool = createWebSearchToolEntry({
745
+ resolveSecret,
746
+ resolveBuilderCredentials,
747
+ getBuilderWebSearchBaseUrl,
748
+ getBuilderRequestHeaders: getBuilderGatewayRequestHeaders,
749
+ });
750
+ }
706
751
  }
707
752
  catch { }
708
753
  let workspaceFilesTool = {};
@@ -752,11 +797,13 @@ export function createAgentChatPlugin(options) {
752
797
  let coreEmailTools = {};
753
798
  let backgroundCoreEmailTools = {};
754
799
  try {
755
- const { createCoreEmailActionEntries } = await import("./email-actions.js");
756
- coreEmailTools = createCoreEmailActionEntries();
757
- backgroundCoreEmailTools = createCoreEmailActionEntries({
758
- unattended: true,
759
- });
800
+ if (frameworkTools.isEnabled("email")) {
801
+ const { createCoreEmailActionEntries } = await import("./email-actions.js");
802
+ coreEmailTools = createCoreEmailActionEntries();
803
+ backgroundCoreEmailTools = createCoreEmailActionEntries({
804
+ unattended: true,
805
+ });
806
+ }
760
807
  }
761
808
  catch { }
762
809
  // Core read-attachment tool — always registered so the agent can page
@@ -1233,7 +1280,7 @@ export function createAgentChatPlugin(options) {
1233
1280
  const devActive = isDevMode();
1234
1281
  // Build the same system prompt the interactive agent uses
1235
1282
  const owner = userEmail;
1236
- const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId);
1283
+ const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId, undefined, { disabledFrameworkGroups });
1237
1284
  const schemaBlock = lazyContext
1238
1285
  ? ""
1239
1286
  : await buildSchemaBlock(owner, databaseToolsMode);
@@ -1701,7 +1748,7 @@ export function createAgentChatPlugin(options) {
1701
1748
  // catalog on its very first request, undermining the compact MCP
1702
1749
  // catalog this surface is supposed to keep external callers on.
1703
1750
  const mcpToolSurface = createA2AEngineToolSurface(actionsToEngineTools(mcpActions), effectiveInitialToolNames);
1704
- const resources = await loadResourcesForPrompt(SHARED_OWNER, lazyContext, options?.appId);
1751
+ const resources = await loadResourcesForPrompt(SHARED_OWNER, lazyContext, options?.appId, undefined, { disabledFrameworkGroups });
1705
1752
  const schemaBlock = lazyContext
1706
1753
  ? ""
1707
1754
  : await buildSchemaBlock(SHARED_OWNER, databaseToolsMode);
@@ -2156,8 +2203,10 @@ export function createAgentChatPlugin(options) {
2156
2203
  // Job management tool (manage-jobs)
2157
2204
  let jobTools = {};
2158
2205
  try {
2159
- const { createJobTools } = await import("../jobs/tools.js");
2160
- jobTools = createJobTools();
2206
+ if (automationGroupEnabled) {
2207
+ const { createJobTools } = await import("../jobs/tools.js");
2208
+ jobTools = createJobTools();
2209
+ }
2161
2210
  }
2162
2211
  catch { }
2163
2212
  // Lean mode: only template actions + essential framework tools. Drop
@@ -2487,7 +2536,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
2487
2536
  modelOverlay +
2488
2537
  runtimeContext);
2489
2538
  }
2490
- const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId);
2539
+ const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId, undefined, { disabledFrameworkGroups });
2491
2540
  // In lazy context mode, skip embedding the full schema. When database
2492
2541
  // tools are enabled the agent can call `db-schema` on demand.
2493
2542
  const schemaBlock = lazyContext
@@ -2720,7 +2769,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
2720
2769
  });
2721
2770
  return setSystemPromptOnContext(leanBasePrompt + extra + modelOverlay + runtimeContext);
2722
2771
  }
2723
- const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId);
2772
+ const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId, undefined, { disabledFrameworkGroups });
2724
2773
  const schemaBlock = lazyContext || !databaseToolsEnabled
2725
2774
  ? ""
2726
2775
  : await buildSchemaBlock(owner, databaseToolsMode);
@@ -4687,6 +4736,17 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4687
4736
  }));
4688
4737
  const isBackgroundRuntime = isInBackgroundFunctionRuntime();
4689
4738
  const disableRecurringJobsRuntime = isBackgroundRuntime || shouldDisableRecurringJobsRuntime();
4739
+ // Opt-in kill switch for the backstop sweep timers below. On serverless
4740
+ // these are billed per warm container, so their query rate scales with
4741
+ // instance count rather than with anything happening. See
4742
+ // `shouldDisableInProcessSweeps`.
4743
+ const sweepsDisabled = shouldDisableInProcessSweeps();
4744
+ if (sweepsDisabled) {
4745
+ console.log("[agent-native] In-process backstop sweeps disabled " +
4746
+ "(AGENT_NATIVE_DISABLE_INPROCESS_SWEEPS). A durable scheduler must " +
4747
+ "drive automation redispatch, agent-teams reconciliation, and " +
4748
+ "sandbox-execution recovery, or queued work will not be retried.");
4749
+ }
4690
4750
  // ─── Recurring Jobs Scheduler ──────────────────────────────────────
4691
4751
  // Poll every 60 seconds for due recurring jobs and execute them.
4692
4752
  // Uses setInterval so it works in all deployment environments without
@@ -4696,7 +4756,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4696
4756
  const schedulerDeps = {
4697
4757
  getActions: getBackgroundActionEntries,
4698
4758
  getSystemPrompt: async (owner) => {
4699
- const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId);
4759
+ const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId, undefined, { disabledFrameworkGroups });
4700
4760
  const schemaBlock = lazyContext
4701
4761
  ? ""
4702
4762
  : await buildSchemaBlock(owner, databaseToolsMode);
@@ -4712,8 +4772,11 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4712
4772
  // fetchTool/webSearchTool/toolActions catalog every tick.
4713
4773
  getInitialToolNames: (job) => [
4714
4774
  ...effectiveInitialToolNames,
4715
- "manage-jobs",
4716
- "manage-progress",
4775
+ // Only promotable while the group that supplies them is on; with
4776
+ // `automation` off the prompt no longer names them either.
4777
+ ...(automationGroupEnabled
4778
+ ? ["manage-jobs", "manage-progress"]
4779
+ : []),
4717
4780
  ...(job?.meta.mcpTools ?? []),
4718
4781
  ],
4719
4782
  apiKey: options?.apiKey,
@@ -4781,7 +4844,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4781
4844
  // A non-Netlify self-dispatch only waits for the request to leave the
4782
4845
  // current invocation. Keep the durable history row as a retryable queue
4783
4846
  // so a frozen serverless handoff is recovered on the next sweep.
4784
- if (!isBackgroundRuntime) {
4847
+ if (!isBackgroundRuntime && !sweepsDisabled) {
4785
4848
  (() => {
4786
4849
  let inFlight = false;
4787
4850
  const sweep = async () => {
@@ -4813,7 +4876,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4813
4876
  // per instance; cheap (one indexed query when no active tasks are found).
4814
4877
  // Throttled by the same per-owner interval guard inside reconcileAgentTeamRunsForOwner.
4815
4878
  (() => {
4816
- if (isBackgroundRuntime)
4879
+ if (isBackgroundRuntime || sweepsDisabled)
4817
4880
  return;
4818
4881
  // Track when this instance last ran the sweep so only one sweep fires
4819
4882
  // per 2-min window even if multiple timers fire in overlapping invocations.
@@ -4962,7 +5025,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4962
5025
  // comment above for why this exists and the timing budget in
4963
5026
  // run-store.ts's `UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS` doc comment.
4964
5027
  (() => {
4965
- if (isBackgroundRuntime)
5028
+ if (isBackgroundRuntime || sweepsDisabled)
4966
5029
  return;
4967
5030
  setTimeout(() => {
4968
5031
  (async () => {
@@ -5005,7 +5068,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
5005
5068
  // still fails loud, and it survives the fast sweep having missed a row
5006
5069
  // entirely (e.g. a restart landed between fast-sweep ticks).
5007
5070
  (() => {
5008
- if (isBackgroundRuntime)
5071
+ if (isBackgroundRuntime || sweepsDisabled)
5009
5072
  return;
5010
5073
  let lastSweep = 0;
5011
5074
  const SWEEP_INTERVAL_MS = 2 * 60 * 1000;
@@ -5073,7 +5136,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
5073
5136
  await initTriggerDispatcher({
5074
5137
  getActions: getBackgroundActionEntries,
5075
5138
  getSystemPrompt: async (owner) => {
5076
- const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId);
5139
+ const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId, undefined, { disabledFrameworkGroups });
5077
5140
  const schemaBlock = lazyContext
5078
5141
  ? ""
5079
5142
  : await buildSchemaBlock(owner, databaseToolsMode);
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -226,6 +226,9 @@ export declare function removeSession(token: string): Promise<void>;
226
226
  /**
227
227
  * Look up the email associated with a legacy session token.
228
228
  * Returns null if the session doesn't exist, is expired, or has no email.
229
+ *
230
+ * Resolutions are cached across requests — see `session-email-cache.ts` for the
231
+ * TTL and the only-cache-successes rule.
229
232
  */
230
233
  export declare function getSessionEmail(token: string): Promise<string | null>;
231
234
  export interface DesktopExchangeErrorPayload {
@@ -71,6 +71,7 @@ import { isIdentitySsoEnabled } from "./identity-sso-store.js";
71
71
  import { safeOAuthReturnUrl } from "./oauth-return-url.js";
72
72
  import { getOnboardingHtml, getResetPasswordHtml, } from "./onboarding-html.js";
73
73
  import { captureAuthError } from "./sentry.js";
74
+ import { forgetCachedSessionEmail, getCachedSessionEmail, invalidateSessionEmailCache, setCachedSessionEmail, } from "./session-email-cache.js";
74
75
  import { isWorkspaceOAuthCallbackRelayEnabled } from "./workspace-oauth.js";
75
76
  /**
76
77
  * Get the configured session max age. Desktop SSO broker writes from
@@ -741,6 +742,9 @@ export async function addSession(token, email) {
741
742
  : `INSERT OR REPLACE INTO sessions (token, email, created_at) VALUES (?, ?, ?)`,
742
743
  args: [token, email ?? null, Date.now()],
743
744
  }));
745
+ // The upsert can REBIND an existing token to a different email, so a cached
746
+ // resolution for it is now wrong.
747
+ invalidateSessionEmailCache();
744
748
  }
745
749
  export async function hasLegacySessionForEmail(email) {
746
750
  await ensureSessionTable();
@@ -759,12 +763,20 @@ export async function removeSession(token) {
759
763
  sql: `DELETE FROM sessions WHERE token = ?`,
760
764
  args: [token],
761
765
  }));
766
+ // Sign-out must take effect immediately, not after the cache TTL.
767
+ invalidateSessionEmailCache();
762
768
  }
763
769
  /**
764
770
  * Look up the email associated with a legacy session token.
765
771
  * Returns null if the session doesn't exist, is expired, or has no email.
772
+ *
773
+ * Resolutions are cached across requests — see `session-email-cache.ts` for the
774
+ * TTL and the only-cache-successes rule.
766
775
  */
767
776
  export async function getSessionEmail(token) {
777
+ const cached = getCachedSessionEmail(token);
778
+ if (cached !== undefined)
779
+ return cached;
768
780
  await ensureSessionTable();
769
781
  const client = getDbExec();
770
782
  const { rows } = await retryIfSessionsMissing(() => client.execute({
@@ -779,9 +791,13 @@ export async function getSessionEmail(token) {
779
791
  sql: `DELETE FROM sessions WHERE token = ?`,
780
792
  args: [token],
781
793
  });
794
+ forgetCachedSessionEmail(token);
782
795
  return null;
783
796
  }
784
- return rows[0].email ?? null;
797
+ const email = rows[0].email ?? null;
798
+ if (email)
799
+ setCachedSessionEmail(token, email);
800
+ return email;
785
801
  }
786
802
  // ---------------------------------------------------------------------------
787
803
  // getSession — the auth contract
@@ -937,6 +953,7 @@ async function consumeDesktopExchangeFromDB(flowId) {
937
953
  sql: `DELETE FROM sessions WHERE token = ? AND created_at > ? RETURNING email`,
938
954
  args: [`dex:${flowId}`, Date.now() - DESKTOP_EXCHANGE_TTL_MS],
939
955
  });
956
+ forgetCachedSessionEmail(`dex:${flowId}`);
940
957
  if (rows.length === 0)
941
958
  return null;
942
959
  const packed = (rows[0].email ?? rows[0][0]);
@@ -2706,6 +2723,9 @@ async function mountBetterAuthRoutes(app, options) {
2706
2723
  args: [userEmail],
2707
2724
  });
2708
2725
  }
2726
+ // Deleted by email, so the removed tokens are unknown here — the
2727
+ // whole cache goes.
2728
+ invalidateSessionEmailCache();
2709
2729
  }
2710
2730
  }
2711
2731
  catch {
@@ -2939,6 +2959,7 @@ async function mountBetterAuthRoutes(app, options) {
2939
2959
  catch {
2940
2960
  // Best-effort.
2941
2961
  }
2962
+ invalidateSessionEmailCache();
2942
2963
  // 3. Drop the current request's cookie and best-effort sign out
2943
2964
  // of Better Auth (so the response sets the proper expiry header).
2944
2965
  clearFrameworkSessionCookies(event);
@@ -211,8 +211,12 @@ export function createCollabPlugin(options = {}) {
211
211
  (action === "json" && method === "POST") ||
212
212
  (action === "patch" && method === "POST");
213
213
  if (isWrite) {
214
- // assertAccess throws ForbiddenError (→ 403) if no editor access
215
- const access = await assertAccess(resourceType, resourceId, "editor");
214
+ // assertAccess throws ForbiddenError (→ 403) if no editor access.
215
+ // Projected: only ownerEmail/orgId are read below, and a collab
216
+ // resource's body is the largest column it has — loading it here
217
+ // means every keystroke-driven update read the whole document
218
+ // twice, once for the ACL and once for the edit itself.
219
+ const access = await assertAccess(resourceType, resourceId, "editor", undefined, { skipResourceBody: true });
216
220
  const resource = access.resource;
217
221
  const awarenessScope = {
218
222
  resourceType,
@@ -229,8 +233,10 @@ export function createCollabPlugin(options = {}) {
229
233
  }
230
234
  }
231
235
  else {
232
- // resolveAccess returns null when no access; return 404 to avoid leaking existence
233
- const access = await resolveAccess(resourceType, resourceId);
236
+ // resolveAccess returns null when no access; return 404 to avoid leaking existence.
237
+ // Projected: only ownerEmail/orgId are read below, and a collab
238
+ // resource's body is the largest column it has.
239
+ const access = await resolveAccess(resourceType, resourceId, undefined, { skipResourceBody: true });
234
240
  if (!access) {
235
241
  setResponseStatus(event, 404);
236
242
  return { error: "Not found" };
@@ -57,6 +57,7 @@ import { createPollHandler } from "./poll.js";
57
57
  import { createRealtimeTokenHandler } from "./realtime-token.js";
58
58
  import { getRequestContext, hasRequestContext, runWithRequestContext, } from "./request-context.js";
59
59
  import { findUnsupportedScopedKeyNames, saveKeyValuesToScopedSecrets, ScopedKeyStorageError, } from "./scoped-key-storage.js";
60
+ import { shouldDisableInProcessSweeps } from "./sweep-runtime.js";
60
61
  import { createTranscribeVoiceHandler } from "./transcribe-voice.js";
61
62
  import { createVoiceProvidersStatusHandler } from "./voice-providers-status.js";
62
63
  import { createWorkspaceProviderOAuthHandler } from "./workspace-provider-oauth.js";
@@ -1260,6 +1261,8 @@ export function createCoreRoutesPlugin(options = {}) {
1260
1261
  // poll-time drain in run-code covers deployments where warm-instance
1261
1262
  // timers rarely fire.
1262
1263
  (() => {
1264
+ if (shouldDisableInProcessSweeps())
1265
+ return;
1263
1266
  let lastSweep = 0;
1264
1267
  const SWEEP_INTERVAL_MS = 2 * 60 * 1000;
1265
1268
  setTimeout(() => {
@@ -8,11 +8,16 @@
8
8
  * - Detailed "Parallel Tool Calls" prose (replaced with one-liner)
9
9
  * - Detailed "Resources" section (agent uses resources tool)
10
10
  */
11
+ import { type FrameworkToolGroup } from "../../framework-tools.js";
11
12
  import { type DatabaseToolsOption } from "../../scripts/db/tool-mode.js";
12
13
  import { type PromptExamples } from "./shared-rules.js";
13
14
  export interface FrameworkCoreCompactPromptOptions {
14
15
  databaseTools?: DatabaseToolsOption;
15
16
  extensionTools?: boolean;
17
+ /** Framework tool groups this app switched off. Every block below that names
18
+ * a group's tool by name is gated on this — a prompt naming an absent tool
19
+ * makes the model call it, fail, and often report the capability as missing. */
20
+ disabledFrameworkGroups?: ReadonlySet<FrameworkToolGroup>;
16
21
  }
17
22
  /**
18
23
  * Build the compact FRAMEWORK_CORE prompt string.