@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
@@ -45,6 +45,7 @@ import { ANALYTICS_CLIENT_PLATFORM_BODY_FIELD } from "../shared/analytics-platfo
45
45
  import { isContextDegraded, preloadedContextUnavailableNote, readReportedContextStatus, screenContextUnavailableNote, SELECTION_CONTEXT_UNAVAILABLE_NOTE, worstContextStatus, } from "../shared/context-status.js";
46
46
  import { parseBase64DataUrl } from "../shared/data-url.js";
47
47
  import { stripDiagnosticSnippets } from "../shared/diagnostic-snippet.js";
48
+ import { DurableAttachmentReferenceRequiredError, stripInlineBytes, } from "../shared/inline-bytes.js";
48
49
  import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasoningEffort, } from "../shared/reasoning-effort.js";
49
50
  import { SYNTHETIC_TRAFFIC_BETA_E2E, SYNTHETIC_TRAFFIC_HEADER, } from "../shared/test-traffic.js";
50
51
  import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
@@ -53,17 +54,17 @@ import { CONTINUE_UNAVAILABLE_CODE, } from "./auto-continue.js";
53
54
  import { clipHead } from "./clip-text.js";
54
55
  import { connectionRequiredMessage, priorConnectionContextNote, resolvePriorConnectionNote, } from "./connection-required-note.js";
55
56
  import { buildSystemManifestSections, readContextXraySystemSections, } from "./context-xray/manifest.js";
56
- import { AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRuntimeDiagnosticDetail, dispatchPathTargetsNetlifyBackgroundFunction, isAgentChatDurableBackgroundEnabled, isAgentChatForegroundSelfChainEnabled, isInBackgroundFunctionRuntime, resolveAgentChatProcessRunDispatchPath, shouldUseBackgroundFunctionTimeoutForWorker, } from "./durable-background.js";
57
+ import { AGENT_CHAT_BROWSER_SESSION_ID_FIELD, AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRuntimeDiagnosticDetail, dispatchPathTargetsNetlifyBackgroundFunction, isAgentChatDurableBackgroundEnabled, isAgentChatForegroundSelfChainEnabled, isInBackgroundFunctionRuntime, resolveAgentChatProcessRunDispatchPath, shouldUseBackgroundFunctionTimeoutForWorker, } from "./durable-background.js";
57
58
  import { applyContextXrayTransformForIteration } from "./engine/context-directives-transform.js";
58
59
  import { attemptContinuationDispatch } from "./engine/continuation-dispatch-retry.js";
59
60
  import { formatLlmCredentialErrorMessage, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, userFacingLlmCredentialError, } from "./engine/credential-errors.js";
60
- import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, PROVIDER_RATE_LIMITED_ERROR_CODE, PROVIDER_TRANSIENT_REJECTION_ERROR_CODE, } from "./engine/error-detail.js";
61
+ import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, isContextOverflowCode, isContextOverflowMessage, isInvalidAttachmentProviderMessage, isProviderConnectionErrorMessage, PROVIDER_RATE_LIMITED_ERROR_CODE, PROVIDER_TRANSIENT_REJECTION_ERROR_CODE, } from "./engine/error-detail.js";
61
62
  import { resolveEngine, explicitEngineName, registerBuiltinEngines, getStoredModelForEngine, normalizeModelForEngine, isResolvedEngineUsableForRequest, } from "./engine/index.js";
62
63
  import { limitProviderTools, MAX_PROVIDER_TOOLS, } from "./engine/limit-provider-tools.js";
63
64
  import { resolveEmptyResponseRetryMaxOutputTokens, resolveMainChatMaxOutputTokens, resolveMaxOutputTokensForEngine, } from "./engine/output-tokens.js";
64
65
  import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js";
65
66
  import { JOURNALED_TOOL_REPLAY_PREFIX, RECOVERED_TOOL_REPLAY_PREFIX, loadedSkillPagesContext, loadPriorTurnToolCallJournal, seedRepeatedToolErrorCountsFromJournal, seedRepeatedToolCallCountsFromJournal, } from "./engine/tool-call-journal-seed.js";
66
- import { backfillEngineMessagesToolResults, stringifyToolUseInputForGateway, unmatchedToolResultReplayText, } from "./engine/translate-anthropic.js";
67
+ import { backfillEngineMessagesToolResults, INTERRUPTED_TOOL_RESULT_MARKER, isInterruptedToolResult, stringifyToolUseInputForGateway, unmatchedToolResultReplayText, } from "./engine/translate-anthropic.js";
67
68
  import { EngineError } from "./engine/types.js";
68
69
  import { FOLLOW_UP_SUGGESTIONS_COMPLETION_SYSTEM_PROMPT, FOLLOW_UP_SUGGESTIONS_INSTRUCTION, FOLLOW_UP_SUGGESTIONS_MAX_OUTPUT_TOKENS, FOLLOW_UP_SUGGESTIONS_TOOL_NAME, buildFollowUpCompletionMessages, followUpSuggestionsTool, identifyFollowUpSuggestions, parseFollowUpSuggestions, } from "./follow-up-suggestions.js";
69
70
  import { filterHostedHarnessToolNames, normalizeHostedHarnessRuntime, } from "./harness/hosted.js";
@@ -1100,6 +1101,26 @@ const MAX_SELECTION_CONTEXT_CHARS = 8_000;
1100
1101
  const MAX_RESOURCE_INVENTORY_ITEMS = 40;
1101
1102
  const MAX_RESOURCE_INVENTORY_DESCRIPTION_CHARS = 160;
