@agent-native/core 0.109.0 → 0.109.2
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 +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/integrations.mdx +87 -253
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +24 -2
- package/corpus/core/src/a2a/correlation.ts +50 -0
- package/corpus/core/src/a2a/handlers.ts +140 -14
- package/corpus/core/src/a2a/index.ts +1 -0
- package/corpus/core/src/a2a/invoke.ts +10 -1
- package/corpus/core/src/a2a/task-store.ts +146 -6
- package/corpus/core/src/a2a/types.ts +16 -0
- package/corpus/core/src/action.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +23 -0
- package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
- package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
- package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
- package/corpus/core/src/db/client.ts +55 -0
- package/corpus/core/src/deploy/build.ts +63 -3
- package/corpus/core/src/observability/traces.ts +172 -19
- package/corpus/core/src/scripts/call-agent.ts +53 -2
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
- package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
- package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
- package/corpus/templates/clips/AGENTS.md +10 -0
- package/corpus/templates/clips/actions/create-dictation.ts +29 -4
- package/corpus/templates/clips/actions/finalize-recording.ts +542 -65
- package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
- package/corpus/templates/clips/actions/set-mobile-capture-state.ts +37 -0
- package/corpus/templates/clips/actions/update-dictation.ts +9 -1
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -3
- package/corpus/templates/clips/app/routes/record.tsx +3 -3
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +19 -5
- package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
- package/corpus/templates/clips/desktop/src/app.tsx +35 -6
- package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +141 -18
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +28 -7
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -4
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +59 -18
- package/corpus/templates/clips/server/db/schema.ts +9 -1
- package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +74 -7
- package/corpus/templates/clips/server/lib/media-verification-state.ts +55 -0
- package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
- package/corpus/templates/clips/server/plugins/auth.ts +5 -5
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +13 -1
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
- package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
- package/corpus/templates/clips/shared/share-meta.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +3 -0
- package/dist/a2a/client.d.ts +11 -2
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +16 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/correlation.d.ts +10 -0
- package/dist/a2a/correlation.d.ts.map +1 -0
- package/dist/a2a/correlation.js +40 -0
- package/dist/a2a/correlation.js.map +1 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +100 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/invoke.d.ts +4 -1
- package/dist/a2a/invoke.d.ts.map +1 -1
- package/dist/a2a/invoke.js +3 -0
- package/dist/a2a/invoke.js.map +1 -1
- package/dist/a2a/task-store.d.ts +12 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +96 -6
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +15 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +2 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +9 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +14 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/blocks/library/diagram.config.d.ts +2 -0
- package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.config.js +8 -1
- package/dist/client/blocks/library/diagram.config.js.map +1 -1
- package/dist/client/blocks/library/diagram.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.js +9 -4
- package/dist/client/blocks/library/diagram.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +25 -1
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +33 -0
- package/dist/db/client.js.map +1 -1
- package/dist/deploy/build.d.ts +3 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +45 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/observability/traces.d.ts +9 -0
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +137 -19
- package/dist/observability/traces.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +35 -3
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts +18 -2
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +35 -7
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +93 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/docs/content/integrations.mdx +87 -253
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { A2ACorrelationMetadata } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const MAX_A2A_CORRELATION_VALUE_CHARS = 200;
|
|
4
|
+
|
|
5
|
+
const APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
6
|
+
const CORRELATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
7
|
+
|
|
8
|
+
function boundedIdentifier(
|
|
9
|
+
value: unknown,
|
|
10
|
+
pattern: RegExp,
|
|
11
|
+
): string | undefined {
|
|
12
|
+
if (typeof value !== "string") return undefined;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
if (
|
|
15
|
+
!trimmed ||
|
|
16
|
+
trimmed.length > MAX_A2A_CORRELATION_VALUE_CHARS ||
|
|
17
|
+
!pattern.test(trimmed)
|
|
18
|
+
) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sanitizeA2ACorrelationId(value: unknown): string | undefined {
|
|
25
|
+
return boundedIdentifier(value, CORRELATION_ID_PATTERN);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keep only bounded, opaque ASCII correlation identifiers. These values
|
|
30
|
+
* remain telemetry hints; authentication continues to come exclusively from
|
|
31
|
+
* the verified A2A token/request context.
|
|
32
|
+
*/
|
|
33
|
+
export function sanitizeA2ACorrelationMetadata(
|
|
34
|
+
value: unknown,
|
|
35
|
+
): A2ACorrelationMetadata {
|
|
36
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
37
|
+
const metadata = value as Record<string, unknown>;
|
|
38
|
+
const callerApp = boundedIdentifier(metadata.callerApp, APP_ID_PATTERN);
|
|
39
|
+
const callerThreadId = sanitizeA2ACorrelationId(metadata.callerThreadId);
|
|
40
|
+
const parentRunId = sanitizeA2ACorrelationId(metadata.parentRunId);
|
|
41
|
+
const parentTurnId = sanitizeA2ACorrelationId(metadata.parentTurnId);
|
|
42
|
+
const invocationId = sanitizeA2ACorrelationId(metadata.invocationId);
|
|
43
|
+
return {
|
|
44
|
+
...(callerApp ? { callerApp } : {}),
|
|
45
|
+
...(callerThreadId ? { callerThreadId } : {}),
|
|
46
|
+
...(parentRunId ? { parentRunId } : {}),
|
|
47
|
+
...(parentTurnId ? { parentTurnId } : {}),
|
|
48
|
+
...(invocationId ? { invocationId } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
1
3
|
import { setResponseHeader, setResponseStatus } from "h3";
|
|
2
4
|
|
|
3
5
|
import {
|
|
@@ -7,18 +9,22 @@ import {
|
|
|
7
9
|
isAgentChatDurableBackgroundEnabled,
|
|
8
10
|
resolveAgentChatProcessRunDispatchPath,
|
|
9
11
|
} from "../agent/durable-background.js";
|
|
12
|
+
import { trackingIdentityProperties } from "../observability/tracking-identity.js";
|
|
10
13
|
import { withConfiguredAppBasePath } from "../server/app-base-path.js";
|
|
11
14
|
import { getOrigin, isConfiguredAppOrigin } from "../server/google-oauth.js";
|
|
12
15
|
import { fireInternalDispatch } from "../server/self-dispatch.js";
|
|
13
16
|
import { agentChat } from "../shared/agent-chat.js";
|
|
17
|
+
import { track } from "../tracking/registry.js";
|
|
14
18
|
import {
|
|
15
19
|
hasConfiguredA2ASecret,
|
|
16
20
|
isA2AProductionRuntime,
|
|
17
21
|
} from "./auth-policy.js";
|
|
22
|
+
import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
|
|
18
23
|
import {
|
|
19
24
|
createTask,
|
|
25
|
+
createOrReuseTask,
|
|
20
26
|
getTask,
|
|
21
|
-
|
|
27
|
+
getTaskOwnership,
|
|
22
28
|
updateTask,
|
|
23
29
|
claimA2ATaskForProcessing,
|
|
24
30
|
getA2ATaskDispatchState,
|
|
@@ -28,6 +34,8 @@ import {
|
|
|
28
34
|
touchQueuedA2ATaskDispatch,
|
|
29
35
|
touchProcessingA2ATask,
|
|
30
36
|
pauseProcessingA2ATask,
|
|
37
|
+
MAX_A2A_IDEMPOTENCY_KEY_CHARS,
|
|
38
|
+
A2A_PERSONAL_OWNER_SCOPE,
|
|
31
39
|
} from "./task-store.js";
|
|
32
40
|
import type {
|
|
33
41
|
A2AApprovedAction,
|
|
@@ -51,6 +59,7 @@ const A2A_PROCESSING_HEARTBEAT_MS = 30_000;
|
|
|
51
59
|
const MAX_A2A_APPROVED_ACTIONS = 10;
|
|
52
60
|
const MAX_A2A_DIRECT_ACTION_NAME_CHARS = 200;
|
|
53
61
|
const MAX_A2A_DIRECT_ACTION_INPUT_BYTES = 64 * 1024;
|
|
62
|
+
const A2A_READ_INVOKE_EVENT = "$a2a_read_invoke";
|
|
54
63
|
|
|
55
64
|
function trustedApprovedActions(
|
|
56
65
|
value: unknown,
|
|
@@ -580,6 +589,22 @@ async function runHandlerAndPersist(
|
|
|
580
589
|
}
|
|
581
590
|
}
|
|
582
591
|
|
|
592
|
+
function verifiedTaskOwner(event?: any): {
|
|
593
|
+
ownerEmail: string | null;
|
|
594
|
+
ownerScope: string | null;
|
|
595
|
+
} {
|
|
596
|
+
const ownerEmail =
|
|
597
|
+
(event?.context?.__a2aVerifiedEmail as string | undefined) ?? null;
|
|
598
|
+
return {
|
|
599
|
+
ownerEmail,
|
|
600
|
+
ownerScope: ownerEmail
|
|
601
|
+
? ((event?.context?.__a2aOrgDomain as string | undefined)
|
|
602
|
+
?.trim()
|
|
603
|
+
.toLowerCase() ?? A2A_PERSONAL_OWNER_SCOPE)
|
|
604
|
+
: null,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
583
608
|
async function handleSend(
|
|
584
609
|
params: Record<string, unknown>,
|
|
585
610
|
config: A2AConfig,
|
|
@@ -606,8 +631,31 @@ async function handleSend(
|
|
|
606
631
|
// on every subsequent tasks/get and tasks/cancel call. Caller-supplied
|
|
607
632
|
// metadata.userEmail is NEVER used for ownership; that would re-introduce
|
|
608
633
|
// the IDOR class fixed here.
|
|
609
|
-
const ownerEmailForTask =
|
|
610
|
-
(event
|
|
634
|
+
const { ownerEmail: ownerEmailForTask, ownerScope: ownerScopeForTask } =
|
|
635
|
+
verifiedTaskOwner(event);
|
|
636
|
+
let idempotencyKey: string | undefined;
|
|
637
|
+
if (ownerEmailForTask && params.idempotencyKey !== undefined) {
|
|
638
|
+
if (typeof params.idempotencyKey !== "string") {
|
|
639
|
+
return {
|
|
640
|
+
...jsonRpcError(
|
|
641
|
+
0,
|
|
642
|
+
-32602,
|
|
643
|
+
"Invalid params: idempotencyKey must be a string",
|
|
644
|
+
),
|
|
645
|
+
_id: 0,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
idempotencyKey = params.idempotencyKey.trim();
|
|
649
|
+
if (
|
|
650
|
+
!idempotencyKey ||
|
|
651
|
+
idempotencyKey.length > MAX_A2A_IDEMPOTENCY_KEY_CHARS
|
|
652
|
+
) {
|
|
653
|
+
return {
|
|
654
|
+
...jsonRpcError(0, -32602, "Invalid params: idempotencyKey is invalid"),
|
|
655
|
+
_id: 0,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
611
659
|
|
|
612
660
|
// Async mode: return the task immediately in `working` state, run the
|
|
613
661
|
// handler in the background, and let the caller poll `tasks/get`. This is
|
|
@@ -620,6 +668,7 @@ async function handleSend(
|
|
|
620
668
|
// production — see the additional gate below.
|
|
621
669
|
const asyncMode =
|
|
622
670
|
params.async === true || (event && event.context?.__a2aForceAsync === true);
|
|
671
|
+
if (!asyncMode) idempotencyKey = undefined;
|
|
623
672
|
|
|
624
673
|
if (asyncMode) {
|
|
625
674
|
// Refuse async mode entirely when no auth is configured in production.
|
|
@@ -667,12 +716,20 @@ async function handleSend(
|
|
|
667
716
|
approvedActions: approvedActions ?? null,
|
|
668
717
|
},
|
|
669
718
|
};
|
|
670
|
-
const task = await
|
|
719
|
+
const { task, reused } = await createOrReuseTask(
|
|
671
720
|
message,
|
|
672
721
|
contextId,
|
|
673
722
|
taskMetadata,
|
|
674
723
|
ownerEmailForTask,
|
|
724
|
+
ownerScopeForTask,
|
|
725
|
+
idempotencyKey,
|
|
675
726
|
);
|
|
727
|
+
if (reused) {
|
|
728
|
+
return {
|
|
729
|
+
...jsonRpcResult(0, sanitizeTaskForResponse(task)),
|
|
730
|
+
_id: 0,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
676
733
|
const working = await updateTask(task.id, { state: "working" });
|
|
677
734
|
|
|
678
735
|
// Awaited, not fire-and-forget: this handler is about to return, and a
|
|
@@ -695,12 +752,20 @@ async function handleSend(
|
|
|
695
752
|
}
|
|
696
753
|
|
|
697
754
|
return withA2ARequestContext(metadata, event, async () => {
|
|
698
|
-
const task = await
|
|
755
|
+
const { task, reused } = await createOrReuseTask(
|
|
699
756
|
message,
|
|
700
757
|
contextId,
|
|
701
758
|
undefined,
|
|
702
759
|
ownerEmailForTask,
|
|
760
|
+
ownerScopeForTask,
|
|
761
|
+
idempotencyKey,
|
|
703
762
|
);
|
|
763
|
+
if (reused) {
|
|
764
|
+
return {
|
|
765
|
+
...jsonRpcResult(0, sanitizeTaskForResponse(task)),
|
|
766
|
+
_id: 0,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
704
769
|
await updateTask(task.id, { state: "working" });
|
|
705
770
|
|
|
706
771
|
const ctx = makeHandlerContext(
|
|
@@ -790,8 +855,8 @@ async function handleStream(
|
|
|
790
855
|
const contextId = params.contextId as string | undefined;
|
|
791
856
|
const metadata = params.metadata as Record<string, unknown> | undefined;
|
|
792
857
|
const approvedActions = trustedApprovedActions(params.approvedActions, event);
|
|
793
|
-
const ownerEmailForTask =
|
|
794
|
-
(event
|
|
858
|
+
const { ownerEmail: ownerEmailForTask, ownerScope: ownerScopeForTask } =
|
|
859
|
+
verifiedTaskOwner(event);
|
|
795
860
|
|
|
796
861
|
await withA2ARequestContext(metadata, event, async () => {
|
|
797
862
|
const task = await createTask(
|
|
@@ -799,6 +864,7 @@ async function handleStream(
|
|
|
799
864
|
contextId,
|
|
800
865
|
undefined,
|
|
801
866
|
ownerEmailForTask,
|
|
867
|
+
ownerScopeForTask,
|
|
802
868
|
);
|
|
803
869
|
|
|
804
870
|
await updateTask(task.id, { state: "working" });
|
|
@@ -879,16 +945,23 @@ const SENSITIVE_METADATA_KEYS = new Set([
|
|
|
879
945
|
|
|
880
946
|
function sanitizeTaskForResponse(task: any): any {
|
|
881
947
|
if (!task || typeof task !== "object") return task;
|
|
882
|
-
|
|
948
|
+
const {
|
|
949
|
+
ownerEmail: _ownerEmail,
|
|
950
|
+
ownerScope: _ownerScope,
|
|
951
|
+
...publicTask
|
|
952
|
+
} = task;
|
|
953
|
+
if (!publicTask.metadata || typeof publicTask.metadata !== "object") {
|
|
954
|
+
return publicTask;
|
|
955
|
+
}
|
|
883
956
|
|
|
884
|
-
const meta =
|
|
957
|
+
const meta = publicTask.metadata as Record<string, unknown>;
|
|
885
958
|
const publicMeta: Record<string, unknown> = {};
|
|
886
959
|
for (const [k, v] of Object.entries(meta)) {
|
|
887
960
|
if (k === "__a2a_processor") continue;
|
|
888
961
|
if (SENSITIVE_METADATA_KEYS.has(k)) continue;
|
|
889
962
|
publicMeta[k] = v;
|
|
890
963
|
}
|
|
891
|
-
return { ...
|
|
964
|
+
return { ...publicTask, metadata: publicMeta };
|
|
892
965
|
}
|
|
893
966
|
|
|
894
967
|
/**
|
|
@@ -907,6 +980,7 @@ function sanitizeTaskForResponse(task: any): any {
|
|
|
907
980
|
*/
|
|
908
981
|
function authorizeTaskAccess(
|
|
909
982
|
taskOwnerEmail: string | null,
|
|
983
|
+
taskOwnerScope: string | null,
|
|
910
984
|
event: any,
|
|
911
985
|
config: A2AConfig,
|
|
912
986
|
): JsonRpcResponse | null {
|
|
@@ -928,6 +1002,15 @@ function authorizeTaskAccess(
|
|
|
928
1002
|
if (verifiedEmail.toLowerCase() !== taskOwnerEmail.toLowerCase()) {
|
|
929
1003
|
return jsonRpcError(0, -32001, "Task not found");
|
|
930
1004
|
}
|
|
1005
|
+
if (taskOwnerScope) {
|
|
1006
|
+
const verifiedScope =
|
|
1007
|
+
(event?.context?.__a2aOrgDomain as string | undefined)
|
|
1008
|
+
?.trim()
|
|
1009
|
+
.toLowerCase() ?? A2A_PERSONAL_OWNER_SCOPE;
|
|
1010
|
+
if (verifiedScope !== taskOwnerScope.toLowerCase()) {
|
|
1011
|
+
return jsonRpcError(0, -32001, "Task not found");
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
931
1014
|
}
|
|
932
1015
|
// Legacy row (no owner_email recorded). The route-level auth gate is the
|
|
933
1016
|
// only thing protecting it — fall through and serve.
|
|
@@ -943,8 +1026,13 @@ async function handleGet(
|
|
|
943
1026
|
if (!id) {
|
|
944
1027
|
return jsonRpcError(0, -32602, "Invalid params: id required");
|
|
945
1028
|
}
|
|
946
|
-
const
|
|
947
|
-
const denied = authorizeTaskAccess(
|
|
1029
|
+
const ownership = await getTaskOwnership(id);
|
|
1030
|
+
const denied = authorizeTaskAccess(
|
|
1031
|
+
ownership.ownerEmail,
|
|
1032
|
+
ownership.ownerScope,
|
|
1033
|
+
event,
|
|
1034
|
+
config,
|
|
1035
|
+
);
|
|
948
1036
|
if (denied) return denied;
|
|
949
1037
|
|
|
950
1038
|
const task = await getTask(id);
|
|
@@ -1038,8 +1126,13 @@ async function handleCancel(
|
|
|
1038
1126
|
if (!id) {
|
|
1039
1127
|
return jsonRpcError(0, -32602, "Invalid params: id required");
|
|
1040
1128
|
}
|
|
1041
|
-
const
|
|
1042
|
-
const denied = authorizeTaskAccess(
|
|
1129
|
+
const ownership = await getTaskOwnership(id);
|
|
1130
|
+
const denied = authorizeTaskAccess(
|
|
1131
|
+
ownership.ownerEmail,
|
|
1132
|
+
ownership.ownerScope,
|
|
1133
|
+
event,
|
|
1134
|
+
config,
|
|
1135
|
+
);
|
|
1043
1136
|
if (denied) return denied;
|
|
1044
1137
|
|
|
1045
1138
|
const task = await updateTask(id, { state: "canceled" });
|
|
@@ -1089,15 +1182,48 @@ async function handleInvokeReadOnlyAction(
|
|
|
1089
1182
|
return jsonRpcError(0, -32601, "Direct action invocation not supported");
|
|
1090
1183
|
}
|
|
1091
1184
|
|
|
1185
|
+
const startedAt = Date.now();
|
|
1186
|
+
const correlation = sanitizeA2ACorrelationMetadata(params.metadata);
|
|
1187
|
+
const invocationId = crypto.randomUUID();
|
|
1188
|
+
const verifiedEmail = event.context.__a2aVerifiedEmail as string;
|
|
1189
|
+
const emitTracking = (status: "completed" | "failed") => {
|
|
1190
|
+
const identity = trackingIdentityProperties();
|
|
1191
|
+
track(
|
|
1192
|
+
A2A_READ_INVOKE_EVENT,
|
|
1193
|
+
{
|
|
1194
|
+
...identity,
|
|
1195
|
+
action,
|
|
1196
|
+
receiver_app: config.appId ?? identity.app ?? config.name,
|
|
1197
|
+
caller_app: correlation.callerApp ?? "unknown",
|
|
1198
|
+
duration_ms: Math.max(0, Date.now() - startedAt),
|
|
1199
|
+
status,
|
|
1200
|
+
invocation_id: invocationId,
|
|
1201
|
+
...(correlation.parentRunId
|
|
1202
|
+
? { parent_run_id: correlation.parentRunId }
|
|
1203
|
+
: {}),
|
|
1204
|
+
...(correlation.parentTurnId
|
|
1205
|
+
? { parent_turn_id: correlation.parentTurnId }
|
|
1206
|
+
: {}),
|
|
1207
|
+
...(correlation.invocationId
|
|
1208
|
+
? { caller_invocation_id: correlation.invocationId }
|
|
1209
|
+
: {}),
|
|
1210
|
+
},
|
|
1211
|
+
{ userId: verifiedEmail },
|
|
1212
|
+
);
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1092
1215
|
try {
|
|
1093
1216
|
const result = await withA2ARequestContext(undefined, event, () =>
|
|
1094
1217
|
config.executeReadOnlyAction!({
|
|
1095
1218
|
action,
|
|
1096
1219
|
input: input as Record<string, unknown>,
|
|
1220
|
+
invocationId,
|
|
1097
1221
|
}),
|
|
1098
1222
|
);
|
|
1223
|
+
emitTracking(result.status);
|
|
1099
1224
|
return jsonRpcResult(0, { action, ...result });
|
|
1100
1225
|
} catch (error) {
|
|
1226
|
+
emitTracking("failed");
|
|
1101
1227
|
console.error(`[a2a] Direct action ${action} failed:`, error);
|
|
1102
1228
|
return jsonRpcError(0, -32000, "Direct action invocation failed");
|
|
1103
1229
|
}
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
callAction as defaultCallAction,
|
|
8
8
|
callAgent as defaultCallAgent,
|
|
9
9
|
} from "./client.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
A2ACorrelationMetadata,
|
|
12
|
+
A2AReadOnlyActionResult,
|
|
13
|
+
} from "./types.js";
|
|
11
14
|
|
|
12
15
|
export type AgentInvocationErrorCode =
|
|
13
16
|
| "missing-target"
|
|
@@ -82,6 +85,8 @@ export interface InvokeAgentOptions extends ResolveAgentInvocationTargetOptions
|
|
|
82
85
|
timeoutMs?: number;
|
|
83
86
|
pollIntervalMs?: number;
|
|
84
87
|
includeInvocationHint?: boolean;
|
|
88
|
+
correlation?: A2ACorrelationMetadata;
|
|
89
|
+
idempotencyKey?: string;
|
|
85
90
|
runtime?: Partial<AgentInvocationRuntime>;
|
|
86
91
|
}
|
|
87
92
|
|
|
@@ -94,6 +99,7 @@ export interface InvokeAgentActionOptions extends ResolveAgentInvocationTargetOp
|
|
|
94
99
|
orgDomain?: string;
|
|
95
100
|
orgSecret?: string;
|
|
96
101
|
requestTimeoutMs?: number;
|
|
102
|
+
correlation?: A2ACorrelationMetadata;
|
|
97
103
|
runtime?: Partial<AgentInvocationRuntime>;
|
|
98
104
|
}
|
|
99
105
|
|
|
@@ -197,6 +203,8 @@ export async function invokeAgent(
|
|
|
197
203
|
async: options.async,
|
|
198
204
|
timeoutMs: options.timeoutMs,
|
|
199
205
|
pollIntervalMs: options.pollIntervalMs,
|
|
206
|
+
correlation: options.correlation,
|
|
207
|
+
idempotencyKey: options.idempotencyKey,
|
|
200
208
|
});
|
|
201
209
|
|
|
202
210
|
return {
|
|
@@ -243,6 +251,7 @@ export async function invokeAgentAction(
|
|
|
243
251
|
orgDomain: options.orgDomain,
|
|
244
252
|
orgSecret: options.orgSecret,
|
|
245
253
|
requestTimeoutMs: options.requestTimeoutMs,
|
|
254
|
+
correlation: options.correlation,
|
|
246
255
|
});
|
|
247
256
|
|
|
248
257
|
return { target, action, result };
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
|
|
3
3
|
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ensureTableExists,
|
|
6
|
+
ensureColumnExists,
|
|
7
|
+
ensureIndexExists,
|
|
8
|
+
} from "../db/ddl-guard.js";
|
|
5
9
|
import type { Task, Message, TaskState, Artifact } from "./types.js";
|
|
6
10
|
|
|
7
11
|
let _initPromise: Promise<void> | undefined;
|
|
12
|
+
export const MAX_A2A_IDEMPOTENCY_KEY_CHARS = 128;
|
|
13
|
+
const A2A_IDEMPOTENCY_INDEX = "idx_a2a_tasks_owner_scope_idempotency";
|
|
14
|
+
export const A2A_PERSONAL_OWNER_SCOPE = "__personal__";
|
|
8
15
|
|
|
9
16
|
async function ensureTable(): Promise<void> {
|
|
10
17
|
if (!_initPromise) {
|
|
@@ -20,10 +27,16 @@ async function ensureTable(): Promise<void> {
|
|
|
20
27
|
history TEXT NOT NULL DEFAULT '[]',
|
|
21
28
|
artifacts TEXT NOT NULL DEFAULT '[]',
|
|
22
29
|
metadata TEXT,
|
|
30
|
+
owner_email TEXT,
|
|
31
|
+
owner_scope TEXT NOT NULL DEFAULT '',
|
|
32
|
+
idempotency_key TEXT,
|
|
23
33
|
created_at ${intType()} NOT NULL,
|
|
24
34
|
updated_at ${intType()} NOT NULL
|
|
25
35
|
)
|
|
26
36
|
`;
|
|
37
|
+
const createIdempotencyIndexSql =
|
|
38
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS ${A2A_IDEMPOTENCY_INDEX} ` +
|
|
39
|
+
`ON a2a_tasks(owner_email, owner_scope, idempotency_key)`;
|
|
27
40
|
const createApprovalsSql = `
|
|
28
41
|
CREATE TABLE IF NOT EXISTS a2a_approvals (
|
|
29
42
|
id TEXT PRIMARY KEY,
|
|
@@ -57,6 +70,20 @@ async function ensureTable(): Promise<void> {
|
|
|
57
70
|
"owner_email",
|
|
58
71
|
`ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS owner_email TEXT`,
|
|
59
72
|
);
|
|
73
|
+
await ensureColumnExists(
|
|
74
|
+
"a2a_tasks",
|
|
75
|
+
"owner_scope",
|
|
76
|
+
`ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS owner_scope TEXT NOT NULL DEFAULT ''`,
|
|
77
|
+
);
|
|
78
|
+
await ensureColumnExists(
|
|
79
|
+
"a2a_tasks",
|
|
80
|
+
"idempotency_key",
|
|
81
|
+
`ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS idempotency_key TEXT`,
|
|
82
|
+
);
|
|
83
|
+
await ensureIndexExists(
|
|
84
|
+
A2A_IDEMPOTENCY_INDEX,
|
|
85
|
+
createIdempotencyIndexSql,
|
|
86
|
+
);
|
|
60
87
|
await ensureTableExists("a2a_approvals", createApprovalsSql);
|
|
61
88
|
return;
|
|
62
89
|
}
|
|
@@ -69,6 +96,13 @@ async function ensureTable(): Promise<void> {
|
|
|
69
96
|
// have NULL owner_email and remain accessible to legacy callers via
|
|
70
97
|
// the legacy-token apiKeyEnv path; new rows are scoped from this point
|
|
71
98
|
// forward.
|
|
99
|
+
try {
|
|
100
|
+
await client.execute(
|
|
101
|
+
`ALTER TABLE a2a_tasks ADD COLUMN owner_scope TEXT NOT NULL DEFAULT ''`,
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
// Column already exists — expected on every restart after first run.
|
|
105
|
+
}
|
|
72
106
|
try {
|
|
73
107
|
await client.execute(
|
|
74
108
|
`ALTER TABLE a2a_tasks ADD COLUMN owner_email TEXT`,
|
|
@@ -76,6 +110,14 @@ async function ensureTable(): Promise<void> {
|
|
|
76
110
|
} catch {
|
|
77
111
|
// Column already exists — expected on every restart after first run.
|
|
78
112
|
}
|
|
113
|
+
try {
|
|
114
|
+
await client.execute(
|
|
115
|
+
`ALTER TABLE a2a_tasks ADD COLUMN idempotency_key TEXT`,
|
|
116
|
+
);
|
|
117
|
+
} catch {
|
|
118
|
+
// Column already exists — expected on every restart after first run.
|
|
119
|
+
}
|
|
120
|
+
await client.execute(createIdempotencyIndexSql);
|
|
79
121
|
await client.execute(createApprovalsSql);
|
|
80
122
|
})().catch((err) => {
|
|
81
123
|
// Retry init on the next call after a failed startup.
|
|
@@ -320,7 +362,10 @@ export async function pauseProcessingA2ATask(
|
|
|
320
362
|
return task;
|
|
321
363
|
}
|
|
322
364
|
|
|
323
|
-
function taskFromRow(row: any): Task & {
|
|
365
|
+
function taskFromRow(row: any): Task & {
|
|
366
|
+
ownerEmail?: string | null;
|
|
367
|
+
ownerScope?: string | null;
|
|
368
|
+
} {
|
|
324
369
|
return {
|
|
325
370
|
id: row.id as string,
|
|
326
371
|
contextId: (row.context_id as string) || undefined,
|
|
@@ -335,6 +380,7 @@ function taskFromRow(row: any): Task & { ownerEmail?: string | null } {
|
|
|
335
380
|
artifacts: JSON.parse(row.artifacts as string),
|
|
336
381
|
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined,
|
|
337
382
|
ownerEmail: (row.owner_email as string | null) ?? null,
|
|
383
|
+
ownerScope: (row.owner_scope as string | null) ?? null,
|
|
338
384
|
};
|
|
339
385
|
}
|
|
340
386
|
|
|
@@ -356,6 +402,7 @@ export async function createTask(
|
|
|
356
402
|
contextId?: string,
|
|
357
403
|
metadata?: Record<string, unknown>,
|
|
358
404
|
ownerEmail?: string | null,
|
|
405
|
+
ownerScope?: string | null,
|
|
359
406
|
): Promise<Task> {
|
|
360
407
|
await ensureTable();
|
|
361
408
|
const client = getDbExec();
|
|
@@ -373,7 +420,7 @@ export async function createTask(
|
|
|
373
420
|
};
|
|
374
421
|
|
|
375
422
|
await client.execute({
|
|
376
|
-
sql: `INSERT INTO a2a_tasks (id, context_id, status_state, status_timestamp, history, artifacts, metadata, owner_email, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
423
|
+
sql: `INSERT INTO a2a_tasks (id, context_id, status_state, status_timestamp, history, artifacts, metadata, owner_email, owner_scope, idempotency_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
377
424
|
args: [
|
|
378
425
|
id,
|
|
379
426
|
contextId ?? null,
|
|
@@ -383,6 +430,8 @@ export async function createTask(
|
|
|
383
430
|
"[]",
|
|
384
431
|
metadata ? JSON.stringify(metadata) : null,
|
|
385
432
|
ownerEmail ?? null,
|
|
433
|
+
ownerScope ?? "",
|
|
434
|
+
null,
|
|
386
435
|
now,
|
|
387
436
|
now,
|
|
388
437
|
],
|
|
@@ -391,6 +440,87 @@ export async function createTask(
|
|
|
391
440
|
return task;
|
|
392
441
|
}
|
|
393
442
|
|
|
443
|
+
export async function createOrReuseTask(
|
|
444
|
+
message: Message,
|
|
445
|
+
contextId: string | undefined,
|
|
446
|
+
metadata: Record<string, unknown> | undefined,
|
|
447
|
+
ownerEmail: string | null,
|
|
448
|
+
ownerScope: string | null,
|
|
449
|
+
idempotencyKey: string | undefined,
|
|
450
|
+
): Promise<{ task: Task; reused: boolean }> {
|
|
451
|
+
if (!ownerEmail || !idempotencyKey) {
|
|
452
|
+
return {
|
|
453
|
+
task: await createTask(
|
|
454
|
+
message,
|
|
455
|
+
contextId,
|
|
456
|
+
metadata,
|
|
457
|
+
ownerEmail,
|
|
458
|
+
ownerScope,
|
|
459
|
+
),
|
|
460
|
+
reused: false,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (idempotencyKey.length > MAX_A2A_IDEMPOTENCY_KEY_CHARS) {
|
|
464
|
+
throw new Error("A2A idempotency key is too long");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
await ensureTable();
|
|
468
|
+
const client = getDbExec();
|
|
469
|
+
const normalizedOwner = ownerEmail.trim().toLowerCase();
|
|
470
|
+
const normalizedScope =
|
|
471
|
+
ownerScope?.trim().toLowerCase() || A2A_PERSONAL_OWNER_SCOPE;
|
|
472
|
+
|
|
473
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
474
|
+
const id = crypto.randomUUID();
|
|
475
|
+
const now = Date.now();
|
|
476
|
+
const timestamp = new Date().toISOString();
|
|
477
|
+
await client.execute({
|
|
478
|
+
sql: `INSERT INTO a2a_tasks (id, context_id, status_state, status_timestamp, history, artifacts, metadata, owner_email, owner_scope, idempotency_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(owner_email, owner_scope, idempotency_key) DO NOTHING`,
|
|
479
|
+
args: [
|
|
480
|
+
id,
|
|
481
|
+
contextId ?? null,
|
|
482
|
+
"submitted",
|
|
483
|
+
timestamp,
|
|
484
|
+
JSON.stringify([message]),
|
|
485
|
+
"[]",
|
|
486
|
+
metadata ? JSON.stringify(metadata) : null,
|
|
487
|
+
normalizedOwner,
|
|
488
|
+
normalizedScope,
|
|
489
|
+
idempotencyKey,
|
|
490
|
+
now,
|
|
491
|
+
now,
|
|
492
|
+
],
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const { rows } = await client.execute({
|
|
496
|
+
sql: `SELECT * FROM a2a_tasks WHERE owner_email = ? AND owner_scope = ? AND idempotency_key = ?`,
|
|
497
|
+
args: [normalizedOwner, normalizedScope, idempotencyKey],
|
|
498
|
+
});
|
|
499
|
+
if (rows.length === 0) {
|
|
500
|
+
throw new Error("A2A idempotent task insert could not be verified");
|
|
501
|
+
}
|
|
502
|
+
const task = taskFromRow(rows[0]);
|
|
503
|
+
const reused = task.id !== id;
|
|
504
|
+
if (
|
|
505
|
+
reused &&
|
|
506
|
+
(task.status.state === "failed" || task.status.state === "canceled")
|
|
507
|
+
) {
|
|
508
|
+
await client.execute({
|
|
509
|
+
sql: `UPDATE a2a_tasks SET idempotency_key = NULL, updated_at = ? WHERE id = ? AND idempotency_key = ? AND status_state IN ('failed', 'canceled')`,
|
|
510
|
+
args: [Date.now(), task.id, idempotencyKey],
|
|
511
|
+
});
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
return { task, reused };
|
|
515
|
+
}
|
|
516
|
+
throw new Error("A2A terminal task idempotency key could not be released");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export interface A2ATaskOwnership {
|
|
520
|
+
ownerEmail: string | null;
|
|
521
|
+
ownerScope: string | null;
|
|
522
|
+
}
|
|
523
|
+
|
|
394
524
|
/**
|
|
395
525
|
* Fetch the verified owner email recorded against a task at creation time.
|
|
396
526
|
* Returns null when the task has no owner (legacy rows or unauthenticated
|
|
@@ -400,15 +530,25 @@ export async function createTask(
|
|
|
400
530
|
* verified caller's email must match `owner_email` to read or cancel.
|
|
401
531
|
*/
|
|
402
532
|
export async function getTaskOwner(id: string): Promise<string | null> {
|
|
533
|
+
return (await getTaskOwnership(id)).ownerEmail;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function getTaskOwnership(id: string): Promise<A2ATaskOwnership> {
|
|
403
537
|
await ensureTable();
|
|
404
538
|
const client = getDbExec();
|
|
405
539
|
const { rows } = await client.execute({
|
|
406
|
-
sql: `SELECT owner_email FROM a2a_tasks WHERE id = ?`,
|
|
540
|
+
sql: `SELECT owner_email, owner_scope FROM a2a_tasks WHERE id = ?`,
|
|
407
541
|
args: [id],
|
|
408
542
|
});
|
|
409
|
-
if (rows.length === 0) return null;
|
|
543
|
+
if (rows.length === 0) return { ownerEmail: null, ownerScope: null };
|
|
410
544
|
const ownerEmail = (rows[0] as any).owner_email;
|
|
411
|
-
|
|
545
|
+
const ownerScope = (rows[0] as any).owner_scope;
|
|
546
|
+
return {
|
|
547
|
+
ownerEmail:
|
|
548
|
+
typeof ownerEmail === "string" && ownerEmail ? ownerEmail : null,
|
|
549
|
+
ownerScope:
|
|
550
|
+
typeof ownerScope === "string" && ownerScope ? ownerScope : null,
|
|
551
|
+
};
|
|
412
552
|
}
|
|
413
553
|
|
|
414
554
|
/**
|
|
@@ -132,6 +132,19 @@ export interface A2AApprovedAction {
|
|
|
132
132
|
input: unknown;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Telemetry-only cross-app correlation. Receivers must never use these
|
|
137
|
+
* caller-supplied values for identity, ownership, org scoping, access, or
|
|
138
|
+
* approval decisions.
|
|
139
|
+
*/
|
|
140
|
+
export interface A2ACorrelationMetadata {
|
|
141
|
+
callerApp?: string;
|
|
142
|
+
callerThreadId?: string;
|
|
143
|
+
parentRunId?: string;
|
|
144
|
+
parentTurnId?: string;
|
|
145
|
+
invocationId?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
135
148
|
// --- Framework config ---
|
|
136
149
|
|
|
137
150
|
export interface A2AHandlerContext {
|
|
@@ -168,6 +181,7 @@ export interface A2AApprovalExecution {
|
|
|
168
181
|
export interface A2AReadOnlyActionInvocation {
|
|
169
182
|
action: string;
|
|
170
183
|
input: Record<string, unknown>;
|
|
184
|
+
invocationId: string;
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
export interface A2AReadOnlyActionResult {
|
|
@@ -183,6 +197,8 @@ export type A2AHandler = (
|
|
|
183
197
|
|
|
184
198
|
export interface A2AConfig {
|
|
185
199
|
name: string;
|
|
200
|
+
/** Canonical receiver app id used only for telemetry attribution. */
|
|
201
|
+
appId?: string;
|
|
186
202
|
description: string;
|
|
187
203
|
version?: string;
|
|
188
204
|
skills: AgentSkill[];
|