@agent-native/core 0.145.3 → 0.145.5

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 (51) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/plugins/agent-chat.ts +10 -8
  3. package/corpus/templates/assets/server/plugins/agent-chat.ts +1 -1
  4. package/corpus/templates/calendar/server/plugins/agent-chat.ts +1 -1
  5. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +8 -3
  6. package/corpus/templates/clips/app/components/editor/waveform.tsx +2 -1
  7. package/corpus/templates/clips/app/components/library/search-bar.tsx +1 -1
  8. package/corpus/templates/clips/app/global.css +9 -0
  9. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +22 -1
  10. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +10 -11
  11. package/corpus/templates/design/actions/view-screen.ts +1 -0
  12. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +14 -1
  13. package/corpus/templates/design/changelog/2026-08-07-cmd-c-now-copies-selected-text-in-the-agent-panel-instead-of.md +6 -0
  14. package/corpus/templates/design/changelog/2026-08-07-real-time-voice-can-now-read-the-current-selection-and-edit-.md +6 -0
  15. package/corpus/templates/design/server/lib/reprompt-action-guard.ts +7 -3
  16. package/corpus/templates/mail/server/plugins/agent-chat.ts +1 -1
  17. package/corpus/templates/plan/server/lib/plan-framework-tools.ts +11 -0
  18. package/corpus/templates/plan/server/plugins/00-mcp.ts +25 -68
  19. package/corpus/templates/plan/server/plugins/agent-chat.ts +8 -2
  20. package/dist/collab/awareness.d.ts +2 -2
  21. package/dist/collab/routes.d.ts +1 -1
  22. package/dist/framework-tools.d.ts +33 -1
  23. package/dist/framework-tools.js +88 -4
  24. package/dist/mcp/build-server.d.ts +15 -0
  25. package/dist/mcp/build-server.js +94 -30
  26. package/dist/mcp/oauth-client-metadata.js +6 -2
  27. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  28. package/dist/notifications/routes.d.ts +2 -2
  29. package/dist/observability/routes.d.ts +3 -3
  30. package/dist/progress/routes.d.ts +1 -1
  31. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  32. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  33. package/dist/secrets/routes.d.ts +9 -9
  34. package/dist/server/action-discovery.d.ts +3 -18
  35. package/dist/server/action-discovery.js +9 -65
  36. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -2
  37. package/dist/server/agent-chat/action-filters-a2a.js +1 -1
  38. package/dist/server/agent-chat/mcp-options.d.ts +106 -0
  39. package/dist/server/agent-chat/mcp-options.js +50 -0
  40. package/dist/server/agent-chat/plugin-options.d.ts +17 -26
  41. package/dist/server/agent-chat-plugin.js +21 -11
  42. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  43. package/dist/server/index.d.ts +3 -0
  44. package/dist/server/index.js +7 -0
  45. package/dist/server/realtime-token.d.ts +1 -1
  46. package/dist/server/realtime-voice.d.ts +2 -1
  47. package/dist/server/realtime-voice.js +38 -56
  48. package/dist/server/short-lived-token.d.ts +43 -0
  49. package/dist/server/short-lived-token.js +74 -0
  50. package/dist/server/transcribe-voice.d.ts +1 -1
  51. package/package.json +2 -2
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 7850
34
+ - template files: 7853
@@ -907,14 +907,16 @@ export default createAgentChatPlugin({
907
907
  durableBackgroundRuns: true,
908
908
  runSoftTimeoutMs: ANALYTICS_BACKGROUND_RUN_SOFT_TIMEOUT_MS,
909
909
  runNoProgressTimeoutMs: ANALYTICS_BACKGROUND_RUN_NO_PROGRESS_TIMEOUT_MS,
910
- connectorCatalog: [...ANALYTICS_CONNECTOR_CATALOG],
911
- externalAgents: {
912
- // Keep the direct MCP surface deliberately curated. External agents
913
- // should use ask_app by default; cataloged actions are optional stable
914
- // semantic reads for callers with an exact, fully known contract. They are
915
- // never a fallback for slow or failed delegation.
916
- authenticatedReads: "off",
917
- writes: "ask_app_only",
910
+ mcp: {
911
+ connectorCatalog: [...ANALYTICS_CONNECTOR_CATALOG],
912
+ externalAgents: {
913
+ // Keep the direct MCP surface deliberately curated. External agents
914
+ // should use ask_app by default; cataloged actions are optional stable
915
+ // semantic reads for callers with an exact, fully known contract. They
916
+ // are never a fallback for slow or failed delegation.
917
+ authenticatedReads: "off",
918
+ writes: "ask_app_only",
919
+ },
918
920
  },
919
921
  resolveOrgId: async (event) => {
920
922
  const ctx = await getOrgContext(event);
@@ -34,7 +34,7 @@ const INITIAL_TOOL_NAMES = [
34
34
 
35
35
  export default createAgentChatPlugin({
36
36
  appId: "assets",
37
- mcpServerInfo: {
37
+ mcp: {
38
38
  title: "Agent-Native Assets",
39
39
  description:
40
40
  "Create, search, select, and export brand image and video assets from Assets.",
@@ -90,7 +90,7 @@ export default createAgentChatPlugin({
90
90
  // background worker, as content, slides and analytics already do.
91
91
  durableBackgroundRuns: true,
92
92
  initialToolNames: INITIAL_TOOL_NAMES,
93
- connectorCatalog: [...CALENDAR_CONNECTOR_CATALOG],
93
+ mcp: { connectorCatalog: [...CALENDAR_CONNECTOR_CATALOG] },
94
94
  // Enable sandboxed JavaScript execution so Calendar agents can fetch,
95
95
  // paginate, and reduce provider data through providerFetch() without us
96
96
  // hardcoding one action per Google Calendar / CRM endpoint.
@@ -255,6 +255,11 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
255
255
  [viewportWidth, zoom],
256
256
  );
257
257
 
258
+ const clampedScrollLeft = Math.min(
259
+ scrollLeft,
260
+ Math.max(0, totalWidth - viewportWidth),
261
+ );
262
+
258
263
  const calculateAnchoredScrollLeft = useCallback(
259
264
  (
260
265
  nextZoom: number,
@@ -815,7 +820,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
815
820
  splitPoints={splitPoints}
816
821
  activityRanges={transcriptSegments}
817
822
  onSeek={seek}
818
- scrollLeft={scrollLeft}
823
+ scrollLeft={clampedScrollLeft}
819
824
  onScroll={(s) => setScrollLeft(s)}
820
825
  />
821
826
  <div
@@ -826,7 +831,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
826
831
  className="relative h-full"
827
832
  style={{
828
833
  width: totalWidth,
829
- transform: `translateX(${-scrollLeft}px)`,
834
+ transform: `translateX(${-clampedScrollLeft}px)`,
830
835
  }}
831
836
  >
832
837
  {durationMs > 0 && (
@@ -849,7 +854,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
849
854
  >
850
855
  <div
851
856
  style={{
852
- transform: `translateX(${-scrollLeft}px)`,
857
+ transform: `translateX(${-clampedScrollLeft}px)`,
853
858
  width: totalWidth,
854
859
  }}
855
860
  >
@@ -185,7 +185,8 @@ export function Waveform({
185
185
 
186
186
  const ctx = canvas.getContext("2d");
187
187
  if (!ctx) return;
188
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
188
+ const scaleX = canvas.width / totalWidth;
189
+ ctx.setTransform(scaleX, 0, 0, dpr, 0, 0);
189
190
 
190
191
  const hasPeaks = Boolean(
191
192
  peaks?.bucketCount &&
@@ -31,7 +31,7 @@ function highlight(
31
31
  parts.push(
32
32
  <mark
33
33
  key={`${idx}-${parts.length}`}
34
- className="bg-yellow-200 text-foreground rounded-sm px-0.5"
34
+ className="bg-highlight/30 text-foreground rounded-sm px-0.5"
35
35
  >
36
36
  {text.slice(idx, idx + q.length)}
37
37
  </mark>,
@@ -36,6 +36,8 @@
36
36
  --sidebar-accent-foreground: 0 0% 15%;
37
37
  --sidebar-border: 0 0% 90%;
38
38
  --sidebar-ring: 0 0% 40%;
39
+ --highlight: 197 100% 50%;
40
+ --highlight-foreground: 0 0% 10%;
39
41
  }
40
42
 
41
43
  .dark {
@@ -67,6 +69,13 @@
67
69
  --sidebar-accent-foreground: 0 0% 90%;
68
70
  --sidebar-border: 0 0% 20%;
69
71
  --sidebar-ring: 0 0% 60%;
72
+ --highlight: 197 100% 50%;
73
+ --highlight-foreground: 0 0% 10%;
74
+ }
75
+
76
+ @theme inline {
77
+ --color-highlight: hsl(var(--highlight));
78
+ --color-highlight-foreground: hsl(var(--highlight-foreground));
70
79
  }
71
80
 
72
81
  @layer base {
@@ -2,6 +2,7 @@ import {
2
2
  chooseFallbackAudioInput,
3
3
  enumerateAudioInputDevices,
4
4
  isLikelyPhoneMicLabel,
5
+ normalizedMediaDeviceId,
5
6
  type AudioInputFallback,
6
7
  } from "../../../shared/media-device-selection";
7
8
 
@@ -204,7 +205,27 @@ export async function getAudioStreamWithFallback(
204
205
  const stream = await getVoiceFocusedAudioStream(id);
205
206
  return id ? stream : replacePhoneDefaultMicIfPossible(stream);
206
207
  } catch (err) {
207
- if (!isMediaConstraintFailure(err)) throw err;
208
+ let deviceGone = false;
209
+ if (id && err instanceof DOMException && err.name === "NotAllowedError") {
210
+ try {
211
+ deviceGone = !(await enumerateAudioInputDevices()).some(
212
+ (device) => normalizedMediaDeviceId(device.deviceId) === id,
213
+ );
214
+ } catch (enumerateErr) {
215
+ console.warn(
216
+ "[clips-recorder] could not confirm saved mic is still present after NotAllowedError",
217
+ enumerateErr,
218
+ );
219
+ deviceGone = true;
220
+ }
221
+ }
222
+ if (deviceGone) {
223
+ console.warn(
224
+ "[clips-recorder] saved mic no longer enumerated after NotAllowedError; falling back",
225
+ { deviceId: id },
226
+ );
227
+ }
228
+ if (!isMediaConstraintFailure(err) && !deviceGone) throw err;
208
229
  if (id) {
209
230
  const fallback = await tryFallbackAudioInput(savedLabel, [id]);
210
231
  if (fallback) return fallback;
@@ -392,7 +392,6 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
392
392
  let mut ever_seen_front = false;
393
393
  let mut last_front_at: Option<Instant> = None;
394
394
  let mut fired = false;
395
- let mut last_front_was_generic_browser = false;
396
395
  let mut call_app_used_microphone = false;
397
396
  let mut microphone_released_at: Option<Instant> = None;
398
397
  let mut generation: Option<u64> = None;
@@ -408,7 +407,6 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
408
407
  ever_seen_front = false;
409
408
  last_front_at = None;
410
409
  fired = false;
411
- last_front_was_generic_browser = false;
412
410
  call_app_used_microphone = false;
413
411
  microphone_released_at = None;
414
412
  continue;
@@ -417,7 +415,6 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
417
415
  ever_seen_front = false;
418
416
  last_front_at = None;
419
417
  fired = false;
420
- last_front_was_generic_browser = false;
421
418
  call_app_used_microphone = false;
422
419
  microphone_released_at = None;
423
420
  generation = Some(active_generation);
@@ -450,7 +447,6 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
450
447
  if is_strong_vc || is_generic_browser {
451
448
  ever_seen_front = true;
452
449
  last_front_at = Some(Instant::now());
453
- last_front_was_generic_browser = is_generic_browser;
454
450
  }
455
451
  if fired {
456
452
  continue;
@@ -474,10 +470,18 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
474
470
  _ => {}
475
471
  }
476
472
 
473
+ // Require audio corroboration for every trigger in this
474
+ // watcher: backgrounding the call app, or its process
475
+ // dropping its mic input, does not by itself prove the call
476
+ // ended — a browser tab can report either transition while
477
+ // the meeting is still playing through system audio. Only
478
+ // quiet mic+system audio alongside the signal does.
479
+ let audio_quiet = audio_recently_silent(&state, threshold_ms);
480
+
477
481
  let microphone_released = microphone_release_stop_ready(
478
482
  call_app_used_microphone,
479
483
  microphone_released_at.map(|at| Instant::now().duration_since(at)),
480
- );
484
+ ) && audio_quiet;
481
485
 
482
486
  let frontmost_call_ended = ever_seen_front
483
487
  && last_front_at
@@ -485,12 +489,7 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
485
489
  Instant::now().duration_since(t).as_millis() as u64 >= threshold_ms
486
490
  })
487
491
  .unwrap_or(false)
488
- // Require audio corroboration for browser-hosted calls:
489
- // backgrounding Chrome/Arc alone does not prove that the
490
- // Meet tab ended. Use the last known conference app, not
491
- // the unrelated app now in front.
492
- && (!last_front_was_generic_browser
493
- || audio_recently_silent(&state, threshold_ms));
492
+ && audio_quiet;
494
493
 
495
494
  if microphone_released || frontmost_call_ended {
496
495
  let _ = app.emit("meetings:call-ended", ());
@@ -220,6 +220,7 @@ export default defineAction({
220
220
  "See what the user is currently looking at on screen. Returns the current navigation state including which design or template is open, which view they are on (list, templates, editor, design-systems, present, settings), active/focused design screen, selected element, active inspector tab (design, comments, or tweaks), active left rail panel (file, agent, assets, import, tools, tokens, or code), active code file metadata, overview canvas state, review status and feedback queue summary, plus any pending question overlay. Always call this first before taking any action.",
221
221
  schema: z.object({}),
222
222
  http: false,
223
+ readOnly: true,
223
224
  run: async (_, ctx) => {
224
225
  const [navigation, designSelection] = await Promise.all([
225
226
  readAppStateForCurrentTab("navigation"),
@@ -277,6 +277,18 @@ export function isDesignHotkeyEditableTarget(target: EventTarget | null) {
277
277
  return tagName === "input" || tagName === "textarea" || tagName === "select";
278
278
  }
279
279
 
280
+ /** Chat bodies and panel labels are selectable but not editable targets, so the
281
+ * editable guard never sees them. Canvas layers live in the preview iframe and
282
+ * produce no parent-document range, so this cannot mask a real layer copy. */
283
+ export function hasDocumentTextSelection() {
284
+ if (typeof window === "undefined" || !window.getSelection) return false;
285
+ const selection = window.getSelection();
286
+ if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
287
+ return false;
288
+ }
289
+ return selection.toString().trim().length > 0;
290
+ }
291
+
280
292
  export function isShowKeyboardShortcutsHotkey(event: KeyboardEvent) {
281
293
  if (!(event.ctrlKey || event.metaKey) || !event.shiftKey || event.altKey) {
282
294
  return false;
@@ -523,7 +535,7 @@ export function handleDesignHotkey(
523
535
  if (primary && event.shiftKey && key === "x") {
524
536
  return run(props.onToggleStrikethrough);
525
537
  }
526
- if (primary && key === "x") {
538
+ if (primary && key === "x" && !hasDocumentTextSelection()) {
527
539
  return runSharedCanvasCommand() || run(props.onCut);
528
540
  }
529
541
  // Figma's Cmd+U — toggle underline. No existing binding claims plain "u".
@@ -556,6 +568,7 @@ export function handleDesignHotkey(
556
568
  if (primary && key === "c") {
557
569
  if (event.altKey) return run(props.onCopyProps);
558
570
  if (event.shiftKey) return run(props.onCopyAsPng);
571
+ if (hasDocumentTextSelection()) return false;
559
572
  return runSharedCanvasCommand() || run(props.onCopy);
560
573
  }
561
574
  if (primary && key === "v") {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Cmd+C now copies selected text in the agent panel instead of being captured by the canvas copy shortcut
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Real-time voice can now read the current selection and edit the design, instead of failing to verify the agent thread
@@ -77,13 +77,17 @@ export function guardRepromptActionRegistry(
77
77
  ...entry,
78
78
  run: async (args, context) => {
79
79
  if (context?.caller === "tool" && context.threadId) {
80
- const thread = await getThread(context.threadId);
81
- if (!thread) {
80
+ let thread: Awaited<ReturnType<typeof getThread>>;
81
+ try {
82
+ thread = await getThread(context.threadId);
83
+ } catch {
82
84
  throw new Error(
83
85
  `Cannot verify the agent thread for mutating action ${name}.`,
84
86
  );
85
87
  }
86
- const intent = threadSelectionIntent(thread);
88
+ // Off-thread callers (realtime voice, MCP, A2A, cron) have no
89
+ // stored thread, so they cannot be inside a reprompt turn.
90
+ const intent = thread ? threadSelectionIntent(thread) : null;
87
91
  if (
88
92
  intent === "reprompt" &&
89
93
  !REPROMPT_MUTATION_ALLOWLIST.has(name)
@@ -40,7 +40,7 @@ export default createAgentChatPlugin({
40
40
  // background worker, as content, slides and analytics already do.
41
41
  durableBackgroundRuns: true,
42
42
  initialToolNames: INITIAL_TOOL_NAMES,
43
- connectorCatalog: [...MAIL_CONNECTOR_CATALOG],
43
+ mcp: { connectorCatalog: [...MAIL_CONNECTOR_CATALOG] },
44
44
  resolveOrgId: async (event) => {
45
45
  const ctx = await getOrgContext(event);
46
46
  return ctx.orgId;
@@ -0,0 +1,11 @@
1
+ import type { FrameworkToolsConfig } from "@agent-native/core/server";
2
+
3
+ /**
4
+ * Framework tool policy shared by Plan's two mounts.
5
+ *
6
+ * Plan serves MCP from a dedicated early plugin (`plugins/00-mcp.ts`) rather
7
+ * than the agent-chat plugin, so the policy has to live somewhere both can
8
+ * read. Setting it in only one place is how an app ends up advertising a tool
9
+ * over MCP that its own agent no longer has.
10
+ */
11
+ export const PLAN_FRAMEWORK_TOOLS: FrameworkToolsConfig = {};
@@ -1,77 +1,16 @@
1
1
  import { mountMCP } from "@agent-native/core/mcp";
2
2
  import {
3
+ attachToolSearch,
4
+ filterFrameworkToolGroups,
3
5
  loadActionsFromStaticRegistry,
4
- type ActionEntry,
6
+ resolveFrameworkTools,
5
7
  } from "@agent-native/core/server";
6
8
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
7
9
  import { createAutomationToolEntries } from "@agent-native/core/triggers";
8
10
 
9
11
  import actionsRegistry from "../../.generated/actions-registry.js";
10
12
  import { PLAN_CONNECTOR_CATALOG } from "../lib/plan-connector-catalog.js";
11
-
12
- const TOOL_SEARCH_ACTION_NAME = "tool-search";
13
-
14
- function attachPlanToolSearch(
15
- registry: Record<string, ActionEntry>,
16
- ): Record<string, ActionEntry> {
17
- const searchableNames = new Set(PLAN_CONNECTOR_CATALOG);
18
- registry[TOOL_SEARCH_ACTION_NAME] = {
19
- tool: {
20
- description:
21
- "Discover callable Plan MCP tools by name and description. Omit query to list the visible Plan connector surface; pass query to filter by capability.",
22
- parameters: {
23
- type: "object",
24
- properties: {
25
- query: {
26
- type: "string",
27
- description:
28
- "Optional search text, e.g. create plan, share resource, or comments.",
29
- },
30
- limit: {
31
- type: "number",
32
- description: "Maximum results to return. Defaults to 8.",
33
- },
34
- },
35
- },
36
- },
37
- http: false,
38
- readOnly: true,
39
- run: async (args: { query?: unknown; limit?: unknown } = {}) => {
40
- const query = String(args.query ?? "")
41
- .trim()
42
- .toLowerCase();
43
- const rawLimit = Number(args.limit);
44
- const limit =
45
- Number.isFinite(rawLimit) && rawLimit > 0
46
- ? Math.min(Math.floor(rawLimit), 25)
47
- : 8;
48
- const results = Object.entries(registry)
49
- .filter(([name, entry]) => {
50
- if (name === TOOL_SEARCH_ACTION_NAME) return false;
51
- if (!searchableNames.has(name)) return false;
52
- const description = entry.tool.description ?? "";
53
- if (!query) return true;
54
- return `${name} ${description}`.toLowerCase().includes(query);
55
- })
56
- .slice(0, query ? limit : 100)
57
- .map(([name, entry]) => ({
58
- name,
59
- description: entry.tool.description ?? name,
60
- readOnly: entry.readOnly === true,
61
- }));
62
-
63
- return {
64
- query,
65
- totalTools: Object.keys(registry).filter((name) =>
66
- searchableNames.has(name),
67
- ).length,
68
- count: results.length,
69
- results,
70
- };
71
- },
72
- };
73
- return registry;
74
- }
13
+ import { PLAN_FRAMEWORK_TOOLS } from "../lib/plan-framework-tools.js";
75
14
 
76
15
  function requireMcpOwner(operation: string): string {
77
16
  const owner = getRequestUserEmail();
@@ -84,11 +23,29 @@ function requireMcpOwner(operation: string): string {
84
23
  }
85
24
 
86
25
  export default function planMcpPlugin(nitroApp: any) {
87
- const actions = attachPlanToolSearch({
88
- ...loadActionsFromStaticRegistry(actionsRegistry),
89
- ...createAutomationToolEntries(() => requireMcpOwner("manage automations")),
26
+ // This mount deliberately bypasses the agent-chat plugin so the external
27
+ // connector does not wait on chat initialization — which means it has to
28
+ // apply the framework tool policy itself. Reading the same
29
+ // `PLAN_FRAMEWORK_TOOLS` the chat plugin does is what keeps the two mounts
30
+ // from disagreeing about which tools exist.
31
+ const frameworkTools = resolveFrameworkTools({
32
+ frameworkTools: PLAN_FRAMEWORK_TOOLS,
90
33
  });
91
34
 
35
+ const actions = attachToolSearch(
36
+ filterFrameworkToolGroups(
37
+ {
38
+ ...loadActionsFromStaticRegistry(actionsRegistry),
39
+ ...(frameworkTools.isEnabled("automation")
40
+ ? createAutomationToolEntries(() =>
41
+ requireMcpOwner("manage automations"),
42
+ )
43
+ : {}),
44
+ },
45
+ frameworkTools.disabledGroups,
46
+ ),
47
+ );
48
+
92
49
  mountMCP(nitroApp, {
93
50
  name: "Plan",
94
51
  title: "Agent-Native Plan",
@@ -8,6 +8,7 @@ import { z } from "zod";
8
8
 
9
9
  import actionsRegistry from "../../.generated/actions-registry.js";
10
10
  import { PLAN_CONNECTOR_CATALOG } from "../lib/plan-connector-catalog.js";
11
+ import { PLAN_FRAMEWORK_TOOLS } from "../lib/plan-framework-tools.js";
11
12
  import { resolvePlanAnonymousOwner } from "../lib/public-plans.js";
12
13
 
13
14
  const PLAN_BACKGROUND_RUN_SOFT_TIMEOUT_MS = 13 * 60_000;
@@ -130,8 +131,13 @@ const planAgentChatOptions = {
130
131
  initialToolNames: INITIAL_TOOL_NAMES,
131
132
  anonymousOwner: resolvePlanAnonymousOwner,
132
133
  resolveOrgId: async (event) => (await getOrgContext(event)).orgId,
133
- connectorCatalog: PLAN_CONNECTOR_CATALOG,
134
- disableMcp: true,
134
+ frameworkTools: PLAN_FRAMEWORK_TOOLS,
135
+ mcp: {
136
+ // Plan mounts MCP from a dedicated early plugin (`00-mcp.ts`) so its
137
+ // external connector does not wait on chat plugin initialization.
138
+ enabled: false,
139
+ connectorCatalog: PLAN_CONNECTOR_CATALOG,
140
+ },
135
141
  durableBackgroundRuns: true,
136
142
  runSoftTimeoutMs: PLAN_BACKGROUND_RUN_SOFT_TIMEOUT_MS,
137
143
  };
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -103,18 +103,50 @@ export interface ResolvedFrameworkTools {
103
103
  * "why can't the agent see db-query" report ends up unexplainable.
104
104
  */
105
105
  export declare function resolveFrameworkTools(input: FrameworkToolsInput | undefined): ResolvedFrameworkTools;
106
+ /**
107
+ * Which `frameworkTools` switch owns each framework action kit.
108
+ *
109
+ * This map is the authority. `ActionEntry.frameworkGroup` is an optional
110
+ * pre-resolved copy of the same answer, not a requirement: it is stamped only
111
+ * by `mergeCoreSharingActions`, which runs against the ungated `httpActions`
112
+ * registry, so for a year every app loading core kits through
113
+ * `loadActionsFromStaticRegistry` or its own actions directory carried
114
+ * untagged entries and silently ignored eight of the `frameworkTools`
115
+ * switches. Resolve by name and the switch works no matter how the action was
116
+ * registered.
117
+ *
118
+ * Membership does two jobs: one filter subtracts a whole kit from every agent
119
+ * tool surface at once (the plugin spreads these registries at thirteen
120
+ * composition sites), and it keeps these ~45 schemas out of the DEFAULT
121
+ * first-request tool list. They stay reachable through `tool-search`.
122
+ *
123
+ * Anything absent from this map is always-on and must be listed in
124
+ * `ALWAYS_ON_CORE_ACTIONS` instead. `action-discovery.spec.ts` fails when a new
125
+ * core action appears in neither, so "always-on" stays a decision someone made
126
+ * rather than the default that happens when a map goes un-updated.
127
+ */
128
+ export declare const CORE_ACTION_GROUPS: Record<string, FrameworkToolGroup>;
106
129
  /** Structural view of the one field these helpers read, so tagging utilities
107
130
  * stay free of an import cycle with `ActionEntry`. */
108
131
  interface FrameworkGrouped {
109
132
  frameworkGroup?: FrameworkToolGroup;
110
133
  }
134
+ /**
135
+ * Which group owns an action, by name first and tag second.
136
+ *
137
+ * Name first because the name is present on every registration path, while the
138
+ * tag is stamped on exactly one — an app is not less entitled to its
139
+ * `frameworkTools` switches because it loaded core kits from a generated
140
+ * registry instead of `autoDiscoverActions`.
141
+ */
142
+ export declare function resolveFrameworkGroup(name: string, entry: FrameworkGrouped | undefined): FrameworkToolGroup | undefined;
111
143
  /**
112
144
  * Drop every action belonging to a disabled group. Returns the input untouched
113
145
  * when nothing is disabled so the default path allocates nothing.
114
146
  */
115
147
  export declare function filterFrameworkToolGroups<T extends FrameworkGrouped>(actions: Record<string, T>, disabledGroups: ReadonlySet<FrameworkToolGroup>): Record<string, T>;
116
148
  /** True for actions the framework contributes rather than the app's own. */
117
- export declare function isFrameworkGroupedAction(entry: FrameworkGrouped): boolean;
149
+ export declare function isFrameworkGroupedAction(name: string, entry: FrameworkGrouped | undefined): boolean;
118
150
  /**
119
151
  * Whether a group is live, for prompt builders that only receive the disabled
120
152
  * set. Prompt text and tool schemas must agree: a prompt that names a tool the