@superblocksteam/gateway 2.0.177 → 2.0.178-next.0

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 (221) hide show
  1. package/README.md +52 -19
  2. package/dist/agents/hosted-boundary.d.ts +36 -0
  3. package/dist/agents/hosted-boundary.d.ts.map +1 -0
  4. package/dist/agents/hosted-boundary.js +66 -0
  5. package/dist/agents/hosted-boundary.js.map +1 -0
  6. package/dist/agents/resolve-orchestrator-url.d.ts +8 -0
  7. package/dist/agents/resolve-orchestrator-url.d.ts.map +1 -1
  8. package/dist/agents/resolve-orchestrator-url.js +5 -0
  9. package/dist/agents/resolve-orchestrator-url.js.map +1 -1
  10. package/dist/capabilities/integration-metadata.d.ts.map +1 -1
  11. package/dist/capabilities/integration-metadata.js +12 -1
  12. package/dist/capabilities/integration-metadata.js.map +1 -1
  13. package/dist/capabilities/lifecycle.d.ts +1 -6
  14. package/dist/capabilities/lifecycle.d.ts.map +1 -1
  15. package/dist/capabilities/lifecycle.js +339 -21
  16. package/dist/capabilities/lifecycle.js.map +1 -1
  17. package/dist/capabilities/live-state-attaches.d.ts +28 -0
  18. package/dist/capabilities/live-state-attaches.d.ts.map +1 -1
  19. package/dist/capabilities/live-state-attaches.js +33 -0
  20. package/dist/capabilities/live-state-attaches.js.map +1 -1
  21. package/dist/capabilities/query-integration.d.ts.map +1 -1
  22. package/dist/capabilities/query-integration.js +12 -1
  23. package/dist/capabilities/query-integration.js.map +1 -1
  24. package/dist/capabilities/tool-permission-reply.d.ts.map +1 -1
  25. package/dist/capabilities/tool-permission-reply.js.map +1 -1
  26. package/dist/capabilities/types.d.ts +3 -4
  27. package/dist/capabilities/types.d.ts.map +1 -1
  28. package/dist/capabilities/types.js.map +1 -1
  29. package/dist/capture/browser-contract.d.ts +1 -18
  30. package/dist/capture/browser-contract.d.ts.map +1 -1
  31. package/dist/capture/browser-contract.js +1 -13
  32. package/dist/capture/browser-contract.js.map +1 -1
  33. package/dist/capture/browser-instructions.d.ts +3 -3
  34. package/dist/capture/browser-instructions.d.ts.map +1 -1
  35. package/dist/capture/browser-instructions.js +4 -5
  36. package/dist/capture/browser-instructions.js.map +1 -1
  37. package/dist/capture/capture-library-probe.d.ts +0 -1
  38. package/dist/capture/capture-library-probe.d.ts.map +1 -1
  39. package/dist/capture/capture-library-probe.js +1 -1
  40. package/dist/capture/capture-library-probe.js.map +1 -1
  41. package/dist/capture/capture-library.d.ts +2 -1
  42. package/dist/capture/capture-library.d.ts.map +1 -1
  43. package/dist/capture/classify-library-paint.d.ts +2 -1
  44. package/dist/capture/classify-library-paint.d.ts.map +1 -1
  45. package/dist/capture/library-parent-script.d.ts +3 -2
  46. package/dist/capture/library-parent-script.d.ts.map +1 -1
  47. package/dist/capture/mode.d.ts +2 -3
  48. package/dist/capture/mode.d.ts.map +1 -1
  49. package/dist/capture/mode.js +2 -3
  50. package/dist/capture/mode.js.map +1 -1
  51. package/dist/config.d.ts +21 -1
  52. package/dist/config.d.ts.map +1 -1
  53. package/dist/config.js +3 -0
  54. package/dist/config.js.map +1 -1
  55. package/dist/debug.js.map +1 -1
  56. package/dist/deps.d.ts +3 -3
  57. package/dist/health/readiness.d.ts +2 -1
  58. package/dist/health/readiness.d.ts.map +1 -1
  59. package/dist/http/main.d.ts +3 -0
  60. package/dist/http/main.d.ts.map +1 -0
  61. package/dist/http/main.js +11 -0
  62. package/dist/http/main.js.map +1 -0
  63. package/dist/http/serve.d.ts +26 -0
  64. package/dist/http/serve.d.ts.map +1 -0
  65. package/dist/http/serve.js +298 -0
  66. package/dist/http/serve.js.map +1 -0
  67. package/dist/integrations/filters.d.ts.map +1 -1
  68. package/dist/integrations/filters.js +1 -16
  69. package/dist/integrations/filters.js.map +1 -1
  70. package/dist/integrations/map.d.ts +0 -5
  71. package/dist/integrations/map.d.ts.map +1 -1
  72. package/dist/integrations/map.js +1 -1
  73. package/dist/integrations/map.js.map +1 -1
  74. package/dist/internal-ingress.d.ts +47 -0
  75. package/dist/internal-ingress.d.ts.map +1 -0
  76. package/dist/internal-ingress.js +90 -0
  77. package/dist/internal-ingress.js.map +1 -0
  78. package/dist/orchestrator/client.d.ts +11 -5
  79. package/dist/orchestrator/client.d.ts.map +1 -1
  80. package/dist/orchestrator/client.js +14 -3
  81. package/dist/orchestrator/client.js.map +1 -1
  82. package/dist/playwright/ensure-chromium.js.map +1 -1
  83. package/dist/preview/capture-screenshot.d.ts +2 -2
  84. package/dist/preview/capture-screenshot.d.ts.map +1 -1
  85. package/dist/preview/capture-screenshot.js +2 -2
  86. package/dist/preview/capture-screenshot.js.map +1 -1
  87. package/dist/process/fault-barrier.d.ts +4 -3
  88. package/dist/process/fault-barrier.d.ts.map +1 -1
  89. package/dist/sabs/app-state.d.ts +2 -13
  90. package/dist/sabs/app-state.d.ts.map +1 -1
  91. package/dist/sabs/app-state.js +7 -2
  92. package/dist/sabs/app-state.js.map +1 -1
  93. package/dist/sabs/awaited-decision.d.ts.map +1 -1
  94. package/dist/sabs/browser-context.d.ts +4 -1
  95. package/dist/sabs/browser-context.d.ts.map +1 -1
  96. package/dist/sabs/browser-context.js +1 -0
  97. package/dist/sabs/browser-context.js.map +1 -1
  98. package/dist/sabs/editor-client-methods.d.ts.map +1 -1
  99. package/dist/sabs/editor-client-methods.js +1 -1
  100. package/dist/sabs/editor-client-methods.js.map +1 -1
  101. package/dist/sabs/editor-socket.d.ts.map +1 -1
  102. package/dist/sabs/editor-socket.js.map +1 -1
  103. package/dist/sabs/session-peer.d.ts +13 -1
  104. package/dist/sabs/session-peer.d.ts.map +1 -1
  105. package/dist/sabs/session-peer.js.map +1 -1
  106. package/dist/sabs/streamed-reply.d.ts.map +1 -1
  107. package/dist/sabs/streamed-reply.js.map +1 -1
  108. package/dist/sabs/turn-collector.d.ts.map +1 -1
  109. package/dist/sabs/turn-collector.js.map +1 -1
  110. package/dist/sabs/websocket-session-peer.d.ts +17 -1
  111. package/dist/sabs/websocket-session-peer.d.ts.map +1 -1
  112. package/dist/sabs/websocket-session-peer.js +89 -15
  113. package/dist/sabs/websocket-session-peer.js.map +1 -1
  114. package/dist/server/client.d.ts +48 -0
  115. package/dist/server/client.d.ts.map +1 -1
  116. package/dist/server/client.js +91 -173
  117. package/dist/server/client.js.map +1 -1
  118. package/dist/server/identity-reads.d.ts +5 -3
  119. package/dist/server/identity-reads.d.ts.map +1 -1
  120. package/dist/server/identity-reads.js +31 -4
  121. package/dist/server/identity-reads.js.map +1 -1
  122. package/dist/start.d.ts +5 -1
  123. package/dist/start.d.ts.map +1 -1
  124. package/dist/start.js +23 -9
  125. package/dist/start.js.map +1 -1
  126. package/dist/stores/memory.d.ts.map +1 -1
  127. package/dist/stores/types.d.ts +2 -1
  128. package/dist/stores/types.d.ts.map +1 -1
  129. package/dist/telemetry/mcp-client.d.ts +2 -1
  130. package/dist/telemetry/mcp-client.d.ts.map +1 -1
  131. package/dist/telemetry/mcp-client.js.map +1 -1
  132. package/dist/telemetry/mcp-usage.d.ts +22 -0
  133. package/dist/telemetry/mcp-usage.d.ts.map +1 -0
  134. package/dist/telemetry/mcp-usage.js +102 -0
  135. package/dist/telemetry/mcp-usage.js.map +1 -0
  136. package/dist/telemetry/metrics.d.ts +9 -0
  137. package/dist/telemetry/metrics.d.ts.map +1 -1
  138. package/dist/telemetry/metrics.js +21 -0
  139. package/dist/telemetry/metrics.js.map +1 -1
  140. package/dist/telemetry/runtime.d.ts +2 -1
  141. package/dist/telemetry/runtime.d.ts.map +1 -1
  142. package/dist/telemetry/runtime.js +18 -5
  143. package/dist/telemetry/runtime.js.map +1 -1
  144. package/dist/transports/mcp/admin-tools.d.ts +3 -7
  145. package/dist/transports/mcp/admin-tools.d.ts.map +1 -1
  146. package/dist/transports/mcp/admin-tools.js +9 -82
  147. package/dist/transports/mcp/admin-tools.js.map +1 -1
  148. package/dist/transports/mcp/app-status-html.d.ts +2 -2
  149. package/dist/transports/mcp/app-status-html.d.ts.map +1 -1
  150. package/dist/transports/mcp/app-status-html.js +1 -27
  151. package/dist/transports/mcp/app-status-html.js.map +1 -1
  152. package/dist/transports/mcp/client-presentation.d.ts +10 -13
  153. package/dist/transports/mcp/client-presentation.d.ts.map +1 -1
  154. package/dist/transports/mcp/client-presentation.js +2 -14
  155. package/dist/transports/mcp/client-presentation.js.map +1 -1
  156. package/dist/transports/mcp/decision-elicitation.js +23 -1
  157. package/dist/transports/mcp/decision-elicitation.js.map +1 -1
  158. package/dist/transports/mcp/dispatch-probe.js.map +1 -1
  159. package/dist/transports/mcp/editor-document-probe.js.map +1 -1
  160. package/dist/transports/mcp/editor-integration-setup-url.d.ts +0 -1
  161. package/dist/transports/mcp/editor-integration-setup-url.d.ts.map +1 -1
  162. package/dist/transports/mcp/editor-integration-setup-url.js +0 -3
  163. package/dist/transports/mcp/editor-integration-setup-url.js.map +1 -1
  164. package/dist/transports/mcp/format-tool-content.js +1 -1
  165. package/dist/transports/mcp/format-tool-content.js.map +1 -1
  166. package/dist/transports/mcp/instructions/tools/build-app.d.ts +2 -3
  167. package/dist/transports/mcp/instructions/tools/build-app.d.ts.map +1 -1
  168. package/dist/transports/mcp/instructions/tools/build-app.js +5 -11
  169. package/dist/transports/mcp/instructions/tools/build-app.js.map +1 -1
  170. package/dist/transports/mcp/instructions/tools/check-app-progress.d.ts.map +1 -1
  171. package/dist/transports/mcp/instructions/tools/check-app-progress.js +1 -25
  172. package/dist/transports/mcp/instructions/tools/check-app-progress.js.map +1 -1
  173. package/dist/transports/mcp/instructions/tools/check-publish-progress.d.ts +1 -2
  174. package/dist/transports/mcp/instructions/tools/check-publish-progress.d.ts.map +1 -1
  175. package/dist/transports/mcp/instructions/tools/check-publish-progress.js +2 -11
  176. package/dist/transports/mcp/instructions/tools/check-publish-progress.js.map +1 -1
  177. package/dist/transports/mcp/instructions/tools/copy.d.ts +1 -8
  178. package/dist/transports/mcp/instructions/tools/copy.d.ts.map +1 -1
  179. package/dist/transports/mcp/instructions/tools/copy.js +4 -28
  180. package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
  181. package/dist/transports/mcp/instructions/tools/create-integration.d.ts +1 -2
  182. package/dist/transports/mcp/instructions/tools/create-integration.d.ts.map +1 -1
  183. package/dist/transports/mcp/instructions/tools/create-integration.js +4 -8
  184. package/dist/transports/mcp/instructions/tools/create-integration.js.map +1 -1
  185. package/dist/transports/mcp/instructions/tools/edit-app.js +2 -2
  186. package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
  187. package/dist/transports/mcp/instructions/tools/get-app.d.ts.map +1 -1
  188. package/dist/transports/mcp/instructions/tools/get-app.js +1 -23
  189. package/dist/transports/mcp/instructions/tools/get-app.js.map +1 -1
  190. package/dist/transports/mcp/instructions/tools/index.d.ts.map +1 -1
  191. package/dist/transports/mcp/instructions/tools/index.js +0 -4
  192. package/dist/transports/mcp/instructions/tools/index.js.map +1 -1
  193. package/dist/transports/mcp/instructions/tools/publish-app.d.ts +1 -2
  194. package/dist/transports/mcp/instructions/tools/publish-app.d.ts.map +1 -1
  195. package/dist/transports/mcp/instructions/tools/publish-app.js +1 -10
  196. package/dist/transports/mcp/instructions/tools/publish-app.js.map +1 -1
  197. package/dist/transports/mcp/instructions/tools/start-app.d.ts +1 -2
  198. package/dist/transports/mcp/instructions/tools/start-app.d.ts.map +1 -1
  199. package/dist/transports/mcp/instructions/tools/start-app.js +3 -3
  200. package/dist/transports/mcp/instructions/tools/start-app.js.map +1 -1
  201. package/dist/transports/mcp/mount.d.ts +8 -15
  202. package/dist/transports/mcp/mount.d.ts.map +1 -1
  203. package/dist/transports/mcp/mount.js +233 -206
  204. package/dist/transports/mcp/mount.js.map +1 -1
  205. package/dist/transports/mcp/plan-approval.d.ts.map +1 -1
  206. package/dist/transports/mcp/plan-approval.js.map +1 -1
  207. package/dist/transports/mcp/tool-permissions.d.ts +30 -0
  208. package/dist/transports/mcp/tool-permissions.d.ts.map +1 -0
  209. package/dist/transports/mcp/tool-permissions.js +39 -0
  210. package/dist/transports/mcp/tool-permissions.js.map +1 -0
  211. package/package.json +28 -22
  212. package/skills/superblocks-import/SKILL.md +5 -3
  213. package/dist/capture/cowork-editor-url.d.ts +0 -7
  214. package/dist/capture/cowork-editor-url.d.ts.map +0 -1
  215. package/dist/capture/cowork-editor-url.js +0 -12
  216. package/dist/capture/cowork-editor-url.js.map +0 -1
  217. package/dist/transports/mcp/native-browser-presence.d.ts +0 -46
  218. package/dist/transports/mcp/native-browser-presence.d.ts.map +0 -1
  219. package/dist/transports/mcp/native-browser-presence.js +0 -158
  220. package/dist/transports/mcp/native-browser-presence.js.map +0 -1
  221. package/skills/superblocks-build/SKILL.md +0 -59
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFile, realpath, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { detectAttachmentArchiveType, getAttachmentMimeTypeFromFileName, getTextAttachmentType, isSupportedAttachmentUploadType, } from "@superblocksteam/shared";
5
- import { buildBrowserDriverContract, HOST_BROWSER_TOOLS_COWORK, needsBrowserDriverError, } from "../capture/browser-contract.js";
6
- import { withCoworkEditorHost } from "../capture/cowork-editor-url.js";
4
+ import { DESTRUCTIVE_MIGRATION_ACK_REQUIRED_MESSAGE, detectAttachmentArchiveType, getAttachmentMimeTypeFromFileName, getTextAttachmentType, isSupportedAttachmentUploadType, } from "@superblocksteam/shared";
5
+ import { HOSTED_CUSTOMER_AGENT_PREVIEW_MESSAGE, hostedAgentAllowed, } from "../agents/hosted-boundary.js";
6
+ import { buildBrowserDriverContract, needsBrowserDriverError, } from "../capture/browser-contract.js";
7
7
  import { resolveCaptureMode } from "../capture/mode.js";
