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

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 (66) hide show
  1. package/dist/agent/default-agent-engine.js +25 -0
  2. package/dist/agent/durable-background.d.ts +2 -0
  3. package/dist/agent/durable-background.js +2 -0
  4. package/dist/agent/engine/error-detail.d.ts +1 -0
  5. package/dist/agent/engine/error-detail.js +6 -1
  6. package/dist/agent/engine/translate-anthropic.d.ts +9 -0
  7. package/dist/agent/engine/translate-anthropic.js +18 -3
  8. package/dist/agent/production-agent.d.ts +4 -4
  9. package/dist/agent/production-agent.js +78 -74
  10. package/dist/agent/run-store.js +15 -3
  11. package/dist/agent/thread-data-builder.js +1 -2
  12. package/dist/automation/index.d.ts +1 -1
  13. package/dist/chat-threads/store.js +6 -2
  14. package/dist/client/chat/agentkit-agent-native.js +11 -10
  15. package/dist/client/chat/agentkit-protocol.js +5 -1
  16. package/dist/client/chat/runtime.d.ts +3 -1
  17. package/dist/client/chat/runtime.js +101 -43
  18. package/dist/client/use-action.d.ts +1 -0
  19. package/dist/client/use-action.js +2 -1
  20. package/dist/collab/routes.d.ts +1 -1
  21. package/dist/collab/struct-routes.d.ts +1 -1
  22. package/dist/deploy/build.js +1 -1
  23. package/dist/deploy/workspace-deploy.js +65 -6
  24. package/dist/file-upload/actions/upload-image.d.ts +3 -3
  25. package/dist/file-upload/registry.d.ts +7 -1
  26. package/dist/file-upload/registry.js +25 -7
  27. package/dist/mcp/build-server.js +64 -26
  28. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  29. package/dist/notifications/routes.d.ts +1 -1
  30. package/dist/observability/metrics.d.ts +5 -0
  31. package/dist/observability/metrics.js +62 -7
  32. package/dist/observability/routes.d.ts +4 -4
  33. package/dist/org/handlers.d.ts +1 -1
  34. package/dist/progress/routes.d.ts +1 -1
  35. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  36. package/dist/resources/handlers.d.ts +1 -1
  37. package/dist/secrets/index.d.ts +1 -1
  38. package/dist/secrets/index.js +1 -1
  39. package/dist/secrets/storage.d.ts +1 -0
  40. package/dist/secrets/storage.js +8 -0
  41. package/dist/server/action-routes.js +1 -1
  42. package/dist/server/agent-engine-api-key-route.d.ts +2 -1
  43. package/dist/server/agent-engine-api-key-route.js +66 -2
  44. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  45. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  46. package/dist/server/agent-run-context.js +21 -1
  47. package/dist/server/agent-teams-run-queue.js +2 -1
  48. package/dist/server/auth.js +1 -0
  49. package/dist/server/core-routes-plugin.d.ts +1 -1
  50. package/dist/server/core-routes-plugin.js +12 -0
  51. package/dist/server/http-response-telemetry.js +37 -8
  52. package/dist/server/realtime-token.d.ts +1 -1
  53. package/dist/shared/inline-bytes.d.ts +29 -0
  54. package/dist/shared/inline-bytes.js +170 -0
  55. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  56. package/dist/shared/mcp-embed-headers.js +1 -1
  57. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  58. package/dist/testing.d.ts +1 -0
  59. package/dist/testing.js +1 -0
  60. package/dist/tracking/registry.d.ts +3 -2
  61. package/dist/tracking/registry.js +12 -3
  62. package/dist/triggers/actions/manage-automation.d.ts +3 -3
  63. package/dist/vite/client.js +6 -1
  64. package/docs/content/observability.mdx +15 -10
  65. package/docs/content/tracking.mdx +1 -1
  66. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import { AgentKitRunSlotBusyError } from "@agent-native/agentkit/client";
2
+ import { isInlineDataUrl, } from "@agent-native/agentkit/protocol";
2
3
  import { AUTO_CONTINUE_OF_RUN_METADATA_KEY, CONTINUE_OF_RUN_METADATA_KEY, } from "../../agent/auto-continue.js";
3
4
  import { agentEngineStatusUrlForChatApi, requireAgentEngineConfiguredForDispatch, } from "../agent-engine-readiness.js";
4
5
  import { getOrCreateAnalyticsSessionId } from "../analytics-session.js";
