@superblocksteam/gateway 2.0.155-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.
- package/LICENSE.txt +87 -0
- package/README.md +170 -0
- package/dist/agents/resolve-orchestrator-url.d.ts +22 -0
- package/dist/agents/resolve-orchestrator-url.js +38 -0
- package/dist/agents/resolve-orchestrator-url.js.map +1 -0
- package/dist/capabilities/import-prompt.d.ts +11 -0
- package/dist/capabilities/import-prompt.js +96 -0
- package/dist/capabilities/import-prompt.js.map +1 -0
- package/dist/capabilities/lifecycle.d.ts +73 -0
- package/dist/capabilities/lifecycle.js +2531 -0
- package/dist/capabilities/lifecycle.js.map +1 -0
- package/dist/capabilities/persisted-progress.d.ts +46 -0
- package/dist/capabilities/persisted-progress.js +246 -0
- package/dist/capabilities/persisted-progress.js.map +1 -0
- package/dist/capabilities/types.d.ts +446 -0
- package/dist/capabilities/types.js +47 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/capture/browser-contract.d.ts +36 -0
- package/dist/capture/browser-contract.js +24 -0
- package/dist/capture/browser-contract.js.map +1 -0
- package/dist/capture/browser-instructions.d.ts +9 -0
- package/dist/capture/browser-instructions.js +17 -0
- package/dist/capture/browser-instructions.js.map +1 -0
- package/dist/capture/capture-library-probe.d.ts +55 -0
- package/dist/capture/capture-library-probe.js +177 -0
- package/dist/capture/capture-library-probe.js.map +1 -0
- package/dist/capture/capture-library.d.ts +52 -0
- package/dist/capture/capture-library.js +249 -0
- package/dist/capture/capture-library.js.map +1 -0
- package/dist/capture/classify-library-paint.d.ts +21 -0
- package/dist/capture/classify-library-paint.js +13 -0
- package/dist/capture/classify-library-paint.js.map +1 -0
- package/dist/capture/host-mcp-scan.d.ts +16 -0
- package/dist/capture/host-mcp-scan.js +64 -0
- package/dist/capture/host-mcp-scan.js.map +1 -0
- package/dist/capture/library-parent-script.d.ts +34 -0
- package/dist/capture/library-parent-script.js +82 -0
- package/dist/capture/library-parent-script.js.map +1 -0
- package/dist/capture/mode.d.ts +22 -0
- package/dist/capture/mode.js +45 -0
- package/dist/capture/mode.js.map +1 -0
- package/dist/config.d.ts +141 -0
- package/dist/config.js +109 -0
- package/dist/config.js.map +1 -0
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +9 -0
- package/dist/debug.js.map +1 -0
- package/dist/deps.d.ts +36 -0
- package/dist/deps.js +2 -0
- package/dist/deps.js.map +1 -0
- package/dist/events/cursor.d.ts +43 -0
- package/dist/events/cursor.js +78 -0
- package/dist/events/cursor.js.map +1 -0
- package/dist/events/memory-event-store.d.ts +34 -0
- package/dist/events/memory-event-store.js +110 -0
- package/dist/events/memory-event-store.js.map +1 -0
- package/dist/events/merge.d.ts +23 -0
- package/dist/events/merge.js +97 -0
- package/dist/events/merge.js.map +1 -0
- package/dist/events/normalized-collector.d.ts +62 -0
- package/dist/events/normalized-collector.js +156 -0
- package/dist/events/normalized-collector.js.map +1 -0
- package/dist/events/schema.d.ts +9 -0
- package/dist/events/schema.js +93 -0
- package/dist/events/schema.js.map +1 -0
- package/dist/events/snapshot.d.ts +32 -0
- package/dist/events/snapshot.js +57 -0
- package/dist/events/snapshot.js.map +1 -0
- package/dist/events/stream-key.d.ts +2 -0
- package/dist/events/stream-key.js +31 -0
- package/dist/events/stream-key.js.map +1 -0
- package/dist/events/types.d.ts +179 -0
- package/dist/events/types.js +66 -0
- package/dist/events/types.js.map +1 -0
- package/dist/health/readiness.d.ts +99 -0
- package/dist/health/readiness.js +202 -0
- package/dist/health/readiness.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/filters.d.ts +8 -0
- package/dist/integrations/filters.js +40 -0
- package/dist/integrations/filters.js.map +1 -0
- package/dist/integrations/map.d.ts +26 -0
- package/dist/integrations/map.js +43 -0
- package/dist/integrations/map.js.map +1 -0
- package/dist/integrations/profile-key.d.ts +19 -0
- package/dist/integrations/profile-key.js +35 -0
- package/dist/integrations/profile-key.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +1 -0
- package/dist/orchestrator/client.d.ts +62 -0
- package/dist/orchestrator/client.js +160 -0
- package/dist/orchestrator/client.js.map +1 -0
- package/dist/playwright/ensure-chromium.d.ts +26 -0
- package/dist/playwright/ensure-chromium.js +60 -0
- package/dist/playwright/ensure-chromium.js.map +1 -0
- package/dist/preview/capture-screenshot.d.ts +52 -0
- package/dist/preview/capture-screenshot.js +158 -0
- package/dist/preview/capture-screenshot.js.map +1 -0
- package/dist/preview/viewer-url.d.ts +82 -0
- package/dist/preview/viewer-url.js +128 -0
- package/dist/preview/viewer-url.js.map +1 -0
- package/dist/process/fault-barrier.d.ts +55 -0
- package/dist/process/fault-barrier.js +113 -0
- package/dist/process/fault-barrier.js.map +1 -0
- package/dist/resume/memory-progress-store.d.ts +39 -0
- package/dist/resume/memory-progress-store.js +82 -0
- package/dist/resume/memory-progress-store.js.map +1 -0
- package/dist/resume/memory-recent-app-store.d.ts +14 -0
- package/dist/resume/memory-recent-app-store.js +27 -0
- package/dist/resume/memory-recent-app-store.js.map +1 -0
- package/dist/resume/memory-turn-store.d.ts +18 -0
- package/dist/resume/memory-turn-store.js +73 -0
- package/dist/resume/memory-turn-store.js.map +1 -0
- package/dist/resume/progress-key.d.ts +21 -0
- package/dist/resume/progress-key.js +58 -0
- package/dist/resume/progress-key.js.map +1 -0
- package/dist/resume/stores.d.ts +14 -0
- package/dist/resume/stores.js +18 -0
- package/dist/resume/stores.js.map +1 -0
- package/dist/resume/types.d.ts +124 -0
- package/dist/resume/types.js +13 -0
- package/dist/resume/types.js.map +1 -0
- package/dist/sabs/agent-facing-text.d.ts +33 -0
- package/dist/sabs/agent-facing-text.js +125 -0
- package/dist/sabs/agent-facing-text.js.map +1 -0
- package/dist/sabs/browser-context.d.ts +53 -0
- package/dist/sabs/browser-context.js +91 -0
- package/dist/sabs/browser-context.js.map +1 -0
- package/dist/sabs/editor-client-methods.d.ts +147 -0
- package/dist/sabs/editor-client-methods.js +715 -0
- package/dist/sabs/editor-client-methods.js.map +1 -0
- package/dist/sabs/recent-tool-runs.d.ts +14 -0
- package/dist/sabs/recent-tool-runs.js +61 -0
- package/dist/sabs/recent-tool-runs.js.map +1 -0
- package/dist/sabs/session-peer.d.ts +351 -0
- package/dist/sabs/session-peer.js +2 -0
- package/dist/sabs/session-peer.js.map +1 -0
- package/dist/sabs/turn-collector.d.ts +152 -0
- package/dist/sabs/turn-collector.js +573 -0
- package/dist/sabs/turn-collector.js.map +1 -0
- package/dist/sabs/websocket-session-peer.d.ts +514 -0
- package/dist/sabs/websocket-session-peer.js +2487 -0
- package/dist/sabs/websocket-session-peer.js.map +1 -0
- package/dist/security/constant-time.d.ts +9 -0
- package/dist/security/constant-time.js +13 -0
- package/dist/security/constant-time.js.map +1 -0
- package/dist/server/client.d.ts +371 -0
- package/dist/server/client.js +572 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/identity-reads.d.ts +76 -0
- package/dist/server/identity-reads.js +209 -0
- package/dist/server/identity-reads.js.map +1 -0
- package/dist/start.d.ts +14 -0
- package/dist/start.js +75 -0
- package/dist/start.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +32 -0
- package/dist/telemetry/metrics.js +57 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/transports/mcp/admin-tools.d.ts +47 -0
- package/dist/transports/mcp/admin-tools.js +164 -0
- package/dist/transports/mcp/admin-tools.js.map +1 -0
- package/dist/transports/mcp/app-status-html.d.ts +15 -0
- package/dist/transports/mcp/app-status-html.js +563 -0
- package/dist/transports/mcp/app-status-html.js.map +1 -0
- package/dist/transports/mcp/decision-elicitation.d.ts +74 -0
- package/dist/transports/mcp/decision-elicitation.js +233 -0
- package/dist/transports/mcp/decision-elicitation.js.map +1 -0
- package/dist/transports/mcp/dispatch-probe.d.ts +24 -0
- package/dist/transports/mcp/dispatch-probe.js +48 -0
- package/dist/transports/mcp/dispatch-probe.js.map +1 -0
- package/dist/transports/mcp/format-tool-content.d.ts +25 -0
- package/dist/transports/mcp/format-tool-content.js +48 -0
- package/dist/transports/mcp/format-tool-content.js.map +1 -0
- package/dist/transports/mcp/mount.d.ts +9 -0
- package/dist/transports/mcp/mount.js +469 -0
- package/dist/transports/mcp/mount.js.map +1 -0
- package/dist/transports/mcp/progress-notifier.d.ts +39 -0
- package/dist/transports/mcp/progress-notifier.js +52 -0
- package/dist/transports/mcp/progress-notifier.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { getBasePluginId, } from "@superblocksteam/shared";
|
|
2
|
+
import { listIntegrationsForClark, toPromptContextIntegration, } from "../integrations/map.js";
|
|
3
|
+
import { sanitizeAgentFacingText, sanitizeAgentFacingValue, } from "./agent-facing-text.js";
|
|
4
|
+
const ADMIN_MCP_SETUP_MESSAGE = "Integrations CRUD is available on this Superblocks MCP via create_integration, update_integration, and list_integrations (same connector as start_app). Use those Admin tools, then continue with edit_app. The headless editor session cannot open the Integrations UI.";
|
|
5
|
+
/**
|
|
6
|
+
* Clark tools that already use PROMPT access rules in the editor. The headless
|
|
7
|
+
* gateway has no human approval UI, so only these may be auto-approved; any
|
|
8
|
+
* other toolName is denied.
|
|
9
|
+
*/
|
|
10
|
+
const HEADLESS_AUTO_APPROVED_TOOLS = new Set([
|
|
11
|
+
"build_deleteFile",
|
|
12
|
+
"build_installPackages",
|
|
13
|
+
"executeRequestToIntegration",
|
|
14
|
+
"runMigrationVerification",
|
|
15
|
+
"testApi",
|
|
16
|
+
"updateMigrationWritePolicy",
|
|
17
|
+
]);
|
|
18
|
+
function taskLabel(summary) {
|
|
19
|
+
const parts = [
|
|
20
|
+
summary.label || summary.taskType,
|
|
21
|
+
summary.state,
|
|
22
|
+
summary.message,
|
|
23
|
+
].filter((part) => Boolean(part && part.trim()));
|
|
24
|
+
return parts.join(": ").slice(0, 160);
|
|
25
|
+
}
|
|
26
|
+
function changeLabel(info) {
|
|
27
|
+
const parts = [info.action || info.status, info.type, info.name].filter((part) => Boolean(part && String(part).trim()));
|
|
28
|
+
return parts.join(" ").slice(0, 160);
|
|
29
|
+
}
|
|
30
|
+
function missingSessionError(action) {
|
|
31
|
+
return new Error(`the headless gateway session cannot ${action} without an active prompt session`);
|
|
32
|
+
}
|
|
33
|
+
function formatIntegrationResult(data) {
|
|
34
|
+
if (data === undefined) {
|
|
35
|
+
return { response: "" };
|
|
36
|
+
}
|
|
37
|
+
if (data === null) {
|
|
38
|
+
return { response: "null" };
|
|
39
|
+
}
|
|
40
|
+
if (typeof data === "string") {
|
|
41
|
+
return { response: sanitizeAgentFacingText(data) };
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
// Sanitized as a value first, then as text. Serializing first leaves only
|
|
45
|
+
// the text patterns to work with, and those cannot see field names: a row
|
|
46
|
+
// with a short `password` column reads as ordinary JSON to them.
|
|
47
|
+
const serialized = JSON.stringify(sanitizeAgentFacingValue(data), null, 2);
|
|
48
|
+
if (serialized) {
|
|
49
|
+
return { response: sanitizeAgentFacingText(serialized) };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
error: sanitizeAgentFacingText(`Failed to serialize integration result: ${error instanceof Error ? error.message : String(error)}`),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { response: sanitizeAgentFacingText(String(data)) };
|
|
58
|
+
}
|
|
59
|
+
function extractIntegrationResult(executionResult) {
|
|
60
|
+
if (!executionResult || typeof executionResult !== "object") {
|
|
61
|
+
return executionResult;
|
|
62
|
+
}
|
|
63
|
+
const result = executionResult;
|
|
64
|
+
if (result.output?.result !== undefined) {
|
|
65
|
+
return result.output.result;
|
|
66
|
+
}
|
|
67
|
+
for (const event of result.events ?? []) {
|
|
68
|
+
const eventResult = event.result?.event?.end?.output?.result;
|
|
69
|
+
if (eventResult !== undefined) {
|
|
70
|
+
return eventResult;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return executionResult;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* True only for interactive IdP / OAuth style failures — not generic HTTP 401s
|
|
77
|
+
* from control-plane hydration (e.g. missing JWT on integration fetch) and not
|
|
78
|
+
* SQL/object permission wording like Snowflake's "does not exist or not authorized".
|
|
79
|
+
*/
|
|
80
|
+
function looksLikeInteractiveAuthRequired(message) {
|
|
81
|
+
return /authentication is required|please authenticate|oauth(?:\s+token)?|idp|login required|token (?:expired|missing)|interactive auth/i.test(message);
|
|
82
|
+
}
|
|
83
|
+
/** Snowflake object RBAC / wrong FQN — not Superblocks or IdP auth. */
|
|
84
|
+
function looksLikeSnowflakeObjectPermissionError(message) {
|
|
85
|
+
return /does not exist or not authorized/i.test(message);
|
|
86
|
+
}
|
|
87
|
+
function looksLikeOrchestratorJwtGate401(message) {
|
|
88
|
+
return /orchestrator.*\b401\b/i.test(message);
|
|
89
|
+
}
|
|
90
|
+
const SNOWFLAKE_OBJECT_PERMISSION_HINT = " This is a Snowflake object permission or schema/name error for the integration's role — not Superblocks re-auth. Fix the fully-qualified name or grant access, then retry.";
|
|
91
|
+
const ORCHESTRATOR_JWT_GATE_HINT = " This is Superblocks/orchestrator auth (the scoped JWT or selected profile), not Snowflake username/password. Do not tell the user to re-enter credentials in the integrations panel. If the build still reaches ready, treat this as a mid-build API-check note rather than a user-facing blocker.";
|
|
92
|
+
function clarifySdkExecutionErrorMessage(message) {
|
|
93
|
+
let clarified = message;
|
|
94
|
+
if (looksLikeOrchestratorJwtGate401(message)) {
|
|
95
|
+
if (!message.includes(ORCHESTRATOR_JWT_GATE_HINT.trim())) {
|
|
96
|
+
clarified = `${message}${ORCHESTRATOR_JWT_GATE_HINT}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (looksLikeSnowflakeObjectPermissionError(message)) {
|
|
100
|
+
if (!message.includes(SNOWFLAKE_OBJECT_PERMISSION_HINT.trim())) {
|
|
101
|
+
clarified = `${message}${SNOWFLAKE_OBJECT_PERMISSION_HINT}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return sanitizeAgentFacingText(clarified);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Runs Clark's executeRequestToIntegration the same way the browser editor
|
|
108
|
+
* does: one inline V2 API step against the orchestrator. Username/password and
|
|
109
|
+
* other stored-credential integrations execute here; interactive IdP auth
|
|
110
|
+
* surfaces as an error asking the user to authenticate in the editor.
|
|
111
|
+
*/
|
|
112
|
+
export async function executeRequestToIntegrationViaOrchestrator(context, payload) {
|
|
113
|
+
let pluginId = payload.integrationHint?.type;
|
|
114
|
+
let integrationName = payload.integrationHint?.displayName ??
|
|
115
|
+
payload.integrationHint?.name ??
|
|
116
|
+
"Integration";
|
|
117
|
+
if (!pluginId) {
|
|
118
|
+
let row;
|
|
119
|
+
try {
|
|
120
|
+
row = await context.server.getIntegration({
|
|
121
|
+
apiKey: context.apiKey,
|
|
122
|
+
integrationId: payload.integrationId,
|
|
123
|
+
});
|
|
124
|
+
if (!row) {
|
|
125
|
+
const rows = await context.server.getSupersetIntegrations({
|
|
126
|
+
apiKey: context.apiKey,
|
|
127
|
+
applicationId: context.applicationId,
|
|
128
|
+
organizationId: context.organizationId,
|
|
129
|
+
});
|
|
130
|
+
row = rows.find((candidate) => candidate.id === payload.integrationId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.warn("gateway failed to resolve integration for execute", {
|
|
135
|
+
applicationId: context.applicationId,
|
|
136
|
+
integrationId: payload.integrationId,
|
|
137
|
+
message: error instanceof Error ? error.message : String(error),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!row?.pluginId) {
|
|
141
|
+
return {
|
|
142
|
+
error: `Integration ${payload.integrationId} not found or not accessible`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
pluginId = row.pluginId;
|
|
146
|
+
integrationName = row.name || integrationName;
|
|
147
|
+
}
|
|
148
|
+
const pluginIdForStep = getBasePluginId(pluginId);
|
|
149
|
+
const now = new Date().toISOString();
|
|
150
|
+
const inlineApiId = `ai-inline-${payload.integrationId}-${Date.now()}`;
|
|
151
|
+
const definition = {
|
|
152
|
+
blocks: [
|
|
153
|
+
{
|
|
154
|
+
name: "InlineQuery",
|
|
155
|
+
step: {
|
|
156
|
+
integration: payload.integrationId,
|
|
157
|
+
[pluginIdForStep]: { ...payload.request },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
metadata: {
|
|
162
|
+
id: inlineApiId,
|
|
163
|
+
name: `${integrationName} AI Query`,
|
|
164
|
+
organization: context.organizationId,
|
|
165
|
+
timestamps: { created: now, updated: now },
|
|
166
|
+
},
|
|
167
|
+
trigger: { application: {} },
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const result = await context.orchestrator.executeV2Api({
|
|
171
|
+
apiKey: context.apiKey,
|
|
172
|
+
body: {
|
|
173
|
+
definition: { api: definition },
|
|
174
|
+
inputs: {},
|
|
175
|
+
options: {
|
|
176
|
+
includeEventOutputs: true,
|
|
177
|
+
includeEvents: true,
|
|
178
|
+
includeResolved: true,
|
|
179
|
+
isAiTriggered: true,
|
|
180
|
+
suppressTracking: true,
|
|
181
|
+
},
|
|
182
|
+
...(context.profileKey
|
|
183
|
+
? { profile: { name: context.profileKey } }
|
|
184
|
+
: {}),
|
|
185
|
+
},
|
|
186
|
+
profileKey: context.profileKey,
|
|
187
|
+
scopedToken: context.sessionToken,
|
|
188
|
+
});
|
|
189
|
+
if (result && typeof result === "object" && "systemError" in result) {
|
|
190
|
+
const systemError = String(result.systemError ??
|
|
191
|
+
"Integration execution failed");
|
|
192
|
+
if (looksLikeInteractiveAuthRequired(systemError)) {
|
|
193
|
+
return {
|
|
194
|
+
error: sanitizeAgentFacingText(`${systemError} Authenticate this integration in the Superblocks editor (open the app edit URL), then ask Superblocks to retry the query.`),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { error: clarifySdkExecutionErrorMessage(systemError) };
|
|
198
|
+
}
|
|
199
|
+
if (result === undefined) {
|
|
200
|
+
return { error: "No response received from integration" };
|
|
201
|
+
}
|
|
202
|
+
const errors = result.errors;
|
|
203
|
+
const unhandled = errors
|
|
204
|
+
?.filter((entry) => !entry.handled)
|
|
205
|
+
.map((entry) => entry.message);
|
|
206
|
+
if (unhandled && unhandled.length > 0) {
|
|
207
|
+
const message = JSON.stringify(unhandled);
|
|
208
|
+
if (looksLikeInteractiveAuthRequired(message)) {
|
|
209
|
+
return {
|
|
210
|
+
error: sanitizeAgentFacingText(`${message} Authenticate this integration in the Superblocks editor (open the app edit URL), then ask Superblocks to retry the query.`),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return { error: clarifySdkExecutionErrorMessage(message) };
|
|
214
|
+
}
|
|
215
|
+
return formatIntegrationResult(extractIntegrationResult(result));
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const message = error instanceof Error
|
|
219
|
+
? error.message
|
|
220
|
+
: "Failed to execute integration query";
|
|
221
|
+
if (looksLikeInteractiveAuthRequired(message)) {
|
|
222
|
+
return {
|
|
223
|
+
error: sanitizeAgentFacingText(`${message} Authenticate this integration in the Superblocks editor (open the app edit URL), then ask Superblocks to retry the query.`),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { error: clarifySdkExecutionErrorMessage(message) };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Prefills Clark's promptContext.integrations the same way the editor does.
|
|
231
|
+
*/
|
|
232
|
+
export async function loadIntegrationsForPrompt(context) {
|
|
233
|
+
const rows = await context.server.getSupersetIntegrations({
|
|
234
|
+
apiKey: context.apiKey,
|
|
235
|
+
applicationId: context.applicationId,
|
|
236
|
+
organizationId: context.organizationId,
|
|
237
|
+
});
|
|
238
|
+
return listIntegrationsForClark(rows);
|
|
239
|
+
}
|
|
240
|
+
export function mapSdkExecutionResponse(response) {
|
|
241
|
+
if (response && typeof response === "object" && "systemError" in response) {
|
|
242
|
+
const systemError = response.systemError;
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
error: {
|
|
246
|
+
code: "SYSTEM_ERROR",
|
|
247
|
+
message: clarifySdkExecutionErrorMessage(typeof systemError === "string"
|
|
248
|
+
? systemError
|
|
249
|
+
: "Orchestrator returned a system error"),
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (response &&
|
|
254
|
+
typeof response === "object" &&
|
|
255
|
+
"errors" in response &&
|
|
256
|
+
Array.isArray(response.errors) &&
|
|
257
|
+
response.errors.length > 0) {
|
|
258
|
+
const first = response
|
|
259
|
+
.errors[0];
|
|
260
|
+
return {
|
|
261
|
+
success: false,
|
|
262
|
+
error: {
|
|
263
|
+
code: "EXECUTION_ERROR",
|
|
264
|
+
message: clarifySdkExecutionErrorMessage(first?.message ?? "API execution failed"),
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (response && typeof response === "object") {
|
|
269
|
+
const output = response.output
|
|
270
|
+
?.result;
|
|
271
|
+
return {
|
|
272
|
+
success: true,
|
|
273
|
+
output: sanitizeAgentFacingValue(output),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
success: false,
|
|
278
|
+
error: {
|
|
279
|
+
code: "EMPTY_RESPONSE",
|
|
280
|
+
message: "No response from orchestrator",
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Server -> editor calls the dev server makes into the peer driving a turn,
|
|
286
|
+
* whether that peer negotiated protocol "editor" or "gateway".
|
|
287
|
+
*
|
|
288
|
+
* Leaving these unimplemented is not inert. ISocket answers an unknown method
|
|
289
|
+
* with `{ code: 2, message: "unknown method <name>" }`, and the dev server does
|
|
290
|
+
* not catch every one of those rejections — `resetScope` in particular escalates
|
|
291
|
+
* to `unhandledRejection`, which exits the sandbox with "Dev server fatal exit"
|
|
292
|
+
* and takes the in-flight Clark build down with it. A browser editor answers
|
|
293
|
+
* all of them, so a held gateway socket has to as well.
|
|
294
|
+
*
|
|
295
|
+
* Keyed on `EditorClientMethods` rather than a hand-kept list so that a method
|
|
296
|
+
* added to the protocol upstream fails this package's typecheck instead of
|
|
297
|
+
* reaching a sandbox and crashing it. Same handler surface as
|
|
298
|
+
* `createEditorHandlers` in vite-plugin-file-sync's dev-server-ws-smoke-client,
|
|
299
|
+
* the other non-browser peer that speaks this protocol; it is not imported from
|
|
300
|
+
* there because the gateway does not depend on the vite plugin.
|
|
301
|
+
*
|
|
302
|
+
* When a listener is provided, chat, generation-state, task/change, and
|
|
303
|
+
* preview-ready pushes are forwarded so the gateway can wait for Clark's turn
|
|
304
|
+
* and surface progress over MCP.
|
|
305
|
+
*/
|
|
306
|
+
export function createEditorClientMethods(getListener, onInbound, deps) {
|
|
307
|
+
// Notifications and state pushes. The gateway keeps no editor state unless a
|
|
308
|
+
// turn wait is armed, so acknowledging is the whole contract — the dev
|
|
309
|
+
// server only needs to know the peer is alive and listening.
|
|
310
|
+
const ack = [async () => undefined];
|
|
311
|
+
const setupUnavailable = [
|
|
312
|
+
async () => ({
|
|
313
|
+
error: ADMIN_MCP_SETUP_MESSAGE,
|
|
314
|
+
success: false,
|
|
315
|
+
}),
|
|
316
|
+
];
|
|
317
|
+
const handlers = {
|
|
318
|
+
aiApplyApplicationEnvironmentSettings: ack,
|
|
319
|
+
aiClearToolPermission: ack,
|
|
320
|
+
aiPushApiUsageSummary: ack,
|
|
321
|
+
aiPushChangeInfo: [
|
|
322
|
+
async (info) => {
|
|
323
|
+
const label = changeLabel(info);
|
|
324
|
+
if (!label) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
getListener?.()?.({ kind: "change_info", label });
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
aiPushContextUsed: ack,
|
|
331
|
+
aiPushErrorSummary: ack,
|
|
332
|
+
aiPushMessage: [
|
|
333
|
+
async (message) => {
|
|
334
|
+
const listener = getListener?.();
|
|
335
|
+
listener?.({ kind: "message", message });
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
aiPushPolicyAgentStep: ack,
|
|
339
|
+
aiPushPromptInterpretation: ack,
|
|
340
|
+
aiPushTaskStatus: [
|
|
341
|
+
async (summary) => {
|
|
342
|
+
const label = taskLabel(summary);
|
|
343
|
+
if (!label) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
getListener?.()?.({ kind: "task_status", label });
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
aiPushTokenUsage: ack,
|
|
350
|
+
aiRequestToolPermission: [
|
|
351
|
+
async (payload) => {
|
|
352
|
+
// Headless gateway has no editor approval UI. Auto-approve only the
|
|
353
|
+
// Clark agent tools that already have PROMPT access rules and must run
|
|
354
|
+
// unattended for local builds (testApi WRITE, migration checks, etc.).
|
|
355
|
+
// Unknown tools are denied by default.
|
|
356
|
+
if (!deps) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const toolName = payload.toolName?.trim() ?? "";
|
|
360
|
+
const approved = HEADLESS_AUTO_APPROVED_TOOLS.has(toolName);
|
|
361
|
+
await deps.respondToToolPermission({
|
|
362
|
+
approved,
|
|
363
|
+
messageId: payload.messageId,
|
|
364
|
+
requestId: payload.requestId ?? payload.messageId,
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
// Says whether Clark left uncommitted work behind. The gateway needs it
|
|
369
|
+
// for the same reason the browser editor does: a pending draft has to be
|
|
370
|
+
// accepted before the app the edit URL serves matches what Clark built.
|
|
371
|
+
aiSetDraftState: [
|
|
372
|
+
async (payload) => {
|
|
373
|
+
getListener?.()?.({
|
|
374
|
+
kind: "draft_state",
|
|
375
|
+
hasDraft: Boolean(payload?.hasDraft),
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
aiSetGenerationState: [
|
|
380
|
+
async (snapshot) => {
|
|
381
|
+
if (typeof snapshot?.state === "string") {
|
|
382
|
+
getListener?.()?.({
|
|
383
|
+
hasError: snapshot.hasError,
|
|
384
|
+
hasSuggestions: snapshot.hasSuggestions,
|
|
385
|
+
kind: "generation_state",
|
|
386
|
+
state: snapshot.state,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
appFactsChanged: ack,
|
|
392
|
+
clarkLog: ack,
|
|
393
|
+
closedByServer: ack,
|
|
394
|
+
error: ack,
|
|
395
|
+
manualDeleteApi: ack,
|
|
396
|
+
manualUpdateApi: ack,
|
|
397
|
+
notifyDeploySuccess: ack,
|
|
398
|
+
reloadIframe: [
|
|
399
|
+
async () => {
|
|
400
|
+
getListener?.()?.({ kind: "reload_iframe" });
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
releasingLock: ack,
|
|
404
|
+
requestGitRefresh: ack,
|
|
405
|
+
resetScope: ack,
|
|
406
|
+
setApiDependencies: ack,
|
|
407
|
+
setDegradedMode: ack,
|
|
408
|
+
syncStatus: [
|
|
409
|
+
async (payload) => {
|
|
410
|
+
getListener?.()?.({
|
|
411
|
+
kind: "sync_status",
|
|
412
|
+
status: typeof payload?.status === "string" ? payload.status : undefined,
|
|
413
|
+
});
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
updateApiInputs: ack,
|
|
417
|
+
updateScope: ack,
|
|
418
|
+
// No browser tab means no viewport to photograph. The flag matters more
|
|
419
|
+
// than the message: it tells the ai-service this was a refusal, not a
|
|
420
|
+
// failed capture, so Clark can fall back to the sandbox's own browser
|
|
421
|
+
// instead of reporting that it cannot see the app it just built.
|
|
422
|
+
aiCaptureScreenshot: [
|
|
423
|
+
async () => ({
|
|
424
|
+
error: "the headless gateway session has no browser viewport",
|
|
425
|
+
success: false,
|
|
426
|
+
viewportUnavailable: true,
|
|
427
|
+
}),
|
|
428
|
+
],
|
|
429
|
+
aiCloseIntegrationSetup: [async () => ({ success: true })],
|
|
430
|
+
aiDeleteIntegration: setupUnavailable,
|
|
431
|
+
aiExecuteRequestToIntegration: [
|
|
432
|
+
async (payload) => {
|
|
433
|
+
const context = deps?.getContext();
|
|
434
|
+
if (!context) {
|
|
435
|
+
return {
|
|
436
|
+
error: "the headless gateway session cannot execute integration requests without an active prompt session",
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const result = await executeRequestToIntegrationViaOrchestrator(context, payload);
|
|
440
|
+
const name = payload.integrationHint?.name ||
|
|
441
|
+
payload.integrationId ||
|
|
442
|
+
"integration";
|
|
443
|
+
deps?.onToolRun?.({
|
|
444
|
+
detail: result,
|
|
445
|
+
kind: "test_integration",
|
|
446
|
+
name,
|
|
447
|
+
success: !("error" in result && result.error),
|
|
448
|
+
});
|
|
449
|
+
return result;
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
aiExecuteSdkApi: [
|
|
453
|
+
async (payload) => {
|
|
454
|
+
const context = deps?.getContext();
|
|
455
|
+
if (!context) {
|
|
456
|
+
throw missingSessionError("execute SDK APIs");
|
|
457
|
+
}
|
|
458
|
+
if (!payload.entryPoint) {
|
|
459
|
+
const missing = {
|
|
460
|
+
success: false,
|
|
461
|
+
error: {
|
|
462
|
+
code: "UNKNOWN_API",
|
|
463
|
+
message: `No entryPoint registered for API "${payload.apiName}". Was it discovered?`,
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
deps?.onToolRun?.({
|
|
467
|
+
detail: missing,
|
|
468
|
+
kind: "test_api",
|
|
469
|
+
name: payload.apiName,
|
|
470
|
+
success: false,
|
|
471
|
+
});
|
|
472
|
+
return missing;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const inSync = await context.waitUntilFilesSynced();
|
|
476
|
+
if (!inSync) {
|
|
477
|
+
const syncing = {
|
|
478
|
+
success: false,
|
|
479
|
+
error: {
|
|
480
|
+
code: "SYSTEM_ERROR",
|
|
481
|
+
message: "The API files are still syncing. Wait for the current edit to finish saving, then retry testApi.",
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
deps?.onToolRun?.({
|
|
485
|
+
detail: syncing,
|
|
486
|
+
kind: "test_api",
|
|
487
|
+
name: payload.apiName,
|
|
488
|
+
success: false,
|
|
489
|
+
});
|
|
490
|
+
return syncing;
|
|
491
|
+
}
|
|
492
|
+
const response = await context.orchestrator.executeSdkApiV3({
|
|
493
|
+
apiKey: context.apiKey,
|
|
494
|
+
applicationId: context.applicationId,
|
|
495
|
+
branchName: context.branchName,
|
|
496
|
+
entryPoint: payload.entryPoint,
|
|
497
|
+
exportName: payload.exportName,
|
|
498
|
+
inputs: payload.input ?? {},
|
|
499
|
+
profileKey: context.profileKey,
|
|
500
|
+
scopedToken: context.sessionToken,
|
|
501
|
+
});
|
|
502
|
+
const mapped = mapSdkExecutionResponse(response);
|
|
503
|
+
deps?.onToolRun?.({
|
|
504
|
+
detail: mapped,
|
|
505
|
+
kind: "test_api",
|
|
506
|
+
name: payload.apiName,
|
|
507
|
+
success: mapped.success,
|
|
508
|
+
});
|
|
509
|
+
return mapped;
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
const failed = {
|
|
513
|
+
success: false,
|
|
514
|
+
error: {
|
|
515
|
+
code: "SYSTEM_ERROR",
|
|
516
|
+
message: error instanceof Error
|
|
517
|
+
? clarifySdkExecutionErrorMessage(error.message)
|
|
518
|
+
: "Failed to execute SDK API",
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
deps?.onToolRun?.({
|
|
522
|
+
detail: failed,
|
|
523
|
+
kind: "test_api",
|
|
524
|
+
name: payload.apiName,
|
|
525
|
+
success: false,
|
|
526
|
+
});
|
|
527
|
+
return failed;
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
aiExecuteV2Api: [
|
|
532
|
+
async (payload) => {
|
|
533
|
+
const context = deps?.getContext();
|
|
534
|
+
if (!context) {
|
|
535
|
+
throw missingSessionError("execute V2 APIs");
|
|
536
|
+
}
|
|
537
|
+
if (payload.api.type === "fetch") {
|
|
538
|
+
return {
|
|
539
|
+
systemError: `V2 API fetch for '${payload.api.apiName}' requires the browser editor; use SDK APIs (aiExecuteSdkApi) from Gateway, or open the app in the UI.`,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const now = new Date().toISOString();
|
|
543
|
+
const definition = {
|
|
544
|
+
...payload.api.definition,
|
|
545
|
+
metadata: {
|
|
546
|
+
...(payload.api.definition.metadata ?? {}),
|
|
547
|
+
organization: context.organizationId,
|
|
548
|
+
timestamps: { created: now, updated: now },
|
|
549
|
+
},
|
|
550
|
+
trigger: { application: {} },
|
|
551
|
+
};
|
|
552
|
+
try {
|
|
553
|
+
const result = await context.orchestrator.executeV2Api({
|
|
554
|
+
apiKey: context.apiKey,
|
|
555
|
+
body: {
|
|
556
|
+
definition: { api: definition },
|
|
557
|
+
inputs: payload.inputs ?? {},
|
|
558
|
+
options: {
|
|
559
|
+
includeEventOutputs: true,
|
|
560
|
+
includeEvents: true,
|
|
561
|
+
includeResolved: true,
|
|
562
|
+
isAiTriggered: true,
|
|
563
|
+
suppressTracking: true,
|
|
564
|
+
},
|
|
565
|
+
...(context.profileKey
|
|
566
|
+
? { profile: { name: context.profileKey } }
|
|
567
|
+
: {}),
|
|
568
|
+
},
|
|
569
|
+
profileKey: context.profileKey,
|
|
570
|
+
scopedToken: context.sessionToken,
|
|
571
|
+
});
|
|
572
|
+
if (result && typeof result === "object" && "systemError" in result) {
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
if (result === undefined) {
|
|
576
|
+
return { systemError: "No response received from API execution" };
|
|
577
|
+
}
|
|
578
|
+
const outputs = result.output?.result ?? {};
|
|
579
|
+
return {
|
|
580
|
+
errors: result.errors,
|
|
581
|
+
outputs,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
return {
|
|
586
|
+
systemError: error instanceof Error
|
|
587
|
+
? error.message
|
|
588
|
+
: "Failed to execute V2 API",
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
aiGetIntegration: [
|
|
594
|
+
async (payload) => {
|
|
595
|
+
const context = deps?.getContext();
|
|
596
|
+
if (!context) {
|
|
597
|
+
throw missingSessionError("provide integration metadata");
|
|
598
|
+
}
|
|
599
|
+
// A rejection here cancels Clark's turn, so every lookup failure
|
|
600
|
+
// degrades to the unknown-integration placeholder below.
|
|
601
|
+
let row;
|
|
602
|
+
try {
|
|
603
|
+
row = await context.server.getIntegration({
|
|
604
|
+
apiKey: context.apiKey,
|
|
605
|
+
integrationId: payload.integrationId,
|
|
606
|
+
});
|
|
607
|
+
if (!row) {
|
|
608
|
+
const rows = await context.server.getSupersetIntegrations({
|
|
609
|
+
apiKey: context.apiKey,
|
|
610
|
+
applicationId: context.applicationId,
|
|
611
|
+
organizationId: context.organizationId,
|
|
612
|
+
});
|
|
613
|
+
row = rows.find((candidate) => candidate.id === payload.integrationId);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
console.warn("gateway failed to load integration for Clark", {
|
|
618
|
+
applicationId: context.applicationId,
|
|
619
|
+
integrationId: payload.integrationId,
|
|
620
|
+
message: error instanceof Error ? error.message : String(error),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
if (!row) {
|
|
624
|
+
return {
|
|
625
|
+
displayName: payload.integrationId,
|
|
626
|
+
id: payload.integrationId,
|
|
627
|
+
metadata: {},
|
|
628
|
+
name: payload.integrationId,
|
|
629
|
+
type: "unknown",
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
let metadata = {};
|
|
633
|
+
try {
|
|
634
|
+
metadata = await context.orchestrator.getMetadata({
|
|
635
|
+
apiKey: context.apiKey,
|
|
636
|
+
integrationId: payload.integrationId,
|
|
637
|
+
profileKey: context.profileKey,
|
|
638
|
+
scopedToken: context.sessionToken,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
metadata = {};
|
|
643
|
+
}
|
|
644
|
+
return toPromptContextIntegration(row, metadata);
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
aiGetIntegrationDependents: [
|
|
648
|
+
async () => ({
|
|
649
|
+
error: "the headless gateway session cannot inspect integration dependents",
|
|
650
|
+
success: false,
|
|
651
|
+
}),
|
|
652
|
+
],
|
|
653
|
+
aiGetIntegrationFormSchema: [
|
|
654
|
+
async () => ({
|
|
655
|
+
error: ADMIN_MCP_SETUP_MESSAGE,
|
|
656
|
+
success: false,
|
|
657
|
+
}),
|
|
658
|
+
],
|
|
659
|
+
aiGetIntegrations: [
|
|
660
|
+
async () => {
|
|
661
|
+
const context = deps?.getContext();
|
|
662
|
+
if (!context) {
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
return await loadIntegrationsForPrompt(context);
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
// Clark cancels the whole turn if this rejects, so a failed lookup
|
|
670
|
+
// degrades to "no integrations" instead of killing the build.
|
|
671
|
+
console.warn("gateway failed to list integrations for Clark", {
|
|
672
|
+
applicationId: context.applicationId,
|
|
673
|
+
message: error instanceof Error ? error.message : String(error),
|
|
674
|
+
});
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
aiGetOpenApiSpec: [async () => undefined],
|
|
680
|
+
aiNavigatePreviewRoute: [
|
|
681
|
+
async () => ({
|
|
682
|
+
error: "the headless gateway session has no browser route",
|
|
683
|
+
success: false,
|
|
684
|
+
}),
|
|
685
|
+
],
|
|
686
|
+
aiOpenExistingIntegration: setupUnavailable,
|
|
687
|
+
aiOpenIntegrationSetup: setupUnavailable,
|
|
688
|
+
aiSaveIntegration: setupUnavailable,
|
|
689
|
+
aiTestIntegrationConnection: [
|
|
690
|
+
async () => ({
|
|
691
|
+
error: {
|
|
692
|
+
details: "",
|
|
693
|
+
message: ADMIN_MCP_SETUP_MESSAGE,
|
|
694
|
+
},
|
|
695
|
+
success: false,
|
|
696
|
+
}),
|
|
697
|
+
],
|
|
698
|
+
aiUpdateIntegrationFields: setupUnavailable,
|
|
699
|
+
};
|
|
700
|
+
const observed = handlers;
|
|
701
|
+
if (onInbound) {
|
|
702
|
+
for (const key of Object.keys(observed)) {
|
|
703
|
+
const chain = observed[key];
|
|
704
|
+
observed[key] = chain.map((handler) => async (...args) => {
|
|
705
|
+
onInbound(key);
|
|
706
|
+
return handler(...args);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// ISocket's MethodHandlers is keyed on the socket's own generic parameters,
|
|
711
|
+
// which the gateway leaves empty because it calls only one server method.
|
|
712
|
+
// Exhaustiveness is already enforced above, on the protocol type.
|
|
713
|
+
return observed;
|
|
714
|
+
}
|
|
715
|
+
//# sourceMappingURL=editor-client-methods.js.map
|