1102
1103
  const MAX_INLINE_SKILL_REFERENCE_CHARS = 40_000;
1104
+ const MAX_LOADED_SKILL_SLUGS = 16;
1105
+ const LOADED_SKILL_SLUG_PATTERN = /^skill-[a-z0-9]+(?:-+[a-z0-9]+)*$/;
1106
+ export function normalizeLoadedSkillSlugs(value) {
1107
+ if (!Array.isArray(value))
1108
+ return [];
1109
+ const slugs = value.filter((slug) => typeof slug === "string" &&
1110
+ slug.length <= 200 &&
1111
+ LOADED_SKILL_SLUG_PATTERN.test(slug));
1112
+ return [...new Set(slugs.reverse())]
1113
+ .slice(0, MAX_LOADED_SKILL_SLUGS)
1114
+ .reverse();
1115
+ }
1116
+ function skillPageIntactInHistory(messages, slug, page) {
1117
+ const ending = page.trimEnd().slice(-200);
1118
+ return messages.some((message) => message.content.some((part) => part.type === "tool-result" &&
1119
+ part.toolName === "docs-search" &&
1120
+ !part.isError &&
1121
+ part.toolInput.includes(JSON.stringify(slug)) &&
1122
+ part.content.includes(ending)));
1123
+ }
1103
1124
  export function resolveSourceSweepToolCallThreshold() {
1104
1125
  return getAppConfig().agent.sourceSweepToolCallThreshold;
1105
1126
  }
