@superblocksteam/gateway 2.0.165-next.0 → 2.0.166-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/dist/agents/resolve-orchestrator-url.d.ts +19 -1
- package/dist/agents/resolve-orchestrator-url.js +71 -20
- package/dist/agents/resolve-orchestrator-url.js.map +1 -1
- package/dist/capabilities/integration-metadata.d.ts +3 -0
- package/dist/capabilities/integration-metadata.js +34 -11
- package/dist/capabilities/integration-metadata.js.map +1 -1
- package/dist/capabilities/lifecycle.d.ts +16 -1
- package/dist/capabilities/lifecycle.js +72 -25
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/query-integration.d.ts +5 -0
- package/dist/capabilities/query-integration.js +61 -18
- package/dist/capabilities/query-integration.js.map +1 -1
- package/dist/{transports/mcp → capture}/cowork-editor-url.d.ts +2 -2
- package/dist/capture/cowork-editor-url.js +12 -0
- package/dist/capture/cowork-editor-url.js.map +1 -0
- package/dist/integrations/profile-key.d.ts +50 -6
- package/dist/integrations/profile-key.js +101 -22
- package/dist/integrations/profile-key.js.map +1 -1
- package/dist/integrations/resolve-profile.d.ts +30 -0
- package/dist/integrations/resolve-profile.js +104 -0
- package/dist/integrations/resolve-profile.js.map +1 -0
- package/dist/orchestrator/client.d.ts +1 -1
- package/dist/orchestrator/client.js +1 -1
- package/dist/orchestrator/client.js.map +1 -1
- package/dist/sabs/editor-client-methods.d.ts +7 -2
- package/dist/sabs/editor-client-methods.js +10 -2
- package/dist/sabs/editor-client-methods.js.map +1 -1
- package/dist/sabs/recent-tool-runs.d.ts +1 -1
- package/dist/sabs/recent-tool-runs.js +2 -2
- package/dist/sabs/recent-tool-runs.js.map +1 -1
- package/dist/sabs/websocket-session-peer.d.ts +31 -0
- package/dist/sabs/websocket-session-peer.js +202 -35
- package/dist/sabs/websocket-session-peer.js.map +1 -1
- package/dist/server/client.d.ts +67 -15
- package/dist/server/client.js +403 -51
- package/dist/server/client.js.map +1 -1
- package/dist/telemetry/metrics.d.ts +14 -0
- package/dist/telemetry/metrics.js +25 -0
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/check-app-progress.js +6 -2
- package/dist/transports/mcp/instructions/tools/check-app-progress.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/copy.d.ts +2 -0
- package/dist/transports/mcp/instructions/tools/copy.js +31 -1
- package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js +4 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/get-integration-metadata.js +3 -0
- package/dist/transports/mcp/instructions/tools/get-integration-metadata.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/start-app.js +4 -1
- package/dist/transports/mcp/instructions/tools/start-app.js.map +1 -1
- package/dist/transports/mcp/mount.d.ts +1 -1
- package/dist/transports/mcp/mount.js +80 -10
- package/dist/transports/mcp/mount.js.map +1 -1
- package/dist/transports/mcp/plan-approval.d.ts +29 -0
- package/dist/transports/mcp/plan-approval.js +72 -18
- package/dist/transports/mcp/plan-approval.js.map +1 -1
- package/package.json +5 -5
- package/dist/transports/mcp/cowork-editor-url.js +0 -10
- package/dist/transports/mcp/cowork-editor-url.js.map +0 -1
package/dist/server/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uploadAttachmentToServerApi } from "@superblocksteam/shared";
|
|
2
|
-
import { recordIdentityReadOutcome } from "../telemetry/metrics.js";
|
|
2
|
+
import { recordFactsFlagBootstrapOutcome, recordIdentityReadOutcome, recordServerReadRetryOutcome, } from "../telemetry/metrics.js";
|
|
3
3
|
import { withGuardedIdentityReads, } from "./identity-reads.js";
|
|
4
4
|
const MAX_DETAIL_LENGTH = 300;
|
|
5
5
|
/**
|
|
@@ -31,12 +31,98 @@ function detailSuffix(body) {
|
|
|
31
31
|
}
|
|
32
32
|
export class ServerHttpError extends Error {
|
|
33
33
|
status;
|
|
34
|
-
|
|
34
|
+
retryAfterMs;
|
|
35
|
+
constructor(message, status, retryAfterMs) {
|
|
35
36
|
super(message);
|
|
36
37
|
this.status = status;
|
|
38
|
+
this.retryAfterMs = retryAfterMs;
|
|
37
39
|
this.name = "ServerHttpError";
|
|
38
40
|
}
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* This client gave up on a request before the server answered it.
|
|
44
|
+
*
|
|
45
|
+
* Told apart from a lost connection because the two deserve different
|
|
46
|
+
* treatment on a retry: a connection that was refused or dropped is cheap to
|
|
47
|
+
* try again, while repeating a request that already burned its whole bound
|
|
48
|
+
* multiplies the wait a caller is sitting through.
|
|
49
|
+
*/
|
|
50
|
+
export class ServerTimeoutError extends Error {
|
|
51
|
+
constructor(message, options) {
|
|
52
|
+
super(message, options);
|
|
53
|
+
this.name = "ServerTimeoutError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const RETRYABLE_TRANSPORT_CODES = new Set([
|
|
57
|
+
"EAI_AGAIN",
|
|
58
|
+
"ECONNABORTED",
|
|
59
|
+
"ECONNREFUSED",
|
|
60
|
+
"ECONNRESET",
|
|
61
|
+
"EHOSTUNREACH",
|
|
62
|
+
"ENETDOWN",
|
|
63
|
+
"ENETUNREACH",
|
|
64
|
+
"EPIPE",
|
|
65
|
+
"ETIMEDOUT",
|
|
66
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
67
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
68
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
69
|
+
"UND_ERR_SOCKET",
|
|
70
|
+
]);
|
|
71
|
+
const UNSENT_TRANSPORT_CODES = new Set([
|
|
72
|
+
"EAI_AGAIN",
|
|
73
|
+
"ECONNREFUSED",
|
|
74
|
+
"EHOSTUNREACH",
|
|
75
|
+
"ENETDOWN",
|
|
76
|
+
"ENETUNREACH",
|
|
77
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
78
|
+
]);
|
|
79
|
+
function errorCode(error, depth = 0) {
|
|
80
|
+
if (depth >= 4 || !error || typeof error !== "object") {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const { cause, code } = error;
|
|
84
|
+
if (typeof code === "string") {
|
|
85
|
+
return code;
|
|
86
|
+
}
|
|
87
|
+
const causeCode = errorCode(cause, depth + 1);
|
|
88
|
+
if (causeCode) {
|
|
89
|
+
return causeCode;
|
|
90
|
+
}
|
|
91
|
+
if (error instanceof AggregateError) {
|
|
92
|
+
for (const nested of error.errors) {
|
|
93
|
+
const nestedCode = errorCode(nested, depth + 1);
|
|
94
|
+
if (nestedCode) {
|
|
95
|
+
return nestedCode;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The connection failed: refused, reset, or dropped before the answer was
|
|
103
|
+
* complete. Named so a retry can cover it without also covering a response
|
|
104
|
+
* that did arrive and turned out to be unreadable.
|
|
105
|
+
*
|
|
106
|
+
* Most failures do not prove whether the request reached a server, so repeat
|
|
107
|
+
* safety remains the call site's decision. `requestNotSent` is reserved for
|
|
108
|
+
* pre-connect failures whose error code proves otherwise.
|
|
109
|
+
*/
|
|
110
|
+
export class ServerTransportError extends Error {
|
|
111
|
+
code;
|
|
112
|
+
requestNotSent;
|
|
113
|
+
retryable;
|
|
114
|
+
constructor(message, options) {
|
|
115
|
+
const code = errorCode(options.cause);
|
|
116
|
+
super(code ? `${message} (${code})` : message, options);
|
|
117
|
+
this.name = "ServerTransportError";
|
|
118
|
+
this.code = code;
|
|
119
|
+
this.requestNotSent =
|
|
120
|
+
options.requestNotSent === true &&
|
|
121
|
+
code !== undefined &&
|
|
122
|
+
UNSENT_TRANSPORT_CODES.has(code);
|
|
123
|
+
this.retryable = code !== undefined && RETRYABLE_TRANSPORT_CODES.has(code);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
40
126
|
/**
|
|
41
127
|
* Generous enough for a sandbox start.
|
|
42
128
|
*
|
|
@@ -73,21 +159,142 @@ const DEFAULT_IDENTITY_TIMEOUT_MS = 30_000;
|
|
|
73
159
|
* kept going straight at an endpoint already asking for less load.
|
|
74
160
|
*/
|
|
75
161
|
function identityEndpointUnhealthy(error) {
|
|
76
|
-
return !(error instanceof ServerHttpError &&
|
|
77
|
-
|
|
78
|
-
|
|
162
|
+
return !(error instanceof ServerHttpError && !isTransientHttpStatus(error.status));
|
|
163
|
+
}
|
|
164
|
+
function isTransientHttpStatus(status) {
|
|
165
|
+
return status >= 500 || status === 429;
|
|
79
166
|
}
|
|
80
167
|
/**
|
|
81
|
-
*
|
|
168
|
+
* How long a read waits before each re-send, by what failed it.
|
|
169
|
+
*
|
|
170
|
+
* Only reads are retried, and only because the call sites that leave `retry`
|
|
171
|
+
* on are pure lookups: a repeat of one changes nothing. A failed POST may well
|
|
172
|
+
* have taken effect, and re-sending one can deploy or commit twice; a read
|
|
173
|
+
* that provisions, or that already sits in its own poll loop, opts out.
|
|
174
|
+
*
|
|
175
|
+
* A transient status is a fast answer from a route that is up, so a short
|
|
176
|
+
* pause is enough. 429 counts as transient alongside 5xx: it is the route
|
|
177
|
+
* reporting its own capacity, and backing off is the whole point of it.
|
|
178
|
+
* Retry-After replaces that pause when it is at most five seconds; a longer
|
|
179
|
+
* server window is surfaced immediately rather than parking a tool call.
|
|
180
|
+
*
|
|
181
|
+
* A lost connection is usually a deployment rolling through the API, which
|
|
182
|
+
* refuses and resets connections for a few seconds. This schedule waits that
|
|
183
|
+
* out, and still finishes well inside the ten seconds a tool call waits on
|
|
184
|
+
* identity. A timeout is never retried: that attempt already spent its whole
|
|
185
|
+
* bound, and sending it again spends another.
|
|
82
186
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* commit twice.
|
|
187
|
+
* Each delay is one retry: transient statuses get two, lost connections get
|
|
188
|
+
* four.
|
|
86
189
|
*/
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
190
|
+
const TRANSIENT_STATUS_RETRY_DELAYS_MS = [100, 200];
|
|
191
|
+
const LOST_CONNECTION_RETRY_DELAYS_MS = [250, 500, 1_000, 2_000];
|
|
192
|
+
const MAX_RETRY_AFTER_MS = 5_000;
|
|
193
|
+
const MIN_RETRY_ATTEMPT_MS = 50;
|
|
194
|
+
function jitter(delayMs) {
|
|
195
|
+
return Math.ceil(delayMs * (0.75 + Math.random() * 0.25));
|
|
196
|
+
}
|
|
197
|
+
function jitterAfterMinimum(delayMs) {
|
|
198
|
+
return Math.ceil(delayMs * (1 + Math.random() * 0.25));
|
|
199
|
+
}
|
|
200
|
+
class ReadRetrySchedule {
|
|
201
|
+
lostConnections = 0;
|
|
202
|
+
transientStatuses = 0;
|
|
203
|
+
next(error) {
|
|
204
|
+
if (error instanceof ServerHttpError) {
|
|
205
|
+
if (!isTransientHttpStatus(error.status)) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const delayMs = TRANSIENT_STATUS_RETRY_DELAYS_MS[this.transientStatuses++];
|
|
209
|
+
if (delayMs === undefined) {
|
|
210
|
+
return "exhausted";
|
|
211
|
+
}
|
|
212
|
+
if (error.retryAfterMs === undefined) {
|
|
213
|
+
return { cause: "http", waitMs: jitter(delayMs) };
|
|
214
|
+
}
|
|
215
|
+
return error.retryAfterMs > MAX_RETRY_AFTER_MS
|
|
216
|
+
? undefined
|
|
217
|
+
: {
|
|
218
|
+
cause: "http",
|
|
219
|
+
waitMs: jitterAfterMinimum(Math.max(delayMs, error.retryAfterMs)),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (error instanceof ServerTransportError && error.retryable) {
|
|
223
|
+
const delayMs = LOST_CONNECTION_RETRY_DELAYS_MS[this.lostConnections++];
|
|
224
|
+
return delayMs === undefined
|
|
225
|
+
? "exhausted"
|
|
226
|
+
: { cause: "transport", waitMs: jitter(delayMs) };
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function parseRetryAfterMs(value) {
|
|
232
|
+
const retryAfter = value?.trim();
|
|
233
|
+
if (!retryAfter) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
const seconds = Number(retryAfter);
|
|
237
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
238
|
+
return Math.ceil(seconds * 1_000);
|
|
239
|
+
}
|
|
240
|
+
const atMs = Date.parse(retryAfter);
|
|
241
|
+
return Number.isNaN(atMs) ? undefined : Math.max(0, atMs - Date.now());
|
|
242
|
+
}
|
|
243
|
+
function withAttemptCount(error, attempts) {
|
|
244
|
+
if (attempts < 2 || !(error instanceof Error)) {
|
|
245
|
+
return error;
|
|
246
|
+
}
|
|
247
|
+
error.message = `${error.message}; ${attempts} attempts failed`;
|
|
248
|
+
return error;
|
|
249
|
+
}
|
|
250
|
+
function delay(ms, signal) {
|
|
251
|
+
if (signal?.aborted) {
|
|
252
|
+
return Promise.reject(signal.reason);
|
|
253
|
+
}
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const onAbort = () => {
|
|
256
|
+
clearTimeout(timeout);
|
|
257
|
+
reject(signal?.reason);
|
|
258
|
+
};
|
|
259
|
+
const timeout = setTimeout(() => {
|
|
260
|
+
signal?.removeEventListener("abort", onAbort);
|
|
261
|
+
resolve();
|
|
262
|
+
}, ms);
|
|
263
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* One attempt at one request: the signal that bounds it, and the judgement of
|
|
268
|
+
* why it failed, applied alike to the connection and to the body read so a
|
|
269
|
+
* connection lost mid-response counts the same as one refused up front.
|
|
270
|
+
*/
|
|
271
|
+
class ServerAttempt {
|
|
272
|
+
path;
|
|
273
|
+
boundMs;
|
|
274
|
+
callerSignal;
|
|
275
|
+
timeoutSignal;
|
|
276
|
+
signal;
|
|
277
|
+
constructor(path, boundMs, callerSignal) {
|
|
278
|
+
this.path = path;
|
|
279
|
+
this.boundMs = boundMs;
|
|
280
|
+
this.callerSignal = callerSignal;
|
|
281
|
+
this.timeoutSignal = AbortSignal.timeout(boundMs);
|
|
282
|
+
this.signal = callerSignal
|
|
283
|
+
? AbortSignal.any([callerSignal, this.timeoutSignal])
|
|
284
|
+
: this.timeoutSignal;
|
|
285
|
+
}
|
|
286
|
+
/** What to throw for a fetch or body read that rejected with `error`. */
|
|
287
|
+
failure(error, lost, requestNotSent = false) {
|
|
288
|
+
// The caller's own abort is neither our timeout nor a lost connection,
|
|
289
|
+
// and wrapping it as one would hand it to the read retry.
|
|
290
|
+
if (this.callerSignal?.aborted) {
|
|
291
|
+
return error;
|
|
292
|
+
}
|
|
293
|
+
if (this.timeoutSignal.aborted) {
|
|
294
|
+
return new ServerTimeoutError(`Server ${this.path} timed out after ${this.boundMs}ms`, { cause: error });
|
|
295
|
+
}
|
|
296
|
+
return new ServerTransportError(`Server ${this.path} ${lost}: ${error instanceof Error ? error.message : String(error)}`, { cause: error, requestNotSent });
|
|
297
|
+
}
|
|
91
298
|
}
|
|
92
299
|
const DEPLOY_STATUSES = [
|
|
93
300
|
"awaiting_ack",
|
|
@@ -188,67 +395,152 @@ export class HttpSuperblocksServerClient {
|
|
|
188
395
|
return body.data;
|
|
189
396
|
}
|
|
190
397
|
/**
|
|
191
|
-
* Issues the request,
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* and leave the caller guessing whether their account was broken.
|
|
398
|
+
* Issues the request, sending a read again when what failed it says nothing
|
|
399
|
+
* about the request itself: a transient status, or a connection that was
|
|
400
|
+
* lost. {@link ReadRetrySchedule} decides which failures qualify and for how
|
|
401
|
+
* long.
|
|
196
402
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
403
|
+
* A transient failure on the identity lookup used to fail the whole tool
|
|
404
|
+
* call and leave the caller guessing whether their account was broken. A
|
|
405
|
+
* lost connection used to fail on first sight, which is how a build's poll
|
|
406
|
+
* loop lost its caller's identity while a deployment rolled through the API.
|
|
199
407
|
*/
|
|
200
408
|
async request(path, init) {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
409
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
410
|
+
const repeatSafe = init.retry ?? method === "GET";
|
|
411
|
+
const retries = new ReadRetrySchedule();
|
|
412
|
+
const timeoutMs = init.timeoutMs ?? this.timeoutMs;
|
|
413
|
+
const deadlineAtMs = Date.now() + timeoutMs;
|
|
414
|
+
let attempts = 0;
|
|
415
|
+
let lastFailure;
|
|
416
|
+
let retryCause;
|
|
417
|
+
let retryExhausted = false;
|
|
418
|
+
const operation = init.operation ?? "other";
|
|
419
|
+
const timeoutError = (cause = lastFailure) => new ServerTimeoutError(`Server ${path} timed out after ${timeoutMs}ms${attempts > 0 ? `; ${attempts} ${attempts === 1 ? "attempt" : "attempts"} failed` : ""}`, cause === undefined ? undefined : { cause });
|
|
420
|
+
try {
|
|
421
|
+
for (;;) {
|
|
422
|
+
const remainingMs = deadlineAtMs - Date.now();
|
|
423
|
+
if (remainingMs <= 0) {
|
|
424
|
+
retryExhausted = retryCause !== undefined;
|
|
425
|
+
throw timeoutError();
|
|
426
|
+
}
|
|
427
|
+
attempts += 1;
|
|
428
|
+
try {
|
|
429
|
+
const result = await this.send(path, {
|
|
430
|
+
...init,
|
|
431
|
+
timeoutMs: remainingMs,
|
|
432
|
+
});
|
|
433
|
+
if (retryCause) {
|
|
434
|
+
recordServerReadRetryOutcome({
|
|
435
|
+
cause: retryCause,
|
|
436
|
+
operation,
|
|
437
|
+
outcome: "recovered",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
if (init.signal?.aborted) {
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
if (error instanceof ServerTimeoutError) {
|
|
447
|
+
retryExhausted = retryCause !== undefined;
|
|
448
|
+
throw timeoutError(lastFailure ?? error.cause);
|
|
449
|
+
}
|
|
450
|
+
lastFailure = error;
|
|
451
|
+
const retry = repeatSafe ||
|
|
452
|
+
(method === "GET" &&
|
|
453
|
+
error instanceof ServerTransportError &&
|
|
454
|
+
error.requestNotSent)
|
|
455
|
+
? retries.next(error)
|
|
456
|
+
: undefined;
|
|
457
|
+
if (retry === undefined || retry === "exhausted") {
|
|
458
|
+
retryExhausted = retryCause !== undefined && retry === "exhausted";
|
|
459
|
+
throw withAttemptCount(error, attempts);
|
|
460
|
+
}
|
|
461
|
+
const remainingAfterAttemptMs = deadlineAtMs - Date.now();
|
|
462
|
+
if (remainingAfterAttemptMs <= 0) {
|
|
463
|
+
retryExhausted = retryCause !== undefined;
|
|
464
|
+
throw timeoutError();
|
|
465
|
+
}
|
|
466
|
+
if (remainingAfterAttemptMs - retry.waitMs < MIN_RETRY_ATTEMPT_MS) {
|
|
467
|
+
retryExhausted = retryCause !== undefined;
|
|
468
|
+
throw withAttemptCount(error, attempts);
|
|
469
|
+
}
|
|
470
|
+
retryCause ??= retry.cause;
|
|
471
|
+
await delay(retry.waitMs, init.signal);
|
|
472
|
+
if (init.signal?.aborted) {
|
|
473
|
+
throw init.signal.reason;
|
|
474
|
+
}
|
|
475
|
+
if (Date.now() >= deadlineAtMs) {
|
|
476
|
+
retryExhausted = true;
|
|
477
|
+
throw timeoutError();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
206
480
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
if (retryCause && !init.signal?.aborted) {
|
|
484
|
+
if (retryExhausted) {
|
|
485
|
+
console.error("gateway server read retries exhausted", {
|
|
486
|
+
attempts,
|
|
487
|
+
cause: retryCause,
|
|
488
|
+
message: error instanceof Error ? error.message : String(error),
|
|
489
|
+
operation,
|
|
490
|
+
path,
|
|
491
|
+
});
|
|
214
492
|
}
|
|
215
|
-
|
|
493
|
+
recordServerReadRetryOutcome({
|
|
494
|
+
cause: retryCause,
|
|
495
|
+
operation,
|
|
496
|
+
outcome: retryExhausted ? "exhausted" : "terminal",
|
|
497
|
+
});
|
|
216
498
|
}
|
|
499
|
+
throw error;
|
|
217
500
|
}
|
|
218
|
-
throw lastError;
|
|
219
501
|
}
|
|
220
502
|
async send(path, init) {
|
|
221
|
-
const { apiKey, headers: callerHeaders, retry: _retry, signal: callerSignal, timeoutMs, ...rest } = init;
|
|
222
|
-
const bound = timeoutMs ?? this.timeoutMs;
|
|
503
|
+
const { apiKey, headers: callerHeaders, operation: _operation, retry: _retry, signal: callerSignal, timeoutMs, ...rest } = init;
|
|
223
504
|
const mergedHeaders = new Headers(callerHeaders);
|
|
224
505
|
// Auth and content-type are owned by this client — never override from callers.
|
|
225
506
|
mergedHeaders.set("Authorization", `Bearer ${apiKey}`);
|
|
226
507
|
if (!mergedHeaders.has("Content-Type")) {
|
|
227
508
|
mergedHeaders.set("Content-Type", "application/json");
|
|
228
509
|
}
|
|
229
|
-
const
|
|
510
|
+
const attempt = new ServerAttempt(path, timeoutMs ?? this.timeoutMs, callerSignal);
|
|
230
511
|
let response;
|
|
231
512
|
try {
|
|
232
513
|
response = await fetch(`${this.baseUrl()}${path}`, {
|
|
233
514
|
...rest,
|
|
234
515
|
headers: mergedHeaders,
|
|
235
|
-
signal:
|
|
236
|
-
? AbortSignal.any([callerSignal, timeoutSignal])
|
|
237
|
-
: timeoutSignal,
|
|
516
|
+
signal: attempt.signal,
|
|
238
517
|
});
|
|
239
518
|
}
|
|
240
519
|
catch (error) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
520
|
+
throw attempt.failure(error, "could not be reached", true);
|
|
521
|
+
}
|
|
522
|
+
let body;
|
|
523
|
+
try {
|
|
524
|
+
body = await response.text();
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
const failure = attempt.failure(error, "dropped the connection mid-response");
|
|
528
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get("Retry-After"));
|
|
529
|
+
if (!response.ok &&
|
|
530
|
+
!callerSignal?.aborted &&
|
|
531
|
+
(response.status === 429 ||
|
|
532
|
+
!isTransientHttpStatus(response.status) ||
|
|
533
|
+
(failure instanceof ServerTransportError && !failure.retryable))) {
|
|
534
|
+
throw new ServerHttpError(`Server ${path} failed: ${response.status}`, response.status, retryAfterMs);
|
|
245
535
|
}
|
|
246
|
-
throw
|
|
536
|
+
throw failure;
|
|
247
537
|
}
|
|
248
538
|
if (!response.ok) {
|
|
249
|
-
throw new ServerHttpError(`Server ${path} failed: ${response.status}${detailSuffix(
|
|
539
|
+
throw new ServerHttpError(`Server ${path} failed: ${response.status}${detailSuffix(body)}`, response.status, parseRetryAfterMs(response.headers.get("Retry-After")));
|
|
250
540
|
}
|
|
251
|
-
|
|
541
|
+
// A body that arrived whole but cannot be parsed is an answer, not a lost
|
|
542
|
+
// connection: the route is up and will say the same thing again.
|
|
543
|
+
return JSON.parse(body);
|
|
252
544
|
}
|
|
253
545
|
/**
|
|
254
546
|
* The Superblocks user behind a downstream credential.
|
|
@@ -266,6 +558,7 @@ export class HttpSuperblocksServerClient {
|
|
|
266
558
|
const data = await this.requestData("/api/v1/users/me", {
|
|
267
559
|
apiKey,
|
|
268
560
|
method: "GET",
|
|
561
|
+
operation: "identity",
|
|
269
562
|
timeoutMs: this.identityTimeoutMs,
|
|
270
563
|
});
|
|
271
564
|
const user = data.user;
|
|
@@ -291,6 +584,7 @@ export class HttpSuperblocksServerClient {
|
|
|
291
584
|
const data = await this.requestData("/api/v1/users/me", {
|
|
292
585
|
apiKey: input.apiKey,
|
|
293
586
|
method: "GET",
|
|
587
|
+
operation: "identity",
|
|
294
588
|
// Same route, so the same bound. This one runs on the build path rather
|
|
295
589
|
// than per tool call, and left at the sandbox timeout a wedged
|
|
296
590
|
// /users/me held a start_app open for two minutes here instead.
|
|
@@ -317,9 +611,17 @@ export class HttpSuperblocksServerClient {
|
|
|
317
611
|
url: raw.url,
|
|
318
612
|
});
|
|
319
613
|
}
|
|
614
|
+
const factsFlag = data.flagBootstrap?.["ai.facts.enabled"];
|
|
615
|
+
recordFactsFlagBootstrapOutcome(factsFlag === true
|
|
616
|
+
? "enabled"
|
|
617
|
+
: factsFlag === false
|
|
618
|
+
? "disabled"
|
|
619
|
+
: "invalid");
|
|
320
620
|
return {
|
|
321
621
|
agentType: organization.agentType,
|
|
322
622
|
agents,
|
|
623
|
+
controlPlaneUrl: this.baseUrl(),
|
|
624
|
+
isFactsEnabled: factsFlag === true,
|
|
323
625
|
};
|
|
324
626
|
}
|
|
325
627
|
async createApplication(input) {
|
|
@@ -384,6 +686,7 @@ export class HttpSuperblocksServerClient {
|
|
|
384
686
|
return await this.requestData(`${path}${query}`, {
|
|
385
687
|
apiKey: input.apiKey,
|
|
386
688
|
method: "GET",
|
|
689
|
+
operation: "live_edit",
|
|
387
690
|
retry: false,
|
|
388
691
|
timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
|
|
389
692
|
});
|
|
@@ -502,12 +805,48 @@ export class HttpSuperblocksServerClient {
|
|
|
502
805
|
if (input.applicationId) {
|
|
503
806
|
params.set("applicationId", input.applicationId);
|
|
504
807
|
}
|
|
505
|
-
const data = await this.requestData(`/api/v1/integrations/superset?${params.toString()}`, {
|
|
808
|
+
const data = await this.requestData(`/api/v1/integrations/superset?${params.toString()}`, {
|
|
809
|
+
apiKey: input.apiKey,
|
|
810
|
+
method: "GET",
|
|
811
|
+
operation: "superset_integrations",
|
|
812
|
+
});
|
|
506
813
|
return Array.isArray(data) ? data : [];
|
|
507
814
|
}
|
|
508
815
|
async getIntegration(input) {
|
|
509
816
|
try {
|
|
510
|
-
return await this.requestData(`/api/v1/integrations/${encodeURIComponent(input.integrationId)}`, { apiKey: input.apiKey, method: "GET" });
|
|
817
|
+
return await this.requestData(`/api/v1/integrations/${encodeURIComponent(input.integrationId)}`, { apiKey: input.apiKey, method: "GET", operation: "integration" });
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
if (error instanceof ServerHttpError && error.status === 404) {
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async getApplicationEnvironment(input) {
|
|
827
|
+
try {
|
|
828
|
+
const data = await this.requestData(`/api/v2/applications/${encodeURIComponent(input.applicationId)}/environments`, { apiKey: input.apiKey, method: "GET" });
|
|
829
|
+
const editor = data?.profiles?.editor;
|
|
830
|
+
const availableProfileIds = Array.isArray(editor?.availableProfileIds)
|
|
831
|
+
? editor.availableProfileIds.filter((id) => typeof id === "string")
|
|
832
|
+
: undefined;
|
|
833
|
+
return {
|
|
834
|
+
...(typeof data?.needsAttention?.editor === "boolean"
|
|
835
|
+
? { needsAttention: { editor: data.needsAttention.editor } }
|
|
836
|
+
: {}),
|
|
837
|
+
profiles: {
|
|
838
|
+
...(availableProfileIds
|
|
839
|
+
? {
|
|
840
|
+
editor: {
|
|
841
|
+
availableProfileIds,
|
|
842
|
+
...(typeof editor?.defaultProfileId === "string"
|
|
843
|
+
? { defaultProfileId: editor.defaultProfileId }
|
|
844
|
+
: {}),
|
|
845
|
+
},
|
|
846
|
+
}
|
|
847
|
+
: {}),
|
|
848
|
+
},
|
|
849
|
+
};
|
|
511
850
|
}
|
|
512
851
|
catch (error) {
|
|
513
852
|
if (error instanceof ServerHttpError && error.status === 404) {
|
|
@@ -517,7 +856,11 @@ export class HttpSuperblocksServerClient {
|
|
|
517
856
|
}
|
|
518
857
|
}
|
|
519
858
|
async getOrganizationProfiles(input) {
|
|
520
|
-
const data = await this.requestData(`/api/v1/organizations/${encodeURIComponent(input.organizationId)}/profiles`, {
|
|
859
|
+
const data = await this.requestData(`/api/v1/organizations/${encodeURIComponent(input.organizationId)}/profiles`, {
|
|
860
|
+
apiKey: input.apiKey,
|
|
861
|
+
method: "GET",
|
|
862
|
+
operation: "organization_profiles",
|
|
863
|
+
});
|
|
521
864
|
if (!Array.isArray(data)) {
|
|
522
865
|
return [];
|
|
523
866
|
}
|
|
@@ -527,6 +870,9 @@ export class HttpSuperblocksServerClient {
|
|
|
527
870
|
...(row.allowedInEditor !== undefined
|
|
528
871
|
? { allowedInEditor: row.allowedInEditor }
|
|
529
872
|
: {}),
|
|
873
|
+
...(typeof row.defaultInEditor === "boolean"
|
|
874
|
+
? { defaultInEditor: row.defaultInEditor }
|
|
875
|
+
: {}),
|
|
530
876
|
...(row.displayName ? { displayName: row.displayName } : {}),
|
|
531
877
|
...(row.id ? { id: row.id } : {}),
|
|
532
878
|
key: row.key,
|
|
@@ -585,6 +931,8 @@ export class HttpSuperblocksServerClient {
|
|
|
585
931
|
apiKey: input.apiKey,
|
|
586
932
|
body: JSON.stringify({ directoryHashes: [input.directoryHash] }),
|
|
587
933
|
method: "POST",
|
|
934
|
+
operation: "build_progress",
|
|
935
|
+
retry: true,
|
|
588
936
|
...(input.signal ? { signal: input.signal } : {}),
|
|
589
937
|
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
590
938
|
});
|
|
@@ -669,7 +1017,11 @@ export class HttpSuperblocksServerClient {
|
|
|
669
1017
|
* why nothing on a hot path should call it.
|
|
670
1018
|
*/
|
|
671
1019
|
async getApplicationName(input) {
|
|
672
|
-
const body = await this.request("/api/v2/applications", {
|
|
1020
|
+
const body = await this.request("/api/v2/applications", {
|
|
1021
|
+
apiKey: input.apiKey,
|
|
1022
|
+
method: "GET",
|
|
1023
|
+
operation: "applications",
|
|
1024
|
+
});
|
|
673
1025
|
const found = body?.data?.applications?.find((application) => application?.id === input.applicationId);
|
|
674
1026
|
return found?.name?.trim() || undefined;
|
|
675
1027
|
}
|