@agent-native/core 0.201.0 → 0.201.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.
Files changed (53) hide show
  1. package/dist/a2a/handlers.js +96 -44
  2. package/dist/a2a/organization-identity.d.ts +9 -0
  3. package/dist/a2a/organization-identity.js +34 -0
  4. package/dist/a2a/server.d.ts +13 -7
  5. package/dist/a2a/server.js +187 -88
  6. package/dist/a2a/task-store.d.ts +7 -2
  7. package/dist/a2a/task-store.js +23 -8
  8. package/dist/a2a-claims.js +12 -2
  9. package/dist/app-config/mcp.d.ts +4 -0
  10. package/dist/app-config/mcp.js +7 -0
  11. package/dist/app-config/schema.d.ts +3 -0
  12. package/dist/app-config/schema.js +2 -0
  13. package/dist/automation/index.d.ts +1 -1
  14. package/dist/client/route-chunk-recovery.js +75 -11
  15. package/dist/client/route-warmup.d.ts +2 -0
  16. package/dist/client/route-warmup.js +3 -0
  17. package/dist/client/vite-dev-recovery-script.js +85 -17
  18. package/dist/collab/awareness.d.ts +2 -2
  19. package/dist/collab/struct-routes.d.ts +1 -1
  20. package/dist/deploy/build.js +3 -1
  21. package/dist/feature-flags/a2a-action-route.js +3 -4
  22. package/dist/mcp/build-server.d.ts +4 -0
  23. package/dist/mcp/build-server.js +152 -43
  24. package/dist/mcp/embed-app.js +5 -5
  25. package/dist/mcp/server.d.ts +0 -18
  26. package/dist/mcp/server.js +99 -9
  27. package/dist/onboarding/app-profile.js +2 -1
  28. package/dist/onboarding/workspace-services.js +3 -0
  29. package/dist/org/context.d.ts +13 -0
  30. package/dist/org/context.js +60 -0
  31. package/dist/org/handlers.d.ts +1 -1
  32. package/dist/org/index.d.ts +1 -1
  33. package/dist/org/membership.d.ts +6 -0
  34. package/dist/org/membership.js +23 -5
  35. package/dist/progress/routes.d.ts +1 -1
  36. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  37. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  38. package/dist/secrets/routes.d.ts +11 -11
  39. package/dist/server/action-routes.js +12 -5
  40. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  41. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  42. package/dist/server/builder-browser.d.ts +4 -2
  43. package/dist/server/builder-browser.js +147 -9
  44. package/dist/server/core-routes-plugin.js +13 -6
  45. package/dist/server/entry-server.js +43 -2
  46. package/dist/server/sentry.js +9 -1
  47. package/dist/server/ssr-handler.d.ts +1 -1
  48. package/dist/server/ssr-handler.js +3 -7
  49. package/dist/shared/cache-control.d.ts +4 -1
  50. package/dist/shared/cache-control.js +11 -6
  51. package/dist/shared/route-chunk-recovery-bootstrap.d.ts +7 -0
  52. package/dist/shared/route-chunk-recovery-bootstrap.js +38 -0
  53. package/package.json +5 -3
@@ -5,14 +5,14 @@ import { trackingIdentityProperties } from "../observability/tracking-identity.j
5
5
  import { findWorkspaceDispatchAgent } from "../server/agent-discovery.js";
6
6
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
7
7
  import { getOrigin, isConfiguredAppOrigin } from "../server/google-oauth.js";
8
+ import { markExplicitPersonalOrgScope } from "../server/request-context.js";
8
9
  import { fireInternalDispatch } from "../server/self-dispatch.js";
9
10
  import { agentChat } from "../shared/agent-chat.js";
10
11
  import { track } from "../tracking/registry.js";
11
12
  import { hasConfiguredA2ASecret, isA2AProductionRuntime, } from "./auth-policy.js";
12
13
  import { callAction } from "./client.js";
13
14
  import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