@@ -1139,59 +1160,9 @@ function limitInventoryLines(lines, label) {
1139
1160
  function generateRunId() {
1140
1161
  return `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1141
1162
  }
1142
- export class DurableAttachmentReferenceRequiredError extends Error {
1143
- code = "attachment_storage_required";
1144
- constructor() {
1145
- super("An attachment has inline bytes but no durable file URL. Configure file storage and retry this background run.");
1146
- this.name = "DurableAttachmentReferenceRequiredError";
1147
- }
1148
- }
1149
- function isDataUrlReference(value) {
1150
- return typeof value === "string" && /^\s*data:/i.test(value);
1151
- }
1152
- function hasDurableAttachmentUrl(value) {
1153
- return (typeof value === "string" &&
1154
- value.trim().length > 0 &&
1155
- !isDataUrlReference(value));
1156
- }
1157
- function sanitizeDurableAttachment(value, attachmentContext = false) {
1158
- if (Array.isArray(value)) {
1159
- return value.map((item) => sanitizeDurableAttachment(item));
1160
- }
1161
- if (!value || typeof value !== "object")
1162
- return value;
1163
- const item = value;
1164
- const isAttachment = attachmentContext ||
1165
- item.type === "image" ||
1166
- item.type === "file" ||
1167
- item.type === "document";
1168
- if (isAttachment &&
1169
- (isDataUrlReference(item.url) || isDataUrlReference(item.referenceUrl))) {
1170
- throw new DurableAttachmentReferenceRequiredError();
1171
- }
1172
- const hasInlineData = isAttachment && item.data !== undefined;
1173
- if (hasInlineData &&
1174
- !hasDurableAttachmentUrl(item.url) &&
1175
- !hasDurableAttachmentUrl(item.referenceUrl)) {
1176
- throw new DurableAttachmentReferenceRequiredError();
1177
- }
1178
- const sanitized = {};
1179
- for (const [key, child] of Object.entries(item)) {
1180
- if (key === "data" && hasInlineData)
1181
- continue;
1182
- if (key === "attachments" || key === "requestAttachments") {
1183
- sanitized[key] = Array.isArray(child)
1184
- ? child.map((attachment) => sanitizeDurableAttachment(attachment, true))
1185
- : sanitizeDurableAttachment(child);
1186
- }
1187
- else {
1188
- sanitized[key] = sanitizeDurableAttachment(child);
1189
- }
1190
- }
1191
- return sanitized;
1192
- }
1163
+ export { DurableAttachmentReferenceRequiredError };
1193
1164
  export function serializeDurableDispatchPayload(body) {
1194
- const payload = JSON.stringify(sanitizeDurableAttachment(body));
1165
+ const payload = JSON.stringify(stripInlineBytes(body, "reject"));
1195
1166
  if (typeof payload !== "string") {
1196
1167
  throw new TypeError("Durable dispatch payload could not be serialized");
1197
1168
  }
@@ -1550,6 +1521,51 @@ export function buildUserContentWithAttachments(opts) {
1550
1521
  });
1551
1522
  return userContent;
1552
1523
  }
1524
+ /** Images reach only a model that reads them; the model learns one was withheld. */
1525
+ function replaceImagesForModelWithoutVision(messages, model) {
1526
+ const note = `<chat-attachment-processing-error code="model-without-vision" model="${escapeAttachmentAttribute(model)}">An attached image was not sent because the active model cannot read images. Do not infer or describe its contents. Tell the user to switch to a vision-capable model if the image matters.</chat-attachment-processing-error>`;
1527
+ return messages.map((message) => message.content.some((part) => part.type === "image")
1528
+ ? {
1529
+ ...message,
1530
+ content: message.content.map((part) => part.type === "image"
1531
+ ? { type: "text", text: note }
1532
+ : part),
1533
+ }
1534
+ : message);
1535
+ }
1536
+ /**
1537
+ * Every engine's stop error passes through here with the request in hand, so
1538
+ * this is where a provider's rejection of an inline attachment becomes the
1539
+ * non-retryable `invalid_attachment`: resending the same bytes fails the same
1540
+ * way, and an `http_400` lets Retry loop on it.
1541
+ */
1542
+ function engineStopError(event, messages) {
1543
+ const error = event.error ?? "Engine stream error";
1544
+ const parts = messages.flatMap((message) => message.content);
1545
+ const hasInlineAttachment = parts.some((part) => part.type === "image" ||
1546
+ part.type === "file" ||
1547
+ (part.type === "tool-result" && (part.images?.length ?? 0) > 0));
1548
+ const attachmentRejected = event.errorCode === "invalid_attachment" ||
1549
+ (hasInlineAttachment &&
1550
+ (event.statusCode === 413 ||
1551
+ ((event.statusCode === undefined ||
1552
+ event.statusCode === 400 ||
1553
+ event.statusCode === 422) &&
1554
+ isInvalidAttachmentProviderMessage(error))));
1555
+ const names = parts.flatMap((part) => part.type === "file" && part.filename ? [`"${part.filename}"`] : []);
1556
+ return new EngineError(attachmentRejected
1557
+ ? `The model provider rejected an attachment in this request${names.length > 0 ? ` (${names.join(", ")})` : ""}: ${error}`
1558
+ : error, {
1559
+ errorCode: attachmentRejected ? "invalid_attachment" : event.errorCode,
1560
+ upgradeUrl: event.upgradeUrl,
1561
+ statusCode: event.statusCode,
1562
+ providerRetryable: attachmentRejected ? false : event.providerRetryable,
1563
+ contextOverflow: event.contextOverflow,
1564
+ requestId: event.requestId,
1565
+ requestShape: event.requestShape,
1566
+ retryAfterMs: event.retryAfterMs,
1567
+ });
1568
+ }
1553
1569
  export function appendRequestAttachmentContextToResumedHistory(messages, attachments) {
1554
1570
  if (!attachments?.length)
1555
1571
  return;
@@ -2448,7 +2464,6 @@ export function isCachedToolResultVisibleInContext(contextMessages, toolCall, ca
2448
2464
  }
2449
2465
  return false;
2450
2466
  }
2451
- const INTERRUPTED_TOOL_RESULT_MARKER = "Interrupted before this tool returned a result.";
2452
2467
  const INTERRUPTED_TOOL_LEDGER_RECOVERY_TIMEOUT_MS = 5_000;
2453
2468
  const MAX_IDENTICAL_TOOL_ERRORS = 3;
2454
2469
  export const MAX_SAME_ERROR_ACROSS_ARGUMENTS = 3;
@@ -2535,9 +2550,10 @@ function seedWriteToolInterruptionsFromHistory(messages, actions) {
2535
2550
  const call = pendingToolCalls.get(part.toolCallId);
2536
2551
  if (!call)
2537
2552
  continue;
2538
- if (typeof part.content === "string" &&
2539
- (part.content === INTERRUPTED_TOOL_RESULT_MARKER ||
2540
- (part.isError === true && isToolCallTimeoutResult(part.content)))) {
2553
+ if (isInterruptedToolResult(part.content) ||
2554
+ (part.isError === true &&
2555
+ typeof part.content === "string" &&
2556
+ isToolCallTimeoutResult(part.content))) {
2541
2557
  const key = toolCallCacheKey(call.name, call.input);
2542
2558
  interruptions.set(key, (interruptions.get(key) ?? 0) + 1);
2543
2559
  }
@@ -3744,7 +3760,8 @@ export async function runAgentLoop(opts) {
3744
3760
  const toolCallJournal = journalRead.status === "read" ? journalRead.toolCallJournal : null;
3745
3761
  const journaledPriorToolCalls = journalRead.status === "read" ? journalRead.priorToolCalls : [];
3746
3762
  const journaledPriorToolResults = journalRead.status === "read" ? journalRead.priorToolResults : [];
3747
- let loadedSkillsContext = "";
3763
+ let threadSkillPages = new Map();
3764
+ let currentJournalSkillBodies = new Map();
3748
3765
  const hasLoadedSkillPage = journaledPriorToolResults.some((result) => {
3749
3766
  const input = result.input && typeof result.input === "object"
3750
3767
  ? result.input
@@ -3755,14 +3772,29 @@ export async function runAgentLoop(opts) {
3755
3772
  input.slug.startsWith("skill-") &&
3756
3773
  result.content.startsWith("# Skill:"));
3757
3774
  });
3758
- if (isInternalContinuationTurn(messages) && hasLoadedSkillPage) {
3759
- const { loadAgentsBundle, getRuntimeSkillsForUser, skillDocsSlug } = await import("../server/agents-bundle.js");
3760
- const runtimeSkills = await getRuntimeSkillsForUser(await loadAgentsBundle(), opts.ownerEmail ?? getRequestUserEmail());
3761
- loadedSkillsContext = loadedSkillPagesContext(journaledPriorToolResults, new Set(runtimeSkills.map((skill) => skillDocsSlug(skill.meta.name))));
3762
- }
3763
- const continuationSystemPrompt = loadedSkillsContext
3764
- ? `${systemPrompt}\n\n${loadedSkillsContext}`
3765
- : systemPrompt;
3775
+ const reuseJournaledSkillPages = isInternalContinuationTurn(messages) && hasLoadedSkillPage;
3776
+ const threadSkillSlugs = opts.loadedSkillSlugs ?? [];
3777
+ if (reuseJournaledSkillPages || threadSkillSlugs.length > 0) {
3778
+ const skillUserEmail = opts.ownerEmail ?? getRequestUserEmail();
3779
+ const { loadSkillDocPages } = await import("../scripts/docs/search.js");
3780
+ threadSkillPages = await loadSkillDocPages(threadSkillSlugs, skillUserEmail);
3781
+ if (reuseJournaledSkillPages) {
3782
+ const { loadAgentsBundle, getRuntimeSkillsForUser, skillDocsSlug } = await import("../server/agents-bundle.js");
3783
+ const runtimeSkills = await getRuntimeSkillsForUser(await loadAgentsBundle(), skillUserEmail);
3784
+ currentJournalSkillBodies = new Map(runtimeSkills.map((skill) => [
3785
+ skillDocsSlug(skill.meta.name),
3786
+ skill.content,
3787
+ ]));
3788
+ }
3789
+ }
3790
+ // Dedupe against the messages the model actually receives: memory
3791
+ // compaction and retry trimming can remove a page that `messages` still has.
3792
+ const continuationSystemPromptFor = (sentMessages) => {
3793
+ const loadedSkillsContext = loadedSkillPagesContext(reuseJournaledSkillPages ? journaledPriorToolResults : [], threadSkillPages, currentJournalSkillBodies, (slug, page) => skillPageIntactInHistory(sentMessages, slug, page));
3794
+ return loadedSkillsContext
3795
+ ? `${systemPrompt}\n\n${loadedSkillsContext}`
3796
+ : systemPrompt;
3797
+ };
3766
3798
  toolCallHistory.push(...journaledPriorToolCalls);
3767
3799
  toolResultHistory.push(...journaledPriorToolResults);
3768
3800
  const unreadableJournalStop = journalRead.status === "unreadable" && isInternalContinuationTurn(messages)
@@ -3930,12 +3962,15 @@ export async function runAgentLoop(opts) {
3930
3962
  const { thinking: _thinking, ...anthropic } = providerOptions.anthropic;
3931
3963
  providerOptions = { ...providerOptions, anthropic };
3932
3964
  }
3965
+ const engineMessages = engine.capabilities.vision === false
3966
+ ? replaceImagesForModelWithoutVision(contextMessages, model)
3967
+ : contextMessages;
3933
3968
  const streamOpts = {
3934
3969
  model,
3935
3970
  systemPrompt: completingFollowUpSuggestions
3936
3971
  ? FOLLOW_UP_SUGGESTIONS_COMPLETION_SYSTEM_PROMPT
3937
- : continuationSystemPrompt,
3938
- messages: contextMessages,
3972
+ : continuationSystemPromptFor(engineMessages),
3973
+ messages: engineMessages,
3939
3974
  tools: loopBreakerCloseout
3940
3975
  ? []
3941
3976
  : completingFollowUpSuggestions
@@ -3954,7 +3989,7 @@ export async function runAgentLoop(opts) {
3954
3989
  providerOptions,
3955
3990
  };
3956
3991
  usage.llmCalls = (usage.llmCalls ?? 0) + 1;
3957
- notifyModelInputObserver(opts.onModelInput, contextMessages);
3992
+ notifyModelInputObserver(opts.onModelInput, engineMessages);
3958
3993
  const eventStream = engine.stream(streamOpts);
3959
3994
  let thinkingBuffer = "";
3960
3995
  const toolInputNames = new Map();
@@ -4224,16 +4259,7 @@ export async function runAgentLoop(opts) {
4224
4259
  else if (event.type === "stop") {
4225
4260
  terminalStopReason = event.reason;
4226
4261
  if (event.reason === "error") {
4227
- throw new EngineError(event.error ?? "Engine stream error", {
4228
- errorCode: event.errorCode,
4229
- upgradeUrl: event.upgradeUrl,
4230
- statusCode: event.statusCode,
4231
- providerRetryable: event.providerRetryable,
4232
- contextOverflow: event.contextOverflow,
4233
- requestId: event.requestId,
4234
- requestShape: event.requestShape,
4235
- retryAfterMs: event.retryAfterMs,
4236
- });
4262
+ throw engineStopError(event, engineMessages);
4237
4263
  }
4238
4264
  }
4239
4265
  if (hasNoProgressStalled()) {
@@ -6500,11 +6526,20 @@ async function readAdmittedQueuedMessagePromotion(opts) {
6500
6526
  ...(options ? { options: options } : {}),
6501
6527
  };
6502
6528
  }
6503
- function queuedPromotionAttachments(attachments) {
6529
+ /** @internal exported for unit tests only */
6530
+ export function queuedPromotionAttachments(attachments) {
6504
6531
  return attachments.map((value) => {
6505
6532
  const file = queuedPromotionRecord(value);
6533
+ // A saved queue entry is always a file part; the media type decides whether
6534
+ // the model sees pixels (image) or a file reference.
6535
+ const isImage = typeof file.mediaType === "string" &&
6536
+ file.mediaType
6537
+ .split(";", 1)[0]
6538
+ .trim()
6539
+ .toLowerCase()
6540
+ .startsWith("image/");
6506
6541
  return {
6507
- type: "file",
6542
+ type: isImage ? "image" : "file",
6508
6543
  name: file.name,
6509
6544
  ...(typeof file.fileId === "string" ? { id: file.fileId } : {}),
6510
6545
  ...(typeof file.mediaType === "string"
@@ -6944,7 +6979,7 @@ export function createProductionAgentHandler(options) {
6944
6979
  return { error: "Invalid request body" };
6945
6980
  }
6946
6981
  }
6947
- const { message, history: submittedHistory = [], structuredHistory: submittedStructuredHistory, references: submittedReferences = [], threadId, attachments, displayMessage, parentId, queuedMessageId, queuedMessageClaimId, agentKitMessageId: requestedAgentKitMessageId, internalContinuation: submittedInternalContinuation, autoContinueOfRunId: requestedAutoContinueOfRunId, continueOfRunId: requestedContinueOfRunId, turnId: requestTurnId, model: submittedModel, engine: submittedEngine, effort: submittedEffort, browserTabId, scope, harness: submittedHarness, trackInRunsTray, skipPendingSelectionContext, } = body;
6982
+ const { message, history: submittedHistory = [], structuredHistory: submittedStructuredHistory, loadedSkillSlugs: requestedLoadedSkillSlugs, references: submittedReferences = [], threadId, attachments, displayMessage, parentId, queuedMessageId, queuedMessageClaimId, agentKitMessageId: requestedAgentKitMessageId, internalContinuation: submittedInternalContinuation, autoContinueOfRunId: requestedAutoContinueOfRunId, continueOfRunId: requestedContinueOfRunId, turnId: requestTurnId, model: submittedModel, engine: submittedEngine, effort: submittedEffort, browserTabId, scope, harness: submittedHarness, trackInRunsTray, skipPendingSelectionContext, } = body;
6948
6983
  let requestHistory = submittedHistory;
6949
6984
  let requestStructuredHistory = submittedStructuredHistory;
6950
6985
  let requestReferences = submittedReferences;
@@ -7063,6 +7098,9 @@ export function createProductionAgentHandler(options) {
7063
7098
  });
7064
7099
  const mutableBody = body;
7065
7100
  if (!isBackgroundWorker) {
7101
+ // A client can submit this private field, so overwrite it at the boundary.
7102
+ mutableBody[AGENT_CHAT_BROWSER_SESSION_ID_FIELD] =
7103
+ getRequestContext()?.browserSessionId ?? null;
7066
7104
  delete mutableBody[ANALYTICS_CLIENT_PLATFORM_BODY_FIELD];
7067
7105
  }
7068
7106
  if (dispatchToBackground) {
@@ -7531,7 +7569,9 @@ export function createProductionAgentHandler(options) {
7531
7569
  const turnUsageLabel = normalizeUsageLabel(body.usageLabel);
7532
7570
  let experimentAssignments = [];
7533
7571
  try {
7534
- if (ownerEmail) {
7572
+ // A pinned request model suppresses the experiment's model override, so
7573
+ // resolving would assign (and persist) a variant this turn never ran.
7574
+ if (ownerEmail && !requestModelIsExplicit) {
7535
7575
  const { resolveActiveExperimentConfig } = await import("../observability/experiments.js");
7536
7576
  const expConfig = await resolveActiveExperimentConfig(ownerEmail);
7537
7577
  if (expConfig) {
@@ -8926,6 +8966,7 @@ export function createProductionAgentHandler(options) {
8926
8966
  maxIterations: loopSettings.maxIterations,
8927
8967
  maxRunInputTokens: loopSettings.maxRunInputTokens,
8928
8968
  priorTurnInputTokens: turnInputTokens,
8969
+ loadedSkillSlugs: normalizeLoadedSkillSlugs(requestedLoadedSkillSlugs),
8929
8970
  finalResponseGuard: options.finalResponseGuard,
8930
8971
  finalResponseGuardRequestText: messageToPersist,
8931
8972
  ...(resolvedRunSoftTimeoutMs > 0
@@ -5,6 +5,7 @@ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
5
5
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
6
6
  import { lockAndAssertServicePrincipalMayStartRun, prepareServicePrincipalRunStart, recordServicePrincipalDenial, } from "../org/service-principal-guard.js";
7
7
  import { captureError } from "../server/capture-error.js";
8
+ import { stripInlineBytes, stripInlineBytesFromJson, } from "../shared/inline-bytes.js";
8
9
  import { isRequestedStopAbortReason } from "./abort-reasons.js";
9
10
  import { admitAutoContinue, admitManualContinue, } from "./auto-continue.js";
10
11
  import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
@@ -1335,7 +1336,7 @@ function staleRecoveryDispatchPayload(payload) {
1335
1336
  return payload;
1336
1337
  }
1337
1338
  return JSON.stringify({
1338
- ...parsed,
1339
+ ...stripInlineBytes(parsed, "placeholder"),
1339
1340
  internalContinuation: true,
1340
1341
  });
1341
1342
  }
@@ -1823,7 +1824,13 @@ export async function insertRunEvent(runId, seq, eventData) {
1823
1824
  WHERE id = ? AND status <> 'running'
1824
1825
  )
1825
1826
  ON CONFLICT (run_id, seq) DO NOTHING`,
1826
- args: [runId, seq, Date.now(), eventData, runId],
1827
+ args: [
1828
+ runId,
1829
+ seq,
1830
+ Date.now(),
1831
+ stripInlineBytesFromJson(eventData, "placeholder"),
1832
+ runId,
1833
+ ],
1827
1834
  });
