@agent-native/core 0.205.0 → 0.206.0-nightly-20261009011246

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 (115) hide show
  1. package/dist/a2a/client.d.ts +2 -2
  2. package/dist/agent/engine/anthropic-engine.d.ts +1 -1
  3. package/dist/agent/engine/builder-engine.d.ts +1 -1
  4. package/dist/agent/engine/builder-engine.js +23 -8
  5. package/dist/agent/engine/error-telemetry.d.ts +5 -0
  6. package/dist/agent/engine/error-telemetry.js +41 -0
  7. package/dist/agent/engine/registry.d.ts +1 -6
  8. package/dist/agent/engine/registry.js +2 -31
  9. package/dist/agent/model-config.d.ts +26 -14
  10. package/dist/agent/model-config.js +131 -8
  11. package/dist/agent/model-version.d.ts +19 -0
  12. package/dist/agent/model-version.js +153 -0
  13. package/dist/agent/production-agent.d.ts +1 -0
  14. package/dist/agent/production-agent.js +24 -6
  15. package/dist/agent/provider-model-selection.js +6 -4
  16. package/dist/agent/run-manager.js +5 -11
  17. package/dist/agent/thread-data-builder.js +14 -6
  18. package/dist/authorization/check-action.js +8 -4
  19. package/dist/chat-threads/store.js +22 -9
  20. package/dist/client/analytics.d.ts +1 -0
  21. package/dist/client/analytics.js +10 -2
  22. package/dist/client/chat/runtime.js +18 -0
  23. package/dist/client/chat-model-groups.d.ts +3 -0
  24. package/dist/client/error-format.js +10 -7
  25. package/dist/client/onboarding/use-onboarding.d.ts +12 -0
  26. package/dist/client/onboarding/use-onboarding.js +491 -4
  27. package/dist/client/session-replay-privacy.d.ts +8 -0
  28. package/dist/client/session-replay-privacy.js +8 -0
  29. package/dist/client/session-replay.d.ts +1 -0
  30. package/dist/client/session-replay.js +18 -9
  31. package/dist/client/sse-event-processor.js +10 -6
  32. package/dist/client/use-chat-models.d.ts +5 -0
  33. package/dist/client/use-chat-models.js +51 -5
  34. package/dist/client/use-session.d.ts +8 -1
  35. package/dist/client/use-session.js +20 -1
  36. package/dist/collab/struct-routes.d.ts +1 -1
  37. package/dist/deploy/build.js +353 -3
  38. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  39. package/dist/jobs/backfill-run-history-ownership.d.ts +12 -0
  40. package/dist/jobs/backfill-run-history-ownership.js +87 -0
  41. package/dist/jobs/background-automation-runner.js +3 -9
  42. package/dist/jobs/remote-execution.js +3 -9
  43. package/dist/jobs/run-history-ownership.d.ts +5 -0
  44. package/dist/jobs/run-history-ownership.js +16 -0
  45. package/dist/jobs/run-now.js +2 -3
  46. package/dist/localization/agent-resources-messages.d.ts +3 -0
  47. package/dist/localization/agent-resources-messages.js +33 -0
  48. package/dist/mcp/actions/create-org-service-token.js +5 -7
  49. package/dist/mcp/build-server.d.ts +23 -18
  50. package/dist/mcp/build-server.js +220 -212
  51. package/dist/mcp/connect-route.d.ts +11 -2
  52. package/dist/mcp/connect-route.js +34 -24
  53. package/dist/mcp/connect-store.d.ts +9 -13
  54. package/dist/mcp/connect-store.js +11 -24
  55. package/dist/mcp/credential-membership.js +17 -6
  56. package/dist/mcp/oauth-route.d.ts +11 -1
  57. package/dist/mcp/oauth-route.js +28 -6
  58. package/dist/mcp/oauth-token.d.ts +12 -2
  59. package/dist/mcp/oauth-token.js +15 -4
  60. package/dist/mcp/server.js +9 -5
  61. package/dist/notifications/routes.d.ts +2 -2
  62. package/dist/observability/posthog-ai.d.ts +1 -0
  63. package/dist/observability/posthog-ai.js +20 -8
  64. package/dist/observability/routes.d.ts +6 -6
  65. package/dist/observability/traces.js +61 -39
  66. package/dist/org/app-roles.d.ts +8 -1
  67. package/dist/org/app-roles.js +29 -9
  68. package/dist/org/context.js +24 -8
  69. package/dist/org/index.d.ts +2 -2
  70. package/dist/org/workspace-app-identity.d.ts +1 -0
  71. package/dist/org/workspace-app-identity.js +25 -0
  72. package/dist/progress/routes.d.ts +1 -1
  73. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  74. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  75. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  76. package/dist/scripts/agent-engines/list-agent-engines.js +1 -0
  77. package/dist/server/action-routes.js +8 -1
  78. package/dist/server/agent-chat-plugin.js +1 -0
  79. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  80. package/dist/server/auth.js +33 -17
  81. package/dist/server/bearer-credential-refusal.d.ts +23 -0
  82. package/dist/server/bearer-credential-refusal.js +67 -0
  83. package/dist/server/better-auth-instance.js +13 -6
  84. package/dist/server/builder-browser.d.ts +2 -0
  85. package/dist/server/builder-browser.js +16 -3
  86. package/dist/server/capture-error.d.ts +1 -0
  87. package/dist/server/capture-error.js +28 -4
  88. package/dist/server/complete-text.js +16 -3
  89. package/dist/server/core-routes-plugin.js +2 -0
  90. package/dist/server/dev-action-bridge.js +7 -1
  91. package/dist/server/google-oauth.js +6 -3
  92. package/dist/server/index.d.ts +1 -1
  93. package/dist/server/index.js +1 -1
  94. package/dist/server/realtime-token.d.ts +1 -1
  95. package/dist/server/recap-image-route.js +4 -0
  96. package/dist/server/request-context.d.ts +16 -0
  97. package/dist/server/request-context.js +17 -0
  98. package/dist/shared/agent-chat-context.d.ts +1 -0
  99. package/dist/shared/agent-chat-context.js +26 -0
  100. package/dist/shared/index.d.ts +1 -1
  101. package/dist/shared/index.js +1 -1
  102. package/dist/shared/reasoning-effort.js +10 -1
  103. package/dist/shared/ssr-session-bootstrap.d.ts +7 -0
  104. package/dist/shared/ssr-session-bootstrap.js +21 -0
  105. package/dist/sharing/access.js +86 -5
  106. package/dist/templates/builder-code-starter/server/routes/index.get.ts +2 -0
  107. package/dist/triggers/routes.js +2 -1
  108. package/docs/content/automation-connectors.mdx +5 -3
  109. package/docs/content/automations.mdx +78 -49
  110. package/docs/content/observability.mdx +16 -5
  111. package/docs/content/organizations-teams-permissions.mdx +44 -2
  112. package/docs/content/recurring-jobs.mdx +4 -8
  113. package/docs/content/template-forms.mdx +6 -0
  114. package/package.json +5 -4
  115. package/src/templates/builder-code-starter/server/routes/index.get.ts +2 -0