14
- import { createTask, createOrReuseTask, getTask, getTaskOwnership, updateTask, claimA2ATaskForProcessing, getA2ATaskDispatchState, failStuckA2ATask, failStuckQueuedA2ATask, settleProcessingA2ATask, touchQueuedA2ATaskDispatch, touchProcessingA2ATask, pauseProcessingA2ATask, MAX_A2A_IDEMPOTENCY_KEY_CHARS, A2A_PERSONAL_OWNER_SCOPE, } from "./task-store.js";
15
- const getA2ASecretByDomain = (...args) => import("../org/context.js").then(({ getA2ASecretByDomain }) => getA2ASecretByDomain(...args));
15
+ import { createTask, createOrReuseTask, getTask, getTaskOwnership, updateTask, claimA2ATaskForProcessing, getA2ATaskDispatchState, failStuckA2ATask, failStuckQueuedA2ATask, settleProcessingA2ATask, touchQueuedA2ATaskDispatch, touchProcessingA2ATask, pauseProcessingA2ATask, MAX_A2A_IDEMPOTENCY_KEY_CHARS, A2A_PERSONAL_OWNER_SCOPE, A2A_ORG_ID_OWNER_SCOPE_PREFIX, } from "./task-store.js";
16
16
  const A2A_PROCESS_TASK_PATH = "/_agent-native/a2a/_process-task";
17
17
  const PORTABLE_FALLBACK_HANDOFF_TIMEOUT_MS = 1_000;
18
18
  const A2A_QUEUED_DISPATCH_STUCK_AFTER_MS = 10_000;
