@tangle-network/agent-app 0.35.0 → 0.37.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/chunk-DTS5TZRN.js +293 -0
- package/dist/chunk-DTS5TZRN.js.map +1 -0
- package/dist/{chunk-GZGF3JWQ.js → chunk-FFBZJEBE.js} +226 -171
- package/dist/chunk-FFBZJEBE.js.map +1 -0
- package/dist/{chunk-MH6AVXQ7.js → chunk-VT5DI6GL.js} +40 -34
- package/dist/chunk-VT5DI6GL.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/sandbox/index.d.ts +22 -26
- package/dist/sandbox/index.js +2 -2
- package/dist/studio/index.d.ts +132 -0
- package/dist/studio/index.js +59 -0
- package/dist/studio/index.js.map +1 -0
- package/dist/studio-react/index.d.ts +193 -0
- package/dist/studio-react/index.js +1217 -0
- package/dist/studio-react/index.js.map +1 -0
- package/dist/studio-react/studio.css +73 -0
- package/dist/tools/index.js +1 -1
- package/dist/web-react/index.js +31 -13
- package/dist/web-react/index.js.map +1 -1
- package/package.json +38 -6
- package/dist/chunk-GZGF3JWQ.js.map +0 -1
- package/dist/chunk-MH6AVXQ7.js.map +0 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertHarnessModelCompatible
|
|
3
3
|
} from "./chunk-5VXPDXZJ.js";
|
|
4
|
+
import {
|
|
5
|
+
base64UrlDecodeText,
|
|
6
|
+
base64UrlEncodeText,
|
|
7
|
+
constantTimeEqual,
|
|
8
|
+
hmacSha256Base64Url
|
|
9
|
+
} from "./chunk-VT5DI6GL.js";
|
|
4
10
|
import {
|
|
5
11
|
buildAppToolMcpServer
|
|
6
12
|
} from "./chunk-A76ZHWNF.js";
|
|
@@ -10,6 +16,41 @@ import {
|
|
|
10
16
|
Sandbox
|
|
11
17
|
} from "@tangle-network/sandbox";
|
|
12
18
|
|
|
19
|
+
// src/sandbox/outcome.ts
|
|
20
|
+
var ok = (value) => ({ succeeded: true, value });
|
|
21
|
+
var fail = (error) => ({
|
|
22
|
+
succeeded: false,
|
|
23
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/sandbox/terminal-proxy-token.ts
|
|
27
|
+
var TERMINAL_PROXY_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
28
|
+
async function mintTerminalProxyToken(secret, identity, ttlMs = TERMINAL_PROXY_TOKEN_TTL_MS, now = Date.now) {
|
|
29
|
+
if (!secret) return fail(new Error("mintTerminalProxyToken: secret is required"));
|
|
30
|
+
if (!identity.userId || !identity.workspaceId || !identity.sandboxId) {
|
|
31
|
+
return fail(new Error("mintTerminalProxyToken: userId/workspaceId/sandboxId are required"));
|
|
32
|
+
}
|
|
33
|
+
const expiresAt = new Date(now() + ttlMs);
|
|
34
|
+
const payload = { ...identity, exp: Math.floor(expiresAt.getTime() / 1e3) };
|
|
35
|
+
const encoded = base64UrlEncodeText(JSON.stringify(payload));
|
|
36
|
+
const sig = await hmacSha256Base64Url(encoded, secret);
|
|
37
|
+
return ok({ token: `${encoded}.${sig}`, expiresAt });
|
|
38
|
+
}
|
|
39
|
+
async function verifyTerminalProxyToken(secret, token, expected, now = Date.now) {
|
|
40
|
+
if (!secret) return false;
|
|
41
|
+
const [encoded, sig, extra] = token.split(".");
|
|
42
|
+
if (!encoded || !sig || extra !== void 0) return false;
|
|
43
|
+
const expectedSig = await hmacSha256Base64Url(encoded, secret);
|
|
44
|
+
if (!constantTimeEqual(sig, expectedSig)) return false;
|
|
45
|
+
let payload;
|
|
46
|
+
try {
|
|
47
|
+
payload = JSON.parse(base64UrlDecodeText(encoded));
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return payload.userId === expected.userId && payload.workspaceId === expected.workspaceId && payload.sandboxId === expected.sandboxId && Number.isFinite(payload.exp) && payload.exp > Math.floor(now() / 1e3);
|
|
52
|
+
}
|
|
53
|
+
|
|
13
54
|
// src/sandbox/workspace-terminal.ts
|
|
14
55
|
function createWorkspaceSandboxManager(opts) {
|
|
15
56
|
return {
|
|
@@ -43,9 +84,9 @@ function createWorkspaceSandboxManager(opts) {
|
|
|
43
84
|
}
|
|
44
85
|
};
|
|
45
86
|
}
|
|
46
|
-
var DEFAULT_TERMINAL_TOKEN_PREFIX = "sbxt_";
|
|
47
87
|
var DEFAULT_TERMINAL_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
48
88
|
var BEARER_SUBPROTOCOL_PREFIX = "bearer.";
|
|
89
|
+
var LEGACY_TERMINAL_TOKEN_PREFIX = "sbxt_";
|
|
49
90
|
async function createSandboxTerminalToken(subject, opts) {
|
|
50
91
|
validateTerminalSubject(subject);
|
|
51
92
|
const secret = opts.secret?.trim();
|
|
@@ -53,38 +94,16 @@ async function createSandboxTerminalToken(subject, opts) {
|
|
|
53
94
|
const now = opts.now ?? Date.now;
|
|
54
95
|
const expiresInMs = opts.expiresInMs ?? DEFAULT_TERMINAL_TOKEN_TTL_MS;
|
|
55
96
|
if (!Number.isFinite(expiresInMs) || expiresInMs <= 0) throw new Error("expiresInMs must be a positive number");
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
exp: Math.floor(expiresAt.getTime() / 1e3),
|
|
60
|
-
n: crypto.randomUUID()
|
|
61
|
-
};
|
|
62
|
-
const encodedPayload = base64urlText(JSON.stringify(payload));
|
|
63
|
-
const signature = await signText(encodedPayload, secret);
|
|
64
|
-
return {
|
|
65
|
-
token: `${opts.prefix ?? DEFAULT_TERMINAL_TOKEN_PREFIX}${encodedPayload}.${signature}`,
|
|
66
|
-
expiresAt
|
|
67
|
-
};
|
|
97
|
+
const minted = await mintTerminalProxyToken(secret, subject, expiresInMs, now);
|
|
98
|
+
if (!minted.succeeded) throw minted.error;
|
|
99
|
+
return minted.value;
|
|
68
100
|
}
|
|
69
101
|
async function verifySandboxTerminalToken(token, expected, opts) {
|
|
70
102
|
validateTerminalSubject(expected);
|
|
71
103
|
const secret = opts.secret?.trim();
|
|
72
|
-
const prefix = opts.prefix ?? DEFAULT_TERMINAL_TOKEN_PREFIX;
|
|
73
|
-
if (!secret || !token.startsWith(prefix)) return false;
|
|
74
|
-
const body = token.slice(prefix.length);
|
|
75
|
-
const dot = body.lastIndexOf(".");
|
|
76
|
-
if (dot <= 0 || dot === body.length - 1) return false;
|
|
77
|
-
const encodedPayload = body.slice(0, dot);
|
|
78
|
-
const signature = body.slice(dot + 1);
|
|
79
|
-
if (!timingSafeEqual(signature, await signText(encodedPayload, secret))) return false;
|
|
80
|
-
let payload;
|
|
81
|
-
try {
|
|
82
|
-
payload = JSON.parse(textFromBase64url(encodedPayload));
|
|
83
|
-
} catch {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
104
|
const now = opts.now ?? Date.now;
|
|
87
|
-
|
|
105
|
+
const normalized = token.startsWith(LEGACY_TERMINAL_TOKEN_PREFIX) ? token.slice(LEGACY_TERMINAL_TOKEN_PREFIX.length) : token;
|
|
106
|
+
return verifyTerminalProxyToken(secret ?? "", normalized, expected, now);
|
|
88
107
|
}
|
|
89
108
|
function createWorkspaceSandboxConnectionHandler(opts) {
|
|
90
109
|
return async function handleWorkspaceSandboxConnection({ request, params }) {
|
|
@@ -101,7 +120,7 @@ function createWorkspaceSandboxConnectionHandler(opts) {
|
|
|
101
120
|
{ status: 500 }
|
|
102
121
|
);
|
|
103
122
|
}
|
|
104
|
-
const directSidecarUrl = box.connection?.sidecarUrl ??
|
|
123
|
+
const directSidecarUrl = box.connection?.sidecarUrl ?? box.connection?.runtimeUrl;
|
|
105
124
|
const directSidecarToken = box.connection?.authToken ?? box.connection?.sidecarToken;
|
|
106
125
|
const directSidecarExpiresAt = box.connection?.authTokenExpiresAt;
|
|
107
126
|
if (opts.exposeDirectSidecar && directSidecarUrl && directSidecarToken && directSidecarExpiresAt) {
|
|
@@ -114,7 +133,7 @@ function createWorkspaceSandboxConnectionHandler(opts) {
|
|
|
114
133
|
sandboxId: box.id
|
|
115
134
|
});
|
|
116
135
|
}
|
|
117
|
-
if (!
|
|
136
|
+
if (!directSidecarUrl) {
|
|
118
137
|
return Response.json(
|
|
119
138
|
{
|
|
120
139
|
error: "Workspace sandbox runtime not ready. The sandbox is still initializing -- retry in a few seconds.",
|
|
@@ -128,7 +147,7 @@ function createWorkspaceSandboxConnectionHandler(opts) {
|
|
|
128
147
|
try {
|
|
129
148
|
scoped = await createSandboxTerminalToken(
|
|
130
149
|
{ userId: user.id, workspaceId, sandboxId: box.id },
|
|
131
|
-
{ secret, expiresInMs: opts.tokenExpiresInMs
|
|
150
|
+
{ secret, expiresInMs: opts.tokenExpiresInMs }
|
|
132
151
|
);
|
|
133
152
|
} catch (err) {
|
|
134
153
|
return Response.json(
|
|
@@ -161,20 +180,21 @@ function createWorkspaceSandboxRuntimeProxyHandler(opts) {
|
|
|
161
180
|
await opts.requireWorkspaceAccess({ request, userId: user.id, workspaceId, sandboxId });
|
|
162
181
|
const token = terminalTokenFromRequest(request.headers);
|
|
163
182
|
const secret = typeof opts.tokenSecret === "function" ? opts.tokenSecret() : opts.tokenSecret;
|
|
164
|
-
if (!token || !await verifySandboxTerminalToken(token, { userId: user.id, workspaceId, sandboxId }, { secret
|
|
183
|
+
if (!token || !await verifySandboxTerminalToken(token, { userId: user.id, workspaceId, sandboxId }, { secret })) {
|
|
165
184
|
return Response.json({ error: "Invalid terminal token" }, { status: 403 });
|
|
166
185
|
}
|
|
167
186
|
const requestUrl = new URL(request.url);
|
|
168
187
|
const runtimeConnection = await opts.getSandboxRuntimeConnection?.({ request, userId: user.id, workspaceId, sandboxId });
|
|
169
|
-
const
|
|
170
|
-
const
|
|
188
|
+
const directRuntimeConnection = runtimeConnection?.runtimeUrl && runtimeConnection.authToken ? runtimeConnection : null;
|
|
189
|
+
const credentials = directRuntimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
|
|
190
|
+
const upstreamUrl = directRuntimeConnection ? new URL(encodedRuntimePath, `${directRuntimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(
|
|
171
191
|
`/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${encodedRuntimePath}`,
|
|
172
192
|
credentials.baseUrl
|
|
173
193
|
);
|
|
174
194
|
upstreamUrl.search = requestUrl.search;
|
|
175
195
|
const headers = buildSandboxRuntimeProxyHeaders(
|
|
176
196
|
request.headers,
|
|
177
|
-
|
|
197
|
+
directRuntimeConnection?.authToken ?? credentials.apiKey,
|
|
178
198
|
opts.forwardHeaders
|
|
179
199
|
);
|
|
180
200
|
const init = {
|
|
@@ -203,7 +223,10 @@ function matchSandboxTerminalWsPath(pathname) {
|
|
|
203
223
|
if (!m) return null;
|
|
204
224
|
const [, workspaceId, sandboxId, subPath] = m;
|
|
205
225
|
if (!workspaceId || !sandboxId || !subPath) return null;
|
|
206
|
-
|
|
226
|
+
const decodedWorkspaceId = safeDecodeURIComponent(workspaceId);
|
|
227
|
+
const decodedSandboxId = safeDecodeURIComponent(sandboxId);
|
|
228
|
+
if (!decodedWorkspaceId || !decodedSandboxId) return null;
|
|
229
|
+
return { workspaceId: decodedWorkspaceId, sandboxId: decodedSandboxId, subPath };
|
|
207
230
|
}
|
|
208
231
|
function isSandboxTerminalWsUpgrade(request) {
|
|
209
232
|
if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") return false;
|
|
@@ -238,16 +261,19 @@ function createWorkspaceSandboxTerminalUpgradeHandler(opts) {
|
|
|
238
261
|
}
|
|
239
262
|
const token = terminalTokenFromRequest(request.headers);
|
|
240
263
|
const secret = typeof opts.tokenSecret === "function" ? opts.tokenSecret() : opts.tokenSecret;
|
|
241
|
-
if (!token || !await verifySandboxTerminalToken(token, { userId: user.id, workspaceId, sandboxId }, { secret
|
|
264
|
+
if (!token || !await verifySandboxTerminalToken(token, { userId: user.id, workspaceId, sandboxId }, { secret })) {
|
|
242
265
|
return new Response("Invalid terminal token", { status: 403 });
|
|
243
266
|
}
|
|
244
267
|
const runtimeConnection = await opts.getSandboxRuntimeConnection?.({ request, userId: user.id, workspaceId, sandboxId });
|
|
245
|
-
const
|
|
246
|
-
const
|
|
268
|
+
const directRuntimeConnection = runtimeConnection?.runtimeUrl && runtimeConnection.authToken ? runtimeConnection : null;
|
|
269
|
+
const credentials = directRuntimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
|
|
270
|
+
const upstreamUrl = directRuntimeConnection ? new URL(subPath, `${directRuntimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${subPath}`, credentials.baseUrl);
|
|
247
271
|
upstreamUrl.search = url.search;
|
|
248
272
|
const upstreamHeaders = new Headers(request.headers);
|
|
249
|
-
|
|
273
|
+
const upstreamBearer = directRuntimeConnection?.authToken ?? credentials.apiKey;
|
|
274
|
+
upstreamHeaders.set("Authorization", `Bearer ${upstreamBearer}`);
|
|
250
275
|
upstreamHeaders.delete("host");
|
|
276
|
+
stripBearerSubprotocol(upstreamHeaders);
|
|
251
277
|
const fetchImpl = opts.fetch ?? fetch;
|
|
252
278
|
return fetchImpl(upstreamUrl.toString(), { method: request.method, headers: upstreamHeaders });
|
|
253
279
|
};
|
|
@@ -286,7 +312,7 @@ function bearerSubprotocolToken(value) {
|
|
|
286
312
|
const encoded = protocol.slice(BEARER_SUBPROTOCOL_PREFIX.length);
|
|
287
313
|
if (!encoded) return null;
|
|
288
314
|
try {
|
|
289
|
-
const token =
|
|
315
|
+
const token = base64UrlDecodeText(encoded).trim();
|
|
290
316
|
return token || null;
|
|
291
317
|
} catch {
|
|
292
318
|
return null;
|
|
@@ -297,45 +323,30 @@ function bearerSubprotocolToken(value) {
|
|
|
297
323
|
function terminalTokenFromRequest(headers) {
|
|
298
324
|
return bearerToken(headers.get("Authorization")) ?? bearerSubprotocolToken(headers.get("Sec-WebSocket-Protocol"));
|
|
299
325
|
}
|
|
326
|
+
function safeDecodeURIComponent(value) {
|
|
327
|
+
try {
|
|
328
|
+
return decodeURIComponent(value);
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function stripBearerSubprotocol(headers) {
|
|
334
|
+
const value = headers.get("Sec-WebSocket-Protocol");
|
|
335
|
+
if (!value) return;
|
|
336
|
+
const protocols = value.split(",").map((part) => part.trim()).filter((part) => part && !part.toLowerCase().startsWith(BEARER_SUBPROTOCOL_PREFIX));
|
|
337
|
+
if (protocols.length) {
|
|
338
|
+
headers.set("Sec-WebSocket-Protocol", protocols.join(", "));
|
|
339
|
+
} else {
|
|
340
|
+
headers.delete("Sec-WebSocket-Protocol");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
300
343
|
function validateTerminalSubject(subject) {
|
|
301
344
|
if (!subject.userId) throw new Error("userId is required");
|
|
302
345
|
if (!subject.workspaceId) throw new Error("workspaceId is required");
|
|
303
346
|
if (!subject.sandboxId) throw new Error("sandboxId is required");
|
|
304
347
|
}
|
|
305
|
-
async function signText(message, secret) {
|
|
306
|
-
const enc = new TextEncoder();
|
|
307
|
-
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
308
|
-
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
|
|
309
|
-
return base64url(new Uint8Array(sig));
|
|
310
|
-
}
|
|
311
|
-
function base64urlText(text) {
|
|
312
|
-
return base64url(new TextEncoder().encode(text));
|
|
313
|
-
}
|
|
314
|
-
function textFromBase64url(value) {
|
|
315
|
-
const b64 = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
316
|
-
const bin = atob(b64);
|
|
317
|
-
const bytes = new Uint8Array(bin.length);
|
|
318
|
-
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
319
|
-
return new TextDecoder().decode(bytes);
|
|
320
|
-
}
|
|
321
|
-
function base64url(bytes) {
|
|
322
|
-
let s = "";
|
|
323
|
-
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
324
|
-
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
325
|
-
}
|
|
326
|
-
function timingSafeEqual(a, b) {
|
|
327
|
-
if (a.length !== b.length) return false;
|
|
328
|
-
let diff = 0;
|
|
329
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
330
|
-
return diff === 0;
|
|
331
|
-
}
|
|
332
348
|
|
|
333
349
|
// src/sandbox/index.ts
|
|
334
|
-
var ok = (value) => ({ succeeded: true, value });
|
|
335
|
-
var fail = (error) => ({
|
|
336
|
-
succeeded: false,
|
|
337
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
338
|
-
});
|
|
339
350
|
var DEFAULT_SANDBOX_RESOURCES = {
|
|
340
351
|
image: "universal",
|
|
341
352
|
cpuCores: 2,
|
|
@@ -380,6 +391,14 @@ function buildAppToolMcpServers(options) {
|
|
|
380
391
|
function shellSingleQuote(value) {
|
|
381
392
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
382
393
|
}
|
|
394
|
+
function shellPath(path) {
|
|
395
|
+
if (path === "~") return '"$HOME"';
|
|
396
|
+
if (path.startsWith("~/")) {
|
|
397
|
+
const rest = path.slice(2);
|
|
398
|
+
return rest ? `"$HOME"/${shellSingleQuote(rest)}` : '"$HOME"';
|
|
399
|
+
}
|
|
400
|
+
return shellSingleQuote(path);
|
|
401
|
+
}
|
|
383
402
|
function splitDeferredProfileFiles(profile) {
|
|
384
403
|
const files = profile.resources?.files ?? [];
|
|
385
404
|
const deferredFiles = [];
|
|
@@ -395,6 +414,25 @@ function splitDeferredProfileFiles(profile) {
|
|
|
395
414
|
};
|
|
396
415
|
return { leanProfile, deferredFiles };
|
|
397
416
|
}
|
|
417
|
+
var PROFILE_WRITE_B64_CHUNK_CHARS = 3e3;
|
|
418
|
+
var PROFILE_WRITE_MAX_429_RETRIES = 4;
|
|
419
|
+
var PROFILE_WRITE_RETRY_BASE_MS = 250;
|
|
420
|
+
var PROFILE_WRITE_RETRY_MAX_MS = 2e3;
|
|
421
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
422
|
+
function rateLimit(err) {
|
|
423
|
+
if (err && typeof err === "object") {
|
|
424
|
+
const e = err;
|
|
425
|
+
const retryAfterMs = typeof e.retryAfterMs === "number" ? e.retryAfterMs : void 0;
|
|
426
|
+
if (e.status === 429) return { is429: true, retryAfterMs };
|
|
427
|
+
if (typeof e.code === "string" && /rate.?limit|too.?many.?requests|429/i.test(e.code)) {
|
|
428
|
+
return { is429: true, retryAfterMs };
|
|
429
|
+
}
|
|
430
|
+
if (typeof e.message === "string" && /\b429\b|rate.?limit|too many requests/i.test(e.message)) {
|
|
431
|
+
return { is429: true, retryAfterMs };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { is429: false };
|
|
435
|
+
}
|
|
398
436
|
async function writeProfileFilesToBox(box, files) {
|
|
399
437
|
for (const mount of files) {
|
|
400
438
|
if (mount.resource.kind !== "inline") continue;
|
|
@@ -404,22 +442,45 @@ async function writeProfileFilesToBox(box, files) {
|
|
|
404
442
|
const dir = path.replace(/\/[^/]*$/, "");
|
|
405
443
|
const isBin = /(^|\/)(s?bin)\//.test(path);
|
|
406
444
|
const executable = mount.executable ?? isBin;
|
|
407
|
-
const q =
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
445
|
+
const q = shellPath(path);
|
|
446
|
+
const qb64 = shellPath(`${path}.b64`);
|
|
447
|
+
const step = async (cmd) => {
|
|
448
|
+
for (let attempt = 0; ; attempt++) {
|
|
449
|
+
try {
|
|
450
|
+
const res2 = await box.exec(cmd);
|
|
451
|
+
if (res2.exitCode !== 0) {
|
|
452
|
+
return fail(
|
|
453
|
+
new Error(
|
|
454
|
+
`writeProfileFilesToBox: failed to write ${path} (exit ${res2.exitCode}): ${res2.stderr.slice(0, 500)}`
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return ok(void 0);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const { is429, retryAfterMs } = rateLimit(err);
|
|
461
|
+
if (is429 && attempt < PROFILE_WRITE_MAX_429_RETRIES) {
|
|
462
|
+
const backoff = Math.min(
|
|
463
|
+
PROFILE_WRITE_RETRY_BASE_MS * 2 ** attempt,
|
|
464
|
+
PROFILE_WRITE_RETRY_MAX_MS
|
|
465
|
+
);
|
|
466
|
+
await sleep(retryAfterMs ?? backoff);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
return fail(new Error(`writeProfileFilesToBox: exec failed for ${path}`, { cause: err }));
|
|
470
|
+
}
|
|
419
471
|
}
|
|
420
|
-
}
|
|
421
|
-
|
|
472
|
+
};
|
|
473
|
+
const mkdir = dir && dir !== path ? `mkdir -p ${shellPath(dir)} && ` : "";
|
|
474
|
+
let res = await step(`${mkdir}: > ${qb64}`);
|
|
475
|
+
if (!res.succeeded) return res;
|
|
476
|
+
for (let i = 0; i < b64.length; i += PROFILE_WRITE_B64_CHUNK_CHARS) {
|
|
477
|
+
const slice = b64.slice(i, i + PROFILE_WRITE_B64_CHUNK_CHARS);
|
|
478
|
+
res = await step(`printf '%s' '${slice}' >> ${qb64}`);
|
|
479
|
+
if (!res.succeeded) return res;
|
|
422
480
|
}
|
|
481
|
+
const chmod = executable ? ` && chmod +x ${q}` : "";
|
|
482
|
+
res = await step(`base64 -d ${qb64} > ${q} && rm -f ${qb64}${chmod}`);
|
|
483
|
+
if (!res.succeeded) return res;
|
|
423
484
|
}
|
|
424
485
|
return ok(void 0);
|
|
425
486
|
}
|
|
@@ -479,6 +540,38 @@ async function isBoxAlive(box, harness, probe) {
|
|
|
479
540
|
return false;
|
|
480
541
|
}
|
|
481
542
|
}
|
|
543
|
+
var RUNTIME_CONNECTION_WAIT_MS = 3e4;
|
|
544
|
+
var RUNTIME_CONNECTION_POLL_MS = 1e3;
|
|
545
|
+
function sandboxRuntimeUrl(box) {
|
|
546
|
+
const connection = box.connection;
|
|
547
|
+
return connection?.sidecarUrl ?? connection?.runtimeUrl;
|
|
548
|
+
}
|
|
549
|
+
function sandboxEdgeFailed(box) {
|
|
550
|
+
const connection = box.connection;
|
|
551
|
+
return connection?.edgeStatus === "failed" || Boolean(connection?.edgeError);
|
|
552
|
+
}
|
|
553
|
+
async function refreshRuntimeConnection(client, box) {
|
|
554
|
+
let current = box;
|
|
555
|
+
if (sandboxRuntimeUrl(current)) return current;
|
|
556
|
+
const deadline = Date.now() + RUNTIME_CONNECTION_WAIT_MS;
|
|
557
|
+
while (Date.now() < deadline) {
|
|
558
|
+
try {
|
|
559
|
+
await current.refresh();
|
|
560
|
+
if (sandboxRuntimeUrl(current)) return current;
|
|
561
|
+
const latest = await client.get(current.id);
|
|
562
|
+
if (latest) current = latest;
|
|
563
|
+
if (sandboxRuntimeUrl(current)) return current;
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
await new Promise((resolve) => setTimeout(resolve, RUNTIME_CONNECTION_POLL_MS));
|
|
567
|
+
}
|
|
568
|
+
return current;
|
|
569
|
+
}
|
|
570
|
+
async function isReusableBox(box, harness, probe) {
|
|
571
|
+
if (sandboxEdgeFailed(box)) return false;
|
|
572
|
+
if (!sandboxRuntimeUrl(box)) return false;
|
|
573
|
+
return isBoxAlive(box, harness, probe);
|
|
574
|
+
}
|
|
482
575
|
async function resumeStoppedBox(client, name, timeoutMs) {
|
|
483
576
|
try {
|
|
484
577
|
const stopped = await client.list({ status: "stopped" });
|
|
@@ -508,18 +601,28 @@ async function ensureWorkspaceSandbox(shell, options) {
|
|
|
508
601
|
if (!dropped.succeeded) {
|
|
509
602
|
throw new Error(`forceNew: sandbox ${name} could not be deleted`, { cause: dropped.error });
|
|
510
603
|
}
|
|
511
|
-
} else if (found.metadata?.harness === harness
|
|
512
|
-
const
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const boot = await shell.bootstrap(found, scope);
|
|
518
|
-
if (!boot.succeeded) {
|
|
519
|
-
throw new Error(`bootstrap failed on reused box ${name}`, { cause: boot.error });
|
|
604
|
+
} else if (found.metadata?.harness === harness) {
|
|
605
|
+
const ready = await refreshRuntimeConnection(client, found);
|
|
606
|
+
if (await isReusableBox(ready, harness, shell.livenessProbe)) {
|
|
607
|
+
const written = await materializeDeferredFilesForExistingBox(shell, ready, workspaceId, userId);
|
|
608
|
+
if (!written.succeeded) {
|
|
609
|
+
throw new Error(`deferred file write failed on reused box ${name}`, { cause: written.error });
|
|
520
610
|
}
|
|
611
|
+
if (shell.bootstrap) {
|
|
612
|
+
const boot = await shell.bootstrap(ready, scope);
|
|
613
|
+
if (!boot.succeeded) {
|
|
614
|
+
throw new Error(`bootstrap failed on reused box ${name}`, { cause: boot.error });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return ready;
|
|
618
|
+
}
|
|
619
|
+
const dropped = await deleteBox(ready);
|
|
620
|
+
if (!dropped.succeeded) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`sandbox ${name} (was ${String(found.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
|
|
623
|
+
{ cause: dropped.error }
|
|
624
|
+
);
|
|
521
625
|
}
|
|
522
|
-
return found;
|
|
523
626
|
} else {
|
|
524
627
|
const dropped = await deleteBox(found);
|
|
525
628
|
if (!dropped.succeeded) {
|
|
@@ -532,19 +635,28 @@ async function ensureWorkspaceSandbox(shell, options) {
|
|
|
532
635
|
}
|
|
533
636
|
if (!forceNew && shell.resumeStopped !== false) {
|
|
534
637
|
const resumed = await resumeStoppedBox(client, name, resumeTimeout);
|
|
535
|
-
if (resumed.succeeded && resumed.value
|
|
536
|
-
const box2 = resumed.value;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
638
|
+
if (resumed.succeeded && resumed.value) {
|
|
639
|
+
const box2 = await refreshRuntimeConnection(client, resumed.value);
|
|
640
|
+
if (await isReusableBox(box2, harness, shell.livenessProbe)) {
|
|
641
|
+
const written = await materializeDeferredFilesForExistingBox(shell, box2, workspaceId, userId);
|
|
642
|
+
if (!written.succeeded) {
|
|
643
|
+
throw new Error(`deferred file write failed on resumed box ${name}`, { cause: written.error });
|
|
644
|
+
}
|
|
645
|
+
if (shell.bootstrap) {
|
|
646
|
+
const boot = await shell.bootstrap(box2, scope);
|
|
647
|
+
if (!boot.succeeded) {
|
|
648
|
+
throw new Error(`bootstrap failed on resumed box ${name}`, { cause: boot.error });
|
|
649
|
+
}
|
|
545
650
|
}
|
|
651
|
+
return box2;
|
|
652
|
+
}
|
|
653
|
+
const dropped = await deleteBox(box2);
|
|
654
|
+
if (!dropped.succeeded) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
`resumed sandbox ${name} (was ${String(box2.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
|
|
657
|
+
{ cause: dropped.error }
|
|
658
|
+
);
|
|
546
659
|
}
|
|
547
|
-
return box2;
|
|
548
660
|
}
|
|
549
661
|
}
|
|
550
662
|
const connectedIntegrationIds = await shell.connectedIntegrationIds(workspaceId);
|
|
@@ -585,6 +697,7 @@ async function ensureWorkspaceSandbox(shell, options) {
|
|
|
585
697
|
backend: { type: harness, profile, ...model ? { model } : {} },
|
|
586
698
|
...storage ? { storage } : {},
|
|
587
699
|
...restore ? restore : {},
|
|
700
|
+
...shell.webTerminalEnabled ? { webTerminalEnabled: true } : {},
|
|
588
701
|
maxLifetimeSeconds: resources.maxLifetimeSeconds,
|
|
589
702
|
idleTimeoutSeconds: resources.idleTimeoutSeconds,
|
|
590
703
|
resources: {
|
|
@@ -593,9 +706,9 @@ async function ensureWorkspaceSandbox(shell, options) {
|
|
|
593
706
|
diskGB: resources.diskGB
|
|
594
707
|
}
|
|
595
708
|
};
|
|
596
|
-
|
|
709
|
+
let box = await client.create(payload);
|
|
597
710
|
await box.waitFor("running", { timeoutMs: 12e4 });
|
|
598
|
-
|
|
711
|
+
box = await refreshRuntimeConnection(client, box);
|
|
599
712
|
if (deferredFiles.length > 0) {
|
|
600
713
|
const written = await writeProfileFilesToBox(box, deferredFiles);
|
|
601
714
|
if (!written.succeeded) {
|
|
@@ -828,64 +941,6 @@ async function driveSandboxTurn(shell, box, message, options) {
|
|
|
828
941
|
return fail(err);
|
|
829
942
|
}
|
|
830
943
|
}
|
|
831
|
-
var TERMINAL_PROXY_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
832
|
-
async function signTerminalProxyToken(secret, encodedPayload) {
|
|
833
|
-
const key = await crypto.subtle.importKey(
|
|
834
|
-
"raw",
|
|
835
|
-
new TextEncoder().encode(secret),
|
|
836
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
837
|
-
false,
|
|
838
|
-
["sign"]
|
|
839
|
-
);
|
|
840
|
-
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(encodedPayload));
|
|
841
|
-
return base64UrlEncodeBytes(new Uint8Array(sig));
|
|
842
|
-
}
|
|
843
|
-
async function mintTerminalProxyToken(secret, identity, ttlMs = TERMINAL_PROXY_TOKEN_TTL_MS) {
|
|
844
|
-
if (!secret) return fail(new Error("mintTerminalProxyToken: secret is required"));
|
|
845
|
-
if (!identity.userId || !identity.workspaceId || !identity.sandboxId) {
|
|
846
|
-
return fail(new Error("mintTerminalProxyToken: userId/workspaceId/sandboxId are required"));
|
|
847
|
-
}
|
|
848
|
-
const expiresAt = new Date(Date.now() + ttlMs);
|
|
849
|
-
const payload = { ...identity, exp: Math.floor(expiresAt.getTime() / 1e3) };
|
|
850
|
-
const encoded = base64UrlEncodeUtf8(JSON.stringify(payload));
|
|
851
|
-
const sig = await signTerminalProxyToken(secret, encoded);
|
|
852
|
-
return ok({ token: `${encoded}.${sig}`, expiresAt });
|
|
853
|
-
}
|
|
854
|
-
async function verifyTerminalProxyToken(secret, token, expected) {
|
|
855
|
-
if (!secret) return false;
|
|
856
|
-
const [encoded, sig, extra] = token.split(".");
|
|
857
|
-
if (!encoded || !sig || extra !== void 0) return false;
|
|
858
|
-
const expectedSig = await signTerminalProxyToken(secret, encoded);
|
|
859
|
-
if (!constantTimeEqual(sig, expectedSig)) return false;
|
|
860
|
-
let payload;
|
|
861
|
-
try {
|
|
862
|
-
payload = JSON.parse(base64UrlDecodeUtf8(encoded));
|
|
863
|
-
} catch {
|
|
864
|
-
return false;
|
|
865
|
-
}
|
|
866
|
-
return payload.userId === expected.userId && payload.workspaceId === expected.workspaceId && payload.sandboxId === expected.sandboxId && Number.isFinite(payload.exp) && payload.exp > Math.floor(Date.now() / 1e3);
|
|
867
|
-
}
|
|
868
|
-
function base64UrlEncodeBytes(bytes) {
|
|
869
|
-
let bin = "";
|
|
870
|
-
for (const b of bytes) bin += String.fromCharCode(b);
|
|
871
|
-
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
872
|
-
}
|
|
873
|
-
function base64UrlEncodeUtf8(v) {
|
|
874
|
-
return base64UrlEncodeBytes(new TextEncoder().encode(v));
|
|
875
|
-
}
|
|
876
|
-
function base64UrlDecodeUtf8(v) {
|
|
877
|
-
const padded = v.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(v.length / 4) * 4, "=");
|
|
878
|
-
const bin = atob(padded);
|
|
879
|
-
const bytes = new Uint8Array(bin.length);
|
|
880
|
-
for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i);
|
|
881
|
-
return new TextDecoder().decode(bytes);
|
|
882
|
-
}
|
|
883
|
-
function constantTimeEqual(a, b) {
|
|
884
|
-
if (a.length !== b.length) return false;
|
|
885
|
-
let r = 0;
|
|
886
|
-
for (let i = 0; i < a.length; i += 1) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
887
|
-
return r === 0;
|
|
888
|
-
}
|
|
889
944
|
var SEVERED_FINISH_REASONS = /* @__PURE__ */ new Set(["error", "other", "unknown"]);
|
|
890
945
|
function asPlainRecord(v) {
|
|
891
946
|
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
@@ -928,6 +983,8 @@ function firstQuestionText(value) {
|
|
|
928
983
|
}
|
|
929
984
|
|
|
930
985
|
export {
|
|
986
|
+
mintTerminalProxyToken,
|
|
987
|
+
verifyTerminalProxyToken,
|
|
931
988
|
createWorkspaceSandboxManager,
|
|
932
989
|
createSandboxTerminalToken,
|
|
933
990
|
verifySandboxTerminalToken,
|
|
@@ -963,10 +1020,8 @@ export {
|
|
|
963
1020
|
deleteSecret,
|
|
964
1021
|
mintSandboxScopedToken,
|
|
965
1022
|
driveSandboxTurn,
|
|
966
|
-
mintTerminalProxyToken,
|
|
967
|
-
verifyTerminalProxyToken,
|
|
968
1023
|
classifySeveredStream,
|
|
969
1024
|
isTerminalPromptEvent,
|
|
970
1025
|
detectInteractiveQuestion
|
|
971
1026
|
};
|
|
972
|
-
//# sourceMappingURL=chunk-
|
|
1027
|
+
//# sourceMappingURL=chunk-FFBZJEBE.js.map
|