@agent-native/core 0.109.0 → 0.109.1
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 +7 -0
- 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/use-run-stuck-detection.ts +27 -1
- package/corpus/core/src/db/client.ts +55 -0
- 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/actions/finalize-recording.ts +541 -62
- 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 +1 -1
- package/corpus/templates/clips/app/routes/record.tsx +3 -3
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -3
- 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-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 +18 -6
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +111 -13
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +16 -7
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -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 +45 -18
- package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +15 -0
- package/corpus/templates/clips/server/lib/media-verification-state.ts +38 -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 +6 -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]/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/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/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/collab/struct-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/observability/routes.d.ts +5 -5
- 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/resources/handlers.d.ts +2 -2
- 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/secrets/routes.d.ts +9 -9
- 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/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
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.109.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d74a5a4: Return retryable responses for transient agent-chat database failures and back off active-run polling while the database recovers.
|
|
8
|
+
- d74a5a4: Add bounded per-tool generation telemetry, delegated trace linkage, direct-read telemetry, and idempotent A2A submissions.
|
|
9
|
+
|
|
3
10
|
## 0.109.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.109.
|
|
3
|
+
"version": "0.109.1",
|
|
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": {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as jose from "jose";
|
|
2
2
|
|
|
3
3
|
import { ssrfSafeFetch } from "../extensions/url-safety.js";
|
|
4
|
+
import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
|
|
4
5
|
import type {
|
|
5
6
|
A2AApprovedAction,
|
|
7
|
+
A2ACorrelationMetadata,
|
|
6
8
|
A2AReadOnlyActionResult,
|
|
7
9
|
AgentCard,
|
|
8
10
|
JsonRpcRequest,
|
|
@@ -239,6 +241,7 @@ export class A2AClient {
|
|
|
239
241
|
opts?: {
|
|
240
242
|
contextId?: string;
|
|
241
243
|
metadata?: Record<string, unknown>;
|
|
244
|
+
idempotencyKey?: string;
|
|
242
245
|
approvedActions?: A2AApprovedAction[];
|
|
243
246
|
/**
|
|
244
247
|
* If true, ask the server to return the task immediately in `working`
|
|
@@ -255,6 +258,7 @@ export class A2AClient {
|
|
|
255
258
|
message,
|
|
256
259
|
contextId: opts?.contextId,
|
|
257
260
|
metadata: opts?.metadata,
|
|
261
|
+
...(opts?.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
|
|
258
262
|
...(opts?.approvedActions?.length
|
|
259
263
|
? { approvedActions: opts.approvedActions }
|
|
260
264
|
: {}),
|
|
@@ -291,8 +295,14 @@ export class A2AClient {
|
|
|
291
295
|
async invokeAction(
|
|
292
296
|
action: string,
|
|
293
297
|
input: Record<string, unknown> = {},
|
|
298
|
+
opts?: { metadata?: A2ACorrelationMetadata },
|
|
294
299
|
): Promise<A2AReadOnlyActionResult> {
|
|
295
|
-
const
|
|
300
|
+
const metadata = sanitizeA2ACorrelationMetadata(opts?.metadata);
|
|
301
|
+
const response = await this.rpc("actions/invoke", {
|
|
302
|
+
action,
|
|
303
|
+
input,
|
|
304
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
305
|
+
});
|
|
296
306
|
if (response.error) {
|
|
297
307
|
throw new Error(
|
|
298
308
|
`A2A error (${response.error.code}): ${response.error.message}`,
|
|
@@ -315,6 +325,7 @@ export class A2AClient {
|
|
|
315
325
|
opts?: {
|
|
316
326
|
contextId?: string;
|
|
317
327
|
metadata?: Record<string, unknown>;
|
|
328
|
+
idempotencyKey?: string;
|
|
318
329
|
approvedActions?: A2AApprovedAction[];
|
|
319
330
|
/** Total time to wait for completion. Default 5 min. */
|
|
320
331
|
timeoutMs?: number;
|
|
@@ -327,6 +338,7 @@ export class A2AClient {
|
|
|
327
338
|
const submitted = await this.send(message, {
|
|
328
339
|
contextId: opts?.contextId,
|
|
329
340
|
metadata: opts?.metadata,
|
|
341
|
+
idempotencyKey: opts?.idempotencyKey,
|
|
330
342
|
...(opts?.approvedActions?.length
|
|
331
343
|
? { approvedActions: opts.approvedActions }
|
|
332
344
|
: {}),
|
|
@@ -620,6 +632,10 @@ export async function callAgent(
|
|
|
620
632
|
requestOrigin?: string;
|
|
621
633
|
/** Exact downstream actions explicitly authorized in the caller's chat. */
|
|
622
634
|
approvedActions?: A2AApprovedAction[];
|
|
635
|
+
/** Bounded telemetry-only lineage forwarded to the receiving app. */
|
|
636
|
+
correlation?: A2ACorrelationMetadata;
|
|
637
|
+
/** Stable caller-generated key for one message submission. */
|
|
638
|
+
idempotencyKey?: string;
|
|
623
639
|
/**
|
|
624
640
|
* Use async/poll instead of a single blocking POST. Recommended for
|
|
625
641
|
* cross-app calls that may exceed a synchronous serverless request budget.
|
|
@@ -657,6 +673,7 @@ export async function callAgent(
|
|
|
657
673
|
if (opts?.userEmail) metadata.userEmail = opts.userEmail;
|
|
658
674
|
if (opts?.orgDomain) metadata.orgDomain = opts.orgDomain;
|
|
659
675
|
if (opts?.requestOrigin) metadata.requestOrigin = opts.requestOrigin;
|
|
676
|
+
Object.assign(metadata, sanitizeA2ACorrelationMetadata(opts?.correlation));
|
|
660
677
|
|
|
661
678
|
// Default to async + poll. The receiving A2A server's `_process-task` route
|
|
662
679
|
// runs the handler in a fresh function execution (cross-platform queue
|
|
@@ -691,6 +708,7 @@ export async function callAgent(
|
|
|
691
708
|
: await client.sendAndWait(message, {
|
|
692
709
|
contextId: opts?.contextId,
|
|
693
710
|
metadata,
|
|
711
|
+
idempotencyKey: opts?.idempotencyKey,
|
|
694
712
|
...(opts?.approvedActions?.length
|
|
695
713
|
? { approvedActions: opts.approvedActions }
|
|
696
714
|
: {}),
|
|
@@ -705,6 +723,7 @@ export async function callAgent(
|
|
|
705
723
|
task = await client.send(message, {
|
|
706
724
|
contextId: opts?.contextId,
|
|
707
725
|
metadata,
|
|
726
|
+
idempotencyKey: opts?.idempotencyKey,
|
|
708
727
|
...(opts?.approvedActions?.length
|
|
709
728
|
? { approvedActions: opts.approvedActions }
|
|
710
729
|
: {}),
|
|
@@ -756,6 +775,7 @@ export async function callAction(
|
|
|
756
775
|
orgDomain?: string;
|
|
757
776
|
orgSecret?: string;
|
|
758
777
|
requestTimeoutMs?: number;
|
|
778
|
+
correlation?: A2ACorrelationMetadata;
|
|
759
779
|
},
|
|
760
780
|
): Promise<A2AReadOnlyActionResult> {
|
|
761
781
|
const actionName = action.trim();
|
|
@@ -775,7 +795,9 @@ export async function callAction(
|
|
|
775
795
|
fallbackApiKeys,
|
|
776
796
|
requestTimeoutMs: opts?.requestTimeoutMs,
|
|
777
797
|
});
|
|
778
|
-
return client.invokeAction(actionName, input
|
|
798
|
+
return client.invokeAction(actionName, input, {
|
|
799
|
+
metadata: sanitizeA2ACorrelationMetadata(opts?.correlation),
|
|
800
|
+
});
|
|
779
801
|
}
|
|
780
802
|
|
|
781
803
|
async function buildA2AApiKeyAttempts(
|
|
@@ -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 };
|