@agent-native/core 0.101.10 → 0.101.12
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 +12 -0
- 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/coding-tools/run-code.ts +65 -12
- package/corpus/core/src/extensions/actions.ts +141 -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/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +59 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +5 -2
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +13 -4
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +63 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +30 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
- 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/extensions/NotionExtensions.tsx +78 -0
- package/corpus/templates/content/app/global.css +53 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/content/app/i18n-data.ts +163 -0
- package/corpus/templates/content/app/root.tsx +2 -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/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 +17 -14
- package/corpus/templates/content/vite.config.ts +0 -1
- 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/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/coding-tools/run-code.js +64 -11
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +129 -7
- package/dist/extensions/actions.js.map +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 +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- 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/secrets/routes.d.ts +9 -9
- 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/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.101.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2ff004e: Fix workspace-file resolution and run-code paging/routing edge cases. `contentFromWorkspaceFile` now resolves the same file the run-code `workspaceRead`/`workspaceWrite` bridge sees (bridge scope first, then Resources), and fails closed on a bridge read error instead of silently falling back to a possibly-different same-path Resources body. `workspaceRead` returns null instead of a silently truncated prefix when a later page fails, and the run-code bridge now returns a distinct "not registered" (404) error for unknown tools instead of a misleading read-only access error.
|
|
8
|
+
|
|
9
|
+
## 0.101.11
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fd0ddb8: Wait for agent-card and A2A routes to finish initializing on cold starts, preserve human approval gates across A2A tasks, and carry exact one-time chat authorization through authenticated agent delegation.
|
|
14
|
+
|
|
3
15
|
## 0.101.10
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.101.
|
|
3
|
+
"version": "0.101.12",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -2,6 +2,7 @@ import * as jose from "jose";
|
|
|
2
2
|
|
|
3
3
|
import { ssrfSafeFetch } from "../extensions/url-safety.js";
|
|
4
4
|
import type {
|
|
5
|
+
A2AApprovedAction,
|
|
5
6
|
AgentCard,
|
|
6
7
|
JsonRpcRequest,
|
|
7
8
|
JsonRpcResponse,
|
|
@@ -227,6 +228,7 @@ export class A2AClient {
|
|
|
227
228
|
opts?: {
|
|
228
229
|
contextId?: string;
|
|
229
230
|
metadata?: Record<string, unknown>;
|
|
231
|
+
approvedActions?: A2AApprovedAction[];
|
|
230
232
|
/**
|
|
231
233
|
* If true, ask the server to return the task immediately in `working`
|
|
232
234
|
* state and process the handler in the background. The caller should
|
|
@@ -242,6 +244,9 @@ export class A2AClient {
|
|
|
242
244
|
message,
|
|
243
245
|
contextId: opts?.contextId,
|
|
244
246
|
metadata: opts?.metadata,
|
|
247
|
+
...(opts?.approvedActions?.length
|
|
248
|
+
? { approvedActions: opts.approvedActions }
|
|
249
|
+
: {}),
|
|
245
250
|
...(opts?.async ? { async: true } : {}),
|
|
246
251
|
});
|
|
247
252
|
|
|
@@ -281,6 +286,7 @@ export class A2AClient {
|
|
|
281
286
|
opts?: {
|
|
282
287
|
contextId?: string;
|
|
283
288
|
metadata?: Record<string, unknown>;
|
|
289
|
+
approvedActions?: A2AApprovedAction[];
|
|
284
290
|
/** Total time to wait for completion. Default 5 min. */
|
|
285
291
|
timeoutMs?: number;
|
|
286
292
|
/** Poll interval. Default 2s. */
|
|
@@ -292,10 +298,18 @@ export class A2AClient {
|
|
|
292
298
|
const submitted = await this.send(message, {
|
|
293
299
|
contextId: opts?.contextId,
|
|
294
300
|
metadata: opts?.metadata,
|
|
301
|
+
...(opts?.approvedActions?.length
|
|
302
|
+
? { approvedActions: opts.approvedActions }
|
|
303
|
+
: {}),
|
|
295
304
|
async: true,
|
|
296
305
|
});
|
|
297
306
|
|
|
298
|
-
const terminalStates = new Set([
|
|
307
|
+
const terminalStates = new Set([
|
|
308
|
+
"completed",
|
|
309
|
+
"failed",
|
|
310
|
+
"canceled",
|
|
311
|
+
"input-required",
|
|
312
|
+
]);
|
|
299
313
|
if (terminalStates.has(submitted.status.state)) return submitted;
|
|
300
314
|
|
|
301
315
|
const timeoutMs = opts?.timeoutMs ?? 5 * 60_000;
|
|
@@ -542,6 +556,8 @@ export async function callAgent(
|
|
|
542
556
|
orgSecret?: string;
|
|
543
557
|
/** Origin used to build links back to the receiving app. */
|
|
544
558
|
requestOrigin?: string;
|
|
559
|
+
/** Exact downstream actions explicitly authorized in the caller's chat. */
|
|
560
|
+
approvedActions?: A2AApprovedAction[];
|
|
545
561
|
/**
|
|
546
562
|
* Use async/poll instead of a single blocking POST. Recommended for
|
|
547
563
|
* cross-app calls that may exceed a synchronous serverless request budget.
|
|
@@ -602,6 +618,9 @@ export async function callAgent(
|
|
|
602
618
|
task = await client.sendAndWait(message, {
|
|
603
619
|
contextId: opts?.contextId,
|
|
604
620
|
metadata,
|
|
621
|
+
...(opts?.approvedActions?.length
|
|
622
|
+
? { approvedActions: opts.approvedActions }
|
|
623
|
+
: {}),
|
|
605
624
|
timeoutMs: opts?.timeoutMs,
|
|
606
625
|
pollIntervalMs: opts?.pollIntervalMs,
|
|
607
626
|
onUpdate: opts?.onUpdate,
|
|
@@ -610,6 +629,9 @@ export async function callAgent(
|
|
|
610
629
|
task = await client.send(message, {
|
|
611
630
|
contextId: opts?.contextId,
|
|
612
631
|
metadata,
|
|
632
|
+
...(opts?.approvedActions?.length
|
|
633
|
+
? { approvedActions: opts.approvedActions }
|
|
634
|
+
: {}),
|
|
613
635
|
});
|
|
614
636
|
}
|
|
615
637
|
|
|
@@ -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
|