@agent-native/core 0.101.7 → 0.101.11

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 (219) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/deployment.mdx +9 -7
  4. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
  5. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
  6. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
  7. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
  8. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
  9. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
  10. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
  11. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
  12. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
  13. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/client.ts +23 -1
  16. package/corpus/core/src/a2a/handlers.ts +65 -1
  17. package/corpus/core/src/a2a/server.ts +124 -0
  18. package/corpus/core/src/a2a/task-store.ts +253 -0
  19. package/corpus/core/src/a2a/types.ts +26 -0
  20. package/corpus/core/src/agent/production-agent.ts +5 -2
  21. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  22. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  23. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  24. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  25. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  26. package/corpus/core/src/mcp/builtin-tools.ts +25 -1
  27. package/corpus/core/src/scripts/call-agent.ts +22 -3
  28. package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
  29. package/corpus/core/src/server/auth.ts +25 -1
  30. package/corpus/core/src/server/builder-browser.ts +353 -1
  31. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  32. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
  33. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  34. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  35. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  36. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  37. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  38. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  39. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  40. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  41. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
  42. package/corpus/templates/content/AGENTS.md +3 -2
  43. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  44. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  45. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  46. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  47. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  48. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  49. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  50. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  51. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  52. package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
  53. package/corpus/templates/content/actions/_database-utils.ts +89 -12
  54. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  55. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  56. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  57. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  58. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  59. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  60. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  61. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  62. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  63. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  64. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  65. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
  66. package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
  67. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
  68. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  69. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  70. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
  71. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  72. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  73. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
  74. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  75. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  76. package/corpus/templates/content/app/global.css +71 -1
  77. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  78. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  79. package/corpus/templates/content/app/i18n/zh-TW.ts +38 -0
  80. package/corpus/templates/content/app/i18n-data.ts +450 -0
  81. package/corpus/templates/content/app/root.tsx +2 -0
  82. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  83. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  93. package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
  94. package/corpus/templates/content/package.json +1 -0
  95. package/corpus/templates/content/parity/matrix.md +27 -25
  96. package/corpus/templates/content/parity/matrix.ts +50 -2
  97. package/corpus/templates/content/server/db/schema.ts +47 -0
  98. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  99. package/corpus/templates/content/server/plugins/db.ts +53 -0
  100. package/corpus/templates/content/shared/api.ts +70 -1
  101. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  102. package/corpus/templates/content/shared/document-export.ts +296 -25
  103. package/corpus/templates/content/shared/inline-math.ts +128 -0
  104. package/corpus/templates/content/shared/math-rendering.ts +50 -0
  105. package/corpus/templates/content/shared/nfm.ts +20 -17
  106. package/corpus/templates/content/vite.config.ts +0 -1
  107. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  108. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  109. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  110. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  111. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  112. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  113. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  114. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  115. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  116. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  117. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  118. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  119. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  120. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  121. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  122. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  123. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  124. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  125. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  126. package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
  127. package/corpus/templates/mail/AGENTS.md +21 -19
  128. package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
  129. package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
  130. package/corpus/templates/mail/server/handlers/media.ts +84 -61
  131. package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
  132. package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
  133. package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
  134. package/corpus/templates/mail/server/plugins/auth.ts +9 -1
  135. package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
  136. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  137. package/dist/a2a/client.d.ts +5 -1
  138. package/dist/a2a/client.d.ts.map +1 -1
  139. package/dist/a2a/client.js +18 -1
  140. package/dist/a2a/client.js.map +1 -1
  141. package/dist/a2a/handlers.d.ts.map +1 -1
  142. package/dist/a2a/handlers.js +57 -8
  143. package/dist/a2a/handlers.js.map +1 -1
  144. package/dist/a2a/server.d.ts.map +1 -1
  145. package/dist/a2a/server.js +87 -1
  146. package/dist/a2a/server.js.map +1 -1
  147. package/dist/a2a/task-store.d.ts +27 -0
  148. package/dist/a2a/task-store.d.ts.map +1 -1
  149. package/dist/a2a/task-store.js +216 -0
  150. package/dist/a2a/task-store.js.map +1 -1
  151. package/dist/a2a/types.d.ts +24 -0
  152. package/dist/a2a/types.d.ts.map +1 -1
  153. package/dist/a2a/types.js.map +1 -1
  154. package/dist/agent/production-agent.d.ts +1 -0
  155. package/dist/agent/production-agent.d.ts.map +1 -1
  156. package/dist/agent/production-agent.js +5 -2
  157. package/dist/agent/production-agent.js.map +1 -1
  158. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  159. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  160. package/dist/client/review/ReviewCommentComposer.js +3 -3
  161. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  162. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  163. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  164. package/dist/client/review/ReviewThreadPanel.js +6 -4
  165. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  166. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  167. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  168. package/dist/client/settings/SettingsPanel.js +11 -4
  169. package/dist/client/settings/SettingsPanel.js.map +1 -1
  170. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  171. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  172. package/dist/client/settings/useBuilderStatus.js +19 -6
  173. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  174. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  175. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  176. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  177. package/dist/collab/awareness.d.ts +2 -2
  178. package/dist/collab/awareness.d.ts.map +1 -1
  179. package/dist/collab/struct-routes.d.ts +1 -1
  180. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  181. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  182. package/dist/mcp/builtin-tools.js +22 -3
  183. package/dist/mcp/builtin-tools.js.map +1 -1
  184. package/dist/notifications/routes.d.ts +3 -3
  185. package/dist/resources/handlers.d.ts +2 -2
  186. package/dist/scripts/call-agent.d.ts +1 -1
  187. package/dist/scripts/call-agent.d.ts.map +1 -1
  188. package/dist/scripts/call-agent.js +19 -1
  189. package/dist/scripts/call-agent.js.map +1 -1
  190. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  191. package/dist/server/agent-chat-plugin.js +71 -2
  192. package/dist/server/agent-chat-plugin.js.map +1 -1
  193. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  194. package/dist/server/auth.d.ts +1 -0
  195. package/dist/server/auth.d.ts.map +1 -1
  196. package/dist/server/auth.js +19 -2
  197. package/dist/server/auth.js.map +1 -1
  198. package/dist/server/builder-browser.d.ts +78 -0
  199. package/dist/server/builder-browser.d.ts.map +1 -1
  200. package/dist/server/builder-browser.js +246 -1
  201. package/dist/server/builder-browser.js.map +1 -1
  202. package/dist/server/core-routes-plugin.d.ts +41 -0
  203. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  204. package/dist/server/core-routes-plugin.js +260 -14
  205. package/dist/server/core-routes-plugin.js.map +1 -1
  206. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
  207. package/docs/content/deployment.mdx +9 -7
  208. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  209. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  210. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  211. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  212. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  213. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  214. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  215. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  216. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  217. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  218. package/package.json +2 -2
  219. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
