@agent-native/core 0.206.0 → 0.206.1-nightly-20261009213000

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 (93) hide show
  1. package/dist/action.d.ts +2 -0
  2. package/dist/agent/default-agent-engine.js +25 -0
  3. package/dist/agent/durable-background.d.ts +2 -0
  4. package/dist/agent/durable-background.js +2 -0
  5. package/dist/agent/engine/error-detail.d.ts +1 -0
  6. package/dist/agent/engine/error-detail.js +6 -1
  7. package/dist/agent/engine/tool-call-journal-seed.d.ts +1 -1
  8. package/dist/agent/engine/tool-call-journal-seed.js +47 -11
  9. package/dist/agent/engine/translate-anthropic.d.ts +9 -0
  10. package/dist/agent/engine/translate-anthropic.js +18 -3
  11. package/dist/agent/production-agent.d.ts +6 -4
  12. package/dist/agent/production-agent.js +126 -85
  13. package/dist/agent/run-store.js +15 -3
  14. package/dist/agent/thread-data-builder.js +1 -2
  15. package/dist/agent/types.d.ts +1 -0
  16. package/dist/automation/index.d.ts +1 -1
  17. package/dist/chat-threads/store.js +6 -2
  18. package/dist/cli/design-connect.d.ts +1 -0
  19. package/dist/cli/design-connect.js +72 -19
  20. package/dist/client/analytics.js +48 -58
  21. package/dist/client/chat/agentkit-agent-native.js +11 -10
  22. package/dist/client/chat/agentkit-protocol.js +5 -1
  23. package/dist/client/chat/runtime.d.ts +4 -1
  24. package/dist/client/chat/runtime.js +135 -43
  25. package/dist/client/session-replay-endpoint.d.ts +1 -0
  26. package/dist/client/session-replay-endpoint.js +35 -0
  27. package/dist/client/session-replay.js +2 -24
  28. package/dist/client/sharing/share-controller-helpers.d.ts +1 -1
  29. package/dist/client/sharing/share-controller-helpers.js +2 -1
  30. package/dist/client/sharing/useShareButtonController.js +2 -9
  31. package/dist/client/use-action.d.ts +1 -0
  32. package/dist/client/use-action.js +2 -1
  33. package/dist/deploy/build.js +1 -1
  34. package/dist/deploy/workspace-deploy.js +65 -6
  35. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  36. package/dist/file-upload/registry.d.ts +7 -1
  37. package/dist/file-upload/registry.js +25 -7
  38. package/dist/mcp/build-server.d.ts +6 -0
  39. package/dist/mcp/build-server.js +97 -28
  40. package/dist/mcp/embed-app.js +9 -8
  41. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  42. package/dist/notifications/routes.d.ts +3 -3
  43. package/dist/observability/metrics.d.ts +5 -0
  44. package/dist/observability/metrics.js +62 -7
  45. package/dist/observability/routes.d.ts +4 -4
  46. package/dist/org/handlers.d.ts +1 -1
  47. package/dist/org/index.d.ts +1 -1
  48. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  49. package/dist/provider-api/actions/provider-api.d.ts +5 -5
  50. package/dist/scripts/docs/search.d.ts +1 -0
  51. package/dist/scripts/docs/search.js +17 -4
  52. package/dist/secrets/index.d.ts +1 -1
  53. package/dist/secrets/index.js +1 -1
  54. package/dist/secrets/storage.d.ts +1 -0
  55. package/dist/secrets/storage.js +8 -0
  56. package/dist/server/action-routes.js +62 -29
  57. package/dist/server/agent-chat/mcp-options.d.ts +2 -0
  58. package/dist/server/agent-chat/prompt-resources.js +2 -2
  59. package/dist/server/agent-engine-api-key-route.d.ts +2 -1
  60. package/dist/server/agent-engine-api-key-route.js +66 -2
  61. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  62. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  63. package/dist/server/agent-run-context.js +21 -1
  64. package/dist/server/agent-teams-run-queue.js +2 -1
  65. package/dist/server/agents-bundle.js +1 -1
  66. package/dist/server/auth.js +1 -0
  67. package/dist/server/core-routes-plugin.js +12 -0
  68. package/dist/server/embed-session.js +15 -1
  69. package/dist/server/http-response-telemetry.js +37 -8
  70. package/dist/server/realtime-token.d.ts +1 -1
  71. package/dist/shared/embed-auth.d.ts +4 -0
  72. package/dist/shared/embed-auth.js +156 -23
  73. package/dist/shared/inline-bytes.d.ts +29 -0
  74. package/dist/shared/inline-bytes.js +170 -0
  75. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  76. package/dist/shared/mcp-embed-headers.js +1 -1
  77. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  78. package/dist/sharing/actions/list-resource-shares.js +10 -4
  79. package/dist/sharing/actions/set-resource-visibility.js +3 -1
  80. package/dist/sharing/actions/share-resource.js +4 -2
  81. package/dist/sharing/actions/unshare-resource.js +3 -1
  82. package/dist/sharing/widget-grant.d.ts +19 -0
  83. package/dist/sharing/widget-grant.js +55 -0
  84. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +20 -21
  85. package/dist/testing.d.ts +1 -0
  86. package/dist/testing.js +1 -0
  87. package/dist/tracking/registry.d.ts +3 -2
  88. package/dist/tracking/registry.js +12 -3
  89. package/dist/vite/client.js +6 -1
  90. package/docs/content/observability.mdx +15 -10
  91. package/docs/content/tracking.mdx +1 -1
  92. package/package.json +4 -4
  93. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +20 -21
