@tangle-network/agent-app 0.36.0 → 0.38.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.
@@ -1,15 +1,60 @@
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";
13
+ import {
14
+ resolveTangleExecutionEnvironment,
15
+ trimOrNull
16
+ } from "./chunk-7W5XSTUF.js";
7
17
 
8
18
  // src/sandbox/index.ts
9
19
  import {
10
20
  Sandbox
11
21
  } from "@tangle-network/sandbox";
12
22
 
23
+ // src/sandbox/outcome.ts
24
+ var ok = (value) => ({ succeeded: true, value });
25
+ var fail = (error) => ({
26
+ succeeded: false,
27
+ error: error instanceof Error ? error : new Error(String(error))
28
+ });
29
+
30
+ // src/sandbox/terminal-proxy-token.ts
31
+ var TERMINAL_PROXY_TOKEN_TTL_MS = 15 * 60 * 1e3;
32
+ async function mintTerminalProxyToken(secret, identity, ttlMs = TERMINAL_PROXY_TOKEN_TTL_MS, now = Date.now) {
33
+ if (!secret) return fail(new Error("mintTerminalProxyToken: secret is required"));
34
+ if (!identity.userId || !identity.workspaceId || !identity.sandboxId) {
35
+ return fail(new Error("mintTerminalProxyToken: userId/workspaceId/sandboxId are required"));
36
+ }
37
+ const expiresAt = new Date(now() + ttlMs);
38
+ const payload = { ...identity, exp: Math.floor(expiresAt.getTime() / 1e3) };
39
+ const encoded = base64UrlEncodeText(JSON.stringify(payload));
40
+ const sig = await hmacSha256Base64Url(encoded, secret);
41
+ return ok({ token: `${encoded}.${sig}`, expiresAt });
42
+ }
43
+ async function verifyTerminalProxyToken(secret, token, expected, now = Date.now) {
44
+ if (!secret) return false;
45
+ const [encoded, sig, extra] = token.split(".");
46
+ if (!encoded || !sig || extra !== void 0) return false;
47
+ const expectedSig = await hmacSha256Base64Url(encoded, secret);
48
+ if (!constantTimeEqual(sig, expectedSig)) return false;
49
+ let payload;
50
+ try {
51
+ payload = JSON.parse(base64UrlDecodeText(encoded));
52
+ } catch {
53
+ return false;
54
+ }
55
+ return payload.userId === expected.userId && payload.workspaceId === expected.workspaceId && payload.sandboxId === expected.sandboxId && Number.isFinite(payload.exp) && payload.exp > Math.floor(now() / 1e3);
56
+ }
57
+
13
58
  // src/sandbox/workspace-terminal.ts
14
59
  function createWorkspaceSandboxManager(opts) {
15
60
  return {
@@ -43,9 +88,9 @@ function createWorkspaceSandboxManager(opts) {
43
88
  }
44
89
  };
45
90
  }
46
- var DEFAULT_TERMINAL_TOKEN_PREFIX = "sbxt_";
47
91
  var DEFAULT_TERMINAL_TOKEN_TTL_MS = 15 * 60 * 1e3;
48
92
  var BEARER_SUBPROTOCOL_PREFIX = "bearer.";
93
+ var LEGACY_TERMINAL_TOKEN_PREFIX = "sbxt_";
49
94
  async function createSandboxTerminalToken(subject, opts) {
50
95
  validateTerminalSubject(subject);
51
96
  const secret = opts.secret?.trim();
@@ -53,38 +98,16 @@ async function createSandboxTerminalToken(subject, opts) {
53
98
  const now = opts.now ?? Date.now;
54
99
  const expiresInMs = opts.expiresInMs ?? DEFAULT_TERMINAL_TOKEN_TTL_MS;
55
100
  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
- };
101
+ const minted = await mintTerminalProxyToken(secret, subject, expiresInMs, now);
102
+ if (!minted.succeeded) throw minted.error;
103
+ return minted.value;
68
104
  }
69
105
  async function verifySandboxTerminalToken(token, expected, opts) {
70
106
  validateTerminalSubject(expected);
71
107
  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
108
  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);
