@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
@@ -12,6 +12,7 @@ import { generateAgentCard } from "./agent-card.js";
12
12
  import { canonicalA2AAudience } from "./audience.js";
13
13
  import { hasConfiguredA2ASecret, isA2AProductionRuntime, } from "./auth-policy.js";
14
14
  import { handleJsonRpcH3, processA2ATaskFromQueue } from "./handlers.js";
15
+ import { verifyA2AOrganizationIdentity } from "./organization-identity.js";
15
16
  import { claimA2AApproval, getA2AApprovalForOwner, settleA2AApproval, } from "./task-store.js";
16
17
  let _warnedUnauthA2A = false;
17
18
  function warnA2AUnauthOnce() {
@@ -22,11 +23,48 @@ function warnA2AUnauthOnce() {
22
23
  console.warn("[a2a] No A2A_SECRET or apiKeyEnv configured — A2A endpoint runs unauthenticated. " +
23
24
  "This is allowed in development but blocked in production. Set A2A_SECRET before deploying.");
24
25
  }
25
- function addSecretCandidate(candidates, secret) {
26
- const trimmed = secret?.trim();
27
- if (!trimmed || candidates.includes(trimmed))
28
- return;
29
- candidates.push(trimmed);
26
+ export class A2AIdentityVerificationUnavailableError extends Error {
27
+ cause;
28
+ constructor(cause) {
29
+ super("A2A identity verification is temporarily unavailable");
30
+ this.name = "A2AIdentityVerificationUnavailableError";
31
+ this.cause = cause;
32
+ }
33
+ }
34
+ export function isA2AIdentityVerificationUnavailableError(error) {
35
+ return (error instanceof A2AIdentityVerificationUnavailableError ||
36
+ (error instanceof Error &&
37
+ error.name === "A2AIdentityVerificationUnavailableError"));
38
+ }
39
+ function identityPayload(payload, audienceOptions, verifiedOrganization) {
40
+ const claims = verifiedOrganization
41
+ ? {
42
+ ...payload,
43
+ org_id: verifiedOrganization.orgId,
44
+ ...(verifiedOrganization.orgDomain
45
+ ? { org_domain: verifiedOrganization.orgDomain }
46
+ : { org_domain: undefined }),
47
+ }
48
+ : payload;
49
+ return {
50
+ email: payload.sub ?? null,
51
+ orgDomain: verifiedOrganization?.orgDomain ?? payload.org_domain ?? null,
52
+ ...(verifiedOrganization ? { orgId: verifiedOrganization.orgId } : {}),
53
+ ...(audienceOptions?.includeClaims ? { claims } : {}),
54
+ };
55
+ }
56
+ async function verifyJwtWithSecret(token, secret, options) {
57
+ try {
58
+ const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(secret), options);
59
+ return isMcpCredential(payload) ? null : payload;
60
+ }
61
+ catch {
62
+ // coercion-ok: invalid signatures, expiry, or audience are rejected tokens; credential lookup failures throw separately.
63
+ return null;
64
+ }
65
+ }
66
+ function isMcpCredential(payload) {
67
+ return (payload.scope === "mcp-connect" || payload.typ === "agent-native-mcp-oauth");
30
68
  }
31
69
  /**
32
70
  * Resolve the audience (`aud`) value to expect in an inbound JWT. We use the
@@ -67,15 +105,6 @@ function audienceForRoute(baseAudience, options) {
67
105
  ? [routeAudience, baseAudience]
68
106
  : routeAudience;
69
107
  }
70
- /**
71
- * MCP connect tokens and MCP OAuth access tokens can be signed with the A2A
72
- * secret, but they are long-lived bearers whose revocation and membership
73
- * checks live in the MCP endpoint's `verifyAuth`. A2A callers sign a
74
- * short-lived token per call, so an MCP credential is never an A2A token.
75
- */
76
- function isMcpCredential(payload) {
77
- return (payload.scope === "mcp-connect" || payload.typ === "agent-native-mcp-oauth");
78
- }
79
108
  function tokenHasAudienceClaim(token) {
80
109
  return typeof jose.decodeJwt(token).aud !== "undefined";
81
110
  }
@@ -86,12 +115,14 @@ function isDirectReadSkill(skill) {
86
115
  /**
87
116
  * Verify an inbound A2A bearer token (HS256) exactly as the
88
117
  * `/_agent-native/a2a` endpoint does: it peeks at the unverified `org_domain`
89
- * claim to build an ordered candidate-secret set (`process.env.A2A_SECRET`
90
- * plus any org-level secret for that domain), then verifies the JWT — checking
118
+ * claim to locate an org credential after trying the global credential, then
119
+ * verifies the JWT — checking
91
120
  * `aud`/`iss` when the token carries them and `exp` always. Returns the
92
121
  * caller's email (`sub`) and org domain on success, or `{ email: null,
93
- * orgDomain: null }` on any failure (malformed, bad signature, expired, no
94
- * secret configured, or an MCP credential), never throwing.
122
+ * orgDomain: null }` for an invalid token or absent credential. If the
123
+ * organization credential or membership evidence cannot be read, throws
124
+ * `A2AIdentityVerificationUnavailableError` so callers cannot treat an
125
+ * unreadable authorization lookup as an unauthenticated fallback.
95
126
  *
96
127
  * Exported so workspaces can accept A2A callers on the HTTP action route with
97
128
  * the same routine — including org-level fallback secrets — instead of
@@ -101,6 +132,9 @@ function isDirectReadSkill(skill) {
101
132
  * the same audience the client derived from the advertised URL.
102
133
  */
103
134
  export async function verifyA2AToken(token, event, audienceOptions) {
135
+ return (await verifyA2ATokenInternal(token, event, audienceOptions)).payload;
136
+ }
137
+ async function verifyA2ATokenInternal(token, event, audienceOptions) {
104
138
  // Step 1: Peek at JWT claims WITHOUT verification to get org_domain.
105
139
  // This is safe because we only use org_domain to look up the secret,
106
140
  // then verify the full JWT with that secret. If someone forges a JWT
@@ -110,35 +144,24 @@ export async function verifyA2AToken(token, event, audienceOptions) {
110
144
  let unverifiedPayload;
111
145
  try {
112
146
  unverifiedPayload = jose.decodeJwt(token);
113
- orgDomainHint = unverifiedPayload.org_domain;
147
+ const domain = unverifiedPayload.org_domain;
148
+ if (typeof domain === "string" && domain.trim()) {
149
+ orgDomainHint = domain.trim();
150
+ }
114
151
  }
115
152
  catch {
116
153
  // Malformed token — fall through to global secret attempt
117
154
  }
118
- // Step 2: Build a small, ordered set of candidate secrets. An explicit
119
- // verification credential is isolated from the deployment-wide and
120
- // org-level credentials so a caller cannot use one app's trust grant as
155
+ // An explicit verification credential is isolated from the deployment-wide
156
+ // and org-level credentials so a caller cannot use one app's trust grant as
121
157
  // another app's identity.
122
- const candidateSecrets = [];
123
158
  const hasExplicitVerificationSecret = audienceOptions?.verificationSecret !== undefined;
124
- addSecretCandidate(candidateSecrets, hasExplicitVerificationSecret
159
+ const globalSecret = (hasExplicitVerificationSecret
125
160
  ? audienceOptions?.verificationSecret
126
- : readDeployCredentialEnv("A2A_SECRET"));
127
- if (orgDomainHint &&
128
- !audienceOptions?.globalSecretOnly &&
129
- !hasExplicitVerificationSecret) {
130
- try {
131
- const { getA2ASecretByDomain } = await import("../org/context.js");
132
- const orgSecret = await getA2ASecretByDomain(orgDomainHint);
133
- addSecretCandidate(candidateSecrets, orgSecret ?? undefined);
134
- }
135
- catch {
136
- // DB not ready or column doesn't exist yet — fall through
137
- }
138
- }
139
- if (candidateSecrets.length === 0)
140
- return { email: null, orgDomain: null };
141
- // Step 3: Verify JWT with the candidate secrets.
161
+ : readDeployCredentialEnv("A2A_SECRET"))?.trim();
162
+ // Step 2: Build verification options once, then try the explicit/global
163
+ // credential first. A token accepted by that credential remains a global
164
+ // or explicitly delegated caller and does not depend on org-table reads.
142
165
  //
143
166
  // - `audience`: passed only when the token carries an `aud` claim
144
167
  // (backward-compat: tokens minted by older `signA2AToken` versions
@@ -153,49 +176,93 @@ export async function verifyA2AToken(token, event, audienceOptions) {
153
176
  // it was minted from", which `jose.jwtVerify` validates exactly when
154
177
  // `issuer` is supplied as a string. Backward-compat: when the token
155
178
  // has no `iss`, we skip the check.
156
- try {
157
- const verifyOptions = {};
158
- if (unverifiedPayload && typeof unverifiedPayload.aud !== "undefined") {
159
- // Fail closed: the token was minted for a specific audience, but this
160
- // receiver can't derive its own expected audience (no APP_URL/URL and no
161
- // usable request host). Accepting here would let a correctly-signed token
162
- // whose `aud` targets ANOTHER service verify against a shared secret. A
163
- // token that self-declares an audience must be checked against ours, so
164
- // when we have nothing to check it against we reject rather than skip.
165
- const aud = expectedJwtAudience(event, audienceOptions);
166
- if (!aud)
167
- return { email: null, orgDomain: null };
168
- verifyOptions.audience = aud;
169
- }
170
- if (unverifiedPayload &&
171
- typeof unverifiedPayload.iss === "string" &&
172
- unverifiedPayload.iss.length > 0) {
173
- verifyOptions.issuer = unverifiedPayload.iss;
174
- }
175
- for (const secret of candidateSecrets) {
179
+ const verifyOptions = {};
180
+ if (unverifiedPayload && typeof unverifiedPayload.aud !== "undefined") {
181
+ // Fail closed: the token was minted for a specific audience, but this
182
+ // receiver can't derive its own expected audience (no APP_URL/URL and no
183
+ // usable request host). Accepting here would let a correctly-signed token
184
+ // whose `aud` targets ANOTHER service verify against a shared secret. A
185
+ // token that self-declares an audience must be checked against ours, so
186
+ // when we have nothing to check it against we reject rather than skip.
187
+ const aud = expectedJwtAudience(event, audienceOptions);
188
+ if (!aud)
189
+ return { payload: { email: null, orgDomain: null } };
190
+ verifyOptions.audience = aud;
191
+ }
192
+ if (unverifiedPayload &&
193
+ typeof unverifiedPayload.iss === "string" &&
194
+ unverifiedPayload.iss.length > 0) {
195
+ verifyOptions.issuer = unverifiedPayload.iss;
196
+ }
197
+ if (globalSecret) {
198
+ const payload = await verifyJwtWithSecret(token, globalSecret, verifyOptions);
199
+ if (payload) {
200
+ let verifiedOrganization;
176
201
  try {
177
- const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(secret), verifyOptions);
178
- if (isMcpCredential(payload))
179
- return { email: null, orgDomain: null };
180
- const orgId = typeof payload.org_id === "string" && payload.org_id.trim()
181
- ? payload.org_id.trim()
182
- : undefined;
183
- return {
184
- email: payload.sub ?? null,
185
- orgDomain: payload.org_domain ?? null,
186
- ...(orgId ? { orgId } : {}),
187
- ...(audienceOptions?.includeClaims ? { claims: payload } : {}),
188
- };
202
+ verifiedOrganization = await verifyA2AOrganizationIdentity(payload);
203
+ }
204
+ catch (cause) {
205
+ throw new A2AIdentityVerificationUnavailableError(cause);
189
206
  }
190
- catch {
191
- // Try the next candidate without leaking which secret failed.
207
+ if (verifiedOrganization === null) {
208
+ return { payload: { email: null, orgDomain: null } };
192
209
  }
210
+ return {
211
+ payload: identityPayload(payload, audienceOptions, verifiedOrganization),
212
+ ...(verifiedOrganization
213
+ ? { verifiedOrgId: verifiedOrganization.orgId }
214
+ : {}),
215
+ };
193
216
  }
194
217
  }
195
- catch {
196
- // Keep malformed option construction indistinguishable from auth failure.
218
+ if (!orgDomainHint ||
219
+ audienceOptions?.globalSecretOnly ||
220
+ hasExplicitVerificationSecret) {
221
+ return { payload: { email: null, orgDomain: null } };
222
+ }
223
+ let organization;
224
+ try {
225
+ const { resolveA2AOrganizationCredentialsByDomain } = await import("../org/context.js");
226
+ organization =
227
+ await resolveA2AOrganizationCredentialsByDomain(orgDomainHint);
197
228
  }
198
- return { email: null, orgDomain: null };
229
+ catch (cause) {
230
+ throw new A2AIdentityVerificationUnavailableError(cause);
231
+ }
232
+ if (!organization)
233
+ return { payload: { email: null, orgDomain: null } };
234
+ const payload = await verifyJwtWithSecret(token, organization.secret, verifyOptions);
235
+ if (!payload)
236
+ return { payload: { email: null, orgDomain: null } };
237
+ const verifiedDomain = typeof payload.org_domain === "string"
238
+ ? payload.org_domain.trim().toLowerCase()
239
+ : "";
240
+ const email = typeof payload.sub === "string" ? payload.sub.trim() : "";
241
+ const claimedOrgId = payload.org_id;
242
+ if (verifiedDomain !== organization.orgDomain ||
243
+ !email ||
244
+ (typeof claimedOrgId !== "undefined" &&
245
+ (typeof claimedOrgId !== "string" ||
246
+ claimedOrgId.trim() !== organization.orgId))) {
247
+ return { payload: { email: null, orgDomain: null } };
248
+ }
249
+ let member;
250
+ try {
251
+ const { isOrgMemberForA2A } = await import("../org/membership.js");
252
+ member = await isOrgMemberForA2A(organization.orgId, email);
253
+ }
254
+ catch (cause) {
255
+ throw new A2AIdentityVerificationUnavailableError(cause);
256
+ }
257
+ if (!member)
258
+ return { payload: { email: null, orgDomain: null } };
259
+ return {
260
+ payload: identityPayload(payload, audienceOptions, {
261
+ orgId: organization.orgId,
262
+ orgDomain: organization.orgDomain,
263
+ }),
264
+ verifiedOrgId: organization.orgId,
265
+ };
199
266
  }
200
267
  export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
201
268
  // Public agent card endpoint (no auth required).
@@ -220,14 +287,25 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
220
287
  if (config.authenticatedSkills?.length) {
221
288
  const bearer = extractBearerToken(getRequestHeader(event, "authorization"));
222
289
  if (bearer) {
223
- const payload = await verifyA2AToken(bearer, event, {
224
- routePrefix,
225
- allowBaseAudience: true,
226
- });
227
- if (payload.email) {
228
- skills = tokenHasAudienceClaim(bearer)
229
- ? config.authenticatedSkills
230
- : config.authenticatedSkills.filter((skill) => !isDirectReadSkill(skill));
290
+ try {
291
+ const { payload } = await verifyA2ATokenInternal(bearer, event, {
292
+ routePrefix,
293
+ allowBaseAudience: true,
294
+ });
295
+ if (payload.email) {
296
+ skills = tokenHasAudienceClaim(bearer)
297
+ ? config.authenticatedSkills
298
+ : config.authenticatedSkills.filter((skill) => !isDirectReadSkill(skill));
299
+ }
300
+ }
301
+ catch (error) {
302
+ if (!(error instanceof A2AIdentityVerificationUnavailableError)) {
303
+ throw error;
304
+ }
305
+ setResponseStatus(event, 503);
306
+ return {
307
+ error: "A2A identity verification is temporarily unavailable",
308
+ };
231
309
  }
232
310
  }
233
311
  }
@@ -383,6 +461,7 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
383
461
  const bearerToken = extractBearerToken(authHeader);
384
462
  let verifiedCallerEmail = null;
385
463
  let verifiedOrgDomain = null;
464
+ let verifiedOrgId;
386
465
  let verifiedAudienceBound = false;
387
466
  let legacyApiKeyAuthenticated = false;
388
467
  let bearerTokenRejectedByJwt = false;
@@ -394,9 +473,26 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
394
473
  const hasA2ASecret = hasConfiguredA2ASecret();
395
474
  const hasApiKey = !!(config.apiKeyEnv && process.env[config.apiKeyEnv]);
396
475
  if (bearerToken) {
397
- const tokenPayload = await verifyA2AToken(bearerToken, event, {
398
- routePrefix,
399
- });
476
+ let tokenPayload;
477
+ try {
478
+ const verification = await verifyA2ATokenInternal(bearerToken, event, { routePrefix });
479
+ tokenPayload = verification.payload;
480
+ verifiedOrgId = verification.verifiedOrgId;
481
+ }
482
+ catch (error) {
483
+ if (!(error instanceof A2AIdentityVerificationUnavailableError)) {
484
+ throw error;
485
+ }
486
+ setResponseStatus(event, 503);
487
+ return {
488
+ jsonrpc: "2.0",
489
+ id: null,
490
+ error: {
491
+ code: -32003,
492
+ message: "A2A identity verification is temporarily unavailable",
493
+ },
494
+ };
495
+ }
400
496
  verifiedCallerEmail = tokenPayload.email;
401
497
  verifiedOrgDomain = tokenPayload.orgDomain;
402
498
  if (verifiedCallerEmail) {
@@ -477,6 +573,9 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
477
573
  if (verifiedOrgDomain) {
478
574
  event.context.__a2aOrgDomain = verifiedOrgDomain;
479
575
  }
576
+ if (verifiedOrgId) {
577
+ event.context.__a2aVerifiedOrgId = verifiedOrgId;
578
+ }
480
579
  const body = await readBody(event);
481
580
  return handleJsonRpcH3(body, event, config);
482
581
  }));
@@ -1,6 +1,7 @@
1
1
  import type { Task, Message, TaskState, Artifact } from "./types.js";
2
2
  export declare const MAX_A2A_IDEMPOTENCY_KEY_CHARS = 128;
3
3
  export declare const A2A_PERSONAL_OWNER_SCOPE = "__personal__";
4
+ export declare const A2A_ORG_ID_OWNER_SCOPE_PREFIX = "__a2a_org_id__:";
4
5
  export interface A2ATaskListCursor {
5
6
  createdAt: number;
6
7
  id: string;
@@ -42,6 +43,10 @@ export interface A2ATaskOwnership {
42
43
  ownerEmail: string | null;
43
44
  ownerScope: string | null;
44
45
  }
46
+ export interface A2ATaskAccessScope {
47
+ ownerEmail: string;
48
+ ownerScope: string | null;
49
+ }
45
50
  export declare function getTaskOwner(id: string): Promise<string | null>;
46
51
  export declare function getTaskOwnership(id: string): Promise<A2ATaskOwnership>;
47
52
  export declare function claimA2ATaskForProcessing(id: string): Promise<Task | null>;
@@ -57,12 +62,12 @@ export declare function touchProcessingA2ATask(id: string): Promise<boolean>;
57
62
  export declare function resetStuckA2ATaskForRetry(id: string, processingCutoff: number): Promise<boolean>;
58
63
  export declare function failStuckA2ATask(id: string, processingCutoff: number, reason: string, createdAtCutoff?: number): Promise<boolean>;
59
64
  export declare function failStuckQueuedA2ATask(id: string, createdAtCutoff: number, reason: string): Promise<boolean>;
60
- export declare function getTask(id: string): Promise<Task | null>;
65
+ export declare function getTask(id: string, accessScope?: A2ATaskAccessScope): Promise<Task | null>;
61
66
  export declare function updateTask(id: string, update: {
62
67
  state?: TaskState;
63
68
  message?: Message;
64
69
  artifacts?: Artifact[];
65
- }): Promise<Task | null>;
70
+ }, accessScope?: A2ATaskAccessScope): Promise<Task | null>;
66
71
  /**
67
72
  * Persist the terminal result produced by the async processor, but only while
68
73
  * that processor still owns a task in `processing`. A tasks/get request may
@@ -6,6 +6,7 @@ export const MAX_A2A_IDEMPOTENCY_KEY_CHARS = 128;
6
6
  const A2A_IDEMPOTENCY_INDEX = "idx_a2a_tasks_owner_scope_idempotency";
7
7
  const A2A_RECOVERY_INDEX = "idx_a2a_tasks_recovery_created";
8
8
  export const A2A_PERSONAL_OWNER_SCOPE = "__personal__";
9
+ export const A2A_ORG_ID_OWNER_SCOPE_PREFIX = "__a2a_org_id__:";
9
10
  const MAX_TASK_LIST_PAGE_SIZE = 100;
10
11
  export async function ensureTable() {
11
12
  if (!_initPromise) {
@@ -371,6 +372,14 @@ export async function createOrReuseTask(message, contextId, metadata, ownerEmail
371
372
  }
372
373
  throw new Error("A2A terminal task idempotency key could not be released");
373
374
  }
375
+ function taskAccessPredicate(scope) {
376
+ if (!scope)
377
+ return { sql: "", args: [] };
378
+ return {
379
+ sql: " AND LOWER(COALESCE(owner_email, '')) = LOWER(?) AND LOWER(COALESCE(owner_scope, '')) = LOWER(?)",
380
+ args: [scope.ownerEmail, scope.ownerScope ?? ""],
381
+ };
382
+ }
374
383
  export async function getTaskOwner(id) {
375
384
  return (await getTaskOwnership(id)).ownerEmail;
376
385
  }
@@ -537,23 +546,25 @@ export async function failStuckQueuedA2ATask(id, createdAtCutoff, reason) {
537
546
  const affected = getAffectedRowCount(result);
538
547
  return affected !== 0;
539
548
  }
540
- export async function getTask(id) {
549
+ export async function getTask(id, accessScope) {
541
550
  await ensureTable();
542
551
  const client = getDbExec();
552
+ const predicate = taskAccessPredicate(accessScope);
543
553
  const { rows } = await client.execute({
544
- sql: `SELECT * FROM a2a_tasks WHERE id = ?`,
545
- args: [id],
554
+ sql: `SELECT * FROM a2a_tasks WHERE id = ?${predicate.sql}`,
555
+ args: [id, ...predicate.args],
546
556
  });
547
557
  if (rows.length === 0)
548
558
  return null;
549
559
  return taskFromRow(rows[0]);
550
560
  }
551
- export async function updateTask(id, update) {
561
+ export async function updateTask(id, update, accessScope) {
552
562
  await ensureTable();
553
563
  const client = getDbExec();
564
+ const predicate = taskAccessPredicate(accessScope);
554
565
  const { rows } = await client.execute({
555
- sql: `SELECT * FROM a2a_tasks WHERE id = ?`,
556
- args: [id],
566
+ sql: `SELECT * FROM a2a_tasks WHERE id = ?${predicate.sql}`,
567
+ args: [id, ...predicate.args],
557
568
  });
558
569
  if (rows.length === 0)
559
570
  return null;
@@ -572,8 +583,8 @@ export async function updateTask(id, update) {
572
583
  if (update.artifacts) {
573
584
  task.artifacts = [...(task.artifacts ?? []), ...update.artifacts];
574
585
  }
575
- await client.execute({
576
- sql: `UPDATE a2a_tasks SET status_state = ?, status_message = ?, status_timestamp = ?, history = ?, artifacts = ?, updated_at = ? WHERE id = ?`,
586
+ const result = await client.execute({
587
+ sql: `UPDATE a2a_tasks SET status_state = ?, status_message = ?, status_timestamp = ?, history = ?, artifacts = ?, updated_at = ? WHERE id = ?${predicate.sql}`,
577
588
  args: [
578
589
  task.status.state,
579
590
  task.status.message ? JSON.stringify(task.status.message) : null,
@@ -582,8 +593,12 @@ export async function updateTask(id, update) {
582
593
  JSON.stringify(task.artifacts),
583
594
  now,
584
595
  id,
596
+ ...predicate.args,
585
597
  ],
586
598
  });
599
+ if (accessScope && getAffectedRowCount(result) !== 1) {
600
+ return null;
601
+ }
587
602
  return task;
588
603
  }
589
604
  /**
@@ -13,14 +13,24 @@ export async function verifyA2ATokenWithClaims(token, event) {
13
13
  : [];
14
14
  if (audiences.length === 0 || audiences.some((value) => !value.trim()))
15
15
  return null;
16
- const orgId = typeof raw.org_id === "string" ? raw.org_id.trim() : "";
16
+ const claimedOrgId = typeof raw.org_id === "string" ? raw.org_id.trim() : "";
17
+ // Global-secret verification can resolve an ID-only claim locally; require
18
+ // the signed domain too so the ID alone cannot choose the authorization scope.
19
+ const claimedOrgDomain = typeof raw.org_domain === "string"
20
+ ? raw.org_domain.trim().toLowerCase()
21
+ : "";
22
+ const orgId = identity.orgId?.trim() ?? "";
17
23
  const jti = typeof raw.jti === "string" ? raw.jti.trim() : "";
18
24
  const scopes = typeof raw.scope === "string"
19
25
  ? raw.scope.split(/\s+/).filter(Boolean)
20
26
  : [];
21
27
  const issuer = typeof raw.iss === "string" ? raw.iss.trim() : "";
22
28
  const orgDomain = identity.orgDomain?.trim().toLowerCase() ?? "";
23
- return orgId && orgDomain && jti
29
+ return claimedOrgId &&
30
+ claimedOrgId === orgId &&
31
+ claimedOrgDomain &&
32
+ claimedOrgDomain === orgDomain &&
33
+ jti
24
34
  ? {
25
35
  email: identity.email,
26
36
  orgId,
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export declare const mcpConfig: z.ZodObject<{
3
+ allowDevOpen: z.ZodDefault<z.ZodBoolean>;
4
+ }, z.core.$strip>;
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ export const mcpConfig = z.object({
3
+ allowDevOpen: z.boolean().default(false).meta({
4
+ env: "AGENT_NATIVE_MCP_DEV_OPEN",
5
+ doc: "Allow unauthenticated MCP requests from loopback during local development.",
6
+ }),
7
+ });
@@ -79,6 +79,9 @@ export declare const appConfigSchema: z.ZodObject<{
79
79
  launchDarkly: z.ZodPrefault<z.ZodObject<{
80
80
  sdkKey: z.ZodOptional<z.ZodString>;
81
81
  }, z.core.$strip>>;
82
+ mcp: z.ZodPrefault<z.ZodObject<{
83
+ allowDevOpen: z.ZodDefault<z.ZodBoolean>;
84
+ }, z.core.$strip>>;
82
85
  migration: z.ZodPrefault<z.ZodObject<{
83
86
  releaseMigrations: z.ZodDefault<z.ZodBoolean>;
84
87
  deployContext: z.ZodOptional<z.ZodString>;
@@ -8,6 +8,7 @@ import { authConfig } from "./auth.js";
8
8
  import { dispatchConfig } from "./dispatch.js";
9
9
  import { integrationsConfig } from "./integrations.js";
10
10
  import { launchDarklyConfig } from "./launchdarkly.js";
11
+ import { mcpConfig } from "./mcp.js";
11
12
  import { migrationConfig } from "./migration.js";
12
13
  import { observabilityConfig } from "./observability.js";
13
14
  import { onboardingConfig } from "./onboarding.js";
@@ -27,6 +28,7 @@ export const appConfigSchema = z.object({
27
28
  dispatch: dispatchConfig.prefault({}),
28
29
  integrations: integrationsConfig.prefault({}),
29
30
  launchDarkly: launchDarklyConfig.prefault({}),
31
+ mcp: mcpConfig.prefault({}),
30
32
  migration: migrationConfig.prefault({}),
31
33
  observability: observabilityConfig.prefault({}),
32
34
  onboarding: onboardingConfig.prefault({}),
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
- error?: undefined;
143
142
  accepted: boolean;
144
143
  duplicate: boolean;
145
144
  eventId: string;
145
+ error?: undefined;
146
146
  }>;