@tangle-network/sandbox 0.10.2 → 0.10.4

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,159 +1,6 @@
1
+ import { n as normalizeSessionInfo, r as normalizeRuntimeBackendConfig, t as SandboxRuntimeApi } from "./runtime-api-CYA2DUQw.js";
1
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";
2
3
  import { addTokenUsage, readTokenCostUsd, readTokenUsage } from "@tangle-network/agent-core";
3
- //#region src/backend-config.ts
4
- const LEGACY_QUESTION_ALIAS_KEY = ["question", "Channel"].join("");
5
- function parseModelString(model) {
6
- const parts = model.split("/");
7
- if (parts.length >= 2) return {
8
- provider: parts[0],
9
- model: parts.slice(1).join("/")
10
- };
11
- return { model };
12
- }
13
- /**
14
- * Normalize runtime backend config for the wire format.
15
- *
16
- * Callers set `backend.profile` (named string or portable AgentProfile).
17
- * This function generates the native `inlineProfile` shim automatically.
18
- */
19
- function normalizeRuntimeBackendConfig(backend, options = {}) {
20
- if (!backend && !options.model) return void 0;
21
- if (backend && LEGACY_QUESTION_ALIAS_KEY in backend) throw new Error(`[sandbox-sdk] backend.${LEGACY_QUESTION_ALIAS_KEY} was removed. Use backend.interactions.question instead.`);
22
- const portableProfile = backend?.profile && typeof backend.profile !== "string" ? backend.profile : void 0;
23
- const inlineProfile = portableProfile ? toBackendProfile(portableProfile) : void 0;
24
- const callerInlineProfile = backend?.inlineProfile;
25
- if (callerInlineProfile && !inlineProfile) console.warn("[sandbox-sdk] backend.inlineProfile is deprecated. Use backend.profile (AgentPortableProfile) instead.");
26
- if (inlineProfile && portableProfile) {
27
- const reasoningEffort = portableProfile.model?.reasoningEffort ?? portableProfile.extensions?.[backend?.type ?? ""]?.reasoningEffort;
28
- if (typeof reasoningEffort === "string" && reasoningEffort !== "auto" && reasoningEffort !== "none") inlineProfile.reasoningEffort = reasoningEffort;
29
- }
30
- return {
31
- ...backend?.type ? { type: backend.type } : {},
32
- ...typeof backend?.profile === "string" ? { profile: backend.profile } : {},
33
- ...inlineProfile ? { inlineProfile } : callerInlineProfile ? { inlineProfile: callerInlineProfile } : {},
34
- ...backend?.model ? { model: backend.model } : options.model ? { model: parseModelString(options.model) } : {},
35
- ...backend?.server ? { server: backend.server } : {},
36
- ...backend?.interactions !== void 0 ? { interactions: backend.interactions } : {},
37
- ...backend?.metadata !== void 0 ? { metadata: backend.metadata } : {}
38
- };
39
- }
40
- /**
41
- * Permission keys the sidecar's strict `permission` schema accepts
42
- * (apps/sidecar/src/schemas/agent-profile-schema.ts → permissionConfigSchema).
43
- * Any other portable-permission key is a per-tool gate, not a permission, and
44
- * is routed into `tools` instead of passed through — the sidecar's `.strict()`
45
- * schema rejects unknown permission keys with `unrecognized_keys`.
46
- *
47
- * Name must not carry an UPPERCASE `SIDECAR_`/`ORCHESTRATOR_`-style prefix:
48
- * obfuscate.sh runs `--rename-globals false`, so a module-global identifier
49
- * ships verbatim in dist/, and verify-dist.sh bans those prefixes as
50
- * closed-source leaks.
51
- */
52
- const CANONICAL_PERMISSION_KEYS = new Set([
53
- "edit",
54
- "bash",
55
- "webfetch",
56
- "mcp",
57
- "doom_loop",
58
- "external_directory"
59
- ]);
60
- function hasAnyProfileResource(resources) {
61
- return !!(resources.files?.length || resources.tools?.length || resources.skills?.length || resources.agents?.length || resources.commands?.length || resources.instructions);
62
- }
63
- function validateProfileResources(profile) {
64
- if (profile.resources !== void 0 && (profile.resources === null || typeof profile.resources !== "object" || Array.isArray(profile.resources))) throw new Error("backend.profile.resources must be an object when provided");
65
- if (profile.resources?.plugins?.length) throw new Error("backend.profile.resources.plugins is not supported by the sandbox SDK profile contract; use backend-specific provider profiles for OpenCode plugin resources");
66
- }
67
- function toBackendProfile(profile) {
68
- validateProfileResources(profile);
69
- const permission = {};
70
- const derivedTools = {};
71
- for (const [rawKey, value] of Object.entries(profile.permissions ?? {})) {
72
- const key = rawKey === "network" ? "webfetch" : rawKey;
73
- if (CANONICAL_PERMISSION_KEYS.has(key)) permission[key] = value;
74
- else if (typeof value === "string") derivedTools[key] = value !== "deny";
75
- else console.warn(`[sandbox-sdk] dropping unsupported permission key "${rawKey}": not a canonical sidecar permission and not a string tool gate`);
76
- }
77
- const tools = {
78
- ...derivedTools,
79
- ...profile.tools ?? {}
80
- };
81
- const mcp = profile.mcp ? Object.fromEntries(Object.entries(profile.mcp).map(([name, config]) => {
82
- const c = config;
83
- const transport = c.transport;
84
- const command = c.command;
85
- const hasCommand = Array.isArray(command) ? command.length > 0 : typeof command === "string" && command.trim().length > 0;
86
- const isRemote = transport === "http" || transport === "sse" || typeof c.url === "string" && !hasCommand;
87
- const enabled = c.enabled;
88
- const timeout = c.timeout;
89
- if (isRemote) return [name, {
90
- type: "remote",
91
- url: c.url,
92
- ...c.headers ? { headers: c.headers } : {},
93
- ...enabled !== void 0 ? { enabled } : {},
94
- ...timeout !== void 0 ? { timeout } : {}
95
- }];
96
- return [name, {
97
- type: "local",
98
- command: [...Array.isArray(command) ? command : typeof command === "string" ? command.trim().split(/\s+/).filter(Boolean) : [], ...c.args ?? []],
99
- ...c.env ? { environment: c.env } : {},
100
- ...enabled !== void 0 ? { enabled } : {},
101
- ...timeout !== void 0 ? { timeout } : {}
102
- }];
103
- })) : void 0;
104
- const openCodeExtension = profile.extensions?.opencode;
105
- const plugins = openCodeExtension?.plugins;
106
- if (plugins !== void 0 && (!Array.isArray(plugins) || plugins.some((plugin) => typeof plugin !== "string" || !plugin.trim()))) throw new Error("backend.profile.extensions.opencode.plugins must be an array of non-empty package names");
107
- const commands = openCodeExtension?.commands;
108
- if (commands !== void 0 && (commands === null || typeof commands !== "object" || Array.isArray(commands))) throw new Error("backend.profile.extensions.opencode.commands must be an object");
109
- const profileExtends = openCodeExtension?.extends;
110
- if (profileExtends !== void 0 && typeof profileExtends !== "string" && (!Array.isArray(profileExtends) || profileExtends.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error("backend.profile.extensions.opencode.extends must be a profile name or array of profile names");
111
- const agents = profile.subagents ? Object.fromEntries(Object.entries(profile.subagents).map(([name, subagent]) => [name, {
112
- description: subagent.description,
113
- prompt: subagent.prompt,
114
- model: subagent.model,
115
- tools: subagent.tools,
116
- permission: subagent.permissions,
117
- maxSteps: subagent.maxSteps,
118
- ...subagent.metadata ?? {}
119
- }])) : void 0;
120
- return {
121
- name: profile.name,
122
- description: profile.description,
123
- version: profile.version,
124
- tags: profile.tags,
125
- ...profileExtends ? { extends: profileExtends } : {},
126
- model: profile.model?.default,
127
- small_model: profile.model?.small,
128
- ...profile.prompt?.systemPrompt ? { systemPrompt: profile.prompt.systemPrompt } : {},
129
- ...Object.keys(tools).length > 0 ? { tools } : {},
130
- ...profile.prompt?.instructions?.length ? { instructions: profile.prompt.instructions } : {},
131
- ...Object.keys(permission).length > 0 ? { permission } : {},
132
- ...mcp ? { mcp } : {},
133
- ...profile.connections?.length ? { connections: profile.connections } : {},
134
- ...agents ? { agent: agents } : {},
135
- ...profile.resources && hasAnyProfileResource(profile.resources) ? { resources: profile.resources } : {},
136
- ...profile.hooks ? { hooks: profile.hooks } : {},
137
- ...profile.modes ? { modes: profile.modes } : {},
138
- ...commands ? { command: commands } : {},
139
- ...plugins?.length ? { plugin: plugins } : {},
140
- ...profile.extensions ? { extensions: profile.extensions } : {}
141
- };
142
- }
143
- /**
144
- * Serialize a portable AgentProfile into the sidecar's canonical inline-profile
145
- * wire shape (validated by inlineProfileSchema in
146
- * apps/sidecar/src/schemas/agent-profile-schema.ts).
147
- *
148
- * Single choke point every product shares: streamPrompt →
149
- * normalizeRuntimeBackendConfig → toBackendProfile. Exported so callers and the
150
- * drift-guard test target one contract — the test parses this output with the
151
- * sidecar's own schema, so any future schema tightening fails here first.
152
- */
153
- function serializeForSidecar(profile) {
154
- return toBackendProfile(profile);
155
- }
156
- //#endregion
157
4
  //#region src/lib/sse-parser.ts
158
5
  /**
159
6
  * Parse a streaming SSE response body into an async iterable of
@@ -599,244 +446,6 @@ function collectAgentResponseText(events) {
599
446
  return responseText && responseText.trim().length > 0 ? responseText : void 0;
600
447
  }
601
448
  //#endregion
602
- //#region src/runtime-api.ts
603
- /** Internal typed client for routes served by a sandbox runtime. */
604
- var SandboxRuntimeApi = class {
605
- terminals;
606
- constructor(transport) {
607
- this.transport = transport;
608
- this.terminals = {
609
- create: (options, request) => this.createTerminal(options, request),
610
- input: (terminalId, data, request) => this.writeTerminal(terminalId, data, request),
611
- list: () => this.listTerminals(),
612
- get: (terminalId) => this.getTerminal(terminalId)
613
- };
614
- }
615
- async health() {
616
- await this.transport.ensureRunning();
617
- return this.json("/health", { method: "GET" });
618
- }
619
- async ports() {
620
- await this.transport.ensureRunning();
621
- return (await this.json("/ports", { method: "GET" })).data?.ports ?? [];
622
- }
623
- async profiles() {
624
- await this.transport.ensureRunning();
625
- return this.json("/profiles", { method: "GET" });
626
- }
627
- async readFiles(paths, options = {}) {
628
- if (paths.length === 0) return {
629
- files: [],
630
- errors: [],
631
- stats: {
632
- requested: 0,
633
- succeeded: 0,
634
- failed: 0
635
- }
636
- };
637
- await this.transport.ensureRunning();
638
- const files = [];
639
- const errors = [];
640
- let requested = 0;
641
- let succeeded = 0;
642
- let failed = 0;
643
- for (let offset = 0; offset < paths.length; offset += 100) {
644
- const chunk = paths.slice(offset, offset + 100);
645
- const params = new URLSearchParams();
646
- if (options.sessionId) params.set("sessionId", options.sessionId);
647
- const query = params.toString();
648
- const payload = await this.json(`/files/read-batch${query ? `?${query}` : ""}`, {
649
- method: "POST",
650
- body: JSON.stringify({
651
- paths: chunk,
652
- ...options.encoding ? { encoding: options.encoding } : {}
653
- })
654
- });
655
- for (const [path, result] of Object.entries(payload.data.files)) if (result.success) files.push({
656
- path,
657
- content: result.content,
658
- encoding: result.encoding,
659
- hash: result.hash,
660
- size: result.size,
661
- mtime: result.mtime
662
- });
663
- else errors.push({
664
- path,
665
- error: result.error,
666
- code: result.code
667
- });
668
- const results = Object.values(payload.data.files);
669
- requested += payload.data.stats?.requested ?? chunk.length;
670
- succeeded += payload.data.stats?.succeeded ?? results.filter((entry) => entry.success).length;
671
- failed += payload.data.stats?.failed ?? results.filter((entry) => !entry.success).length;
672
- }
673
- return {
674
- files,
675
- errors,
676
- stats: {
677
- requested,
678
- succeeded,
679
- failed
680
- }
681
- };
682
- }
683
- async fileTree(path, options = {}) {
684
- if (options.maxDepth !== void 0 && (!Number.isInteger(options.maxDepth) || options.maxDepth < 1 || options.maxDepth > 20)) throw new ValidationError("maxDepth must be an integer between 1 and 20");
685
- await this.transport.ensureRunning();
686
- const params = new URLSearchParams({ path });
687
- if (options.maxDepth !== void 0) params.set("maxDepth", String(options.maxDepth));
688
- if (options.sessionId) params.set("sessionId", options.sessionId);
689
- return (await this.json(`/files/tree?${params}`, { method: "GET" })).data;
690
- }
691
- async createSession(options) {
692
- await this.transport.ensureRunning();
693
- const info = normalizeSessionInfo(await this.json("/agents/sessions", {
694
- method: "POST",
695
- body: JSON.stringify({
696
- ...options.title !== void 0 ? { title: options.title } : {},
697
- ...options.parentId !== void 0 ? { parentID: options.parentId } : {},
698
- backend: normalizeRuntimeBackendConfig(options.backend)
699
- })
700
- }));
701
- if (!info) throw new ServerError("Session creation response missing session id", 502, {
702
- endpoint: "/agents/sessions",
703
- origin: "runtime"
704
- });
705
- return info;
706
- }
707
- async createTaskSession(options) {
708
- await this.transport.ensureRunning();
709
- const info = await this.json("/tasks", {
710
- method: "POST",
711
- body: JSON.stringify({
712
- sessionId: options.sessionId,
713
- backend: normalizeRuntimeBackendConfig(options.backend),
714
- title: options.title,
715
- ...options.parentSessionId !== void 0 ? { parentSessionId: options.parentSessionId } : {},
716
- ...options.profile !== void 0 ? { profile: options.profile } : {},
717
- ...options.isolateFileWrites !== void 0 ? { isolateFileWrites: options.isolateFileWrites } : {}
718
- })
719
- });
720
- if (!info.id) throw new ServerError("Task session creation response missing session id", 502, {
721
- endpoint: "/tasks",
722
- origin: "runtime"
723
- });
724
- return info;
725
- }
726
- async sendSessionMessage(id, request, options = {}) {
727
- if (options.timeoutMs !== void 0 && options.timeoutMs <= 0) throw new ValidationError("timeoutMs must be greater than zero");
728
- await this.transport.ensureRunning();
729
- const headers = new Headers({ "x-no-retry": "true" });
730
- if (options.credentials?.apiKey) headers.set("X-Backend-Api-Key", options.credentials.apiKey);
731
- if (options.credentials?.baseUrl) headers.set("X-Backend-Base-Url", options.credentials.baseUrl);
732
- const timeoutSignal = options.timeoutMs !== void 0 ? AbortSignal.timeout(options.timeoutMs) : void 0;
733
- const signal = options.signal && timeoutSignal ? AbortSignal.any([options.signal, timeoutSignal]) : options.signal ?? timeoutSignal;
734
- return this.json(`/agents/sessions/${encodeURIComponent(id)}/messages`, {
735
- method: "POST",
736
- headers,
737
- signal,
738
- body: JSON.stringify({
739
- ...request.messageId !== void 0 ? { messageID: request.messageId } : {},
740
- parts: request.parts,
741
- ...request.system !== void 0 ? { system: request.system } : {},
742
- ...request.agent !== void 0 ? { agent: request.agent } : {},
743
- ...request.model !== void 0 ? { model: {
744
- providerID: request.model.providerId,
745
- modelID: request.model.modelId
746
- } } : {},
747
- ...request.reasoningEffort !== void 0 ? { reasoningEffort: request.reasoningEffort } : {},
748
- ...request.turnId !== void 0 ? { turnId: request.turnId } : {},
749
- ...request.interactions !== void 0 ? { interactions: request.interactions } : {}
750
- })
751
- });
752
- }
753
- async deleteSession(id) {
754
- await this.transport.ensureRunning();
755
- await this.request(`/agents/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
756
- }
757
- async taskSessionChanges(id) {
758
- await this.transport.ensureRunning();
759
- return this.json(`/tasks/${encodeURIComponent(id)}/changes`, { method: "GET" });
760
- }
761
- async commitTaskSession(id, options = {}) {
762
- await this.transport.ensureRunning();
763
- return this.json(`/tasks/${encodeURIComponent(id)}/changes`, {
764
- method: "POST",
765
- body: JSON.stringify({
766
- ...options.message !== void 0 ? { message: options.message } : {},
767
- ...options.files !== void 0 ? { files: options.files } : {}
768
- })
769
- });
770
- }
771
- async createTerminal(options = {}, request = {}) {
772
- await this.transport.ensureRunning();
773
- const params = new URLSearchParams();
774
- if (request.sessionId) params.set("sessionId", request.sessionId);
775
- const query = params.toString();
776
- return (await this.json(`/terminals${query ? `?${query}` : ""}`, {
777
- method: "POST",
778
- body: JSON.stringify(options)
779
- })).data;
780
- }
781
- async writeTerminal(terminalId, data, request = {}) {
782
- await this.transport.ensureRunning();
783
- const params = new URLSearchParams();
784
- if (request.sessionId) params.set("sessionId", request.sessionId);
785
- const query = params.toString();
786
- await this.request(`/terminals/${encodeURIComponent(terminalId)}/input${query ? `?${query}` : ""}`, {
787
- method: "POST",
788
- body: JSON.stringify({ data })
789
- });
790
- }
791
- async listTerminals() {
792
- await this.transport.ensureRunning();
793
- return (await this.json("/terminals", { method: "GET" })).data ?? [];
794
- }
795
- async getTerminal(terminalId) {
796
- await this.transport.ensureRunning();
797
- const response = await this.transport.fetch(`/terminals/${encodeURIComponent(terminalId)}`, { method: "GET" });
798
- if (response.status === 404) return null;
799
- await this.assertOk(response);
800
- return (await response.json()).data;
801
- }
802
- async json(path, init) {
803
- return await (await this.request(path, init)).json();
804
- }
805
- async request(path, init) {
806
- const response = await this.transport.fetch(path, init);
807
- await this.assertOk(response);
808
- return response;
809
- }
810
- async assertOk(response) {
811
- if (response.ok) return;
812
- const body = await response.text();
813
- throw parseErrorResponse(response.status, body, void 0, response.headers);
814
- }
815
- };
816
- function normalizeSessionInfo(raw) {
817
- const id = raw.id;
818
- if (typeof id !== "string") return null;
819
- const status = raw.status || "running";
820
- return {
821
- id,
822
- parentId: typeof raw.parentID === "string" ? raw.parentID : typeof raw.parentId === "string" ? raw.parentId : void 0,
823
- status: [
824
- "queued",
825
- "running",
826
- "completed",
827
- "failed",
828
- "cancelled"
829
- ].includes(status) ? status : "running",
830
- backend: typeof raw.backendType === "string" ? raw.backendType : typeof raw.backend === "string" ? raw.backend : void 0,
831
- model: typeof raw.model === "string" ? raw.model : void 0,
832
- promptCount: typeof raw.promptCount === "number" ? raw.promptCount : void 0,
833
- createdAt: raw.createdAt ? new Date(raw.createdAt) : void 0,
834
- startedAt: raw.startedAt ? new Date(raw.startedAt) : void 0,
835
- endedAt: raw.endedAt ? new Date(raw.endedAt) : void 0,
836
- raw
837
- };
838
- }
839
- //#endregion
840
449
  //#region src/interactive.ts
841
450
  /**
842
451
  * Handle for one session's interactive harness. Obtained via
@@ -1143,6 +752,13 @@ function isTransientSnapshotVisibilityError(error) {
1143
752
  return message.includes("SNAPSHOT_NOT_FOUND") || message.includes("Failed to list snapshot") || message.includes("Storage agent error (404)");
1144
753
  }
1145
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
+ /**
1146
762
  * Single source of truth for a run's outcome, shared by
1147
763
  * `prompt`/`task`/`_sessionResult`/turn settling. A run succeeds only when it
1148
764
  * reached a terminal event, raised no run-level error, hit no approval gate,
@@ -2757,7 +2373,7 @@ var SandboxInstance = class SandboxInstance {
2757
2373
  const fs = await import("node:fs/promises");
2758
2374
  const path = await import("node:path");
2759
2375
  await fs.mkdir(path.dirname(localPath), { recursive: true });
2760
- const response = await this.runtimeFetch(`/fs/download${remotePath}`, { method: "GET" });
2376
+ const response = await this.runtimeFetch(runtimeFilesystemPath("/fs/download", remotePath), { method: "GET" });
2761
2377
  if (!response.ok) {
2762
2378
  const body = await response.text();
2763
2379
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -2807,7 +2423,7 @@ var SandboxInstance = class SandboxInstance {
2807
2423
  const fs = await import("node:fs/promises");
2808
2424
  const path = await import("node:path");
2809
2425
  const { execSync } = await import("node:child_process");
2810
- const response = await this.runtimeFetch(`/fs/download-dir${remoteDir}`, { method: "GET" });
2426
+ const response = await this.runtimeFetch(runtimeFilesystemPath("/fs/download-dir", remoteDir), { method: "GET" });
2811
2427
  if (!response.ok) {
2812
2428
  const body = await response.text();
2813
2429
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -2830,7 +2446,8 @@ var SandboxInstance = class SandboxInstance {
2830
2446
  if (options?.long) params.set("long", "true");
2831
2447
  if (options?.sessionId) params.set("sessionId", options.sessionId);
2832
2448
  const query = params.toString();
2833
- const url = query ? `/fs/list${path}?${query}` : `/fs/list${path}`;
2449
+ const basePath = runtimeFilesystemPath("/fs/list", path);
2450
+ const url = query ? `${basePath}?${query}` : basePath;
2834
2451
  const response = await this.runtimeFetch(url, { method: "GET" });
2835
2452
  if (!response.ok) {
2836
2453
  const body = await response.text();
@@ -2852,7 +2469,7 @@ var SandboxInstance = class SandboxInstance {
2852
2469
  }
2853
2470
  async fsStat(path) {
2854
2471
  await this.ensureRunning();
2855
- const response = await this.runtimeFetch(`/fs/stat${path}`, { method: "GET" });
2472
+ const response = await this.runtimeFetch(runtimeFilesystemPath("/fs/stat", path), { method: "GET" });
2856
2473
  if (!response.ok) {
2857
2474
  const body = await response.text();
2858
2475
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -2890,7 +2507,8 @@ var SandboxInstance = class SandboxInstance {
2890
2507
  if (options?.recursive) params.set("recursive", "true");
2891
2508
  if (options?.sessionId) params.set("sessionId", options.sessionId);
2892
2509
  const query = params.toString();
2893
- const url = query ? `/fs${path}?${query}` : `/fs${path}`;
2510
+ const basePath = runtimeFilesystemPath("/fs", path);
2511
+ const url = query ? `${basePath}?${query}` : basePath;
2894
2512
  const response = await this.runtimeFetch(url, { method: "DELETE" });
2895
2513
  if (!response.ok) {
2896
2514
  const body = await response.text();
@@ -2902,7 +2520,8 @@ var SandboxInstance = class SandboxInstance {
2902
2520
  const params = new URLSearchParams();
2903
2521
  if (options?.recursive) params.set("recursive", "true");
2904
2522
  const query = params.toString();
2905
- const url = query ? `/fs/mkdir${path}?${query}` : `/fs/mkdir${path}`;
2523
+ const basePath = runtimeFilesystemPath("/fs/mkdir", path);
2524
+ const url = query ? `${basePath}?${query}` : basePath;
2906
2525
  const response = await this.runtimeFetch(url, { method: "POST" });
2907
2526
  if (!response.ok) {
2908
2527
  const body = await response.text();
@@ -2911,7 +2530,7 @@ var SandboxInstance = class SandboxInstance {
2911
2530
  }
2912
2531
  async fsExists(path) {
2913
2532
  await this.ensureRunning();
2914
- const response = await this.runtimeFetch(`/fs/exists${path}`, { method: "GET" });
2533
+ const response = await this.runtimeFetch(runtimeFilesystemPath("/fs/exists", path), { method: "GET" });
2915
2534
  if (!response.ok) {
2916
2535
  const body = await response.text();
2917
2536
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -4112,7 +3731,89 @@ var SandboxInstance = class SandboxInstance {
4112
3731
  return ((await response.json()).children ?? []).map((child) => new SandboxInstance(this.client, this.parseInfo(child), this.defaultRuntimeBackend));
4113
3732
  }
4114
3733
  /**
3734
+ * Speculative rollouts: branch this RUNNING sandbox into `n` copy-on-write
3735
+ * children, run the SAME agent turn in every child in parallel, and
3736
+ * optionally score each child by running `scorer.command` inside it (exit 0
3737
+ * passes; stdout is parsed as the numeric score — highest wins).
3738
+ *
3739
+ * Async-op shape: resolves as soon as the children exist (their turns are
3740
+ * still running); poll {@link getRollout} or use {@link waitForRollout} for
3741
+ * results. Children are first-class sandboxes the caller may promote or
3742
+ * delete — each occupies a concurrent-session quota slot like a create.
3743
+ *
3744
+ * @example
3745
+ * ```typescript
3746
+ * const started = await box.rollout({
3747
+ * n: 4,
3748
+ * parts: [{ type: "text", text: "make the failing test pass" }],
3749
+ * scorer: { type: "command", command: "npm test --silent && echo 1" },
3750
+ * });
3751
+ * const settled = await box.waitForRollout(started.rolloutId);
3752
+ * console.log(settled.winner, settled.results);
3753
+ * ```
3754
+ */
3755
+ async rollout(options) {
3756
+ 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})`);
3757
+ if (!Array.isArray(options.parts) || options.parts.length === 0) throw new ValidationError("rollout parts must be a non-empty array");
3758
+ if (options.scorer?.type === "command" && !options.scorer.command) throw new ValidationError("scorer.command is required when scorer.type is \"command\"");
3759
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/rollouts`, {
3760
+ method: "POST",
3761
+ body: JSON.stringify({
3762
+ n: options.n,
3763
+ parts: options.parts,
3764
+ ...options.scorer ? { scorer: options.scorer } : {}
3765
+ })
3766
+ });
3767
+ if (!response.ok) {
3768
+ const body = await response.text();
3769
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3770
+ }
3771
+ const data = await response.json();
3772
+ 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);
3773
+ return data;
3774
+ }
3775
+ /**
3776
+ * Poll a rollout's status and per-child results.
3777
+ *
3778
+ * Failed or timed-out children appear as result entries with `error` set —
3779
+ * partial results are never dropped. Rollout poll-state lives in
3780
+ * orchestrator RAM: after an orchestrator restart this returns 404 while
3781
+ * the children themselves survive as first-class sandboxes.
3782
+ */
3783
+ async getRollout(rolloutId) {
3784
+ if (!rolloutId) throw new ValidationError("rolloutId is required");
3785
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/rollouts/${encodeURIComponent(rolloutId)}`, { method: "GET" });
3786
+ if (!response.ok) {
3787
+ const body = await response.text();
3788
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3789
+ }
3790
+ return await response.json();
3791
+ }
3792
+ /**
3793
+ * Poll {@link getRollout} until the rollout leaves `running`, then return
3794
+ * the terminal record — including `partial`/`failed` outcomes, whose
3795
+ * per-child `error` entries are results the caller inspects, not
3796
+ * exceptions. Throws {@link TimeoutError} on deadline or abort.
3797
+ */
3798
+ async waitForRollout(rolloutId, options) {
3799
+ const timeoutMs = options?.timeoutMs ?? 36e4;
3800
+ const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
3801
+ const startTime = Date.now();
3802
+ while (true) {
3803
+ if (options?.signal?.aborted) throw new TimeoutError(0, "Aborted");
3804
+ const record = await this.getRollout(rolloutId);
3805
+ if (record.status !== "running") return record;
3806
+ if (Date.now() - startTime > timeoutMs) throw new TimeoutError(timeoutMs, `Timed out waiting for rollout ${rolloutId} to settle`);
3807
+ await this.sleep(pollIntervalMs);
3808
+ }
3809
+ }
3810
+ /**
4115
3811
  * Stop the sandbox (keeps state for resume).
3812
+ *
3813
+ * The stop response carries the orchestrator's per-phase stop breakdown
3814
+ * (`stop_metering` / `container_kill` / `stop_complete` /
3815
+ * `stop_unaccounted`); after this resolves, {@link startupDiagnostics}
3816
+ * returns it (replacing the create breakdown).
4116
3817
  */
4117
3818
  async stop() {
4118
3819
  const response = await this.client.fetch(`/v1/sandboxes/${this.id}/stop`, { method: "POST" });
@@ -4120,7 +3821,9 @@ var SandboxInstance = class SandboxInstance {
4120
3821
  const body = await response.text();
4121
3822
  throw parseErrorResponse(response.status, body, void 0, response.headers);
4122
3823
  }
3824
+ const diagnostics = await this.readLifecycleDiagnostics(response);
4123
3825
  await this.refresh();
3826
+ if (diagnostics) this.info.startupDiagnostics = diagnostics;
4124
3827
  }
4125
3828
  /**
4126
3829
  * Resume a stopped sandbox.
@@ -4137,10 +3840,18 @@ var SandboxInstance = class SandboxInstance {
4137
3840
  const body = await response.text();
4138
3841
  throw parseErrorResponse(response.status, body, void 0, response.headers);
4139
3842
  }
3843
+ const diagnostics = await this.readLifecycleDiagnostics(response);
4140
3844
  await this.refresh();
3845
+ if (diagnostics) this.info.startupDiagnostics = diagnostics;
4141
3846
  }
4142
3847
  /**
4143
3848
  * Delete the sandbox permanently.
3849
+ *
3850
+ * The delete response carries the orchestrator's per-phase delete breakdown
3851
+ * (`container_remove` / `storage_binding_wait` / `cleanup_parallel` /
3852
+ * `delete_complete` / `delete_unaccounted`); after this resolves,
3853
+ * {@link startupDiagnostics} returns it for post-mortem reads on the
3854
+ * now-gone sandbox.
4144
3855
  */
4145
3856
  async delete() {
4146
3857
  const response = await this.client.fetch(`/v1/sandboxes/${this.id}`, { method: "DELETE" });
@@ -4148,6 +3859,17 @@ var SandboxInstance = class SandboxInstance {
4148
3859
  const body = await response.text();
4149
3860
  throw parseErrorResponse(response.status, body, void 0, response.headers);
4150
3861
  }
3862
+ const diagnostics = await this.readLifecycleDiagnostics(response);
3863
+ if (diagnostics) this.info.startupDiagnostics = diagnostics;
3864
+ }
3865
+ /**
3866
+ * Read the per-phase lifecycle breakdown off a stop/resume/delete response
3867
+ * body. Never throws: diagnostics are measurement data — a body that fails
3868
+ * to parse (older API, empty body) must not fail the lifecycle call.
3869
+ */
3870
+ async readLifecycleDiagnostics(response) {
3871
+ const data = await response.json().catch(() => null);
3872
+ return data ? normalizeStartupDiagnostics(data.startupDiagnostics) : null;
4151
3873
  }
4152
3874
  /**
4153
3875
  * keepAlive is intentionally unavailable until the API exposes timeout updates.
@@ -4732,6 +4454,7 @@ var SandboxInstance = class SandboxInstance {
4732
4454
  * gates ProductTokenIssuer-issued JWTs — no new sidecar surface.
4733
4455
  */
4734
4456
  async mintScopedToken(opts) {
4457
+ if ((opts.scope === "session" || opts.scope === "session-runtime") && !opts.sessionId) throw new ValidationError(`${opts.scope} scope requires a sessionId`);
4735
4458
  const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/scoped-token`, {
4736
4459
  method: "POST",
4737
4460
  headers: { "Content-Type": "application/json" },
@@ -4772,7 +4495,7 @@ var SandboxInstance = class SandboxInstance {
4772
4495
  const search = new URLSearchParams();
4773
4496
  search.set("sessionId", id);
4774
4497
  if (opts?.since) search.set("since", opts.since);
4775
- const response = await this.runtimeFetch(`/agents/events/?${search.toString()}`, { signal: opts?.signal });
4498
+ const response = await this.runtimeFetch(`/agents/events?${search.toString()}`, { signal: opts?.signal });
4776
4499
  if (!response.ok) {
4777
4500
  const body = await response.text();
4778
4501
  throw parseErrorResponse(response.status, body, void 0, response.headers);
@@ -5047,4 +4770,4 @@ function quoteForShell(value) {
5047
4770
  return `'${value.replace(/'/g, "'\\''")}'`;
5048
4771
  }
5049
4772
  //#endregion
5050
- export { normalizeRuntimeBackendConfig as _, InteractiveSessionHandle as a, getSandboxEventText as c, exportTraceBundle as d, otelTraceIdForTangleTrace as f, parseSSEStream as g, encodeTextForWire as h, SandboxSession as i, normalizeConnection as l, encodePromptForWire as m, normalizeStartupDiagnostics as n, applySandboxEventText as o, toOtelJson as p, SandboxTaskSession as r, collectAgentResponseText as s, SandboxInstance as t, buildTraceExportPayload as u, serializeForSidecar as v };
4773
+ export { InteractiveSessionHandle as a, getSandboxEventText as c, exportTraceBundle as d, otelTraceIdForTangleTrace as f, parseSSEStream as g, encodeTextForWire as h, SandboxSession as i, normalizeConnection as l, encodePromptForWire as m, normalizeStartupDiagnostics as n, applySandboxEventText as o, toOtelJson as p, SandboxTaskSession as r, collectAgentResponseText as s, SandboxInstance as t, buildTraceExportPayload as u };