109
+ const normalized = token.startsWith(LEGACY_TERMINAL_TOKEN_PREFIX) ? token.slice(LEGACY_TERMINAL_TOKEN_PREFIX.length) : token;
110
+ return verifyTerminalProxyToken(secret ?? "", normalized, expected, now);
88
111
  }
89
112
  function createWorkspaceSandboxConnectionHandler(opts) {
90
113
  return async function handleWorkspaceSandboxConnection({ request, params }) {
@@ -101,7 +124,7 @@ function createWorkspaceSandboxConnectionHandler(opts) {
101
124
  { status: 500 }
102
125
  );
103
126
  }
104
- const directSidecarUrl = box.connection?.sidecarUrl ?? (box.connection?.authToken ? box.connection?.runtimeUrl : void 0);
127
+ const directSidecarUrl = box.connection?.sidecarUrl ?? box.connection?.runtimeUrl;
105
128
  const directSidecarToken = box.connection?.authToken ?? box.connection?.sidecarToken;
106
129
  const directSidecarExpiresAt = box.connection?.authTokenExpiresAt;
107
130
  if (opts.exposeDirectSidecar && directSidecarUrl && directSidecarToken && directSidecarExpiresAt) {
@@ -114,7 +137,7 @@ function createWorkspaceSandboxConnectionHandler(opts) {
114
137
  sandboxId: box.id
115
138
  });
116
139
  }
