@agent-native/core 0.126.0 → 0.127.3

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 (181) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +83 -0
  3. package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
  4. package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/index.ts +2 -0
  7. package/corpus/core/src/agent/production-agent.ts +6 -1
  8. package/corpus/core/src/agent/run-manager.ts +37 -0
  9. package/corpus/core/src/agent/run-store.ts +18 -0
  10. package/corpus/core/src/cli/template-sync.ts +96 -0
  11. package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
  12. package/corpus/core/src/client/agent-chat.ts +19 -1
  13. package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
  14. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
  15. package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
  16. package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
  17. package/corpus/core/src/client/use-chat-models.ts +54 -37
  18. package/corpus/core/src/localization/default-messages.ts +11 -0
  19. package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
  20. package/corpus/core/src/mcp/builtin-tools.ts +146 -34
  21. package/corpus/core/src/server/agent-capabilities.ts +8 -1
  22. package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
  23. package/corpus/core/src/server/agent-discovery.ts +1 -1
  24. package/corpus/core/src/server/agent-peer-probe.ts +133 -0
  25. package/corpus/core/src/server/builder-design-systems.ts +1 -1
  26. package/corpus/core/src/server/core-routes-plugin.ts +67 -0
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
  28. package/corpus/core/src/templates/chat/_gitignore +2 -0
  29. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  30. package/corpus/templates/chat/_gitignore +2 -0
  31. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -0
  32. package/corpus/templates/design/actions/create-file.ts +15 -0
  33. package/corpus/templates/design/actions/generate-design.ts +40 -0
  34. package/corpus/templates/design/actions/present-design-variants.ts +16 -0
  35. package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
  36. package/corpus/templates/design/app/pages/design-editor/save-failure.ts +7 -1
  37. package/corpus/templates/design/changelog/2026-07-28-generated-designs-with-malformed-html-are-now-rejected-at-sa.md +6 -0
  38. package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
  39. package/corpus/templates/design/server/source-workspace.ts +1 -0
  40. package/corpus/templates/design/shared/html-integrity.ts +608 -18
  41. package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
  42. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -6
  43. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
  44. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
  45. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
  46. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
  47. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
  48. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
  49. package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
  50. package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
  51. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
  52. package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
  53. package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
  54. package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
  55. package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
  56. package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
  57. package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
  58. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
  59. package/corpus/toolkit/CHANGELOG.md +6 -0
  60. package/corpus/toolkit/package.json +1 -1
  61. package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
  62. package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
  63. package/dist/a2a/index.d.ts +2 -0
  64. package/dist/a2a/index.d.ts.map +1 -1
  65. package/dist/a2a/index.js +1 -0
  66. package/dist/a2a/index.js.map +1 -1
  67. package/dist/agent/production-agent.d.ts +2 -2
  68. package/dist/agent/production-agent.d.ts.map +1 -1
  69. package/dist/agent/production-agent.js +6 -3
  70. package/dist/agent/production-agent.js.map +1 -1
  71. package/dist/agent/run-manager.d.ts +11 -0
  72. package/dist/agent/run-manager.d.ts.map +1 -1
  73. package/dist/agent/run-manager.js +34 -1
  74. package/dist/agent/run-manager.js.map +1 -1
  75. package/dist/agent/run-store.d.ts +8 -0
  76. package/dist/agent/run-store.d.ts.map +1 -1
  77. package/dist/agent/run-store.js +16 -0
  78. package/dist/agent/run-store.js.map +1 -1
  79. package/dist/cli/template-sync.d.ts.map +1 -1
  80. package/dist/cli/template-sync.js +88 -0
  81. package/dist/cli/template-sync.js.map +1 -1
  82. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  83. package/dist/client/MultiTabAssistantChat.js +60 -31
  84. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  85. package/dist/client/agent-chat.d.ts +6 -0
  86. package/dist/client/agent-chat.d.ts.map +1 -1
  87. package/dist/client/agent-chat.js +13 -1
  88. package/dist/client/agent-chat.js.map +1 -1
  89. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  90. package/dist/client/chat/tool-call-display.js +14 -0
  91. package/dist/client/chat/tool-call-display.js.map +1 -1
  92. package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
  93. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  94. package/dist/client/resources/mcp-integration-catalog.js +16 -0
  95. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  96. package/dist/client/settings/AgentsSection.d.ts.map +1 -1
  97. package/dist/client/settings/AgentsSection.js +250 -30
  98. package/dist/client/settings/AgentsSection.js.map +1 -1
  99. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  100. package/dist/client/settings/SecretsSection.js +16 -2
  101. package/dist/client/settings/SecretsSection.js.map +1 -1
  102. package/dist/client/use-chat-models.d.ts.map +1 -1
  103. package/dist/client/use-chat-models.js +46 -27
  104. package/dist/client/use-chat-models.js.map +1 -1
  105. package/dist/collab/routes.d.ts +2 -2
  106. package/dist/collab/struct-routes.d.ts +1 -1
  107. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  108. package/dist/localization/default-messages.d.ts +9 -0
  109. package/dist/localization/default-messages.d.ts.map +1 -1
  110. package/dist/localization/default-messages.js +9 -0
  111. package/dist/localization/default-messages.js.map +1 -1
  112. package/dist/mcp/ask-app-task-handle.d.ts +25 -0
  113. package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
  114. package/dist/mcp/ask-app-task-handle.js +136 -0
  115. package/dist/mcp/ask-app-task-handle.js.map +1 -0
  116. package/dist/mcp/builtin-tools.d.ts +3 -3
  117. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  118. package/dist/mcp/builtin-tools.js +115 -38
  119. package/dist/mcp/builtin-tools.js.map +1 -1
  120. package/dist/observability/routes.d.ts +3 -3
  121. package/dist/progress/routes.d.ts +1 -1
  122. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  123. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  124. package/dist/resources/handlers.d.ts +1 -1
  125. package/dist/secrets/routes.d.ts +9 -9
  126. package/dist/server/agent-capabilities.d.ts +7 -1
  127. package/dist/server/agent-capabilities.d.ts.map +1 -1
  128. package/dist/server/agent-capabilities.js +1 -0
  129. package/dist/server/agent-capabilities.js.map +1 -1
  130. package/dist/server/agent-chat-plugin.d.ts +7 -0
  131. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  132. package/dist/server/agent-chat-plugin.js +16 -1
  133. package/dist/server/agent-chat-plugin.js.map +1 -1
  134. package/dist/server/agent-discovery.d.ts +1 -0
  135. package/dist/server/agent-discovery.d.ts.map +1 -1
  136. package/dist/server/agent-discovery.js +1 -1
  137. package/dist/server/agent-discovery.js.map +1 -1
  138. package/dist/server/agent-peer-probe.d.ts +37 -0
  139. package/dist/server/agent-peer-probe.d.ts.map +1 -0
  140. package/dist/server/agent-peer-probe.js +90 -0
  141. package/dist/server/agent-peer-probe.js.map +1 -0
  142. package/dist/server/builder-design-systems.js +1 -1
  143. package/dist/server/builder-design-systems.js.map +1 -1
  144. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  145. package/dist/server/core-routes-plugin.js +53 -0
  146. package/dist/server/core-routes-plugin.js.map +1 -1
  147. package/dist/server/realtime-token.d.ts +1 -1
  148. package/dist/server/transcribe-voice.d.ts +1 -1
  149. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  150. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  151. package/dist/shared/mcp-embed-headers.js +4 -0
  152. package/dist/shared/mcp-embed-headers.js.map +1 -1
  153. package/dist/templates/chat/_gitignore +2 -0
  154. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
  155. package/docs/content/external-agents-catalog.mdx +14 -11
  156. package/docs/content/mcp-protocol.mdx +9 -4
  157. package/package.json +2 -2
  158. package/src/a2a/index.ts +2 -0
  159. package/src/agent/production-agent.ts +6 -1
  160. package/src/agent/run-manager.ts +37 -0
  161. package/src/agent/run-store.ts +18 -0
  162. package/src/cli/template-sync.ts +96 -0
  163. package/src/client/MultiTabAssistantChat.tsx +75 -43
  164. package/src/client/agent-chat.ts +19 -1
  165. package/src/client/chat/tool-call-display.tsx +25 -0
  166. package/src/client/resources/mcp-integration-catalog.ts +23 -0
  167. package/src/client/settings/AgentsSection.tsx +565 -71
  168. package/src/client/settings/SecretsSection.tsx +71 -36
  169. package/src/client/use-chat-models.ts +54 -37
  170. package/src/localization/default-messages.ts +11 -0
  171. package/src/mcp/ask-app-task-handle.ts +183 -0
  172. package/src/mcp/builtin-tools.ts +146 -34
  173. package/src/server/agent-capabilities.ts +8 -1
  174. package/src/server/agent-chat-plugin.ts +17 -0
  175. package/src/server/agent-discovery.ts +1 -1
  176. package/src/server/agent-peer-probe.ts +133 -0
  177. package/src/server/builder-design-systems.ts +1 -1
  178. package/src/server/core-routes-plugin.ts +67 -0
  179. package/src/shared/mcp-embed-headers.ts +4 -0
  180. package/src/templates/chat/_gitignore +2 -0
  181. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
