@superblocksteam/gateway 2.0.156 → 2.0.157-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 (40) hide show
  1. package/README.md +30 -3
  2. package/dist/capabilities/integration-metadata.d.ts +28 -0
  3. package/dist/capabilities/integration-metadata.js +174 -0
  4. package/dist/capabilities/integration-metadata.js.map +1 -0
  5. package/dist/capabilities/lifecycle.d.ts +12 -1
  6. package/dist/capabilities/lifecycle.js +99 -51
  7. package/dist/capabilities/lifecycle.js.map +1 -1
  8. package/dist/capabilities/types.d.ts +5 -0
  9. package/dist/capabilities/types.js.map +1 -1
  10. package/dist/deps.d.ts +2 -0
  11. package/dist/integrations/map.d.ts +1 -0
  12. package/dist/integrations/map.js +1 -1
  13. package/dist/integrations/map.js.map +1 -1
  14. package/dist/sabs/editor-client-methods.js +4 -5
  15. package/dist/sabs/editor-client-methods.js.map +1 -1
  16. package/dist/sabs/session-peer.d.ts +4 -0
  17. package/dist/sabs/websocket-session-peer.d.ts +2 -1
  18. package/dist/sabs/websocket-session-peer.js +21 -11
  19. package/dist/sabs/websocket-session-peer.js.map +1 -1
  20. package/dist/server/client.d.ts +10 -4
  21. package/dist/server/client.js +13 -4
  22. package/dist/server/client.js.map +1 -1
  23. package/dist/start.d.ts +10 -0
  24. package/dist/start.js +5 -2
  25. package/dist/start.js.map +1 -1
  26. package/dist/telemetry/mcp-client.d.ts +22 -0
  27. package/dist/telemetry/mcp-client.js +129 -0
  28. package/dist/telemetry/mcp-client.js.map +1 -0
  29. package/dist/telemetry/runtime.d.ts +9 -0
  30. package/dist/telemetry/runtime.js +121 -0
  31. package/dist/telemetry/runtime.js.map +1 -0
  32. package/dist/transports/mcp/admin-tools.d.ts +3 -1
  33. package/dist/transports/mcp/admin-tools.js +12 -1
  34. package/dist/transports/mcp/admin-tools.js.map +1 -1
  35. package/dist/transports/mcp/app-status-html.d.ts +2 -2
  36. package/dist/transports/mcp/app-status-html.js +139 -8
  37. package/dist/transports/mcp/app-status-html.js.map +1 -1
  38. package/dist/transports/mcp/mount.js +89 -24
  39. package/dist/transports/mcp/mount.js.map +1 -1
  40. package/package.json +5 -4
package/README.md CHANGED
@@ -4,8 +4,8 @@ Standalone Superblocks entry point for a **single MCP connector** (Admin + Build
4
4
  over **stdio**, running as the already-logged-in Superblocks CLI user.
5
5
 
6
6
  Builder tools (`start_app`, `import_app`, `edit_app`, `check_app_progress`,
7
- `get_app`, `preview_app`, `publish_app`) plus customer Admin tools from
8
- `@superblocksteam/mcp-server` share this process.
7
+ `get_app`, `get_integration_metadata`, `preview_app`, `publish_app`) plus
8
+ customer Admin tools from `@superblocksteam/mcp-server` share this process.
9
9
 
10
10
  The MCP host owns process lifecycle: it spawns `superblocks gateway serve`.
11
11
  There is no foreground HTTP `/mcp`, no OAuth resource server, and no linked-grant
@@ -147,7 +147,18 @@ package only the customer Admin surface. Builder tools are not registered.
147
147
  ## MCP tools
148
148
 
149
149
  After `gateway setup`, restart Claude Code or Cursor and call `start_app`,
150
- `edit_app`, `check_app_progress`, `get_app`, `preview_app`, and `publish_app`.
150
+ `edit_app`, `check_app_progress`, `get_app`, `get_integration_metadata`,
151
+ `preview_app`, and `publish_app`. `get_integration_metadata` reads tables,
152
+ columns, and types from a connected integration; use `search`, `limit`, and
153
+ `offset` for large results. It needs no application: without one it mints an
154
+ `integrations:build` token scoped to that single integration. Pass
155
+ `applicationId` only for an integration owned by one application, such as a
156
+ Native DB, which is invisible without app context.
157
+ It resolves the integration through the RBAC-annotated superset list rather than
158
+ the org-wide lookup by id, so it answers `integration_not_permitted` without
159
+ build permission on that integration and `integration_not_supported` for a
160
+ plugin Clark cannot use as a tool.
161
+ `get_integration_config_schema` is the create-integration form, not live data.
151
162
  See `.env.example` for optional configuration.
152
163
 
153
164
  ## Env