@@ -667,12 +668,39 @@ export function createHttpAgentChatRuntime(options) {
667
668
  };
668
669
  return runtime;
669
670
  }
671
+ /**
672
+ * What an earlier user turn's file or image becomes in replayed history: its
673
+ * name, type, and durable URL, so a later turn can still refer to it. Inline
674
+ * bytes are never replayed.
675
+ */
676
+ function historyAttachmentStub(message, part) {
677
+ if (message.role !== "user")
678
+ return undefined;
679
+ if (part.type !== "file" && part.type !== "image")
680
+ return undefined;
681
+ const name = part.type === "file" ? part.filename : part.alt;
682
+ const url = typeof part.url === "string" && !isInlineDataUrl(part.url)
683
+ ? part.url
684
+ : undefined;
685
+ return `[attached: ${[name || part.type, part.mediaType, url].filter(Boolean).join(" ")}]`;
686
+ }
687
+ /** Reasoning is the model's own scratch work and is never replayed as history. */
670
688
  function runtimeMessageText(message) {
671
689
  return message.content
672
- .map((part) => part.type === "text" || part.type === "reasoning" ? part.text : "")
690
+ .map((part) => part.type === "text"
691
+ ? part.text
692
+ : (historyAttachmentStub(message, part) ?? ""))
673
693
  .filter(Boolean)
674
694
  .join("\n");
675
695
  }