1828
1835
  }
1829
1836
  export const CHECKPOINT_TERMINAL_EVENT_SEQ = 1_000_000_000;
@@ -2479,6 +2486,11 @@ async function appendTerminalRunEvent(runId, event) {
2479
2486
  const nextSeq = last ? Number(last.seq ?? -1) + 1 : 0;
2480
2487
  await client.execute({
2481
2488
  sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data) VALUES (?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
2482
- args: [runId, nextSeq, Date.now(), JSON.stringify(event)],
2489
+ args: [
2490
+ runId,
2491
+ nextSeq,
2492
+ Date.now(),
2493
+ stripInlineBytesFromJson(JSON.stringify(event), "placeholder"),
2494
+ ],
2483
2495
  });
2484
2496
  }
@@ -4,10 +4,9 @@ import { isCredentialGapCodeAgentEvent, normalizeCodeAgentTranscript, } from "..
4
4
  import { stripAgentChatContextFromMessage } from "../shared/agent-chat-context.js";
5
5
  import { RUN_NOT_STARTED_METADATA_KEY, } from "../shared/agent-chat-run-not-started.js";
6
6
  import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE } from "./engine/error-detail.js";
7
- import { stringifyToolUseInputForGateway } from "./engine/translate-anthropic.js";
7
+ import { INTERRUPTED_TOOL_RESULT_MARKER as INTERRUPTED_TOOL_RESULT, stringifyToolUseInputForGateway, } from "./engine/translate-anthropic.js";
8
8
  import { parseFollowUpSuggestions } from "./follow-up-suggestions.js";