@@ -168,3 +179,19 @@ Gateway debug logs include tool names, result states, decision branches, and
168
179
  entry-point stacks. They intentionally omit credentials, prompts, answers, and
169
180
  result payloads. Run `superblocks gateway setup --client <client> --debug`, then
170
181
  restart the MCP host. Re-run setup without `--debug` to turn them off.
182
+
183
+ ## Remote traceability
184
+
185
+ Gateway exports one OpenTelemetry trace per MCP process through the authenticated
186
+ Server `/api/v1/traces` proxy. Each tool call is a child span with the MCP client
187
+ name and version, tool name, duration, outcome, organization ID, and application
188
+ ID when available.
189
+
190
+ Prompts, answers, result payloads, credentials, and raw error messages are never
191
+ added to these spans. Use the application ID to correlate a trace with the
192
+ existing server-side app and build records.
193
+
194
+ Cloud-Prem uses `SUPERBLOCKS_DEPLOYMENT_TYPE=cloud-prem` (or `GATEWAY_LOCAL_AGENT`)
195
+ so the stricter telemetry policy is selected. Laptop agents still drop client,
196
+ organization, and application attributes until the Tier 2 traces contract lists
197
+ them; tool name, duration, and outcome still export.
@@ -0,0 +1,28 @@
1
+ import { type CapabilityContext } from "./lifecycle.js";
2
+ import type { CapabilityResult } from "./types.js";
3
+ export type GetIntegrationMetadataInput = {
4
+ applicationId?: string;
5
+ integrationId: string;
6
+ limit?: number;
7
+ offset?: number;
8
+ search?: string;
9
+ };
10
+ type MetadataEntry = {
11
+ path: string;
12
+ value: boolean | null | number | string;
13
+ };
14
+ export type GetIntegrationMetadataResult = {
15
+ /** Absent when the metadata was read through an integration-scoped token. */
16
+ applicationId?: string;
17
+ entries: MetadataEntry[];
18
+ hasMore: boolean;
19
+ integrationId: string;
20
+ integrationName: string;
21
+ nextOffset: number | null;
22
+ offset: number;
23
+ pluginId: string;
24
+ returnedCount: number;
25
+ totalCount: number;
26
+ };
27
+ export declare function getIntegrationMetadata(context: CapabilityContext, input: GetIntegrationMetadataInput): Promise<CapabilityResult<GetIntegrationMetadataResult>>;
28
+ export {};
@@ -0,0 +1,174 @@
1
+ import { pickOrchestratorBaseUrl } from "../agents/resolve-orchestrator-url.js";
2
+ import { integrationIsToolSupported } from "../integrations/filters.js";
3
+ import { hasBuildPermission } from "../integrations/map.js";
4
+ import { pickEditorProfileKey } from "../integrations/profile-key.js";
5
+ import { HttpOrchestratorClient } from "../orchestrator/client.js";
6
+ import { sanitizeAgentFacingText, sanitizeAgentFacingValue, } from "../sabs/agent-facing-text.js";
7
+ import { ensurePrincipal, resolveApplicationId, } from "./lifecycle.js";
8
+ const DEFAULT_LIMIT = 100;
9
+ function labelPart(value) {
10
+ return typeof value === "string"
11
+ ? value.trim().replaceAll(/[.[\]]/g, "_")
12
+ : "";
13
+ }
14
+ /**
15
+ * A table name alone is ambiguous: `public.users` and `analytics.users` are
16
+ * different tables, so qualify with the schema whenever the row carries one.
17
+ */
18
+ function arrayEntryLabel(value, index) {
19
+ if (value && typeof value === "object") {
20
+ const row = value;
21
+ const name = labelPart(row.name);
22
+ if (name) {
23
+ const schema = labelPart(row.schema);
24
+ return schema ? `${schema}.${name}` : name;
25
+ }
26
+ }
27
+ return String(index);
28
+ }
29
+ function metadataEntries(value, path = "", entries = []) {
30
+ if (value === null ||
31
+ typeof value === "boolean" ||
32
+ typeof value === "number" ||
33
+ typeof value === "string") {
34
+ entries.push({ path, value });
35
+ return entries;
36
+ }
37
+ if (Array.isArray(value)) {
38
+ const seen = new Set();
39
+ value.forEach((entry, index) => {
40
+ const label = arrayEntryLabel(entry, index);
41
+ // Two rows that still label identically would be indistinguishable, so
42
+ // fall back to the index for the later one.
43
+ const unique = seen.has(label) ? `${label}#${index}` : label;
44
+ seen.add(unique);
45
+ metadataEntries(entry, `${path}[${unique}]`, entries);
46
+ });
47
+ return entries;
48
+ }
49
+ if (value && typeof value === "object") {
50
+ for (const [key, entry] of Object.entries(value)) {
51
+ metadataEntries(entry, path ? `${path}.${key}` : key, entries);
52
+ }
53
+ }
54
+ return entries;
55
+ }
56
+ export async function getIntegrationMetadata(context, input) {
57
+ const principal = await ensurePrincipal(context);
58
+ if (principal.kind !== "ok") {
59
+ return principal;
60
+ }
61
+ let applicationId;
62
+ // Named for the agent before the integration resolves, so a failure on the
63
+ // way to the metadata still reports which integration it was reaching for.
64
+ let label = sanitizeAgentFacingText(input.integrationId);
65
+ try {
66
+ // An app is preferred but not required. App-owned integrations (Native DB
67
+ // among them) are only visible through an app-scoped token, so use one
68
+ // whenever an app is in play and fall back to the integration-scoped token
69
+ // rather than making the caller invent an application.
70
+ const resolved = await resolveApplicationId(context, principal.value, input.applicationId);
71
+ applicationId = resolved.kind === "ok" ? resolved.value : undefined;
72
+ // The RBAC-annotated list, not the org-wide lookup by id: only this one
73
+ // carries the permissions that say whether the caller may build with the
74
+ // integration at all.
75
+ const rows = await context.server.getSupersetIntegrations({
76
+ apiKey: principal.value.apiKey,
77
+ ...(applicationId && { applicationId }),
78
+ organizationId: principal.value.organizationId,
79
+ });
80
+ const integration = rows.find((row) => row.id === input.integrationId);
81
+ if (!integration) {
82
+ return {
83
+ ...(applicationId && { applicationId }),
84
+ kind: "error",
85
+ code: "integration_not_accessible",
86
+ message: `Integration ${label} was not found or is not accessible.`,
87
+ };
88
+ }
89
+ const integrationName = sanitizeAgentFacingText(integration.name);
90
+ const pluginId = sanitizeAgentFacingText(integration.pluginId);
91
+ label = integrationName;
92
+ if (!hasBuildPermission(integration)) {
93
+ return {
94
+ ...(applicationId && { applicationId }),
95
+ kind: "error",
96
+ code: "integration_not_permitted",
97
+ message: `You do not have build permission on ${integrationName}.`,
98
+ };
99
+ }
100
+ if (!integrationIsToolSupported(integration.pluginId)) {
101
+ return {
102
+ ...(applicationId && { applicationId }),
103
+ kind: "error",
104
+ code: "integration_not_supported",
105
+ message: `${integrationName} is a ${pluginId} integration, which this tool cannot inspect.`,
106
+ };
107
+ }
108
+ const profiles = await context.server.getOrganizationProfiles({
109
+ apiKey: principal.value.apiKey,
110
+ organizationId: principal.value.organizationId,
111
+ });
112
+ const profileKey = pickEditorProfileKey(context.config.profileKey, profiles);
113
+ const inventory = await context.server.getAgentInventory({
114
+ apiKey: principal.value.apiKey,
115
+ organizationId: principal.value.organizationId,
116
+ });
117
+ const orchestratorUrl = pickOrchestratorBaseUrl({
118
+ agentType: inventory.agentType,
119
+ agents: inventory.agents,
120
+ profileKey,
121
+ });
122
+ const scopedToken = await context.server.issueScopedToken(applicationId
123
+ ? {
124
+ apiKey: principal.value.apiKey,
125
+ applicationId,
126
+ scope: "apps:update",
127
+ }
128
+ : {
129
+ apiKey: principal.value.apiKey,
130
+ integrationId: input.integrationId,
131
+ scope: "integrations:build",
132
+ });
133
+ const metadata = await new HttpOrchestratorClient(orchestratorUrl).getMetadata({
134
+ apiKey: principal.value.apiKey,
135
+ integrationId: input.integrationId,
136
+ profileKey,
137
+ scopedToken,
138
+ });
139
+ const sanitized = sanitizeAgentFacingValue(metadata);
140
+ const search = input.search?.trim().toLowerCase();
141
+ const matched = metadataEntries(sanitized).filter((entry) => !search ||
142
+ entry.path.toLowerCase().includes(search) ||
143
+ String(entry.value).toLowerCase().includes(search));
144
+ const limit = input.limit ?? DEFAULT_LIMIT;
145
+ const offset = input.offset ?? 0;
146
+ const entries = matched.slice(offset, offset + limit);
147
+ const nextOffset = offset + entries.length < matched.length ? offset + entries.length : null;
148
+ return {
149
+ kind: "ok",
150
+ value: {
151
+ ...(applicationId && { applicationId }),
152
+ entries,
153
+ hasMore: nextOffset !== null,
154
+ integrationId: integration.id,
155
+ integrationName,
156
+ nextOffset,
157
+ offset,
158
+ pluginId,
159
+ returnedCount: entries.length,
160
+ totalCount: matched.length,
161
+ },
162
+ };
163
+ }
164
+ catch (error) {
165
+ const detail = sanitizeAgentFacingText(error instanceof Error ? error.message : "Unknown metadata error").replaceAll("\u2014", "-");
166
+ return {
167
+ ...(applicationId && { applicationId }),
168
+ kind: "error",
169
+ code: "integration_metadata_unavailable",
170
+ message: `Could not load metadata for ${label}: ${detail}`,
171
+ };
172
+ }
173
+ }
174
+ //# sourceMappingURL=integration-metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration-metadata.js","sourceRoot":"","sources":["../../src/capabilities/integration-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,eAAe,EACf,oBAAoB,GAErB,MAAM,gBAAgB,CAAC;AAGxB,MAAM,aAAa,GAAG,GAAG,CAAC;AA6B1B,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ;QAC9B,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC;QACzC,CAAC,CAAC,EAAE,CAAC;AACT,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,KAAc,EAAE,KAAa;IACpD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,KAAgC,CAAC;QAC7C,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACrC,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,eAAe,CACtB,KAAc,EACd,IAAI,GAAG,EAAE,EACT,UAA2B,EAAE;IAE7B,IACE,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,SAAS;QAC1B,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,QAAQ,EACzB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9B,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YAC7B,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC5C,uEAAuE;YACvE,4CAA4C;YAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;YAC7D,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACjB,eAAe,CAAC,KAAK,EAAE,GAAG,IAAI,IAAI,MAAM,GAAG,EAAE,OAAO,CAAC,CAAC;QACxD,CAAC,CAAC,CAAC;QACH,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAA0B,EAC1B,KAAkC;IAElC,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,SAAS,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,aAAiC,CAAC;IACtC,2EAA2E;IAC3E,2EAA2E;IAC3E,IAAI,KAAK,GAAG,uBAAuB,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IAEzD,IAAI,CAAC;QACH,0EAA0E;QAC1E,uEAAuE;QACvE,2EAA2E;QAC3E,uDAAuD;QACvD,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CACzC,OAAO,EACP,SAAS,CAAC,KAAK,EACf,KAAK,CAAC,aAAa,CACpB,CAAC;QACF,aAAa,GAAG,QAAQ,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QAEpE,wEAAwE;QACxE,yEAAyE;QACzE,sBAAsB;QACtB,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,uBAAuB,CAAC;YACxD,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM;YAC9B,GAAG,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC;YACvC,cAAc,EAAE,SAAS,CAAC,KAAK,CAAC,cAAc;SAC/C,CAAC,CAAC;QACH,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,aAAa,CAAC,CAAC;QACvE,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO;gBACL,GAAG,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC;gBACvC,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,4BAA4B;gBAClC,OAAO,EAAE,eAAe,KAAK,sCAAsC;aACpE,CAAC;QACJ,CAAC;QAED,MAAM,eAAe,GAAG,uBAAuB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAClE,MAAM,QAAQ,GAAG,uBAAuB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC/D,KAAK,GAAG,eAAe,CAAC;QAExB,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,EAAE,CAAC;YACrC,OAAO;gBACL,GAAG,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC;gBACvC,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,2BAA2B;gBACjC,OAAO,EAAE,uCAAuC,eAAe,GAAG;aACnE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,0BAA0B,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtD,OAAO;gBACL,GAAG,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC;gBACvC,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,2BAA2B;gBACjC,OAAO,EAAE,GAAG,eAAe,SAAS,QAAQ,+CAA+C;aAC5F,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,uBAAuB,CAAC;YAC5D,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM;YAC9B,cAAc,EAAE,SAAS,CAAC,KAAK,CAAC,cAAc;SAC/C,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,oBAAoB,CACrC,OAAO,CAAC,MAAM,CAAC,UAAU,EACzB,QAAQ,CACT,CAAC;QACF,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;YACvD,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM;YAC9B,cAAc,EAAE,SAAS,CAAC,KAAK,CAAC,cAAc;SAC/C,CAAC,CAAC;QACH,MAAM,eAAe,GAAG,uBAAuB,CAAC;YAC9C,SAAS,EAAE,SAAS,CAAC,SAAS;YAC9B,MAAM,EAAE,SAAS,CAAC,MAAM;YACxB,UAAU;SACX,CAAC,CAAC;QACH,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,gBAAgB,CACvD,aAAa;YACX,CAAC,CAAC;gBACE,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM;gBAC9B,aAAa;gBACb,KAAK,EAAE,aAAa;aACrB;YACH,CAAC,CAAC;gBACE,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM;gBAC9B,aAAa,EAAE,KAAK,CAAC,aAAa;gBAClC,KAAK,EAAE,oBAAoB;aAC5B,CACN,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,IAAI,sBAAsB,CAC/C,eAAe,CAChB,CAAC,WAAW,CAAC;YACZ,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,MAAM;YAC9B,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,UAAU;YACV,WAAW;SACZ,CAAC,CAAC;QAEH,MAAM,SAAS,GAAG,wBAAwB,CAAC,QAAQ,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAClD,MAAM,OAAO,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC,MAAM,CAC/C,CAAC,KAAK,EAAE,EAAE,CACR,CAAC,MAAM;YACP,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;YACzC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CACrD,CAAC;QACF,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,aAAa,CAAC;QAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;QACjC,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC;QACtD,MAAM,UAAU,GACd,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QAE5E,OAAO;YACL,IAAI,EAAE,IAAI;YACV,KAAK,EAAE;gBACL,GAAG,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC;gBACvC,OAAO;gBACP,OAAO,EAAE,UAAU,KAAK,IAAI;gBAC5B,aAAa,EAAE,WAAW,CAAC,EAAE;gBAC7B,eAAe;gBACf,UAAU;gBACV,MAAM;gBACN,QAAQ;gBACR,aAAa,EAAE,OAAO,CAAC,MAAM;gBAC7B,UAAU,EAAE,OAAO,CAAC,MAAM;aAC3B;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,uBAAuB,CACpC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAClE,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC5B,OAAO;YACL,GAAG,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC;YACvC,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,kCAAkC;YACxC,OAAO,EAAE,+BAA+B,KAAK,KAAK,MAAM,EAAE;SAC3D,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -5,7 +5,7 @@ import type { CapturePreviewScreenshot } from "../preview/capture-screenshot.js"
5
5
  import type { CallerRef, ProgressStore, RecentAppStore, TurnStore } from "../resume/types.js";