@@ -21,9 +21,9 @@
21
21
  * `open_app` / `create_workspace_app` return an **absolute** URL on the
22
22
  * *target* app's origin when it differs from this app (so a workspace link
23
23
  * lands in the right app), and a relative path for the same app / standalone.
24
- * `ask_app` routes to a *different* workspace app over A2A when possible and
25
- * reports `routedVia: "a2a"`; otherwise it answers locally
26
- * (`routedVia: "local"`) and never falsely claims cross-app delegation.
24
+ * `ask_app` routes a known *different* workspace app over A2A and reports
25
+ * `routedVia: "a2a"`; unknown cross-app targets fail closed instead of running
26
+ * on the current app. Same-app requests report `routedVia: "local"`.
27
27
  * | `list_templates` | none | `{ templates: [...] }` (allow-list only) |
28
28
  *
29
29
  * Node-only at call time (workspace resolution + scaffolding use `fs`), but
@@ -36,7 +36,15 @@ import type { ActionEntry } from "../agent/production-agent.js";
36
36
  import type { ActionTool } from "../agent/types.js";
37
37
  import { getConfiguredAppBasePath } from "../server/app-base-path.js";
38
38
  import { buildDeepLink } from "../server/deep-link.js";
39
+ import {
40
+ getRequestOrgId,
41
+ getRequestUserEmail,
42
+ } from "../server/request-context.js";
39
43
  import { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from "../shared/embed-auth.js";
44
+ import {
45
+ signAskAppTaskHandle,
46
+ verifyAskAppTaskHandle,
47
+ } from "./ask-app-task-handle.js";
40
48
  import type { MCPConfig } from "./build-server.js";
41
49
  import { embedApp } from "./embed-app.js";
42
50
  import { fetchOrgApps, type OrgApp } from "./org-directory.js";
@@ -112,6 +120,7 @@ interface AskAppTaskResult {
112
120
  app: string;
113
121
  routedVia: "local" | "a2a";
114
122
  taskId: string;
123
+ taskHandle?: string;
115
124
  status: string;
116
125
  response?: string;
117
126
  error?: string;
@@ -120,7 +129,7 @@ interface AskAppTaskResult {
120
129
  pollAfterMs?: number;
121
130
  poll?: {
122
131
  tool: "ask_app_status";
123
- arguments: { app: string; taskId: string };
132
+ arguments: { app: string; taskId: string; taskHandle?: string };
124
133
  };
125
134
  message?: string;
126
135
  statusRead?: "unavailable";
@@ -200,6 +209,27 @@ function selfA2AEndpointUrl(requestMeta?: AskAppRequestMeta): string | null {
200
209
  return agentNativeA2AEndpoint(`${origin}${basePath}`);
201
210
  }
202
211
 
212
+ function askAppIssuerAudience(requestMeta?: AskAppRequestMeta): string | null {
213
+ const origin = requestMeta?.origin?.replace(/\/+$/, "");
214
+ if (!origin) return null;
215
+ try {
216
+ const originUrl = new URL(origin);
217
+ if (
218
+ !["http:", "https:"].includes(originUrl.protocol) ||
219
+ originUrl.username ||
220
+ originUrl.password
221
+ ) {
222
+ return null;
223
+ }
224
+ const basePath = requestMeta?.basePath || getConfiguredAppBasePath();
225
+ const audience = new URL(basePath || "/", `${originUrl.origin}/`);
226
+ const pathname = audience.pathname.replace(/\/+$/, "");
227
+ return `${audience.origin}${pathname}`;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
203
233
  function boundedAskAppWaitMs(raw: unknown): number {
204
234
  if (raw == null || raw === "") return ASK_APP_DEFAULT_INLINE_WAIT_MS;
205
235
  const parsed = Number(raw);
@@ -231,13 +261,18 @@ function taskText(task: Task): string {
231
261
  );
232
262
  }
233
263
 
234
- function askAppTaskResult(route: AskAppRoute, task: Task): AskAppTaskResult {
264
+ function askAppTaskResult(
265
+ route: AskAppRoute,
266
+ task: Task,
267
+ taskHandle?: string,
268
+ ): AskAppTaskResult {
235
269
  const status = taskState(task);
236
270
  const response = taskText(task);
237
271
  const base = {
238
272
  app: route.app,
239
273
  routedVia: route.routedVia,
240
274
  taskId: task.id,
275
+ ...(taskHandle ? { taskHandle } : {}),
241
276
  status,
242
277
  ...(route.note ? { note: route.note } : {}),
243
278
  };
@@ -273,11 +308,17 @@ function askAppTaskResult(route: AskAppRoute, task: Task): AskAppTaskResult {
273
308
  pollAfterMs: ASK_APP_POLL_INTERVAL_MS,
274
309
  poll: {
275
310
  tool: "ask_app_status",
276
- arguments: { app: route.app, taskId: task.id },
311
+ arguments: {
312
+ app: route.app,
313
+ taskId: task.id,
314
+ ...(taskHandle ? { taskHandle } : {}),
315
+ },
277
316
  },
278
317
  message:
279
318
  `ask_app is still ${status}. Call ask_app_status with ` +
280
- `taskId "${task.id}" to retrieve the final response.`,
319
+ (taskHandle
320
+ ? "the returned taskHandle to retrieve the final response."
321
+ : `taskId "${task.id}" to retrieve the final response.`),
281
322
  };
282
323
  }
283
324
 
@@ -412,6 +453,8 @@ async function waitForA2ATask(
412
453
 
413
454
  async function submitAskAppA2ATask(
414
455
  route: AskAppRoute,
456
+ issuerApp: string,
457
+ issuerAudience: string,
415
458
  message: string,
416
459
  maxWaitMs: number,
417
460
  approvedActions?: A2AApprovedAction[],
@@ -434,12 +477,23 @@ async function submitAskAppA2ATask(
434
477
  },
435
478
  );
436
479
  const finalOrRunning = await waitForA2ATask(client, task, deadline);
437
- return askAppTaskResult(route, finalOrRunning);
480
+ const subject =
481
+ typeof metadata.userEmail === "string" ? metadata.userEmail : "anonymous";
482
+ const taskHandle = await signAskAppTaskHandle({
483
+ issuerApp,
484
+ issuerAudience,
485
+ organization: getRequestOrgId() ?? "",
486
+ subject,
487
+ route,
488
+ taskId: task.id,
489
+ });
490
+ return askAppTaskResult(route, finalOrRunning, taskHandle);
438
491
  }
439
492
 
440
493
  async function fetchAskAppA2ATask(
441
494
  route: AskAppRoute,
442
495
  taskId: string,
496
+ taskHandle?: string,
443
497
  ): Promise<AskAppTaskResult> {
444
498
  const { client } = await createA2AClientForAskApp(route.origin);
445
499
  const maxAttempts = ASK_APP_STATUS_RETRY_DELAYS_MS.length + 1;
@@ -451,7 +505,7 @@ async function fetchAskAppA2ATask(
451
505
  const startedAt = Date.now();
452
506
  try {
453
507
  const task = await client.getTask(taskId);
454
- return askAppTaskResult(route, task);
508
+ return askAppTaskResult(route, task, taskHandle);
455
509
  } catch (err) {
456
510
  const delayMs = ASK_APP_STATUS_RETRY_DELAYS_MS[attempt];
457
511
  const errorCategory = askAppStatusErrorCategory(err);
@@ -482,6 +536,7 @@ async function fetchAskAppA2ATask(
482
536
  taskId,
483
537
  errorCategory,
484
538
  maxAttempts,
539
+ taskHandle,
485
540
  );
486
541
  }
487
542
  await new Promise((resolve) => setTimeout(resolve, delayMs));
@@ -541,11 +596,13 @@ function askAppStatusReadUnavailableResult(
541
596
  taskId: string,
542
597
  errorCategory: AskAppStatusErrorCategory,
543
598
  attempts: number,
599
+ taskHandle?: string,
544
600
  ): AskAppTaskResult {
545
601
  return {
546
602
  app: route.app,
547
603
  routedVia: route.routedVia,
548
604
  taskId,
605
+ ...(taskHandle ? { taskHandle } : {}),
549
606
  status: "unknown",
550
607
  statusRead: "unavailable",
551
608
  retryable: true,
@@ -554,12 +611,18 @@ function askAppStatusReadUnavailableResult(
554
611
  pollAfterMs: ASK_APP_POLL_INTERVAL_MS,
555
612
  poll: {
556
613
  tool: "ask_app_status",
557
- arguments: { app: route.app, taskId },
614
+ arguments: {
615
+ app: route.app,
616
+ taskId,
617
+ ...(taskHandle ? { taskHandle } : {}),
618
+ },
558
619
  },
559
620
  message:
560
621
  "The durable ask_app task status could not be read after bounded retries. " +
561
622
  "The task may still be running or completed. Retry ask_app_status " +
562
- "with the same app and taskId; do not resubmit ask_app.",
623
+ (taskHandle
624
+ ? "with the same taskHandle; do not resubmit ask_app."
625
+ : "with the same app and taskId; do not resubmit ask_app."),
563
626
  };
564
627
  }
565
628
 
@@ -631,7 +694,7 @@ function listAppsTool(
631
694
  livePort = 0;
632
695
  }
633
696
  }
634
- const selfId = (config.appId ?? "").toLowerCase();
697
+ const selfId = currentAppId(config);
635
698
  const isSelf = (id: string) =>
636
699
  !!liveOrigin &&
637
700
  (!ws.isWorkspace || (!!selfId && id.toLowerCase() === selfId));
@@ -644,7 +707,10 @@ function listAppsTool(
644
707
  source: "workspace" | "org-directory";
645
708
  }
646
709
 
647
- const apps: AppEntry[] = ws.apps.map((a) =>
710
+ const resolvedApps = ws.isWorkspace
711
+ ? ws.apps
712
+ : ws.apps.map((app) => ({ ...app, id: selfId, name: config.name }));
713
+ const apps: AppEntry[] = resolvedApps.map((a) =>
648
714
  isSelf(a.id)
649
715
  ? {
650
716
  id: a.id,
@@ -991,6 +1057,8 @@ async function routeAskOverA2A(
991
1057
  message: string,
992
1058
  options?: {
993
1059
  durable?: boolean;
1060
+ issuerApp?: string;
1061
+ issuerAudience?: string;
994
1062
  maxWaitMs?: number;
995
1063
  requestOrigin?: string;
996
1064
  approvedActions?: A2AApprovedAction[];
@@ -999,6 +1067,11 @@ async function routeAskOverA2A(
999
1067
  { app: string; routedVia: "a2a"; response: string } | AskAppTaskResult
1000
1068
  > {
1001
1069
  if (options?.durable) {
1070
+ if (!options.issuerApp || !options.issuerAudience) {
1071
+ throw new Error(
1072
+ "ask_app durable routing requires an issuer app id and audience.",
1073
+ );
1074
+ }
1002
1075
  return submitAskAppA2ATask(
1003
1076
  {
1004
1077
  app: id,
@@ -1006,6 +1079,8 @@ async function routeAskOverA2A(
1006
1079
  routedVia: "a2a",
1007
1080
  requestOrigin: options.requestOrigin ?? origin,
1008
1081
  },
1082
+ options.issuerApp,
1083
+ options.issuerAudience,
1009
1084
  message,
1010
1085
  options.maxWaitMs ?? ASK_APP_DEFAULT_INLINE_WAIT_MS,
1011
1086
  options.approvedActions,
@@ -1097,8 +1172,8 @@ function askAppTool(
1097
1172
  "param is optional (defaults to this app). When 'app' names a " +
1098
1173
  "different workspace app it is routed there over A2A; the result's " +
1099
1174
  "'routedVia' field reports whether it ran cross-app or locally. " +
1100
- "On hosted MCP, long tasks may return a durable taskId instead of a " +
1101
- "final response; call ask_app_status with that taskId until completed.",
1175
+ "On hosted MCP, long tasks may return a durable taskHandle instead of " +
1176
+ "a final response; pass that handle to ask_app_status until completed.",
1102
1177
  {
1103
1178
  app: {
1104
1179
  type: "string",
@@ -1111,12 +1186,12 @@ function askAppTool(
1111
1186
  async: {
1112
1187
  type: "boolean",
1113
1188
  description:
1114
- "When true, start a durable task and return immediately with a taskId.",
1189
+ "When true, start a durable task and return immediately with a taskHandle and legacy taskId.",
1115
1190
  },
1116
1191
  maxWaitMs: {
1117
1192
  type: "number",
1118
1193
  description:
1119
- "Maximum time to wait inline before returning a taskId. Hosted MCP clamps this to 25000ms.",
1194
+ "Maximum time to wait inline before returning a taskHandle. Hosted MCP clamps this to 25000ms.",
1120
1195
  },
1121
1196
  approvedActions: {
1122
1197
  type: "array",
@@ -1140,6 +1215,12 @@ function askAppTool(
1140
1215
  const requestedApp = String(args.app ?? "").trim();
1141
1216
  const selfId = currentAppId(config);
1142
1217
  const useDurableA2A = Boolean(requestMeta?.origin);
1218
+ const issuerAudience = askAppIssuerAudience(requestMeta);
1219
+ if (useDurableA2A && !issuerAudience) {
1220
+ throw new Error(
1221
+ "ask_app durable routing requires a valid HTTP(S) issuer audience.",
1222
+ );
1223
+ }
1143
1224
  const maxWaitMs = isExplicitAsyncAsk(args.async)
1144
1225
  ? 0
1145
1226
  : boundedAskAppWaitMs(args.maxWaitMs);
@@ -1160,6 +1241,8 @@ function askAppTool(
1160
1241
  message,
1161
1242
  {
1162
1243
  durable: useDurableA2A,
1244
+ issuerApp: selfId,
1245
+ issuerAudience: issuerAudience ?? undefined,
1163
1246
  maxWaitMs,
1164
1247
  requestOrigin: targetApp.origin,
1165
1248
  approvedActions,
@@ -1195,6 +1278,8 @@ function askAppTool(
1195
1278
  message,
1196
1279
  {
1197
1280
  durable: useDurableA2A,
1281
+ issuerApp: selfId,
1282
+ issuerAudience: issuerAudience ?? undefined,
1198
1283
  maxWaitMs,
1199
1284
  requestOrigin: dirMatch.url,
1200
1285
  approvedActions,
@@ -1209,7 +1294,14 @@ function askAppTool(
1209
1294
  }
1210
1295
  }
1211
1296
 
1212
- // Same app (or no workspace / unknown target): answer locally with this
1297
+ if (requestedApp && requestedApp.toLowerCase() !== selfId) {
1298
+ throw new Error(
1299
+ `No reachable ask_app route for app "${requestedApp}". ` +
1300
+ "Call list_apps and retry with an available app id.",
1301
+ );
1302
+ }
1303
+
1304
+ // Same app (or no target): answer locally with this
1213
1305
  // app's own ask-agent handler — the same entry point the HTTP MCP mount
1214
1306
  // + A2A use, so there is no second agent runner.
1215
1307
  if (!config.askAgent) {
@@ -1218,15 +1310,6 @@ function askAppTool(
1218
1310
  );
1219
1311
  }
1220
1312
 
1221
- // If the caller named an app we couldn't route to (unknown id, or no
1222
- // workspace), say so honestly instead of claiming we reached it.
1223
- const unresolved =
1224
- !!requestedApp && requestedApp.toLowerCase() !== selfId;
1225
- const note = unresolved
1226
- ? `Requested app "${requestedApp}" is not a reachable workspace ` +
1227
- `app; answered with this app ("${selfId}") instead.`
1228
- : undefined;
1229
-
1230
1313
  // Hosted MCP cannot safely keep a JSON request/response open for a full
1231
1314
  // agent loop: serverless gateways can return an inactivity 504 before
1232
1315
  // the result body exists. When we know the running app origin, submit the
@@ -1240,8 +1323,9 @@ function askAppTool(
1240
1323
  origin: localA2AEndpointUrl,
1241
1324
  routedVia: "local",
1242
1325
  requestOrigin: requestMeta?.origin,
1243
- ...(note ? { note } : {}),
1244
1326
  },
1327
+ selfId,
1328
+ issuerAudience ?? "",
1245
1329
  message,
1246
1330
  maxWaitMs,
1247
1331
  approvedActions,
@@ -1267,7 +1351,6 @@ function askAppTool(
1267
1351
  return {
1268
1352
  app: selfId,
1269
1353
  routedVia: "local",
1270
- ...(note ? { note } : {}),
1271
1354
  response: inline.response || "(no response)",
1272
1355
  };
1273
1356
  }
@@ -1276,7 +1359,7 @@ function askAppTool(
1276
1359
  // same way the old unbounded `await config.askAgent(message)` did.
1277
1360
  throw new Error(inline.error || "ask_app task failed.");
1278
1361
  }
1279
- return askAppInlineTaskResult(selfId, inline.taskId, inline, note);
1362
+ return askAppInlineTaskResult(selfId, inline.taskId, inline);
1280
1363
  },
1281
1364
  };
1282
1365
  }
@@ -1300,16 +1383,45 @@ function askAppStatusTool(
1300
1383
  },
1301
1384
  taskId: {
1302
1385
  type: "string",
1303
- description: "The durable task id returned by ask_app.",
1386
+ description:
1387
+ "Legacy durable task id returned by ask_app. Use taskHandle when present.",
1388
+ },
1389
+ taskHandle: {
1390
+ type: "string",
1391
+ description:
1392
+ "Opaque task handle returned by ask_app. It preserves the original route across cold starts and discovery changes.",
1304
1393
  },
1305
1394
  },
1306
- ["taskId"],
1307
1395
  ),
1308
1396
  readOnly: true,
1309
1397
  parallelSafe: true,
1310
1398
  run: async (args: Record<string, any>) => {
1311
- const taskId = String(args.taskId ?? "").trim();
1312
- if (!taskId) throw new Error("ask_app_status requires 'taskId'.");
1399
+ const suppliedTaskId = String(args.taskId ?? "").trim();
1400
+ const taskHandle = String(args.taskHandle ?? "").trim();
1401
+ if (!taskHandle && !suppliedTaskId) {
1402
+ throw new Error("ask_app_status requires 'taskHandle' or 'taskId'.");
1403
+ }
1404
+
1405
+ if (taskHandle) {
1406
+ const verified = await verifyAskAppTaskHandle(taskHandle, {
1407
+ issuerApp: currentAppId(config),
1408
+ issuerAudience: askAppIssuerAudience(requestMeta) ?? "",
1409
+ organization: getRequestOrgId() ?? "",
1410
+ subject: getRequestUserEmail() ?? "anonymous",
1411
+ });
1412
+ const suppliedApp = String(args.app ?? "")
1413
+ .trim()
1414
+ .toLowerCase();
1415
+ if (
1416
+ (suppliedTaskId && suppliedTaskId !== verified.taskId) ||
1417
+ (suppliedApp && suppliedApp !== verified.route.app.toLowerCase())
1418
+ ) {
1419
+ throw new Error("Invalid or expired ask_app task handle.");
1420
+ }
1421
+ return fetchAskAppA2ATask(verified.route, verified.taskId, taskHandle);
1422
+ }
1423
+
1424
+ const taskId = suppliedTaskId;
1313
1425
 
1314
1426
  // The no-derivable-origin ask_app fallback tracks its tasks in a
1315
1427
  // process-local map, not the durable A2A store. Check it FIRST —
@@ -1,5 +1,5 @@
1
1
  import { A2AClient } from "../a2a/client.js";
2
- import type { AgentSkill } from "../a2a/types.js";
2
+ import type { AgentCard, AgentSkill } from "../a2a/types.js";
3
3
  import { type DiscoveredAgent } from "./agent-discovery.js";
4
4
 
5
5
  /** Matches the `<available-apps>` prompt block so both surfaces agree. */
@@ -16,6 +16,12 @@ export interface PeerCapabilities {
16
16
  skills: AgentSkill[] | null;
17
17
  cardDescription?: string;
18
18
  error?: string;
19
+ /**
20
+ * The full card from this same fetch, for callers that need more than
21
+ * skills/description (e.g. the peer-probe route's name/securitySchemes).
22
+ * Kept optional so existing consumers of this shape are unaffected.
23
+ */
24
+ card?: AgentCard;
19
25
  }
20
26
 
21
27
  function truncate(value: string, max: number): string {
@@ -35,6 +41,7 @@ export async function loadCapabilities(
35
41
  skills: Array.isArray(card.skills) ? card.skills : [],
36
42
  cardDescription:
37
43
  typeof card.description === "string" ? card.description : undefined,
44
+ card,
38
45
  };
39
46
  } catch (error) {
40
47
  return {
@@ -74,6 +74,7 @@ import {
74
74
  toolCallCacheKey,
75
75
  getActiveRunForThreadAsync,
76
76
  abortRunDurably,
77
+ abortTurnDurably,
77
78
  subscribeToRun,
78
79
  type ActionEntry,
79
80
  } from "../agent/production-agent.js";
@@ -441,6 +442,19 @@ const generateTitleRateLimit = new Map<string, number[]>();
441
442
  * so the common small-map case stays O(1). */
442
443
  const RATE_LIMIT_SWEEP_THRESHOLD = 1000;
443
444
 
445
+ /**
446
+ * Abort reasons that mean a human asked to stop, so `/runs/:id/abort` escalates
447
+ * to a turn-wide abort. Watchdog reasons (`no_progress`, `auto_stuck_retry`)
448
+ * stay single-run: they end a chunk the client believes is dead and must not
449
+ * kill a server-side continuation chain that is still making progress.
450
+ */
451
+ export const USER_STOP_ABORT_REASONS = new Set([
452
+ "user",
453
+ "abort",
454
+ "user_stuck_cancel",
455
+ "user_stuck_retry",
456
+ ]);
457
+
444
458
  /**
445
459
  * Pick the URL allowlist to enforce for a `${keys.NAME}` reference used by
446
460
  * the agent fetch tool.
@@ -4572,6 +4586,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4572
4586
  // the cross-isolate abort + terminal event to be durable. Returning
4573
4587
  // early lets Retry collide with the still-running row.
4574
4588
  await abortRunDurably(runId, reason);
4589
+ if (USER_STOP_ABORT_REASONS.has(reason)) {
4590
+ await abortTurnDurably(runId, reason);
4591
+ }
4575
4592
  return { ok: true };
4576
4593
  }
4577
4594
 
@@ -79,7 +79,7 @@ const HIDDEN_FIRST_PARTY_AGENT_IDS = new Set([
79
79
  "workbench",
80
80
  ]);
81
81
 
82
- function normalizeAgentId(id: string): string {
82
+ export function normalizeAgentId(id: string): string {
83
83
  const normalized = id.trim().toLowerCase();
84
84
  if (
85
85
  normalized === "image" ||
@@ -0,0 +1,133 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import { resolveA2ACallerAuth } from "../a2a/caller-auth.js";
4
+ import { A2AClient } from "../a2a/client.js";
5
+ import {
6
+ loadCapabilities,
7
+ type PeerCapabilities,
8
+ } from "./agent-capabilities.js";
9
+ import type { DiscoveredAgent } from "./agent-discovery.js";
10
+
11
+ const AUTH_PROBE_TIMEOUT_MS = 6_000;
12
+ /** Matches CARD_CONCURRENCY in agent-capabilities.ts — bounds simultaneous
13
+ * outbound probes the same way card loading already does. */
14
+ const PROBE_CONCURRENCY = 8;
15
+
16
+ export interface PeerProbeResult {
17
+ url: string;
18
+ reachable: boolean;
19
+ name?: string;
20
+ description?: string;
21
+ securitySchemes?: string[];
22
+ /** Absent (not false) whenever the auth check never ran or never resolved. */
23
+ authorized?: boolean;
24
+ authError?: string;
25
+ publicSkills?: number;
26
+ error?: string;
27
+ }
28
+
29
+ export interface PeerProbeDeps {
30
+ loadCapabilities: (agent: DiscoveredAgent) => Promise<PeerCapabilities>;
31
+ resolveCallerAuth: typeof resolveA2ACallerAuth;
32
+ createClient: (
33
+ baseUrl: string,
34
+ apiKey?: string,
35
+ options?: { requestTimeoutMs?: number; fallbackApiKeys?: string[] },
36
+ ) => Pick<A2AClient, "getTask">;
37
+ }
38
+
39
+ const defaultPeerProbeDeps: PeerProbeDeps = {
40
+ loadCapabilities,
41
+ resolveCallerAuth: resolveA2ACallerAuth,
42
+ createClient: (baseUrl, apiKey, options) =>
43
+ new A2AClient(baseUrl, apiKey, options),
44
+ };
45
+
46
+ /**
47
+ * Probe one peer: does its agent card load (reachability), and separately,
48
+ * will OUR calls to it authenticate. These two questions must never collapse
49
+ * into one boolean — an unreachable peer can't tell us anything about auth,
50
+ * so `authorized` stays absent rather than `false` in that case, and a
51
+ * transport failure on the auth-only call (timeout, DNS, 5xx) must never be
52
+ * reported as an auth rejection either.
53
+ */
54
+ export async function probePeerAgent(
55
+ agent: DiscoveredAgent,
56
+ deps: PeerProbeDeps = defaultPeerProbeDeps,
57
+ ): Promise<PeerProbeResult> {
58
+ const capabilities = await deps.loadCapabilities(agent);
59
+ if (capabilities.skills === null || !capabilities.card) {
60
+ // Unreachable (includes malformed/SSRF-blocked URLs, which the caller
61
+ // reclassifies into a 400 by checking for the "SSRF blocked:" prefix).
62
+ // `authorized` is intentionally omitted — reachability and auth are
63
+ // independent questions, and we have no evidence either way here.
64
+ return {
65
+ url: agent.url,
66
+ reachable: false,
67
+ error: capabilities.error ?? "unreachable",
68
+ };
69
+ }
70
+
71
+ const card = capabilities.card;
72
+ const result: PeerProbeResult = {
73
+ url: agent.url,
74
+ reachable: true,
75
+ name: card.name,
76
+ description: capabilities.cardDescription,
77
+ securitySchemes: card.securitySchemes
78
+ ? Object.keys(card.securitySchemes)
79
+ : undefined,
80
+ publicSkills: capabilities.skills.length,
81
+ };
82
+
83
+ const auth = await deps.resolveCallerAuth();
84
+ const client = deps.createClient(agent.url, auth.apiKey, {
85
+ requestTimeoutMs: AUTH_PROBE_TIMEOUT_MS,
86
+ fallbackApiKeys: auth.apiKeyFallbacks,
87
+ });
88
+
89
+ try {
90
+ // An id that cannot exist. A "task not found" JSON-RPC error proves the
91
+ // peer authenticated us and ran our request; a 401 proves it rejected us;
92
+ // anything else is a transport failure this call can't resolve either way.
93
+ await client.getTask(`probe-${crypto.randomUUID()}`, {
94
+ requestTimeoutMs: AUTH_PROBE_TIMEOUT_MS,
95
+ });
96
+ result.authorized = true;
97
+ } catch (err) {
98
+ const message = err instanceof Error ? err.message : String(err);
99
+ if (/A2A request failed \(401\)/.test(message)) {
100
+ result.authorized = false;
101
+ result.authError = "401";
102
+ } else if (/A2A error \(-?\d+\): task not found/i.test(message)) {
103
+ result.authorized = true;
104
+ } else {
105
+ // Timeout, DNS failure, non-401 HTTP status, etc. The card fetch above
106
+ // already proved reachability, so this must not be reported as
107
+ // `reachable: false`, and an unresolved auth outcome must not be
108
+ // reported as `authorized: false` either — leave it absent.
109
+ result.authError = message;
110
+ }
111
+ }
112
+
113
+ return result;
114
+ }
115
+
116
+ export async function probeAllPeerAgents(
117
+ agents: DiscoveredAgent[],
118
+ deps: PeerProbeDeps = defaultPeerProbeDeps,
119
+ ): Promise<Array<PeerProbeResult & { id: string }>> {
120
+ const results: Array<PeerProbeResult & { id: string }> = [];
121
+ for (let i = 0; i < agents.length; i += PROBE_CONCURRENCY) {
122
+ const batch = agents.slice(i, i + PROBE_CONCURRENCY);
123
+ results.push(
124
+ ...(await Promise.all(
125
+ batch.map(async (agent) => ({
126
+ id: agent.id,
127
+ ...(await probePeerAgent(agent, deps)),
128
+ })),
129
+ )),
130
+ );
131
+ }
132
+ return results;
133
+ }
@@ -140,7 +140,7 @@ function trimTrailingSlash(value: string): string {
140
140
  export function getBuilderDesignSystemsBaseUrl(): string {
141
141
  return (
142
142
  process.env.BUILDER_DESIGN_SYSTEMS_BASE_URL ||
143
- `${trimTrailingSlash(getBuilderProxyOrigin())}/agent-native/design-systems/v1`
143
+ `${trimTrailingSlash(getBuilderProxyOrigin())}/design-systems/v1`
144
144
  );
145
145
  }
146
146