9
9
  import { isContinuationTerminalReason } from "./types.js";
10
- const INTERRUPTED_TOOL_RESULT = "Interrupted before this tool returned a result.";
11
10
  const INTERRUPTED_ACTIVITY_RESULT = "Stopped before this action started.";
12
11
  export const ASSISTANT_RUN_DURATION_METADATA_KEY = "agentNativeRunDurationMs";
13
12
  const MAX_STORED_ATTACHMENT_CHARS = 60_000;
@@ -163,6 +163,7 @@ export interface AgentChatRequest {
163
163
  displayMessage?: string;
164
164
  history?: AgentMessage[];
165
165
  structuredHistory?: AgentChatStructuredMessage[];
166
+ loadedSkillSlugs?: string[];
166
167
  references?: AgentChatReference[];
167
168
  threadId?: string;
168
169
  parentId?: string | null;
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
- error?: undefined;
143
142
  accepted: boolean;
144
143
  duplicate: boolean;
145
144
  eventId: string;
145
+ error?: undefined;
146
146
  }>;
@@ -7,6 +7,7 @@ import { createGetDb } from "../db/create-get-db.js";
7
7
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
8
8
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
9
9
  import { getRequestOrgId } from "../server/request-context.js";
10
+ import { stripInlineBytes } from "../shared/inline-bytes.js";
10
11
  import { resolveAccess } from "../sharing/access.js";