@@ -94,12 +94,15 @@ async function trustedSourceContext(value, event) {
94
94
  if (!dispatch)
95
95
  return undefined;
96
96
  const orgDomain = event?.context?.__a2aOrgDomain;
97
+ const verifiedOrgId = event?.context?.__a2aVerifiedOrgId;
97
98
  let orgSecret;
98
99
  if (orgDomain) {
99
- try {
100
- orgSecret = (await getA2ASecretByDomain(orgDomain)) ?? undefined;
100
+ const { resolveA2AOrganizationCredentialsByDomain } = await import("../org/context.js");
101
+ const organization = await resolveA2AOrganizationCredentialsByDomain(orgDomain);
102
+ if (!verifiedOrgId || organization?.orgId !== verifiedOrgId) {
103
+ return undefined;
101
104
  }
102
- catch { }
105
+ orgSecret = organization.secret;
103
106
  }
104
107
  try {
105
108
  const result = await callAction(dispatch.url, "resolve-integration-source-context", { integrationTaskId: reference.integrationTaskId }, {
@@ -252,6 +255,9 @@ export async function processA2ATaskFromQueue(taskId, config, event) {
252
255
  const processorMeta = (meta.__a2a_processor ?? {});
253
256
  const verifiedEmail = processorMeta.verifiedEmail;
254
257
  const orgDomainHint = processorMeta.orgDomainHint;
258
+ const verifiedOrgId = typeof processorMeta.verifiedOrgId === "string"
259
+ ? processorMeta.verifiedOrgId.trim()
260
+ : undefined;
255
261
  const requestOrigin = requestOriginFromMetadata(processorMeta) ?? requestOriginFromEvent(event);
256
262
  const contextId = processorMeta.contextId ?? undefined;
257
263
  const callerMetadata = processorMeta.callerMetadata ?? undefined;
@@ -259,7 +265,17 @@ export async function processA2ATaskFromQueue(taskId, config, event) {
259
265
  ? processorMeta.approvedActions
260
266
  : undefined;
261
267
  const sourceContext = processorMeta.sourceContext;
262
- const resolvedOrgId = await resolveVerifiedA2AOrgId(verifiedEmail, orgDomainHint);
268
+ const resolvedOrgId = verifiedOrgId || undefined;
269
+ if (event?.context) {
270
+ if (verifiedEmail)
271
+ event.context.__a2aVerifiedEmail = verifiedEmail;
272
+ if (orgDomainHint)
273
+ event.context.__a2aOrgDomain = orgDomainHint;
274
+ if (verifiedOrgId)
275
+ event.context.__a2aVerifiedOrgId = verifiedOrgId;
276
+ if (verifiedEmail && !resolvedOrgId)
277
+ markExplicitPersonalOrgScope(event);
278
+ }
263
279
  const { runWithRequestContext } = await import("../server/request-context.js");
264
280
  const heartbeat = setInterval(() => {
265
281
  touchProcessingA2ATask(taskId).catch((err) => console.error("[a2a] Failed to heartbeat async task:", err));
@@ -268,7 +284,11 @@ export async function processA2ATaskFromQueue(taskId, config, event) {
268
284
  try {
269
285
  await runWithRequestContext({
270
286
  userEmail: verifiedEmail,
271
- orgId: resolvedOrgId,
287
+ ...(resolvedOrgId
288
+ ? { orgId: resolvedOrgId }
289
+ : verifiedEmail
290
+ ? { orgScope: "personal" }
291
+ : {}),
272
292
  ...(requestOrigin ? { requestOrigin } : {}),
273
293
  }, () => runHandlerAndPersist(taskId, message, config, contextId, callerMetadata, event, approvedActions, sourceContext));
274
294
  }
@@ -378,38 +398,21 @@ function makeHandlerContext(taskId, contextId, metadata, event, approvedActions,
378
398
  async function withA2ARequestContext(metadata, event, fn) {
379
399
  const { runWithRequestContext } = await import("../server/request-context.js");
380
400
  const verifiedEmail = event?.context?.__a2aVerifiedEmail ?? undefined;
381
- const orgDomain = event?.context?.__a2aOrgDomain ?? undefined;
382
- const resolvedOrgId = await resolveVerifiedA2AOrgId(verifiedEmail, orgDomain);
401
+ const verifiedOrgId = event?.context?.__a2aVerifiedOrgId ?? undefined;
383
402
  const requestOrigin = requestOriginForContext(metadata, event);
403
+ if (event?.context && verifiedEmail && !verifiedOrgId) {
404
+ markExplicitPersonalOrgScope(event);
405
+ }
384
406
  return runWithRequestContext({
385
407
  userEmail: verifiedEmail,
386
- orgId: resolvedOrgId,
408
+ ...(verifiedOrgId
409
+ ? { orgId: verifiedOrgId }
410
+ : verifiedEmail
411
+ ? { orgScope: "personal" }
412
+ : {}),
387
413
  ...(requestOrigin ? { requestOrigin } : {}),
388
414
  }, fn);
389
415
  }
390
- async function resolveVerifiedA2AOrgId(verifiedEmail, verifiedOrgDomain) {
391
- if (verifiedOrgDomain) {
392
- try {
393
- const { resolveOrgByDomain } = await import("../org/context.js");
394
- const org = await resolveOrgByDomain(verifiedOrgDomain);
395
- if (org)
396
- return org.orgId;
397
- }
398
- catch {
399
- // Org tables may not exist — continue without org context
400
- }
401
- }
402
- if (verifiedEmail) {
403
- try {
404
- const { resolveOrgIdForEmail } = await import("../org/context.js");
405
- return (await resolveOrgIdForEmail(verifiedEmail)) ?? undefined;
406
- }
407
- catch {
408
- // Org tables may not exist — continue without org context
409
- }
410
- }
411
- return undefined;
412
- }
413
416
  async function runHandlerAndPersist(taskId, message, config, contextId, metadata, event, approvedActions, sourceContext) {
414
417
  const { context, artifacts } = makeHandlerContext(taskId, contextId, metadata, event, approvedActions, sourceContext);
415
418
  try {
@@ -464,15 +467,24 @@ function taskFailureMessage(error, fallback) {
464
467
  }
465
468
  function verifiedTaskOwner(event) {
466
469
  const ownerEmail = event?.context?.__a2aVerifiedEmail ?? null;
470
+ const verifiedOrgId = event?.context?.__a2aVerifiedOrgId
471
+ ?.trim()
472
+ .toLowerCase() ?? "";
467
473
  return {
468
474
  ownerEmail,
469
475
  ownerScope: ownerEmail
470
- ? (event?.context?.__a2aOrgDomain
471
- ?.trim()
472
- .toLowerCase() ?? A2A_PERSONAL_OWNER_SCOPE)
476
+ ? verifiedOrgId
477
+ ? `${A2A_ORG_ID_OWNER_SCOPE_PREFIX}${verifiedOrgId}`
478
+ : A2A_PERSONAL_OWNER_SCOPE
473
479
  : null,
474
480
  };
475
481
  }
482
+ function hasUnboundVerifiedOrgIdentity(event) {
483
+ const verifiedEmail = event?.context?.__a2aVerifiedEmail?.trim() ?? "";
484
+ const verifiedOrgDomain = event?.context?.__a2aOrgDomain?.trim() ?? "";
485
+ const verifiedOrgId = event?.context?.__a2aVerifiedOrgId?.trim() ?? "";
486
+ return Boolean(verifiedEmail && verifiedOrgDomain && !verifiedOrgId);
487
+ }
476
488
  async function handleSend(params, config, event) {
477
489
  const message = params.message;
478
490
  if (!message || !message.role || !Array.isArray(message.parts)) {
@@ -481,6 +493,12 @@ async function handleSend(params, config, event) {
481
493
  _id: 0,
482
494
  };
483
495
  }
496
+ if (hasUnboundVerifiedOrgIdentity(event)) {
497
+ return {
498
+ ...jsonRpcError(0, -32001, "A stable verified organization identity is required"),
499
+ _id: 0,
500
+ };
501
+ }
484
502
  const contextId = params.contextId;
485
503
  const metadata = params.metadata;
486
504
  const approvedActions = trustedApprovedActions(params.approvedActions, event);
@@ -538,6 +556,9 @@ async function handleSend(params, config, event) {
538
556
  __a2a_processor: {
539
557
  verifiedEmail,
540
558
  orgDomainHint,
559
+ ...(typeof event?.context?.__a2aVerifiedOrgId === "string"
560
+ ? { verifiedOrgId: event.context.__a2aVerifiedOrgId }
561
+ : {}),
541
562
  ...(requestOrigin ? { requestOrigin } : {}),
542
563
  contextId: contextId ?? null,
543
564
  callerMetadata: safeMetadata ?? null,
@@ -638,6 +659,11 @@ async function handleStream(params, config, res, event) {
638
659
  res.end();
639
660
  return;
640
661
  }
662
+ if (hasUnboundVerifiedOrgIdentity(event)) {
663
+ res.write(`data: ${JSON.stringify(jsonRpcError(0, -32001, "A stable verified organization identity is required"))}\n\n`);
664
+ res.end();
665
+ return;
666
+ }
641
667
  const contextId = params.contextId;
642
668
  const metadata = params.metadata;
643
669
  const approvedActions = trustedApprovedActions(params.approvedActions, event);
@@ -754,17 +780,39 @@ function authorizeTaskAccess(taskOwnerEmail, taskOwnerScope, event, config) {
754
780
  if (verifiedEmail.toLowerCase() !== taskOwnerEmail.toLowerCase()) {
755
781
  return jsonRpcError(0, -32001, "Task not found");
756
782
  }
757
- if (taskOwnerScope) {
758
- const verifiedScope = event?.context?.__a2aOrgDomain
759
- ?.trim()
760
- .toLowerCase() ?? A2A_PERSONAL_OWNER_SCOPE;
761
- if (verifiedScope !== taskOwnerScope.toLowerCase()) {
783
+ const storedScope = taskOwnerScope?.trim().toLowerCase() ?? "";
784
+ if (!storedScope) {
785
+ // Legacy empty scopes cannot distinguish personal tasks from org tasks.
786
+ return jsonRpcError(0, -32001, "Task not found");
787
+ }
788
+ const verifiedOrgId = event?.context?.__a2aVerifiedOrgId
789
+ ?.trim()
790
+ .toLowerCase() ?? "";
791
+ const verifiedOrgDomain = event?.context?.__a2aOrgDomain?.trim() ?? "";
792
+ if (storedScope.startsWith(A2A_ORG_ID_OWNER_SCOPE_PREFIX)) {
793
+ if (!verifiedOrgId ||
794
+ storedScope !== `${A2A_ORG_ID_OWNER_SCOPE_PREFIX}${verifiedOrgId}`) {
762
795
  return jsonRpcError(0, -32001, "Task not found");
763
796
  }
764
797
  }
798
+ else if (storedScope === A2A_PERSONAL_OWNER_SCOPE &&
799
+ !verifiedOrgId &&
800
+ !verifiedOrgDomain) {
801
+ // A verified identity with no organization is in its personal scope.
802
+ }
803
+ else {
804
+ // Legacy domain scopes cannot be safely rebound after a domain change.
805
+ return jsonRpcError(0, -32001, "Task not found");
806
+ }
765
807
  }
766
808
  return null;
767
809
  }
810
+ function taskAccessScope(ownership, event) {
811
+ const verifiedEmail = event?.context?.__a2aVerifiedEmail?.trim() ?? "";
812
+ if (!ownership.ownerEmail || !verifiedEmail)
813
+ return undefined;
814
+ return { ownerEmail: verifiedEmail, ownerScope: ownership.ownerScope };
815
+ }
768
816
  async function handleGet(params, event, config) {
769
817
  const id = params.id;
770
818
  if (!id) {
@@ -774,7 +822,8 @@ async function handleGet(params, event, config) {
774
822
  const denied = authorizeTaskAccess(ownership.ownerEmail, ownership.ownerScope, event, config);
775
823
  if (denied)
776
824
  return denied;
777
- const task = await getTask(id);
825
+ const accessScope = taskAccessScope(ownership, event);
826
+ const task = await getTask(id, accessScope);
778
827
  if (!task) {
779
828
  return jsonRpcError(0, -32001, "Task not found");
780
829
  }
@@ -783,7 +832,7 @@ async function handleGet(params, event, config) {
783
832
  return false;
784
833
  });
785
834
  if (taskChanged) {
786
- const updated = await getTask(id);
835
+ const updated = await getTask(id, accessScope);
787
836
  if (updated)
788
837
  return jsonRpcResult(0, sanitizeTaskForResponse(updated));
789
838
  }
@@ -837,7 +886,7 @@ async function handleCancel(params, event, config) {
837
886
  const denied = authorizeTaskAccess(ownership.ownerEmail, ownership.ownerScope, event, config);
838
887
  if (denied)
839
888
  return denied;
840
- const task = await updateTask(id, { state: "canceled" });
889
+ const task = await updateTask(id, { state: "canceled" }, taskAccessScope(ownership, event));
841
890
  if (!task) {
842
891
  return jsonRpcError(0, -32001, "Task not found");
843
892
  }
@@ -869,6 +918,9 @@ async function handleInvokeReadOnlyAction(params, event, config) {
869
918
  event?.context?.__a2aAudienceVerified !== true) {
870
919
  return jsonRpcError(0, -32001, "A verified, audience-bound user identity is required for direct action invocation");
871
920
  }
921
+ if (hasUnboundVerifiedOrgIdentity(event)) {
922
+ return jsonRpcError(0, -32001, "A stable verified organization identity is required for direct action invocation");
923
+ }
872
924
  if (!config.executeReadOnlyAction) {
873
925
  return jsonRpcError(0, -32601, "Direct action invocation not supported");
874
926
  }
@@ -0,0 +1,9 @@
1
+ export interface VerifiedA2AOrganizationIdentity {
2
+ orgId: string;
3
+ orgDomain: string | null;
4
+ }
5
+ /**
6
+ * Bind shared-secret organization claims to this receiver's organization
7
+ * metadata before they can become request or action scope.
8
+ */
9
+ export declare function verifyA2AOrganizationIdentity(payload: Record<string, unknown>): Promise<VerifiedA2AOrganizationIdentity | null | undefined>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bind shared-secret organization claims to this receiver's organization
3
+ * metadata before they can become request or action scope.
4
+ */
5
+ export async function verifyA2AOrganizationIdentity(payload) {
6
+ const hasOrgId = Object.prototype.hasOwnProperty.call(payload, "org_id");
7
+ const hasOrgDomain = Object.prototype.hasOwnProperty.call(payload, "org_domain");
8
+ const rawOrgId = payload.org_id;
9
+ const rawOrgDomain = payload.org_domain;
10
+ if (!hasOrgId && !hasOrgDomain)
11
+ return undefined;
12
+ if ((hasOrgId &&
13
+ rawOrgId !== null &&
14
+ (typeof rawOrgId !== "string" || !rawOrgId.trim())) ||
15
+ (hasOrgDomain && (typeof rawOrgDomain !== "string" || !rawOrgDomain.trim()))) {
16
+ return null;
17
+ }
18
+ const orgId = typeof rawOrgId === "string" ? rawOrgId.trim() : "";
19
+ const orgDomain = typeof rawOrgDomain === "string" ? rawOrgDomain.trim().toLowerCase() : "";
20
+ if (!orgId && !orgDomain)
21
+ return undefined;
22
+ const context = await import("../org/context.js");
23
+ const organization = orgDomain
24
+ ? await context.resolveA2AOrganizationMetadataByDomain(orgDomain)
25
+ : await context.resolveA2AOrganizationMetadataById(orgId);
26
+ if (!organization || (orgId && organization.orgId !== orgId))
27
+ return null;
28
+ const email = typeof payload.sub === "string" ? payload.sub.trim() : "";
29
+ if (!email)
30
+ return null;
31
+ // A shared deployment secret is the cross-app trust grant. The receiver's
32
+ // membership list is not authoritative for a sibling app's user roster.
33
+ return organization;
34
+ }
@@ -4,9 +4,8 @@ import type { A2AConfig, AgentSkill } from "./types.js";
4
4
  /**
5
5
  * Result of verifying an inbound A2A JWT. `email` is the caller identity from
6
6
  * the token's `sub` claim (null when verification fails), `orgDomain` mirrors
7
- * the verified `org_domain` claim when present, and `orgId` mirrors the
8
- * optional verified `org_id` claim used when a sender knows its exact org but
9
- * cannot resolve that org's domain.
7
+ * locally resolved organization metadata when present, and `orgId` is the
8
+ * matching local organization bound to the token's organization claims.
10
9
  */
11
10
  export interface A2ATokenPayload {
12
11
  email: string | null;
@@ -14,15 +13,22 @@ export interface A2ATokenPayload {
14
13
  orgId?: string;
15
14
  claims?: jose.JWTPayload;
16
15
  }
16
+ export declare class A2AIdentityVerificationUnavailableError extends Error {
17
+ readonly cause: unknown;
18
+ constructor(cause: unknown);
19
+ }
20
+ export declare function isA2AIdentityVerificationUnavailableError(error: unknown): error is A2AIdentityVerificationUnavailableError;
17
21
  /**
18
22
  * Verify an inbound A2A bearer token (HS256) exactly as the
19
23
  * `/_agent-native/a2a` endpoint does: it peeks at the unverified `org_domain`
20
- * claim to build an ordered candidate-secret set (`process.env.A2A_SECRET`
21
- * plus any org-level secret for that domain), then verifies the JWT — checking
24
+ * claim to locate an org credential after trying the global credential, then
25
+ * verifies the JWT — checking
22
26
  * `aud`/`iss` when the token carries them and `exp` always. Returns the
23
27
  * caller's email (`sub`) and org domain on success, or `{ email: null,
24
- * orgDomain: null }` on any failure (malformed, bad signature, expired, no
25
- * secret configured, or an MCP credential), never throwing.
28
+ * orgDomain: null }` for an invalid token or absent credential. If the
29
+ * organization credential or membership evidence cannot be read, throws
30
+ * `A2AIdentityVerificationUnavailableError` so callers cannot treat an
31
+ * unreadable authorization lookup as an unauthenticated fallback.
26
32
  *
27
33
  * Exported so workspaces can accept A2A callers on the HTTP action route with
28
34
  * the same routine — including org-level fallback secrets — instead of