@tangle-network/sandbox 0.10.3 → 0.10.5-develop.20260713235503.8f2cefc

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,4 +1,4 @@
1
- import { $ as EventStreamOptions, A as CommitTaskSessionOptions, B as CreateTaskSessionOptions, Bt as IntelligenceReport, Ci as WriteManyFile, Ct as GitBranch, D as CodeLanguage, Dt as GitStatus, E as CodeExecutionResult, Er as SandboxTraceBundle, Et as GitDiff, Gr as SessionMessage, Gt as JsonValue, Hr as SessionForkOptions, Ht as IntelligenceReportCompareTo, In as SandboxEvent, Jr as SnapshotInfo, Kt as ListMessagesOptions, Lr as SendSessionMessageOptions, Mn as SSHCredentials, Nr as SearchMatch, Nt as GpuLeaseManager, On as RestoreSnapshotOptions, Ot as GpuLease, Pn as SandboxConnection, Pr as SearchOptions, Qr as StartupDiagnostics, Rr as SendSessionMessageRequest, Si as WriteFileOptions, T as CodeExecutionOptions, U as DispatchPromptOptions, Ur as SessionInfo, Vr as SessionEventStreamOptions, Vt as IntelligenceReportBudget, W as DispatchedSession, Wr as SessionListOptions, Wt as IntelligenceReportWindow, Xr as SnapshotResult, Y as DriverInfo, Yr as SnapshotOptions, Z as EgressManager, Zt as MintScopedTokenOptions, _n as ProvisionResult, ai as TaskSessionCommitResult, an as PreviewLinkManager, br as SandboxRuntimeProfileList, di as TeeAttestationResponse, dt as FileWriteResult, en as NetworkManager, et as ExecOptions, gi as TurnDriveResult, gn as ProvisionEvent, gr as SandboxResourceUsage, hn as PromptResult, ii as TaskSessionChanges, j as CompletedTurnResult, jn as SSHCommandDescriptor, jr as ScopedToken, kn as ResumeOptions, kr as SandboxTraceOptions, l as BackendManager, li as TeeAttestationOptions, ln as ProcessManager, mn as PromptOptions, ni as TaskOptions, pi as TeePublicKeyResponse, pn as PromptInputPart, pr as SandboxPortBinding, q as DriveTurnOptions, ri as TaskResult, rn as PermissionsManager, s as BackendConfig, si as TaskSessionInfo, st as FileSystem, tr as SandboxFleetTraceBundle, tt as ExecResult, ur as SandboxInfo, vr as SandboxRuntimeHealth, w as BranchOptions, wi as WriteManyOptions, wr as SandboxTerminalManager, wt as GitCommit, xi as WaitForOptions, xr as SandboxStatus, z as CreateSessionOptions, zr as SentSessionMessage, zt as InstalledTool } from "./types-Cmpv-ox_.js";
1
+ import { $ as EventStreamOptions, $r as SessionListOptions, A as CommitTaskSessionOptions, Ai as WaitForOptions, An as RestoreSnapshotOptions, Ar as SandboxStatus, B as CreateTaskSessionOptions, Bn as SSHCommandDescriptor, Br as ScopedToken, Bt as InstalledTool, D as CodeLanguage, Dr as SandboxRuntimeHealth, Dt as GitDiff, E as CodeExecutionResult, Fr as SandboxTraceBundle, Gn as SandboxEvent, Gt as IntelligenceReportWindow, Hr as SearchMatch, Ht as IntelligenceReportBudget, In as RolloutStartResult, Jr as SentSessionMessage, Kr as SendSessionMessageOptions, Kt as JsonValue, Mi as WriteFileOptions, Mn as Rollout, Ni as WriteManyFile, Nr as SandboxTerminalManager, Ot as GitStatus, Pi as WriteManyOptions, Pn as RolloutOptions, Pt as GpuLeaseManager, Qr as SessionInfo, Qt as MintScopedTokenOptions, Rr as SandboxTraceOptions, Si as TeePublicKeyResponse, Sr as SandboxPortBinding, T as CodeExecutionOptions, Ti as TurnDriveResult, Tr as SandboxResourceUsage, Tt as GitCommit, U as DispatchPromptOptions, Un as SandboxConnection, Ur as SearchOptions, Ut as IntelligenceReportCompareTo, Vn as SSHCredentials, Vt as IntelligenceReport, W as DispatchedSession, Xr as SessionEventStreamOptions, Y as DriverInfo, Z as EgressManager, Zr as SessionForkOptions, _n as ProvisionEvent, ai as SnapshotResult, bi as TeeAttestationResponse, ct as FileSystem, di as TaskOptions, ei as SessionMessage, et as ExecOptions, fi as TaskResult, ft as FileWriteResult, gi as TaskSessionInfo, gn as PromptResult, hn as PromptOptions, ii as SnapshotOptions, in as PermissionsManager, j as CompletedTurnResult, ji as WaitForRolloutOptions, jn as ResumeOptions, kr as SandboxRuntimeProfileList, kt as GpuLease, l as BackendManager, mi as TaskSessionCommitResult, mn as PromptInputPart, on as PreviewLinkManager, pi as TaskSessionChanges, q as DriveTurnOptions, qr as SendSessionMessageRequest, qt as ListMessagesOptions, ri as SnapshotInfo, s as BackendConfig, si as StartupDiagnostics, tn as NetworkManager, tt as ExecResult, un as ProcessManager, ur as SandboxFleetTraceBundle, vi as TeeAttestationOptions, vn as ProvisionResult, w as BranchOptions, wt as GitBranch, yr as SandboxInfo, z as CreateSessionOptions } from "./types-yNRVRww-.js";
2
2
 