696
+ function isUserAsk(message) {
697
+ return (message.role === "user" &&
698
+ message.content.some((part) => (part.type === "text" && part.text.trim()) ||
699
+ historyAttachmentStub(message, part) !== undefined));
700
+ }
701
+ function hasHistoryAttachment(message) {
702
+ return message.content.some((part) => historyAttachmentStub(message, part) !== undefined);
703
+ }
676
704
  function isSyntheticToolHistoryOmissionMessage(message) {
677
705
  return (message.role === "assistant" &&
678
706
  (message.id === AGENTKIT_TOOL_HISTORY_OMISSION_MESSAGE_ID_PREFIX ||
@@ -719,6 +747,9 @@ const MAX_STRUCTURED_HISTORY_TEXT_SOURCE_PARTS = MAX_TOOL_HISTORY_CALLS * 2;
719
747
  const MAX_STRUCTURED_HISTORY_SOURCE_PARTS = MAX_STRUCTURED_HISTORY_TOOL_SOURCE_PARTS +
720
748
  MAX_STRUCTURED_HISTORY_TEXT_SOURCE_PARTS;
721
749
  const MAX_STRUCTURED_HISTORY_SOURCE_MESSAGES = 1024;
750
+ const MAX_PINNED_ATTACHMENT_PROMPTS = 32;
751
+ /** Above every other candidate, so the byte budget drops pinned asks last. */
752
+ const PINNED_USER_PROMPT_TEXT_PRIORITY = 4;
722
753
  const MAX_STRUCTURED_HISTORY_SOURCE_SCAN_STEPS = MAX_STRUCTURED_HISTORY_SOURCE_PARTS * 4;
723
754
  const TOOL_INPUT_OMISSION_TEXT = "Tool input omitted from history because it could not be serialized.";
724
755
  const TOOL_INPUT_SIZE_OMISSION_TEXT = "Tool input omitted from history because it exceeds 64 KiB.";
@@ -1177,7 +1208,7 @@ function runtimeMessageTextMatches(message, expected) {
1177
1208
  return { matches: true, complete: false };
1178
1209
  }
1179
1210
  scannedParts++;
1180
- if (part.type !== "text" && part.type !== "reasoning")
1211
+ if (part.type !== "text")
1181
1212
  continue;
1182
1213
  if (!part.text)
1183
1214
  continue;
@@ -1229,16 +1260,19 @@ function boundedStructuredHistorySources(messages, currentPrompt, supplementalMe
1229
1260
  }
1230
1261
  }
1231
1262
  let firstUserPromptMessageIndex;
1263
+ const attachmentPromptMessageIndices = [];
1232
1264
  const historyWindowStart = Math.max(0, historyMessages.length - MAX_STRUCTURED_HISTORY_SOURCE_MESSAGES);
1233
1265
  for (let index = historyWindowStart; index < historyMessages.length; index++) {
1234
1266
  if (index === currentPromptMessageIndex)
1235
1267
  continue;
1236
1268
  const message = historyMessages[index];
1237
- if (message.role === "user" &&
1238
- message.content.some((part) => (part.type === "text" || part.type === "reasoning") &&
1239
- part.text.trim())) {
1269
+ if (message.role !== "user")
1270
+ continue;
1271
+ if (firstUserPromptMessageIndex === undefined && isUserAsk(message)) {
1240
1272
  firstUserPromptMessageIndex = index;
1241
- break;
1273
+ }
1274
+ if (hasHistoryAttachment(message)) {
1275
+ attachmentPromptMessageIndices.push(index);
1242
1276
  }
1243
1277
  }
1244
1278
  let previousUserPromptMessageIndex;
@@ -1246,25 +1280,43 @@ function boundedStructuredHistorySources(messages, currentPrompt, supplementalMe
1246
1280
  for (let index = historyMessages.length - 1; index >= 0; index--) {
1247
1281
  if (index === currentPromptMessageIndex)
1248
1282
  continue;
1249
- const message = historyMessages[index];
1250
- if (message.role === "user" &&
1251
- message.content.some((part) => (part.type === "text" || part.type === "reasoning") &&
1252
- part.text.trim())) {
1283
+ if (isUserAsk(historyMessages[index])) {
1253
1284
  previousUserPromptMessageIndex = index;
1254
1285
  break;
1255
1286
  }
1256
1287
  }
1257
1288
  }
1258
- const protectedUserPromptMessageIndices = new Set([firstUserPromptMessageIndex, previousUserPromptMessageIndex].filter((index) => index !== undefined));
1289
+ // The first ask ("LinkedIn ad 1200x627 PNG") and every turn that attached a
1290
+ // file stay in the window however long the agentic tail after them grows.
1291
+ // ponytail: only the newest 32 attachment turns are pinned; older ones fall
1292
+ // back to the newest-first window.
1293
+ const pinnedUserPromptMessageIndices = new Set([
1294
+ firstUserPromptMessageIndex,
1295
+ previousUserPromptMessageIndex,
1296
+ ...attachmentPromptMessageIndices.slice(-MAX_PINNED_ATTACHMENT_PROMPTS),
1297
+ ].filter((index) => index !== undefined));
1259
1298
  const regularTextPartLimit = MAX_STRUCTURED_HISTORY_TEXT_SOURCE_PARTS -
1260
- protectedUserPromptMessageIndices.size;
1299
+ pinnedUserPromptMessageIndices.size;
1261
1300
  const selectedReversed = [];
1301
+ const pinnedPromptsAdded = new Set();
1262
1302
  let selectedToolPartCount = 0;
1263
1303
  let selectedTextPartCount = 0;
1264
1304
  let scannedPartCount = 0;
1265
1305
  let visitedMessageCount = 0;
1266
1306
  let toolBoundary;
1267
1307
  let stop = false;
1308
+ const addPinnedPromptText = (parts, messageIndex) => {
1309
+ pinnedPromptsAdded.add(messageIndex);
1310
+ const text = runtimeMessageText(historyMessages[messageIndex]);
1311
+ if (!text.trim())
1312
+ return;
1313
+ if (selectedTextPartCount >= MAX_STRUCTURED_HISTORY_TEXT_SOURCE_PARTS) {
1314
+ omitted = true;
1315
+ return;
1316
+ }
1317
+ selectedTextPartCount++;
1318
+ parts.push({ type: "text", text });
1319
+ };
1268
1320
  const visitMessage = (message, list, messageIndex) => {
1269
1321
  if (visitedMessageCount >= MAX_STRUCTURED_HISTORY_SOURCE_MESSAGES) {
1270
1322
  omitted = true;
@@ -1280,12 +1332,12 @@ function boundedStructuredHistorySources(messages, currentPrompt, supplementalMe
1280
1332
  visitedMessageCount++;
1281
1333
  if (message.role !== "user" && message.role !== "assistant")
1282
1334
  return;
1283
- const isFirstUserPrompt = list === "messages" && messageIndex === firstUserPromptMessageIndex;
1284
- const isPreviousUserPrompt = list === "messages" && messageIndex === previousUserPromptMessageIndex;
1285
- const isProtectedUserPrompt = isFirstUserPrompt || isPreviousUserPrompt;
1335
+ const isPinnedUserPrompt = list === "messages" && pinnedUserPromptMessageIndices.has(messageIndex);
1286
1336
  const partsReversed = [];
1287
- let protectedPromptTextAdded = false;
1288
1337
  for (let partIndex = message.content.length - 1; partIndex >= 0; partIndex--) {
1338
+ const part = message.content[partIndex];
1339
+ if (part.type === "reasoning")
1340
+ continue;
1289
1341
  if (scannedPartCount >= MAX_STRUCTURED_HISTORY_SOURCE_SCAN_STEPS) {
1290
1342
  omitted = true;
1291
1343
  toolHistoryOmitted = true;
@@ -1294,30 +1346,15 @@ function boundedStructuredHistorySources(messages, currentPrompt, supplementalMe
1294
1346
  break;
1295
1347
  }
1296
1348
  scannedPartCount++;
1297
- const part = message.content[partIndex];
1349
+ const attachmentStub = historyAttachmentStub(message, part);
1298
1350
  const isToolPart = (part.type === "tool-call" && message.role === "assistant") ||
1299
1351
  part.type === "tool-result";
1300
- const isTextPart = part.type === "text" || part.type === "reasoning";
1352
+ const isTextPart = part.type === "text" || attachmentStub !== undefined;
1301
1353
  if (!isToolPart && !isTextPart)
1302
1354
  continue;
1303
- if (isProtectedUserPrompt && isTextPart) {
1304
- if (!protectedPromptTextAdded) {
1305
- const text = message.content
1306
- .filter((candidate) => candidate.type === "text" || candidate.type === "reasoning")
1307
- .map((candidate) => candidate.type === "text" || candidate.type === "reasoning"
1308
- ? candidate.text
1309
- : "")
1310
- .join("\n");
1311
- if (text.trim()) {
1312
- if (selectedTextPartCount >= MAX_STRUCTURED_HISTORY_TEXT_SOURCE_PARTS) {
1313
- omitted = true;
1314
- }
1315
- else {
1316
- selectedTextPartCount++;
1317
- partsReversed.push({ type: "text", text });
1318
- }
1319
- }
1320
- protectedPromptTextAdded = true;
1355
+ if (isPinnedUserPrompt && isTextPart) {
1356
+ if (!pinnedPromptsAdded.has(messageIndex)) {
1357
+ addPinnedPromptText(partsReversed, messageIndex);
1321
1358
  }
1322
1359
  continue;
1323
1360
  }
@@ -1345,15 +1382,17 @@ function boundedStructuredHistorySources(messages, currentPrompt, supplementalMe
1345
1382
  }
1346
1383
  selectedTextPartCount++;
1347
1384
  }
1348
- partsReversed.push(part);
1385
+ partsReversed.push(attachmentStub === undefined
1386
+ ? part
1387
+ : { type: "text", text: attachmentStub });
1349
1388
  }
1350
1389
  if (partsReversed.length) {
1351
1390
  selectedReversed.push({
1352
1391
  message,
1353
1392
  parts: partsReversed.reverse(),
1354
1393
  priority: list === "supplemental",
1355
- textPriority: isPreviousUserPrompt
1356
- ? 2
1394
+ textPriority: isPinnedUserPrompt
1395
+ ? PINNED_USER_PROMPT_TEXT_PRIORITY
1357
1396
  : list === "supplemental"
1358
1397
  ? 1
1359
1398
  : 0,
@@ -1370,6 +1409,22 @@ function boundedStructuredHistorySources(messages, currentPrompt, supplementalMe
1370
1409
  continue;
1371
1410
  visitMessage(historyMessages[index], "messages", index);
1372
1411
  }
1412
+ // The newest-first scan stops long before the first turns of a long agentic
1413
+ // thread; the pinned asks it never reached are replayed anyway.
1414
+ for (const index of [...pinnedUserPromptMessageIndices].sort((left, right) => right - left)) {
1415
+ if (pinnedPromptsAdded.has(index))
1416
+ continue;
1417
+ const parts = [];
1418
+ addPinnedPromptText(parts, index);
1419
+ if (parts.length) {
1420
+ selectedReversed.push({
1421
+ message: historyMessages[index],
1422
+ parts,
1423
+ priority: false,
1424
+ textPriority: PINNED_USER_PROMPT_TEXT_PRIORITY,
1425
+ });
1426
+ }
1427
+ }
1373
1428
  return {
1374
1429
  messages: selectedReversed.reverse(),
1375
1430
  omitted,
@@ -1520,7 +1575,7 @@ function nativeStructuredHistoryFromMessages(messages, currentPrompt, supplement
1520
1575
  results = [];
1521
1576
  };
1522
1577
  for (const part of parts) {
1523
- if (part.type === "text" || part.type === "reasoning") {
1578
+ if (part.type === "text") {
1524
1579
  if (jsonStringByteLengthWithinLimit(part.text, MAX_ADDED_TOOL_HISTORY_BYTES) === undefined ||
1525
1580
  part.text.trim()) {
1526
1581
  flushResults();
@@ -2700,6 +2755,9 @@ export function createAgentNativeChatRuntime(options = {}) {
2700
2755
  // Only the continuation that names a stopped run continues it; an approval
2701
2756
  // or connection answered later is not another continuation of that run.
2702
2757
  const { [AUTO_CONTINUE_OF_RUN_METADATA_KEY]: _autoContinueOf, [CONTINUE_OF_RUN_METADATA_KEY]: _continueOf, ...previousMetadata } = previousTurn?.metadata ?? {};
2758
+ const turnToContinue = continuation.attachments?.length
2759
+ ? { ...previousTurn, attachments: continuation.attachments }
2760
+ : previousTurn;
2703
2761
  const approval = continuation.approval;
2704
2762
  const connection = continuation.connection;
2705
2763
  const messageStateKey = continuation.turnId ?? session.id;
@@ -2716,7 +2774,7 @@ export function createAgentNativeChatRuntime(options = {}) {
2716
2774
  if (messageState)
2717
2775
  messageState.message.connectionPending = false;
2718
2776
  return startTurn({
2719
- ...previousTurn,
2777
+ ...turnToContinue,
2720
2778
  prompt: continuation.prompt ??
2721
2779
  `The ${connection.id} connection is now available. Continue the requested work.`,
2722
2780
  metadata: {
@@ -2736,7 +2794,7 @@ export function createAgentNativeChatRuntime(options = {}) {
2736
2794
  if (messageState)
2737
2795
  messageState.message.approvalPending = false;
2738
2796
  return startTurn({
2739
- ...previousTurn,
2797
+ ...turnToContinue,
2740
2798
  prompt: continuation.prompt,
2741
2799
  metadata: {
2742
2800
  ...previousMetadata,
@@ -2759,7 +2817,7 @@ export function createAgentNativeChatRuntime(options = {}) {
2759
2817
  if (messageState)
2760
2818
  messageState.message.approvalPending = false;
2761
2819
  return startTurn({
2762
- ...previousTurn,
2820
+ ...turnToContinue,
2763
2821
  prompt: continuation.prompt ??
2764
2822
  "Approved. Go ahead and run the requested action.",
2765
2823
  metadata: {
@@ -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.
@@ -13,8 +13,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
13
13
  };
14
14
  }>>;
15
15
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -1,6 +1,6 @@
1
1
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
- ok?: undefined;
3
2
  error: string;
3
+ ok?: undefined;
4
4
  } | {
5
5
  error?: undefined;
6
6
  ok: boolean;
@@ -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,20 +33,20 @@ 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
  } | {
42
42
  url: string;
43
43
  id: string;
44
44
  provider: string;
45
- configured?: undefined;
46
45
  error?: undefined;
46
+ configured?: 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() {
@@ -894,16 +894,6 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
894
894
  (ctx?.orgId ?? undefined))) {
895
895
  throw new Error("The widget write grant must match the authenticated MCP user and organization.");
896
896
  }
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
897
  const revocationAnchorCandidate = ctx?.mcpCredentialIssuedAtMs;
908
898
  const revocationAnchorCreatedAtMs = typeof revocationAnchorCandidate === "number" &&
909
899
  Number.isSafeInteger(revocationAnchorCandidate)
@@ -913,18 +903,50 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
913
903
  revocationAnchorCreatedAtMs === undefined) {
914
904
  throw new Error("Directory widget sessions require a trusted MCP credential issue time.");
915
905
  }
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
- });
906
+ const mintTicket = async (capability) => {
907
+ const scope = capability
908
+ ? capability.mode === "write"
909
+ ? createMcpDirectoryWidgetWriteCapability(capability.value)
910
+ : createMcpDirectoryWidgetReadCapability(capability.value)
911
+ : typeof out.chrome === "string"
912
+ ? out.chrome
913
+ : null;
914
+ if (capability && !scope) {
915
+ throw new Error("Could not create a valid scoped capability for this MCP directory widget.");
916
+ }
917
+ return createEmbedSessionTicket({
918
+ ownerEmail,
919
+ orgId: ctx?.orgId,
920
+ targetPath,
921
+ scope,
922
+ ...(capability?.mode === "write" ? { ttlSeconds: 5 * 60 } : {}),
923
+ ...(capability ? { revocationAnchorCreatedAtMs } : {}),
924
+ });
925
+ };
926
+ const writeCapability = directoryWidget?.capability.mode === "write"
927
+ ? directoryWidget.capability
928
+ : undefined;
929
+ let ticket;
930
+ try {
931
+ ticket = await mintTicket(directoryWidget?.capability);
932
+ }
933
+ catch (error) {
934
+ if (!writeCapability)
935
+ throw error;
936
+ // The write grant is an upgrade over the read grant. Losing it must not
937
+ // also lose the widget's session ticket, which the shell cannot start without.
938
+ console.error("[mcp:directory] Could not mint the widget write grant; issuing a read-only widget session instead.", error);
939
+ const { appId, resourceUri, resourceIds, readActionArguments } = writeCapability.value;
940
+ ticket = await mintTicket({
941
+ mode: "read",
942
+ value: {
943
+ appId,
944
+ resourceUri,
945
+ resourceIds,
946
+ actionArguments: readActionArguments,
947
+ },
948
+ });
949
+ }
928
950
  const startPath = buildEmbedStartPath(ticket.ticket);
929
951
  const embedStartUrl = meta?.origin
930
952
  ? new URL(startPath, meta.origin).toString()
@@ -2441,11 +2463,24 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2441
2463
  mcpAppResourceCandidate !== null &&
2442
2464
  directoryWidget === undefined;
2443
2465
  const suppressDirectoryWidget = directoryCatalog && !hasVerifiedMcpUserIdentity(effectiveIdentity);
2466
+ // The host mounts a widget for this tool from its descriptor, with or
2467
+ // without a ticket in the result, so a withheld ticket must be visible.
2468
+ if (missingDirectoryWidgetCapability &&
2469
+ !suppressDirectoryWidget &&
2470
+ !mcpResultIsError) {
2471
+ console.warn(`[mcp:directory] ${name} returned no widget session ticket: ${typeof trustedCredentialIssuedAtMs === "number"
2472
+ ? "the result has no widget target or scoped actions"
2473
+ : "the credential carries no issue time"}.`);
2474
+ }
2444
2475
  const rawResultForClient = missingDirectoryWidgetCapability || suppressDirectoryWidget
2445
2476
  ? withoutMcpAppEmbedTicket(projectedRawResult)
2446
2477
  : mcpAppResourceCandidate
2447
2478
  ? await withServerMintedMcpAppEmbedStart(projectedRawResult, requestMeta, directoryLinkUrl, directoryWidget)
2448
2479
  : projectedRawResult;
2480
+ if (directoryWidget &&
2481
+ typeof metadataObject(rawResultForClient).embedStartUrl !== "string") {
2482
+ console.error(`[mcp:directory] ${name} built a widget capability but issued no session ticket.`);
2483
+ }
2449
2484
  const { value: actionResultForClient, images: resultImages, notes: resultImageNotes, } = extractAgentImagesFromActionResult(rawResultForClient);
2450
2485
  const textResultForClient = mcpResult
2451
2486
  ? projectedResultForClient
@@ -2996,12 +3031,15 @@ async function admitIssuedMcpCredential(credential, requestOrigin) {
2996
3031
  stored = lookup;
2997
3032
  }
2998
3033
  const orgId = credential.orgId !== undefined ? credential.orgId : stored?.orgId;
2999
- const credentialIssuedAtMs = credential.oauthClientId !== undefined
3000
- ? credential.grantCreatedAtMs
3001
- : typeof credential.issuedAt === "number" &&
3034
+ // Access tokens signed before grant times existed carry no claim, and the
3035
+ // directory widget refuses to mint a session without an issue time. Their
3036
+ // signed `iat` is the only anchor they have. `iat` moves on every refresh, so
3037
+ // it must stay the fallback and never outrank a signed grant time.
3038
+ const credentialIssuedAtMs = credential.grantCreatedAtMs ??
3039
+ (typeof credential.issuedAt === "number" &&
3002
3040
  Number.isSafeInteger(credential.issuedAt)
3003
3041
  ? credential.issuedAt * 1000
3004
- : undefined;
3042
+ : undefined);
3005
3043
  const mcpCredentialIssuedAtMs = credentialIssuedAtMs !== undefined &&
3006
3044
  Number.isSafeInteger(credentialIssuedAtMs)
3007
3045
  ? credentialIssuedAtMs