@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.
- package/dist/a2a/handlers.js +96 -44
- package/dist/a2a/organization-identity.d.ts +9 -0
- package/dist/a2a/organization-identity.js +34 -0
- package/dist/a2a/server.d.ts +13 -7
- package/dist/a2a/server.js +187 -88
- package/dist/a2a/task-store.d.ts +7 -2
- package/dist/a2a/task-store.js +23 -8
- package/dist/a2a-claims.js +12 -2
- package/dist/app-config/mcp.d.ts +4 -0
- package/dist/app-config/mcp.js +7 -0
- package/dist/app-config/schema.d.ts +3 -0
- package/dist/app-config/schema.js +2 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/route-chunk-recovery.js +75 -11
- package/dist/client/route-warmup.d.ts +2 -0
- package/dist/client/route-warmup.js +3 -0
- package/dist/client/vite-dev-recovery-script.js +85 -17
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +3 -1
- package/dist/feature-flags/a2a-action-route.js +3 -4
- package/dist/mcp/build-server.d.ts +4 -0
- package/dist/mcp/build-server.js +152 -43
- package/dist/mcp/embed-app.js +5 -5
- package/dist/mcp/server.d.ts +0 -18
- package/dist/mcp/server.js +99 -9
- package/dist/onboarding/app-profile.js +2 -1
- package/dist/onboarding/workspace-services.js +3 -0
- package/dist/org/context.d.ts +13 -0
- package/dist/org/context.js +60 -0
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/org/membership.d.ts +6 -0
- package/dist/org/membership.js +23 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +6 -6
- package/dist/secrets/routes.d.ts +11 -11
- package/dist/server/action-routes.js +12 -5
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/builder-browser.d.ts +4 -2
- package/dist/server/builder-browser.js +147 -9
- package/dist/server/core-routes-plugin.js +13 -6
- package/dist/server/entry-server.js +43 -2
- package/dist/server/sentry.js +9 -1
- package/dist/server/ssr-handler.d.ts +1 -1
- package/dist/server/ssr-handler.js +3 -7
- package/dist/shared/cache-control.d.ts +4 -1
- package/dist/shared/cache-control.js +11 -6
- package/dist/shared/route-chunk-recovery-bootstrap.d.ts +7 -0
- package/dist/shared/route-chunk-recovery-bootstrap.js +38 -0
- package/package.json +5 -3
package/dist/a2a/server.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
90
|
-
*
|
|
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 }`
|
|
94
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
119
|
-
//
|
|
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
|
-
|
|
159
|
+
const globalSecret = (hasExplicitVerificationSecret
|
|
125
160
|
? audienceOptions?.verificationSecret
|
|
126
|
-
: readDeployCredentialEnv("A2A_SECRET"));
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
}));
|
package/dist/a2a/task-store.d.ts
CHANGED
|
@@ -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
|
package/dist/a2a/task-store.js
CHANGED
|
@@ -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
|
/**
|
package/dist/a2a-claims.js
CHANGED
|
@@ -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
|
|
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
|
|
29
|
+
return claimedOrgId &&
|
|
30
|
+
claimedOrgId === orgId &&
|
|
31
|
+
claimedOrgDomain &&
|
|
32
|
+
claimedOrgDomain === orgDomain &&
|
|
33
|
+
jti
|
|
24
34
|
? {
|
|
25
35
|
email: identity.email,
|
|
26
36
|
orgId,
|
|
@@ -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
|
}>;
|