8
8
  import { withDeadline } from "../deadline.js";
9
9
  import { gatewayDebug } from "../debug.js";
@@ -11,7 +11,7 @@ import { playwrightChromiumPresent } from "../playwright/ensure-chromium.js";
11
11
  import { isTrustedEmbedOrigin, } from "../preview/capture-screenshot.js";
12
12
  import { buildEmbedShellPreviewUrl, buildOpenablePreviewUrls, isHttpsUrl, } from "../preview/viewer-url.js";
13
13
  import { PromptNotSentError, } from "../sabs/session-peer.js";
14
- import { ServerHttpError } from "../server/client.js";
14
+ import { ServerHttpError, ServerTimeoutError } from "../server/client.js";
15
15
  import { IdentityUnavailableError } from "../server/identity-reads.js";
16
16
  import { archiveSourceFiles } from "./source-files-archive.js";
17
17
  import { TOOL_PERMISSION_ANSWERS, ToolPermissionReply, } from "./tool-permission-reply.js";
@@ -36,15 +36,12 @@ const RECENT_APP_FALLBACK_WINDOW_MS = 30 * 60 * 1000;
36
36
  function editorBaseUrl(config, applicationId) {
37
37
  return `${config.uiBaseUrl.replace(/\/$/, "")}/code-mode/applications/edit/${applicationId}`;
38
38
  }