@@ -1,6 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getMethod, getHeader } from "h3";
3
- import { signA2AToken } from "../a2a/client.js";
4
3
  import { getAppConfig } from "../app-config/index.js";
5
4
  import { mcpSettingsMessagesForLocale } from "../localization/mcp-settings-messages.js";
6
5
  import { resolveLocaleFromRequest } from "../localization/server.js";
@@ -13,8 +12,9 @@ import { publicFrameworkPath } from "../server/framework-route-prefix.js";
13
12
  import { readBody } from "../server/h3-helpers.js";
14
13
  import { FRAMEWORK_INTERNAL_ROUTE_PREFIX } from "../shared/framework-route-prefix.js";
15
14
  import { MCP_CONNECT_MCP_URL_TEMPLATE, getMcpConnectGuides, getMcpStaticTokenFallback, interpolateMcpConnectTemplate, resolveMcpConnectGuideId, } from "../shared/mcp-connect-content.js";
16
- import { ensureConnectTables, recordMintedToken, listTokens, revokeToken, normalizeServiceName, serviceIdentityEmail, createDeviceCode, getDeviceCode, getDeviceCodeByUserCode, approveDeviceCode, consumeDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, expireDeviceCode, MCP_CONNECT_OAUTH_CLIENT_ID, MCP_CONNECT_SCOPE, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, MAX_SERVICE_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
15
+ import { ensureConnectTables, recordMintedToken, listTokens, revokeToken, normalizeServiceName, serviceIdentityEmail, createDeviceCode, getDeviceCode, getDeviceCodeByUserCode, approveDeviceCode, consumeDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, expireDeviceCode, MCP_CONNECT_OAUTH_CLIENT_ID, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, MAX_SERVICE_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
17
16
  import { McpCredentialIssuanceError, withMcpCredentialIssuance, } from "./credential-issuance.js";
17
+ import { getMcpOAuthIssuer, resolveMcpOAuthIssuer } from "./oauth-route.js";
18
18
  import { MCP_OAUTH_DEFAULT_SCOPE, signMcpOAuthAccessToken, } from "./oauth-token.js";
19
19
  import { resolveMcpOrgChoices } from "./org-choice.js";
20
20
  import { MCP_PUBLIC_ROUTE_PREFIX } from "./route-paths.js";
@@ -157,7 +157,7 @@ async function mintConnectToken(params) {
157
157
  ownerEmail: params.email,
158
158
  orgId: params.orgId,
159
159
  orgDomain,
160
- appUrl: params.appUrl,
160
+ issuer: params.issuer,
161
161
  expiresIn: `${params.ttlDays}d`,
162
162
  jti,
163
163
  ...(params.catalogScope === "full" ? { catalogScope: "full" } : {}),
@@ -171,34 +171,38 @@ async function mintConnectToken(params) {
171
171
  return { token, jti };
172
172
  });
173
173
  }
174
+ /**
175
+ * Connect tokens are MCP OAuth access tokens bound to this app's MCP URL, so
176
+ * they share OAuth's verification path. Do not sign them as A2A JWTs again:
177
+ * that format let cross-app trust rules reinterpret a credential this app
178
+ * issued.
179
+ */
174
180
  async function signConnectToken(params) {
175
- if (readDeployCredentialEnv("A2A_SECRET")?.trim()) {
176
- return signA2AToken(params.ownerEmail, params.orgDomain, undefined, {
177
- preferGlobalSecret: true,
178
- expiresIn: params.expiresIn,
179
- extraClaims: {
180
- jti: params.jti,
181
- scope: MCP_CONNECT_SCOPE,
182
- ...(params.includeOrgIdClaim && params.orgId
183
- ? { org_id: params.orgId }
184
- : {}),
185
- ...(params.catalogScope === "full" ? { catalog_scope: "full" } : {}),
186
- },
187
- });
188
- }
189
181
  return signMcpOAuthAccessToken({
190
182
  ownerEmail: params.ownerEmail,
191
183
  orgId: params.orgId ?? null,
192
184
  orgDomain: params.orgDomain ?? null,
193
185
  clientId: MCP_CONNECT_OAUTH_CLIENT_ID,
194
186
  scope: MCP_OAUTH_DEFAULT_SCOPE,
195
- resource: mcpResourceUrl(params.appUrl),
196
- issuer: params.appUrl,
187
+ resource: mcpResourceUrl(params.issuer),
188
+ issuer: params.issuer,
197
189
  jti: params.jti,
198
190
  expiresIn: params.expiresIn,
199
191
  ...(params.catalogScope === "full" ? { catalogScope: "full" } : {}),
192
+ ...(params.service ? { service: true } : {}),
200
193
  });
201
194
  }
195
+ /**
196
+ * Neither the request nor configuration names this app's public URL, so no
197
+ * audience `verifyAuth` would accept can be resolved. Guessing one would mint a
198
+ * token the app then refuses.
199
+ */
200
+ export class OrgServiceTokenAppUrlError extends Error {
201
+ constructor() {
202
+ super("Could not determine the app URL needed to mint a token. Set APP_URL on the deployment.");
203
+ this.name = "OrgServiceTokenAppUrlError";
204
+ }
205
+ }
202
206
  /**
203
207
  * Mint an ORG SERVICE token: a connect-scoped, revocable bearer whose subject
204
208
  * is the synthetic service identity `svc-<name>@service.<orgId>` instead of a
@@ -209,7 +213,7 @@ async function signConnectToken(params) {
209
213
  *
210
214
  * The token value is returned exactly once and never persisted — only the
211
215
  * random `jti` is stored, so the standard revocation path
212
- * (`isJtiRevoked` in `verifyAuth`) applies to service tokens identically.
216
+ * (`lookupConnectTokenOrg` in `verifyAuth`) applies to service tokens identically.
213
217
  *
214
218
  * The `create-org-service-token` action gates on org owner/admin before
215
219
  * calling this. Offboarding can remove that admin before the mint, so the
@@ -221,6 +225,9 @@ export async function mintOrgServiceToken(params) {
221
225
  const serviceEmail = serviceIdentityEmail(serviceName, params.orgId);
222
226
  const orgDomain = await resolveOrgDomain(params.orgId);
223
227
  const ttlDays = clampTtlDays(params.ttlDays ?? DEFAULT_TOKEN_TTL_DAYS, MAX_SERVICE_TOKEN_TTL_DAYS);
228
+ const issuer = resolveMcpOAuthIssuer(params.appUrl || undefined);
229
+ if (!issuer)
230
+ throw new OrgServiceTokenAppUrlError();
224
231
  await prepareConnectIssuance();
225
232
  return withMcpCredentialIssuance({
226
233
  email: params.createdBy,
@@ -233,10 +240,10 @@ export async function mintOrgServiceToken(params) {
233
240
  ownerEmail: serviceEmail,
234
241
  orgId: params.orgId,
235
242
  orgDomain,
236
- appUrl: params.appUrl,
243
+ issuer,
237
244
  expiresIn: `${ttlDays}d`,
238
245
  jti,
239
- includeOrgIdClaim: true,
246
+ service: true,
240
247
  });
241
248
  const id = await recordMintedToken({
242
249
  jti,
@@ -1205,6 +1212,9 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1205
1212
  const origin = deriveOrigin(event);
1206
1213
  const basePath = configuredBasePath();
1207
1214
  const appUrl = `${origin}${basePath}`;
1215
+ // Tokens bind to an audience verifyAuth accepts. Reached through an alias of
1216
+ // a configured public URL, that differs from the appUrl this page displays.
1217
+ const tokenIssuer = getMcpOAuthIssuer(event) ?? appUrl;
1208
1218
  let requestUrl = null;
1209
1219
  try {
1210
1220
  requestUrl = new URL(event.node?.req?.url ?? event.path ?? "/", "http://an.invalid");
@@ -1295,7 +1305,7 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1295
1305
  orgId: defaultOrganizationId,
1296
1306
  label,
1297
1307
  ttlDays,
1298
- appUrl,
1308
+ issuer: tokenIssuer,
1299
1309
  requestOrigin: origin,
1300
1310
  ...(catalogScope ? { catalogScope } : {}),
1301
1311
  });
@@ -1451,7 +1461,7 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1451
1461
  ownerEmail: claimed.ownerEmail,
1452
1462
  orgId: claimed.orgId,
1453
1463
  orgDomain,
1454
- appUrl,
1464
+ issuer: tokenIssuer,
1455
1465
  expiresIn: `${DEFAULT_TOKEN_TTL_DAYS}d`,
1456
1466
  jti,
1457
1467
  ...(claimed.catalogScope
@@ -57,33 +57,29 @@ export declare function recordMintedToken(params: {
57
57
  serviceName?: string | null;
58
58
  createdBy?: string | null;
59
59
  }, exec?: DbExec): Promise<string>;
60
- /**
61
- * Returns true when the given `jti` corresponds to a token that has been
62
- * revoked. Fails OPEN on a store/DB error: a transient Neon WS drop must not
63
- * lock every connected agent out. Signature verification is unaffected — this
64
- * is only the post-verify revoke check (see `verifyAuth` in build-server.ts).
65
- */
66
- /**
67
- * Throws when the revocation state can't be read. Answering "not revoked"
68
- * instead would let a transient read failure admit a revoked token.
69
- */
70
- export declare function isJtiRevoked(jti: string): Promise<boolean>;
71
60
  export type StoredConnectTokenIdentity = Pick<MintedTokenRow, "kind" | "ownerEmail" | "orgId">;
72
61
  export type ConnectTokenOrgLookup = ({
73
62
  status: "found";
74
63
  } & StoredConnectTokenIdentity) | {
64
+ status: "revoked";
65
+ } | {
75
66
  status: "missing";
76
67
  } | {
77
68
  status: "unavailable";
78
69
  };
70
+ /**
71
+ * A connect token's standing, read in one query. `unavailable` covers a read
72
+ * failure and a malformed row: answering `missing` or `found` instead would
73
+ * admit a revoked token, or refuse a live one, on a transient error.
74
+ */
79
75
  export declare function lookupConnectTokenOrg(jti: string): Promise<ConnectTokenOrgLookup>;
80
76
  export declare function listTokens(ownerEmail: string): Promise<MintedTokenRow[]>;
81
77
  export declare function listOrgServiceTokens(orgId: string): Promise<MintedTokenRow[]>;
82
78
  /**
83
79
  * Revoke an org service token by id, scoped to `orgId` AND `kind = 'service'`
84
80
  * so a caller can never revoke another org's token (or someone's personal
85
- * token) through this path. Uses the same `revoked_at` gate `isJtiRevoked`
86
- * checks, so revocation takes effect on the next request like personal
81
+ * token) through this path. Uses the same `revoked_at` gate
82
+ * `lookupConnectTokenOrg` checks, so revocation takes effect on the next request like personal
87
83
  * tokens. Idempotent; returns true when a row actually transitioned.
88
84
  */
89
85
  export declare function revokeOrgServiceToken(orgId: string, id: string): Promise<boolean>;
@@ -127,37 +127,23 @@ export async function recordMintedToken(params, exec) {
127
127
  return id;
128
128
  }
129
129
  /**
130
- * Returns true when the given `jti` corresponds to a token that has been
131
- * revoked. Fails OPEN on a store/DB error: a transient Neon WS drop must not
132
- * lock every connected agent out. Signature verification is unaffected — this
133
- * is only the post-verify revoke check (see `verifyAuth` in build-server.ts).
130
+ * A connect token's standing, read in one query. `unavailable` covers a read
131
+ * failure and a malformed row: answering `missing` or `found` instead would
132
+ * admit a revoked token, or refuse a live one, on a transient error.
134
133
  */
135
- /**
136
- * Throws when the revocation state can't be read. Answering "not revoked"
137
- * instead would let a transient read failure admit a revoked token.
138
- */
139
- export async function isJtiRevoked(jti) {
140
- await ensureTable();
141
- const client = getDbExec();
142
- const { rows } = await client.execute({
143
- sql: `SELECT revoked_at FROM mcp_connect_tokens WHERE jti = ?`,
144
- args: [jti],
145
- });
146
- if (rows.length === 0)
147
- return false;
148
- const revokedAt = rows[0].revoked_at ?? rows[0].revokedAt;
149
- return revokedAt != null;
150
- }
151
134
  export async function lookupConnectTokenOrg(jti) {
152
135
  try {
153
136
  await ensureTable();
154
137
  const client = getDbExec();
155
138
  const { rows } = await client.execute({
156
- sql: `SELECT org_id, owner_email, kind FROM mcp_connect_tokens WHERE jti = ?`,
139
+ sql: `SELECT org_id, owner_email, kind, revoked_at FROM mcp_connect_tokens WHERE jti = ?`,
157
140
  args: [jti],
158
141
  });
159
142
  if (rows.length === 0)
160
143
  return { status: "missing" };
144
+ if ((rows[0].revoked_at ?? rows[0].revokedAt) != null) {
145
+ return { status: "revoked" };
146
+ }
161
147
  const rawOrgId = rows[0].org_id ?? rows[0].orgId;
162
148
  const ownerEmail = rows[0].owner_email ?? rows[0].ownerEmail;
163
149
  const kind = rows[0].kind;
@@ -176,7 +162,8 @@ export async function lookupConnectTokenOrg(jti) {
176
162
  : null,
177
163
  };
178
164
  }
179
- catch {
165
+ catch (error) {
166
+ console.error("[mcp] Connect-token lookup failed:", error);
180
167
  return { status: "unavailable" };
181
168
  }
182
169
  }
@@ -223,8 +210,8 @@ export async function listOrgServiceTokens(orgId) {
223
210
  /**
224
211
  * Revoke an org service token by id, scoped to `orgId` AND `kind = 'service'`
225
212
  * so a caller can never revoke another org's token (or someone's personal
226
- * token) through this path. Uses the same `revoked_at` gate `isJtiRevoked`
227
- * checks, so revocation takes effect on the next request like personal
213
+ * token) through this path. Uses the same `revoked_at` gate
214
+ * `lookupConnectTokenOrg` checks, so revocation takes effect on the next request like personal
228
215
  * tokens. Idempotent; returns true when a row actually transitioned.
229
216
  */
230
217
  export async function revokeOrgServiceToken(orgId, id) {
@@ -23,15 +23,26 @@ export async function checkCredentialOrgMembership(input) {
23
23
  // A human OAuth subject can use a service-shaped address. Only the local
24
24
  // record of an authenticated connect token proves it is a service identity.
25
25
  const stored = input.storedConnectToken;
26
- if (stored?.kind === "service" &&
27
- stored.ownerEmail === email &&
28
- stored.orgId === orgId &&
29
- implicitServiceOrgRole({ email, orgId, requestOrgId: stored.orgId })) {
30
- return "member";
31
- }
32
26
  const lookup = () => isOrgMember(orgId, email, { requireOrganizationMetadata: true });
33
27
  const context = getRequestContext();
34
28
  try {
29
+ if (stored?.kind === "service" &&
30
+ stored.ownerEmail === email &&
31
+ stored.orgId === orgId &&
32
+ implicitServiceOrgRole({ email, orgId, requestOrgId: stored.orgId })) {
33
+ const organization = await getDbExec().execute({
34
+ sql: `SELECT identity_authority, identity_id
35
+ FROM organizations WHERE id = ? LIMIT 1`,
36
+ args: [orgId],
37
+ });
38
+ const metadata = organization.rows[0];
39
+ if (!metadata)
40
+ return "not-member";
41
+ return String(metadata.identity_authority ?? "").trim() ||
42
+ String(metadata.identity_id ?? "").trim()
43
+ ? "not-member"
44
+ : "member";
45
+ }
35
46
  const member = !input.requestOrigin || context?.requestOrigin
36
47
  ? await lookup()
37
48
  : await runWithRequestContext({ ...context, requestOrigin: input.requestOrigin }, lookup);
@@ -1,9 +1,18 @@
1
1
  import type { H3Event } from "h3";
2
+ import { type BearerCredentialRefusal } from "../server/bearer-credential-refusal.js";
2
3
  export interface McpOAuthRouteOptions {
3
4
  appId?: string;
4
5
  appName?: string;
5
6
  }
6
7
  export declare function getMcpOAuthIssuer(event: H3Event): string | undefined;
8
+ /**
9
+ * The issuer MCP credentials are bound to, given the caller's request origin.
10
+ * Verification accepts only this issuer's resources, so anything that mints a
11
+ * bearer for this app must resolve its audience here, not from request headers:
12
+ * a configured public URL wins over the host a page was reached through.
13
+ */
14
+ export declare function resolveMcpOAuthIssuer(requestOrigin: string | undefined): string | undefined;
15
+ export declare function getMcpConnectUrl(event: H3Event): string | undefined;
7
16
  export declare function getMcpOAuthResource(event: H3Event, routePath?: string): string | undefined;
8
17
  /**
9
18
  * All plausible MCP OAuth resource audiences for the given request, deduped.
@@ -16,7 +25,8 @@ export declare function getMcpOAuthResource(event: H3Event, routePath?: string):
16
25
  */
17
26
  export declare function getMcpOAuthAudiences(event: H3Event, routePath?: string): string[];
18
27
  export declare function getMcpOAuthProtectedResourceMetadataUrl(event: H3Event, routePath?: string): string | undefined;
19
- export declare function buildMcpOAuthChallenge(event: H3Event, routePath?: string): string;
28
+ /** `refusal` is set only when the request presented a bearer token. */
29
+ export declare function buildMcpOAuthChallenge(event: H3Event, routePath?: string, refusal?: BearerCredentialRefusal): string;
20
30
  export declare function handleMcpOAuthProtectedResourceMetadata(event: H3Event): Response;
21
31
  export declare function handleMcpOAuthAuthorizationServerMetadata(event: H3Event): Response;
22
32
  export declare function handleMcpOAuth(event: H3Event, subpath: string, options?: McpOAuthRouteOptions): Promise<Response>;
@@ -2,6 +2,7 @@ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { getHeader, getMethod, getQuery, setResponseStatus } from "h3";
3
3
  import { getOrgDomain } from "../org/context.js";
4
4
  import { getConfiguredLoginHtml, getSession } from "../server/auth.js";
5
+ import { describeBearerCredentialRefusalWithRecovery, } from "../server/bearer-credential-refusal.js";
5
6
  import { getAuthSecret } from "../server/better-auth-instance.js";
6
7
  import { CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE } from "../server/credential-membership-unavailable.js";
7
8
  import { getOrigin } from "../server/google-oauth.js";
@@ -126,11 +127,24 @@ function deriveOrigin(event) {
126
127
  return host ? `${proto}://${host}` : "";
127
128
  }
128
129
  export function getMcpOAuthIssuer(event) {
129
- const baseUrl = configuredPublicBaseUrl() || deriveOrigin(event);
130
+ return resolveMcpOAuthIssuer(deriveOrigin(event));
131
+ }
132
+ /**
133
+ * The issuer MCP credentials are bound to, given the caller's request origin.
134
+ * Verification accepts only this issuer's resources, so anything that mints a
135
+ * bearer for this app must resolve its audience here, not from request headers:
136
+ * a configured public URL wins over the host a page was reached through.
137
+ */
138
+ export function resolveMcpOAuthIssuer(requestOrigin) {
139
+ const baseUrl = configuredPublicBaseUrl() || requestOrigin;
130
140
  if (!baseUrl)
131
141
  return undefined;
132
142
  return appendConfiguredBasePath(baseUrl);
133
143
  }
144
+ export function getMcpConnectUrl(event) {
145
+ const issuer = getMcpOAuthIssuer(event);
146
+ return issuer ? `${issuer}${MCP_PUBLIC_ROUTE_PREFIX}/connect` : undefined;
147
+ }
134
148
  function normalizeMcpResourcePath(routePath) {
135
149
  if (routePath === MCP_LEGACY_ROUTE_PREFIX ||
136
150
  routePath === MCP_DIRECTORY_ROUTE_PREFIX) {
@@ -198,12 +212,20 @@ export function getMcpOAuthProtectedResourceMetadataUrl(event, routePath = MCP_P
198
212
  }
199
213
  return metadataUrl.toString();
200
214
  }
201
- export function buildMcpOAuthChallenge(event, routePath = MCP_PUBLIC_ROUTE_PREFIX) {
215
+ /** `refusal` is set only when the request presented a bearer token. */
216
+ export function buildMcpOAuthChallenge(event, routePath = MCP_PUBLIC_ROUTE_PREFIX, refusal) {
202
217
  const metadata = getMcpOAuthProtectedResourceMetadataUrl(event, routePath);
203
- const scope = MCP_OAUTH_RESOURCE_SCOPE;
204
- return metadata
205
- ? `Bearer resource_metadata="${metadata}", scope="${scope}"`
206
- : `Bearer scope="${scope}"`;
218
+ const params = [
219
+ ...(metadata ? [`resource_metadata="${metadata}"`] : []),
220
+ `scope="${MCP_OAUTH_RESOURCE_SCOPE}"`,
221
+ ...(refusal
222
+ ? [
223
+ `error="invalid_token"`,
224
+ `error_description="${describeBearerCredentialRefusalWithRecovery(refusal, getMcpConnectUrl(event))}"`,
225
+ ]
226
+ : []),
227
+ ];
228
+ return `Bearer ${params.join(", ")}`;
207
229
  }
208
230
  function protectedResourcePathFromRequest(event) {
209
231
  let pathname = event.url?.pathname ?? "";
@@ -2,7 +2,15 @@ import { MCP_OAUTH_ACCESS_TOKEN_TTL, MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS } from "
2
2
  export { MCP_OAUTH_ACCESS_TOKEN_TTL, MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS };
3
3
  export declare const MCP_OAUTH_SCOPES: readonly ["mcp:read", "mcp:write", "mcp:apps", "offline_access"];
4
4
  export declare const MCP_OAUTH_DEFAULT_SCOPE: string;
5
+ export declare const MCP_OAUTH_TOKEN_TYPE = "agent-native-mcp-oauth";
5
6
  declare const MCP_OAUTH_CREDENTIAL_VERSION = 2;
7
+ /**
8
+ * Service credentials carry a version that verifiers predating service
9
+ * identity assurance reject. Those verifiers admit any MCP OAuth token as a
10
+ * verified user, so a service token they accepted could approve gated actions.
11
+ * Never sign a service credential with `MCP_OAUTH_CREDENTIAL_VERSION`.
12
+ */
13
+ declare const MCP_OAUTH_SERVICE_CREDENTIAL_VERSION = 3;
6
14
  export interface McpOAuthAccessTokenClaims {
7
15
  sub: string;
8
16
  org_id?: string | null;
@@ -11,8 +19,8 @@ export interface McpOAuthAccessTokenClaims {
11
19
  client_id: string;
12
20
  resource: string;
13
21
  jti?: string;
14
- typ: "agent-native-mcp-oauth";
15
- credential_version: typeof MCP_OAUTH_CREDENTIAL_VERSION;
22
+ typ: typeof MCP_OAUTH_TOKEN_TYPE;
23
+ credential_version: typeof MCP_OAUTH_CREDENTIAL_VERSION | typeof MCP_OAUTH_SERVICE_CREDENTIAL_VERSION;
16
24
  }
17
25
  export declare function normalizeOAuthScope(input: unknown): string | null;
18
26
  export declare function scopeList(scope: string | undefined): string[];
@@ -31,6 +39,8 @@ export declare function signMcpOAuthAccessToken(params: {
31
39
  jti?: string;
32
40
  expiresIn?: string | number;
33
41
  catalogScope?: "full";
42
+ /** An org service identity, not a person. */
43
+ service?: true;
34
44
  }): Promise<string>;
35
45
  export declare function verifyMcpOAuthAccessToken(token: string, resource: string | string[] | undefined): Promise<{
36
46
  userEmail: string;
@@ -11,7 +11,15 @@ export const MCP_OAUTH_SCOPES = [
11
11
  "offline_access",
12
12
  ];
13
13
  export const MCP_OAUTH_DEFAULT_SCOPE = MCP_OAUTH_SCOPES.join(" ");
14
+ export const MCP_OAUTH_TOKEN_TYPE = "agent-native-mcp-oauth";
14
15
  const MCP_OAUTH_CREDENTIAL_VERSION = 2;
16
+ /**
17
+ * Service credentials carry a version that verifiers predating service
18
+ * identity assurance reject. Those verifiers admit any MCP OAuth token as a
19
+ * verified user, so a service token they accepted could approve gated actions.
20
+ * Never sign a service credential with `MCP_OAUTH_CREDENTIAL_VERSION`.
21
+ */
22
+ const MCP_OAUTH_SERVICE_CREDENTIAL_VERSION = 3;
15
23
  function signingSecret() {
16
24
  return new TextEncoder().encode(process.env.A2A_SECRET?.trim() || getAuthSecret());
17
25
  }
@@ -60,8 +68,10 @@ export function parseMcpOAuthOrgIdClaim(payload) {
60
68
  }
61
69
  export async function signMcpOAuthAccessToken(params) {
62
70
  return new jose.SignJWT({
63
- typ: "agent-native-mcp-oauth",
64
- credential_version: MCP_OAUTH_CREDENTIAL_VERSION,
71
+ typ: MCP_OAUTH_TOKEN_TYPE,
72
+ credential_version: params.service
73
+ ? MCP_OAUTH_SERVICE_CREDENTIAL_VERSION
74
+ : MCP_OAUTH_CREDENTIAL_VERSION,
65
75
  sub: params.ownerEmail,
66
76
  ...(params.orgId !== undefined ? { org_id: params.orgId } : {}),
67
77
  ...(params.orgDomain ? { org_domain: params.orgDomain } : {}),
@@ -122,9 +132,10 @@ export async function verifyMcpOAuthAccessToken(token, resource) {
122
132
  if (!payload)
123
133
  return null;
124
134
  try {
125
- if (payload.typ !== "agent-native-mcp-oauth")
135
+ if (payload.typ !== MCP_OAUTH_TOKEN_TYPE)
126
136
  return null;
127
- if (payload.credential_version !== MCP_OAUTH_CREDENTIAL_VERSION)
137
+ if (payload.credential_version !== MCP_OAUTH_CREDENTIAL_VERSION &&
138
+ payload.credential_version !== MCP_OAUTH_SERVICE_CREDENTIAL_VERSION)
128
139
  return null;
129
140
  if (typeof payload.resource !== "string")
130
141
  return null;
@@ -2,6 +2,7 @@ import { defineEventHandler, setResponseStatus, setResponseHeader, getMethod, ge
2
2
  import { getAppConfig } from "../app-config/store.js";
3
3
  import { getConfiguredAppBasePath } from "../server/app-base-path.js";
4
4
  import { isLoopbackRequest } from "../server/auth.js";
5
+ import { describeBearerCredentialRefusal, } from "../server/bearer-credential-refusal.js";
5
6
  import { CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE } from "../server/credential-membership-unavailable.js";
6
7
  import { getH3App } from "../server/framework-request-handler.js";
7
8
  import { getOrigin } from "../server/google-oauth.js";
@@ -88,7 +89,7 @@ function buildWebRequest(event, method, routePath = MCP_PUBLIC_ROUTE_PREFIX) {
88
89
  const url = `${proto}://${host}${basePath}${routePath}`;
89
90
  return new Request(url, { method, headers });
90
91
  }
91
- function buildUnauthorizedBody(event, routePath = MCP_PUBLIC_ROUTE_PREFIX) {
92
+ function buildUnauthorizedBody(event, routePath = MCP_PUBLIC_ROUTE_PREFIX, refusal) {
92
93
  const issuer = getMcpOAuthIssuer(event);
93
94
  const mcpUrl = getMcpOAuthResource(event, routePath);
94
95
  const resourceMetadataUrl = getMcpOAuthProtectedResourceMetadataUrl(event, routePath);
@@ -101,7 +102,7 @@ function buildUnauthorizedBody(event, routePath = MCP_PUBLIC_ROUTE_PREFIX) {
101
102
  const authorizeUrl = issuer
102
103
  ? `${issuer}${MCP_PUBLIC_ROUTE_PREFIX}/oauth/authorize`
103
104
  : undefined;
104
- const message = command
105
+ const instructions = command
105
106
  ? `Authentication required. Run \`${command}\` to re-authenticate this ` +
106
107
  `MCP connector without reinstalling it (or, in a Claude Code host, ` +
107
108
  `run /mcp and choose Authenticate), then retry. For first-time ` +
@@ -110,7 +111,10 @@ function buildUnauthorizedBody(event, routePath = MCP_PUBLIC_ROUTE_PREFIX) {
110
111
  "then retry.";
111
112
  return {
112
113
  error: "Unauthorized",
113
- message,
114
+ ...(refusal ? { reason: refusal } : {}),
115
+ message: refusal
116
+ ? `${describeBearerCredentialRefusal(refusal)} ${instructions}`
117
+ : instructions,
114
118
  authenticate: {
115
119
  ...(command ? { command } : {}),
116
120
  ...(firstTimeCommand ? { firstTimeCommand } : {}),
@@ -263,8 +267,8 @@ async function handleMcpRequestInternal(event, config, routePath = MCP_PUBLIC_RO
263
267
  }
264
268
  if (!authResult.authed) {
265
269
  setResponseStatus(event, 401);
266
- setResponseHeader(event, "WWW-Authenticate", buildMcpOAuthChallenge(event, routePath));
267
- return buildUnauthorizedBody(event, routePath);
270
+ setResponseHeader(event, "WWW-Authenticate", buildMcpOAuthChallenge(event, routePath, authResult.refusal));
271
+ return buildUnauthorizedBody(event, routePath, authResult.refusal);
268
272
  }
269
273
  const body = method === "POST" ? await readBody(event) : undefined;
270
274
  onMcpMethod?.(directoryLogMethod(body) ?? method);
@@ -1,13 +1,13 @@
1
1
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
2
2
  count: number;
3
3
  updated?: undefined;
4
- ok?: undefined;
5
4
  error?: undefined;
5
+ ok?: undefined;
6
6
  } | {
7
7
  count?: undefined;
8
8
  updated: number;
9
- ok?: undefined;
10
9
  error?: undefined;
10
+ ok?: undefined;
11
11
  } | {
12
12
  count?: undefined;
13
13
  updated?: undefined;
@@ -5,6 +5,7 @@ export interface AiErrorDetail {
5
5
  terminal_code?: string;
6
6
  terminal_state?: string;
7
7
  retryable?: boolean;
8
+ cause?: string;
8
9
  }
9
10
  export declare function resolveAiError(isError: boolean, error: AiErrorDetail | undefined): AiErrorDetail | undefined;
10
11
  export declare function boundAiContent(value: unknown): {
@@ -1,14 +1,16 @@
1
+ import { runErrorTelemetryProperties } from "../agent/engine/error-telemetry.js";
2
+ import { isToolDoneFailure } from "../agent/tool-done-error.js";
1
3
  import { sendPostHogEvent } from "../tracking/providers.js";
2
- import { boundedText } from "../tracking/redaction.js";
3
4
  export const MAX_AI_CONTENT_BYTES = 128 * 1024;
4
5
  export const MAX_AI_SPANS_PER_RUN = 100;
5
- const UNREPORTED_ERROR = {
6
- message: "failed without a reported error message",
7
- };
8
6
  export function resolveAiError(isError, error) {
9
7
  if (!isError)
10
8
  return undefined;
11
- return error ?? UNREPORTED_ERROR;
9
+ return failedRunErrorDetail(error?.message, {
10
+ state: error?.terminal_state,
11
+ code: error?.terminal_code,
12
+ retryable: error?.retryable,
13
+ });
12
14
  }
13
15
  function trackAiEvent(name, properties, userId, occurredAt) {
14
16
  for (const key of Object.keys(properties)) {
@@ -122,7 +124,12 @@ export function toPostHogMessages(value) {
122
124
  ...(typeof part.toolName === "string"
123
125
  ? { name: part.toolName }
124
126
  : {}),
125
- content: part.content,
127
+ content: isToolDoneFailure({
128
+ isError: part.isError,
129
+ result: part.content,
130
+ })
131
+ ? "[tool error message omitted from telemetry]"
132
+ : part.content,
126
133
  });
127
134
  break;
128
135
  case "tool-call":
@@ -245,12 +252,17 @@ export function emitAiFeedbackSurveyEvent(input) {
245
252
  export function toAiErrorDetail(errorMessage, terminalOutcome) {
246
253
  if (!errorMessage && !terminalOutcome?.code)
247
254
  return undefined;
255
+ return failedRunErrorDetail(errorMessage, terminalOutcome);
256
+ }
257
+ function failedRunErrorDetail(errorMessage, terminalOutcome) {
258
+ const { error_code, error_cause } = runErrorTelemetryProperties(terminalOutcome?.code, errorMessage);
248
259
  return {
249
- message: boundedText(errorMessage ?? terminalOutcome?.code ?? "error", 1000),
260
+ message: `Agent run failed (${error_code})`,
261
+ cause: error_cause,
250
262
  ...(terminalOutcome?.state
251
263
  ? { terminal_state: terminalOutcome.state }
252
264
  : {}),
253
- ...(terminalOutcome?.code ? { terminal_code: terminalOutcome.code } : {}),
265
+ terminal_code: error_code,
254
266
  ...(terminalOutcome?.retryable !== undefined
255
267
  ? { retryable: terminalOutcome.retryable }
256
268
  : {}),
@@ -27,43 +27,43 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
27
27
  thumbsUpRate: number;
28
28
  avgEvalScore: number;
29
29
  } | {
30
- ok?: undefined;
31
30
  error: string;
32
31
  message: string;
33
32
  summary?: undefined;
34
33
  spans?: undefined;
35
- } | {
36
34
  ok?: undefined;
35
+ } | {
37
36
  message?: undefined;
38
37
  summary: import("./types.js").TraceSummary;
39
38
  spans: import("./types.js").TraceSpan[];
40
39
  error?: undefined;
41
- } | {
42
40
  ok?: undefined;
41
+ } | {
43
42
  message?: undefined;
44
43
  summary?: undefined;
45
44
  spans?: undefined;
46
45
  traceMissing: true;
47
46
  id: string;
48
47
  error?: undefined;
49
- } | {
50
48
  ok?: undefined;
49
+ } | {
51
50
  message?: undefined;
52
51
  summary?: undefined;
53
52
  spans?: undefined;
54
53
  traceMissing?: undefined;
55
54
  id: string;
56
55
  error?: undefined;
57
- } | {
58
56
  ok?: undefined;
57
+ } | {
59
58
  message?: undefined;
60
59
  summary?: undefined;
61
60
  spans?: undefined;
62
61
  error: any;
62
+ ok?: undefined;
63
63
  } | {
64
64
  message?: undefined;
65
65
  summary?: undefined;
66
66
  spans?: undefined;
67
- ok: boolean;
68
67
  error?: undefined;
68
+ ok: boolean;
69
69
  }>>;