@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
@@ -75,7 +75,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- id?: undefined;
78
+ message?: undefined;
79
79
  deleted?: undefined;
80
80
  found?: undefined;
81
81
  providers: {
@@ -90,19 +90,20 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
90
90
  count: number;
91
91
  provider?: undefined;
92
92
  registered?: undefined;
93
+ id?: undefined;
93
94
  label?: undefined;
94
- message?: undefined;
95
95
  } | {
96
- id?: undefined;
96
+ message?: undefined;
97
97
  deleted?: undefined;
98
98
  providers?: undefined;
99
99
  count?: undefined;
100
100
  found: boolean;
101
101
  provider: import("../custom-registry.js").CustomProviderConfig;
102
102
  registered?: undefined;
103
+ id?: undefined;
103
104
  label?: undefined;
104
- message?: undefined;
105
105
  } | {
106
+ message?: undefined;
106
107
  deleted?: undefined;
107
108
  providers?: undefined;
108
109
  count?: undefined;
@@ -111,8 +112,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
111
112
  id: string;
112
113
  registered?: undefined;
113
114
  label?: undefined;
114
- message?: undefined;
115
115
  } | {
116
+ message?: undefined;
116
117
  found?: undefined;
117
118
  providers?: undefined;
118
119
  count?: undefined;
@@ -121,7 +122,6 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
121
122
  id: string;
122
123
  registered?: undefined;
123
124
  label?: undefined;
124
- message?: undefined;
125
125
  } | {
126
126
  deleted?: undefined;
127
127
  found?: undefined;
@@ -311,9 +311,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
311
311
  notes?: string | undefined;
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
+ deleted?: undefined;
314
315
  id?: undefined;
315
316
  provider?: undefined;
316
- deleted?: undefined;
317
317
  found?: undefined;
318
318
  providers: {
319
319
  id: string;
@@ -329,9 +329,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
329
329
  label?: undefined;
330
330
  message?: undefined;
331
331
  } | {
332
- id?: undefined;
333
332
  deleted?: undefined;
334
333
  count?: undefined;
334
+ id?: undefined;
335
335
  providers?: undefined;
336
336
  found: boolean;
337
337
  provider: import("../custom-registry.js").CustomProviderConfig;
@@ -339,9 +339,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
339
339
  label?: undefined;
340
340
  message?: undefined;
341
341
  } | {
342
- provider?: undefined;
343
342
  deleted?: undefined;
344
343
  count?: undefined;
344
+ provider?: undefined;
345
345
  providers?: undefined;
346
346
  found: boolean;
347
347
  id: string;
@@ -349,9 +349,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
349
349
  label?: undefined;
350
350
  message?: undefined;
351
351
  } | {
352
+ count?: undefined;
352
353
  provider?: undefined;
353
354
  found?: undefined;
354
- count?: undefined;
355
355
  providers?: undefined;
356
356
  deleted: boolean;
357
357
  id: string;
@@ -359,10 +359,10 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
359
359
  label?: undefined;
360
360
  message?: undefined;
361
361
  } | {
362
- provider?: undefined;
363
362
  deleted?: undefined;
364
- found?: undefined;
365
363
  count?: undefined;
364
+ provider?: undefined;
365
+ found?: undefined;
366
366
  providers?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
@@ -1,5 +1,5 @@
1
1
  import { isLocalWorkspaceResourceId } from "../local-artifacts/index.js";
2
- import type { StoreWriteOptions } from "../settings/store.js";
2
+ import { type StoreWriteOptions } from "../settings/store.js";
3
3
  export declare const SHARED_OWNER = "__shared__";
4
4
  export declare const WORKSPACE_OWNER = "__workspace__";
5
5
  /**
@@ -4,6 +4,7 @@ import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db
4
4
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
5
5
  import { canUseLocalWorkspaceResourcePath, deleteLocalWorkspaceResource, isLocalWorkspaceResourceId, isLocalWorkspaceResourcesEnabled, listLocalWorkspaceResources, localWorkspaceResourcePathFromId, readLocalWorkspaceResource, writeLocalWorkspaceResource, } from "../local-artifacts/index.js";
6
6
  import { getRequestOrgId, getRequestUserEmail, } from "../server/request-context.js";
7
+ import { getSetting, putSetting, } from "../settings/store.js";
7
8
  import { emitResourceChange, emitResourceDelete } from "./emitter.js";
8
9
  export const SHARED_OWNER = "__shared__";
9
10
  export const WORKSPACE_OWNER = "__workspace__";
@@ -570,14 +571,34 @@ async function grantedWorkspaceResourceByPath(path, options) {
570
571
  return null;
571
572
  }
572
573
  }
573
- async function cleanupExpiredAgentScratchResources(client) {
574
+ /**
575
+ * Expire agent scratch resources, WITHOUT blocking the read that triggered it.
576
+ *
577
+ * This is garbage collection, not part of any read's answer: a caller asking for
578
+ * one resource does not need yesterday's expired scratch rows gone first. Awaiting
579
+ * it made every read path issue a `DELETE` before its own `SELECT` — double the
580
+ * round trips, unservable by a replica, taking row locks inside a request a user
581
+ * is waiting on, and (since nothing caught it) able to fail the read outright.
582
+ *
583
+ * The throttle is a module-scope timestamp, so it starts at 0 in a fresh isolate
584
+ * and the first resource read after every cold start still triggers one sweep.
585
+ * That is why the index below exists, and why this must not be awaited.
586
+ */
587
+ function scheduleExpiredAgentScratchCleanup(client) {
574
588
  const now = Date.now();
575
589
  if (now - _lastScratchCleanupAt < SCRATCH_CLEANUP_INTERVAL_MS)
576
590
  return;
577
591
  _lastScratchCleanupAt = now;
578
- await client.execute({
592
+ void client
593
+ .execute({
579
594
  sql: `DELETE FROM resources WHERE visibility = ? AND expires_at IS NOT NULL AND expires_at <= ?`,
580
595
  args: ["agent_scratch", now],
596
+ })
597
+ .catch((err) => {
598
+ // Say so rather than swallowing: scratch rows accumulating forever is a
599
+ // slow leak nobody would otherwise notice.
600
+ // coercion-ok: failed GC degrades storage over time, never read correctness
601
+ console.warn("[resources] expired agent-scratch cleanup failed; will retry on a later read:", err?.message ?? err);
581
602
  });
582
603
  }
583
604
  async function ensureTable() {
@@ -655,7 +676,7 @@ async function _doEnsureTable() {
655
676
  "updated_at",
656
677
  "expires_at",
657
678
  ]);
658
- // `cleanupExpiredAgentScratchResources` filters on exactly these two columns
679
+ // `scheduleExpiredAgentScratchCleanup` filters on exactly these two columns
659
680
  // and, without this, full-scans `resources` — a table shared by every
660
681
  // template and read from agent discovery, docs, chat scratch and remote-agent
661
682
  // manifests. Its 60s throttle is a module-scope timestamp that starts at 0 in
@@ -669,7 +690,20 @@ async function _doEnsureTable() {
669
690
  // coercion-ok: absence of an index degrades latency, never correctness
670
691
  console.warn("[resources] could not ensure resources_visibility_expires_idx; scratch cleanup will full-scan:", err?.message ?? err);
671
692
  });
672
- // Seed default shared resources if they don't exist (INSERT OR IGNORE to avoid race conditions)
693
+ // Seed default shared resources if they don't exist (INSERT OR IGNORE to avoid
694
+ // race conditions).
695
+ //
696
+ // Guarded by a durable marker: `_doEnsureTable` runs once per PROCESS, which on
697
+ // serverless is once per cold start, so this block was issuing ~10 writes and
698
+ // 2 migration scans per container to insert rows that had existed since day
699
+ // one (53,785 of them in one production sample). The marker makes it once per
700
+ // database, matching what the comments here already assumed.
701
+ //
702
+ // Consequence worth knowing: a default resource the user DELETES is no longer
703
+ // recreated on the next cold start. That silent resurrection was never
704
+ // intended — see `RESOURCE_SEED_VERSION` to force a re-seed.
705
+ if (await alreadySeeded(SHARED_SEED_KEY))
706
+ return;
673
707
  const now = Date.now();
674
708
  const seedSql = isPostgres()
675
709
  ? `INSERT INTO resources (id, path, owner, content, mime_type, size, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (path, owner) DO NOTHING`
@@ -797,8 +831,54 @@ async function _doEnsureTable() {
797
831
  catch {
798
832
  // Migration best-effort
799
833
  }
834
+ await markSeeded(SHARED_SEED_KEY);
800
835
  }
836
+ /**
837
+ * Bump when a default resource is ADDED, or when existing seed content must
838
+ * reach databases that already ran an earlier seed.
839
+ *
840
+ * The marker below records which version a database has been seeded at, so the
841
+ * seed runs once per database instead of once per process. Without a version, a
842
+ * newly added default would never reach an already-seeded deployment.
843
+ */
844
+ const RESOURCE_SEED_VERSION = 1;
845
+ /**
846
+ * Per-process memo ON TOP of the durable marker, so a warm process doesn't even
847
+ * pay the marker read. The durable marker is what makes this correct across cold
848
+ * starts; this only avoids a repeat lookup within one process.
849
+ */
801
850
  const _personalSeeded = new Set();
851
+ function personalSeedKey(owner) {
852
+ return `resources-seeded:personal:v${RESOURCE_SEED_VERSION}:${owner.toLowerCase()}`;
853
+ }
854
+ const SHARED_SEED_KEY = `resources-seeded:shared:v${RESOURCE_SEED_VERSION}`;
855
+ /**
856
+ * Has this database already been seeded at the current version?
857
+ *
858
+ * A failed marker read returns `false` — "seed again" — because the seeds are
859
+ * `ON CONFLICT DO NOTHING` and therefore idempotent. Guessing "already seeded"
860
+ * on an unreadable marker would skip seeding a fresh database and leave it
861
+ * without its default AGENTS.md / LEARNINGS.md, which is not recoverable by
862
+ * retrying a read.
863
+ */
864
+ async function alreadySeeded(key) {
865
+ try {
866
+ const row = await getSetting(key);
867
+ return row?.at !== undefined;
868
+ }
869
+ catch {
870
+ // coercion-ok: "not seeded" is the SAFE answer, and it is not indistinguishable from success — it re-runs an idempotent ON CONFLICT DO NOTHING seed, exactly the pre-marker behaviour. Guessing "already seeded" would leave a fresh database permanently without its default AGENTS.md / LEARNINGS.md, which no retry recovers.
871
+ return false;
872
+ }
873
+ }
874
+ async function markSeeded(key) {
875
+ try {
876
+ await putSetting(key, { at: Date.now() });
877
+ }
878
+ catch {
879
+ // coercion-ok: the marker is an optimization, not state anything reads for correctness. Failing to write it costs one repeat idempotent seed on the next cold start — the pre-marker behaviour — and the seed itself already succeeded, so there is no failure to report to the caller.
880
+ }
881
+ }
802
882
  /**
803
883
  * Seed personal AGENTS.md and LEARNINGS.md for a user if they don't exist.
804
884
  * Called when listing resources or from the agent chat plugin.
@@ -810,6 +890,14 @@ export async function ensurePersonalDefaults(owner) {
810
890
  return;
811
891
  }
812
892
  await ensureTable();
893
+ // The in-memory Set resets on every cold start, so on serverless this seed ran
894
+ // its 5 writes per container per owner, forever. The durable marker is what
895
+ // actually makes it once-per-owner.
896
+ const seedKey = personalSeedKey(owner);
897
+ if (await alreadySeeded(seedKey)) {
898
+ _personalSeeded.add(owner);
899
+ return;
900
+ }
813
901
  const client = getDbExec();
814
902
  const now = Date.now();
815
903
  const seedSql = isPostgres()
@@ -886,6 +974,7 @@ export async function ensurePersonalDefaults(owner) {
886
974
  // retries instead of permanently skipping seeding. Seeds use INSERT OR IGNORE
887
975
  // / ON CONFLICT DO NOTHING, so a concurrent re-run is harmless.
888
976
  _personalSeeded.add(owner);
977
+ await markSeeded(seedKey);
889
978
  }
890
979
  function rowToResource(row) {
891
980
  return {
@@ -932,7 +1021,7 @@ export async function resourceGet(id, options) {
932
1021
  return localWorkspaceResourceById(id);
933
1022
  }
934
1023
  const client = getDbExec();
935
- await cleanupExpiredAgentScratchResources(client);
1024
+ scheduleExpiredAgentScratchCleanup(client);
936
1025
  const { rows } = await client.execute({
937
1026
  sql: `SELECT * FROM resources WHERE id = ?`,
938
1027
  args: [id],
@@ -949,7 +1038,7 @@ export async function resourceGetByPath(owner, path, options) {
949
1038
  return local;
950
1039
  }
951
1040
  const client = getDbExec();
952
- await cleanupExpiredAgentScratchResources(client);
1041
+ scheduleExpiredAgentScratchCleanup(client);
953
1042
  const { rows } = await client.execute({
954
1043
  sql: `SELECT * FROM resources WHERE owner = ? AND path = ?`,
955
1044
  args: [owner, path],
@@ -1165,7 +1254,7 @@ export async function resourceDeleteByPath(owner, path) {
1165
1254
  export async function resourceList(owner, pathPrefix, options) {
1166
1255
  await ensureTable();
1167
1256
  const client = getDbExec();
1168
- await cleanupExpiredAgentScratchResources(client);
1257
+ scheduleExpiredAgentScratchCleanup(client);
1169
1258
  const visibilitySql = scratchFilterSql(options);
1170
1259
  if (pathPrefix) {
1171
1260
  const { rows } = await client.execute({
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- status?: undefined;
38
37
  ok?: undefined;
38
+ status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ ok?: undefined;
57
58
  error: string;
58
59
  note?: undefined;
59
- ok?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
@@ -26,6 +26,7 @@
26
26
  * ```
27
27
  */
28
28
  import type { ActionEntry } from "../agent/production-agent.js";
29
+ import type { FrameworkToolGroup } from "../framework-tools.js";
29
30
  /**
30
31
  * Register a map of actions contributed by a published package.
31
32
  *
@@ -80,6 +81,32 @@ export declare function loadActionsFromStaticRegistry(modules: Record<string, un
80
81
  * passing to `createAgentChatPlugin({ actions })`.
81
82
  */
82
83
  export declare function autoDiscoverActions(from: string): Promise<Record<string, ActionEntry>>;
84
+ /**
85
+ * Which `frameworkTools` switch owns each framework action kit.
86
+ *
87
+ * The group is stamped onto the entry as `frameworkGroup`, which is what lets
88
+ * one filter subtract a whole kit from every agent tool surface at once — the
89
+ * plugin spreads these registries at thirteen composition sites, and a
90
+ * per-site condition would be the same omission waiting to happen thirteen
91
+ * times. The tag also keeps these ~45 schemas out of the DEFAULT first-request
92
+ * tool list; they stay reachable through `tool-search`.
93
+ *
94
+ * Anything absent from this map is always-on and must be listed in
95
+ * `ALWAYS_ON_CORE_ACTIONS` instead. `action-discovery.spec.ts` fails when a new
96
+ * core action appears in neither, so "always-on" stays a decision someone made
97
+ * rather than the default that happens when a map goes un-updated.
98
+ */
99
+ export declare const CORE_ACTION_GROUPS: Record<string, FrameworkToolGroup>;
100
+ /**
101
+ * Core actions with no `frameworkTools` switch, and why:
102
+ *
103
+ * - `upload-image` is load-bearing for ordinary work.
104
+ * - The email catalog is mounted everywhere on purpose so Dispatch can ask any
105
+ * app what it sends without that app opting in (see its comment below). It is
106
+ * a read surface, not the `email` group's send capability.
107
+ * - MCP tools and org service tokens are already governed by `disableMcp`.
108
+ */
109
+ export declare const ALWAYS_ON_CORE_ACTIONS: ReadonlySet<string>;
83
110
  export declare function mergeCoreSharingActions(registry: Record<string, ActionEntry>): Promise<void>;
84
111
  /** @deprecated Use `autoDiscoverActions` instead */
85
112
  export declare const autoDiscoverScripts: typeof autoDiscoverActions;
@@ -497,6 +497,92 @@ export async function autoDiscoverActions(from) {
497
497
  }
498
498
  return registry;
499
499
  }
500
+ /**
501
+ * Which `frameworkTools` switch owns each framework action kit.
502
+ *
503
+ * The group is stamped onto the entry as `frameworkGroup`, which is what lets
504
+ * one filter subtract a whole kit from every agent tool surface at once — the
505
+ * plugin spreads these registries at thirteen composition sites, and a
506
+ * per-site condition would be the same omission waiting to happen thirteen
507
+ * times. The tag also keeps these ~45 schemas out of the DEFAULT first-request
508
+ * tool list; they stay reachable through `tool-search`.
509
+ *
510
+ * Anything absent from this map is always-on and must be listed in
511
+ * `ALWAYS_ON_CORE_ACTIONS` instead. `action-discovery.spec.ts` fails when a new
512
+ * core action appears in neither, so "always-on" stays a decision someone made
513
+ * rather than the default that happens when a map goes un-updated.
514
+ */
515
+ export const CORE_ACTION_GROUPS = {
516
+ "share-resource": "sharing",
517
+ "unshare-resource": "sharing",
518
+ "list-resource-shares": "sharing",
519
+ "set-resource-visibility": "sharing",
520
+ "create-agent-resource-link": "sharing",
521
+ "get-feature-flags": "featureFlags",
522
+ "list-feature-flags": "featureFlags",
523
+ "set-feature-flag": "featureFlags",
524
+ "list-recurring-jobs": "automation",
525
+ "manage-recurring-job": "automation",
526
+ "run-automation-now": "automation",
527
+ "list-automation-runs": "automation",
528
+ "list-automations": "automation",
529
+ "manage-automation": "automation",
530
+ "context-manifest-get": "contextXray",
531
+ "context-preview-get": "contextXray",
532
+ "context-pin": "contextXray",
533
+ "context-evict": "contextXray",
534
+ "context-restore": "contextXray",
535
+ "context-report": "contextXray",
536
+ "get-localization-preference": "localization",
537
+ "set-localization-preference": "localization",
538
+ // Profile, credentials, and appearance share one switch: an app that hides
539
+ // its profile surface from the agent hides password management with it.
540
+ "get-user-profile": "userProfile",
541
+ "update-user-profile": "userProfile",
542
+ "get-auth-methods": "userProfile",
543
+ "set-password": "userProfile",
544
+ "change-password": "userProfile",
545
+ "change-appearance": "userProfile",
546
+ "list-audit-events": "audit",
547
+ "get-audit-event": "audit",
548
+ "export-audit-events": "audit",
549
+ "create-resource-version": "history",
550
+ "list-resource-versions": "history",
551
+ "get-resource-version": "history",
552
+ "restore-resource-version": "history",
553
+ "list-resource-history": "history",
554
+ "list-review-comments": "review",
555
+ "create-review-comment": "review",
556
+ "reply-review-comment": "review",
557
+ "resolve-review-thread": "review",
558
+ "delete-review-comment": "review",
559
+ "consume-review-feedback": "review",
560
+ "get-review-feedback": "review",
561
+ "set-review-status": "review",
562
+ "send-review-thread-to-agent": "review",
563
+ };
564
+ /**
565
+ * Core actions with no `frameworkTools` switch, and why:
566
+ *
567
+ * - `upload-image` is load-bearing for ordinary work.
568
+ * - The email catalog is mounted everywhere on purpose so Dispatch can ask any
569
+ * app what it sends without that app opting in (see its comment below). It is
570
+ * a read surface, not the `email` group's send capability.
571
+ * - MCP tools and org service tokens are already governed by `disableMcp`.
572
+ */
573
+ export const ALWAYS_ON_CORE_ACTIONS = new Set([
574
+ "upload-image",
575
+ "list-transactional-emails",
576
+ "render-transactional-email-preview",
577
+ "list-email-log",
578
+ "list-email-activity",
579
+ "list-email-engagement",
580
+ "create-org-service-token",
581
+ "list-org-service-tokens",
582
+ "revoke-org-service-token",
583
+ "list-mcp-tools",
584
+ "call-mcp-tool",
585
+ ]);
500
586
  export async function mergeCoreSharingActions(registry) {
501
587
  const entries = [
502
588
  ["share-resource", () => import("../sharing/actions/share-resource.js")],
@@ -732,6 +818,9 @@ export async function mergeCoreSharingActions(registry) {
732
818
  // actions' `toolCallable: false` (audit-H5) is dropped and the
733
819
  // tools-iframe bridge 403 in action-routes.ts never fires.
734
820
  ...preserveActionFlags(def),
821
+ ...(CORE_ACTION_GROUPS[name]
822
+ ? { frameworkGroup: CORE_ACTION_GROUPS[name] }
823
+ : {}),
735
824
  };
736
825
  }
737
826
  }
@@ -117,5 +117,16 @@ export declare function createA2AEngineToolSurface(availableTools: EngineTool[],
117
117
  tools: EngineTool[];
118
118
  availableTools: EngineTool[];
119
119
  };
120
+ /**
121
+ * The first-request tool catalog: an explicit `initialToolNames` verbatim, or
122
+ * the app's OWN actions by default.
123
+ *
124
+ * "Its own actions" excludes the framework kits. They arrive in this same
125
+ * registry through `autoDiscoverActions` -> `mergeCoreSharingActions`, so the
126
+ * plain `Object.keys` default promoted ~45 sharing/review/history/flag schemas
127
+ * into every app's first request whether or not the app had those surfaces. They
128
+ * remain in `availableTools` and are still found by `tool-search`; an app that
129
+ * wants one on turn one names it in `initialToolNames`.
130
+ */
120
131
  export declare function resolveInitialToolNames(templateActions: Record<string, ActionEntry>, configured?: string[]): string[];
121
132
  export {};
@@ -5,6 +5,7 @@ import { resolveMainChatMaxOutputTokens } from "../../agent/engine/output-tokens
5
5
  import { filterInitialEngineTools, resolveAgentRequestReasoningEffort, } from "../../agent/production-agent.js";
6
6
  import { runAgentLoopDirectWithSoftTimeout } from "../../agent/run-loop-with-resume.js";
7
7
  import { resolveRunSoftTimeoutMs } from "../../agent/run-manager.js";
8
+ import { isFrameworkGroupedAction } from "../../framework-tools.js";
8
9
  import { isAuthenticatedReadAction, isAutoReadExcludedActionName, } from "../../mcp/build-server.js";
9
10
  import { withConfiguredAppBasePath } from "../app-base-path.js";
10
11
  // ---------------------------------------------------------------------------
@@ -389,6 +390,21 @@ export function createA2AEngineToolSurface(availableTools, initialToolNames) {
389
390
  availableTools,
390
391
  };
391
392
  }
393
+ /**
394
+ * The first-request tool catalog: an explicit `initialToolNames` verbatim, or
395
+ * the app's OWN actions by default.
396
+ *
397
+ * "Its own actions" excludes the framework kits. They arrive in this same
398
+ * registry through `autoDiscoverActions` -> `mergeCoreSharingActions`, so the
399
+ * plain `Object.keys` default promoted ~45 sharing/review/history/flag schemas
400
+ * into every app's first request whether or not the app had those surfaces. They
401
+ * remain in `availableTools` and are still found by `tool-search`; an app that
402
+ * wants one on turn one names it in `initialToolNames`.
403
+ */
392
404
  export function resolveInitialToolNames(templateActions, configured) {
393
- return configured ?? Object.keys(templateActions);
405
+ if (configured)
406
+ return configured;
407
+ return Object.entries(templateActions)
408
+ .filter(([, entry]) => !isFrameworkGroupedAction(entry))
409
+ .map(([name]) => name);
394
410
  }
@@ -1,4 +1,5 @@
1
1
  import type { ActionEntry } from "../../agent/production-agent.js";
2
+ import type { FrameworkToolGroup } from "../../framework-tools.js";
2
3
  import type { DatabaseToolsOption } from "../../scripts/db/tool-mode.js";
3
4
  import { type PromptExamples } from "../prompts/index.js";
4
5
  import { resolveInitialToolNames } from "./action-filters-a2a.js";
@@ -15,6 +16,7 @@ import { createDataWidgetActionEntries } from "./context-tools.js";
15
16
  export declare function buildFrameworkPrompts(examples?: PromptExamples, options?: {
16
17
  databaseTools?: DatabaseToolsOption;
17
18
  extensionTools?: boolean;
19
+ disabledFrameworkGroups?: ReadonlySet<FrameworkToolGroup>;
18
20
  }): {
19
21
  FRAMEWORK_CORE: string;
20
22
  FRAMEWORK_CORE_COMPACT: string;
@@ -1,6 +1,7 @@
1
1
  import type { ActionEntry } from "../../agent/production-agent.js";
2
2
  import type { AgentLoopFinalResponseGuard } from "../../agent/production-agent.js";
3
3
  import type { AgentChatAttachment, AgentChatReference, MentionProvider } from "../../agent/types.js";
4
+ import type { FrameworkToolsConfig } from "../../framework-tools.js";
4
5
  import type { ExternalAgentPolicy } from "../../mcp/external-agent-policy.js";
5
6
  import type { DatabaseToolsOption } from "../../scripts/db/tool-mode.js";
6
7
  import type { PromptExamples } from "../prompts/index.js";
@@ -236,6 +237,29 @@ export interface AgentChatPluginOptions {
236
237
  * Defaults to `false`.
237
238
  */
238
239
  nativeActionsInDev?: boolean;
240
+ /**
241
+ * Which of the framework's OWN agent tools this app exposes — raw SQL,
242
+ * extensions, sharing, review comments, version history, feature flags,
243
+ * localization, audit, context X-Ray, profile, automations, docs, resources,
244
+ * web, cross-app delegation, chat, email.
245
+ *
246
+ * Every group defaults to today's behavior, so omitting this leaves the tool
247
+ * surface unchanged. `"minimal"` (or `{ preset: "minimal" }`) turns them all
248
+ * off for voice-first and single-purpose apps; an explicit group key wins over
249
+ * the preset.
250
+ *
251
+ * Disabling a group removes it from the AGENT surfaces only — chat, MCP, A2A,
252
+ * background runs. The matching HTTP action routes stay mounted because the UI
253
+ * reaches them through client hooks. Weigh the parity cost first: the
254
+ * framework's contract is that anything the UI can do, the agent can do, so
255
+ * `sharing: false` alongside a visible `ShareButton` is a deliberate break and
256
+ * only makes sense when the app has no such surface.
257
+ *
258
+ * Independent of this option, framework tools are no longer promoted into the
259
+ * first model request by default — they are reachable through `tool-search`,
260
+ * or by naming them in `initialToolNames`.
261
+ */
262
+ frameworkTools?: FrameworkToolsConfig;
239
263
  /**
240
264
  * Expose raw SQL/native database tools to the app agent.
241
265
  *
@@ -244,6 +268,9 @@ export interface AgentChatPluginOptions {
244
268
  * `true`) to expose `db-exec`/`db-patch` for scoped raw SQL maintenance.
245
269
  * Set to `"off"` (also `false`) for chat-first apps that want agents to use
246
270
  * typed actions only.
271
+ *
272
+ * @deprecated Use `frameworkTools: { database: … }`. Still honored for one
273
+ * minor; setting both to conflicting values throws at plugin init.
247
274
  */
248
275
  databaseTools?: DatabaseToolsOption;
249
276
  /**
@@ -252,6 +279,9 @@ export interface AgentChatPluginOptions {
252
279
  * false. Set to true for apps that intentionally let the LLM create or
253
280
  * manage sandboxed extension mini-apps. Core extension routes may still be
254
281
  * mounted for compatibility and app-owned surfaces.
282
+ *
283
+ * @deprecated Use `frameworkTools: { extensions: … }`. Still honored for one
284
+ * minor; setting both to conflicting values throws at plugin init.
255
285
  */
256
286
  extensionTools?: boolean;
257
287
  /**
@@ -1,3 +1,4 @@
1
+ import { type FrameworkToolGroup } from "../../framework-tools.js";
1
2
  import type { ContextGovernanceTier, ContextManifestSourceRef, ContextSystemProvenance } from "../../shared/context-xray.js";
2
3
  export declare const COMPACT_PROMPT_RESOURCE_MAX_CHARS = 6000;
3
4
  export interface PromptContextProviderContext {
@@ -89,4 +90,6 @@ export declare function resourceScopeForOwner(owner: string, currentOwner?: stri
89
90
  * AGENTS.md and restarting the server is all it takes; Vite HMR invalidates
90
91
  * the bundle in dev so changes land instantly.
91
92
  */
92
- export declare function loadResourcesForPrompt(owner: string, compact?: boolean, selfAppId?: string, orgId?: string | null): Promise<string>;
93
+ export declare function loadResourcesForPrompt(owner: string, compact?: boolean, selfAppId?: string, orgId?: string | null, opts?: {
94
+ disabledFrameworkGroups?: ReadonlySet<FrameworkToolGroup>;
95
+ }): Promise<string>;
@@ -1,3 +1,4 @@
1
+ import { frameworkGroupEnabled, } from "../../framework-tools.js";
1
2
  import { getFrontmatterValue, getSkillNameFromPath, parseFrontmatter, } from "../../resources/metadata.js";
2
3
  import { ensurePersonalDefaults, organizationIdFromResourceOwner, resourceGet, resourceGetByPath, resourceList, resourceListAccessible, SHARED_OWNER, sharedResourceOwner, WORKSPACE_OWNER, } from "../../resources/store.js";
3
4
  import { discoverAgents } from "../agent-discovery.js";
@@ -600,7 +601,7 @@ async function loadResourceIndexForPrompt(owner, scope) {
600
601
  * AGENTS.md and restarting the server is all it takes; Vite HMR invalidates
601
602
  * the bundle in dev so changes land instantly.
602
603
  */
603
- export async function loadResourcesForPrompt(owner, compact = false, selfAppId, orgId = getRequestOrgId() ?? null) {
604
+ export async function loadResourcesForPrompt(owner, compact = false, selfAppId, orgId = getRequestOrgId() ?? null, opts) {
604
605
  await ensurePersonalDefaults(owner);
605
606
  const sections = [];
606
607
  const addSection = (content, governance = "inherited") => {
@@ -774,7 +775,12 @@ export async function loadResourcesForPrompt(owner, compact = false, selfAppId,
774
775
  orgId,
775
776
  }));
776
777
  try {
777
- const agents = (await discoverAgents(selfAppId)).slice(0, 30);
778
+ // Both tools this block names by name come from the `workspaceApps` group.
779
+ // With that group off there is no `call-agent` to delegate through, so the
780
+ // block would only teach a capability the request cannot carry.
781
+ const agents = frameworkGroupEnabled(opts?.disabledFrameworkGroups, "workspaceApps")
782
+ ? (await discoverAgents(selfAppId)).slice(0, 30)
783
+ : [];
778
784
  if (agents.length > 0) {
779
785
  const lines = agents.map((agent) => `- ${agent.name} (${agent.id}) — ${agent.description || "Connected A2A app"}`);
780
786
  // Nothing else in the prompt or the initial tool surface tells the agent