39
- /** The editor URL handed to the caller, in the host mode its client renders. */
39
+ /** The editor URL handed to the caller. Every client gets the same one. */
40
40
  function editUrl(ctx, applicationId, branch) {
41
41
  const base = editorBaseUrl(ctx.config, applicationId);
42
- const url = branch
42
+ return branch
43
43
  ? `${base}?${new URLSearchParams({ branch }).toString()}`
44
44
  : base;
45
- return ctx.hostBrowserTools === HOST_BROWSER_TOOLS_COWORK
46
- ? withCoworkEditorHost(url)
47
- : url;
48
45
  }
49
46
  function fullscreenPreviewUrl(config, applicationId, branch) {
50
47
  const params = new URLSearchParams({ fullscreen: "true" });
@@ -188,6 +185,7 @@ async function attachLiveState(ctx, principal, scope) {
188
185
  branch,
189
186
  organizationId: principal.organizationId,
190
187
  });
188
+ ctx.liveStateAttaches.opened(scope, session.liveEditId);
191
189
  const sessionToken = await ctx.server.issueScopedToken({
192
190
  apiKey: principal.apiKey,
193
191
  applicationId,
@@ -212,6 +210,7 @@ async function attachLiveState(ctx, principal, scope) {
212
210
  },
213
211
  liveEditId: session.liveEditId,
214
212
  liveEditUrl: session.liveEditUrl,
213
+ ...(session.targetBranch ? { liveEditBranch: session.targetBranch } : {}),
215
214
  organizationId: principal.organizationId,
216
215
  sessionToken,
217
216
  userId: principal.userId,
@@ -237,6 +236,33 @@ async function attachLiveState(ctx, principal, scope) {
237
236
  }
238
237
  return { kind: "ok", value: undefined };
239
238
  }
239
+ /**
240
+ * Whether the hosted Platform MCP may load this org's app in its own Chromium.
241
+ * The preview picks the org's agents itself, so for an org that runs its own
242
+ * agent any API that runs on page load would reach into the customer's network
243
+ * from our control plane. Fails closed when the agent type cannot be read.
244
+ */
245
+ async function hostedBrowserMayLoadApp(ctx, principal) {
246
+ if (!ctx.config.hosted) {
247
+ return true;
248
+ }
249
+ try {
250
+ const inventory = await ctx.server.getAgentInventory({
251
+ apiKey: principal.apiKey,
252
+ organizationId: principal.organizationId,
253
+ });
254
+ return hostedAgentAllowed({
255
+ agentType: inventory.agentType,
256
+ hosted: true,
257
+ });
258
+ }
259
+ catch (error) {
260
+ console.warn("gateway hosted agent type unreadable; skipping preview", {
261
+ message: error instanceof Error ? error.message : String(error),
262
+ });
263
+ return false;
264
+ }
265
+ }
240
266
  async function captureLiveLibraryIfNeeded(ctx, applicationId, live) {
241
267
  if (!live) {
242
268
  return {};
@@ -275,7 +301,6 @@ async function browserContractFor(ctx, directEditUrl) {
275
301
  return buildBrowserDriverContract({
276
302
  captureMode: await captureModeOf(ctx),
277
303
  editUrl: directEditUrl,
278
- ...(ctx.hostBrowserTools ? { hostBrowserTools: ctx.hostBrowserTools } : {}),
279
304
  isolated: ctx.config.captureHostIsolated,
280
305
  uiOrigin: uiOrigin(ctx.config),
281
306
  });
@@ -766,6 +791,7 @@ function turnSummary(turn) {
766
791
  async function promptInput(ctx, input) {
767
792
  return {
768
793
  apiKey: ctx.principal.apiKey,
794
+ mcpClientName: ctx.mcpClientName,
769
795
  applicationId: input.applicationId,
770
796
  ...(input.attachments ? { attachments: input.attachments } : {}),
771
797
  ...(input.branch ? { branch: input.branch } : {}),
@@ -786,6 +812,7 @@ async function promptInput(ctx, input) {
786
812
  },
787
813
  ...(input.liveEditId ? { liveEditId: input.liveEditId } : {}),
788
814
  liveEditUrl: input.liveEditUrl,
815
+ ...(input.liveEditBranch ? { liveEditBranch: input.liveEditBranch } : {}),
789
816
  mode: input.mode ?? "PLAN",
790
817
  // Asked for only if the host above turns out not to serve the session any
791
818
  // more. Going back through openLiveEdit rather than connectLiveEdit
@@ -1194,6 +1221,7 @@ async function promptNewApplication(ctx, args) {
1194
1221
  idempotencyKey: input.idempotencyKey,
1195
1222
  liveEditId: session.liveEditId,
1196
1223
  liveEditUrl: session.liveEditUrl,
1224
+ ...(session.targetBranch ? { liveEditBranch: session.targetBranch } : {}),
1197
1225
  mode: input.mode ?? "PLAN",
1198
1226
  organizationId: principal.organizationId,
1199
1227
  ...(args.pendingStartToken === undefined
@@ -1544,19 +1572,22 @@ function tooLargeError(size) {
1544
1572
  * out, so it accepts anything attachments support rather than archives alone.
1545
1573
  * The other two routes are text channels and stay archive-shaped.
1546
1574
  */
1547
- async function resolveImportArchive(input, searchDirs) {
1575
+ async function resolveImportArchive(input, config) {
1548
1576
  const hasFiles = Boolean(input.files?.length);
1549
- const hasPath = Boolean(input.filePath?.trim());
1577
+ // Hosted, the disk is the microVM's own, never the user's files.
1578
+ const hasPath = !config.hosted && Boolean(input.filePath?.trim());
1550
1579
  const hasBase64 = Boolean(input.zipBase64?.trim());
1551
1580
  if (Number(hasFiles) + Number(hasPath) + Number(hasBase64) !== 1) {
1552
1581
  return {
1553
1582
  kind: "error",
1554
1583
  code: "import_zip_required",
1555
- message: "Provide exactly one of files, filePath (a path or file name on the gateway host), or zipBase64.",
1584
+ message: config.hosted
1585
+ ? "Provide exactly one of files or zipBase64."
1586
+ : "Provide exactly one of files, filePath (a path or file name on the gateway host), or zipBase64.",
1556
1587
  };
1557
1588
  }
1558
1589
  if (hasPath) {
1559
- return resolveFileByPath(input.filePath.trim(), searchDirs);
1590
+ return resolveFileByPath(input.filePath.trim(), config.importSearchDirs);
1560
1591
  }
1561
1592
  let bytes;
1562
1593
  if (hasFiles) {
@@ -1565,7 +1596,7 @@ async function resolveImportArchive(input, searchDirs) {
1565
1596
  return {
1566
1597
  kind: "error",
1567
1598
  code: "import_files_invalid",
1568
- message: `"${binaryFile.path}" is not a text file, and files entries are archived as the text they contain: base64 in there uploads as base64 characters wearing a "${path.extname(binaryFile.path)}" name. Pass filePath with the file's name instead, so the gateway reads its real bytes.`,
1599
+ message: `"${binaryFile.path}" is not a text file, and files entries are archived as the text they contain: base64 in there uploads as base64 characters wearing a "${path.extname(binaryFile.path)}" name. ${config.hosted ? "Put it in a ZIP and pass that as zipBase64 instead, so its real bytes are uploaded. A ZIP too large for zipBase64 cannot be uploaded on this connection yet." : "Pass filePath with the file's name instead, so the gateway reads its real bytes."}`,
1569
1600
  };
1570
1601
  }
1571
1602
  const archive = archiveSourceFiles(input.files);
@@ -1689,7 +1720,7 @@ export async function uploadArtifact(ctx, input) {
1689
1720
  if (principalResult.kind !== "ok") {
1690
1721
  return principalResult;
1691
1722
  }
1692
- const archiveResult = await resolveImportArchive(input, ctx.config.importSearchDirs);
1723
+ const archiveResult = await resolveImportArchive(input, ctx.config);
1693
1724
  if (archiveResult.kind !== "ok") {
1694
1725
  return archiveResult;
1695
1726
  }
@@ -2355,6 +2386,7 @@ async function promptExistingApplication(ctx, args) {
2355
2386
  idempotencyKey: input.idempotencyKey,
2356
2387
  liveEditId: session.liveEditId,
2357
2388
  liveEditUrl: session.liveEditUrl,
2389
+ ...(session.targetBranch ? { liveEditBranch: session.targetBranch } : {}),
2358
2390
  mode: editTurnMode(input, answeringQuestion),
2359
2391
  organizationId: principal.organizationId,
2360
2392
  ...(args.pendingStartToken === undefined
@@ -2551,10 +2583,17 @@ export async function previewApp(ctx, input) {
2551
2583
  ctx.onProgress?.({ message: "Preparing a preview", type: "progress" });
2552
2584
  let commit;
2553
2585
  try {
2586
+ // With branching on, the work is on the user's own branch, and a commit
2587
+ // that names no branch would preview main instead.
2588
+ const branch = input.branch ??
2589
+ (await readUserBranch(ctx, {
2590
+ apiKey: principal.apiKey,
2591
+ applicationId,
2592
+ }))?.branch;
2554
2593
  commit = await ctx.server.createCommit({
2555
2594
  apiKey: principal.apiKey,
2556
2595
  applicationId,
2557
- branch: input.branch,
2596
+ branch,
2558
2597
  message: "Preview",
2559
2598
  });
2560
2599
  }
@@ -2652,6 +2691,13 @@ export async function previewApp(ctx, input) {
2652
2691
  });
2653
2692
  needsBrowserDriver = true;
2654
2693
  }
2694
+ else if (!(await hostedBrowserMayLoadApp(ctx, principal))) {
2695
+ screenshotUnavailableReason = HOSTED_CUSTOMER_AGENT_PREVIEW_MESSAGE;
2696
+ console.error("gateway preview screenshot skipped", {
2697
+ applicationId,
2698
+ reason: "hosted_customer_agent",
2699
+ });
2700
+ }
2655
2701
  else if (!jwt) {
2656
2702
  screenshotUnavailableReason =
2657
2703
  "Preview screenshot unavailable because its scoped credential could not be created.";
@@ -2956,13 +3002,86 @@ async function unbuiltReason(ctx, principal, applicationId, requestedBranch) {
2956
3002
  });
2957
3003
  const candidate = ctx.appState.pendingDecision(applicationId, branch);
2958
3004
  const pending = turn && candidate?.branch !== turn.branch ? undefined : candidate;
2959
- const reason = localUnbuiltReason(turn, pending);
3005
+ // The branch `currentTurnBranch` gave the read's attach.
3006
+ const actualBranch = turn ? turn.branch : (pending?.branch ?? branch);
3007
+ const reason = localUnbuiltReason(turn, pending) ??
3008
+ (await stoppedLiveEditReason(ctx, principal, {
3009
+ applicationId,
3010
+ branch: actualBranch,
3011
+ organizationId: principal.organizationId,
3012
+ userId: principal.userId,
3013
+ }));
2960
3014
  if (!reason) {
2961
3015
  return undefined;
2962
3016
  }
2963
- const actualBranch = turn ? turn.branch : (pending?.branch ?? branch);
2964
3017
  return { ...(actualBranch ? { branch: actualBranch } : {}), reason };
2965
3018
  }
3019
+ /** The status lookup runs after the read's deadline, so it is kept short. */
3020
+ const LIVE_EDIT_STATUS_TIMEOUT_MS = 3_000;
3021
+ const LIVE_EDIT_STOPPED_REPLY = "The Superblocks live-edit session for this app stopped, so nothing is serving the editor or preview. Your saved work is not affected. Call edit_app to start a new session.";
3022
+ /**
3023
+ * Why a read that would otherwise say ready must not: the live edit its own
3024
+ * attach opened has ended.
3025
+ *
3026
+ * A read that holds no socket answers from what this process knows, which
3027
+ * for an app with no turn is "ready". A cold start, a full sandbox pool or
3028
+ * another user's editor lock also leave nothing held, and those stay ready,
3029
+ * so only the control plane saying the session ended counts.
3030
+ */
3031
+ async function stoppedLiveEditReason(ctx, principal, scope) {
3032
+ const opened = ctx.liveStateAttaches.openedLiveEdit(scope);
3033
+ if (!opened ||
3034
+ heldSocketFor(ctx, scope.applicationId, scope.branch) === "this_branch") {
3035
+ return undefined;
3036
+ }
3037
+ // The control plane forgets an ended live edit once its editor lock
3038
+ // expires, so an answer it gave once is kept until a new attach starts.
3039
+ const cached = opened.endedStatus !== undefined;
3040
+ let status = opened.endedStatus;
3041
+ if (!status) {
3042
+ try {
3043
+ status = await ctx.server.getLiveEditStatus({
3044
+ apiKey: principal.apiKey,
3045
+ applicationId: scope.applicationId,
3046
+ branch: scope.branch,
3047
+ liveEditId: opened.liveEditId,
3048
+ signal: AbortSignal.any([
3049
+ AbortSignal.timeout(LIVE_EDIT_STATUS_TIMEOUT_MS),
3050
+ ...(ctx.signal ? [ctx.signal] : []),
3051
+ ]),
3052
+ });
3053
+ }
3054
+ catch (error) {
3055
+ // A caller that went away cancels the lookup, which is not a failure.
3056
+ if (!ctx.signal?.aborted) {
3057
+ console.error("gateway live edit status check failed", {
3058
+ applicationId: scope.applicationId,
3059
+ liveEditId: opened.liveEditId,
3060
+ message: error instanceof Error ? error.message : String(error),
3061
+ });
3062
+ }
3063
+ return undefined;
3064
+ }
3065
+ if (status !== "terminated" && status !== "terminating") {
3066
+ return undefined;
3067
+ }
3068
+ ctx.liveStateAttaches.stopped(scope, opened.liveEditId, status);
3069
+ }
3070
+ console.warn("gateway live edit stopped", {
3071
+ applicationId: scope.applicationId,
3072
+ branch: scope.branch,
3073
+ cached,
3074
+ liveEditId: opened.liveEditId,
3075
+ organizationId: scope.organizationId,
3076
+ status,
3077
+ });
3078
+ return {
3079
+ kind: "empty_turn",
3080
+ pendingAction: "retry_build",
3081
+ reply: LIVE_EDIT_STOPPED_REPLY,
3082
+ status: "live_edit_terminated",
3083
+ };
3084
+ }
2966
3085
  function currentTurnBranch(ctx, principal, applicationId, requestedBranch) {
2967
3086
  const branch = normalizeBranch(requestedBranch);
2968
3087
  const turn = ctx.sessionPeer.turnState({
@@ -3411,7 +3530,7 @@ export async function checkPublishProgress(ctx, input) {
3411
3530
  return namingApplication(applicationId, {
3412
3531
  kind: "error",
3413
3532
  code: "publish_not_started",
3414
- message: "No publish is in flight for this application. Call publish_app to deploy it, or name the commitId to follow.",
3533
+ message: "No publish is in flight for this application. A publish that publish_app already reported as deployed is live. To follow a specific deploy, pass the commitId publish_app returned.",
3415
3534
  });
3416
3535
  }
3417
3536
  const claimedCommit = storedCommitId === undefined;
@@ -3563,6 +3682,155 @@ export async function checkPublishProgress(ctx, input) {
3563
3682
  }
3564
3683
  }
3565
3684
  }
3685
+ /**
3686
+ * The caller's own branch when branching is on for this app, or undefined.
3687
+ *
3688
+ * With branching on, each user edits on their own branch, and a commit that
3689
+ * names no branch goes to main. A server from before branching has no status
3690
+ * route, and every app there commits and publishes as before.
3691
+ */
3692
+ async function readUserBranch(ctx, input) {
3693
+ try {
3694
+ const status = await ctx.server.getBranchStatus({
3695
+ ...input,
3696
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
3697
+ });
3698
+ return status.mode === "user-branch"
3699
+ ? { branch: status.branch, mainHead: status.mainHead }
3700
+ : undefined;
3701
+ }
3702
+ catch (error) {
3703
+ if (error instanceof ServerHttpError && error.status === 404) {
3704
+ return undefined;
3705
+ }
3706
+ throw error;
3707
+ }
3708
+ }
3709
+ /** The server's refusal to deploy a commit that is still on a user's own branch. */
3710
+ const NOT_SHARED_REFUSAL = "Share your work to the team version before you publish";
3711
+ function notShared(applicationId, commitId) {
3712
+ return {
3713
+ kind: "error",
3714
+ applicationId,
3715
+ code: "publish_not_shared",
3716
+ message: `Commit ${commitId} is not the team version, and only the team version can be published. Call publish_app with commitMessage instead of commitId to share the current work and publish it, or ask the user to share it from the Superblocks editor.`,
3717
+ pendingAction: "open_editor",
3718
+ };
3719
+ }
3720
+ /** The server's refusal to land a branch whose merge from main is not resolved yet. */
3721
+ const MERGE_IN_PROGRESS_REFUSAL = "has a merge in progress";
3722
+ function landConflict(applicationId) {
3723
+ return {
3724
+ kind: "error",
3725
+ applicationId,
3726
+ code: "publish_conflict",
3727
+ message: "This work conflicts with changes on the team version. Ask the user to resolve the conflict in the Superblocks editor, then publish again.",
3728
+ pendingAction: "open_editor",
3729
+ };
3730
+ }
3731
+ function landMigrating(applicationId) {
3732
+ return {
3733
+ kind: "error",
3734
+ applicationId,
3735
+ code: "publish_migrating",
3736
+ message: "The app database is still applying this work's migrations. Call publish_app again in a minute.",
3737
+ };
3738
+ }
3739
+ /**
3740
+ * Shares the caller's branch to main, the editor's first step of a publish,
3741
+ * and returns main's head to deploy. Only main's head can be published.
3742
+ */
3743
+ async function landForPublish(ctx, input) {
3744
+ const { apiKey, applicationId, deadlineAtMs } = input;
3745
+ ctx.onProgress?.({
3746
+ message: "Sharing your work to the team version",
3747
+ type: "progress",
3748
+ });
3749
+ const land = (retryFailedMigrations) => ctx.server.landBranch({
3750
+ apiKey,
3751
+ applicationId,
3752
+ retryFailedMigrations,
3753
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
3754
+ timeoutMs: Math.max(1, deadlineAtMs - nowMs(ctx)),
3755
+ });
3756
+ let result;
3757
+ try {
3758
+ // A publish is the user asking again, so a migration that failed before
3759
+ // runs again. The asks that only wait on the migrations do not retry it.
3760
+ result = await land(true);
3761
+ while (result.outcome === "migrating" &&
3762
+ !ctx.signal?.aborted &&
3763
+ nowMs(ctx) + ctx.config.publishPollIntervalMs < deadlineAtMs) {
3764
+ await sleepUntilAbort(ctx.config.publishPollIntervalMs, ctx.signal);
3765
+ result = await land(false);
3766
+ }
3767
+ }
3768
+ catch (error) {
3769
+ if (ctx.signal?.aborted) {
3770
+ return {
3771
+ kind: "error",
3772
+ code: "publish_aborted",
3773
+ message: "Publish was cancelled while sharing your work.",
3774
+ };
3775
+ }
3776
+ // A later ask can get only the time left in the call, too little for the
3777
+ // server to answer. The migrations were still running at the last answer.
3778
+ if (error instanceof ServerTimeoutError &&
3779
+ result?.outcome === "migrating") {
3780
+ return landMigrating(applicationId);
3781
+ }
3782
+ const message = error instanceof Error ? error.message : String(error);
3783
+ if (message.includes(MERGE_IN_PROGRESS_REFUSAL)) {
3784
+ return landConflict(applicationId);
3785
+ }
3786
+ // Only the user can agree to delete data, and only the editor shows what
3787
+ // would go, so this publish never sends that agreement itself.
3788
+ if (message.includes(DESTRUCTIVE_MIGRATION_ACK_REQUIRED_MESSAGE)) {
3789
+ return {
3790
+ kind: "error",
3791
+ applicationId,
3792
+ code: "publish_needs_confirmation",
3793
+ message: "Publishing would delete data in the app database. Ask the user to publish from the Superblocks editor, which shows what would be deleted and asks them to confirm.",
3794
+ pendingAction: "open_editor",
3795
+ };
3796
+ }
3797
+ return { kind: "error", applicationId, code: "publish_failed", message };
3798
+ }
3799
+ // Read before the switch narrows it away: a newer server can send an
3800
+ // outcome this build has no case for.
3801
+ const outcome = result.outcome;
3802
+ switch (result.outcome) {
3803
+ case "landed":
3804
+ case "up-to-date":
3805
+ return { kind: "ok", value: result.mainHead };
3806
+ case "conflict":
3807
+ return landConflict(applicationId);
3808
+ case "migrating":
3809
+ return landMigrating(applicationId);
3810
+ case "contended":
3811
+ return {
3812
+ kind: "error",
3813
+ applicationId,
3814
+ code: "publish_busy",
3815
+ message: "The team version kept changing while this publish tried to share the work. Call publish_app again.",
3816
+ };
3817
+ case "migration-failed":
3818
+ return {
3819
+ kind: "error",
3820
+ applicationId,
3821
+ code: "publish_failed",
3822
+ message: `The app database could not apply this work's migrations: ${result.migrationError ?? "unknown error"}`,
3823
+ };
3824
+ default:
3825
+ return {
3826
+ kind: "error",
3827
+ applicationId,
3828
+ code: "publish_failed",
3829
+ message: `Sharing your work to the team version ended with "${outcome}", which this Platform MCP does not know how to finish. Ask the user to publish from the Superblocks editor.`,
3830
+ pendingAction: "open_editor",
3831
+ };
3832
+ }
3833
+ }
3566
3834
  async function publishResolvedApp(ctx, args) {
3567
3835
  const { applicationId, input, principal } = args;
3568
3836
  const requestedCommitId = input.commitId?.trim();
@@ -3583,6 +3851,37 @@ async function publishResolvedApp(ctx, args) {
3583
3851
  message: "Publish could not start within the host call budget.",
3584
3852
  };
3585
3853
  }
3854
+ let userBranch;
3855
+ try {
3856
+ userBranch = await readUserBranch(ctx, {
3857
+ apiKey: principal.apiKey,
3858
+ applicationId,
3859
+ timeoutMs: Math.max(1, deadlineAtMs - nowMs(ctx)),
3860
+ });
3861
+ }
3862
+ catch (error) {
3863
+ if (ctx.signal?.aborted) {
3864
+ return {
3865
+ kind: "error",
3866
+ code: "publish_aborted",
3867
+ message: "Publish was cancelled before it started.",
3868
+ };
3869
+ }
3870
+ return {
3871
+ kind: "error",
3872
+ applicationId,
3873
+ code: "publish_failed",
3874
+ message: error instanceof Error ? error.message : String(error),
3875
+ };
3876
+ }
3877
+ // The server only deploys main's head, so a commit still on the user's own
3878
+ // branch has to be shared first.
3879
+ if (userBranch &&
3880
+ requestedCommitId &&
3881
+ userBranch.mainHead !== null &&
3882
+ requestedCommitId !== userBranch.mainHead) {
3883
+ return notShared(applicationId, requestedCommitId);
3884
+ }
3586
3885
  // Deploys address a commit and never fall back to the branch head, while
3587
3886
  // Clark's work sits uncommitted in the live-edit session. Committing here is
3588
3887
  // what the editor's own publish flow does, and without it a caller who has
@@ -3599,10 +3898,13 @@ async function publishResolvedApp(ctx, args) {
3599
3898
  }
3600
3899
  ctx.onProgress?.({ message: "Committing current work", type: "progress" });
3601
3900
  try {
3901
+ // On the user's own branch the server still refuses to commit while a
3902
+ // Superblocks draft waits to be accepted, so an unaccepted draft is
3903
+ // never published.
3602
3904
  const commit = await ctx.server.createCommit({
3603
3905
  apiKey: principal.apiKey,
3604
3906
  applicationId,
3605
- branch: input.branch,
3907
+ branch: userBranch?.branch ?? input.branch,
3606
3908
  message: input.commitMessage,
3607
3909
  ...(ctx.signal ? { signal: ctx.signal } : {}),
3608
3910
  timeoutMs: Math.max(1, deadlineAtMs - nowMs(ctx)),
@@ -3623,6 +3925,17 @@ async function publishResolvedApp(ctx, args) {
3623
3925
  message: error instanceof Error ? error.message : String(error),
3624
3926
  };
3625
3927
  }
3928
+ if (userBranch) {
3929
+ const landed = await landForPublish(ctx, {
3930
+ apiKey: principal.apiKey,
3931
+ applicationId,
3932
+ deadlineAtMs,
3933
+ });
3934
+ if (landed.kind !== "ok") {
3935
+ return landed;
3936
+ }
3937
+ commitId = landed.value;
3938
+ }
3626
3939
  }
3627
3940
  if (ctx.signal?.aborted) {
3628
3941
  return {
@@ -3732,6 +4045,11 @@ async function publishResolvedApp(ctx, args) {
3732
4045
  }
3733
4046
  if (error instanceof ServerHttpError) {
3734
4047
  ctx.appState.clearPublishInFlight(applicationId, publishClaim);
4048
+ // The server checks this too, for a commit the branch status above
4049
+ // could not rule out.
4050
+ if (error.message.includes(NOT_SHARED_REFUSAL)) {
4051
+ return notShared(applicationId, commitId);
4052
+ }
3735
4053
  return {
3736
4054
  kind: "error",
3737
4055
  code: "publish_failed",