@@ -27,8 +27,10 @@ import {
27
27
  settleProcessingA2ATask,
28
28
  touchQueuedA2ATaskDispatch,
29
29
  touchProcessingA2ATask,
30
+ pauseProcessingA2ATask,
30
31
  } from "./task-store.js";
31
32
  import type {
33
+ A2AApprovedAction,
32
34
  A2AConfig,
33
35
  A2AHandler,
34
36
  A2AHandlerContext,
@@ -46,6 +48,30 @@ const PORTABLE_FALLBACK_HANDOFF_TIMEOUT_MS = 1_000;
46
48
  const A2A_QUEUED_DISPATCH_STUCK_AFTER_MS = 10_000;
47
49
  const A2A_PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;
48
50
  const A2A_PROCESSING_HEARTBEAT_MS = 30_000;
51
+ const MAX_A2A_APPROVED_ACTIONS = 10;
52
+
53
+ function trustedApprovedActions(
54
+ value: unknown,
55
+ event: any | undefined,
56
+ ): A2AApprovedAction[] | undefined {
57
+ // Static API keys and unsigned requests do not prove which user authorized
58
+ // a consequential action. Only a verified identity-bearing JWT may carry
59
+ // chat authorization across the A2A boundary.
60
+ if (!event?.context?.__a2aVerifiedEmail || !Array.isArray(value)) {
61
+ return undefined;
62
+ }
63
+ const approved = value
64
+ .slice(0, MAX_A2A_APPROVED_ACTIONS)
65
+ .filter(
66
+ (candidate): candidate is A2AApprovedAction =>
67
+ !!candidate &&
68
+ typeof candidate === "object" &&
69
+ typeof (candidate as Record<string, unknown>).tool === "string" &&
70
+ !!(candidate as Record<string, unknown>).tool,
71
+ )
72
+ .map((candidate) => ({ tool: candidate.tool, input: candidate.input }));
73
+ return approved.length > 0 ? approved : undefined;
74
+ }
49
75
 
50
76
  /**
51
77
  * Request origin is routing/link context, not an identity signal. Accept only
@@ -250,6 +276,9 @@ export async function processA2ATaskFromQueue(
250
276
  | Record<string, unknown>
251
277
  | null
252
278
  | undefined) ?? undefined;
279
+ const approvedActions = Array.isArray(processorMeta.approvedActions)
280
+ ? (processorMeta.approvedActions as A2AApprovedAction[])
281
+ : undefined;
253
282
 
254
283
  const resolvedOrgId = await resolveVerifiedA2AOrgId(
255
284
  verifiedEmail,
@@ -281,6 +310,7 @@ export async function processA2ATaskFromQueue(
281
310
  contextId,
282
311
  callerMetadata,
283
312
  event,
313
+ approvedActions,
284
314
  ),
285
315
  );
286
316
  } catch (err: any) {
@@ -386,6 +416,7 @@ function makeHandlerContext(
386
416
  contextId?: string,
387
417
  metadata?: Record<string, unknown>,
388
418
  event?: any,
419
+ approvedActions?: A2AApprovedAction[],
389
420
  ): {
390
421
  context: A2AHandlerContext;
391
422
  artifacts: Artifact[];
@@ -396,6 +427,7 @@ function makeHandlerContext(
396
427
  contextId,
397
428
  metadata,
398
429
  event,
430
+ approvedActions,
399
431
  writeArtifact(name, content, mimeType) {
400
432
  const artifact: Artifact = {
401
433
  name,
@@ -491,12 +523,14 @@ async function runHandlerAndPersist(
491
523
  contextId: string | undefined,
492
524
  metadata: Record<string, unknown> | undefined,
493
525
  event?: any,
526
+ approvedActions?: A2AApprovedAction[],
494
527
  ): Promise<void> {
495
528
  const { context, artifacts } = makeHandlerContext(
496
529
  taskId,
497
530
  contextId,
498
531
  metadata,
499
532
  event,
533
+ approvedActions,
500
534
  );
501
535
  try {
502
536
  const result = getHandler(config)(message, context);
@@ -510,6 +544,10 @@ async function runHandlerAndPersist(
510
544
  for await (const msg of result as AsyncGenerator<Message>) {
511
545
  lastMessage = msg;
512
546
  }
547
+ if (lastMessage?.metadata?.agentNativeTaskState === "input-required") {
548
+ await pauseProcessingA2ATask(taskId, lastMessage);
549
+ return;
550
+ }
513
551
  await settleProcessingA2ATask(taskId, {
514
552
  state: "completed",
515
553
  message: lastMessage,
@@ -520,6 +558,10 @@ async function runHandlerAndPersist(
520
558
 
521
559
  const handlerResult = await (result as Promise<A2AHandlerResult>);
522
560
  const allArtifacts = [...artifacts, ...(handlerResult.artifacts ?? [])];
561
+ if (handlerResult.taskState === "input-required") {
562
+ await pauseProcessingA2ATask(taskId, handlerResult.message);
563
+ return;
564
+ }
523
565
  await settleProcessingA2ATask(taskId, {
524
566
  state: "completed",
525
567
  message: handlerResult.message,
@@ -555,6 +597,7 @@ async function handleSend(
555
597
 
556
598
  const contextId = params.contextId as string | undefined;
557
599
  const metadata = params.metadata as Record<string, unknown> | undefined;
600
+ const approvedActions = trustedApprovedActions(params.approvedActions, event);
558
601
 
559
602
  // The JWT-verified caller email (set by mountA2A in server.ts) is the
560
603
  // single source of truth for task ownership — bound at creation, checked
@@ -619,6 +662,7 @@ async function handleSend(
619
662
  ...(requestOrigin ? { requestOrigin } : {}),
620
663
  contextId: contextId ?? null,
621
664
  callerMetadata: safeMetadata ?? null,
665
+ approvedActions: approvedActions ?? null,
622
666
  },
623
667
  };
624
668
  const task = await createTask(
@@ -642,7 +686,10 @@ async function handleSend(
642
686
  console.error("[a2a] Failed to dispatch process-task:", err);
643
687
  }
644
688
 
645
- return { ...jsonRpcResult(0, working ?? task), _id: 0 };
689
+ return {
690
+ ...jsonRpcResult(0, sanitizeTaskForResponse(working ?? task)),
691
+ _id: 0,
692
+ };
646
693
  }
647
694
 
648
695
  return withA2ARequestContext(metadata, event, async () => {
@@ -659,6 +706,7 @@ async function handleSend(
659
706
  contextId,
660
707
  trustedA2AMetadata(metadata, event),
661
708
  event,
709
+ approvedActions,
662
710
  );
663
711
 
664
712
  try {
@@ -673,6 +721,13 @@ async function handleSend(
673
721
  for await (const msg of result as AsyncGenerator<Message>) {
674
722
  lastMessage = msg;
675
723
  }
724
+ if (lastMessage?.metadata?.agentNativeTaskState === "input-required") {
725
+ const updated = await updateTask(task.id, {
726
+ state: "input-required",
727
+ message: lastMessage,
728
+ });
729
+ return { ...jsonRpcResult(0, updated), _id: 0 };
730
+ }
676
731
  const updated = await updateTask(task.id, {
677
732
  state: "completed",
678
733
  message: lastMessage,
@@ -686,6 +741,13 @@ async function handleSend(
686
741
  ...ctx.artifacts,
687
742
  ...(handlerResult.artifacts ?? []),
688
743
  ];
744
+ if (handlerResult.taskState === "input-required") {
745
+ const updated = await updateTask(task.id, {
746
+ state: "input-required",
747
+ message: handlerResult.message,
748
+ });
749
+ return { ...jsonRpcResult(0, updated), _id: 0 };
750
+ }
689
751
  const updated = await updateTask(task.id, {
690
752
  state: "completed",
691
753
  message: handlerResult.message,
@@ -725,6 +787,7 @@ async function handleStream(
725
787
 
726
788
  const contextId = params.contextId as string | undefined;
727
789
  const metadata = params.metadata as Record<string, unknown> | undefined;
790
+ const approvedActions = trustedApprovedActions(params.approvedActions, event);
728
791
  const ownerEmailForTask =
729
792
  (event?.context?.__a2aVerifiedEmail as string | undefined) ?? null;
730
793
 
@@ -743,6 +806,7 @@ async function handleStream(
743
806
  contextId,
744
807
  trustedA2AMetadata(metadata, event),
745
808
  event,
809
+ approvedActions,
746
810
  );
747
811
 
748
812
  try {
@@ -1,23 +1,31 @@
1
1
  import {
2
2
  defineEventHandler,
3
+ setResponseHeader,
3
4
  setResponseStatus,
4
5
  getMethod,
5
6
  getRequestHeader,
6
7
  } from "h3";
7
8
  import * as jose from "jose";
8
9
 
10
+ import { redactArgsToJson } from "../audit/redact.js";
9
11
  import {
10
12
  extractBearerToken,
11
13
  verifyInternalToken,
12
14
  } from "../integrations/internal-token.js";
13
15
  import { getH3App } from "../server/framework-request-handler.js";
14
16
  import { readBody } from "../server/h3-helpers.js";
17
+ import { isSameOriginRequest } from "../server/request-origin.js";
15
18
  import { generateAgentCard } from "./agent-card.js";
16
19
  import {
17
20
  hasConfiguredA2ASecret,
18
21
  isA2AProductionRuntime,
19
22
  } from "./auth-policy.js";
20
23
  import { handleJsonRpcH3, processA2ATaskFromQueue } from "./handlers.js";
24
+ import {
25
+ claimA2AApproval,
26
+ getA2AApprovalForOwner,
27
+ settleA2AApproval,
28
+ } from "./task-store.js";
21
29
  import type { A2AConfig } from "./types.js";
22
30
 
23
31
  /**
@@ -235,6 +243,122 @@ export function mountA2A(
235
243
  }),
236
244
  );
237
245
 
246
+ // Human-only continuation for consequential A2A tool calls. The opaque id
247
+ // is a lookup handle, not authorization: every read and mutation also
248
+ // requires the task owner's live browser session. Ordinary A2A bearer
249
+ // tokens never reach this control plane and cannot approve themselves.
250
+ getH3App(nitroApp).use(
251
+ `${routePrefix}/a2a/approvals`,
252
+ defineEventHandler(async (event) => {
253
+ const approvalId = (event.path || "")
254
+ .split("?")[0]
255
+ .replace(/^\//, "")
256
+ .split("/")[0];
257
+ if (!approvalId) {
258
+ setResponseStatus(event, 404);
259
+ return { error: "Approval not found" };
260
+ }
261
+
262
+ const { getSession } = await import("../server/auth.js");
263
+ const session = await getSession(event);
264
+ if (!session?.email) {
265
+ setResponseStatus(event, 401);
266
+ return { error: "Sign in to review this approval" };
267
+ }
268
+
269
+ if (getMethod(event) === "GET") {
270
+ const approval = await getA2AApprovalForOwner(
271
+ approvalId,
272
+ session.email,
273
+ session.orgId ?? null,
274
+ );
275
+ if (!approval) {
276
+ setResponseStatus(event, 404);
277
+ return { error: "Approval not found" };
278
+ }
279
+ const safeInput = redactArgsToJson(approval.input) ?? "{}";
280
+ const esc = (value: string) =>
281
+ value.replace(
282
+ /[&<>"']/g,
283
+ (char) =>
284
+ ({
285
+ "&": "&amp;",
286
+ "<": "&lt;",
287
+ ">": "&gt;",
288
+ '"': "&quot;",
289
+ "'": "&#39;",
290
+ })[char] ?? char,
291
+ );
292
+ setResponseHeader(event, "content-type", "text/html; charset=utf-8");
293
+ setResponseHeader(event, "cache-control", "no-store");
294
+ setResponseHeader(event, "x-frame-options", "DENY");
295
+ setResponseHeader(
296
+ event,
297
+ "content-security-policy",
298
+ "frame-ancestors 'none'",
299
+ );
300
+ setResponseHeader(event, "referrer-policy", "no-referrer");
301
+ const expired = approval.expiresAt <= Date.now();
302
+ const active = approval.status === "pending" && !expired;
303
+ const displayStatus = expired ? "expired" : approval.status;
304
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Review agent action</title><style>body{font:15px/1.5 ui-sans-serif,system-ui;background:#f7f7f5;color:#171717;margin:0}.card{max-width:680px;margin:8vh auto;background:white;border:1px solid #ddd;border-radius:14px;padding:28px;box-shadow:0 12px 36px #0001}h1{font-size:22px;margin:0 0 8px}p{color:#555}pre{white-space:pre-wrap;word-break:break-word;background:#f4f4f2;padding:16px;border-radius:9px;max-height:45vh;overflow:auto}button{background:#171717;color:white;border:0;border-radius:8px;padding:11px 16px;font-weight:650;cursor:pointer}button[disabled]{opacity:.5;cursor:default}#status{margin-left:10px;color:#555}</style></head><body><main class="card"><h1>Review agent action</h1><p>This action is paused until you approve it. Approval can be used once.</p><h2>${esc(approval.tool)}</h2><pre>${esc(safeInput)}</pre><button id="approve" ${active ? "" : "disabled"}>${active ? "Approve and run" : esc(displayStatus)}</button><span id="status">${active ? "" : esc(approval.result ?? displayStatus)}</span><script>document.getElementById('approve').onclick=async function(){this.disabled=true;document.getElementById('status').textContent='Running…';try{const r=await fetch(location.href,{method:'POST',credentials:'include',headers:{'content-type':'application/json'}});const j=await r.json();document.getElementById('status').textContent=j.output||j.error||'Done';}catch(e){document.getElementById('status').textContent='Could not run the action.'}}</script></main></body></html>`;
305
+ }
306
+
307
+ if (getMethod(event) !== "POST") {
308
+ setResponseStatus(event, 405);
309
+ return { error: "Method not allowed" };
310
+ }
311
+ if (!isSameOriginRequest(event)) {
312
+ setResponseStatus(event, 403);
313
+ return { error: "Cross-origin approval denied" };
314
+ }
315
+ if (!config.executeApproval) {
316
+ setResponseStatus(event, 501);
317
+ return { error: "Approval execution is not configured" };
318
+ }
319
+
320
+ const approval = await claimA2AApproval(
321
+ approvalId,
322
+ session.email,
323
+ session.orgId ?? null,
324
+ );
325
+ if (!approval) {
326
+ const current = await getA2AApprovalForOwner(
327
+ approvalId,
328
+ session.email,
329
+ session.orgId ?? null,
330
+ );
331
+ setResponseStatus(event, current ? 409 : 404);
332
+ return {
333
+ error: current
334
+ ? `Approval is ${current.status} and cannot be run again`
335
+ : "Approval not found",
336
+ };
337
+ }
338
+
339
+ const { runWithRequestContext } =
340
+ await import("../server/request-context.js");
341
+ let execution: { status: "completed" | "failed"; output: string };
342
+ try {
343
+ execution = await runWithRequestContext(
344
+ { userEmail: session.email, orgId: session.orgId ?? undefined },
345
+ () => config.executeApproval!(approval),
346
+ );
347
+ } catch (error) {
348
+ execution = {
349
+ status: "failed",
350
+ output:
351
+ error instanceof Error
352
+ ? error.message
353
+ : "Approval execution failed",
354
+ };
355
+ }
356
+ await settleA2AApproval(approval.id, execution.status, execution.output);
357
+ if (execution.status === "failed") setResponseStatus(event, 500);
358
+ return execution;
359
+ }),
360
+ );
361
+
238
362
  // Async-mode processor route. MUST be mounted BEFORE the `/a2a` catch-all
239
363
  // below, since h3's `.use()` matches by prefix and `/a2a` would otherwise
240
364
  // swallow `/a2a/_process-task` and return a JSON-RPC "Invalid token" error
@@ -24,6 +24,23 @@ async function ensureTable(): Promise<void> {
24
24
  updated_at ${intType()} NOT NULL
25
25
  )
26
26
  `;
27
+ const createApprovalsSql = `
28
+ CREATE TABLE IF NOT EXISTS a2a_approvals (
29
+ id TEXT PRIMARY KEY,
30
+ task_id TEXT NOT NULL UNIQUE,
31
+ owner_email TEXT NOT NULL,
32
+ org_id TEXT,
33
+ tool_name TEXT NOT NULL,
34
+ tool_input TEXT NOT NULL,
35
+ approval_key TEXT NOT NULL,
36
+ call_id TEXT NOT NULL,
37
+ status TEXT NOT NULL DEFAULT 'pending',
38
+ result TEXT,
39
+ expires_at ${intType()} NOT NULL,
40
+ created_at ${intType()} NOT NULL,
41
+ updated_at ${intType()} NOT NULL
42
+ )
43
+ `;
27
44
 
28
45
  if (isPostgres()) {
29
46
  // PG-guard: probe information_schema before issuing DDL to avoid ACCESS
@@ -40,6 +57,7 @@ async function ensureTable(): Promise<void> {
40
57
  "owner_email",
41
58
  `ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS owner_email TEXT`,
42
59
  );
60
+ await ensureTableExists("a2a_approvals", createApprovalsSql);
43
61
  return;
44
62
  }
45
63
 
@@ -58,6 +76,7 @@ async function ensureTable(): Promise<void> {
58
76
  } catch {
59
77
  // Column already exists — expected on every restart after first run.
60
78
  }
79
+ await client.execute(createApprovalsSql);
61
80
  })().catch((err) => {
62
81
  // Retry init on the next call after a failed startup.
63
82
  _initPromise = undefined;
@@ -67,6 +86,240 @@ async function ensureTable(): Promise<void> {
67
86
  return _initPromise;
68
87
  }
69
88
 
89
+ export interface A2AApprovalRecord {
90
+ id: string;
91
+ taskId: string;
92
+ ownerEmail: string;
93
+ orgId: string | null;
94
+ tool: string;
95
+ input: unknown;
96
+ approvalKey: string;
97
+ callId: string;
98
+ status: "pending" | "processing" | "completed" | "failed";
99
+ result: string | null;
100
+ expiresAt: number;
101
+ }
102
+
103
+ async function withDbTransaction<T>(
104
+ client: ReturnType<typeof getDbExec>,
105
+ fn: (tx: ReturnType<typeof getDbExec>) => Promise<T>,
106
+ ): Promise<T> {
107
+ if (client.transaction) return client.transaction(fn);
108
+ await client.execute(isPostgres() ? "BEGIN" : "BEGIN IMMEDIATE");
109
+ try {
110
+ const result = await fn(client);
111
+ await client.execute("COMMIT");
112
+ return result;
113
+ } catch (error) {
114
+ await client.execute("ROLLBACK").catch(() => {});
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ function approvalFromRow(row: any): A2AApprovalRecord {
120
+ return {
121
+ id: String(row.id),
122
+ taskId: String(row.task_id),
123
+ ownerEmail: String(row.owner_email),
124
+ orgId: row.org_id ? String(row.org_id) : null,
125
+ tool: String(row.tool_name),
126
+ input: JSON.parse(String(row.tool_input)),
127
+ approvalKey: String(row.approval_key),
128
+ callId: String(row.call_id),
129
+ status: row.status,
130
+ result: row.result ? String(row.result) : null,
131
+ expiresAt: Number(row.expires_at),
132
+ };
133
+ }
134
+
135
+ export async function createA2AApproval(input: {
136
+ taskId: string;
137
+ ownerEmail: string;
138
+ orgId?: string | null;
139
+ tool: string;
140
+ toolInput: unknown;
141
+ approvalKey: string;
142
+ callId: string;
143
+ ttlMs?: number;
144
+ }): Promise<A2AApprovalRecord> {
145
+ await ensureTable();
146
+ const client = getDbExec();
147
+ const id = crypto.randomUUID();
148
+ const now = Date.now();
149
+ const expiresAt = now + (input.ttlMs ?? 15 * 60_000);
150
+ await client.execute({
151
+ sql: `INSERT INTO a2a_approvals (id, task_id, owner_email, org_id, tool_name, tool_input, approval_key, call_id, status, expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)`,
152
+ args: [
153
+ id,
154
+ input.taskId,
155
+ input.ownerEmail,
156
+ input.orgId ?? null,
157
+ input.tool,
158
+ JSON.stringify(input.toolInput ?? {}),
159
+ input.approvalKey,
160
+ input.callId,
161
+ expiresAt,
162
+ now,
163
+ now,
164
+ ],
165
+ });
166
+ return {
167
+ id,
168
+ taskId: input.taskId,
169
+ ownerEmail: input.ownerEmail,
170
+ orgId: input.orgId ?? null,
171
+ tool: input.tool,
172
+ input: input.toolInput ?? {},
173
+ approvalKey: input.approvalKey,
174
+ callId: input.callId,
175
+ status: "pending",
176
+ result: null,
177
+ expiresAt,
178
+ };
179
+ }
180
+
181
+ export async function getA2AApprovalForOwner(
182
+ id: string,
183
+ ownerEmail: string,
184
+ orgId?: string | null,
185
+ ): Promise<A2AApprovalRecord | null> {
186
+ await ensureTable();
187
+ const { rows } = await getDbExec().execute({
188
+ sql: `SELECT * FROM a2a_approvals WHERE id = ? AND owner_email = ? AND (org_id IS NULL OR org_id = ?)`,
189
+ args: [id, ownerEmail, orgId ?? null],
190
+ });
191
+ return rows[0] ? approvalFromRow(rows[0]) : null;
192
+ }
193
+
194
+ export async function claimA2AApproval(
195
+ id: string,
196
+ ownerEmail: string,
197
+ orgId?: string | null,
198
+ ): Promise<A2AApprovalRecord | null> {
199
+ await ensureTable();
200
+ const client = getDbExec();
201
+ const now = Date.now();
202
+ class ClaimRejected extends Error {}
203
+ try {
204
+ return await withDbTransaction(client, async (tx) => {
205
+ const approvalUpdate = await tx.execute({
206
+ sql: `UPDATE a2a_approvals SET status = 'processing', updated_at = ? WHERE id = ? AND owner_email = ? AND status = 'pending' AND expires_at > ? AND (org_id IS NULL OR org_id = ?)`,
207
+ args: [now, id, ownerEmail, now, orgId ?? null],
208
+ });
209
+ if (getAffectedRowCount(approvalUpdate) === 0) throw new ClaimRejected();
210
+ const { rows } = await tx.execute({
211
+ sql: `SELECT * FROM a2a_approvals WHERE id = ? AND owner_email = ? AND status = 'processing'`,
212
+ args: [id, ownerEmail],
213
+ });
214
+ if (!rows[0]) throw new ClaimRejected();
215
+ const approval = approvalFromRow(rows[0]);
216
+ const timestamp = new Date(now).toISOString();
217
+ const runningMessage: Message = {
218
+ role: "agent",
219
+ parts: [{ type: "text", text: "Approved action is running." }],
220
+ };
221
+ const taskUpdate = await tx.execute({
222
+ sql: `UPDATE a2a_tasks SET status_state = 'working', status_message = ?, status_timestamp = ?, updated_at = ? WHERE id = ? AND owner_email = ? AND status_state = 'input-required'`,
223
+ args: [
224
+ JSON.stringify(runningMessage),
225
+ timestamp,
226
+ now,
227
+ approval.taskId,
228
+ ownerEmail,
229
+ ],
230
+ });
231
+ if (getAffectedRowCount(taskUpdate) === 0) throw new ClaimRejected();
232
+ return approval;
233
+ });
234
+ } catch (error) {
235
+ if (error instanceof ClaimRejected) return null;
236
+ throw error;
237
+ }
238
+ }
239
+
240
+ export async function settleA2AApproval(
241
+ id: string,
242
+ status: "completed" | "failed",
243
+ resultText: string,
244
+ ): Promise<void> {
245
+ await ensureTable();
246
+ const client = getDbExec();
247
+ const now = Date.now();
248
+ const message: Message = {
249
+ role: "agent",
250
+ parts: [{ type: "text", text: resultText }],
251
+ };
252
+ await withDbTransaction(client, async (tx) => {
253
+ const { rows } = await tx.execute({
254
+ sql: `SELECT task_id FROM a2a_approvals WHERE id = ? AND status = 'processing'`,
255
+ args: [id],
256
+ });
257
+ if (!rows[0]) throw new Error("Approval is not processing");
258
+ const taskId = String((rows[0] as any).task_id);
259
+ const { rows: taskRows } = await tx.execute({
260
+ sql: `SELECT history FROM a2a_tasks WHERE id = ? AND status_state = 'working'`,
261
+ args: [taskId],
262
+ });
263
+ if (!taskRows[0]) {
264
+ throw new Error("Approval task is no longer awaiting settlement");
265
+ }
266
+ const history = JSON.parse(String((taskRows[0] as any).history));
267
+ history.push(message);
268
+ const timestamp = new Date(now).toISOString();
269
+ const taskUpdate = await tx.execute({
270
+ sql: `UPDATE a2a_tasks SET status_state = ?, status_message = ?, status_timestamp = ?, history = ?, updated_at = ? WHERE id = ? AND status_state = 'working'`,
271
+ args: [
272
+ status,
273
+ JSON.stringify(message),
274
+ timestamp,
275
+ JSON.stringify(history),
276
+ now,
277
+ taskId,
278
+ ],
279
+ });
280
+ if (getAffectedRowCount(taskUpdate) === 0) {
281
+ throw new Error("Approval task settlement lost its state claim");
282
+ }
283
+ const approvalUpdate = await tx.execute({
284
+ sql: `UPDATE a2a_approvals SET status = ?, result = ?, updated_at = ? WHERE id = ? AND status = 'processing'`,
285
+ args: [status, resultText, now, id],
286
+ });
287
+ if (getAffectedRowCount(approvalUpdate) === 0) {
288
+ throw new Error("Approval settlement lost its state claim");
289
+ }
290
+ });
291
+ }
292
+
293
+ export async function pauseProcessingA2ATask(
294
+ id: string,
295
+ message: Message,
296
+ ): Promise<Task | null> {
297
+ await ensureTable();
298
+ const client = getDbExec();
299
+ const { rows } = await client.execute({
300
+ sql: `SELECT * FROM a2a_tasks WHERE id = ? AND status_state = 'processing'`,
301
+ args: [id],
302
+ });
303
+ if (!rows[0]) return null;
304
+ const task = taskFromRow(rows[0]);
305
+ task.history?.push(message);
306
+ const now = Date.now();
307
+ const timestamp = new Date(now).toISOString();
308
+ const result = await client.execute({
309
+ sql: `UPDATE a2a_tasks SET status_state = 'input-required', status_message = ?, status_timestamp = ?, history = ?, updated_at = ? WHERE id = ? AND status_state = 'processing'`,
310
+ args: [
311
+ JSON.stringify(message),
312
+ timestamp,
313
+ JSON.stringify(task.history ?? []),
314
+ now,
315
+ id,
316
+ ],
317
+ });
318
+ if (getAffectedRowCount(result) === 0) return null;
319
+ task.status = { state: "input-required", message, timestamp };
320
+ return task;
321
+ }
322
+
70
323
  function taskFromRow(row: any): Task & { ownerEmail?: string | null } {
71
324
  return {
72
325
  id: row.id as string,
@@ -126,6 +126,12 @@ export interface JsonRpcResponse {
126
126
  error?: JsonRpcError;
127
127
  }
128
128
 
129
+ /** One exact downstream action explicitly authorized in the caller's chat. */
130
+ export interface A2AApprovedAction {
131
+ tool: string;
132
+ input: unknown;
133
+ }
134
+
129
135
  // --- Framework config ---
130
136
 
131
137
  export interface A2AHandlerContext {
@@ -135,12 +141,27 @@ export interface A2AHandlerContext {
135
141
  metadata?: Record<string, unknown>;
136
142
  /** Current H3 event when the handler is running inside an HTTP request. */
137
143
  event?: unknown;
144
+ /** Exact one-time action grants from a JWT-authenticated caller. */
145
+ approvedActions?: A2AApprovedAction[];
138
146
  writeArtifact: (name: string, content: string, mimeType?: string) => string;
139
147
  }
140
148
 
141
149
  export interface A2AHandlerResult {
142
150
  message: Message;
143
151
  artifacts?: Artifact[];
152
+ /** Optional non-terminal state requested by the handler. */
153
+ taskState?: Extract<TaskState, "input-required">;
154
+ }
155
+
156
+ export interface A2AApprovalExecution {
157
+ id: string;
158
+ taskId: string;
159
+ ownerEmail: string;
160
+ orgId?: string | null;
161
+ tool: string;
162
+ input: unknown;
163
+ approvalKey: string;
164
+ callId: string;
144
165
  }
145
166
 
146
167
  export type A2AHandler = (
@@ -160,4 +181,9 @@ export interface A2AConfig {
160
181
  streaming?: boolean;
161
182
  /** Route async A2A work through the app's durable background worker when available. */
162
183
  durableBackgroundRuns?: boolean;
184
+ /** Execute a persisted, human-approved A2A tool call. */
185
+ executeApproval?: (approval: A2AApprovalExecution) => Promise<{
186
+ status: "completed" | "failed";
187
+ output: string;
188
+ }>;
163
189
  }
@@ -2747,7 +2747,7 @@ function stableStringify(value: unknown): string {
2747
2747
  .join(",")}}`;
2748
2748
  }
2749
2749
 
2750
- function toolCallCacheKey(toolName: string, input: unknown): string {
2750
+ export function toolCallCacheKey(toolName: string, input: unknown): string {
2751
2751
  return `${toolName}:${stableStringify(normalizeToolCallInputForHistory(input))}`;
2752
2752
  }
2753
2753
 
@@ -4180,7 +4180,10 @@ export async function runAgentLoop(opts: {
4180
4180
  // instead of executing. The action's side effect never happens until a
4181
4181
  // human re-issues the turn approving this call's stable key.
4182
4182
  const approvalKey = toolCallCacheKey(toolCall.name, toolCall.input);
4183
- if (actionEntry.needsApproval && !approvedToolCallKeys.has(approvalKey)) {
4183
+ // Consume a grant on its first exact match. A second identical call in
4184
+ // the same continuation must request its own human approval.
4185
+ const wasApproved = approvedToolCallKeys.delete(approvalKey);
4186
+ if (actionEntry.needsApproval && !wasApproved) {
4184
4187
  let mustApprove = false;
4185
4188
  try {
4186
4189
  mustApprove =