11
12
  import { registerShareableResource } from "../sharing/registry.js";
12
13
  import { roleSatisfies } from "../sharing/schema.js";
@@ -808,7 +809,7 @@ export async function forkThread(sourceId, ownerEmail, opts) {
808
809
  return null;
809
810
  }
810
811
  const id = opts?.id ?? generateId();
811
- const threadData = forkThreadData(source.threadData, id, snapshot?.fromMessageId);
812
+ const threadData = JSON.stringify(stripInlineBytes(JSON.parse(forkThreadData(source.threadData, id, snapshot?.fromMessageId)), "placeholder"));
812
813
  const now = Date.now();
813
814
  const title = source.title ? `${source.title} (fork)` : "";
814
815
  const client = getDbExec();
@@ -1096,7 +1097,10 @@ export async function updateThreadData(id, threadData, title, preview, messageCo
1096
1097
  preserveExistingTopLevelKeys: options.preserveExistingTopLevelKeys ?? true,
1097
1098
  onAnnotationConflict: (conflict) => annotationConflicts.push(conflict),
1098
1099
  });
1099
- nextThreadData = JSON.stringify(merged);
1100
+ // Client snapshots can predate their upload URL, so inline bytes become a
1101
+ // visible placeholder rather than failing the save; legacy rows are
1102
+ // scrubbed on their next write.
1103
+ nextThreadData = JSON.stringify(stripInlineBytes(merged, "placeholder"));
1100
1104
  nextMessageCount = countThreadMessages(merged, messageCount);
1101
1105
  const nextUpdatedAt = Math.max(Date.now(), current.updatedAt + 1);
1102
1106
  // Completion persistence can race the separate generated-title save.