3
3
  //#region src/mcp.d.ts
4
4
  /**
@@ -900,6 +900,7 @@ declare class SandboxInstance {
900
900
  private fsList;
901
901
  private fsStat;
902
902
  private fsDelete;
903
+ private fsRename;
903
904
  private fsMkdir;
904
905
  private fsExists;
905
906
  /**
@@ -1236,8 +1237,52 @@ declare class SandboxInstance {
1236
1237
  * ```
1237
1238
  */
1238
1239
  branch(count: number, options?: BranchOptions): Promise<SandboxInstance[]>;
1240
+ /**
1241
+ * Speculative rollouts: branch this RUNNING sandbox into `n` copy-on-write
1242
+ * children, run the SAME agent turn in every child in parallel, and
1243
+ * optionally score each child by running `scorer.command` inside it (exit 0
1244
+ * passes; stdout is parsed as the numeric score — highest wins).
1245
+ *
1246
+ * Async-op shape: resolves as soon as the children exist (their turns are
1247
+ * still running); poll {@link getRollout} or use {@link waitForRollout} for
1248
+ * results. Children are first-class sandboxes the caller may promote or
1249
+ * delete — each occupies a concurrent-session quota slot like a create.
1250
+ *
1251
+ * @example
1252
+ * ```typescript
1253
+ * const started = await box.rollout({
1254
+ * n: 4,
1255
+ * parts: [{ type: "text", text: "make the failing test pass" }],
1256
+ * scorer: { type: "command", command: "npm test --silent && echo 1" },
1257
+ * });
1258
+ * const settled = await box.waitForRollout(started.rolloutId);
1259
+ * console.log(settled.winner, settled.results);
1260
+ * ```
1261
+ */
1262
+ rollout(options: RolloutOptions): Promise<RolloutStartResult>;
1263
+ /**
1264
+ * Poll a rollout's status and per-child results.
1265
+ *
1266
+ * Failed or timed-out children appear as result entries with `error` set —
1267
+ * partial results are never dropped. Rollout poll-state lives in
1268
+ * orchestrator RAM: after an orchestrator restart this returns 404 while
1269
+ * the children themselves survive as first-class sandboxes.
1270
+ */
1271
+ getRollout(rolloutId: string): Promise<Rollout>;
1272
+ /**
1273
+ * Poll {@link getRollout} until the rollout leaves `running`, then return
1274
+ * the terminal record — including `partial`/`failed` outcomes, whose
1275
+ * per-child `error` entries are results the caller inspects, not
1276
+ * exceptions. Throws {@link TimeoutError} on deadline or abort.
1277
+ */
1278
+ waitForRollout(rolloutId: string, options?: WaitForRolloutOptions): Promise<Rollout>;
1239
1279
  /**
1240
1280
  * Stop the sandbox (keeps state for resume).
1281
+ *
1282
+ * The stop response carries the orchestrator's per-phase stop breakdown
1283
+ * (`stop_metering` / `container_kill` / `stop_complete` /
1284
+ * `stop_unaccounted`); after this resolves, {@link startupDiagnostics}
1285
+ * returns it (replacing the create breakdown).
1241
1286
  */
1242
1287
  stop(): Promise<void>;
1243
1288
  /**
@@ -1246,8 +1291,20 @@ declare class SandboxInstance {
1246
1291
  resume(options?: ResumeOptions): Promise<void>;
1247
1292
  /**
1248
1293
  * Delete the sandbox permanently.
1294
+ *
1295
+ * The delete response carries the orchestrator's per-phase delete breakdown
1296
+ * (`container_remove` / `storage_binding_wait` / `cleanup_parallel` /
1297
+ * `delete_complete` / `delete_unaccounted`); after this resolves,
1298
+ * {@link startupDiagnostics} returns it for post-mortem reads on the
1299
+ * now-gone sandbox.
1249
1300
  */
1250
1301
  delete(): Promise<void>;
1302
+ /**
1303
+ * Read the per-phase lifecycle breakdown off a stop/resume/delete response
1304
+ * body. Never throws: diagnostics are measurement data — a body that fails
1305
+ * to parse (older API, empty body) must not fail the lifecycle call.
1306
+ */
1307
+ private readLifecycleDiagnostics;
1251
1308
  /**
1252
1309
  * keepAlive is intentionally unavailable until the API exposes timeout updates.
1253
1310
  * @param seconds - Reserved for future support
@@ -1442,15 +1499,10 @@ declare class SandboxInstance {
1442
1499
  */
1443
1500
  driveTurn(message: string | PromptInputPart[], opts: DriveTurnOptions): Promise<TurnDriveResult>;
