@superblocksteam/gateway 2.0.177 → 2.0.178-next.1
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.
- package/README.md +52 -19
- package/dist/agents/hosted-boundary.d.ts +36 -0
- package/dist/agents/hosted-boundary.d.ts.map +1 -0
- package/dist/agents/hosted-boundary.js +66 -0
- package/dist/agents/hosted-boundary.js.map +1 -0
- package/dist/agents/resolve-orchestrator-url.d.ts +8 -0
- package/dist/agents/resolve-orchestrator-url.d.ts.map +1 -1
- package/dist/agents/resolve-orchestrator-url.js +5 -0
- package/dist/agents/resolve-orchestrator-url.js.map +1 -1
- package/dist/capabilities/integration-metadata.d.ts.map +1 -1
- package/dist/capabilities/integration-metadata.js +12 -1
- package/dist/capabilities/integration-metadata.js.map +1 -1
- package/dist/capabilities/lifecycle.d.ts +1 -6
- package/dist/capabilities/lifecycle.d.ts.map +1 -1
- package/dist/capabilities/lifecycle.js +339 -21
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/live-state-attaches.d.ts +28 -0
- package/dist/capabilities/live-state-attaches.d.ts.map +1 -1
- package/dist/capabilities/live-state-attaches.js +33 -0
- package/dist/capabilities/live-state-attaches.js.map +1 -1
- package/dist/capabilities/query-integration.d.ts.map +1 -1
- package/dist/capabilities/query-integration.js +12 -1
- package/dist/capabilities/query-integration.js.map +1 -1
- package/dist/capabilities/tool-permission-reply.d.ts.map +1 -1
- package/dist/capabilities/tool-permission-reply.js.map +1 -1
- package/dist/capabilities/types.d.ts +3 -4
- package/dist/capabilities/types.d.ts.map +1 -1
- package/dist/capabilities/types.js.map +1 -1
- package/dist/capture/browser-contract.d.ts +1 -18
- package/dist/capture/browser-contract.d.ts.map +1 -1
- package/dist/capture/browser-contract.js +1 -13
- package/dist/capture/browser-contract.js.map +1 -1
- package/dist/capture/browser-instructions.d.ts +3 -3
- package/dist/capture/browser-instructions.d.ts.map +1 -1
- package/dist/capture/browser-instructions.js +4 -5
- package/dist/capture/browser-instructions.js.map +1 -1
- package/dist/capture/capture-library-probe.d.ts +0 -1
- package/dist/capture/capture-library-probe.d.ts.map +1 -1
- package/dist/capture/capture-library-probe.js +1 -1
- package/dist/capture/capture-library-probe.js.map +1 -1
- package/dist/capture/capture-library.d.ts +2 -1
- package/dist/capture/capture-library.d.ts.map +1 -1
- package/dist/capture/classify-library-paint.d.ts +2 -1
- package/dist/capture/classify-library-paint.d.ts.map +1 -1
- package/dist/capture/library-parent-script.d.ts +3 -2
- package/dist/capture/library-parent-script.d.ts.map +1 -1
- package/dist/capture/mode.d.ts +2 -3
- package/dist/capture/mode.d.ts.map +1 -1
- package/dist/capture/mode.js +2 -3
- package/dist/capture/mode.js.map +1 -1
- package/dist/config.d.ts +21 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/debug.js.map +1 -1
- package/dist/deps.d.ts +3 -3
- package/dist/health/readiness.d.ts +2 -1
- package/dist/health/readiness.d.ts.map +1 -1
- package/dist/http/main.d.ts +3 -0
- package/dist/http/main.d.ts.map +1 -0
- package/dist/http/main.js +11 -0
- package/dist/http/main.js.map +1 -0
- package/dist/http/serve.d.ts +26 -0
- package/dist/http/serve.d.ts.map +1 -0
- package/dist/http/serve.js +298 -0
- package/dist/http/serve.js.map +1 -0
- package/dist/integrations/filters.d.ts.map +1 -1
- package/dist/integrations/filters.js +1 -16
- package/dist/integrations/filters.js.map +1 -1
- package/dist/integrations/map.d.ts +0 -5
- package/dist/integrations/map.d.ts.map +1 -1
- package/dist/integrations/map.js +1 -1
- package/dist/integrations/map.js.map +1 -1
- package/dist/internal-ingress.d.ts +47 -0
- package/dist/internal-ingress.d.ts.map +1 -0
- package/dist/internal-ingress.js +90 -0
- package/dist/internal-ingress.js.map +1 -0
- package/dist/orchestrator/client.d.ts +11 -5
- package/dist/orchestrator/client.d.ts.map +1 -1
- package/dist/orchestrator/client.js +14 -3
- package/dist/orchestrator/client.js.map +1 -1
- package/dist/playwright/ensure-chromium.js.map +1 -1
- package/dist/preview/capture-screenshot.d.ts +2 -2
- package/dist/preview/capture-screenshot.d.ts.map +1 -1
- package/dist/preview/capture-screenshot.js +2 -2
- package/dist/preview/capture-screenshot.js.map +1 -1
- package/dist/process/fault-barrier.d.ts +4 -3
- package/dist/process/fault-barrier.d.ts.map +1 -1
- package/dist/sabs/app-state.d.ts +2 -13
- package/dist/sabs/app-state.d.ts.map +1 -1
- package/dist/sabs/app-state.js +7 -2
- package/dist/sabs/app-state.js.map +1 -1
- package/dist/sabs/awaited-decision.d.ts.map +1 -1
- package/dist/sabs/browser-context.d.ts +4 -1
- package/dist/sabs/browser-context.d.ts.map +1 -1
- package/dist/sabs/browser-context.js +1 -0
- package/dist/sabs/browser-context.js.map +1 -1
- package/dist/sabs/editor-client-methods.d.ts.map +1 -1
- package/dist/sabs/editor-client-methods.js +1 -1
- package/dist/sabs/editor-client-methods.js.map +1 -1
- package/dist/sabs/editor-socket.d.ts.map +1 -1
- package/dist/sabs/editor-socket.js.map +1 -1
- package/dist/sabs/session-peer.d.ts +13 -1
- package/dist/sabs/session-peer.d.ts.map +1 -1
- package/dist/sabs/session-peer.js.map +1 -1
- package/dist/sabs/streamed-reply.d.ts.map +1 -1
- package/dist/sabs/streamed-reply.js.map +1 -1
- package/dist/sabs/turn-collector.d.ts.map +1 -1
- package/dist/sabs/turn-collector.js.map +1 -1
- package/dist/sabs/websocket-session-peer.d.ts +17 -1
- package/dist/sabs/websocket-session-peer.d.ts.map +1 -1
- package/dist/sabs/websocket-session-peer.js +89 -15
- package/dist/sabs/websocket-session-peer.js.map +1 -1
- package/dist/server/client.d.ts +48 -0
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/client.js +91 -173
- package/dist/server/client.js.map +1 -1
- package/dist/server/identity-reads.d.ts +5 -3
- package/dist/server/identity-reads.d.ts.map +1 -1
- package/dist/server/identity-reads.js +31 -4
- package/dist/server/identity-reads.js.map +1 -1
- package/dist/start.d.ts +5 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +23 -9
- package/dist/start.js.map +1 -1
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/types.d.ts +2 -1
- package/dist/stores/types.d.ts.map +1 -1
- package/dist/telemetry/mcp-client.d.ts +2 -1
- package/dist/telemetry/mcp-client.d.ts.map +1 -1
- package/dist/telemetry/mcp-client.js.map +1 -1
- package/dist/telemetry/mcp-usage.d.ts +22 -0
- package/dist/telemetry/mcp-usage.d.ts.map +1 -0
- package/dist/telemetry/mcp-usage.js +102 -0
- package/dist/telemetry/mcp-usage.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +9 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +21 -0
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/runtime.d.ts +2 -1
- package/dist/telemetry/runtime.d.ts.map +1 -1
- package/dist/telemetry/runtime.js +18 -5
- package/dist/telemetry/runtime.js.map +1 -1
- package/dist/transports/mcp/admin-tools.d.ts +3 -7
- package/dist/transports/mcp/admin-tools.d.ts.map +1 -1
- package/dist/transports/mcp/admin-tools.js +9 -82
- package/dist/transports/mcp/admin-tools.js.map +1 -1
- package/dist/transports/mcp/app-status-html.d.ts +2 -2
- package/dist/transports/mcp/app-status-html.d.ts.map +1 -1
- package/dist/transports/mcp/app-status-html.js +1 -27
- package/dist/transports/mcp/app-status-html.js.map +1 -1
- package/dist/transports/mcp/client-presentation.d.ts +10 -13
- package/dist/transports/mcp/client-presentation.d.ts.map +1 -1
- package/dist/transports/mcp/client-presentation.js +2 -14
- package/dist/transports/mcp/client-presentation.js.map +1 -1
- package/dist/transports/mcp/decision-elicitation.js +23 -1
- package/dist/transports/mcp/decision-elicitation.js.map +1 -1
- package/dist/transports/mcp/dispatch-probe.js.map +1 -1
- package/dist/transports/mcp/editor-document-probe.js.map +1 -1
- package/dist/transports/mcp/editor-integration-setup-url.d.ts +0 -1
- package/dist/transports/mcp/editor-integration-setup-url.d.ts.map +1 -1
- package/dist/transports/mcp/editor-integration-setup-url.js +0 -3
- package/dist/transports/mcp/editor-integration-setup-url.js.map +1 -1
- package/dist/transports/mcp/format-tool-content.js +1 -1
- package/dist/transports/mcp/format-tool-content.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/build-app.d.ts +2 -3
- package/dist/transports/mcp/instructions/tools/build-app.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/build-app.js +5 -11
- package/dist/transports/mcp/instructions/tools/build-app.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/check-app-progress.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/check-app-progress.js +1 -25
- package/dist/transports/mcp/instructions/tools/check-app-progress.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/check-publish-progress.d.ts +1 -2
- package/dist/transports/mcp/instructions/tools/check-publish-progress.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/check-publish-progress.js +2 -11
- package/dist/transports/mcp/instructions/tools/check-publish-progress.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/copy.d.ts +1 -8
- package/dist/transports/mcp/instructions/tools/copy.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/copy.js +4 -28
- package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/create-integration.d.ts +1 -2
- package/dist/transports/mcp/instructions/tools/create-integration.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/create-integration.js +4 -8
- package/dist/transports/mcp/instructions/tools/create-integration.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js +2 -2
- package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/get-app.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/get-app.js +1 -23
- package/dist/transports/mcp/instructions/tools/get-app.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/index.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/index.js +0 -4
- package/dist/transports/mcp/instructions/tools/index.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/publish-app.d.ts +1 -2
- package/dist/transports/mcp/instructions/tools/publish-app.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/publish-app.js +1 -10
- package/dist/transports/mcp/instructions/tools/publish-app.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/start-app.d.ts +1 -2
- package/dist/transports/mcp/instructions/tools/start-app.d.ts.map +1 -1
- package/dist/transports/mcp/instructions/tools/start-app.js +3 -3
- package/dist/transports/mcp/instructions/tools/start-app.js.map +1 -1
- package/dist/transports/mcp/mount.d.ts +8 -15
- package/dist/transports/mcp/mount.d.ts.map +1 -1
- package/dist/transports/mcp/mount.js +233 -206
- package/dist/transports/mcp/mount.js.map +1 -1
- package/dist/transports/mcp/plan-approval.d.ts.map +1 -1
- package/dist/transports/mcp/plan-approval.js.map +1 -1
- package/dist/transports/mcp/tool-permissions.d.ts +30 -0
- package/dist/transports/mcp/tool-permissions.d.ts.map +1 -0
- package/dist/transports/mcp/tool-permissions.js +39 -0
- package/dist/transports/mcp/tool-permissions.js.map +1 -0
- package/package.json +28 -22
- package/skills/superblocks-import/SKILL.md +5 -3
- package/dist/capture/cowork-editor-url.d.ts +0 -7
- package/dist/capture/cowork-editor-url.d.ts.map +0 -1
- package/dist/capture/cowork-editor-url.js +0 -12
- package/dist/capture/cowork-editor-url.js.map +0 -1
- package/dist/transports/mcp/native-browser-presence.d.ts +0 -46
- package/dist/transports/mcp/native-browser-presence.d.ts.map +0 -1
- package/dist/transports/mcp/native-browser-presence.js +0 -158
- package/dist/transports/mcp/native-browser-presence.js.map +0 -1
- 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 {
|
|
6
|
-
import {
|
|
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
|
|
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
|
-
|
|
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,
|
|
1575
|
+
async function resolveImportArchive(input, config) {
|
|
1548
1576
|
const hasFiles = Boolean(input.files?.length);
|
|
1549
|
-
|
|
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:
|
|
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(),
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|