6
6
  import type { SessionPeer } from "../sabs/session-peer.js";
7
7
  import type { SuperblocksServerClient } from "../server/client.js";
8
- import type { CapabilityResult, CheckAppProgressInput, CheckAppProgressResult, EditAppInput, EditAppResult, GetAppInput, GetAppResult, ImportAppInput, ImportAppResult, PreviewAppInput, PreviewAppResult, Principal, ProgressEvent, PublishAppInput, PublishAppResult, StartAppInput, StartAppResult } from "./types.js";
8
+ import type { CapabilityResult, CheckAppProgressInput, CheckAppProgressResult, EditAppInput, EditAppResult, GetAppInput, GetAppResult, ImportAppInput, ImportAppResult, PreviewAppInput, PreviewAppResult, Principal, ProgressEvent, PublishAppInput, PublishAppResult, ResolvedPrincipal, StartAppInput, StartAppResult } from "./types.js";
9
9
  export { IMPORT_ZIP_MAX_BYTES } from "./types.js";
10
10
  export type CapabilityContext = {
11
11
  /**
@@ -23,6 +23,7 @@ export type CapabilityContext = {
23
23
  /** Injected so pacing and stall thresholds are testable without real time. */
24
24
  now?: () => number;
25
25
  onProgress?: (event: ProgressEvent) => void;
26
+ onPrincipalResolved?: (principal: ResolvedPrincipal) => void;
26
27
  principal: Principal;
27
28
  /** Where each caller's place in the event stream is kept between calls. */
28
29
  progressCursors?: ProgressStore;
@@ -44,6 +45,16 @@ export type CapabilityContext = {
44
45
  signal?: AbortSignal;
45
46
  turns: TurnStore;
46
47
  };
48
+ /**
49
+ * Resolves the Superblocks user behind the CLI session this Gateway was
50
+ * started with.
51
+ *
52
+ * Identity is the already-logged-in Superblocks CLI user. A rejection here
53
+ * means that personal API key is bad, not that the user needs a second login,
54
+ * so it surfaces as an error rather than an interactive-auth elicitation.
55
+ */
56
+ export declare function ensurePrincipal(ctx: CapabilityContext): Promise<CapabilityResult<ResolvedPrincipal>>;
57
+ export declare function resolveApplicationId(ctx: CapabilityContext, principal: ResolvedPrincipal, applicationId: string | undefined): Promise<CapabilityResult<string>>;
47
58
  export declare function startApp(ctx: CapabilityContext, input: StartAppInput): Promise<CapabilityResult<StartAppResult>>;
48
59
  /**
49
60
  * Creates a fullstack app, uploads the caller's archive as an app attachment,
@@ -227,19 +227,21 @@ function defaultAppName(prompt) {
227
227
  * means that personal API key is bad, not that the user needs a second login,
228
228
  * so it surfaces as an error rather than an interactive-auth elicitation.
229
229
  */
230
- async function ensurePrincipal(ctx) {
230
+ export async function ensurePrincipal(ctx) {
231
231
  const { principal } = ctx;
232
232
  // MCP already has the CLI session key. Looking it up again would spend a
233
233
  // second round trip on `/users/me`, the slowest endpoint the Superblocks
234
234
  // API has, when the principal is already resolved (ENG-5806).
235
235
  if (principal.organizationId && principal.userId) {
236
+ const resolved = {
237
+ ...principal,
238
+ organizationId: principal.organizationId,
239
+ userId: principal.userId,
240
+ };
241
+ ctx.onPrincipalResolved?.(resolved);
236
242
  return {
237
243
  kind: "ok",
238
- value: {
239
- ...principal,
240
- organizationId: principal.organizationId,
241
- userId: principal.userId,
242
- },
244
+ value: resolved,
243
245
  };
244
246
  }
245
247
  let validated;
@@ -276,14 +278,16 @@ async function ensurePrincipal(ctx) {
276
278
  message: "The Superblocks API rejected the credential issued for this caller.",
277
279
  };
278
280
  }
281
+ const resolved = {
282
+ ...principal,
283
+ email: validated.email,
284
+ organizationId: validated.organizationId,
285
+ userId: validated.userId,
286
+ };
287
+ ctx.onPrincipalResolved?.(resolved);
279
288
  return {
280
289
  kind: "ok",
281
- value: {
282
- ...principal,
283
- email: validated.email,
284
- organizationId: validated.organizationId,
285
- userId: validated.userId,
286
- },
290
+ value: resolved,
287
291
  };
288
292
  }
289
293
  /**
@@ -378,7 +382,7 @@ async function elicitStepUp(ctx, idempotencyKey, applicationId) {
378
382
  },
379
383
  };
380
384
  }
381
- async function resolveApplicationId(ctx, principal, applicationId) {
385
+ export async function resolveApplicationId(ctx, principal, applicationId) {
382
386
  if (applicationId?.trim()) {
383
387
  return { kind: "ok", value: applicationId.trim() };
384
388
  }
@@ -512,7 +516,9 @@ function turnSummary(turn) {
512
516
  return "Superblocks is still working, but only the editor can show it — open the edit URL";
513
517
  }
514
518
  if (turn.generationStatus === "live_edit_terminated") {
515
- return "The Superblocks live-edit session terminated before anything was built — send the prompt again with edit_app";
519
+ return turn.retryAction === "retry_import"
520
+ ? "The Superblocks live-edit session terminated before anything was built — send the archive again with import_app"
521
+ : "The Superblocks live-edit session terminated before anything was built — send the prompt again with edit_app";
516
522
  }
517
523
  if (turn.checkpointCommitId) {
518
524
  return `Application ready — checkpoint saved. ${NEXT_STEPS}`;
@@ -668,6 +674,7 @@ async function forwardClarkPrompt(ctx, input) {
668
674
  ...(forward.recentRuns && forward.recentRuns.length > 0
669
675
  ? { recentRuns: forward.recentRuns }
670
676
  : {}),
677
+ ...(forward.retryAction ? { retryAction: forward.retryAction } : {}),
671
678
  viewStatus: forward.viewStatus,
672
679
  },
673
680
  };
@@ -757,17 +764,13 @@ function namingCreatedApplication(applicationId, capability, result) {
757
764
  * The recovery to advertise when the machinery around a turn fails: `edit_app`
758
765
  * with the same prompt, which is what `retry_build` means.
759
766
  *
760
- * Only for a prompt `edit_app` could actually replay. A prompt carrying
761
- * attachments is not one: `edit_app` has no way to resupply an attachment, so
762
- * an import's migration prompt would reach Superblocks describing an archive it
763
- * cannot see. Advertising a recovery that cannot finish is worse than
764
- * advertising none, so the import path gets `applicationId` and no hint.
765
- * TODO(ENG-5917): let `import_app` resume into an application it already
766
- * created, and give the import path a recovery of its own.
767
+ * A regular prompt is retried with `edit_app`. An import prompt carries an
768
+ * attachment, so it must be retried with `import_app` to upload the archive
769
+ * again and obtain a fresh signed URL.
767
770
  */
768
771
  function retryIfReplayable(attachments) {
769
772
  return attachments && attachments.length > 0
770
- ? undefined
773
+ ? { pendingAction: "retry_import" }
771
774
  : { pendingAction: "retry_build" };
772
775
  }
773
776
  /**
@@ -809,7 +812,9 @@ function rememberFailedStart(ctx, input) {
809
812
  organizationId: input.principal.organizationId,
810
813
  // Reaches the caller as the dead start's reply on its next poll, so it
811
814
  // has to stand on its own there rather than lean on this call's error.
812
- reason: `The Superblocks live-edit session for application ${input.applicationId} never opened, so the prompt never reached Superblocks and nothing was built. Nothing is running for that application; send the same instruction again to retry it. (${cause})`,
815
+ reason: input.reason ??
816
+ `The Superblocks live-edit session for application ${input.applicationId} never opened, so the prompt never reached Superblocks and nothing was built. Nothing is running for that application; send the same instruction again to retry it. (${cause})`,
817
+ ...(input.retryAction ? { retryAction: input.retryAction } : {}),
813
818
  userId: input.principal.userId,
814
819
  });
815
820
  }
@@ -865,7 +870,12 @@ async function promptNewApplication(ctx, args) {
865
870
  });
866
871
  }
867
872
  catch (error) {
868
- rememberFailedStart(ctx, { applicationId, error, principal });
873
+ rememberFailedStart(ctx, {
874
+ applicationId,
875
+ error,
876
+ principal,
877
+ ...(args.attachments?.length ? { retryAction: "retry_import" } : {}),
878
+ });
869
879
  return liveEditUnavailable(error, args.attachments);
870
880
  }
871
881
  ctx.onProgress?.({
@@ -1150,14 +1160,19 @@ function normalizeImportSource(source) {
1150
1160
  * wizard, without the sessionStorage hop.
1151
1161
  */
1152
1162
  export async function importApp(ctx, input) {
1163
+ const resumedApplicationId = input.applicationId?.trim();
1153
1164
  const archiveResult = await resolveImportArchive(input, ctx.config.importSearchDirs);
1154
1165
  if (archiveResult.kind !== "ok") {
1155
- return archiveResult;
1166
+ return resumedApplicationId
1167
+ ? namingApplication(resumedApplicationId, archiveResult)
1168
+ : archiveResult;
1156
1169
  }
1157
1170
  const archive = archiveResult.value;
1158
1171
  const principalResult = await ensurePrincipal(ctx);
1159
1172
  if (principalResult.kind !== "ok") {
1160
- return principalResult;
1173
+ return resumedApplicationId
1174
+ ? namingApplication(resumedApplicationId, principalResult)
1175
+ : principalResult;
1161
1176
  }
1162
1177
  const principal = principalResult.value;
1163
1178
  const source = normalizeImportSource(input.source);
@@ -1168,25 +1183,26 @@ export async function importApp(ctx, input) {
1168
1183
  });
1169
1184
  const explicitName = input.name?.trim();
1170
1185
  const name = explicitName || defaultAppName(migrationPrompt);
1171
- ctx.onProgress?.({
1172
- message: "Creating application for import",
1173
- type: "progress",
1174
- });
1175
- const created = await createApplicationForPrompt(ctx, {
1176
- apiKey: principal.apiKey,
1177
- name,
1178
- organizationId: principal.organizationId,
1179
- templateName: "app-fullstack",
1180
- });
1181
- if (created.kind !== "ok") {
1182
- return created;
1186
+ let applicationId = resumedApplicationId;
1187
+ if (!applicationId) {
1188
+ ctx.onProgress?.({
1189
+ message: "Creating application for import",
1190
+ type: "progress",
1191
+ });
1192
+ const created = await createApplicationForPrompt(ctx, {
1193
+ apiKey: principal.apiKey,
1194
+ name,
1195
+ organizationId: principal.organizationId,
1196
+ templateName: "app-fullstack",
1197
+ });
1198
+ if (created.kind !== "ok") {
1199
+ return created;
1200
+ }
1201
+ applicationId = created.value.id;
1183
1202
  }
1184
- const app = created.value;
1185
- // See startApp: the row outlives any failure below it, so it is already this
1186
- // caller's most recent app.
1187
- await rememberApplication(ctx, principal, app.id);
1188
- return namingCreatedApplication(app.id, "import_app", await migrateArchiveIntoApplication(ctx, {
1189
- applicationId: app.id,
1203
+ await rememberApplication(ctx, principal, applicationId);
1204
+ const result = await migrateArchiveIntoApplication(ctx, {
1205
+ applicationId,
1190
1206
  archive,
1191
1207
  explicitName,
1192
1208
  input,
@@ -1194,7 +1210,10 @@ export async function importApp(ctx, input) {
1194
1210
  name,
1195
1211
  principal,
1196
1212
  source,
1197
- }));
1213
+ });
1214
+ return resumedApplicationId
1215
+ ? namingApplication(applicationId, result)
1216
+ : namingCreatedApplication(applicationId, "import_app", result);
1198
1217
  }
1199
1218
  /**
1200
1219
  * Uploads the archive onto an application that already exists and hands
@@ -1218,10 +1237,18 @@ async function migrateArchiveIntoApplication(ctx, args) {
1218
1237
  }
1219
1238
  catch (error) {
1220
1239
  const message = error instanceof Error ? error.message : String(error);
1240
+ rememberFailedStart(ctx, {
1241
+ applicationId,
1242
+ error,
1243
+ principal,
1244
+ reason: `The archive for application ${applicationId} could not be uploaded, so the migration never started and nothing was built. Send the same archive again with import_app to retry it. (${message})`,
1245
+ retryAction: "retry_import",
1246
+ });
1221
1247
  return {
1222
1248
  kind: "error",
1223
1249
  code: "import_upload_failed",
1224
1250
  message,
1251
+ pendingAction: "retry_import",
1225
1252
  };
1226
1253
  }
1227
1254
  return promptNewApplication(ctx, {
@@ -1247,7 +1274,13 @@ async function migrateArchiveIntoApplication(ctx, args) {
1247
1274
  prompt: args.migrationPrompt,
1248
1275
  });
1249
1276
  }
1250
- /** Machine-readable shape a cancelled wait answers with. */
1277
+ /**
1278
+ * Machine-readable shape a cancelled wait answers with.
1279
+ *
1280
+ * Callers name the application alongside it. A cancelled wait says nothing
1281
+ * about the work, which carries on: a caller asked to resume it has to be able
1282
+ * to point at the application rather than start a second one.
1283
+ */
1251
1284
  function cancelled(capability) {
1252
1285
  return {
1253
1286
  kind: "error",
@@ -1336,7 +1369,7 @@ export async function checkAppProgress(ctx, input) {
1336
1369
  // while the credential was being resolved should never reach the point of
1337
1370
  // registering a waiter it will not read.
1338
1371
  if (ctx.signal?.aborted) {
1339
- return cancelled("check_app_progress");
1372
+ return namingApplication(applicationId, cancelled("check_app_progress"));
1340
1373
  }
1341
1374
  const cursorKey = cursorKeyFor(ctx, principal, applicationId);
1342
1375
  const stored = cursorKey
@@ -1350,7 +1383,7 @@ export async function checkAppProgress(ctx, input) {
1350
1383
  waitMs: hostCallBudgetMs(ctx, ctx.config.clarkProgressWaitMs),
1351
1384
  });
1352
1385
  if (ctx.signal?.aborted) {
1353
- return cancelled("check_app_progress");
1386
+ return namingApplication(applicationId, cancelled("check_app_progress"));
1354
1387
  }
1355
1388
  const snapshot = await readSnapshot(ctx, {
1356
1389
  applicationId,
@@ -1468,6 +1501,9 @@ export async function checkAppProgress(ctx, input) {
1468
1501
  reply: update.reply,
1469
1502
  ...(update.decision ? { decision: update.decision } : {}),
1470
1503
  generationStatus: update.generationStatus,
1504
+ ...(update.retryAction && emptyTurnStatus(update.generationStatus)
1505
+ ? { pendingAction: update.retryAction }
1506
+ : {}),
1471
1507
  status,
1472
1508
  viewStatus: update.viewStatus,
1473
1509
  },
@@ -2052,7 +2088,13 @@ function localUnbuiltReason(turn, pending) {
2052
2088
  return undefined;
2053
2089
  }
2054
2090
  const status = emptyTurnStatus(turn.generationStatus);
2055
- return status ? { kind: "empty_turn", status } : undefined;
2091
+ return status
2092
+ ? {
2093
+ kind: "empty_turn",
2094
+ retryAction: turn.retryAction ?? "retry_build",
2095
+ status,
2096
+ }
2097
+ : undefined;
2056
2098
  }
2057
2099
  /**
2058
2100
  * The same question asked of what was persisted, for a caller with no live turn
@@ -2140,6 +2182,8 @@ function persistedTurnEnd(status) {
2140
2182
  // plain question and cannot be approved.
2141
2183
  case "needs_decision":
2142
2184
  return { kind: "awaiting_decision" };
2185
+ // The stream never recorded whether the prompt carried an archive, so
2186
+ // this path cannot pick retry_build vs retry_import. Omit rather than guess.
2143
2187
  case "live_edit_terminated":
2144
2188
  return { kind: "empty_turn", status };
2145
2189
  // A `cancelled` in the tail may still have a real app behind it: the
@@ -2211,7 +2255,11 @@ async function unbuiltApp(ctx, principal, applicationId) {
2211
2255
  if (reason.kind === "empty_turn") {
2212
2256
  return {
2213
2257
  kind: "ok",
2214
- value: { ...app, pendingAction: "retry_build", status: reason.status },
2258
+ value: {
2259
+ ...app,
2260
+ ...(reason.retryAction ? { pendingAction: reason.retryAction } : {}),
2261
+ status: reason.status,
2262
+ },
2215
2263
  };
2216
2264
  }
2217
2265
  return {
@@ -2345,7 +2393,7 @@ async function waitForBuild(ctx, input) {
2345
2393
  let started = false;
2346
2394
  for (;;) {
2347
2395
  if (ctx.signal?.aborted) {
2348
- return cancelled("get_app");
2396
+ return namingApplication(input.applicationId, cancelled("get_app"));
2349
2397
  }
2350
2398
  // Checked before the network call, not only after it: a budget already
2351
2399
  // exhausted when this loop begins (or used up by the previous