1444
1501
  /**
1445
- * Mint a scoped, time-bounded JWT for direct browser access to this
1446
- * sandbox (Issue #913 Gap 1). Authority is the caller's
1447
- * `TANGLE_API_KEY` (sk-tan-*) the Sandbox API mints the token;
1448
- * signing secrets stay server-side.
1449
- *
1450
- * Use this to give a browser direct read access to the sandbox without
1451
- * leaking the full bearer (`box.connection.authToken`). The returned
1452
- * token verifies against the same sidecar middleware that already
1453
- * gates ProductTokenIssuer-issued JWTs — no new sidecar surface.
1502
+ * Mint a scoped, time-bounded JWT for browser access (Issue #913 Gap 1).
1503
+ * Authority is the caller's `TANGLE_API_KEY` (sk-tan-*); signing secrets
1504
+ * stay server-side. Session/project scopes work with SessionGatewayClient,
1505
+ * while session-runtime/read-only scopes work with SandboxRuntimeClient.
1454
1506
  */
1455
1507
  mintScopedToken(opts: MintScopedTokenOptions): Promise<ScopedToken>;
1456
1508
  /** @internal — invoked by SandboxSession.status(). */
@@ -1,5 +1,5 @@
1
- import { n as normalizeSessionInfo, r as normalizeRuntimeBackendConfig, t as SandboxRuntimeApi } from "./runtime-api-CYA2DUQw.js";
2
- import { c as SandboxError, d as TimeoutError, f as ValidationError, i as NetworkError, l as ServerError, n as CapabilityError, p as parseErrorResponse, s as QuotaError, t as AuthError, u as StateError } from "./errors-DSz87Rkk.js";
1
+ import { n as normalizeSessionInfo, r as normalizeRuntimeBackendConfig, t as SandboxRuntimeApi } from "./runtime-api-CaXI7Kl2.js";
2
+ import { c as SandboxError, d as TimeoutError, f as ValidationError, i as NetworkError, l as ServerError, n as CapabilityError, p as parseErrorResponse, s as QuotaError, t as AuthError, u as StateError } from "./errors-wd266B9Q.js";
3
3
  import { addTokenUsage, readTokenCostUsd, readTokenUsage } from "@tangle-network/agent-core";
4
4
  //#region src/lib/sse-parser.ts
5
5
  /**
@@ -752,6 +752,13 @@ function isTransientSnapshotVisibilityError(error) {
752
752
  return message.includes("SNAPSHOT_NOT_FOUND") || message.includes("Failed to list snapshot") || message.includes("Storage agent error (404)");
753
753
  }
754
754
  /**
755
+ * The extra separator is intentional: absolute paths produce `//`, allowing
756
+ * the runtime route wildcard to retain the path's leading slash.
757
+ */
758
+ function runtimeFilesystemPath(endpoint, path) {
759
+ return `${endpoint}/${path}`;
760
+ }
761
+ /**
755
762
  * Single source of truth for a run's outcome, shared by
756
763
  * `prompt`/`task`/`_sessionResult`/turn settling. A run succeeds only when it
757
764
  * reached a terminal event, raised no run-level error, hit no approval gate,
@@ -913,6 +920,22 @@ const WRITE_MANY_DEFAULT_PACE_MS = 150;
913
920
  const WRITE_MANY_DEFAULT_MAX_RETRIES = 4;
914
921
  const WRITE_MANY_RETRY_BASE_MS = 250;
915
922
  const WRITE_MANY_RETRY_MAX_MS = 2e3;
923
+ function parseTextFileReadResponse(body) {
924
+ let payload;
925
+ try {
926
+ payload = JSON.parse(body);
927
+ } catch {
928
+ throw new ServerError("Runtime returned invalid JSON for file read", 502, {
929
+ origin: "runtime",
930
+ endpoint: "/files/read"
931
+ });
932
+ }
933
+ if (typeof payload !== "object" || payload === null || !("success" in payload) || payload.success !== true || !("data" in payload) || typeof payload.data !== "object" || payload.data === null || !("content" in payload.data) || typeof payload.data.content !== "string" || !("encoding" in payload.data) || payload.data.encoding !== "utf8") throw new ServerError("Runtime returned a malformed text file response", 502, {
934
+ origin: "runtime",
935
+ endpoint: "/files/read"
936
+ });
937
+ return payload.data.content;
938
+ }
916
939
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
917
940
  /** Transient file-write failures worth retrying: rate-limit (quota), 5xx,
918
941
  * transport (network), and request timeouts. A non-transient error (bad path,
@@ -1324,18 +1347,20 @@ var SandboxInstance = class SandboxInstance {
1324
1347
  */
1325
1348
  async read(path, options) {
1326
1349
  await this.ensureRunning();
1327
- const headers = {};
1328
- if (options?.sessionId) headers["X-Session-Id"] = options.sessionId;
1350
+ const headers = options?.sessionId ? { "X-Session-Id": options.sessionId } : void 0;
1329
1351
  const response = await this.runtimeFetch("/files/read", {
1330
1352
  method: "POST",
1331
1353
  headers,
1332
- body: JSON.stringify({ path })
1354
+ body: JSON.stringify({
1355
+ path,
1356
+ encoding: "utf8"
1357
+ })
1333
1358
  });
1334
1359
  if (!response.ok) {
1335
1360
  const body = await response.text();
1336
1361
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1337
1362
  }
1338
- return (await response.json()).data?.content ?? "";
1363
+ return parseTextFileReadResponse(await response.text());
1339
1364
  }