117
- if (!box.connection?.runtimeUrl) {
140
+ if (!directSidecarUrl) {
118
141
  return Response.json(
119
142
  {
120
143
  error: "Workspace sandbox runtime not ready. The sandbox is still initializing -- retry in a few seconds.",
@@ -128,7 +151,7 @@ function createWorkspaceSandboxConnectionHandler(opts) {
128
151
  try {
129
152
  scoped = await createSandboxTerminalToken(
130
153
  { userId: user.id, workspaceId, sandboxId: box.id },
131
- { secret, expiresInMs: opts.tokenExpiresInMs, prefix: opts.tokenPrefix }
154
+ { secret, expiresInMs: opts.tokenExpiresInMs }
132
155
  );
133
156
  } catch (err) {
134
157
  return Response.json(
@@ -161,20 +184,21 @@ function createWorkspaceSandboxRuntimeProxyHandler(opts) {
161
184
  await opts.requireWorkspaceAccess({ request, userId: user.id, workspaceId, sandboxId });
162
185
  const token = terminalTokenFromRequest(request.headers);
163
186
  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 })) {
187
+ if (!token || !await verifySandboxTerminalToken(token, { userId: user.id, workspaceId, sandboxId }, { secret })) {
165
188
  return Response.json({ error: "Invalid terminal token" }, { status: 403 });
166
189
  }
167
190
  const requestUrl = new URL(request.url);
168
191
  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(
192
+ const directRuntimeConnection = runtimeConnection?.runtimeUrl && runtimeConnection.authToken ? runtimeConnection : null;
193
+ const credentials = directRuntimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
194
+ const upstreamUrl = directRuntimeConnection ? new URL(encodedRuntimePath, `${directRuntimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(
171
195
  `/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${encodedRuntimePath}`,
172
196
  credentials.baseUrl
173
197
  );
174
198
  upstreamUrl.search = requestUrl.search;
175
199
  const headers = buildSandboxRuntimeProxyHeaders(
176
200
  request.headers,
177
- runtimeConnection?.authToken ?? credentials.apiKey,
201
+ directRuntimeConnection?.authToken ?? credentials.apiKey,
178
202
  opts.forwardHeaders
179
203
  );
180
204
  const init = {
@@ -203,7 +227,10 @@ function matchSandboxTerminalWsPath(pathname) {
203
227
  if (!m) return null;
204
228
  const [, workspaceId, sandboxId, subPath] = m;
205
229
  if (!workspaceId || !sandboxId || !subPath) return null;
206
- return { workspaceId: decodeURIComponent(workspaceId), sandboxId: decodeURIComponent(sandboxId), subPath };
230
+ const decodedWorkspaceId = safeDecodeURIComponent(workspaceId);
231
+ const decodedSandboxId = safeDecodeURIComponent(sandboxId);
232
+ if (!decodedWorkspaceId || !decodedSandboxId) return null;
233
+ return { workspaceId: decodedWorkspaceId, sandboxId: decodedSandboxId, subPath };
207
234
  }
208
235
  function isSandboxTerminalWsUpgrade(request) {
209
236
  if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") return false;
@@ -238,16 +265,19 @@ function createWorkspaceSandboxTerminalUpgradeHandler(opts) {
238
265
  }
239
266
  const token = terminalTokenFromRequest(request.headers);
240
267
  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 })) {
268
+ if (!token || !await verifySandboxTerminalToken(token, { userId: user.id, workspaceId, sandboxId }, { secret })) {
242
269
  return new Response("Invalid terminal token", { status: 403 });
243
270
  }
244
271
  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);
272
+ const directRuntimeConnection = runtimeConnection?.runtimeUrl && runtimeConnection.authToken ? runtimeConnection : null;
273
+ const credentials = directRuntimeConnection ? null : await opts.getSandboxApiCredentials({ request, userId: user.id, workspaceId, sandboxId });
274
+ const upstreamUrl = directRuntimeConnection ? new URL(subPath, `${directRuntimeConnection.runtimeUrl.replace(/\/+$/, "")}/`) : new URL(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/runtime/${subPath}`, credentials.baseUrl);
247
275
  upstreamUrl.search = url.search;
248
276
  const upstreamHeaders = new Headers(request.headers);
249
- upstreamHeaders.set("Authorization", `Bearer ${runtimeConnection?.authToken ?? credentials.apiKey}`);
277
+ const upstreamBearer = directRuntimeConnection?.authToken ?? credentials.apiKey;
278
+ upstreamHeaders.set("Authorization", `Bearer ${upstreamBearer}`);
250
279
  upstreamHeaders.delete("host");
280
+ stripBearerSubprotocol(upstreamHeaders);
251
281
  const fetchImpl = opts.fetch ?? fetch;
252
282
  return fetchImpl(upstreamUrl.toString(), { method: request.method, headers: upstreamHeaders });
253
283
  };
@@ -286,7 +316,7 @@ function bearerSubprotocolToken(value) {
286
316
  const encoded = protocol.slice(BEARER_SUBPROTOCOL_PREFIX.length);
287
317
  if (!encoded) return null;
288
318
  try {
289
- const token = textFromBase64url(encoded).trim();
319
+ const token = base64UrlDecodeText(encoded).trim();
290
320
  return token || null;
291
321
  } catch {
292
322
  return null;
@@ -297,45 +327,94 @@ function bearerSubprotocolToken(value) {
297
327
  function terminalTokenFromRequest(headers) {
298
328
  return bearerToken(headers.get("Authorization")) ?? bearerSubprotocolToken(headers.get("Sec-WebSocket-Protocol"));
299
329
  }
330
+ function safeDecodeURIComponent(value) {
331
+ try {
332
+ return decodeURIComponent(value);
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+ function stripBearerSubprotocol(headers) {
338
+ const value = headers.get("Sec-WebSocket-Protocol");
339
+ if (!value) return;
340
+ const protocols = value.split(",").map((part) => part.trim()).filter((part) => part && !part.toLowerCase().startsWith(BEARER_SUBPROTOCOL_PREFIX));
341
+ if (protocols.length) {
342
+ headers.set("Sec-WebSocket-Protocol", protocols.join(", "));
343
+ } else {
344
+ headers.delete("Sec-WebSocket-Protocol");
345
+ }
346
+ }
300
347
  function validateTerminalSubject(subject) {
301
348
  if (!subject.userId) throw new Error("userId is required");
302
349
  if (!subject.workspaceId) throw new Error("workspaceId is required");
303
350
  if (!subject.sandboxId) throw new Error("sandboxId is required");
304
351
  }
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
352
 
333
353
  // 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
- });
354
+ var DEFAULT_SANDBOX_DIRECT_KEY_NAMES = [
355
+ "TCLOUD_SANDBOX_API_KEY",
356
+ "SANDBOX_API_KEY",
357
+ "TANGLE_API_KEY"
358
+ ];
359
+ var DEFAULT_SANDBOX_BASE_URL_NAMES = ["SANDBOX_GATEWAY_URL", "SANDBOX_API_URL"];
360
+ function normalizeBaseUrl(value) {
361
+ return value.trim().replace(/\/v1\/?$/, "").replace(/\/+$/, "");
362
+ }
363
+ function processEnv() {
364
+ return typeof process === "undefined" ? {} : process.env;
365
+ }
366
+ function directEnvCredentialsAllowed(environment, allow) {
367
+ if (typeof allow === "function") return allow(environment);
368
+ if (typeof allow === "boolean") return allow;
369
+ return environment === "development" || environment === "test";
370
+ }
371
+ function resolveSandboxBaseUrl(env, names, defaultBaseUrl) {
372
+ for (const name of names) {
373
+ const value2 = trimOrNull(env[name]);
374
+ if (value2) return normalizeBaseUrl(value2);
375
+ }
376
+ const value = trimOrNull(defaultBaseUrl);
377
+ if (value) return normalizeBaseUrl(value);
378
+ throw new Error(
379
+ `Sandbox base URL is required (set one of ${names.join(", ")} or pass defaultBaseUrl).`
380
+ );
381
+ }
382
+ function resolveDirectSandboxCredentials(env, keyNames, baseUrlNames, defaultBaseUrl) {
383
+ for (const name of keyNames) {
384
+ const apiKey = trimOrNull(env[name]);
385
+ if (!apiKey) continue;
386
+ return {
387
+ apiKey,
388
+ baseUrl: resolveSandboxBaseUrl(env, baseUrlNames, defaultBaseUrl)
389
+ };
390
+ }
391
+ return null;
392
+ }
393
+ async function resolveSandboxClientCredentials(options = {}) {
394
+ const env = options.env ?? processEnv();
395
+ const environment = options.environment ?? resolveTangleExecutionEnvironment(env);
396
+ const keyNames = options.directKeyNames ?? DEFAULT_SANDBOX_DIRECT_KEY_NAMES;
397
+ const baseUrlNames = options.baseUrlNames ?? DEFAULT_SANDBOX_BASE_URL_NAMES;
398
+ const directAllowed = directEnvCredentialsAllowed(environment, options.allowDirectEnvCredentials);
399
+ const direct = () => directAllowed ? resolveDirectSandboxCredentials(env, keyNames, baseUrlNames, options.defaultBaseUrl) : null;
400
+ if (environment === "development" || environment === "test") {
401
+ const credentials2 = direct();
402
+ if (credentials2) return credentials2;
403
+ }
404
+ const provisioned = await options.provision?.({ environment, env });
405
+ if (provisioned) {
406
+ return {
407
+ apiKey: provisioned.apiKey,
408
+ baseUrl: normalizeBaseUrl(provisioned.baseUrl)
409
+ };
410
+ }
411
+ const credentials = direct();
412
+ if (credentials) return credentials;
413
+ const directHint = directAllowed ? ` or set one of ${keyNames.join(", ")}` : "";
414
+ throw new Error(
415
+ `Sandbox credentials are required for ${environment} (provide a provision callback${directHint}).`
416
+ );
417
+ }
339
418
  var DEFAULT_SANDBOX_RESOURCES = {
340
419
  image: "universal",
341
420
  cpuCores: 2,
@@ -380,6 +459,87 @@ function buildAppToolMcpServers(options) {
380
459
  function shellSingleQuote(value) {
381
460
  return `'${value.replace(/'/g, `'\\''`)}'`;
382
461
  }
462
+ var DEFAULT_SANDBOX_TOOL_BASE_DIR = "/home/agent/tools";
463
+ var SAFE_TOOL_SEGMENT = /^[A-Za-z0-9._-]+$/;
464
+ function normalizeSandboxToolSegment(value, label) {
465
+ const segment = value.trim();
466
+ if (!segment || segment === "." || segment === ".." || !SAFE_TOOL_SEGMENT.test(segment)) {
467
+ throw new Error(`${label} must contain only letters, numbers, dots, underscores, or hyphens.`);
468
+ }
469
+ return segment;
470
+ }
471
+ function normalizeSandboxToolDir(value, label) {
472
+ const dir = value.trim().replace(/\/+$/, "");
473
+ if (!dir || !dir.startsWith("/") || dir.includes("\0") || dir.includes("\n")) {
474
+ throw new Error(`${label} must be an absolute sandbox path.`);
475
+ }
476
+ return dir === "" ? "/" : dir;
477
+ }
478
+ function sandboxToolRootDir(options) {
479
+ const appName = normalizeSandboxToolSegment(options.appName, "sandbox tool appName");
480
+ const baseDir = normalizeSandboxToolDir(
481
+ options.baseDir ?? DEFAULT_SANDBOX_TOOL_BASE_DIR,
482
+ "sandbox tool baseDir"
483
+ );
484
+ return `${baseDir}/${appName}`;
485
+ }
486
+ function sandboxToolBinDir(options) {
487
+ normalizeSandboxToolSegment(options.appName, "sandbox tool appName");
488
+ if (options.binDir) return normalizeSandboxToolDir(options.binDir, "sandbox tool binDir");
489
+ return `${sandboxToolRootDir(options)}/bin`;
490
+ }
491
+ function sandboxToolPath(options) {
492
+ const toolName = normalizeSandboxToolSegment(options.toolName, "sandbox tool name");
493
+ return `${sandboxToolBinDir(options)}/${toolName}`;
494
+ }
495
+ function buildSandboxToolFileMounts(options) {
496
+ return options.tools.map((tool) => {
497
+ const name = normalizeSandboxToolSegment(tool.name, "sandbox tool name");
498
+ return {
499
+ path: sandboxToolPath({ ...options, toolName: name }),
500
+ resource: { kind: "inline", name, content: tool.content },
501
+ executable: tool.executable ?? true
502
+ };
503
+ });
504
+ }
505
+ function buildSandboxToolPathSetupScript(options) {
506
+ const binDir = sandboxToolBinDir(options);
507
+ const exportLine = `export PATH=${binDir}:$PATH`;
508
+ return [
509
+ "set -eu",
510
+ `mkdir -p ${shellSingleQuote(binDir)}`,
511
+ `PATH=${shellSingleQuote(binDir)}:$PATH`,
512
+ "export PATH",
513
+ 'for profile in "${HOME:-/home/agent}/.profile" "${HOME:-/home/agent}/.bashrc" "${HOME:-/home/agent}/.zshrc"; do',
514
+ ' mkdir -p "$(dirname "$profile")"',
515
+ ' touch "$profile"',
516
+ ` grep -Fqx ${shellSingleQuote(exportLine)} "$profile" || printf '\\n%s\\n' ${shellSingleQuote(exportLine)} >> "$profile"`,
517
+ "done"
518
+ ].join("\n");
519
+ }
520
+ async function runSandboxToolPathSetup(box, options) {
521
+ try {
522
+ const res = await box.exec(buildSandboxToolPathSetupScript(options));
523
+ if (res.exitCode !== 0) {
524
+ return fail(
525
+ new Error(
526
+ `runSandboxToolPathSetup: failed to configure PATH for ${sandboxToolBinDir(options)} (exit ${res.exitCode}): ${res.stderr.slice(0, 500)}`
527
+ )
528
+ );
529
+ }
530
+ return ok(void 0);
531
+ } catch (err) {
532
+ return fail(new Error("runSandboxToolPathSetup: exec failed", { cause: err }));
533
+ }
534
+ }
535
+ function shellPath(path) {
536
+ if (path === "~") return '"$HOME"';
537
+ if (path.startsWith("~/")) {
538
+ const rest = path.slice(2);
539
+ return rest ? `"$HOME"/${shellSingleQuote(rest)}` : '"$HOME"';
540
+ }
541
+ return shellSingleQuote(path);
542
+ }
383
543
  function splitDeferredProfileFiles(profile) {
384
544
  const files = profile.resources?.files ?? [];
385
545
  const deferredFiles = [];
@@ -395,6 +555,25 @@ function splitDeferredProfileFiles(profile) {
395
555
  };
396
556
  return { leanProfile, deferredFiles };
397
557
  }
558
+ var PROFILE_WRITE_B64_CHUNK_CHARS = 3e3;
559
+ var PROFILE_WRITE_MAX_429_RETRIES = 4;
560
+ var PROFILE_WRITE_RETRY_BASE_MS = 250;
561
+ var PROFILE_WRITE_RETRY_MAX_MS = 2e3;
562
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
563
+ function rateLimit(err) {
564
+ if (err && typeof err === "object") {
565
+ const e = err;
566
+ const retryAfterMs = typeof e.retryAfterMs === "number" ? e.retryAfterMs : void 0;
567
+ if (e.status === 429) return { is429: true, retryAfterMs };
568
+ if (typeof e.code === "string" && /rate.?limit|too.?many.?requests|429/i.test(e.code)) {
569
+ return { is429: true, retryAfterMs };
570
+ }
571
+ if (typeof e.message === "string" && /\b429\b|rate.?limit|too many requests/i.test(e.message)) {
572
+ return { is429: true, retryAfterMs };
573
+ }
574
+ }
575
+ return { is429: false };
576
+ }
398
577
  async function writeProfileFilesToBox(box, files) {
399
578
  for (const mount of files) {
400
579
  if (mount.resource.kind !== "inline") continue;
@@ -404,22 +583,45 @@ async function writeProfileFilesToBox(box, files) {
404
583
  const dir = path.replace(/\/[^/]*$/, "");
405
584
  const isBin = /(^|\/)(s?bin)\//.test(path);
406
585
  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
- );
586
+ const q = shellPath(path);
587
+ const qb64 = shellPath(`${path}.b64`);
588
+ const step = async (cmd) => {
589
+ for (let attempt = 0; ; attempt++) {
590
+ try {
591
+ const res2 = await box.exec(cmd);
592
+ if (res2.exitCode !== 0) {
593
+ return fail(
594
+ new Error(
595
+ `writeProfileFilesToBox: failed to write ${path} (exit ${res2.exitCode}): ${res2.stderr.slice(0, 500)}`
596
+ )
597
+ );
598
+ }
599
+ return ok(void 0);
600
+ } catch (err) {
601
+ const { is429, retryAfterMs } = rateLimit(err);
602
+ if (is429 && attempt < PROFILE_WRITE_MAX_429_RETRIES) {
603
+ const backoff = Math.min(
604
+ PROFILE_WRITE_RETRY_BASE_MS * 2 ** attempt,
605
+ PROFILE_WRITE_RETRY_MAX_MS
606
+ );
607
+ await sleep(retryAfterMs ?? backoff);
608
+ continue;
609
+ }
610
+ return fail(new Error(`writeProfileFilesToBox: exec failed for ${path}`, { cause: err }));
611
+ }
419
612
  }
420
- } catch (err) {
421
- return fail(new Error(`writeProfileFilesToBox: exec failed for ${path}`, { cause: err }));
613
+ };
614
+ const mkdir = dir && dir !== path ? `mkdir -p ${shellPath(dir)} && ` : "";
615
+ let res = await step(`${mkdir}: > ${qb64}`);
616
+ if (!res.succeeded) return res;
617
+ for (let i = 0; i < b64.length; i += PROFILE_WRITE_B64_CHUNK_CHARS) {
618
+ const slice = b64.slice(i, i + PROFILE_WRITE_B64_CHUNK_CHARS);
619
+ res = await step(`printf '%s' '${slice}' >> ${qb64}`);
620
+ if (!res.succeeded) return res;
422
621
  }
622
+ const chmod = executable ? ` && chmod +x ${q}` : "";
623
+ res = await step(`base64 -d ${qb64} > ${q} && rm -f ${qb64}${chmod}`);
624
+ if (!res.succeeded) return res;
423
625
  }
424
626
  return ok(void 0);
425
627
  }
@@ -479,6 +681,38 @@ async function isBoxAlive(box, harness, probe) {
479
681
  return false;
480
682
  }
481
683
  }
684
+ var RUNTIME_CONNECTION_WAIT_MS = 3e4;
685
+ var RUNTIME_CONNECTION_POLL_MS = 1e3;
686
+ function sandboxRuntimeUrl(box) {
687
+ const connection = box.connection;
688
+ return connection?.sidecarUrl ?? connection?.runtimeUrl;
689
+ }
690
+ function sandboxEdgeFailed(box) {
691
+ const connection = box.connection;
692
+ return connection?.edgeStatus === "failed" || Boolean(connection?.edgeError);
693
+ }
694
+ async function refreshRuntimeConnection(client, box) {
695
+ let current = box;
696
+ if (sandboxRuntimeUrl(current)) return current;
697
+ const deadline = Date.now() + RUNTIME_CONNECTION_WAIT_MS;
698
+ while (Date.now() < deadline) {
699
+ try {
700
+ await current.refresh();
701
+ if (sandboxRuntimeUrl(current)) return current;
702
+ const latest = await client.get(current.id);
703
+ if (latest) current = latest;
704
+ if (sandboxRuntimeUrl(current)) return current;
705
+ } catch {
706
+ }
707
+ await new Promise((resolve) => setTimeout(resolve, RUNTIME_CONNECTION_POLL_MS));
708
+ }
709
+ return current;
710
+ }
711
+ async function isReusableBox(box, harness, probe) {
712
+ if (sandboxEdgeFailed(box)) return false;
713
+ if (!sandboxRuntimeUrl(box)) return false;
714
+ return isBoxAlive(box, harness, probe);
715
+ }
482
716
  async function resumeStoppedBox(client, name, timeoutMs) {
483
717
  try {
484
718
  const stopped = await client.list({ status: "stopped" });
@@ -508,18 +742,28 @@ async function ensureWorkspaceSandbox(shell, options) {
508
742
  if (!dropped.succeeded) {
509
743
  throw new Error(`forceNew: sandbox ${name} could not be deleted`, { cause: dropped.error });
510
744
  }
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 });
745
+ } else if (found.metadata?.harness === harness) {
746
+ const ready = await refreshRuntimeConnection(client, found);
747
+ if (await isReusableBox(ready, harness, shell.livenessProbe)) {
748
+ const written = await materializeDeferredFilesForExistingBox(shell, ready, workspaceId, userId);
749
+ if (!written.succeeded) {
750
+ throw new Error(`deferred file write failed on reused box ${name}`, { cause: written.error });
751
+ }
752
+ if (shell.bootstrap) {
753
+ const boot = await shell.bootstrap(ready, scope);
754
+ if (!boot.succeeded) {
755
+ throw new Error(`bootstrap failed on reused box ${name}`, { cause: boot.error });
756
+ }
520
757
  }
758
+ return ready;
759
+ }
760
+ const dropped = await deleteBox(ready);
761
+ if (!dropped.succeeded) {
762
+ throw new Error(
763
+ `sandbox ${name} (was ${String(found.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
764
+ { cause: dropped.error }
765
+ );
521
766
  }
522
- return found;
523
767
  } else {
524
768
  const dropped = await deleteBox(found);
525
769
  if (!dropped.succeeded) {
@@ -532,19 +776,28 @@ async function ensureWorkspaceSandbox(shell, options) {
532
776
  }
533
777
  if (!forceNew && shell.resumeStopped !== false) {
534
778
  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 });
779
+ if (resumed.succeeded && resumed.value) {
780
+ const box2 = await refreshRuntimeConnection(client, resumed.value);
781
+ if (await isReusableBox(box2, harness, shell.livenessProbe)) {
782
+ const written = await materializeDeferredFilesForExistingBox(shell, box2, workspaceId, userId);
783
+ if (!written.succeeded) {
784
+ throw new Error(`deferred file write failed on resumed box ${name}`, { cause: written.error });
545
785
  }
786
+ if (shell.bootstrap) {
787
+ const boot = await shell.bootstrap(box2, scope);
788
+ if (!boot.succeeded) {
789
+ throw new Error(`bootstrap failed on resumed box ${name}`, { cause: boot.error });
790
+ }
791
+ }
792
+ return box2;
793
+ }
794
+ const dropped = await deleteBox(box2);
795
+ if (!dropped.succeeded) {
796
+ throw new Error(
797
+ `resumed sandbox ${name} (was ${String(box2.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
798
+ { cause: dropped.error }
799
+ );
546
800
  }
547
- return box2;
548
801
  }
549
802
  }
550
803
  const connectedIntegrationIds = await shell.connectedIntegrationIds(workspaceId);
@@ -585,6 +838,7 @@ async function ensureWorkspaceSandbox(shell, options) {
585
838
  backend: { type: harness, profile, ...model ? { model } : {} },
586
839
  ...storage ? { storage } : {},
587
840
  ...restore ? restore : {},
841
+ ...shell.webTerminalEnabled ? { webTerminalEnabled: true } : {},
588
842
  maxLifetimeSeconds: resources.maxLifetimeSeconds,
589
843
  idleTimeoutSeconds: resources.idleTimeoutSeconds,
590
844
  resources: {
@@ -593,9 +847,9 @@ async function ensureWorkspaceSandbox(shell, options) {
593
847
  diskGB: resources.diskGB
594
848
  }
595
849
  };
596
- const box = await client.create(payload);
850
+ let box = await client.create(payload);
597
851
  await box.waitFor("running", { timeoutMs: 12e4 });
598
- if (!box.connection?.runtimeUrl) await box.refresh();
852
+ box = await refreshRuntimeConnection(client, box);
599
853
  if (deferredFiles.length > 0) {
600
854
  const written = await writeProfileFilesToBox(box, deferredFiles);
601
855
  if (!written.succeeded) {
@@ -828,64 +1082,6 @@ async function driveSandboxTurn(shell, box, message, options) {
828
1082
  return fail(err);
829
1083
  }
830
1084
  }
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
1085
  var SEVERED_FINISH_REASONS = /* @__PURE__ */ new Set(["error", "other", "unknown"]);
890
1086
  function asPlainRecord(v) {
891
1087
  return v && typeof v === "object" && !Array.isArray(v) ? v : null;
@@ -928,6 +1124,8 @@ function firstQuestionText(value) {
928
1124
  }
929
1125
 
930
1126
  export {
1127
+ mintTerminalProxyToken,
1128
+ verifyTerminalProxyToken,
931
1129
  createWorkspaceSandboxManager,
932
1130
  createSandboxTerminalToken,
933
1131
  verifySandboxTerminalToken,
@@ -941,10 +1139,17 @@ export {
941
1139
  bearerToken,
942
1140
  bearerSubprotocolToken,
943
1141
  terminalTokenFromRequest,
1142
+ resolveSandboxClientCredentials,
944
1143
  DEFAULT_SANDBOX_RESOURCES,
945
1144
  getClient,
946
1145
  resetClientCache,
947
1146
  buildAppToolMcpServers,
1147
+ sandboxToolRootDir,
1148
+ sandboxToolBinDir,
1149
+ sandboxToolPath,
1150
+ buildSandboxToolFileMounts,
1151
+ buildSandboxToolPathSetupScript,
1152
+ runSandboxToolPathSetup,
948
1153
  splitDeferredProfileFiles,
949
1154
  writeProfileFilesToBox,
950
1155
  ensureWorkspaceSandbox,
@@ -963,10 +1168,8 @@ export {
963
1168
  deleteSecret,
964
1169
  mintSandboxScopedToken,
965
1170
  driveSandboxTurn,
966
- mintTerminalProxyToken,
967
- verifyTerminalProxyToken,
968
1171
  classifySeveredStream,
969
1172
  isTerminalPromptEvent,
970
1173
  detectInteractiveQuestion
971
1174
  };
972
- //# sourceMappingURL=chunk-GZGF3JWQ.js.map
1175
+ //# sourceMappingURL=chunk-MJ3RUHD4.js.map