@tangle-network/agent-app 0.36.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.
@@ -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 expiresAt = new Date(now() + expiresInMs);
57
- const payload = {
58
- ...subject,
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
- return payload.userId === expected.userId && payload.workspaceId === expected.workspaceId && payload.sandboxId === expected.sandboxId && Number.isFinite(payload.exp) && payload.exp > Math.floor(now() / 1e3);
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 ?? (box.connection?.authToken ? box.connection?.runtimeUrl : void 0);
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 (!box.connection?.runtimeUrl) {
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, prefix: opts.tokenPrefix }
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, prefix: opts.tokenPrefix })) {
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 credentials = runtimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
170
- const upstreamUrl = runtimeConnection ? new URL(encodedRuntimePath, `${runtimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(
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
- runtimeConnection?.authToken ?? credentials.apiKey,
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
- return { workspaceId: decodeURIComponent(workspaceId), sandboxId: decodeURIComponent(sandboxId), subPath };
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, prefix: opts.tokenPrefix })) {
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 credentials = runtimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
246
- const upstreamUrl = runtimeConnection ? new URL(subPath, `${runtimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${subPath}`, credentials.baseUrl);
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
- upstreamHeaders.set("Authorization", `Bearer ${runtimeConnection?.authToken ?? credentials.apiKey}`);
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 = textFromBase64url(encoded).trim();
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 = shellSingleQuote(path);
408
- const mkdir = dir && dir !== path ? `mkdir -p ${shellSingleQuote(dir)} && ` : "";
409
- const chmod = executable ? ` && chmod +x ${q}` : "";
410
- const cmd = `${mkdir}printf '%s' ${shellSingleQuote(b64)} | base64 -d > ${q}${chmod}`;
411
- try {
412
- const res = await box.exec(cmd);
413
- if (res.exitCode !== 0) {
414
- return fail(
415
- new Error(
416
- `writeProfileFilesToBox: failed to write ${path} (exit ${res.exitCode}): ${res.stderr.slice(0, 500)}`
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
- } catch (err) {
421
- return fail(new Error(`writeProfileFilesToBox: exec failed for ${path}`, { cause: err }));
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 && await isBoxAlive(found, harness, shell.livenessProbe)) {
512
- const written = await materializeDeferredFilesForExistingBox(shell, found, workspaceId, userId);
513
- if (!written.succeeded) {
514
- throw new Error(`deferred file write failed on reused box ${name}`, { cause: written.error });
515
- }
516
- if (shell.bootstrap) {
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 && await isBoxAlive(resumed.value, harness, shell.livenessProbe)) {
536
- const box2 = resumed.value;
537
- const written = await materializeDeferredFilesForExistingBox(shell, box2, workspaceId, userId);
538
- if (!written.succeeded) {
539
- throw new Error(`deferred file write failed on resumed box ${name}`, { cause: written.error });
540
- }
541
- if (shell.bootstrap) {
542
- const boot = await shell.bootstrap(box2, scope);
543
- if (!boot.succeeded) {
544
- throw new Error(`bootstrap failed on resumed box ${name}`, { cause: boot.error });
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
- const box = await client.create(payload);
709
+ let box = await client.create(payload);
597
710
  await box.waitFor("running", { timeoutMs: 12e4 });
598
- if (!box.connection?.runtimeUrl) await box.refresh();
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-GZGF3JWQ.js.map
1027
+ //# sourceMappingURL=chunk-FFBZJEBE.js.map