1340
1365
  /**
1341
1366
  * Write content to a file in the sandbox.
@@ -1537,8 +1562,8 @@ var SandboxInstance = class SandboxInstance {
1537
1562
  * `error` + `done` so callers never see a thrown generator.
1538
1563
  */
1539
1564
  async *streamPromptInner(message, options) {
1565
+ if (options?.lastEventId && !options.executionId) throw new ValidationError("lastEventId requires executionId so the SDK can replay the correct execution", { executionId: "Required when lastEventId is set" });
1540
1566
  await this.ensureRunning();
1541
- const parts = encodePromptForWire(message);
1542
1567
  const timeoutMs = typeof options?.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : void 0;
1543
1568
  const timeoutSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0;
1544
1569
  const streamSignal = timeoutSignal ? options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal : options?.signal;
@@ -1556,26 +1581,47 @@ var SandboxInstance = class SandboxInstance {
1556
1581
  ...data
1557
1582
  });
1558
1583
  };
1584
+ const explicitReplay = Boolean(options?.executionId && options.lastEventId);
1585
+ const requestEndpoint = explicitReplay ? `/agents/run/${encodeURIComponent(options?.executionId ?? "")}/events?lastEventId=${encodeURIComponent(options?.lastEventId ?? "")}&format=sse` : "/agents/run/stream";
1586
+ let lastEventId = options?.lastEventId;
1587
+ let executionId = options?.executionId;
1588
+ let sessionId = options?.sessionId;
1589
+ let receivedTerminal = false;
1590
+ const RECONNECT_BACKOFF_MS = [
1591
+ 2e3,
1592
+ 4e3,
1593
+ 8e3,
1594
+ 16e3,
1595
+ 3e4,
1596
+ 3e4
1597
+ ];
1598
+ const MAX_RECONNECT_ATTEMPTS = RECONNECT_BACKOFF_MS.length;
1599
+ let lastReplayStatus;
1600
+ let lastExecutionStatus;
1601
+ let firstSseEventSeen = false;
1559
1602
  let response;