@@ -75,6 +75,7 @@ export interface DesignConnectBridgeOptions {
75
75
  * ensuring a leaked preview credential cannot be promoted into write access.
76
76
  */
77
77
  export declare function deriveDesignPreviewToken(bridgeToken: string): string;
78
+ export declare function deriveDesignScopedReadOnlyPreviewToken(bridgeToken: string, designId: string): string;
78
79
  export declare function deriveDesignScopedLiveEditCapability(bridgeToken: string, designId: string): string;
79
80
  export declare function deriveDesignScopedLiveEditRegistrationCapability(bridgeToken: string, designId: string): string;
80
81
  export declare function deriveDesignPreviewAttestationSignature(bridgeToken: string, challenge: string): string;
@@ -88,6 +88,8 @@ async function resolveBridgeToken(rootPath, configuredToken, persist = true) {
88
88
  return bridgeToken;
89
89
  }
90
90
  const PREVIEW_TOKEN_DOMAIN = "agent-native-design-preview-v1\0";
91
+ const DESIGN_SCOPED_READ_ONLY_PREVIEW_TOKEN_DOMAIN = "agent-native-design-public-preview-v1\0";
92
+ const DESIGN_SCOPED_READ_ONLY_PREVIEW_TOKEN_PREFIX = "design-public-preview-v1";
91
93
  const LIVE_EDIT_CAPABILITY_DOMAIN = "agent-native-live-edit-design-v1\0";
92
94
  const LIVE_EDIT_REGISTRATION_CAPABILITY_DOMAIN = "agent-native-live-edit-registration-v1\0";
93
95
  const PREVIEW_ATTESTATION_DOMAIN = "agent-native-design-preview-attestation-v1\0";
@@ -108,6 +110,31 @@ export function deriveDesignPreviewToken(bridgeToken) {
108
110
  .update(bridgeToken)
109
111
  .digest("hex");
110
112
  }
113
+ export function deriveDesignScopedReadOnlyPreviewToken(bridgeToken, designId) {
114
+ const encodedDesignId = Buffer.from(designId, "utf8").toString("base64url");
115
+ const signature = crypto
116
+ .createHmac("sha256", bridgeToken)
117
+ .update(DESIGN_SCOPED_READ_ONLY_PREVIEW_TOKEN_DOMAIN)
118
+ .update(designId)
119
+ .digest("hex");
120
+ return `${DESIGN_SCOPED_READ_ONLY_PREVIEW_TOKEN_PREFIX}.${encodedDesignId}.${signature}`;
121
+ }
122
+ function designIdFromScopedReadOnlyPreviewToken(bridgeToken, token) {
123
+ const [prefix, encodedDesignId, signature, extra] = token.split(".");
124
+ if (prefix !== DESIGN_SCOPED_READ_ONLY_PREVIEW_TOKEN_PREFIX ||
125
+ !encodedDesignId ||
126
+ !signature ||
127
+ extra !== undefined) {
128
+ return null;
129
+ }
130
+ const designId = Buffer.from(encodedDesignId, "base64url").toString("utf8");
131
+ if (!designId ||
132
+ Buffer.from(designId, "utf8").toString("base64url") !== encodedDesignId ||
133
+ !constantTimeTokenMatches(token, deriveDesignScopedReadOnlyPreviewToken(bridgeToken, designId))) {
134
+ return null;
135
+ }
136
+ return designId;
137
+ }
111
138
  export function deriveDesignScopedLiveEditCapability(bridgeToken, designId) {
112
139
  return crypto
113
140
  .createHmac("sha256", bridgeToken)
@@ -1035,10 +1062,7 @@ function keyedFrameNavigation(requestUrl, referer, bridgeUrl) {
1035
1062
  const bridgeKey = bridgeKeyFromUrl(url);
1036
1063
  if (!bridgeKey)
1037
1064
  continue;
1038
- return {
1039
- bridgeKey,
1040
- previewToken: url.searchParams.get("previewToken")?.trim() ?? "",
1041
- };
1065
+ return { bridgeKey };
1042
1066
  }
1043
1067
  return null;
1044
1068
  }
@@ -2172,8 +2196,11 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2172
2196
  const providedPreviewToken = explicitPreviewToken ||
2173
2197
  readRequestCookie(req, PREVIEW_SESSION_COOKIE_NAME) ||
2174
2198
  "";
2175
- const previewTokenValid = constantTimeTokenMatches(providedPreviewToken, previewToken);
2176
- const explicitPreviewTokenValid = constantTimeTokenMatches(explicitPreviewToken, previewToken);
2199
+ const scopedReadOnlyPreviewDesignId = designIdFromScopedReadOnlyPreviewToken(bridgeToken, providedPreviewToken);
2200
+ const previewTokenValid = constantTimeTokenMatches(providedPreviewToken, previewToken) ||
2201
+ scopedReadOnlyPreviewDesignId !== null;
2202
+ const explicitPreviewTokenValid = constantTimeTokenMatches(explicitPreviewToken, previewToken) ||
2203
+ designIdFromScopedReadOnlyPreviewToken(bridgeToken, explicitPreviewToken) !== null;
2177
2204
  const corsApproved = configureBridgeCors(req, res, configuredOrigins, explicitPreviewTokenValid && readHeader(req, "origin") === "null");
2178
2205
  const requestOrigin = readHeader(req, "origin");
2179
2206
  const crossSiteRequest = readHeader(req, "sec-fetch-site").trim().toLowerCase() === "cross-site";
@@ -2183,6 +2210,17 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2183
2210
  : { ok: false, error: "origin is not allowed by this bridge" });
2184
2211
  return;
2185
2212
  }
