@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +23 -1
- package/corpus/core/src/a2a/handlers.ts +65 -1
- package/corpus/core/src/a2a/server.ts +124 -0
- package/corpus/core/src/a2a/task-store.ts +253 -0
- package/corpus/core/src/a2a/types.ts +26 -0
- package/corpus/core/src/agent/production-agent.ts +5 -2
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/mcp/builtin-tools.ts +25 -1
- package/corpus/core/src/scripts/call-agent.ts +22 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
- package/corpus/templates/content/AGENTS.md +3 -2
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
- package/corpus/templates/content/actions/_database-utils.ts +89 -12
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
- package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +71 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +38 -0
- package/corpus/templates/content/app/i18n-data.ts +450 -0
- package/corpus/templates/content/app/root.tsx +2 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +47 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +53 -0
- package/corpus/templates/content/shared/api.ts +70 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/document-export.ts +296 -25
- package/corpus/templates/content/shared/inline-math.ts +128 -0
- package/corpus/templates/content/shared/math-rendering.ts +50 -0
- package/corpus/templates/content/shared/nfm.ts +20 -17
- package/corpus/templates/content/vite.config.ts +0 -1
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
- package/corpus/templates/mail/AGENTS.md +21 -19
- package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
- package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
- package/corpus/templates/mail/server/handlers/media.ts +84 -61
- package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
- package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
- package/corpus/templates/mail/server/plugins/auth.ts +9 -1
- package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/a2a/client.d.ts +5 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +18 -1
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +57 -8
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +87 -1
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/task-store.d.ts +27 -0
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +216 -0
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +24 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +5 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +22 -3
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +19 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +71 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/package.json +2 -2
- 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 {
|
|
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
|
+
"&": "&",
|
|
286
|
+
"<": "<",
|
|
287
|
+
">": ">",
|
|
288
|
+
'"': """,
|
|
289
|
+
"'": "'",
|
|
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
|
-
|
|
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 =
|