@agent-native/core 0.114.3 → 0.114.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/artifact-response.ts +42 -16
  5. package/corpus/core/src/a2a/client.ts +4 -0
  6. package/corpus/core/src/a2a/handlers.ts +133 -2
  7. package/corpus/core/src/a2a/index.ts +1 -0
  8. package/corpus/core/src/a2a/types.ts +14 -0
  9. package/corpus/core/src/integrations/index.ts +6 -0
  10. package/corpus/core/src/integrations/pending-tasks-store.ts +89 -0
  11. package/corpus/core/src/integrations/webhook-handler.ts +17 -7
  12. package/corpus/core/src/scripts/call-agent.ts +54 -10
  13. package/corpus/core/src/server/agent-discovery.ts +22 -0
  14. package/corpus/templates/content/.agents/skills/content/SKILL.md +19 -5
  15. package/corpus/templates/content/changelog/2026-07-17-slack-created-database-entries-now-record-slack-as-their-sub.md +6 -0
  16. package/dist/a2a/artifact-response.d.ts +5 -0
  17. package/dist/a2a/artifact-response.d.ts.map +1 -1
  18. package/dist/a2a/artifact-response.js +20 -5
  19. package/dist/a2a/artifact-response.js.map +1 -1
  20. package/dist/a2a/client.d.ts +3 -1
  21. package/dist/a2a/client.d.ts.map +1 -1
  22. package/dist/a2a/client.js +2 -0
  23. package/dist/a2a/client.js.map +1 -1
  24. package/dist/a2a/handlers.d.ts.map +1 -1
  25. package/dist/a2a/handlers.js +100 -8
  26. package/dist/a2a/handlers.js.map +1 -1
  27. package/dist/a2a/index.d.ts +1 -1
  28. package/dist/a2a/index.d.ts.map +1 -1
  29. package/dist/a2a/index.js.map +1 -1
  30. package/dist/a2a/types.d.ts +12 -0
  31. package/dist/a2a/types.d.ts.map +1 -1
  32. package/dist/a2a/types.js.map +1 -1
  33. package/dist/integrations/index.d.ts +1 -0
  34. package/dist/integrations/index.d.ts.map +1 -1
  35. package/dist/integrations/index.js +1 -0
  36. package/dist/integrations/index.js.map +1 -1
  37. package/dist/integrations/pending-tasks-store.d.ts +9 -0
  38. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  39. package/dist/integrations/pending-tasks-store.js +60 -0
  40. package/dist/integrations/pending-tasks-store.js.map +1 -1
  41. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  42. package/dist/integrations/webhook-handler.js +13 -7
  43. package/dist/integrations/webhook-handler.js.map +1 -1
  44. package/dist/notifications/routes.d.ts +3 -3
  45. package/dist/observability/routes.d.ts +5 -5
  46. package/dist/scripts/call-agent.d.ts.map +1 -1
  47. package/dist/scripts/call-agent.js +39 -10
  48. package/dist/scripts/call-agent.js.map +1 -1
  49. package/dist/secrets/routes.d.ts +9 -9
  50. package/dist/server/agent-discovery.d.ts +2 -0
  51. package/dist/server/agent-discovery.d.ts.map +1 -1
  52. package/dist/server/agent-discovery.js +19 -0
  53. package/dist/server/agent-discovery.js.map +1 -1
  54. package/dist/server/transcribe-voice.d.ts +1 -1
  55. package/package.json +1 -1
  56. package/src/a2a/artifact-response.ts +42 -16
  57. package/src/a2a/client.ts +4 -0
  58. package/src/a2a/handlers.ts +133 -2
  59. package/src/a2a/index.ts +1 -0
  60. package/src/a2a/types.ts +14 -0
  61. package/src/integrations/index.ts +6 -0
  62. package/src/integrations/pending-tasks-store.ts +89 -0
  63. package/src/integrations/webhook-handler.ts +17 -7
  64. package/src/scripts/call-agent.ts +54 -10
  65. package/src/server/agent-discovery.ts +22 -0
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1517
32
32
  - toolkit files: 137