1560
1603
  try {
1561
1604
  logStreamCheckpoint("runtime_post_start", {
1562
- endpoint: "/agents/run/stream",
1605
+ endpoint: requestEndpoint,
1563
1606
  sessionId: options?.sessionId,
1564
1607
  executionId: options?.executionId,
1565
1608
  hasLastEventId: Boolean(options?.lastEventId),
1566
- detach: options?.detach === true
1609
+ detach: options?.detach === true,
1610
+ replayOnly: explicitReplay
1567
1611
  });
1568
- response = await this.runtimeFetch("/agents/run/stream", {
1612
+ const request = explicitReplay ? {
1613
+ method: "GET",
1614
+ signal: streamSignal
1615
+ } : {
1569
1616
  method: "POST",
1570
1617
  signal: streamSignal,
1571
1618
  headers: {
1572
1619
  ...options?.executionId ? { "X-Execution-ID": options.executionId } : {},
1573
- ...options?.lastEventId ? { "Last-Event-ID": options.lastEventId } : {},
1574
1620
  ...options?.detach ? { "x-agent-detach": "true" } : {}
1575
1621
  },
1576
1622
  body: JSON.stringify({
1577
1623
  identifier: "default",
1578
- parts,
1624
+ parts: encodePromptForWire(message),
1579
1625
  sessionId: options?.sessionId,
1580
1626
  turnId: options?.turnId,
1581
1627
  metadata: options?.context,
@@ -1584,7 +1630,8 @@ var SandboxInstance = class SandboxInstance {
1584
1630
  ...options?.detach ? { detach: true } : {},
1585
1631
  backend: normalizeRuntimeBackendConfig(options?.backend ?? this.defaultRuntimeBackend, { model: options?.model })
1586
1632
  })
1587
- });
1633
+ };
1634
+ response = await this.runtimeFetch(requestEndpoint, request);
1588
1635
  logStreamCheckpoint("runtime_response_headers", {
1589
1636
  status: response.status,
1590
1637
  ok: response.ok
@@ -1594,27 +1641,20 @@ var SandboxInstance = class SandboxInstance {
1594
1641
  throwIfTimedOut();
1595
1642
  throw err;
1596
1643
  }
1597
- if (!response.ok) {
1644
+ if (!response.ok && !explicitReplay) {
1598
1645
  const body = await response.text();
1599
1646
  throw parseErrorResponse(response.status, body, void 0, response.headers);
1600
1647
  }
1601
- let lastEventId = options?.lastEventId;
1602
- let executionId = options?.executionId;
1603
- let sessionId = options?.sessionId;
1604
- let receivedTerminal = false;
1605
- const RECONNECT_BACKOFF_MS = [
1606
- 2e3,
1607
- 4e3,
1608
- 8e3,
1609
- 16e3,
1610
- 3e4,
1611
- 3e4
1612
- ];
1613
- const MAX_RECONNECT_ATTEMPTS = RECONNECT_BACKOFF_MS.length;
1614
- let lastReplayStatus;
1615
- let lastExecutionStatus;
1616
- let firstSseEventSeen = false;
1617
- for await (const event of this.parseSSEStream(response, streamSignal)) {
1648
+ if (!response.ok) {
1649
+ lastReplayStatus = response.status;
1650
+ await response.text();
1651
+ logStreamCheckpoint("replay_response_status", {
1652
+ attempt: 0,
1653
+ status: response.status,
1654
+ ok: false
1655
+ });
1656
+ } else for await (const event of this.parseSSEStream(response, streamSignal)) {
1657
+ if (event.type === "history.replay.start" || event.type === "history.replay.end") continue;
1618
1658
  if (!firstSseEventSeen) {
1619
1659
  firstSseEventSeen = true;
1620
1660
  logStreamCheckpoint("first_sse_event", {
@@ -2092,11 +2132,19 @@ var SandboxInstance = class SandboxInstance {
2092
2132
  async resourceUsage() {
2093
2133
  await this.ensureRunning();
2094
2134
  const response = await this.runtimeFetch("/health/detailed", { method: "GET" });
2095
- if (!response.ok) {
2096
- const body = await response.text();
2097
- throw parseErrorResponse(response.status, body, void 0, response.headers);
2135
+ const body = await response.text();
2136
+ let data;
2137
+ try {
2138
+ data = JSON.parse(body);
2139
+ } catch (error) {
2140
+ if (response.ok) throw new NetworkError("Detailed health returned a non-JSON response", error instanceof Error ? error : void 0, {
2141
+ endpoint: "/health/detailed",
2142
+ origin: "runtime"
2143
+ });
2098
2144
  }
2099
- return (await response.json()).resources ?? null;
2145
+ if (data?.resources) return data.resources;
2146
+ if (!response.ok) throw parseErrorResponse(response.status, body, void 0, response.headers);
2147
+ return null;
2100
2148
  }
2101
2149
  /**
2102
2150
  * Git capability object for repository operations.
@@ -2321,6 +2369,7 @@ var SandboxInstance = class SandboxInstance {
2321
2369
  list: (path, options) => this.fsList(path, options),
2322
2370
  stat: (path) => this.fsStat(path),
2323
2371
  delete: (path, options) => this.fsDelete(path, options),
2372
+ rename: (sourcePath, destinationPath, options) => this.fsRename(sourcePath, destinationPath, options),
2324
2373
  mkdir: (path, options) => this.fsMkdir(path, options),
2325
2374
  exists: (path) => this.fsExists(path)
2326
2375
  };
@@ -2366,7 +2415,7 @@ var SandboxInstance = class SandboxInstance {
2366
2415
  const fs = await import("node:fs/promises");
2367
2416
  const path = await import("node:path");
2368
2417
  await fs.mkdir(path.dirname(localPath), { recursive: true });
2369
- const response = await this.runtimeFetch(`/fs/download${remotePath}`, { method: "GET" });
2418
+ const response = await this.runtimeFetch(runtimeFilesystemPath("/fs/download", remotePath), { method: "GET" });
2370
2419
  if (!response.ok) {
2371
2420
  const body = await response.text();
2372
2421
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -2416,7 +2465,7 @@ var SandboxInstance = class SandboxInstance {
2416
2465
  const fs = await import("node:fs/promises");
2417
2466
  const path = await import("node:path");
2418
2467
  const { execSync } = await import("node:child_process");
2419
- const response = await this.runtimeFetch(`/fs/download-dir${remoteDir}`, { method: "GET" });
2468
+ const response = await this.runtimeFetch(runtimeFilesystemPath("/fs/download-dir", remoteDir), { method: "GET" });
2420
2469
  if (!response.ok) {
2421
2470
  const body = await response.text();
2422
2471
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -2439,7 +2488,8 @@ var SandboxInstance = class SandboxInstance {
2439
2488
  if (options?.long) params.set("long", "true");
2440
2489
  if (options?.sessionId) params.set("sessionId", options.sessionId);
2441
2490
  const query = params.toString();
2442
- const url = query ? `/fs/list${path}?${query}` : `/fs/list${path}`;
2491
+ const basePath = runtimeFilesystemPath("/fs/list", path);
2492
+ const url = query ? `${basePath}?${query}` : basePath;
2443
2493
  const response = await this.runtimeFetch(url, { method: "GET" });
2444
2494
  if (!response.ok) {
2445
2495
  const body = await response.text();
@@ -2461,7 +2511,7 @@ var SandboxInstance = class SandboxInstance {
2461
2511
  }
2462
2512
  async fsStat(path) {
2463
2513
  await this.ensureRunning();
2464
- const response = await this.runtimeFetch(`/fs/stat${path}`, { method: "GET" });
2514
+ const response = await this.runtimeFetch(runtimeFilesystemPath("/fs/stat", path), { method: "GET" });
2465
2515
  if (!response.ok) {
2466
2516
  const body = await response.text();
2467
2517
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -2499,19 +2549,24 @@ var SandboxInstance = class SandboxInstance {
2499
2549
  if (options?.recursive) params.set("recursive", "true");
2500
2550
  if (options?.sessionId) params.set("sessionId", options.sessionId);
2501
2551
  const query = params.toString();
2502
- const url = query ? `/fs${path}?${query}` : `/fs${path}`;
2552
+ const basePath = runtimeFilesystemPath("/fs", path);
2553
+ const url = query ? `${basePath}?${query}` : basePath;
2503
2554
  const response = await this.runtimeFetch(url, { method: "DELETE" });
2504
2555
  if (!response.ok) {
2505
2556
  const body = await response.text();
2506
2557
  throw parseErrorResponse(response.status, body, void 0, response.headers);
2507
2558
  }
2508
2559
  }
2560
+ async fsRename(sourcePath, destinationPath, options) {
2561
+ return this.runtime.renameFile(sourcePath, destinationPath, options);
2562
+ }
2509
2563
  async fsMkdir(path, options) {
2510
2564
  await this.ensureRunning();
2511
2565
  const params = new URLSearchParams();
2512
2566
  if (options?.recursive) params.set("recursive", "true");
2513
2567
  const query = params.toString();
2514
- const url = query ? `/fs/mkdir${path}?${query}` : `/fs/mkdir${path}`;
2568
+ const basePath = runtimeFilesystemPath("/fs/mkdir", path);
2569
+ const url = query ? `${basePath}?${query}` : basePath;
2515
2570
  const response = await this.runtimeFetch(url, { method: "POST" });
2516
2571
  if (!response.ok) {
2517
2572
  const body = await response.text();
@@ -2520,7 +2575,7 @@ var SandboxInstance = class SandboxInstance {
2520
2575
  }
2521
2576
  async fsExists(path) {
2522
2577
  await this.ensureRunning();
2523
- const response = await this.runtimeFetch(`/fs/exists${path}`, { method: "GET" });
2578
+ const response = await this.runtimeFetch(runtimeFilesystemPath("/fs/exists", path), { method: "GET" });
2524
2579
  if (!response.ok) {
2525
2580
  const body = await response.text();
2526
2581
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -3721,7 +3776,89 @@ var SandboxInstance = class SandboxInstance {
3721
3776
  return ((await response.json()).children ?? []).map((child) => new SandboxInstance(this.client, this.parseInfo(child), this.defaultRuntimeBackend));
3722
3777
  }
3723
3778
  /**
3779
+ * Speculative rollouts: branch this RUNNING sandbox into `n` copy-on-write
3780
+ * children, run the SAME agent turn in every child in parallel, and
3781
+ * optionally score each child by running `scorer.command` inside it (exit 0
3782
+ * passes; stdout is parsed as the numeric score — highest wins).
3783
+ *
3784
+ * Async-op shape: resolves as soon as the children exist (their turns are
3785
+ * still running); poll {@link getRollout} or use {@link waitForRollout} for
3786
+ * results. Children are first-class sandboxes the caller may promote or
3787
+ * delete — each occupies a concurrent-session quota slot like a create.
3788
+ *
3789
+ * @example
3790
+ * ```typescript
3791
+ * const started = await box.rollout({
3792
+ * n: 4,
3793
+ * parts: [{ type: "text", text: "make the failing test pass" }],
3794
+ * scorer: { type: "command", command: "npm test --silent && echo 1" },
3795
+ * });
3796
+ * const settled = await box.waitForRollout(started.rolloutId);
3797
+ * console.log(settled.winner, settled.results);
3798
+ * ```
3799
+ */
3800
+ async rollout(options) {
3801
+ if (!Number.isInteger(options.n) || options.n < 1 || options.n > 8) throw new ValidationError(`rollout n must be an integer in [1, 8] (got ${options.n})`);
3802
+ if (!Array.isArray(options.parts) || options.parts.length === 0) throw new ValidationError("rollout parts must be a non-empty array");
3803
+ if (options.scorer?.type === "command" && !options.scorer.command) throw new ValidationError("scorer.command is required when scorer.type is \"command\"");
3804
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/rollouts`, {
3805
+ method: "POST",
3806
+ body: JSON.stringify({
3807
+ n: options.n,
3808
+ parts: options.parts,
3809
+ ...options.scorer ? { scorer: options.scorer } : {}
3810
+ })
3811
+ });
3812
+ if (!response.ok) {
3813
+ const body = await response.text();
3814
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3815
+ }
3816
+ const data = await response.json();
3817
+ if (typeof data.rolloutId !== "string" || data.rolloutId.length === 0) throw new ServerError(`Rollout create returned ${response.status} but body had no rolloutId: ${JSON.stringify(data).slice(0, 200)}`, response.status);
3818
+ return data;
3819
+ }
3820
+ /**
3821
+ * Poll a rollout's status and per-child results.
3822
+ *
3823
+ * Failed or timed-out children appear as result entries with `error` set —
3824
+ * partial results are never dropped. Rollout poll-state lives in
3825
+ * orchestrator RAM: after an orchestrator restart this returns 404 while
3826
+ * the children themselves survive as first-class sandboxes.
3827
+ */
3828
+ async getRollout(rolloutId) {
3829
+ if (!rolloutId) throw new ValidationError("rolloutId is required");
3830
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/rollouts/${encodeURIComponent(rolloutId)}`, { method: "GET" });
3831
+ if (!response.ok) {
3832
+ const body = await response.text();
3833
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3834
+ }
3835
+ return await response.json();
3836
+ }
3837
+ /**
3838
+ * Poll {@link getRollout} until the rollout leaves `running`, then return
3839
+ * the terminal record — including `partial`/`failed` outcomes, whose
3840
+ * per-child `error` entries are results the caller inspects, not
3841
+ * exceptions. Throws {@link TimeoutError} on deadline or abort.
3842
+ */
3843
+ async waitForRollout(rolloutId, options) {
3844
+ const timeoutMs = options?.timeoutMs ?? 36e4;
3845
+ const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
3846
+ const startTime = Date.now();
3847
+ while (true) {
3848
+ if (options?.signal?.aborted) throw new TimeoutError(0, "Aborted");
3849
+ const record = await this.getRollout(rolloutId);
3850
+ if (record.status !== "running") return record;
3851
+ if (Date.now() - startTime > timeoutMs) throw new TimeoutError(timeoutMs, `Timed out waiting for rollout ${rolloutId} to settle`);
3852
+ await this.sleep(pollIntervalMs);
3853
+ }
3854
+ }
3855
+ /**
3724
3856
  * Stop the sandbox (keeps state for resume).
3857
+ *
3858
+ * The stop response carries the orchestrator's per-phase stop breakdown
3859
+ * (`stop_metering` / `container_kill` / `stop_complete` /
3860
+ * `stop_unaccounted`); after this resolves, {@link startupDiagnostics}
3861
+ * returns it (replacing the create breakdown).
3725
3862
  */
3726
3863
  async stop() {
3727
3864
  const response = await this.client.fetch(`/v1/sandboxes/${this.id}/stop`, { method: "POST" });
@@ -3729,7 +3866,9 @@ var SandboxInstance = class SandboxInstance {
3729
3866
  const body = await response.text();
3730
3867
  throw parseErrorResponse(response.status, body, void 0, response.headers);
3731
3868
  }
3869
+ const diagnostics = await this.readLifecycleDiagnostics(response);
3732
3870
  await this.refresh();
3871
+ if (diagnostics) this.info.startupDiagnostics = diagnostics;
3733
3872
  }
3734
3873
  /**
3735
3874
  * Resume a stopped sandbox.
@@ -3746,10 +3885,18 @@ var SandboxInstance = class SandboxInstance {
3746
3885
  const body = await response.text();
3747
3886
  throw parseErrorResponse(response.status, body, void 0, response.headers);
3748
3887
  }
3888
+ const diagnostics = await this.readLifecycleDiagnostics(response);
3749
3889
  await this.refresh();
3890
+ if (diagnostics) this.info.startupDiagnostics = diagnostics;
3750
3891
  }
3751
3892
  /**
3752
3893
  * Delete the sandbox permanently.
3894
+ *
3895
+ * The delete response carries the orchestrator's per-phase delete breakdown
3896
+ * (`container_remove` / `storage_binding_wait` / `cleanup_parallel` /
3897
+ * `delete_complete` / `delete_unaccounted`); after this resolves,
3898
+ * {@link startupDiagnostics} returns it for post-mortem reads on the
3899
+ * now-gone sandbox.
3753
3900
  */
3754
3901
  async delete() {
3755
3902
  const response = await this.client.fetch(`/v1/sandboxes/${this.id}`, { method: "DELETE" });
@@ -3757,6 +3904,17 @@ var SandboxInstance = class SandboxInstance {
3757
3904
  const body = await response.text();
3758
3905
  throw parseErrorResponse(response.status, body, void 0, response.headers);
3759
3906
  }
3907
+ const diagnostics = await this.readLifecycleDiagnostics(response);
3908
+ if (diagnostics) this.info.startupDiagnostics = diagnostics;
3909
+ }
3910
+ /**
3911
+ * Read the per-phase lifecycle breakdown off a stop/resume/delete response
3912
+ * body. Never throws: diagnostics are measurement data — a body that fails
3913
+ * to parse (older API, empty body) must not fail the lifecycle call.
3914
+ */
3915
+ async readLifecycleDiagnostics(response) {
3916
+ const data = await response.json().catch(() => null);
3917
+ return data ? normalizeStartupDiagnostics(data.startupDiagnostics) : null;
3760
3918
  }
3761
3919
  /**
3762
3920
  * keepAlive is intentionally unavailable until the API exposes timeout updates.
@@ -4330,15 +4488,10 @@ var SandboxInstance = class SandboxInstance {
4330
4488
  return { state: "running" };
4331
4489
  }
4332
4490
  /**
4333
- * Mint a scoped, time-bounded JWT for direct browser access to this
4334
- * sandbox (Issue #913 Gap 1). Authority is the caller's
4335
- * `TANGLE_API_KEY` (sk-tan-*) the Sandbox API mints the token;
4336
- * signing secrets stay server-side.
4337
- *
4338
- * Use this to give a browser direct read access to the sandbox without
4339
- * leaking the full bearer (`box.connection.authToken`). The returned
4340
- * token verifies against the same sidecar middleware that already
4341
- * gates ProductTokenIssuer-issued JWTs — no new sidecar surface.
4491
+ * Mint a scoped, time-bounded JWT for browser access (Issue #913 Gap 1).
4492
+ * Authority is the caller's `TANGLE_API_KEY` (sk-tan-*); signing secrets
4493
+ * stay server-side. Session/project scopes work with SessionGatewayClient,
4494
+ * while session-runtime/read-only scopes work with SandboxRuntimeClient.
4342
4495
  */
4343
4496
  async mintScopedToken(opts) {
4344
4497
  if ((opts.scope === "session" || opts.scope === "session-runtime") && !opts.sessionId) throw new ValidationError(`${opts.scope} scope requires a sessionId`);
@@ -4382,7 +4535,7 @@ var SandboxInstance = class SandboxInstance {
4382
4535
  const search = new URLSearchParams();
4383
4536
  search.set("sessionId", id);
4384
4537
  if (opts?.since) search.set("since", opts.since);
4385
- const response = await this.runtimeFetch(`/agents/events/?${search.toString()}`, { signal: opts?.signal });
4538
+ const response = await this.runtimeFetch(`/agents/events?${search.toString()}`, { signal: opts?.signal });
4386
4539
  if (!response.ok) {
4387
4540
  const body = await response.text();
4388
4541
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -1,2 +1,2 @@
1
- import { a as TANGLE_JOBS_CONTRACT, c as AgentSandboxBlueprintAbi, d as SandboxCreateParamTypes, f as SandboxCreateResponseParamTypes, i as TANGLE_CHAIN_ID, l as ITangleJobsAbi, n as JOB_SANDBOX_CREATE, o as TANGLE_MAINNET_RPC, p as SandboxIdParamTypes, r as JOB_SANDBOX_DELETE, s as TangleSandboxClientConfig, t as TangleSandboxClient, u as JsonResponseParamTypes } from "../index-DzEUjav-.js";
1
+ import { a as TANGLE_JOBS_CONTRACT, c as AgentSandboxBlueprintAbi, d as SandboxCreateParamTypes, f as SandboxCreateResponseParamTypes, i as TANGLE_CHAIN_ID, l as ITangleJobsAbi, n as JOB_SANDBOX_CREATE, o as TANGLE_MAINNET_RPC, p as SandboxIdParamTypes, r as JOB_SANDBOX_DELETE, s as TangleSandboxClientConfig, t as TangleSandboxClient, u as JsonResponseParamTypes } from "../index-DFJiN98I.js";
2
2
  export { AgentSandboxBlueprintAbi, ITangleJobsAbi, JOB_SANDBOX_CREATE, JOB_SANDBOX_DELETE, JsonResponseParamTypes, SandboxCreateParamTypes, SandboxCreateResponseParamTypes, SandboxIdParamTypes, TANGLE_CHAIN_ID, TANGLE_JOBS_CONTRACT, TANGLE_MAINNET_RPC, TangleSandboxClient, TangleSandboxClientConfig };
@@ -1,2 +1,2 @@
1
- import { a as TANGLE_JOBS_CONTRACT, c as ITangleJobsAbi, d as SandboxCreateResponseParamTypes, f as SandboxIdParamTypes, i as TANGLE_CHAIN_ID, l as JsonResponseParamTypes, n as JOB_SANDBOX_CREATE, o as TANGLE_MAINNET_RPC, r as JOB_SANDBOX_DELETE, s as AgentSandboxBlueprintAbi, t as TangleSandboxClient, u as SandboxCreateParamTypes } from "../tangle-BdRRHMpX.js";
1
+ import { a as TANGLE_JOBS_CONTRACT, c as ITangleJobsAbi, d as SandboxCreateResponseParamTypes, f as SandboxIdParamTypes, i as TANGLE_CHAIN_ID, l as JsonResponseParamTypes, n as JOB_SANDBOX_CREATE, o as TANGLE_MAINNET_RPC, r as JOB_SANDBOX_DELETE, s as AgentSandboxBlueprintAbi, t as TangleSandboxClient, u as SandboxCreateParamTypes } from "../tangle-C-5OvIj8.js";
2
2
  export { AgentSandboxBlueprintAbi, ITangleJobsAbi, JOB_SANDBOX_CREATE, JOB_SANDBOX_DELETE, JsonResponseParamTypes, SandboxCreateParamTypes, SandboxCreateResponseParamTypes, SandboxIdParamTypes, TANGLE_CHAIN_ID, TANGLE_JOBS_CONTRACT, TANGLE_MAINNET_RPC, TangleSandboxClient };
@@ -1,5 +1,5 @@
1
- import { p as parseErrorResponse } from "./errors-DSz87Rkk.js";
2
- import { t as SandboxInstance } from "./sandbox-CxBHIBV1.js";
1
+ import { p as parseErrorResponse } from "./errors-wd266B9Q.js";
2
+ import { t as SandboxInstance } from "./sandbox-CIHssZCH.js";
3
3
  //#region src/tangle/abi.ts
4
4
  /**
5
5
  * Tangle Contract ABI Definitions