@@ -0,0 +1,35 @@
1
+ const REPLAY_ENDPOINT_PATH = "/api/analytics/replay";
2
+ const URL_BASE = "https://analytics-endpoint.invalid";
3
+ const TRACKING_ENDPOINT_SUFFIXES = [
4
+ "/api/analytics/track",
5
+ "/ssr-track",
6
+ "/build-track",
7
+ "/config-track",
8
+ "/track",
9
+ ];
10
+ export function replayEndpointFromAnalyticsEndpoint(endpoint, baseUrl = URL_BASE) {
11
+ const hasScheme = /^[a-z][a-z\d+.-]*:/i.test(endpoint);
12
+ const isProtocolRelative = endpoint.startsWith("//");
13
+ let url;
14
+ try {
15
+ url = new URL(endpoint, baseUrl);
16
+ }
17
+ catch (error) {
18
+ if (error instanceof TypeError)
19
+ return null;
20
+ throw error;
21
+ }
22
+ if (url.protocol !== "http:" && url.protocol !== "https:")
23
+ return null;
24
+ const trackingSuffix = TRACKING_ENDPOINT_SUFFIXES.find((suffix) => url.pathname.endsWith(suffix));
25
+ url.pathname = trackingSuffix
26
+ ? `${url.pathname.slice(0, -trackingSuffix.length)}${REPLAY_ENDPOINT_PATH}`
27
+ : REPLAY_ENDPOINT_PATH;
28
+ url.hash = "";
29
+ if (hasScheme)
30
+ return url.toString();
31
+ if (isProtocolRelative) {
32
+ return `//${url.host}${url.pathname}${url.search}`;
33
+ }
34
+ return `${url.pathname}${url.search}`;
35
+ }
@@ -3,6 +3,7 @@ import { resolveLaneEndpoint } from "../shared/environment-lanes.js";
3
3
  import { loadOptionalPeer, OptionalPeerDependencyError, } from "../shared/optional-peer.js";
4
4
  import { isSyntheticTrafficValue } from "../shared/test-traffic.js";
5
5
  import { getOrCreateAnalyticsAnonymousId, getOrCreateAnalyticsSessionId, } from "./analytics-session.js";
6
+ import { replayEndpointFromAnalyticsEndpoint } from "./session-replay-endpoint.js";
6
7
  import { SESSION_REPLAY_BLOCK_ATTRIBUTE, SESSION_REPLAY_MASK_ATTRIBUTE, } from "./session-replay-privacy.js";
7
8
  import { decideReplayQuotaResponse, parseRetryAfterSeconds, } from "./session-replay-quota.js";
8
9
  import { scrubUrl } from "./url-scrub.js";
@@ -499,33 +500,10 @@ function readFirstEnvNumber(keys) {
499
500
  }
500
501
  return undefined;
501
502
  }