33
- - template files: 6105
33
+ - template files: 6106
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.114.4
4
+
5
+ ### Patch Changes
6
+
7
+ - f3dcee3: Suppress provisional artifact warnings while delegated work is still running, and preserve verified Slack provenance for cross-app intake through an audience-scoped, fail-closed resolver.
8
+
3
9
  ## 0.114.3
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.114.3",
3
+ "version": "0.114.4",
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": {
@@ -14,6 +14,11 @@ export interface A2AArtifactResponseOptions {
14
14
  persistedArtifactSecret?: string;
15
15
  }
16
16
 
17
+ export interface GuardedA2AArtifactResponse {
18
+ text: string;
19
+ rejectedUnverifiedArtifactReferences: boolean;
20
+ }
21
+
17
22
  export interface A2AArtifactIdentityOptions {
18
23
  persistedArtifactSecrets?: readonly string[];
19
24
  }
@@ -1395,11 +1400,11 @@ function formatUnverifiedArtifactMessage(
1395
1400
  : message;
1396
1401
  }
1397
1402
 
1398
- export function appendA2AArtifactLinks(
1403
+ export function guardA2AArtifactResponse(
1399
1404
  responseText: string,
1400
1405
  toolResults: A2AToolResultSummary[],
1401
1406
  options: A2AArtifactResponseOptions = {},
1402
- ): string {
1407
+ ): GuardedA2AArtifactResponse {
1403
1408
  const baseUrl = normalizeBaseUrl(options.baseUrl);
1404
1409
  const includeReferencedArtifacts =
1405
1410
  options.includeReferencedArtifacts ?? false;
@@ -1440,7 +1445,10 @@ export function appendA2AArtifactLinks(
1440
1445
  ) ||
1441
1446
  /\b(?:done|created|ready|here(?:'s| is)|complete|finished)\b/i.test(text))
1442
1447
  ) {
1443
- return finalize(formatIncompleteDesignMessage(incompleteShells));
1448
+ return {
1449
+ text: finalize(formatIncompleteDesignMessage(incompleteShells)),
1450
+ rejectedUnverifiedArtifactReferences: false,
1451
+ };
1444
1452
  }
1445
1453
 
1446
1454
  const unverifiedRefs = findUnverifiedArtifactReferences(
@@ -1454,18 +1462,21 @@ export function appendA2AArtifactLinks(
1454
1462
  generatedDesigns,
1455
1463
  );
1456
1464
  if (unverifiedRefs.length > 0) {
1457
- return finalize(
1458
- formatUnverifiedArtifactMessage(
1459
- unverifiedRefs,
1460
- documents,
1461
- decks,
1462
- dashboards,
1463
- analyses,
1464
- images,
1465
- generatedDesigns,
1466
- baseUrl,
1465
+ return {
1466
+ text: finalize(
1467
+ formatUnverifiedArtifactMessage(
1468
+ unverifiedRefs,
1469
+ documents,
1470
+ decks,
1471
+ dashboards,
1472
+ analyses,
1473
+ images,
1474
+ generatedDesigns,
1475
+ baseUrl,
1476
+ ),
1467
1477
  ),
1468
- );
1478
+ rejectedUnverifiedArtifactReferences: true,
1479
+ };
1469
1480
  }
1470
1481
 
1471
1482
  const missingLines: string[] = [];
@@ -1541,10 +1552,25 @@ export function appendA2AArtifactLinks(
1541
1552
  }
1542
1553
 
1543
1554
  if (missingLines.length === 0) {
1544
- return finalize(text);
1555
+ return {
1556
+ text: finalize(text),
1557
+ rejectedUnverifiedArtifactReferences: false,
1558
+ };
1545
1559
  }
1560
+
1546
1561
  const artifactBlock = `Artifacts:\n${missingLines.join("\n")}`;
1547
- return finalize(text ? `${text}\n\n${artifactBlock}` : artifactBlock);
1562
+ return {
1563
+ text: finalize(text ? `${text}\n\n${artifactBlock}` : artifactBlock),
1564
+ rejectedUnverifiedArtifactReferences: false,
1565
+ };
1566
+ }
1567
+
1568
+ export function appendA2AArtifactLinks(
1569
+ responseText: string,
1570
+ toolResults: A2AToolResultSummary[],
1571
+ options: A2AArtifactResponseOptions = {},
1572
+ ): string {
1573
+ return guardA2AArtifactResponse(responseText, toolResults, options).text;
1548
1574
  }
1549
1575
 
1550
1576
  export function buildA2ARecoverableArtifactMessage(
@@ -5,6 +5,7 @@ import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
5
5
  import type {
6
6
  A2AApprovedAction,
7
7
  A2ACorrelationMetadata,
8
+ A2ASourceContextReference,
8
9
  A2AReadOnlyActionResult,
9
10
  AgentCard,
10
11
  JsonRpcRequest,
@@ -632,6 +633,8 @@ export async function callAgent(
632
633
  requestOrigin?: string;
633
634
  /** Exact downstream actions explicitly authorized in the caller's chat. */
634
635
  approvedActions?: A2AApprovedAction[];
636
+ /** Opaque provenance reference resolved by the receiver through Dispatch. */
637
+ sourceContext?: A2ASourceContextReference;
635
638
  /** Bounded telemetry-only lineage forwarded to the receiving app. */
636
639
  correlation?: A2ACorrelationMetadata;
637
640
  /** Stable caller-generated key for one message submission. */
@@ -673,6 +676,7 @@ export async function callAgent(
673
676
  if (opts?.userEmail) metadata.userEmail = opts.userEmail;
674
677
  if (opts?.orgDomain) metadata.orgDomain = opts.orgDomain;
675
678
  if (opts?.requestOrigin) metadata.requestOrigin = opts.requestOrigin;
679
+ if (opts?.sourceContext) metadata.sourceContext = opts.sourceContext;
676
680
  Object.assign(metadata, sanitizeA2ACorrelationMetadata(opts?.correlation));
677
681
 
678
682
  // Default to async + poll. The receiving A2A server's `_process-task` route
@@ -10,6 +10,8 @@ import {
10
10
  resolveAgentChatProcessRunDispatchPath,
11
11
  } from "../agent/durable-background.js";
12
12
  import { trackingIdentityProperties } from "../observability/tracking-identity.js";
13
+ import { getA2ASecretByDomain } from "../org/context.js";
14
+ import { findWorkspaceDispatchAgent } from "../server/agent-discovery.js";
13
15
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
14
16
  import { getOrigin, isConfiguredAppOrigin } from "../server/google-oauth.js";
15
17
  import { fireInternalDispatch } from "../server/self-dispatch.js";
@@ -19,6 +21,7 @@ import {
19
21
  hasConfiguredA2ASecret,
20
22
  isA2AProductionRuntime,
21
23
  } from "./auth-policy.js";
24
+ import { callAction } from "./client.js";
22
25
  import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
23
26
  import {
24
27
  createTask,
@@ -39,6 +42,8 @@ import {
39
42
  } from "./task-store.js";
40
43
  import type {
41
44
  A2AApprovedAction,
45
+ A2ASourceContext,
46
+ A2ASourceContextReference,
42
47
  A2AConfig,
43
48
  A2AHandler,
44
49
  A2AHandlerContext,
@@ -84,6 +89,107 @@ function trustedApprovedActions(
84
89
  return approved.length > 0 ? approved : undefined;
85
90
  }
86
91
 
92
+ function sourceContextReference(
93
+ value: unknown,
94
+ ): A2ASourceContextReference | undefined {
95
+ if (!value || typeof value !== "object") {
96
+ return undefined;
97
+ }
98
+ const candidate = value as Record<string, unknown>;
99
+ if (
100
+ candidate.platform !== "slack" ||
101
+ typeof candidate.integrationTaskId !== "string"
102
+ ) {
103
+ return undefined;
104
+ }
105
+ const integrationTaskId = candidate.integrationTaskId;
106
+ if (
107
+ !integrationTaskId ||
108
+ integrationTaskId !== integrationTaskId.trim() ||
109
+ integrationTaskId.length > 200
110
+ ) {
111
+ return undefined;
112
+ }
113
+ return { platform: "slack", integrationTaskId };
114
+ }
115
+
116
+ function resolvedSlackSourceContext(
117
+ value: unknown,
118
+ ): A2ASourceContext | undefined {
119
+ if (!value || typeof value !== "object") return undefined;
120
+ const candidate = value as Record<string, unknown>;
121
+ if (
122
+ candidate.platform !== "slack" ||
123
+ typeof candidate.sourceUrl !== "string"
124
+ ) {
125
+ return undefined;
126
+ }
127
+ const sourceUrl = candidate.sourceUrl;
128
+ if (!sourceUrl || sourceUrl !== sourceUrl.trim()) return undefined;
129
+ try {
130
+ const parsed = new URL(sourceUrl);
131
+ const isSlackHost =
132
+ parsed.hostname === "slack.com" || parsed.hostname.endsWith(".slack.com");
133
+ if (
134
+ parsed.protocol !== "https:" ||
135
+ !isSlackHost ||
136
+ parsed.username ||
137
+ parsed.password ||
138
+ parsed.port
139
+ ) {
140
+ return undefined;
141
+ }
142
+ return { platform: "slack", sourceUrl };
143
+ } catch {
144
+ return undefined;
145
+ }
146
+ }
147
+
148
+ async function trustedSourceContext(
149
+ value: unknown,
150
+ event: any | undefined,
151
+ ): Promise<A2ASourceContext | undefined> {
152
+ const verifiedEmail = event?.context?.__a2aVerifiedEmail as
153
+ | string
154
+ | undefined;
155
+ const reference = sourceContextReference(value);
156
+ if (
157
+ !verifiedEmail ||
158
+ event?.context?.__a2aAudienceVerified !== true ||
159
+ !reference
160
+ ) {
161
+ return undefined;
162
+ }
163
+
164
+ const dispatch = findWorkspaceDispatchAgent();
165
+ if (!dispatch) return undefined;
166
+ const orgDomain = event?.context?.__a2aOrgDomain as string | undefined;
167
+ let orgSecret: string | undefined;
168
+ if (orgDomain) {
169
+ try {
170
+ orgSecret = (await getA2ASecretByDomain(orgDomain)) ?? undefined;
171
+ } catch {}
172
+ }
173
+
174
+ try {
175
+ const result = await callAction(
176
+ dispatch.url,
177
+ "resolve-integration-source-context",
178
+ { integrationTaskId: reference.integrationTaskId },
179
+ {
180
+ userEmail: verifiedEmail,
181
+ orgDomain,
182
+ orgSecret,
183
+ requestTimeoutMs: 5_000,
184
+ },
185
+ );
186
+ if (result.status !== "completed") return undefined;
187
+ return resolvedSlackSourceContext(JSON.parse(result.output));
188
+ } catch {
189
+ return undefined;
190
+ }
191
+ }
192
+
87
193
  /**
88
194
  * Request origin is routing/link context, not an identity signal. Accept only
89
195
  * an absolute HTTP(S) origin from caller metadata so queued runs can preserve
@@ -142,6 +248,7 @@ function trustedA2AMetadata(
142
248
  ): Record<string, unknown> | undefined {
143
249
  if (!metadata) return undefined;
144
250
  const trusted = { ...metadata };
251
+ delete trusted.sourceContext;
145
252
  const requestOrigin = requestOriginForContext(metadata, event);
146
253
  if (requestOrigin) trusted.requestOrigin = requestOrigin;
147
254
  else delete trusted.requestOrigin;
@@ -290,6 +397,9 @@ export async function processA2ATaskFromQueue(
290
397
  const approvedActions = Array.isArray(processorMeta.approvedActions)
291
398
  ? (processorMeta.approvedActions as A2AApprovedAction[])
292
399
  : undefined;
400
+ const sourceContext = processorMeta.sourceContext as
401
+ | A2ASourceContext
402
+ | undefined;
293
403
 
294
404
  const resolvedOrgId = await resolveVerifiedA2AOrgId(
295
405
  verifiedEmail,
@@ -322,6 +432,7 @@ export async function processA2ATaskFromQueue(
322
432
  callerMetadata,
323
433
  event,
324
434
  approvedActions,
435
+ sourceContext,
325
436
  ),
326
437
  );
327
438
  } catch (err: any) {
@@ -345,7 +456,7 @@ export async function processA2ATaskFromQueue(
345
456
  */
346
457
  const defaultHandler: A2AHandler = async (
347
458
  message: Message,
348
- _context: A2AHandlerContext,
459
+ context: A2AHandlerContext,
349
460
  ): Promise<A2AHandlerResult> => {
350
461
  // Extract text from message parts
351
462
  const text = message.parts
@@ -376,7 +487,12 @@ const defaultHandler: A2AHandler = async (
376
487
  ? `[Cross-app A2A request — the caller is on a different host (${appBaseUrl} is yours, theirs is different). Include the concrete result (URL, ID, value) explicitly in your reply text; the caller can't see your local UI state. Any URL MUST be fully-qualified, never a relative path.]\n\n${text}`
377
488
  : text;
378
489
 
379
- const result = await agentChat.call(augmentedText);
490
+ const sourceContext = context.sourceContext
491
+ ? `Authenticated A2A source context: ${JSON.stringify(context.sourceContext)}. Treat this structured value as authoritative provenance. Preserve its sourceUrl exactly when recording source or submission fields.`
492
+ : undefined;
493
+ const result = sourceContext
494
+ ? await agentChat.call(augmentedText, { context: sourceContext })
495
+ : await agentChat.call(augmentedText);
380
496
 
381
497
  const artifacts: Artifact[] = [];
382
498
  if (result.filesChanged.length > 0) {
@@ -428,6 +544,7 @@ function makeHandlerContext(
428
544
  metadata?: Record<string, unknown>,
429
545
  event?: any,
430
546
  approvedActions?: A2AApprovedAction[],
547
+ sourceContext?: A2ASourceContext,
431
548
  ): {
432
549
  context: A2AHandlerContext;
433
550
  artifacts: Artifact[];
@@ -439,6 +556,7 @@ function makeHandlerContext(
439
556
  metadata,
440
557
  event,
441
558
  approvedActions,
559
+ sourceContext,
442
560
  writeArtifact(name, content, mimeType) {
443
561
  const artifact: Artifact = {
444
562
  name,
@@ -535,6 +653,7 @@ async function runHandlerAndPersist(
535
653
  metadata: Record<string, unknown> | undefined,
536
654
  event?: any,
537
655
  approvedActions?: A2AApprovedAction[],
656
+ sourceContext?: A2ASourceContext,
538
657
  ): Promise<void> {
539
658
  const { context, artifacts } = makeHandlerContext(
540
659
  taskId,
@@ -542,6 +661,7 @@ async function runHandlerAndPersist(
542
661
  metadata,
543
662
  event,
544
663
  approvedActions,
664
+ sourceContext,
545
665
  );
546
666
  try {
547
667
  const result = getHandler(config)(message, context);
@@ -625,6 +745,10 @@ async function handleSend(
625
745
  const contextId = params.contextId as string | undefined;
626
746
  const metadata = params.metadata as Record<string, unknown> | undefined;
627
747
  const approvedActions = trustedApprovedActions(params.approvedActions, event);
748
+ const sourceContext = await trustedSourceContext(
749
+ metadata?.sourceContext,
750
+ event,
751
+ );
628
752
 
629
753
  // The JWT-verified caller email (set by mountA2A in server.ts) is the
630
754
  // single source of truth for task ownership — bound at creation, checked
@@ -714,6 +838,7 @@ async function handleSend(
714
838
  contextId: contextId ?? null,
715
839
  callerMetadata: safeMetadata ?? null,
716
840
  approvedActions: approvedActions ?? null,
841
+ sourceContext: sourceContext ?? null,
717
842
  },
718
843
  };
719
844
  const { task, reused } = await createOrReuseTask(
@@ -774,6 +899,7 @@ async function handleSend(
774
899
  trustedA2AMetadata(metadata, event),
775
900
  event,
776
901
  approvedActions,
902
+ sourceContext,
777
903
  );
778
904
 
779
905
  try {
@@ -855,6 +981,10 @@ async function handleStream(
855
981
  const contextId = params.contextId as string | undefined;
856
982
  const metadata = params.metadata as Record<string, unknown> | undefined;
857
983
  const approvedActions = trustedApprovedActions(params.approvedActions, event);
984
+ const sourceContext = await trustedSourceContext(
985
+ metadata?.sourceContext,
986
+ event,
987
+ );
858
988
  const { ownerEmail: ownerEmailForTask, ownerScope: ownerScopeForTask } =
859
989
  verifiedTaskOwner(event);
860
990
 
@@ -875,6 +1005,7 @@ async function handleStream(
875
1005
  trustedA2AMetadata(metadata, event),
876
1006
  event,
877
1007
  approvedActions,
1008
+ sourceContext,
878
1009
  );
879
1010
 
880
1011
  try {
@@ -20,6 +20,7 @@ export type {
20
20
  A2AHandler,
21
21
  A2AHandlerContext,
22
22
  A2AHandlerResult,
23
+ A2ASourceContext,
23
24
  AgentCard,
24
25
  AgentSkill,
25
26
  AgentCapabilities,
@@ -132,6 +132,18 @@ export interface A2AApprovedAction {
132
132
  input: unknown;
133
133
  }
134
134
 
135
+ /** Structured provenance accepted only from an authenticated A2A caller. */
136
+ export interface A2ASourceContext {
137
+ platform: "slack";
138
+ sourceUrl: string;
139
+ }
140
+
141
+ /** Opaque reference that a receiver must resolve through its trusted Dispatch app. */
142
+ export interface A2ASourceContextReference {
143
+ platform: "slack";
144
+ integrationTaskId: string;
145
+ }
146
+
135
147
  /**
136
148
  * Telemetry-only cross-app correlation. Receivers must never use these
137
149
  * caller-supplied values for identity, ownership, org scoping, access, or
@@ -156,6 +168,8 @@ export interface A2AHandlerContext {
156
168
  event?: unknown;
157
169
  /** Exact one-time action grants from a JWT-authenticated caller. */
158
170
  approvedActions?: A2AApprovedAction[];
171
+ /** Receiver-validated provenance from a JWT-authenticated caller. */
172
+ sourceContext?: A2ASourceContext;
159
173
  writeArtifact: (name: string, content: string, mimeType?: string) => string;
160
174
  }
161
175
 
@@ -195,6 +195,12 @@ export type {
195
195
  IntegrationScopePolicy,
196
196
  } from "./scope-store.js";
197
197
 
198
+ export {
199
+ resolveIntegrationSourceContext,
200
+ sourceContextFromPendingTask,
201
+ type ResolvedIntegrationSourceContext,
202
+ } from "./pending-tasks-store.js";
203
+
198
204
  export {
199
205
  getIntegrationBudgetSnapshot,
200
206
  getIntegrationUsageBudget,
@@ -246,6 +246,95 @@ export async function getPendingTask(id: string): Promise<PendingTask | null> {
246
246
  return rowToTask(rows[0] as Record<string, unknown>);
247
247
  }
248
248
 
249
+ export interface ResolvedIntegrationSourceContext {
250
+ platform: "slack";
251
+ sourceUrl: string;
252
+ }
253
+
254
+ type PendingTaskSourceRow = Pick<
255
+ PendingTask,
256
+ "platform" | "payload" | "ownerEmail" | "orgId"
257
+ >;
258
+
259
+ export function sourceContextFromPendingTask(
260
+ task: PendingTaskSourceRow | null,
261
+ ownerEmail: string,
262
+ orgId: string | null,
263
+ ): ResolvedIntegrationSourceContext | null {
264
+ if (
265
+ !task ||
266
+ task.ownerEmail !== ownerEmail ||
267
+ task.orgId !== orgId ||
268
+ task.platform !== "slack"
269
+ ) {
270
+ return null;
271
+ }
272
+
273
+ try {
274
+ const payload = JSON.parse(task.payload) as Record<string, unknown>;
275
+ const incoming = payload.incoming;
276
+ if (!incoming || typeof incoming !== "object") return null;
277
+ const incomingRecord = incoming as Record<string, unknown>;
278
+ if (incomingRecord.platform !== "slack") return null;
279
+ const sourceUrl = incomingRecord.sourceUrl;
280
+ if (
281
+ typeof sourceUrl !== "string" ||
282
+ !sourceUrl ||
283
+ sourceUrl !== sourceUrl.trim()
284
+ ) {
285
+ return null;
286
+ }
287
+
288
+ const parsed = new URL(sourceUrl);
289
+ const isSlackHost =
290
+ parsed.hostname === "slack.com" || parsed.hostname.endsWith(".slack.com");
291
+ if (
292
+ parsed.protocol !== "https:" ||
293
+ !isSlackHost ||
294
+ parsed.username ||
295
+ parsed.password ||
296
+ parsed.port
297
+ ) {
298
+ return null;
299
+ }
300
+ return { platform: "slack", sourceUrl };
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+
306
+ /** Resolve trusted Slack provenance without exposing the stored task payload. */
307
+ export async function resolveIntegrationSourceContext(
308
+ id: string,
309
+ ownerEmail: string,
310
+ orgId: string | null,
311
+ ): Promise<ResolvedIntegrationSourceContext | null> {
312
+ await ensureTable();
313
+ const client = getDbExec();
314
+ const { rows } = await client.execute({
315
+ sql: `SELECT platform, payload, owner_email, org_id
316
+ FROM integration_pending_tasks
317
+ WHERE id = ?
318
+ AND owner_email = ?
319
+ AND (org_id = ? OR (org_id IS NULL AND ? IS NULL))
320
+ AND platform = 'slack'
321
+ LIMIT 1`,
322
+ args: [id, ownerEmail, orgId, orgId],
323
+ });
324
+ if (rows.length === 0) return null;
325
+ const row = rows[0] as Record<string, unknown>;
326
+ return sourceContextFromPendingTask(
327
+ {
328
+ platform: row.platform as string,
329
+ payload: row.payload as string,
330
+ ownerEmail: row.owner_email as string,
331
+ orgId: (row.org_id as string | null) ?? null,
332
+ },
333
+ ownerEmail,
334
+ orgId,
335
+ );
336
+ }
337
+
249
338
  /**
250
339
  * Atomically claim a task: transition pending → processing and increment
251
340
  * attempts. Returns the updated task if the transition succeeded, otherwise
@@ -1,9 +1,9 @@
1
1
  import type { H3Event } from "h3";
2
2
 
3
3
  import {
4
- appendA2AArtifactLinks,
5
4
  buildA2AVerifiedMutationReceipt,
6
5
  extractA2AArtifactIdentities,
6
+ guardA2AArtifactResponse,
7
7
  type A2AArtifactIdentity,
8
8
  type A2AToolResultSummary,
9
9
  } from "../a2a/artifact-response.js";
@@ -429,7 +429,7 @@ async function enqueueAndDispatch(
429
429
  options: WebhookHandlerOptions,
430
430
  handlerStartedAt = Date.now(),
431
431
  ): Promise<void> {
432
- const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
432
+ const taskId = crypto.randomUUID();
433
433
 
434
434
  // Resolve the org id once at enqueue-time so the processor doesn't have to
435
435
  // re-derive it (and so we can drop it on the row for observability).
@@ -1029,7 +1029,7 @@ async function processIncomingMessage(
1029
1029
  }
1030
1030
  }
1031
1031
 
1032
- const suppressPlatformReply =
1032
+ let suppressPlatformReply =
1033
1033
  queuedA2AContinuation &&
1034
1034
  isQueuedA2AContinuationDeferral(responseText);
1035
1035
 
@@ -1088,10 +1088,20 @@ async function processIncomingMessage(
1088
1088
  // platforms with rich blocks (Slack) can render a button instead
1089
1089
  // of inlining a `<url|text>` link that auto-unfurls into a giant
1090
1090
  // preview card.
1091
- if (!suppressPlatformReply) {
1092
- responseText = appendA2AArtifactLinks(responseText, toolResults, {
1093
- baseUrl: appBaseUrl || undefined,
1094
- });
1091
+ const guardedResponse = guardA2AArtifactResponse(
1092
+ responseText,
1093
+ toolResults,
1094
+ { baseUrl: appBaseUrl || undefined },
1095
+ );
1096
+ const queuedArtifactRejection =
1097
+ queuedA2AContinuation &&
1098
+ guardedResponse.rejectedUnverifiedArtifactReferences;
1099
+ if (queuedArtifactRejection && verifiedMutationReceipt) {
1100
+ responseText = verifiedMutationReceipt;
1101
+ suppressPlatformReply = false;
1102
+ } else {
1103
+ responseText = guardedResponse.text;
1104
+ suppressPlatformReply ||= queuedArtifactRejection;
1095
1105
  }
1096
1106
  const threadDeepLinkUrl =
1097
1107
  appBaseUrl && threadId