2213
+ if (scopedReadOnlyPreviewDesignId !== null &&
2214
+ req.method &&
2215
+ !["GET", "HEAD"].includes(req.method) &&
2216
+ pathname !== "/live-edit-bridge") {
2217
+ req.resume();
2218
+ sendJson(res, 403, {
2219
+ ok: false,
2220
+ error: "design-scoped read-only preview tokens cannot mutate the app",
2221
+ });
2222
+ return;
2223
+ }
2186
2224
  const rejectInvalidPreviewToken = () => {
2187
2225
  if (previewTokenValid)
2188
2226
  return false;
@@ -2293,6 +2331,8 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2293
2331
  : "";
2294
2332
  if (!designId ||
2295
2333
  !bridgeKey ||
2334
+ (scopedReadOnlyPreviewDesignId !== null &&
2335
+ scopedReadOnlyPreviewDesignId !== designId) ||
2296
2336
  (!isValidLiveEditCapability(req, designId) &&
2297
2337
  !isValidLiveEditRegistrationCapability(req, designId))) {
2298
2338
  sendJson(res, 403, {
@@ -2583,13 +2623,16 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2583
2623
  : targetParsed.search;
2584
2624
  const targetPath = `${targetParsed.pathname}${targetSearch}` || "/";
2585
2625
  const html = injectLiveEditBridge(snapshot.html, new URL("/", manifest.bridgeUrl).toString(), includeEditorBridge ? editorBridgeScript : "", targetPath, requestedBridgeKey
2586
- ? { bridgeKey: requestedBridgeKey, previewToken }
2587
- : { previewToken });
2626
+ ? {
2627
+ bridgeKey: requestedBridgeKey,
2628
+ previewToken: providedPreviewToken,
2629
+ }
2630
+ : { previewToken: providedPreviewToken });
2588
2631
  sendText(res, snapshot.status >= 400 ? snapshot.status : 200, html, snapshot.contentType.includes("html")
2589
2632
  ? snapshot.contentType
2590
2633
  : "text/html; charset=utf-8", [
2591
2634
  ...snapshot.setCookieHeaders,
2592
- previewSessionSetCookie(previewToken),
2635
+ previewSessionSetCookie(providedPreviewToken),
2593
2636
  ], BRIDGE_FRAME_HEADERS);
2594
2637
  }
2595
2638
  catch (err) {
@@ -2852,9 +2895,7 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2852
2895
  const next = new URL("/live-edit", manifest.bridgeUrl);
2853
2896
  next.searchParams.set("url", targetUrl);
2854
2897
  next.searchParams.set("bridgeKey", keyed.bridgeKey);
2855
- if (keyed.previewToken) {
2856
- next.searchParams.set("previewToken", keyed.previewToken);
2857
- }
2898
+ next.searchParams.set("previewToken", providedPreviewToken);
2858
2899
  res.writeHead(302, {
2859
2900
  location: next.toString(),
2860
2901
  ...BRIDGE_FRAME_HEADERS,
@@ -2901,10 +2942,16 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2901
2942
  ? appendQueryPair(search, FRAME_BRIDGE_KEY_PARAM, keyed.bridgeKey)
2902
2943
  : search}` || "/");
2903
2944
  })(), keyed
2904
- ? { bridgeKey: keyed.bridgeKey, previewToken }
2945
+ ? {
2946
+ bridgeKey: keyed.bridgeKey,
2947
+ previewToken: providedPreviewToken,
2948
+ }
2905
2949
  : method === "GET"
2906
- ? { recoverTargetUrl: targetUrl, previewToken }
2907
- : { previewToken }))
2950
+ ? {
2951
+ recoverTargetUrl: targetUrl,
2952
+ previewToken: providedPreviewToken,
2953
+ }
2954
+ : { previewToken: providedPreviewToken }))
2908
2955
  : proxied.body;
2909
2956
  const headStylesheetProbe = method === "HEAD" &&
2910
2957
  previewTokenValid &&
@@ -2921,11 +2968,11 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2921
2968
  const shouldRewriteOpaqueFrameResources = previewTokenValid &&
2922
2969
  (opaqueFrameStylesheet || opaqueFrameJavaScript);
2923
2970
  const rewrittenResourceText = opaqueFrameJavaScript
2924
- ? addOpaqueFrameJavaScriptResourceTokens(responseText, previewToken)
2971
+ ? addOpaqueFrameJavaScriptResourceTokens(responseText, providedPreviewToken)
2925
2972
  : responseText;
2926
2973
  const opaqueFrameResponseBody = shouldRewriteOpaqueFrameResources
2927
2974
  ? Buffer.from(opaqueFrameStylesheet
2928
- ? addOpaqueFrameResourceTokens(rewrittenResourceText, previewToken)
2975
+ ? addOpaqueFrameResourceTokens(rewrittenResourceText, providedPreviewToken)
2929
2976
  : rewrittenResourceText)
2930
2977
  : responseBody;
2931
2978
  const advertisedContentLength = method === "HEAD" && !shouldRewriteOpaqueFrameResources
@@ -2973,10 +3020,16 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2973
3020
  const isViteHmrSocket = readHeader(req, "sec-websocket-protocol")
2974
3021
  .split(",")
2975
3022
  .some((protocol) => protocol.trim() === "vite-hmr");
2976
- if (!constantTimeTokenMatches(providedPreviewToken, previewToken)) {
3023
+ const scopedReadOnlyPreviewDesignId = designIdFromScopedReadOnlyPreviewToken(bridgeToken, providedPreviewToken);
3024
+ if (!constantTimeTokenMatches(providedPreviewToken, previewToken) &&
3025
+ scopedReadOnlyPreviewDesignId === null) {
2977
3026
  clientSocket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
2978
3027
  return;
2979
3028
  }
3029
+ if (scopedReadOnlyPreviewDesignId !== null && !isViteHmrSocket) {
3030
+ clientSocket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
3031
+ return;
3032
+ }
2980
3033
  let targetUrl;
2981
3034
  try {
2982
3035
  targetUrl = new URL(resolvePreviewProxyUrl(manifest.devServerUrl, `${requestUrl.pathname}${stripPreviewTokenQueryParam(requestUrl.search)}`));
@@ -3031,7 +3084,7 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
3031
3084
  const payload = webSocketDataBuffer(data);
3032
3085
  const rewritten = isBinary
3033
3086
  ? undefined
3034
- : rewriteViteHmrUpdate(payload, previewToken);
3087
+ : rewriteViteHmrUpdate(payload, providedPreviewToken);
3035
3088
  browserSocket.send(rewritten ?? payload, { binary: isBinary });
3036
3089
  });
3037
3090
  browserSocket.once("close", () => upstreamSocket.terminate());