502
- function replayEndpointFromAnalyticsEndpoint(value) {
503
- try {
504
- const url = new URL(value);
505
- if (url.pathname.endsWith("/api/analytics/track")) {
506
- url.pathname = url.pathname.replace(/\/api\/analytics\/track$/, "/api/analytics/replay");
507
- return url.toString();
508
- }
509
- if (url.pathname.endsWith("/track")) {
510
- url.pathname = url.pathname.replace(/\/track$/, "/api/analytics/replay");
511
- return url.toString();
512
- }
513
- }
514
- catch {
515
- // Fall through to the relative path cases below.
516
- }
517
- if (value.endsWith("/api/analytics/track")) {
518
- return value.replace(/\/api\/analytics\/track$/, "/api/analytics/replay");
519
- }
520
- if (value.endsWith("/track")) {
521
- return value.replace(/\/track$/, "/api/analytics/replay");
522
- }
523
- return null;
524
- }
525
503
  function defaultReplayEndpoint() {
526
504
  const analyticsEndpoint = readEnvString("VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT");
527
505
  const derived = analyticsEndpoint
528
- ? replayEndpointFromAnalyticsEndpoint(analyticsEndpoint)
506
+ ? replayEndpointFromAnalyticsEndpoint(analyticsEndpoint, typeof document === "undefined" ? undefined : document.baseURI)
529
507
  : null;
530
508
  if (derived)
531
509
  return derived;
@@ -33,7 +33,7 @@ export declare const DEFAULT_MEMBER_SUGGESTION_LIMIT = 25;
33
33
  export declare const DEFAULT_MEMBER_SEARCH_DEBOUNCE_MS = 140;
34
34
  export declare function createShareQueryParams(resourceType: string, resourceId: string): ShareQueryParams;
35
35
  export declare function createShareQueryKey(params: ShareQueryParams): ShareQueryKey;
36
- export declare function useShareQuery<TResponse>(resourceType: string, resourceId: string, enabled?: boolean): {
36
+ export declare function useShareQuery<TResponse>(resourceType: string, resourceId: string, enabled?: boolean, staleTime?: number): {
37
37
  params: ShareQueryParams;
38
38
  queryKey: ShareQueryKey;
39
39
  query: ReturnType<typeof useActionQuery<TResponse>>;
@@ -10,12 +10,13 @@ export function createShareQueryParams(resourceType, resourceId) {
10
10
  export function createShareQueryKey(params) {
11
11
  return ["action", "list-resource-shares", params];
12
12
  }
13
- export function useShareQuery(resourceType, resourceId, enabled = true) {
13
+ export function useShareQuery(resourceType, resourceId, enabled = true, staleTime) {
14
14
  const queryClient = useQueryClient();
15
15
  const params = useMemo(() => createShareQueryParams(resourceType, resourceId), [resourceId, resourceType]);
16
16
  const queryKey = useMemo(() => createShareQueryKey(params), [params]);
17
17
  const query = useActionQuery("list-resource-shares", params, {
18
18
  enabled,
19
+ ...(staleTime === undefined ? {} : { staleTime }),
19
20
  });
20
21
  return { params, queryKey, query, queryClient };
21
22
  }
@@ -10,7 +10,7 @@ export function useShareButtonController(options) {
10
10
  const [visibilityOverride, setVisibilityOverride] = useState(null);
11
11
  const visibilitySequenceStartRef = useRef(null);
12
12
  const appliedDefaultOpenRef = useRef(false);
13
- const { queryKey: shareQueryKey, query: sharesQuery, queryClient, } = useShareQuery(options.resourceType, options.resourceId);
13
+ const { queryKey: shareQueryKey, query: sharesQuery, queryClient, } = useShareQuery(options.resourceType, options.resourceId, open, 0);
14
14
  const { setVisibility, share, unshare } = useShareMutations();
15
15
  const visibilityGuard = useShareMutationGuard();
16
16
  const data = sharesQuery.data;
@@ -24,10 +24,8 @@ export function useShareButtonController(options) {
24
24
  if (nextOpen) {
25
25
  setActiveShareTab(shareTabDefaultValue);
26
26
  options.shareTabs?.onValueChange?.(shareTabDefaultValue);
27
- if (visibilityOverride === null)
28
- void sharesQuery.refetch();
29
27
  }
30
- }, [options, shareTabDefaultValue, sharesQuery, visibilityOverride]);
28
+ }, [options, shareTabDefaultValue]);
31
29
  useEffect(() => {
32
30
  setInviteEmail("");
33
31
  setSelectedGroup(null);
@@ -147,11 +145,6 @@ export function useShareButtonController(options) {
147
145
  return next;
148
146
  });
149
147
  }, []);
150
- useEffect(() => {
151
- void sharesQuery.refetch();
152
- // The resource identity is intentionally stable for this controller.
153
- // eslint-disable-next-line react-hooks/exhaustive-deps
154
- }, []);
155
148
  const memberSearch = useShareOrgMemberSearch(inviteEmail, canManage && suggestionsOpen, {
156
149
  limit: DEFAULT_MEMBER_SUGGESTION_LIMIT,
157
150
  debounceMs: DEFAULT_MEMBER_SEARCH_DEBOUNCE_MS,
@@ -110,5 +110,6 @@ export declare function useActionMutation<TData = undefined, TVariables = undefi
110
110
  method?: "POST" | "PUT" | "DELETE";
111
111
  skipActionQueryInvalidation?: boolean;
112
112
  timeoutMs?: number;
113
+ headers?: Record<string, string> | ((variables: TVariables extends undefined ? ActionParams<TName> : TVariables) => Record<string, string> | undefined);
113
114
  }): import("@tanstack/react-query").UseMutationResult<TData extends undefined ? ActionResult<TName> : TData, Error, TVariables extends undefined ? ActionParams<TName> : TVariables, unknown>;
114
115
  export {};
@@ -782,12 +782,13 @@ export function useActionQuery(actionName, params, options) {
782
782
  }
783
783
  export function useActionMutation(actionName, options) {
784
784
  const queryClient = useQueryClient();
785
- const { method: methodOpt, onSuccess, skipActionQueryInvalidation = false, timeoutMs, ...restOptions } = options ?? {};
785
+ const { method: methodOpt, onSuccess, skipActionQueryInvalidation = false, timeoutMs, headers, ...restOptions } = options ?? {};
786
786
  const method = methodOpt ?? "POST";
787
787
  return useMutation({
788
788
  ...restOptions,
789
789
  mutationFn: (params) => actionFetch(actionName, method, params, {
790
790
  timeoutMs,
791
+ headers: typeof headers === "function" ? headers(params) : headers,
791
792
  }),
792
793
  onSuccess: (...args) => {
793
794
  // A write that succeeded may have fixed whatever was failing reads.
@@ -1910,7 +1910,7 @@ async function getHandler() {
1910
1910
  headers: {
1911
1911
  "Access-Control-Allow-Origin": "*",
1912
1912
  "Access-Control-Allow-Methods": "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
1913
- "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Client-Platform,X-Agent-Native-Desktop-Verifier,X-Agent-Native-Test-Traffic,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-Agent-Native-Embed-Target",
1913
+ "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Content-Save-Origin,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Client-Platform,X-Agent-Native-Desktop-Verifier,X-Agent-Native-Test-Traffic,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-Agent-Native-Embed-Target",
1914
1914
  },
1915
1915
  });
1916
1916
  }
@@ -1,5 +1,6 @@
1
- import { execFileSync } from "child_process";
1
+ import { spawn } from "child_process";
2
2
  import fs from "fs";
3
+ import os from "os";
3
4
  import path from "path";
4
5
  import { AGENT_BACKGROUND_PROCESSOR_A2A, AGENT_BACKGROUND_PROCESSOR_AGENT_TEAM, AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_INTEGRATION, AGENT_BACKGROUND_PROCESSOR_ROUTE, AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, AGENT_TEAM_PROCESS_RUN_PATH, isDurableBackgroundFlagExplicitlyDisabled, } from "../agent/durable-background.js";
5
6
  import { getAppConfig } from "../app-config/index.js";
@@ -210,10 +211,12 @@ export async function runWorkspaceDeploy(opts = {}) {
210
211
  fs.mkdirSync(functionsDir, { recursive: true });
211
212
  }
212
213
  console.log(`[workspace-deploy] Building ${apps.length} app(s) for preset=${preset}`);
213
- const execFile = opts.execFile ?? execFileSync;
214
+ const concurrency = workspaceBuildConcurrency();
215
+ const runBuild = opts.execFile ?? spawnBuild;
216
+ await runWithConcurrency(apps, concurrency, (app) => buildOneApp(workspaceRoot, appsDir, app, preset, runBuild, workspaceApps, workspaceAuthMode, concurrency > 1 ? "pipe" : "inherit"));
217
+ // Copy in app order after all builds so shared outputs stay deterministic.
214
218
  const sweepCrons = [];
215
219
  for (const app of apps) {
216
- buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApps, workspaceAuthMode);
217
220
  moveAppBuildIntoWorkspaceOutput(workspaceRoot, appsDir, app, preset, distDir, vercelOutputDir, workspaceApps, workspaceAuthMode);
218
221
  if (preset === "vercel") {
219
222
  sweepCrons.push(readVercelSweepCron(path.join(appsDir, app, VERCEL_OUTPUT_DIR), `/${app}`));
@@ -245,7 +248,7 @@ export async function runWorkspaceDeploy(opts = {}) {
245
248
  }
246
249
  console.log(`All apps live at https://<origin>/<app-name>/*. Log in once on any app\nand the session is shared across the workspace.`);
247
250
  }
248
- function buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApps, workspaceAuthMode) {
251
+ async function buildOneApp(workspaceRoot, appsDir, app, preset, runBuild, workspaceApps, workspaceAuthMode, stdio) {
249
252
  const appDir = path.join(appsDir, app);
250
253
  const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
251
254
  const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(workspaceApps, app);
@@ -306,10 +309,66 @@ function buildOneApp(workspaceRoot, appsDir, app, preset, execFile, workspaceApp
306
309
  }
307
310
  console.log(`[workspace-deploy] Building ${app} (base=/${app}, preset=${preset})`);
308
311
  cleanAppBuildOutputs(appDir);
309
- execFile("pnpm", ["--filter", app, "build"], {
312
+ const startedAt = Date.now();
313
+ await runBuild("pnpm", ["--filter", app, "build"], {
310
314
  cwd: workspaceRoot,
311
315
  env,
312
- stdio: "inherit",
316
+ stdio,
317
+ });
318
+ console.log(`[workspace-deploy] Built ${app} in ${((Date.now() - startedAt) / 1000).toFixed(1)}s`);
319
+ }
320
+ function workspaceBuildConcurrency() {
321
+ const raw = process.env.AGENT_NATIVE_DEPLOY_CONCURRENCY?.trim();
322
+ if (raw) {
323
+ const parsed = Number(raw);
324
+ if (!Number.isInteger(parsed) || parsed < 1) {
325
+ throw new Error(`AGENT_NATIVE_DEPLOY_CONCURRENCY must be a positive integer, got "${raw}"`);
326
+ }
327
+ return parsed;
328
+ }
329
+ // Each app build can use 1-2 GB, so stay well under typical CI memory limits.
330
+ return Math.min(3, os.availableParallelism());
331
+ }
332
+ async function runWithConcurrency(items, limit, fn) {
333
+ let next = 0;
334
+ const errors = [];
335
+ const worker = async () => {
336
+ while (errors.length === 0 && next < items.length) {
337
+ try {
338
+ await fn(items[next++]);
339
+ }
340
+ catch (error) {
341
+ errors.push(error);
342
+ }
343
+ }
344
+ };
345
+ // Let in-flight builds finish so none are orphaned when the CLI exits and
346
+ // every failure gets reported, not just the first.
347
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
348
+ if (errors.length === 1)
349
+ throw errors[0];
350
+ if (errors.length > 1) {
351
+ throw new Error(errors
352
+ .map((error) => error instanceof Error ? error.message : String(error))
353
+ .join("\n"));
354
+ }
355
+ }
356
+ function spawnBuild(cmd, args, options) {
357
+ return new Promise((resolve, reject) => {
358
+ const child = spawn(cmd, args, options);
359
+ // Buffered so parallel builds print as whole blocks instead of interleaving.
360
+ const chunks = [];
361
+ child.stdout?.on("data", (chunk) => chunks.push(chunk));
362
+ child.stderr?.on("data", (chunk) => chunks.push(chunk));
363
+ child.on("error", reject);
364
+ child.on("close", (code, signal) => {
365
+ if (chunks.length)
366
+ process.stdout.write(Buffer.concat(chunks));
367
+ if (code === 0)
368
+ resolve();
369
+ else
370
+ reject(new Error(`Command failed: ${cmd} ${args.join(" ")} (${signal ?? `exit code ${code}`})`));
371
+ });
313
372
  });
314
373
  }
315
374
  function moveAppBuildIntoWorkspaceOutput(workspaceRoot, appsDir, app, preset, distDir, vercelOutputDir, workspaceApps, workspaceAuthMode) {
@@ -33,9 +33,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
33
33
  idempotencyKey: string;
34
34
  deleted: boolean;
35
35
  } | {
36
- id?: undefined;
37
36
  error: string;
38
37
  url?: undefined;
38
+ id?: undefined;
39
39
  provider?: undefined;
40
40
  configured?: undefined;
41
41
  } | {
@@ -45,8 +45,8 @@ declare const _default: import("../../action.js").ActionDefinition<{
45
45
  configured?: undefined;
46
46
  error?: undefined;
47
47
  } | {
48
- id?: undefined;
49
48
  url?: undefined;
49
+ id?: undefined;
50
50
  provider?: undefined;
51
51
  error: string;
52
52
  configured: boolean;
@@ -2,7 +2,13 @@ import type { FileUploadDeleteInput, FileUploadInput, FileUploadProvider, FileUp
2
2
  export declare function registerFileUploadProvider(provider: FileUploadProvider): void;
3
3
  export declare function unregisterFileUploadProvider(id: string): void;
4
4
  export declare function listFileUploadProviders(): FileUploadProvider[];
5
- /** Resolve a public upload URL only through a provider that claims ownership. */
5
+ /**
6
+ * Resolve a public upload URL only through a provider that claims ownership.
7
+ * Each provider is checked independently: one provider whose check throws
8
+ * (S3 reads its config from the DB) must not hide another's positive claim, so
9
+ * this throws only when no provider claims the URL and at least one could not
10
+ * answer — "unverifiable", never reported as "not owned".
11
+ */
6
12
  export declare function findFileUploadProviderOwningUrl(url: string): Promise<FileUploadProvider | null>;
7
13
  export declare function getActiveFileUploadProvider(): FileUploadProvider | null;
8
14
  /**
@@ -11,17 +11,35 @@ export function unregisterFileUploadProvider(id) {
11
11
  export function listFileUploadProviders() {
12
12
  return [...providers.values()];
13
13
  }
14
- /** Resolve a public upload URL only through a provider that claims ownership. */
14
+ /**
15
+ * Resolve a public upload URL only through a provider that claims ownership.
16
+ * Each provider is checked independently: one provider whose check throws
17
+ * (S3 reads its config from the DB) must not hide another's positive claim, so
18
+ * this throws only when no provider claims the URL and at least one could not
19
+ * answer — "unverifiable", never reported as "not owned".
20
+ */
15
21
  export async function findFileUploadProviderOwningUrl(url) {
16
- const candidates = [...providers.values()];
17
- if (!candidates.includes(builderFileUploadProvider)) {
18
- candidates.push(builderFileUploadProvider);
19
- }
22
+ const candidates = [
23
+ builderFileUploadProvider,
24
+ ...[...providers.values()].filter((p) => p !== builderFileUploadProvider),
25
+ ];
26
+ const failures = [];
27
+ const unverifiable = [];
20
28
  for (const provider of candidates) {
21
- if (provider.isOwnedUrl && (await provider.isOwnedUrl(url))) {
22
- return provider;
29
+ if (!provider.isOwnedUrl)
30
+ continue;
31
+ try {
32
+ if (await provider.isOwnedUrl(url))
33
+ return provider;
34
+ }
35
+ catch (error) {
36
+ failures.push(error);
37
+ unverifiable.push(provider.id);
23
38
  }
24
39
  }
40
+ if (failures.length > 0) {
41
+ throw new AggregateError(failures, `Upload URL ownership is unverifiable: ${unverifiable.join(", ")} failed its check`);
42
+ }
25
43
  return null;
26
44
  }
27
45
  export function getActiveFileUploadProvider() {
@@ -74,6 +74,12 @@ export interface MCPConfig {
74
74
  widgetReadPrivateActions?: readonly string[];
75
75
  /** Authenticated reads surfaced on other agent profiles, but only scoped here. */
76
76
  widgetReadAuthenticatedActions?: readonly string[];
77
+ /**
78
+ * Scoped reads minted only into a write capability, and only for a target
79
+ * that lists the mapped write action: `{ "list-resource-shares":
80
+ * "share-resource" }` keeps collaborator lists out of read-only tickets.
81
+ */
82
+ widgetReadActionWriteGates?: Record<string, string>;
77
83
  /** Omit the one shared resource title when tools have distinct invocation labels. */
78
84
  widgetResourceTitle?: string | false;
79
85
  keyToolNames?: readonly string[];
@@ -356,6 +356,12 @@ export function validateMcpDirectoryProfile(config, sourceActions = config.produ
356
356
  throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget read route "${name}" must be an explicitly scoped read-only GET action with valid resource arguments in the connector allowlist or hidden-read profiles.`);
357
357
  }
358
358
  }
359
+ for (const [readAction, writeAction] of Object.entries(profile?.widgetReadActionWriteGates ?? {})) {
360
+ if (!Object.hasOwn(profile?.widgetReadActionArguments ?? {}, readAction) ||
361
+ !Object.hasOwn(profile?.widgetWriteActionArguments ?? {}, writeAction)) {
362
+ throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget read "${readAction}" is gated on "${writeAction}", which must both be declared widget actions.`);
363
+ }
364
+ }
359
365
  for (const [name, argumentMap] of Object.entries(profile?.widgetWriteActionArguments ?? {})) {
360
366
  const entry = actions[name] ?? config.widgetWriteActions?.[name];
361
367
  if (!entry ||
@@ -894,16 +900,6 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
894
900
  (ctx?.orgId ?? undefined))) {
895
901
  throw new Error("The widget write grant must match the authenticated MCP user and organization.");
896
902
  }
897
- const scope = restrictDirectoryWidgetCapability
898
- ? directoryWidget.capability.mode === "write"
899
- ? createMcpDirectoryWidgetWriteCapability(directoryWidget.capability.value)
900
- : createMcpDirectoryWidgetReadCapability(directoryWidget.capability.value)
901
- : typeof out.chrome === "string"
902
- ? out.chrome
903
- : null;
904
- if (restrictDirectoryWidgetCapability && !scope) {
905
- throw new Error("Could not create a valid scoped capability for this MCP directory widget.");
906
- }
907
903
  const revocationAnchorCandidate = ctx?.mcpCredentialIssuedAtMs;
908
904
  const revocationAnchorCreatedAtMs = typeof revocationAnchorCandidate === "number" &&
909
905
  Number.isSafeInteger(revocationAnchorCandidate)
@@ -913,18 +909,60 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
913
909
  revocationAnchorCreatedAtMs === undefined) {
914
910
  throw new Error("Directory widget sessions require a trusted MCP credential issue time.");
915
911
  }
916
- const ticket = await createEmbedSessionTicket({
917
- ownerEmail,
918
- orgId: ctx?.orgId,
919
- targetPath,
920
- scope,
921
- ...(directoryWidget?.capability.mode === "write"
922
- ? { ttlSeconds: 5 * 60 }
923
- : {}),
924
- ...(restrictDirectoryWidgetCapability
925
- ? { revocationAnchorCreatedAtMs }
926
- : {}),
927
- });
912
+ const mintTicket = async (capability) => {
913
+ const scope = capability
914
+ ? capability.mode === "write"
915
+ ? createMcpDirectoryWidgetWriteCapability(capability.value)
916
+ : createMcpDirectoryWidgetReadCapability(capability.value)
917
+ : typeof out.chrome === "string"
918
+ ? out.chrome
919
+ : null;
920
+ // An oversize or invalid scope has no ticket; the caller degrades or
921
+ // returns the result without one. Throwing here would fail a tool call
922
+ // whose action already ran.
923
+ if (capability && !scope)
924
+ return null;
925
+ return createEmbedSessionTicket({
926
+ ownerEmail,
927
+ orgId: ctx?.orgId,
928
+ targetPath,
929
+ scope,
930
+ ...(capability?.mode === "write" ? { ttlSeconds: 5 * 60 } : {}),
931
+ ...(capability ? { revocationAnchorCreatedAtMs } : {}),
932
+ });
933
+ };
934
+ const writeCapability = directoryWidget?.capability.mode === "write"
935
+ ? directoryWidget.capability
936
+ : undefined;
937
+ let ticket = null;
938
+ let writeMintError = new Error("Could not create a valid scoped capability for this MCP directory widget.");
939
+ try {
940
+ ticket = await mintTicket(directoryWidget?.capability);
941
+ }
942
+ catch (error) {
943
+ if (!writeCapability)
944
+ throw error;
945
+ writeMintError = error;
946
+ }
947
+ if (!ticket && writeCapability) {
948
+ // The write grant is an upgrade over the read grant. Losing it must not
949
+ // also lose the widget's session ticket, which the shell cannot start without.
950
+ console.error("[mcp:directory] Could not mint the widget write grant; issuing a read-only widget session instead.", writeMintError);
951
+ const { appId, resourceUri, resourceIds, readActionArguments } = writeCapability.value;
952
+ ticket = await mintTicket({
953
+ mode: "read",
954
+ value: {
955
+ appId,
956
+ resourceUri,
957
+ resourceIds,
958
+ actionArguments: readActionArguments,
959
+ },
960
+ });
961
+ }
962
+ if (!ticket) {
963
+ console.error("[mcp:directory] Could not build a widget capability within the scope size limits; returning the result without a session ticket.", { targetPath });
964
+ return resultWithoutExistingEmbedTicket;
965
+ }
928
966
  const startPath = buildEmbedStartPath(ticket.ticket);
929
967
  const embedStartUrl = meta?.origin
930
968
  ? new URL(startPath, meta.origin).toString()
@@ -957,6 +995,7 @@ async function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, a
957
995
  if (!target)
958
996
  return undefined;
959
997
  const actionArguments = {};
998
+ const writeGatedReadArguments = {};
960
999
  for (const [actionName, argumentMap] of Object.entries(profile.widgetReadActionArguments ?? {})) {
961
1000
  const scopedPublicRead = profile.widgetReadPublicActions?.includes(actionName);
962
1001
  const scopedPrivateRead = profile.widgetReadPrivateActions?.includes(actionName);
@@ -992,7 +1031,13 @@ async function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, a
992
1031
  }
993
1032
  }
994
1033
  if (Object.keys(scopedArguments).length === Object.keys(argumentMap).length) {
995
- actionArguments[actionName] = scopedArguments;
1034
+ const writeGate = profile.widgetReadActionWriteGates?.[actionName];
1035
+ if (writeGate === undefined) {
1036
+ actionArguments[actionName] = scopedArguments;
1037
+ }
1038
+ else if (target.writeActions?.includes(writeGate)) {
1039
+ writeGatedReadArguments[actionName] = { writeGate, scopedArguments };
1040
+ }
996
1041
  }
997
1042
  }
998
1043
  const writeActionArguments = {};
@@ -1071,7 +1116,15 @@ async function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, a
1071
1116
  userEmail: identity.userEmail,
1072
1117
  ...(typeof requestOrgId === "string" ? { orgId: requestOrgId } : {}),
1073
1118
  expiresAtMs: Date.now() + MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS,
1074
- readActionArguments: actionArguments,
1119
+ readActionArguments: {
1120
+ ...actionArguments,
1121
+ ...Object.fromEntries(Object.entries(writeGatedReadArguments)
1122
+ .filter(([, { writeGate }]) => Object.hasOwn(writeActionArguments, writeGate))
1123
+ .map(([actionName, { scopedArguments }]) => [
1124
+ actionName,
1125
+ scopedArguments,
1126
+ ])),
1127
+ },
1075
1128
  writeActionArguments,
1076
1129
  };
1077
1130
  return {
@@ -2441,11 +2494,24 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2441
2494
  mcpAppResourceCandidate !== null &&
2442
2495
  directoryWidget === undefined;
2443
2496
  const suppressDirectoryWidget = directoryCatalog && !hasVerifiedMcpUserIdentity(effectiveIdentity);
2497
+ // The host mounts a widget for this tool from its descriptor, with or
2498
+ // without a ticket in the result, so a withheld ticket must be visible.
2499
+ if (missingDirectoryWidgetCapability &&
2500
+ !suppressDirectoryWidget &&
2501
+ !mcpResultIsError) {
2502
+ console.warn(`[mcp:directory] ${name} returned no widget session ticket: ${typeof trustedCredentialIssuedAtMs === "number"
2503
+ ? "the result has no widget target or scoped actions"
2504
+ : "the credential carries no issue time"}.`);
2505
+ }
2444
2506
  const rawResultForClient = missingDirectoryWidgetCapability || suppressDirectoryWidget
2445
2507
  ? withoutMcpAppEmbedTicket(projectedRawResult)
2446
2508
  : mcpAppResourceCandidate
2447
2509
  ? await withServerMintedMcpAppEmbedStart(projectedRawResult, requestMeta, directoryLinkUrl, directoryWidget)
2448
2510
  : projectedRawResult;
2511
+ if (directoryWidget &&
2512
+ typeof metadataObject(rawResultForClient).embedStartUrl !== "string") {
2513
+ console.error(`[mcp:directory] ${name} built a widget capability but issued no session ticket.`);
2514
+ }
2449
2515
  const { value: actionResultForClient, images: resultImages, notes: resultImageNotes, } = extractAgentImagesFromActionResult(rawResultForClient);
2450
2516
  const textResultForClient = mcpResult
2451
2517
  ? projectedResultForClient
@@ -2996,12 +3062,15 @@ async function admitIssuedMcpCredential(credential, requestOrigin) {
2996
3062
  stored = lookup;
2997
3063
  }
2998
3064
  const orgId = credential.orgId !== undefined ? credential.orgId : stored?.orgId;
2999
- const credentialIssuedAtMs = credential.oauthClientId !== undefined
3000
- ? credential.grantCreatedAtMs
3001
- : typeof credential.issuedAt === "number" &&
3065
+ // Access tokens signed before grant times existed carry no claim, and the
3066
+ // directory widget refuses to mint a session without an issue time. Their
3067
+ // signed `iat` is the only anchor they have. `iat` moves on every refresh, so
3068
+ // it must stay the fallback and never outrank a signed grant time.
3069
+ const credentialIssuedAtMs = credential.grantCreatedAtMs ??
3070
+ (typeof credential.issuedAt === "number" &&
3002
3071
  Number.isSafeInteger(credential.issuedAt)
3003
3072
  ? credential.issuedAt * 1000
3004
- : undefined;
3073
+ : undefined);
3005
3074
  const mcpCredentialIssuedAtMs = credentialIssuedAtMs !== undefined &&
3006
3075
  Number.isSafeInteger(credentialIssuedAtMs)
3007
3076
  ? credentialIssuedAtMs
@@ -128,6 +128,9 @@ export function embedApp(options = {}) {
128
128
  let toolResponseMetadata = {};
129
129
  let openUrl = "";
130
130
  let openStartUrl = "";
131
+ // Start URLs are single use. The host replays a saved one on every re-sync,
132
+ // so remember which one the server has already refused.
133
+ let spentStartUrl = "";
131
134
  let activeEmbedSessionTicket = "";
132
135
  let startedFor = "";
133
136
  let appFrame = null;
@@ -1503,7 +1506,7 @@ export function embedApp(options = {}) {
1503
1506
  return;
1504
1507
  }
1505
1508
  embedSessionRefreshAttempts += 1;
1506
- openStartUrl = "";
1509
+ spentStartUrl = openStartUrl;
1507
1510
  startedFor = "";
1508
1511
  lastFrameSrc = "";
1509
1512
  setMessage("Refreshing app session");
@@ -2001,17 +2004,15 @@ export function embedApp(options = {}) {
2001
2004
  return;
2002
2005
  }
2003
2006
  if (message.type === "agentNative.embedSessionExpired") {
2004
- if (expiredSessionMessage) {
2005
- refreshExpiredEmbedSession();
2006
- } else if (typeof data.requestId === "string") {
2007
+ if (!expiredSessionMessage && typeof data.requestId === "string") {
2007
2008
  void renewExpiredEmbedSession(
2008
2009
  data.requestId,
2009
2010
  appFrame,
2010
2011
  appFrameDocumentState.loadGeneration,
2011
2012
  );
2012
- } else if (body.dataset.catalogMode !== "directory") {
2013
- // Older app frames do not include a renewal id, so preserve their
2014
- // existing full-refresh fallback until they load the renewal client.
2013
+ } else {
2014
+ // The server's expiry page names no request: it answered in place of
2015
+ // the app, so there is no mounted session to renew in place.
2015
2016
  refreshExpiredEmbedSession();
2016
2017
  }
2017
2018
  return;
@@ -2063,7 +2064,7 @@ export function embedApp(options = {}) {
2063
2064
  notifyHostHeight();
2064
2065
  return;
2065
2066
  }
2066
- let launchUrl = openStartUrl || openUrl;
2067
+ let launchUrl = (openStartUrl !== spentStartUrl && openStartUrl) || openUrl;
2067
2068
  if (!launchUrl) {
2068
2069
  renderAppLaunchError("Open link was not available.");
2069
2070
  return;