@tangle-network/sandbox 0.9.1 → 0.9.2-develop.20260625104748.cb5cb77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +1026 -1
- package/dist/auth/index.js +273 -1
- package/dist/client-BBZ7YLmq.js +2193 -0
- package/dist/{client-DPVYHFLS.d.ts → client-D3FqPlde.d.ts} +11 -2
- package/dist/collaboration/index.js +2 -1
- package/dist/collaboration-BxlfZ2Uh.js +201 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js +4 -1
- package/dist/errors-DZsfJUuc.js +262 -1
- package/dist/{index-Bm9jAzE2.d.ts → index-Dy1fHLYz.d.ts} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1180 -1
- package/dist/intelligence/index.js +225 -1
- package/dist/openai/index.js +1760 -1
- package/dist/{sandbox-jfc8yiwF.d.ts → sandbox-B55j2q9K.d.ts} +3 -1
- package/dist/sandbox-joKtQ5y3.js +4457 -0
- package/dist/session-gateway/index.js +667 -1
- package/dist/tangle/index.d.ts +1 -1
- package/dist/tangle/index.js +2 -1
- package/dist/tangle-DM0o66lW.js +831 -0
- package/package.json +4 -4
- package/dist/client-BZn0szu8.js +0 -1
- package/dist/sandbox-CfuxtBeD.js +0 -1
- package/dist/tangle-DbX32r-5.js +0 -1
|
@@ -0,0 +1,2193 @@
|
|
|
1
|
+
import { f as encodePromptForWire, l as exportTraceBundle, p as parseSSEStream, s as normalizeConnection, t as SandboxInstance } from "./sandbox-joKtQ5y3.js";
|
|
2
|
+
import { d as ValidationError, f as parseErrorResponse, i as NotFoundError, r as NetworkError, t as AuthError, u as TimeoutError } from "./errors-DZsfJUuc.js";
|
|
3
|
+
//#region src/resources.ts
|
|
4
|
+
/**
|
|
5
|
+
* Compute tiers, ordered smallest → largest. Defined HERE (not imported from
|
|
6
|
+
* `@tangle-network/agent-interface`) so the SDK is self-contained: a consumer
|
|
7
|
+
* that type-checks the SDK SOURCE never has to resolve a cross-package export
|
|
8
|
+
* for the size vocabulary. `agent-interface` keeps its own identical copy for
|
|
9
|
+
* `Capability.recommendedSize`; a platform test guards the two against drift.
|
|
10
|
+
*/
|
|
11
|
+
const SANDBOX_SIZE_PRESET_NAMES = [
|
|
12
|
+
"nano",
|
|
13
|
+
"small",
|
|
14
|
+
"medium",
|
|
15
|
+
"large"
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* The concrete compute behind each {@link SandboxSizePreset} — the SINGLE source
|
|
19
|
+
* of truth for what a tier means. `@tangle-network/agent-interface` owns the
|
|
20
|
+
* tier NAMES (so the lowest layer can reference a size); this owns the numbers.
|
|
21
|
+
*
|
|
22
|
+
* Values stay within the sandbox-api default plan caps (8 vCPU / 16 GB / 100 GB)
|
|
23
|
+
* and keep memory at or above the driver's firecracker-snapshot floor (1536 MB),
|
|
24
|
+
* so a requested tier is never silently bumped. `diskGB` is in gigabytes;
|
|
25
|
+
* `memoryMB` in megabytes. The server still clamps every request to the owner's
|
|
26
|
+
* actual plan limits — these are the unclamped tier definitions, not guarantees.
|
|
27
|
+
*/
|
|
28
|
+
const SANDBOX_SIZE_PRESETS = {
|
|
29
|
+
nano: {
|
|
30
|
+
cpuCores: 1,
|
|
31
|
+
memoryMB: 2048,
|
|
32
|
+
diskGB: 5
|
|
33
|
+
},
|
|
34
|
+
small: {
|
|
35
|
+
cpuCores: 2,
|
|
36
|
+
memoryMB: 4096,
|
|
37
|
+
diskGB: 10
|
|
38
|
+
},
|
|
39
|
+
medium: {
|
|
40
|
+
cpuCores: 4,
|
|
41
|
+
memoryMB: 8192,
|
|
42
|
+
diskGB: 20
|
|
43
|
+
},
|
|
44
|
+
large: {
|
|
45
|
+
cpuCores: 8,
|
|
46
|
+
memoryMB: 16384,
|
|
47
|
+
diskGB: 50
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Default tier when a caller provisions without picking a size. The smallest
|
|
52
|
+
* tier that comfortably runs an agent harness (opencode + a repo clone + a
|
|
53
|
+
* build) — deliberately NOT a maxed box, so a thin task is cheap by default and
|
|
54
|
+
* a heavy one opts up explicitly.
|
|
55
|
+
*/
|
|
56
|
+
const DEFAULT_SANDBOX_SIZE = "small";
|
|
57
|
+
/** The resources for a named tier, as a fresh object (never the shared preset).
|
|
58
|
+
* Fails loud on an unknown key rather than spreading `undefined` into an empty
|
|
59
|
+
* resources object — a silent zero would send no compute request downstream. */
|
|
60
|
+
function sandboxResourcesForSize(size) {
|
|
61
|
+
const preset = SANDBOX_SIZE_PRESETS[size];
|
|
62
|
+
if (!preset) throw new Error(`sandboxResourcesForSize: unknown size preset '${size}' (expected one of ${SANDBOX_SIZE_PRESET_NAMES.join(", ")})`);
|
|
63
|
+
return { ...preset };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a caller's size intent to concrete {@link SandboxResources}. Raw
|
|
67
|
+
* `resources` win (an explicit cpu/memory request is authoritative); else a
|
|
68
|
+
* named `size` expands to its preset; else `undefined` (the caller applies its
|
|
69
|
+
* own default). Raw and preset are intentionally exclusive at the schema layer —
|
|
70
|
+
* if both arrive here, the explicit numbers take precedence rather than being
|
|
71
|
+
* silently merged.
|
|
72
|
+
*/
|
|
73
|
+
function resolveSandboxResources(opts) {
|
|
74
|
+
if (opts.resources) return opts.resources;
|
|
75
|
+
if (opts.size) return sandboxResourcesForSize(opts.size);
|
|
76
|
+
}
|
|
77
|
+
function normalizeSandboxResources(resources, driver) {
|
|
78
|
+
const legacyAccelerator = driver?.gpuRequired || driver?.gpuType || driver?.gpuCount ? {
|
|
79
|
+
kind: driver.gpuType ?? "gpu",
|
|
80
|
+
count: driver.gpuCount ?? 1
|
|
81
|
+
} : void 0;
|
|
82
|
+
if (!legacyAccelerator) return resources;
|
|
83
|
+
return {
|
|
84
|
+
...resources,
|
|
85
|
+
accelerator: resources?.accelerator ?? legacyAccelerator
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/fleet.ts
|
|
90
|
+
const FLEET_ID_META = "tangle.fleet.id";
|
|
91
|
+
const MACHINE_ID_META = "tangle.fleet.machine_id";
|
|
92
|
+
const MACHINE_ROLE_META = "tangle.fleet.machine_role";
|
|
93
|
+
const FLEET_CREATED_BY_META = "tangle.fleet.created_by";
|
|
94
|
+
const WORKSPACE_MODE_META = "tangle.fleet.workspace.mode";
|
|
95
|
+
const WORKSPACE_ID_META = "tangle.fleet.workspace.id";
|
|
96
|
+
const WORKSPACE_QUOTA_META = "tangle.fleet.workspace.quota_mb";
|
|
97
|
+
const WORKSPACE_MOUNT_META = "tangle.fleet.workspace.mount_path";
|
|
98
|
+
const RESERVED_METADATA_PREFIX = "tangle.";
|
|
99
|
+
const DEFAULT_MACHINE_LIFETIME_SECONDS = 3600;
|
|
100
|
+
const DEFAULT_MAX_CONCURRENT_CREATES = 4;
|
|
101
|
+
const MAX_CONCURRENT_ARTIFACT_READS = 8;
|
|
102
|
+
const DEFAULT_ARTIFACT_ROOT = "/workspace";
|
|
103
|
+
const DEFAULT_MAX_ARTIFACT_BYTES = 25 * 1024 * 1024;
|
|
104
|
+
const MAX_MACHINE_ID_LENGTH = 64;
|
|
105
|
+
const machineIdPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
106
|
+
function createFleetId() {
|
|
107
|
+
const crypto = globalThis.crypto;
|
|
108
|
+
if (crypto?.randomUUID) return `fleet_${crypto.randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
109
|
+
if (crypto?.getRandomValues) {
|
|
110
|
+
const bytes = crypto.getRandomValues(new Uint8Array(12));
|
|
111
|
+
return `fleet_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
112
|
+
}
|
|
113
|
+
throw new ValidationError("Fleet id generation requires global crypto");
|
|
114
|
+
}
|
|
115
|
+
function assertSafeId(value, label) {
|
|
116
|
+
if (value.length === 0 || value.length > MAX_MACHINE_ID_LENGTH || !machineIdPattern.test(value)) throw new ValidationError(`${label} must be 1-${MAX_MACHINE_ID_LENGTH} characters and contain only letters, numbers, '.', '_' or '-'`, { [label]: "invalid" });
|
|
117
|
+
}
|
|
118
|
+
function assertUniqueMachineIds(machines) {
|
|
119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
120
|
+
for (const machine of machines) {
|
|
121
|
+
assertSafeId(machine.machineId, "machineId");
|
|
122
|
+
if (seen.has(machine.machineId)) throw new ValidationError(`Duplicate machineId '${machine.machineId}' in fleet create request`, { machineId: "duplicate" });
|
|
123
|
+
seen.add(machine.machineId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function assertNoReservedMetadata(metadata, label) {
|
|
127
|
+
if (!metadata) return;
|
|
128
|
+
const reserved = Object.keys(metadata).find((key) => key.startsWith(RESERVED_METADATA_PREFIX));
|
|
129
|
+
if (reserved) throw new ValidationError(`${label} metadata key '${reserved}' is reserved by Tangle`, { metadata: "reserved" });
|
|
130
|
+
}
|
|
131
|
+
function mergeCreateOptions(defaults, machine) {
|
|
132
|
+
const { machineId: _machineId, metadata: _metadata, name: _name, role: _role, ...rest } = machine;
|
|
133
|
+
const backend = defaults?.backend || machine.backend ? {
|
|
134
|
+
...defaults?.backend,
|
|
135
|
+
...machine.backend,
|
|
136
|
+
type: machine.backend?.type ?? defaults?.backend?.type ?? "opencode",
|
|
137
|
+
model: {
|
|
138
|
+
...defaults?.backend?.model,
|
|
139
|
+
...machine.backend?.model
|
|
140
|
+
}
|
|
141
|
+
} : void 0;
|
|
142
|
+
const driver = machine.driver ?? defaults?.driver;
|
|
143
|
+
return {
|
|
144
|
+
...defaults,
|
|
145
|
+
...rest,
|
|
146
|
+
resources: normalizeSandboxResources({
|
|
147
|
+
...defaults?.resources,
|
|
148
|
+
...machine.resources
|
|
149
|
+
}, driver),
|
|
150
|
+
env: {
|
|
151
|
+
...defaults?.env,
|
|
152
|
+
...machine.env
|
|
153
|
+
},
|
|
154
|
+
backend,
|
|
155
|
+
driver
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function assertPolicy(machines, defaults, policy) {
|
|
159
|
+
const maxMachines = policy?.maxMachines ?? machines.length;
|
|
160
|
+
if (!Number.isInteger(maxMachines) || maxMachines <= 0) throw new ValidationError("policy.maxMachines must be a positive integer");
|
|
161
|
+
if (machines.length > maxMachines) throw new ValidationError(`Fleet create requested ${machines.length} machines but policy.maxMachines is ${maxMachines}`, { "policy.maxMachines": "exceeded" });
|
|
162
|
+
if (policy?.maxConcurrentCreates !== void 0 && (!Number.isInteger(policy.maxConcurrentCreates) || policy.maxConcurrentCreates <= 0)) throw new ValidationError("policy.maxConcurrentCreates must be a positive integer", { "policy.maxConcurrentCreates": "invalid" });
|
|
163
|
+
if (policy?.maxSpendUsd !== void 0 && (!Number.isFinite(policy.maxSpendUsd) || policy.maxSpendUsd < 0)) throw new ValidationError("policy.maxSpendUsd must be non-negative", { "policy.maxSpendUsd": "invalid" });
|
|
164
|
+
let totalCpu = 0;
|
|
165
|
+
let totalMemoryMb = 0;
|
|
166
|
+
let totalStorageMb = 0;
|
|
167
|
+
let totalAccelerators = 0;
|
|
168
|
+
for (const machine of machines) {
|
|
169
|
+
const resources = mergeCreateOptions(defaults, machine).resources;
|
|
170
|
+
totalCpu += resources?.cpuCores ?? 0;
|
|
171
|
+
totalMemoryMb += resources?.memoryMB ?? 0;
|
|
172
|
+
totalStorageMb += resources?.diskGB ? resources.diskGB * 1024 : 0;
|
|
173
|
+
totalAccelerators += resources?.accelerator?.count ?? (resources?.accelerator ? 1 : 0);
|
|
174
|
+
}
|
|
175
|
+
if (policy?.maxTotalCpu !== void 0 && totalCpu > policy.maxTotalCpu) throw new ValidationError(`Fleet create requested ${totalCpu} CPU cores but policy.maxTotalCpu is ${policy.maxTotalCpu}`, { "policy.maxTotalCpu": "exceeded" });
|
|
176
|
+
if (policy?.maxTotalMemoryMb !== void 0 && totalMemoryMb > policy.maxTotalMemoryMb) throw new ValidationError(`Fleet create requested ${totalMemoryMb} MB RAM but policy.maxTotalMemoryMb is ${policy.maxTotalMemoryMb}`, { "policy.maxTotalMemoryMb": "exceeded" });
|
|
177
|
+
if (policy?.maxTotalStorageMb !== void 0 && totalStorageMb > policy.maxTotalStorageMb) throw new ValidationError(`Fleet create requested ${totalStorageMb} MB storage but policy.maxTotalStorageMb is ${policy.maxTotalStorageMb}`, { "policy.maxTotalStorageMb": "exceeded" });
|
|
178
|
+
if (policy?.maxTotalAccelerators !== void 0 && totalAccelerators > policy.maxTotalAccelerators) throw new ValidationError(`Fleet create requested ${totalAccelerators} accelerator devices but policy.maxTotalAccelerators is ${policy.maxTotalAccelerators}`, { "policy.maxTotalAccelerators": "exceeded" });
|
|
179
|
+
const maxLifetimeSeconds = Math.max(...machines.map((machine) => mergeCreateOptions(defaults, machine).maxLifetimeSeconds ?? DEFAULT_MACHINE_LIFETIME_SECONDS));
|
|
180
|
+
if (policy?.maxLifetimeSeconds !== void 0 && maxLifetimeSeconds > policy.maxLifetimeSeconds) throw new ValidationError(`Fleet create requested ${maxLifetimeSeconds}s max lifetime but policy.maxLifetimeSeconds is ${policy.maxLifetimeSeconds}`, { "policy.maxLifetimeSeconds": "exceeded" });
|
|
181
|
+
for (const machine of machines) {
|
|
182
|
+
const options = mergeCreateOptions(defaults, machine);
|
|
183
|
+
assertMachineDescriptorPolicy(machine.machineId, options, policy);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function assertMachineDescriptorPolicy(machineId, options, policy) {
|
|
187
|
+
if (!policy) return;
|
|
188
|
+
const driverType = options.driver?.type;
|
|
189
|
+
if (policy.allowedDrivers && driverType && !policy.allowedDrivers.includes(driverType)) throw new ValidationError(`Fleet machine '${machineId}' requested driver '${driverType}' outside policy.allowedDrivers`, { "policy.allowedDrivers": "exceeded" });
|
|
190
|
+
const image = options.environment ?? options.image;
|
|
191
|
+
if (policy.allowedImages && image && !policy.allowedImages.includes(image)) throw new ValidationError(`Fleet machine '${machineId}' requested image '${image}' outside policy.allowedImages`, { "policy.allowedImages": "exceeded" });
|
|
192
|
+
const templateId = options.templateId ?? options.publicTemplateId;
|
|
193
|
+
if (policy.allowedTemplateIds && templateId && !policy.allowedTemplateIds.includes(templateId)) throw new ValidationError(`Fleet machine '${machineId}' requested template '${templateId}' outside policy.allowedTemplateIds`, { "policy.allowedTemplateIds": "exceeded" });
|
|
194
|
+
if (policy.allowAccelerators === false && (options.resources?.accelerator?.count ?? (options.resources?.accelerator ? 1 : 0)) > 0) throw new ValidationError(`Fleet machine '${machineId}' requested accelerators but policy.allowAccelerators is false`, { "policy.allowAccelerators": "exceeded" });
|
|
195
|
+
}
|
|
196
|
+
function calculateResourceTotals(machines, defaults) {
|
|
197
|
+
let totalCpu = 0;
|
|
198
|
+
let totalMemoryMb = 0;
|
|
199
|
+
let totalStorageMb = 0;
|
|
200
|
+
let totalAccelerators = 0;
|
|
201
|
+
let maxLifetimeSeconds = 0;
|
|
202
|
+
for (const machine of machines) {
|
|
203
|
+
const options = mergeCreateOptions(defaults, machine);
|
|
204
|
+
const resources = options.resources;
|
|
205
|
+
totalCpu += resources?.cpuCores ?? 0;
|
|
206
|
+
totalMemoryMb += resources?.memoryMB ?? 0;
|
|
207
|
+
totalStorageMb += resources?.diskGB ? resources.diskGB * 1024 : 0;
|
|
208
|
+
totalAccelerators += resources?.accelerator?.count ?? (resources?.accelerator ? 1 : 0);
|
|
209
|
+
maxLifetimeSeconds = Math.max(maxLifetimeSeconds, options.maxLifetimeSeconds ?? DEFAULT_MACHINE_LIFETIME_SECONDS);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
machines: machines.length,
|
|
213
|
+
totalCpu,
|
|
214
|
+
totalMemoryMb,
|
|
215
|
+
totalStorageMb,
|
|
216
|
+
totalAccelerators,
|
|
217
|
+
maxLifetimeSeconds
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function workspaceMetadata(workspace) {
|
|
221
|
+
return {
|
|
222
|
+
[WORKSPACE_MODE_META]: workspace?.mode ?? "isolated",
|
|
223
|
+
...workspace?.id ? { [WORKSPACE_ID_META]: workspace.id } : {},
|
|
224
|
+
...workspace?.quotaMb ? { [WORKSPACE_QUOTA_META]: workspace.quotaMb } : {},
|
|
225
|
+
...workspace?.mountPath ? { [WORKSPACE_MOUNT_META]: workspace.mountPath } : {}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function resolveMaxConcurrentCreates(value) {
|
|
229
|
+
const resolved = value ?? DEFAULT_MAX_CONCURRENT_CREATES;
|
|
230
|
+
if (!Number.isInteger(resolved) || resolved <= 0) throw new ValidationError("maxConcurrentCreates must be a positive integer");
|
|
231
|
+
return resolved;
|
|
232
|
+
}
|
|
233
|
+
function getMachineId(info) {
|
|
234
|
+
const value = info.metadata?.[MACHINE_ID_META];
|
|
235
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
236
|
+
}
|
|
237
|
+
function getFleetId(info) {
|
|
238
|
+
const value = info.metadata?.[FLEET_ID_META];
|
|
239
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
240
|
+
}
|
|
241
|
+
function dispatchResultsPath(fleetId, dispatchId, options) {
|
|
242
|
+
const params = new URLSearchParams();
|
|
243
|
+
if (options?.cursor) params.set("cursor", options.cursor);
|
|
244
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
245
|
+
for (const machine of options?.machines ?? []) params.append("machine", machine);
|
|
246
|
+
const query = params.toString();
|
|
247
|
+
const path = `/v1/fleets/${fleetId}/dispatch/${encodeURIComponent(dispatchId)}/results`;
|
|
248
|
+
return query ? `${path}?${query}` : path;
|
|
249
|
+
}
|
|
250
|
+
function execDispatchBody(command, options) {
|
|
251
|
+
return {
|
|
252
|
+
type: "exec",
|
|
253
|
+
command,
|
|
254
|
+
cwd: options?.cwd,
|
|
255
|
+
env: options?.env,
|
|
256
|
+
timeoutMs: options?.timeoutMs,
|
|
257
|
+
machines: options?.machines,
|
|
258
|
+
maxConcurrent: options?.maxConcurrent,
|
|
259
|
+
retry: options?.retry,
|
|
260
|
+
dispatchId: options?.dispatchId,
|
|
261
|
+
bufferResults: options?.bufferResults
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function promptDispatchBody(message, options) {
|
|
265
|
+
return {
|
|
266
|
+
type: "prompt",
|
|
267
|
+
parts: encodePromptForWire(message),
|
|
268
|
+
sessionId: options?.sessionId,
|
|
269
|
+
model: options?.model,
|
|
270
|
+
backend: options?.backend,
|
|
271
|
+
context: options?.context,
|
|
272
|
+
timeoutMs: options?.timeoutMs,
|
|
273
|
+
machines: options?.machines,
|
|
274
|
+
maxConcurrent: options?.maxConcurrent,
|
|
275
|
+
retry: options?.retry,
|
|
276
|
+
dispatchId: options?.dispatchId,
|
|
277
|
+
bufferResults: options?.bufferResults
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
var SandboxFleet = class {
|
|
281
|
+
constructor(client, fleetId, machines) {
|
|
282
|
+
this.client = client;
|
|
283
|
+
this.fleetId = fleetId;
|
|
284
|
+
this.machines = machines;
|
|
285
|
+
}
|
|
286
|
+
get ids() {
|
|
287
|
+
return [...this.machines.keys()];
|
|
288
|
+
}
|
|
289
|
+
get(machineId) {
|
|
290
|
+
const machine = this.machines.get(machineId);
|
|
291
|
+
if (!machine) throw new ValidationError(`Unknown fleet machine '${machineId}'`);
|
|
292
|
+
return machine;
|
|
293
|
+
}
|
|
294
|
+
sandbox(machineId) {
|
|
295
|
+
return this.requireSandbox(machineId);
|
|
296
|
+
}
|
|
297
|
+
async collectArtifacts(artifacts) {
|
|
298
|
+
return mapConcurrent(artifacts, Math.min(MAX_CONCURRENT_ARTIFACT_READS, artifacts.length || 1), async (artifact) => {
|
|
299
|
+
assertArtifactPath(artifact.path);
|
|
300
|
+
const machine = this.get(artifact.machineId);
|
|
301
|
+
const content = await (await this.requireSandbox(artifact.machineId)).read(artifact.path);
|
|
302
|
+
assertArtifactSize(content, artifact);
|
|
303
|
+
return {
|
|
304
|
+
...artifact,
|
|
305
|
+
sandboxId: machine.id,
|
|
306
|
+
content
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async exec(machineId, command, options) {
|
|
311
|
+
const [result] = await this.dispatchExec(command, {
|
|
312
|
+
...options,
|
|
313
|
+
machines: [machineId]
|
|
314
|
+
});
|
|
315
|
+
if (result?.ok && result.result) return result.result;
|
|
316
|
+
if (result?.error) throw new Error(result.error.message);
|
|
317
|
+
throw new Error(`Fleet exec produced no result for machine '${machineId}'`);
|
|
318
|
+
}
|
|
319
|
+
async dispatchExec(command, options) {
|
|
320
|
+
return (await this.dispatchExecDetailed(command, options)).results;
|
|
321
|
+
}
|
|
322
|
+
async dispatchExecDetailed(command, options) {
|
|
323
|
+
const response = await this.client.fetch(`/v1/fleets/${this.fleetId}/dispatch`, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
body: JSON.stringify(execDispatchBody(command, options))
|
|
326
|
+
});
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
const body = await response.text();
|
|
329
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
330
|
+
}
|
|
331
|
+
return await response.json();
|
|
332
|
+
}
|
|
333
|
+
async prompt(machineId, message, options) {
|
|
334
|
+
const [result] = await this.dispatchPrompt(message, {
|
|
335
|
+
...options,
|
|
336
|
+
machines: [machineId]
|
|
337
|
+
});
|
|
338
|
+
if (result?.prompt) return result.prompt;
|
|
339
|
+
if (result?.error) throw new Error(result.error.message);
|
|
340
|
+
throw new Error(`Fleet prompt produced no result for machine '${machineId}'`);
|
|
341
|
+
}
|
|
342
|
+
async dispatchPrompt(message, options) {
|
|
343
|
+
return (await this.dispatchPromptDetailed(message, options)).results;
|
|
344
|
+
}
|
|
345
|
+
async dispatchPromptDetailed(message, options) {
|
|
346
|
+
const response = await this.client.fetch(`/v1/fleets/${this.fleetId}/dispatch`, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
body: JSON.stringify(promptDispatchBody(message, options))
|
|
349
|
+
});
|
|
350
|
+
if (!response.ok) {
|
|
351
|
+
const body = await response.text();
|
|
352
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
353
|
+
}
|
|
354
|
+
return await response.json();
|
|
355
|
+
}
|
|
356
|
+
manifest() {
|
|
357
|
+
return this.client.fleets.manifest(this.fleetId);
|
|
358
|
+
}
|
|
359
|
+
attachMachine(machine) {
|
|
360
|
+
return this.client.fleets.attachMachine(this.fleetId, machine);
|
|
361
|
+
}
|
|
362
|
+
detachMachine(machineId) {
|
|
363
|
+
return this.client.fleets.detachMachine(this.fleetId, machineId);
|
|
364
|
+
}
|
|
365
|
+
dispatchResults(dispatchId, options) {
|
|
366
|
+
return this.client.fleets.dispatchResults(this.fleetId, dispatchId, options);
|
|
367
|
+
}
|
|
368
|
+
cancelDispatch(dispatchId, reason) {
|
|
369
|
+
return this.client.fleets.cancelDispatch(this.fleetId, dispatchId, reason);
|
|
370
|
+
}
|
|
371
|
+
createWorkspaceSnapshot() {
|
|
372
|
+
return this.client.fleets.createWorkspaceSnapshot(this.fleetId);
|
|
373
|
+
}
|
|
374
|
+
restoreWorkspaceSnapshot(snapshotId) {
|
|
375
|
+
return this.client.fleets.restoreWorkspaceSnapshot(this.fleetId, snapshotId);
|
|
376
|
+
}
|
|
377
|
+
reconcileWorkspace() {
|
|
378
|
+
return this.client.fleets.reconcileWorkspace(this.fleetId);
|
|
379
|
+
}
|
|
380
|
+
dispatchExecStream(command, options) {
|
|
381
|
+
return this.client.fleets.dispatchExecStream(this.fleetId, command, options);
|
|
382
|
+
}
|
|
383
|
+
dispatchPromptStream(message, options) {
|
|
384
|
+
return this.client.fleets.dispatchPromptStream(this.fleetId, message, options);
|
|
385
|
+
}
|
|
386
|
+
async delete(options) {
|
|
387
|
+
const errors = [];
|
|
388
|
+
await Promise.all([...this.machines.values()].map(async (machine) => {
|
|
389
|
+
const box = await this.client.get(machine.id);
|
|
390
|
+
if (!box) return;
|
|
391
|
+
try {
|
|
392
|
+
await box.delete();
|
|
393
|
+
} catch (err) {
|
|
394
|
+
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
395
|
+
}
|
|
396
|
+
}));
|
|
397
|
+
await this.client.fleets.deleteRecord(this.fleetId);
|
|
398
|
+
if (errors.length > 0) {
|
|
399
|
+
if (!options?.continueOnError) throw errors[0];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async usage() {
|
|
403
|
+
const response = await this.client.fetch(`/v1/fleets/${this.fleetId}/usage`);
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
const body = await response.text();
|
|
406
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
407
|
+
}
|
|
408
|
+
return await response.json();
|
|
409
|
+
}
|
|
410
|
+
async trace(options = {}) {
|
|
411
|
+
return this.client.fleets.trace(this.fleetId, options);
|
|
412
|
+
}
|
|
413
|
+
async intelligence() {
|
|
414
|
+
const intelligence = (await this.trace({ includeIntelligence: true })).intelligence;
|
|
415
|
+
if (!intelligence) throw new Error("Fleet intelligence was not returned");
|
|
416
|
+
return intelligence;
|
|
417
|
+
}
|
|
418
|
+
async createIntelligenceReport(options = {}) {
|
|
419
|
+
const { dispatchId, window, compareTo, ...rest } = options;
|
|
420
|
+
return this.client.intelligence.createReport({
|
|
421
|
+
subject: {
|
|
422
|
+
type: "fleet",
|
|
423
|
+
id: this.fleetId,
|
|
424
|
+
dispatchId,
|
|
425
|
+
window,
|
|
426
|
+
compareTo
|
|
427
|
+
},
|
|
428
|
+
...rest
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async createAgenticIntelligenceReport(options) {
|
|
432
|
+
return this.createIntelligenceReport({
|
|
433
|
+
mode: "agentic",
|
|
434
|
+
budget: {
|
|
435
|
+
billTo: "customer",
|
|
436
|
+
maxUsd: options.maxUsd
|
|
437
|
+
},
|
|
438
|
+
metadata: options.metadata,
|
|
439
|
+
dispatchId: options.dispatchId,
|
|
440
|
+
window: options.window,
|
|
441
|
+
compareTo: options.compareTo
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
async exportTrace(sink) {
|
|
445
|
+
return exportTraceBundle(await this.trace(), sink);
|
|
446
|
+
}
|
|
447
|
+
async cost() {
|
|
448
|
+
return this.client.fleets.cost(this.fleetId);
|
|
449
|
+
}
|
|
450
|
+
async createToken(options) {
|
|
451
|
+
return this.client.fleets.createToken(this.fleetId, options);
|
|
452
|
+
}
|
|
453
|
+
async requireSandbox(machineId) {
|
|
454
|
+
const machine = this.get(machineId);
|
|
455
|
+
const box = await this.client.get(machine.id);
|
|
456
|
+
if (!box) throw new ValidationError(`Fleet machine '${machineId}' no longer exists`, { machineId: "not_found" });
|
|
457
|
+
return box;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
var SandboxFleetClient = class {
|
|
461
|
+
constructor(client) {
|
|
462
|
+
this.client = client;
|
|
463
|
+
}
|
|
464
|
+
async capabilities() {
|
|
465
|
+
const response = await this.client.fetch("/v1/fleets/capabilities");
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
const body = await response.text();
|
|
468
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
469
|
+
}
|
|
470
|
+
return await response.json();
|
|
471
|
+
}
|
|
472
|
+
async operations() {
|
|
473
|
+
const response = await this.client.fetch("/v1/fleets/operations");
|
|
474
|
+
if (!response.ok) {
|
|
475
|
+
const body = await response.text();
|
|
476
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
477
|
+
}
|
|
478
|
+
return await response.json();
|
|
479
|
+
}
|
|
480
|
+
async reconcile(options) {
|
|
481
|
+
const response = await this.client.fetch("/v1/fleets/reconcile", {
|
|
482
|
+
method: "POST",
|
|
483
|
+
body: JSON.stringify(options ?? {})
|
|
484
|
+
});
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
const body = await response.text();
|
|
487
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
488
|
+
}
|
|
489
|
+
return await response.json();
|
|
490
|
+
}
|
|
491
|
+
async estimateCost(options) {
|
|
492
|
+
if (options.machines.length === 0) throw new ValidationError("Fleet cost estimate requires at least one machine");
|
|
493
|
+
assertUniqueMachineIds(options.machines);
|
|
494
|
+
assertPolicy(options.machines, options.defaults, options.policy);
|
|
495
|
+
const response = await this.client.fetch("/v1/fleets/estimate", {
|
|
496
|
+
method: "POST",
|
|
497
|
+
body: JSON.stringify({
|
|
498
|
+
machineCount: options.machines.length,
|
|
499
|
+
policy: options.policy,
|
|
500
|
+
resources: calculateResourceTotals(options.machines, options.defaults),
|
|
501
|
+
maxConcurrentCreates: options.maxConcurrentCreates
|
|
502
|
+
})
|
|
503
|
+
});
|
|
504
|
+
if (!response.ok) {
|
|
505
|
+
const body = await response.text();
|
|
506
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
507
|
+
}
|
|
508
|
+
return (await response.json()).estimate;
|
|
509
|
+
}
|
|
510
|
+
async create(options) {
|
|
511
|
+
const fleetId = options.fleetId ?? createFleetId();
|
|
512
|
+
assertSafeId(fleetId, "fleetId");
|
|
513
|
+
if (options.machines.length === 0) throw new ValidationError("Fleet create requires at least one machine");
|
|
514
|
+
assertUniqueMachineIds(options.machines);
|
|
515
|
+
assertNoReservedMetadata(options.metadata, "Fleet");
|
|
516
|
+
for (const machine of options.machines) assertNoReservedMetadata(machine.metadata, `Fleet machine '${machine.machineId}'`);
|
|
517
|
+
assertPolicy(options.machines, options.defaults, options.policy);
|
|
518
|
+
const maxConcurrentCreates = resolveMaxConcurrentCreates(options.maxConcurrentCreates);
|
|
519
|
+
if (options.policy?.maxConcurrentCreates !== void 0 && maxConcurrentCreates > options.policy.maxConcurrentCreates) throw new ValidationError(`Fleet create requested ${maxConcurrentCreates} concurrent creates but policy.maxConcurrentCreates is ${options.policy.maxConcurrentCreates}`, { "policy.maxConcurrentCreates": "exceeded" });
|
|
520
|
+
if (options.policy?.maxSpendUsd !== void 0) {
|
|
521
|
+
const estimate = await this.estimateCost({
|
|
522
|
+
defaults: options.defaults,
|
|
523
|
+
machines: options.machines,
|
|
524
|
+
policy: options.policy,
|
|
525
|
+
maxConcurrentCreates
|
|
526
|
+
});
|
|
527
|
+
if (estimate.estimatedMaxLifetimeUsd > options.policy.maxSpendUsd) throw new ValidationError(`Fleet estimated max lifetime cost $${estimate.estimatedMaxLifetimeUsd} exceeds policy.maxSpendUsd $${options.policy.maxSpendUsd}`, { "policy.maxSpendUsd": "exceeded" });
|
|
528
|
+
}
|
|
529
|
+
const created = [];
|
|
530
|
+
try {
|
|
531
|
+
for (let i = 0; i < options.machines.length; i += maxConcurrentCreates) {
|
|
532
|
+
const batch = options.machines.slice(i, i + maxConcurrentCreates);
|
|
533
|
+
const results = await Promise.allSettled(batch.map((machine) => this.createMachine(fleetId, machine, options)));
|
|
534
|
+
const rejected = results.find((result) => result.status === "rejected");
|
|
535
|
+
for (const result of results) if (result.status === "fulfilled") created.push(result.value);
|
|
536
|
+
if (rejected) throw rejected.reason;
|
|
537
|
+
}
|
|
538
|
+
} catch (err) {
|
|
539
|
+
if (options.cleanupOnFailure !== false) await Promise.allSettled(created.map(async (machine) => {
|
|
540
|
+
await (await this.client.get(machine.sandbox.id))?.delete();
|
|
541
|
+
}));
|
|
542
|
+
throw err;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
await this.persistFleet(fleetId, created, options);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
if (options.cleanupOnFailure !== false) await Promise.allSettled(created.map(async (machine) => {
|
|
548
|
+
await (await this.client.get(machine.sandbox.id))?.delete();
|
|
549
|
+
}));
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
return this.fromInfo({
|
|
553
|
+
fleetId,
|
|
554
|
+
machines: created
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
async createWithCoordinator(options) {
|
|
558
|
+
const { coordinator: coordinatorOptions, workers, ...createOptions } = options;
|
|
559
|
+
const coordinator = {
|
|
560
|
+
...coordinatorOptions,
|
|
561
|
+
machineId: coordinatorOptions?.machineId ?? "coordinator",
|
|
562
|
+
role: "coordinator"
|
|
563
|
+
};
|
|
564
|
+
return this.create({
|
|
565
|
+
...createOptions,
|
|
566
|
+
machines: [coordinator, ...workers.map((worker) => ({
|
|
567
|
+
...worker,
|
|
568
|
+
role: worker.role ?? "worker"
|
|
569
|
+
}))]
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async createMachine(fleetId, machine, options) {
|
|
573
|
+
const merged = mergeCreateOptions(options.defaults, machine);
|
|
574
|
+
const sandbox = await this.client.create({
|
|
575
|
+
...merged,
|
|
576
|
+
name: machine.name ?? `${fleetId}-${machine.machineId}`,
|
|
577
|
+
maxLifetimeSeconds: merged.maxLifetimeSeconds ?? DEFAULT_MACHINE_LIFETIME_SECONDS,
|
|
578
|
+
metadata: {
|
|
579
|
+
...options.metadata,
|
|
580
|
+
...machine.metadata,
|
|
581
|
+
[FLEET_ID_META]: fleetId,
|
|
582
|
+
[MACHINE_ID_META]: machine.machineId,
|
|
583
|
+
[MACHINE_ROLE_META]: machine.role ?? "worker",
|
|
584
|
+
[FLEET_CREATED_BY_META]: "@tangle-network/sandbox",
|
|
585
|
+
...workspaceMetadata(options.workspace)
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
return {
|
|
589
|
+
machineId: machine.machineId,
|
|
590
|
+
sandbox: sandbox.toJSON(),
|
|
591
|
+
role: machine.role
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
async list(options) {
|
|
595
|
+
assertSafeId(options.fleetId, "fleetId");
|
|
596
|
+
const serverFleet = await this.getServerFleet(options.fleetId);
|
|
597
|
+
if (serverFleet) return this.fromInfo(await this.hydrateServerFleet(serverFleet, options));
|
|
598
|
+
return this.listBySandboxMetadata(options);
|
|
599
|
+
}
|
|
600
|
+
async delete(fleetId, options) {
|
|
601
|
+
await (await this.list({
|
|
602
|
+
...options,
|
|
603
|
+
fleetId
|
|
604
|
+
})).delete({ continueOnError: options?.continueOnError });
|
|
605
|
+
}
|
|
606
|
+
fromInfo(info) {
|
|
607
|
+
const machines = /* @__PURE__ */ new Map();
|
|
608
|
+
for (const machine of info.machines) machines.set(machine.machineId, machine.sandbox);
|
|
609
|
+
return new SandboxFleet(this.client, info.fleetId, machines);
|
|
610
|
+
}
|
|
611
|
+
async persistFleet(fleetId, machines, options) {
|
|
612
|
+
const response = await this.client.fetch("/v1/fleets", {
|
|
613
|
+
method: "POST",
|
|
614
|
+
headers: { "Idempotency-Key": options.idempotencyKey ?? fleetId },
|
|
615
|
+
body: JSON.stringify({
|
|
616
|
+
id: fleetId,
|
|
617
|
+
metadata: {
|
|
618
|
+
...options.metadata,
|
|
619
|
+
...workspaceMetadata(options.workspace)
|
|
620
|
+
},
|
|
621
|
+
workspace: options.workspace,
|
|
622
|
+
policy: options.policy,
|
|
623
|
+
resources: calculateResourceTotals(options.machines, options.defaults),
|
|
624
|
+
maxConcurrentCreates: options.maxConcurrentCreates,
|
|
625
|
+
machines: machines.map((machine) => ({
|
|
626
|
+
machineId: machine.machineId,
|
|
627
|
+
sandboxId: machine.sandbox.id,
|
|
628
|
+
status: machine.sandbox.status,
|
|
629
|
+
role: machine.role,
|
|
630
|
+
...getMachineDescriptor(options, machine.machineId)
|
|
631
|
+
}))
|
|
632
|
+
})
|
|
633
|
+
});
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
const body = await response.text();
|
|
636
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async getServerFleet(fleetId) {
|
|
640
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}`);
|
|
641
|
+
if (response.status === 404) return null;
|
|
642
|
+
if (!response.ok) {
|
|
643
|
+
const body = await response.text();
|
|
644
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
645
|
+
}
|
|
646
|
+
return (await response.json()).fleet ?? null;
|
|
647
|
+
}
|
|
648
|
+
async deleteRecord(fleetId) {
|
|
649
|
+
assertSafeId(fleetId, "fleetId");
|
|
650
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}`, { method: "DELETE" });
|
|
651
|
+
if (response.status === 404) return;
|
|
652
|
+
if (!response.ok) {
|
|
653
|
+
const body = await response.text();
|
|
654
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async usage(fleetId) {
|
|
658
|
+
assertSafeId(fleetId, "fleetId");
|
|
659
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/usage`);
|
|
660
|
+
if (!response.ok) {
|
|
661
|
+
const body = await response.text();
|
|
662
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
663
|
+
}
|
|
664
|
+
return await response.json();
|
|
665
|
+
}
|
|
666
|
+
async trace(fleetId, options = {}) {
|
|
667
|
+
assertSafeId(fleetId, "fleetId");
|
|
668
|
+
const query = new URLSearchParams();
|
|
669
|
+
if (options.includeIntelligence === true) query.set("includeIntelligence", "true");
|
|
670
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/trace${query.size ? `?${query}` : ""}`);
|
|
671
|
+
if (!response.ok) {
|
|
672
|
+
const body = await response.text();
|
|
673
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
674
|
+
}
|
|
675
|
+
return await response.json();
|
|
676
|
+
}
|
|
677
|
+
async intelligence(fleetId) {
|
|
678
|
+
const intelligence = (await this.trace(fleetId, { includeIntelligence: true })).intelligence;
|
|
679
|
+
if (!intelligence) throw new Error("Fleet intelligence was not returned");
|
|
680
|
+
return intelligence;
|
|
681
|
+
}
|
|
682
|
+
async createIntelligenceReport(fleetId, options = {}) {
|
|
683
|
+
return this.client.intelligence.createReport({
|
|
684
|
+
subject: {
|
|
685
|
+
type: "fleet",
|
|
686
|
+
id: fleetId
|
|
687
|
+
},
|
|
688
|
+
...options
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
async createAgenticIntelligenceReport(fleetId, options) {
|
|
692
|
+
return this.createIntelligenceReport(fleetId, {
|
|
693
|
+
mode: "agentic",
|
|
694
|
+
budget: {
|
|
695
|
+
billTo: "customer",
|
|
696
|
+
maxUsd: options.maxUsd
|
|
697
|
+
},
|
|
698
|
+
metadata: options.metadata
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
async exportTrace(fleetId, sink) {
|
|
702
|
+
return exportTraceBundle(await this.trace(fleetId), sink);
|
|
703
|
+
}
|
|
704
|
+
async cost(fleetId) {
|
|
705
|
+
assertSafeId(fleetId, "fleetId");
|
|
706
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/cost`);
|
|
707
|
+
if (!response.ok) {
|
|
708
|
+
const body = await response.text();
|
|
709
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
710
|
+
}
|
|
711
|
+
return (await response.json()).estimate;
|
|
712
|
+
}
|
|
713
|
+
async createToken(fleetId, options) {
|
|
714
|
+
assertSafeId(fleetId, "fleetId");
|
|
715
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/token`, {
|
|
716
|
+
method: "POST",
|
|
717
|
+
body: JSON.stringify(options ?? {})
|
|
718
|
+
});
|
|
719
|
+
if (!response.ok) {
|
|
720
|
+
const body = await response.text();
|
|
721
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
722
|
+
}
|
|
723
|
+
return await response.json();
|
|
724
|
+
}
|
|
725
|
+
async manifest(fleetId) {
|
|
726
|
+
assertSafeId(fleetId, "fleetId");
|
|
727
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/manifest`);
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
const body = await response.text();
|
|
730
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
731
|
+
}
|
|
732
|
+
return await response.json();
|
|
733
|
+
}
|
|
734
|
+
async attachMachine(fleetId, machine) {
|
|
735
|
+
assertSafeId(fleetId, "fleetId");
|
|
736
|
+
assertSafeId(machine.machineId, "machineId");
|
|
737
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/machines`, {
|
|
738
|
+
method: "POST",
|
|
739
|
+
body: JSON.stringify(machine)
|
|
740
|
+
});
|
|
741
|
+
if (!response.ok) {
|
|
742
|
+
const body = await response.text();
|
|
743
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
744
|
+
}
|
|
745
|
+
return (await response.json()).machine;
|
|
746
|
+
}
|
|
747
|
+
async detachMachine(fleetId, machineId) {
|
|
748
|
+
assertSafeId(fleetId, "fleetId");
|
|
749
|
+
assertSafeId(machineId, "machineId");
|
|
750
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/machines/${encodeURIComponent(machineId)}`, { method: "DELETE" });
|
|
751
|
+
if (!response.ok) {
|
|
752
|
+
const body = await response.text();
|
|
753
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
754
|
+
}
|
|
755
|
+
return await response.json();
|
|
756
|
+
}
|
|
757
|
+
async dispatchResults(fleetId, dispatchId, options) {
|
|
758
|
+
assertSafeId(fleetId, "fleetId");
|
|
759
|
+
assertSafeId(dispatchId, "dispatchId");
|
|
760
|
+
const response = await this.client.fetch(dispatchResultsPath(fleetId, dispatchId, options));
|
|
761
|
+
if (!response.ok) {
|
|
762
|
+
const body = await response.text();
|
|
763
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
764
|
+
}
|
|
765
|
+
return await response.json();
|
|
766
|
+
}
|
|
767
|
+
async cancelDispatch(fleetId, dispatchId, reason) {
|
|
768
|
+
assertSafeId(fleetId, "fleetId");
|
|
769
|
+
assertSafeId(dispatchId, "dispatchId");
|
|
770
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/dispatch/${encodeURIComponent(dispatchId)}/cancel`, {
|
|
771
|
+
method: "POST",
|
|
772
|
+
body: JSON.stringify(reason ? { reason } : {})
|
|
773
|
+
});
|
|
774
|
+
if (!response.ok) {
|
|
775
|
+
const body = await response.text();
|
|
776
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
777
|
+
}
|
|
778
|
+
return await response.json();
|
|
779
|
+
}
|
|
780
|
+
async createWorkspaceSnapshot(fleetId) {
|
|
781
|
+
assertSafeId(fleetId, "fleetId");
|
|
782
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/workspace/snapshots`, { method: "POST" });
|
|
783
|
+
if (!response.ok) {
|
|
784
|
+
const body = await response.text();
|
|
785
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
786
|
+
}
|
|
787
|
+
return await response.json();
|
|
788
|
+
}
|
|
789
|
+
async restoreWorkspaceSnapshot(fleetId, snapshotId) {
|
|
790
|
+
assertSafeId(fleetId, "fleetId");
|
|
791
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/workspace/snapshots/${encodeURIComponent(snapshotId)}/restore`, { method: "POST" });
|
|
792
|
+
if (!response.ok) {
|
|
793
|
+
const body = await response.text();
|
|
794
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
795
|
+
}
|
|
796
|
+
return await response.json();
|
|
797
|
+
}
|
|
798
|
+
async reconcileWorkspace(fleetId) {
|
|
799
|
+
assertSafeId(fleetId, "fleetId");
|
|
800
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/workspace/reconcile`, { method: "POST" });
|
|
801
|
+
if (!response.ok) {
|
|
802
|
+
const body = await response.text();
|
|
803
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
804
|
+
}
|
|
805
|
+
return await response.json();
|
|
806
|
+
}
|
|
807
|
+
async *dispatchExecStream(fleetId, command, options) {
|
|
808
|
+
assertSafeId(fleetId, "fleetId");
|
|
809
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/dispatch/stream`, {
|
|
810
|
+
method: "POST",
|
|
811
|
+
headers: { Accept: "text/event-stream" },
|
|
812
|
+
signal: options?.signal,
|
|
813
|
+
body: JSON.stringify(execDispatchBody(command, options))
|
|
814
|
+
});
|
|
815
|
+
if (!response.ok) {
|
|
816
|
+
const body = await response.text();
|
|
817
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
818
|
+
}
|
|
819
|
+
yield* parseSSEStream(response.body, { signal: options?.signal });
|
|
820
|
+
}
|
|
821
|
+
async *dispatchPromptStream(fleetId, message, options) {
|
|
822
|
+
assertSafeId(fleetId, "fleetId");
|
|
823
|
+
const response = await this.client.fetch(`/v1/fleets/${fleetId}/dispatch/stream`, {
|
|
824
|
+
method: "POST",
|
|
825
|
+
headers: { Accept: "text/event-stream" },
|
|
826
|
+
signal: options?.signal,
|
|
827
|
+
body: JSON.stringify(promptDispatchBody(message, options))
|
|
828
|
+
});
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
const body = await response.text();
|
|
831
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
832
|
+
}
|
|
833
|
+
yield* parseSSEStream(response.body, { signal: options?.signal });
|
|
834
|
+
}
|
|
835
|
+
async reapExpired(options) {
|
|
836
|
+
const response = await this.client.fetch("/v1/fleets/reap-expired", {
|
|
837
|
+
method: "POST",
|
|
838
|
+
body: JSON.stringify(options ?? {})
|
|
839
|
+
});
|
|
840
|
+
if (!response.ok) {
|
|
841
|
+
const body = await response.text();
|
|
842
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
843
|
+
}
|
|
844
|
+
return await response.json();
|
|
845
|
+
}
|
|
846
|
+
async hydrateServerFleet(fleet, options) {
|
|
847
|
+
const machines = await Promise.all(fleet.machines.map(async (machine) => {
|
|
848
|
+
const box = await this.client.get(machine.sandboxId);
|
|
849
|
+
if (!box) throw new ValidationError(`Fleet machine '${machine.machineId}' points at missing sandbox '${machine.sandboxId}'`, { machineId: "missing_sandbox" });
|
|
850
|
+
return {
|
|
851
|
+
machineId: machine.machineId,
|
|
852
|
+
sandbox: box.toJSON(),
|
|
853
|
+
role: machine.role
|
|
854
|
+
};
|
|
855
|
+
}));
|
|
856
|
+
const filtered = options.status ? machines.filter((machine) => machine.sandbox.status === options.status) : machines;
|
|
857
|
+
return {
|
|
858
|
+
fleetId: fleet.id,
|
|
859
|
+
machines: filtered
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
async listBySandboxMetadata(options) {
|
|
863
|
+
const machines = (await this.client.list(options)).map((box) => box.toJSON()).filter((info) => getFleetId(info) === options.fleetId).flatMap((info) => {
|
|
864
|
+
const machineId = getMachineId(info);
|
|
865
|
+
return machineId ? [{
|
|
866
|
+
machineId,
|
|
867
|
+
sandbox: info,
|
|
868
|
+
role: getMachineRole(info)
|
|
869
|
+
}] : [];
|
|
870
|
+
});
|
|
871
|
+
return this.fromInfo({
|
|
872
|
+
fleetId: options.fleetId,
|
|
873
|
+
machines
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
function getMachineDescriptor(options, machineId) {
|
|
878
|
+
const spec = options.machines.find((machine) => machine.machineId === machineId);
|
|
879
|
+
if (!spec) return {};
|
|
880
|
+
const merged = mergeCreateOptions(options.defaults, spec);
|
|
881
|
+
return {
|
|
882
|
+
driverType: merged.driver?.type,
|
|
883
|
+
image: merged.image,
|
|
884
|
+
environment: merged.environment,
|
|
885
|
+
templateId: merged.templateId,
|
|
886
|
+
publicTemplateId: merged.publicTemplateId,
|
|
887
|
+
acceleratorCount: merged.resources?.accelerator?.count ?? (merged.resources?.accelerator ? 1 : void 0)
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
async function mapConcurrent(items, concurrency, fn) {
|
|
891
|
+
const results = new Array(items.length);
|
|
892
|
+
let index = 0;
|
|
893
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
894
|
+
while (index < items.length) {
|
|
895
|
+
const current = index;
|
|
896
|
+
index += 1;
|
|
897
|
+
const item = items[current];
|
|
898
|
+
if (item === void 0) throw new Error(`Missing fleet item at index ${current}`);
|
|
899
|
+
results[current] = await fn(item);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
await Promise.all(workers);
|
|
903
|
+
return results;
|
|
904
|
+
}
|
|
905
|
+
function assertArtifactPath(path) {
|
|
906
|
+
if (path !== DEFAULT_ARTIFACT_ROOT && !path.startsWith(`${DEFAULT_ARTIFACT_ROOT}/`)) throw new ValidationError(`Fleet artifact path '${path}' must be under ${DEFAULT_ARTIFACT_ROOT}`, { path: "outside_workspace" });
|
|
907
|
+
if (path.includes("\0") || path.split("/").includes("..")) throw new ValidationError(`Fleet artifact path '${path}' must not contain traversal segments`, { path: "invalid" });
|
|
908
|
+
}
|
|
909
|
+
function assertArtifactSize(content, artifact) {
|
|
910
|
+
const maxBytes = artifact.maxBytes ?? DEFAULT_MAX_ARTIFACT_BYTES;
|
|
911
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0) throw new ValidationError("artifact.maxBytes must be a positive number", { maxBytes: "invalid" });
|
|
912
|
+
if (new TextEncoder().encode(content).byteLength > maxBytes) throw new ValidationError(`Fleet artifact '${artifact.path}' exceeded ${maxBytes} bytes`, { path: "too_large" });
|
|
913
|
+
}
|
|
914
|
+
function getMachineRole(info) {
|
|
915
|
+
const value = info.metadata?.[MACHINE_ROLE_META];
|
|
916
|
+
return value === "coordinator" || value === "worker" ? value : void 0;
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/client.ts
|
|
920
|
+
/**
|
|
921
|
+
* Sandbox Client
|
|
922
|
+
*
|
|
923
|
+
* Main client class for interacting with the Sandbox API.
|
|
924
|
+
*/
|
|
925
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
926
|
+
/**
|
|
927
|
+
* Default time budget for `create()` — the full create-and-reach-running
|
|
928
|
+
* flow. Cold provisions (image pull + container create + sidecar boot)
|
|
929
|
+
* routinely take ~25–40s, well past the generic 30s request timeout, so
|
|
930
|
+
* the create POST and the poll-to-running ride this higher ceiling
|
|
931
|
+
* instead. A POST that rides the deadline polls the box to `running`
|
|
932
|
+
* rather than re-POSTing (the idempotency key already makes a re-POST
|
|
933
|
+
* safe server-side, but polling is cheaper and avoids duplicate work).
|
|
934
|
+
*/
|
|
935
|
+
const DEFAULT_CREATE_TIMEOUT_MS = 12e4;
|
|
936
|
+
/**
|
|
937
|
+
* Extra grace the SDK's client-side batch deadline gets on top of
|
|
938
|
+
* the server-side `timeoutMs`. The server's clock starts when the
|
|
939
|
+
* HTTP request lands on the server; the SDK's `AbortSignal.timeout`
|
|
940
|
+
* clock starts before the TCP handshake. Without a grace buffer,
|
|
941
|
+
* the two deadlines line up almost exactly — and a batch that legitimately
|
|
942
|
+
* runs to the full `timeoutMs` on the server will race the SDK
|
|
943
|
+
* deadline firing as the final `batch.completed` SSE event is still
|
|
944
|
+
* in transit, making the caller throw `AbortError` and drop the
|
|
945
|
+
* result.
|
|
946
|
+
*
|
|
947
|
+
* The client deadline is purely a safety valve against hung
|
|
948
|
+
* connections, not a batch correctness timer — the server enforces
|
|
949
|
+
* the real `timeoutMs`. 30s is enough to cover TCP/TLS setup, the
|
|
950
|
+
* server-side batch bookkeeping, and SSE teardown, while still
|
|
951
|
+
* shutting down quickly on a truly hung stream.
|
|
952
|
+
*/
|
|
953
|
+
const CLIENT_DEADLINE_GRACE_MS = 3e4;
|
|
954
|
+
function isLocalSandboxEndpoint(baseUrl) {
|
|
955
|
+
try {
|
|
956
|
+
const { hostname } = new URL(baseUrl);
|
|
957
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
958
|
+
} catch {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function generateIdempotencyKey() {
|
|
963
|
+
const c = globalThis.crypto;
|
|
964
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
965
|
+
if (c?.getRandomValues) {
|
|
966
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
967
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
968
|
+
}
|
|
969
|
+
throw new Error("idempotency key generation requires global crypto");
|
|
970
|
+
}
|
|
971
|
+
function withRequestContextHeaders(response, path, method) {
|
|
972
|
+
const headers = new Headers(response.headers);
|
|
973
|
+
headers.set("x-tangle-request-path", path);
|
|
974
|
+
if (method) headers.set("x-tangle-request-method", method);
|
|
975
|
+
return new Response(response.body, {
|
|
976
|
+
status: response.status,
|
|
977
|
+
statusText: response.statusText,
|
|
978
|
+
headers
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
async function resolveLocalCliAuthFiles(backendType) {
|
|
982
|
+
try {
|
|
983
|
+
const { discoverLocalCliAuthFiles } = await importLocalCliRegistry();
|
|
984
|
+
return discoverLocalCliAuthFiles(backendType);
|
|
985
|
+
} catch {
|
|
986
|
+
return discoverLocalCliAuthFilesFallback(backendType);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function discoverLocalCliAuthFilesFallback(backendType) {
|
|
990
|
+
try {
|
|
991
|
+
const [{ existsSync, readFileSync }, { homedir }, { join }] = await Promise.all([
|
|
992
|
+
import("node:fs"),
|
|
993
|
+
import("node:os"),
|
|
994
|
+
import("node:path")
|
|
995
|
+
]);
|
|
996
|
+
const homeDir = homedir();
|
|
997
|
+
const authFiles = (backendType === "codex" ? [".codex/auth.json"] : [".claude/.credentials.json", ".claude/settings.json"]).flatMap((path) => {
|
|
998
|
+
const absolutePath = join(homeDir, path);
|
|
999
|
+
if (!existsSync(absolutePath)) return [];
|
|
1000
|
+
return [{
|
|
1001
|
+
path,
|
|
1002
|
+
content: readFileSync(absolutePath, "utf8"),
|
|
1003
|
+
mode: 384
|
|
1004
|
+
}];
|
|
1005
|
+
});
|
|
1006
|
+
return authFiles.length ? authFiles : void 0;
|
|
1007
|
+
} catch {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function describeLocalCliAuthExpectation(backendType) {
|
|
1012
|
+
try {
|
|
1013
|
+
return (await importLocalCliRegistry()).describeLocalCliAuthExpectation(backendType);
|
|
1014
|
+
} catch {
|
|
1015
|
+
return backendType === "codex" ? "~/.codex/auth.json" : "~/.claude/.credentials.json or ~/.claude/settings.json";
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async function importLocalCliRegistry() {
|
|
1019
|
+
return await import(
|
|
1020
|
+
/* @vite-ignore */
|
|
1021
|
+
["@tangle-network", "cli-agent-registry"].join("/")
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
async function hydrateLocalCliBackendAuth(backend, baseUrl, trustLocalCliAuth) {
|
|
1025
|
+
if (!backend) return void 0;
|
|
1026
|
+
if (!isLocalSandboxEndpoint(baseUrl)) return backend;
|
|
1027
|
+
if (backend.type !== "codex" && backend.type !== "claude-code") return backend;
|
|
1028
|
+
if (backend.model?.authFiles?.length) return backend;
|
|
1029
|
+
if (backend.model?.apiKey) return backend;
|
|
1030
|
+
if (backend.model?.authMode === "api-key") return backend;
|
|
1031
|
+
if (!trustLocalCliAuth) {
|
|
1032
|
+
const cliName = backend.type === "codex" ? "codex" : "claude";
|
|
1033
|
+
throw new AuthError(`Local ${backend.type} backend requested for ${baseUrl}, but the SDK will not read CLI credentials from the host home directory unless you opt in by constructing the client with \`new Sandbox({ ..., trustLocalCliAuth: true })\`. Only opt in when the localhost endpoint is one you control. Otherwise, pass \`backend.model.apiKey\` / \`backend.model.authFiles\` explicitly, or run \`${cliName} login\` inside the sandbox.`);
|
|
1034
|
+
}
|
|
1035
|
+
const authFiles = await resolveLocalCliAuthFiles(backend.type);
|
|
1036
|
+
if (!authFiles?.length) {
|
|
1037
|
+
const cliName = backend.type === "codex" ? "codex" : "claude";
|
|
1038
|
+
const expectation = await describeLocalCliAuthExpectation(backend.type);
|
|
1039
|
+
throw new AuthError(`Local ${backend.type} backend requested for ${baseUrl}, but no local CLI auth was found. Run \`${cliName} login\` on the host or provide backend.model.apiKey/authFiles explicitly. Expected auth under ${expectation}.`);
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
...backend,
|
|
1043
|
+
model: {
|
|
1044
|
+
...backend.model,
|
|
1045
|
+
authMode: backend.model?.authMode ?? "oauth",
|
|
1046
|
+
authFiles
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Client for the Tangle Sandbox platform.
|
|
1052
|
+
*
|
|
1053
|
+
* @example
|
|
1054
|
+
* ```typescript
|
|
1055
|
+
* import { Sandbox } from "@tangle-network/sandbox";
|
|
1056
|
+
*
|
|
1057
|
+
* const client = new Sandbox({
|
|
1058
|
+
* apiKey: "sk_sandbox_...",
|
|
1059
|
+
* baseUrl: "https://your-sandbox-api.example.com",
|
|
1060
|
+
* });
|
|
1061
|
+
*
|
|
1062
|
+
* // Create a sandbox
|
|
1063
|
+
* const box = await client.create({
|
|
1064
|
+
* name: "my-project",
|
|
1065
|
+
* sshEnabled: true,
|
|
1066
|
+
* });
|
|
1067
|
+
*
|
|
1068
|
+
* // Execute commands
|
|
1069
|
+
* const result = await box.exec("npm install");
|
|
1070
|
+
*
|
|
1071
|
+
* // Clean up
|
|
1072
|
+
* await box.delete();
|
|
1073
|
+
* ```
|
|
1074
|
+
*/
|
|
1075
|
+
var SandboxClient = class {
|
|
1076
|
+
baseUrl;
|
|
1077
|
+
apiKey;
|
|
1078
|
+
timeoutMs;
|
|
1079
|
+
trustLocalCliAuth;
|
|
1080
|
+
_secrets = null;
|
|
1081
|
+
_sshKeys = null;
|
|
1082
|
+
_fleets = null;
|
|
1083
|
+
_intelligence = null;
|
|
1084
|
+
constructor(config) {
|
|
1085
|
+
if (!config.apiKey) throw new AuthError("API key is required");
|
|
1086
|
+
if (!config.baseUrl) throw new ValidationError("Base URL is required");
|
|
1087
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
1088
|
+
this.apiKey = config.apiKey;
|
|
1089
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1090
|
+
this.trustLocalCliAuth = config.trustLocalCliAuth === true;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Fleet operations for agent-managed groups of sandboxes.
|
|
1094
|
+
*
|
|
1095
|
+
* Fleets are an SDK-level convenience over the existing sandbox lifecycle
|
|
1096
|
+
* API. Each worker is a normal sandbox tagged with stable fleet metadata,
|
|
1097
|
+
* so older clients and dashboards can still inspect and delete them.
|
|
1098
|
+
*/
|
|
1099
|
+
get fleets() {
|
|
1100
|
+
if (!this._fleets) this._fleets = new SandboxFleetClient(this);
|
|
1101
|
+
return this._fleets;
|
|
1102
|
+
}
|
|
1103
|
+
get intelligence() {
|
|
1104
|
+
if (!this._intelligence) this._intelligence = new IntelligenceClient(this);
|
|
1105
|
+
return this._intelligence;
|
|
1106
|
+
}
|
|
1107
|
+
getApiKey() {
|
|
1108
|
+
return this.apiKey;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Access the secrets manager for storing and retrieving encrypted secrets.
|
|
1112
|
+
*
|
|
1113
|
+
* @example
|
|
1114
|
+
* ```typescript
|
|
1115
|
+
* // Create a secret
|
|
1116
|
+
* await client.secrets.create("HF_TOKEN", "hf_xxx");
|
|
1117
|
+
*
|
|
1118
|
+
* // List secrets (names only)
|
|
1119
|
+
* const secrets = await client.secrets.list();
|
|
1120
|
+
*
|
|
1121
|
+
* // Get secret value
|
|
1122
|
+
* const value = await client.secrets.get("HF_TOKEN");
|
|
1123
|
+
*
|
|
1124
|
+
* // Update secret
|
|
1125
|
+
* await client.secrets.update("HF_TOKEN", "hf_new_value");
|
|
1126
|
+
*
|
|
1127
|
+
* // Delete secret
|
|
1128
|
+
* await client.secrets.delete("HF_TOKEN");
|
|
1129
|
+
* ```
|
|
1130
|
+
*/
|
|
1131
|
+
get secrets() {
|
|
1132
|
+
if (!this._secrets) this._secrets = new SecretsManagerImpl(this);
|
|
1133
|
+
return this._secrets;
|
|
1134
|
+
}
|
|
1135
|
+
get sshKeys() {
|
|
1136
|
+
if (!this._sshKeys) this._sshKeys = new SshKeysManagerImpl(this);
|
|
1137
|
+
return this._sshKeys;
|
|
1138
|
+
}
|
|
1139
|
+
_environments = null;
|
|
1140
|
+
/**
|
|
1141
|
+
* Access available development environments.
|
|
1142
|
+
*
|
|
1143
|
+
* @example
|
|
1144
|
+
* ```typescript
|
|
1145
|
+
* const envs = await client.environments.list();
|
|
1146
|
+
* // → [{ id: "universal", description: "Universal environment with all toolchains via Nix", ... }]
|
|
1147
|
+
*
|
|
1148
|
+
* const sandbox = await client.create({ environment: "universal" });
|
|
1149
|
+
* ```
|
|
1150
|
+
*/
|
|
1151
|
+
get environments() {
|
|
1152
|
+
if (!this._environments) this._environments = new EnvironmentsClient(this);
|
|
1153
|
+
return this._environments;
|
|
1154
|
+
}
|
|
1155
|
+
_teams = null;
|
|
1156
|
+
_publicTemplates = null;
|
|
1157
|
+
/**
|
|
1158
|
+
* Access team management (collaboration groups, invitations,
|
|
1159
|
+
* member roles). Teams scope shared sandboxes — pass `teamId` to
|
|
1160
|
+
* `client.create()` to create a sandbox accessible to a team's
|
|
1161
|
+
* members.
|
|
1162
|
+
*
|
|
1163
|
+
* @example
|
|
1164
|
+
* ```typescript
|
|
1165
|
+
* const teams = await client.teams.list();
|
|
1166
|
+
* const invite = await client.teams.invite(teams[0].id, {
|
|
1167
|
+
* email: "alice@example.com",
|
|
1168
|
+
* role: "member",
|
|
1169
|
+
* });
|
|
1170
|
+
* ```
|
|
1171
|
+
*/
|
|
1172
|
+
get teams() {
|
|
1173
|
+
if (!this._teams) this._teams = new TeamsClient(this);
|
|
1174
|
+
return this._teams;
|
|
1175
|
+
}
|
|
1176
|
+
get publicTemplates() {
|
|
1177
|
+
if (!this._publicTemplates) this._publicTemplates = new PublicTemplatesClient(this);
|
|
1178
|
+
return this._publicTemplates;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Create a new sandbox.
|
|
1182
|
+
*
|
|
1183
|
+
* @param options - Configuration for the new sandbox
|
|
1184
|
+
* @returns A SandboxInstance representing the created sandbox
|
|
1185
|
+
*
|
|
1186
|
+
* @example
|
|
1187
|
+
* ```typescript
|
|
1188
|
+
* const box = await client.create({
|
|
1189
|
+
* name: "my-project",
|
|
1190
|
+
* environment: "universal",
|
|
1191
|
+
* sshEnabled: true,
|
|
1192
|
+
* env: { NODE_ENV: "development" },
|
|
1193
|
+
* });
|
|
1194
|
+
* ```
|
|
1195
|
+
*/
|
|
1196
|
+
async create(options, requestOptions) {
|
|
1197
|
+
const timeoutMs = requestOptions?.timeoutMs ?? DEFAULT_CREATE_TIMEOUT_MS;
|
|
1198
|
+
const idempotencyKey = options?.idempotencyKey ?? generateIdempotencyKey();
|
|
1199
|
+
const deadline = Date.now() + timeoutMs;
|
|
1200
|
+
const git = options?.git ? {
|
|
1201
|
+
url: options.git.url,
|
|
1202
|
+
ref: options.git.ref,
|
|
1203
|
+
depth: options.git.depth,
|
|
1204
|
+
sparse: options.git.sparse,
|
|
1205
|
+
auth: options.git.auth
|
|
1206
|
+
} : void 0;
|
|
1207
|
+
const resolvedEnvironment = options?.environment ?? options?.image;
|
|
1208
|
+
const backend = await hydrateLocalCliBackendAuth(options?.backend, this.baseUrl, this.trustLocalCliAuth);
|
|
1209
|
+
const resolvedBackend = backend ? {
|
|
1210
|
+
...backend,
|
|
1211
|
+
type: backend.type ?? "opencode"
|
|
1212
|
+
} : void 0;
|
|
1213
|
+
const resources = normalizeSandboxResources(options?.resources, options?.driver);
|
|
1214
|
+
const postDeadline = AbortSignal.timeout(timeoutMs);
|
|
1215
|
+
const postSignal = requestOptions?.signal ? AbortSignal.any([requestOptions.signal, postDeadline]) : postDeadline;
|
|
1216
|
+
let response;
|
|
1217
|
+
try {
|
|
1218
|
+
response = await this.fetch("/v1/sandboxes", {
|
|
1219
|
+
method: "POST",
|
|
1220
|
+
headers: { "Idempotency-Key": idempotencyKey },
|
|
1221
|
+
signal: postSignal,
|
|
1222
|
+
body: JSON.stringify({
|
|
1223
|
+
name: options?.name,
|
|
1224
|
+
...resolvedEnvironment !== void 0 ? {
|
|
1225
|
+
environment: resolvedEnvironment,
|
|
1226
|
+
image: resolvedEnvironment
|
|
1227
|
+
} : {},
|
|
1228
|
+
git,
|
|
1229
|
+
tools: options?.tools,
|
|
1230
|
+
bare: options?.bare,
|
|
1231
|
+
driver: options?.driver,
|
|
1232
|
+
backend: resolvedBackend,
|
|
1233
|
+
permissions: options?.permissions,
|
|
1234
|
+
env: options?.env,
|
|
1235
|
+
resources,
|
|
1236
|
+
sshEnabled: options?.sshEnabled,
|
|
1237
|
+
sshPublicKeys: options?.sshPublicKeys ?? (options?.sshPublicKey ? [options.sshPublicKey] : void 0),
|
|
1238
|
+
sshKeyIds: options?.sshKeyIds,
|
|
1239
|
+
webTerminalEnabled: options?.webTerminalEnabled,
|
|
1240
|
+
maxLifetimeSeconds: options?.maxLifetimeSeconds,
|
|
1241
|
+
idleTimeoutSeconds: options?.idleTimeoutSeconds,
|
|
1242
|
+
storage: options?.storage,
|
|
1243
|
+
fromSnapshot: options?.fromSnapshot,
|
|
1244
|
+
fromSandboxId: options?.fromSandboxId,
|
|
1245
|
+
templateId: options?.templateId,
|
|
1246
|
+
publicTemplateId: options?.publicTemplateId,
|
|
1247
|
+
publicTemplateVersionId: options?.publicTemplateVersionId,
|
|
1248
|
+
secrets: options?.secrets,
|
|
1249
|
+
confidential: options?.confidential,
|
|
1250
|
+
metadata: options?.metadata,
|
|
1251
|
+
teamId: options?.teamId,
|
|
1252
|
+
capabilities: options?.capabilities,
|
|
1253
|
+
privacy: options?.privacy,
|
|
1254
|
+
egressPolicy: options?.egressPolicy,
|
|
1255
|
+
idempotencyKey
|
|
1256
|
+
})
|
|
1257
|
+
});
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
if (requestOptions?.signal?.aborted) throw err;
|
|
1260
|
+
if (err instanceof TimeoutError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) throw new TimeoutError(timeoutMs, `Sandbox create did not complete within ${timeoutMs}ms. The provision may still be running server-side; retry create() with idempotencyKey="${idempotencyKey}" to join it.`);
|
|
1261
|
+
throw err;
|
|
1262
|
+
}
|
|
1263
|
+
if (!response.ok) {
|
|
1264
|
+
const body = await response.text();
|
|
1265
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1266
|
+
}
|
|
1267
|
+
const data = await response.json();
|
|
1268
|
+
const instance = new SandboxInstance(this, this.parseInfo(data), resolvedBackend);
|
|
1269
|
+
if (instance.status === "provisioning" || instance.status === "pending") {
|
|
1270
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
1271
|
+
await instance.waitFor("running", {
|
|
1272
|
+
timeoutMs: remainingMs,
|
|
1273
|
+
signal: requestOptions?.signal
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
return instance;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Poll a sandbox to `running` by id. Surfaces a real provision failure
|
|
1280
|
+
* as a {@link StateError} (carrying the sandbox's error message) rather
|
|
1281
|
+
* than an opaque timeout, and throws {@link NotFoundError} if the id is
|
|
1282
|
+
* unknown. Use after a {@link create} that timed out client-side but may
|
|
1283
|
+
* have continued provisioning server-side under its idempotency key.
|
|
1284
|
+
*/
|
|
1285
|
+
async waitForRunning(id, options) {
|
|
1286
|
+
const instance = await this.get(id);
|
|
1287
|
+
if (!instance) throw new NotFoundError("Sandbox", id, {
|
|
1288
|
+
endpoint: `/v1/sandboxes/${id}`,
|
|
1289
|
+
origin: "sandbox-api"
|
|
1290
|
+
});
|
|
1291
|
+
await instance.waitFor("running", {
|
|
1292
|
+
timeoutMs: options?.timeoutMs ?? DEFAULT_CREATE_TIMEOUT_MS,
|
|
1293
|
+
signal: options?.signal
|
|
1294
|
+
});
|
|
1295
|
+
return instance;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* List all sandboxes.
|
|
1299
|
+
*
|
|
1300
|
+
* @param options - Filtering and pagination options
|
|
1301
|
+
* @returns Array of SandboxInstance objects
|
|
1302
|
+
*
|
|
1303
|
+
* @example
|
|
1304
|
+
* ```typescript
|
|
1305
|
+
* // List all running sandboxes
|
|
1306
|
+
* const running = await client.list({ status: "running" });
|
|
1307
|
+
*
|
|
1308
|
+
* // List with pagination
|
|
1309
|
+
* const page = await client.list({ limit: 10, offset: 0 });
|
|
1310
|
+
* ```
|
|
1311
|
+
*/
|
|
1312
|
+
async list(options) {
|
|
1313
|
+
const params = new URLSearchParams();
|
|
1314
|
+
if (options?.status) {
|
|
1315
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
1316
|
+
for (const s of statuses) params.append("status", s);
|
|
1317
|
+
}
|
|
1318
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
1319
|
+
if (options?.offset !== void 0) params.set("offset", String(options.offset));
|
|
1320
|
+
if (options?.scope !== void 0) params.set("scope", options.scope);
|
|
1321
|
+
const query = params.toString();
|
|
1322
|
+
const path = query ? `/v1/sandboxes?${query}` : "/v1/sandboxes";
|
|
1323
|
+
const response = await this.fetch(path);
|
|
1324
|
+
if (!response.ok) {
|
|
1325
|
+
const body = await response.text();
|
|
1326
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1327
|
+
}
|
|
1328
|
+
const data = await response.json();
|
|
1329
|
+
return (Array.isArray(data) ? data : data.sandboxes ?? []).map((item) => new SandboxInstance(this, this.parseInfo(item)));
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Get a sandbox by ID.
|
|
1333
|
+
*
|
|
1334
|
+
* @param id - The sandbox ID
|
|
1335
|
+
* @returns A SandboxInstance or null if not found
|
|
1336
|
+
*
|
|
1337
|
+
* @example
|
|
1338
|
+
* ```typescript
|
|
1339
|
+
* const box = await client.get("sandbox_abc123");
|
|
1340
|
+
* if (box) {
|
|
1341
|
+
* console.log(box.status);
|
|
1342
|
+
* }
|
|
1343
|
+
* ```
|
|
1344
|
+
*/
|
|
1345
|
+
async get(id) {
|
|
1346
|
+
const response = await this.fetch(`/v1/sandboxes/${encodeURIComponent(id)}`);
|
|
1347
|
+
if (response.status === 404) return null;
|
|
1348
|
+
if (!response.ok) {
|
|
1349
|
+
const body = await response.text();
|
|
1350
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1351
|
+
}
|
|
1352
|
+
const data = await response.json();
|
|
1353
|
+
return new SandboxInstance(this, this.parseInfo(data));
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Get usage information for the account.
|
|
1357
|
+
*
|
|
1358
|
+
* @returns Usage statistics for the current billing period
|
|
1359
|
+
*
|
|
1360
|
+
* @example
|
|
1361
|
+
* ```typescript
|
|
1362
|
+
* const usage = await client.usage();
|
|
1363
|
+
* console.log(`Active sandboxes: ${usage.activeSandboxes}`);
|
|
1364
|
+
* console.log(`Compute minutes: ${usage.computeMinutes}`);
|
|
1365
|
+
* ```
|
|
1366
|
+
*/
|
|
1367
|
+
async usage() {
|
|
1368
|
+
const response = await this.fetch("/usage");
|
|
1369
|
+
if (!response.ok) {
|
|
1370
|
+
const body = await response.text();
|
|
1371
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1372
|
+
}
|
|
1373
|
+
const data = await response.json();
|
|
1374
|
+
return {
|
|
1375
|
+
computeMinutes: data.computeMinutes ?? 0,
|
|
1376
|
+
activeSandboxes: data.activeSandboxes ?? 0,
|
|
1377
|
+
totalSandboxes: data.totalSandboxes ?? 0,
|
|
1378
|
+
periodStart: new Date(data.periodStart),
|
|
1379
|
+
periodEnd: new Date(data.periodEnd)
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Get subscription and billing information for the account.
|
|
1384
|
+
*
|
|
1385
|
+
* @returns Subscription details including plan, credits, and limits
|
|
1386
|
+
*
|
|
1387
|
+
* @example
|
|
1388
|
+
* ```typescript
|
|
1389
|
+
* const sub = await client.subscription();
|
|
1390
|
+
* console.log(`Plan: ${sub.plan}`);
|
|
1391
|
+
* console.log(`Credits: $${sub.creditsAvailableUsd.toFixed(2)}`);
|
|
1392
|
+
* ```
|
|
1393
|
+
*/
|
|
1394
|
+
async subscription() {
|
|
1395
|
+
const response = await this.fetch("/subscription");
|
|
1396
|
+
if (!response.ok) {
|
|
1397
|
+
const body = await response.text();
|
|
1398
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1399
|
+
}
|
|
1400
|
+
const data = await response.json();
|
|
1401
|
+
return {
|
|
1402
|
+
plan: data.plan ?? "free",
|
|
1403
|
+
status: data.status ?? "active",
|
|
1404
|
+
creditsAvailableUsd: data.creditsAvailableUsd ?? 0,
|
|
1405
|
+
creditsUsedUsd: data.creditsUsedUsd ?? 0,
|
|
1406
|
+
monthlyBalanceUsd: data.monthlyBalanceUsd ?? 0,
|
|
1407
|
+
maxConcurrentSandboxes: data.maxConcurrentSandboxes ?? 0,
|
|
1408
|
+
overageAllowed: data.overageAllowed ?? false,
|
|
1409
|
+
limits: data.limits ?? {
|
|
1410
|
+
maxCpuCores: 1,
|
|
1411
|
+
maxRamGB: 2,
|
|
1412
|
+
maxStorageGB: 50
|
|
1413
|
+
},
|
|
1414
|
+
currentPeriodEnd: data.currentPeriodEnd ?? 0
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Check if the Sandbox API is available.
|
|
1419
|
+
*
|
|
1420
|
+
* @returns true if the API is healthy, false otherwise
|
|
1421
|
+
*/
|
|
1422
|
+
async health() {
|
|
1423
|
+
try {
|
|
1424
|
+
return (await this.fetch("/health")).ok;
|
|
1425
|
+
} catch {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Check if CRIU checkpointing is available on the platform.
|
|
1431
|
+
*
|
|
1432
|
+
* CRIU enables memory preservation for true pause/resume and fork operations.
|
|
1433
|
+
* It requires specific host configuration.
|
|
1434
|
+
*
|
|
1435
|
+
* @returns CRIU availability status
|
|
1436
|
+
*
|
|
1437
|
+
* @example
|
|
1438
|
+
* ```typescript
|
|
1439
|
+
* const status = await client.criuStatus();
|
|
1440
|
+
* if (status.available) {
|
|
1441
|
+
* console.log(`CRIU ${status.criuVersion} available`);
|
|
1442
|
+
* } else {
|
|
1443
|
+
* console.log(`CRIU not available: ${status.reason}`);
|
|
1444
|
+
* }
|
|
1445
|
+
* ```
|
|
1446
|
+
*/
|
|
1447
|
+
async criuStatus() {
|
|
1448
|
+
const response = await this.fetch("/v1/system/criu-status");
|
|
1449
|
+
if (!response.ok) {
|
|
1450
|
+
const body = await response.text();
|
|
1451
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1452
|
+
}
|
|
1453
|
+
return await response.json();
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Run multiple tasks in parallel across sandboxes.
|
|
1457
|
+
* Returns the aggregated results after all tasks complete.
|
|
1458
|
+
*
|
|
1459
|
+
* @param tasks - Array of tasks to execute
|
|
1460
|
+
* @param options - Batch execution options
|
|
1461
|
+
* @returns Aggregated batch results
|
|
1462
|
+
*
|
|
1463
|
+
* @throws {@link TimeoutError} if the server hasn't begun responding
|
|
1464
|
+
* before the deadline (pre-stream timeout).
|
|
1465
|
+
* @throws {@link ValidationError} / {@link QuotaError} / other typed
|
|
1466
|
+
* SDK errors if the server rejects the request before streaming.
|
|
1467
|
+
* @throws `DOMException` with `name === "AbortError"` if
|
|
1468
|
+
* `options.signal` fires OR the safety-valve deadline fires
|
|
1469
|
+
* AFTER streaming has begun — parseSSEStream can't distinguish
|
|
1470
|
+
* the source mid-stream. Callers that need to tell cancel from
|
|
1471
|
+
* timeout should check `options.signal?.aborted` in their catch.
|
|
1472
|
+
* @throws `Error` with the server message if the stream emits a
|
|
1473
|
+
* `batch.failed` event.
|
|
1474
|
+
*
|
|
1475
|
+
* @example
|
|
1476
|
+
* ```typescript
|
|
1477
|
+
* const result = await client.runBatch([
|
|
1478
|
+
* { id: "task-1", message: "Analyze this file" },
|
|
1479
|
+
* { id: "task-2", message: "Generate a summary" },
|
|
1480
|
+
* ]);
|
|
1481
|
+
* console.log(`Success rate: ${result.successRate}%`);
|
|
1482
|
+
* ```
|
|
1483
|
+
*/
|
|
1484
|
+
async runBatch(tasks, options) {
|
|
1485
|
+
const results = [];
|
|
1486
|
+
let totalRetries = 0;
|
|
1487
|
+
for await (const event of this.streamBatch(tasks, options)) {
|
|
1488
|
+
if (event.type === "task.completed") {
|
|
1489
|
+
const data = event.data;
|
|
1490
|
+
const usageTotal = (data.usage?.inputTokens ?? 0) + (data.usage?.outputTokens ?? 0);
|
|
1491
|
+
results.push({
|
|
1492
|
+
taskId: data.taskId,
|
|
1493
|
+
success: true,
|
|
1494
|
+
response: data.resultSummary ?? data.response,
|
|
1495
|
+
durationMs: data.durationMs ?? 0,
|
|
1496
|
+
retries: data.retries ?? 0,
|
|
1497
|
+
tokensUsed: data.tokensUsed ?? (usageTotal > 0 ? usageTotal : void 0)
|
|
1498
|
+
});
|
|
1499
|
+
totalRetries += data.retries ?? 0;
|
|
1500
|
+
}
|
|
1501
|
+
if (event.type === "task.failed") {
|
|
1502
|
+
const data = event.data;
|
|
1503
|
+
results.push({
|
|
1504
|
+
taskId: data.taskId,
|
|
1505
|
+
success: false,
|
|
1506
|
+
error: data.error,
|
|
1507
|
+
durationMs: data.durationMs ?? 0,
|
|
1508
|
+
retries: data.retries ?? 0
|
|
1509
|
+
});
|
|
1510
|
+
totalRetries += data.retries ?? 0;
|
|
1511
|
+
}
|
|
1512
|
+
if (event.type === "batch.failed") {
|
|
1513
|
+
const data = event.data;
|
|
1514
|
+
throw new Error(data.error ?? "Batch failed");
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
1518
|
+
const failed = results.filter((r) => !r.success).length;
|
|
1519
|
+
return {
|
|
1520
|
+
totalTasks: tasks.length,
|
|
1521
|
+
succeeded,
|
|
1522
|
+
failed,
|
|
1523
|
+
totalRetries,
|
|
1524
|
+
successRate: tasks.length > 0 ? succeeded / tasks.length * 100 : 0,
|
|
1525
|
+
results
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Stream events from a batch execution.
|
|
1530
|
+
* Use this for real-time progress updates during batch processing.
|
|
1531
|
+
*
|
|
1532
|
+
* @param tasks - Array of tasks to execute
|
|
1533
|
+
* @param options - Batch execution options
|
|
1534
|
+
*
|
|
1535
|
+
* @throws {@link TimeoutError} if the server hasn't begun responding
|
|
1536
|
+
* before the deadline (pre-stream timeout).
|
|
1537
|
+
* @throws {@link ValidationError} / {@link QuotaError} / other typed
|
|
1538
|
+
* SDK errors if the server rejects the request before streaming.
|
|
1539
|
+
* @throws `DOMException` with `name === "AbortError"` if
|
|
1540
|
+
* `options.signal` fires OR the safety-valve deadline fires
|
|
1541
|
+
* AFTER streaming has begun. Distinguish via
|
|
1542
|
+
* `options.signal?.aborted` in the consumer's catch.
|
|
1543
|
+
*
|
|
1544
|
+
* @example
|
|
1545
|
+
* ```typescript
|
|
1546
|
+
* for await (const event of client.streamBatch(tasks)) {
|
|
1547
|
+
* if (event.type === "task.completed") {
|
|
1548
|
+
* console.log(`Task ${event.data.taskId} completed`);
|
|
1549
|
+
* }
|
|
1550
|
+
* }
|
|
1551
|
+
* ```
|
|
1552
|
+
*/
|
|
1553
|
+
async *streamBatch(tasks, options) {
|
|
1554
|
+
const url = `${this.baseUrl}/batch/run`;
|
|
1555
|
+
const resolvedTimeoutMs = options?.timeoutMs ?? 3e5;
|
|
1556
|
+
const requestBody = JSON.stringify({
|
|
1557
|
+
tasks,
|
|
1558
|
+
backend: options?.backend ? {
|
|
1559
|
+
...options.backend,
|
|
1560
|
+
type: options.backend.type ?? "opencode"
|
|
1561
|
+
} : { type: "opencode" },
|
|
1562
|
+
timeoutMs: resolvedTimeoutMs,
|
|
1563
|
+
scalingMode: options?.scalingMode ?? "balanced",
|
|
1564
|
+
persistent: options?.persistent ?? false,
|
|
1565
|
+
graceMs: options?.graceMs ?? 0
|
|
1566
|
+
});
|
|
1567
|
+
const deadline = AbortSignal.timeout(resolvedTimeoutMs + CLIENT_DEADLINE_GRACE_MS);
|
|
1568
|
+
const combinedSignal = options?.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
|
|
1569
|
+
let response;
|
|
1570
|
+
try {
|
|
1571
|
+
response = await globalThis.fetch(url, {
|
|
1572
|
+
method: "POST",
|
|
1573
|
+
headers: {
|
|
1574
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1575
|
+
"Content-Type": "application/json",
|
|
1576
|
+
Accept: "text/event-stream"
|
|
1577
|
+
},
|
|
1578
|
+
body: requestBody,
|
|
1579
|
+
signal: combinedSignal
|
|
1580
|
+
});
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
if (options?.signal?.aborted) throw err instanceof Error && err.name === "AbortError" ? err : new DOMException("Batch aborted", "AbortError");
|
|
1583
|
+
if (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) throw new TimeoutError(resolvedTimeoutMs, "Batch request timed out before the first SSE event");
|
|
1584
|
+
throw new NetworkError(`Failed to connect to Sandbox API: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0, {
|
|
1585
|
+
endpoint: "/batch/run",
|
|
1586
|
+
origin: "sandbox-api"
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
if (!response.ok) {
|
|
1590
|
+
const errBody = await response.text();
|
|
1591
|
+
throw parseErrorResponse(response.status, errBody, void 0, response.headers);
|
|
1592
|
+
}
|
|
1593
|
+
yield* parseSSEStream(response.body, { signal: combinedSignal });
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Make an authenticated HTTP request to the API.
|
|
1597
|
+
* This is exposed for use by SandboxInstance.
|
|
1598
|
+
*
|
|
1599
|
+
* `fetchOptions.timeoutMs` overrides the client-wide `this.timeoutMs`
|
|
1600
|
+
* for a single request. Slow data-plane operations (snapshot create
|
|
1601
|
+
* blocks the server up to 600s while the host-agent compresses and
|
|
1602
|
+
* uploads the rootfs) must pass a higher ceiling, otherwise the
|
|
1603
|
+
* default client timeout aborts mid-operation with a `TimeoutError`
|
|
1604
|
+
* even though the server is still working.
|
|
1605
|
+
*/
|
|
1606
|
+
async fetch(path, options, fetchOptions) {
|
|
1607
|
+
const url = `${this.baseUrl}${path}`;
|
|
1608
|
+
const requestTimeoutMs = typeof fetchOptions?.timeoutMs === "number" && fetchOptions.timeoutMs > 0 ? fetchOptions.timeoutMs : this.timeoutMs;
|
|
1609
|
+
const controller = new AbortController();
|
|
1610
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
1611
|
+
try {
|
|
1612
|
+
const headers = new Headers(options?.headers);
|
|
1613
|
+
headers.set("Authorization", `Bearer ${this.apiKey}`);
|
|
1614
|
+
const isFormDataBody = typeof FormData !== "undefined" && options?.body instanceof FormData;
|
|
1615
|
+
if (!headers.has("Content-Type") && !isFormDataBody) headers.set("Content-Type", "application/json");
|
|
1616
|
+
return withRequestContextHeaders(await globalThis.fetch(url, {
|
|
1617
|
+
...options,
|
|
1618
|
+
headers,
|
|
1619
|
+
signal: options?.signal ?? controller.signal
|
|
1620
|
+
}), path, options?.method);
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
if (err instanceof Error && err.name === "AbortError") throw new TimeoutError(requestTimeoutMs);
|
|
1623
|
+
throw new NetworkError(`Failed to connect to Sandbox API: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0, {
|
|
1624
|
+
endpoint: path,
|
|
1625
|
+
origin: "sandbox-api"
|
|
1626
|
+
});
|
|
1627
|
+
} finally {
|
|
1628
|
+
clearTimeout(timeoutId);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
parseInfo(data) {
|
|
1632
|
+
return {
|
|
1633
|
+
id: data.id ?? "",
|
|
1634
|
+
name: data.name,
|
|
1635
|
+
status: data.status ?? "pending",
|
|
1636
|
+
connection: normalizeConnection(data.connection),
|
|
1637
|
+
metadata: data.metadata,
|
|
1638
|
+
createdAt: data.createdAt ? new Date(data.createdAt) : /* @__PURE__ */ new Date(),
|
|
1639
|
+
startedAt: data.startedAt ? new Date(data.startedAt) : void 0,
|
|
1640
|
+
lastActivityAt: data.lastActivityAt ? new Date(data.lastActivityAt) : void 0,
|
|
1641
|
+
expiresAt: data.expiresAt ? new Date(data.expiresAt) : void 0,
|
|
1642
|
+
error: data.error
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
_tokenRefreshHandlers = /* @__PURE__ */ new Set();
|
|
1646
|
+
/**
|
|
1647
|
+
* Register a handler invoked when the SDK transparently refreshes a
|
|
1648
|
+
* sandbox bearer after a 401 retry. Returns a function that
|
|
1649
|
+
* unregisters the handler when called.
|
|
1650
|
+
*/
|
|
1651
|
+
onTokenRefresh(handler) {
|
|
1652
|
+
this._tokenRefreshHandlers.add(handler);
|
|
1653
|
+
return () => {
|
|
1654
|
+
this._tokenRefreshHandlers.delete(handler);
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
/** @internal — fired by the runtime-fetch retry path after a successful
|
|
1658
|
+
* refresh. Errors thrown by handlers are caught and surfaced as
|
|
1659
|
+
* console warnings so one bad subscriber does not poison the rest. */
|
|
1660
|
+
_emitTokenRefresh(sandboxId, newToken) {
|
|
1661
|
+
for (const h of this._tokenRefreshHandlers) try {
|
|
1662
|
+
h(sandboxId, newToken);
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
console.warn(`[sandbox-sdk] tokenRefresh handler threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
var SecretsManagerImpl = class {
|
|
1669
|
+
constructor(client) {
|
|
1670
|
+
this.client = client;
|
|
1671
|
+
}
|
|
1672
|
+
async create(name, value) {
|
|
1673
|
+
const response = await this.client.fetch("/v1/secrets", {
|
|
1674
|
+
method: "POST",
|
|
1675
|
+
body: JSON.stringify({
|
|
1676
|
+
name,
|
|
1677
|
+
value
|
|
1678
|
+
})
|
|
1679
|
+
});
|
|
1680
|
+
if (response.status === 409) {
|
|
1681
|
+
const body = await response.text();
|
|
1682
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1683
|
+
}
|
|
1684
|
+
if (response.status === 400) {
|
|
1685
|
+
const body = await response.text();
|
|
1686
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1687
|
+
}
|
|
1688
|
+
if (!response.ok) {
|
|
1689
|
+
const body = await response.text();
|
|
1690
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1691
|
+
}
|
|
1692
|
+
const data = await response.json();
|
|
1693
|
+
return {
|
|
1694
|
+
name: data.name,
|
|
1695
|
+
createdAt: new Date(data.createdAt),
|
|
1696
|
+
updatedAt: new Date(data.updatedAt)
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
async list() {
|
|
1700
|
+
const response = await this.client.fetch("/v1/secrets");
|
|
1701
|
+
if (!response.ok) {
|
|
1702
|
+
const body = await response.text();
|
|
1703
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1704
|
+
}
|
|
1705
|
+
return ((await response.json()).secrets ?? []).map((s) => ({
|
|
1706
|
+
name: s.name,
|
|
1707
|
+
createdAt: new Date(s.createdAt),
|
|
1708
|
+
updatedAt: new Date(s.updatedAt)
|
|
1709
|
+
}));
|
|
1710
|
+
}
|
|
1711
|
+
async get(name) {
|
|
1712
|
+
const response = await this.client.fetch(`/v1/secrets/${encodeURIComponent(name)}`);
|
|
1713
|
+
if (response.status === 404) throw new NotFoundError("Secret", name);
|
|
1714
|
+
if (!response.ok) {
|
|
1715
|
+
const body = await response.text();
|
|
1716
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1717
|
+
}
|
|
1718
|
+
return (await response.json()).value;
|
|
1719
|
+
}
|
|
1720
|
+
async update(name, value) {
|
|
1721
|
+
const response = await this.client.fetch(`/v1/secrets/${encodeURIComponent(name)}`, {
|
|
1722
|
+
method: "PATCH",
|
|
1723
|
+
body: JSON.stringify({ value })
|
|
1724
|
+
});
|
|
1725
|
+
if (response.status === 404) throw new NotFoundError("Secret", name);
|
|
1726
|
+
if (response.status === 400) {
|
|
1727
|
+
const body = await response.text();
|
|
1728
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1729
|
+
}
|
|
1730
|
+
if (!response.ok) {
|
|
1731
|
+
const body = await response.text();
|
|
1732
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1733
|
+
}
|
|
1734
|
+
const data = await response.json();
|
|
1735
|
+
return {
|
|
1736
|
+
name: data.name,
|
|
1737
|
+
createdAt: new Date(data.createdAt),
|
|
1738
|
+
updatedAt: new Date(data.updatedAt)
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
async delete(name) {
|
|
1742
|
+
const response = await this.client.fetch(`/v1/secrets/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
1743
|
+
if (response.status === 404) throw new NotFoundError("Secret", name);
|
|
1744
|
+
if (!response.ok) {
|
|
1745
|
+
const body = await response.text();
|
|
1746
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
var SshKeysManagerImpl = class {
|
|
1751
|
+
constructor(client) {
|
|
1752
|
+
this.client = client;
|
|
1753
|
+
}
|
|
1754
|
+
async create(name, publicKey) {
|
|
1755
|
+
const response = await this.client.fetch("/v1/ssh-keys", {
|
|
1756
|
+
method: "POST",
|
|
1757
|
+
body: JSON.stringify({
|
|
1758
|
+
name,
|
|
1759
|
+
publicKey
|
|
1760
|
+
})
|
|
1761
|
+
});
|
|
1762
|
+
if (!response.ok) {
|
|
1763
|
+
const body = await response.text();
|
|
1764
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1765
|
+
}
|
|
1766
|
+
return toSshKeyInfo((await response.json()).sshKey);
|
|
1767
|
+
}
|
|
1768
|
+
async list() {
|
|
1769
|
+
const response = await this.client.fetch("/v1/ssh-keys");
|
|
1770
|
+
if (!response.ok) {
|
|
1771
|
+
const body = await response.text();
|
|
1772
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1773
|
+
}
|
|
1774
|
+
return ((await response.json()).sshKeys ?? []).map(toSshKeyInfo);
|
|
1775
|
+
}
|
|
1776
|
+
async delete(id) {
|
|
1777
|
+
const response = await this.client.fetch(`/v1/ssh-keys/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
1778
|
+
if (response.status === 404) throw new NotFoundError("SSH key", id);
|
|
1779
|
+
if (!response.ok) {
|
|
1780
|
+
const body = await response.text();
|
|
1781
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
function toSshKeyInfo(key) {
|
|
1786
|
+
return {
|
|
1787
|
+
...key,
|
|
1788
|
+
createdAt: new Date(key.createdAt),
|
|
1789
|
+
updatedAt: new Date(key.updatedAt)
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Client for the Intelligence Reports API.
|
|
1794
|
+
*
|
|
1795
|
+
* Two analysis modes:
|
|
1796
|
+
*
|
|
1797
|
+
* - `deterministic` (default): free, rule-based platform analysis over
|
|
1798
|
+
* the subject's trace evidence. Returns immediately.
|
|
1799
|
+
* - `agentic`: routes to the **Tangle Trace Analyst**, an LLM-driven
|
|
1800
|
+
* reasoning loop that reads OTLP-shaped trace evidence and emits
|
|
1801
|
+
* findings, evidence references, recommended actions, and a
|
|
1802
|
+
* validation plan. Billed against `budget.maxUsd`; the platform
|
|
1803
|
+
* never spends past the budget the caller sets.
|
|
1804
|
+
*
|
|
1805
|
+
* Subjects: `sandbox` or `fleet`. A fleet subject can be narrowed to
|
|
1806
|
+
* a single coordinated command via `subject.dispatchId`.
|
|
1807
|
+
*/
|
|
1808
|
+
var IntelligenceClient = class {
|
|
1809
|
+
constructor(client) {
|
|
1810
|
+
this.client = client;
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Create an intelligence report over the given subject.
|
|
1814
|
+
*
|
|
1815
|
+
* Use `mode: "agentic"` to engage the **Tangle Trace Analyst** with a
|
|
1816
|
+
* spending budget. The budget is enforced server-side before billing
|
|
1817
|
+
* fires — a request whose computed cost exceeds `budget.maxUsd`
|
|
1818
|
+
* returns 402 without charging the customer.
|
|
1819
|
+
*/
|
|
1820
|
+
async createReport(options) {
|
|
1821
|
+
const response = await this.client.fetch("/v1/intelligence/reports", {
|
|
1822
|
+
method: "POST",
|
|
1823
|
+
body: JSON.stringify(options)
|
|
1824
|
+
});
|
|
1825
|
+
const body = await response.text();
|
|
1826
|
+
if (!response.ok) throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1827
|
+
return JSON.parse(body).report;
|
|
1828
|
+
}
|
|
1829
|
+
async createAgenticReport(options) {
|
|
1830
|
+
return this.createReport({
|
|
1831
|
+
subject: options.subject,
|
|
1832
|
+
mode: "agentic",
|
|
1833
|
+
budget: {
|
|
1834
|
+
billTo: "customer",
|
|
1835
|
+
maxUsd: options.maxUsd
|
|
1836
|
+
},
|
|
1837
|
+
metadata: options.metadata
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Estimate the cost of a prospective report without creating one.
|
|
1842
|
+
* Verifies subject ownership the same way `createReport` does, so it
|
|
1843
|
+
* never becomes an existence oracle for foreign subjects.
|
|
1844
|
+
*/
|
|
1845
|
+
async estimateReport(options) {
|
|
1846
|
+
const response = await this.client.fetch("/v1/intelligence/reports/estimate", {
|
|
1847
|
+
method: "POST",
|
|
1848
|
+
body: JSON.stringify(options)
|
|
1849
|
+
});
|
|
1850
|
+
const body = await response.text();
|
|
1851
|
+
if (!response.ok) throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1852
|
+
return JSON.parse(body).estimate;
|
|
1853
|
+
}
|
|
1854
|
+
async getReport(jobId) {
|
|
1855
|
+
const response = await this.client.fetch(`/v1/intelligence/reports/${encodeURIComponent(jobId)}`);
|
|
1856
|
+
const body = await response.text();
|
|
1857
|
+
if (!response.ok) throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1858
|
+
return JSON.parse(body).report;
|
|
1859
|
+
}
|
|
1860
|
+
async listReports(options = {}) {
|
|
1861
|
+
const query = new URLSearchParams();
|
|
1862
|
+
if (options.subjectType) query.set("subjectType", options.subjectType);
|
|
1863
|
+
if (options.subjectId) query.set("subjectId", options.subjectId);
|
|
1864
|
+
if (options.limit !== void 0) query.set("limit", String(options.limit));
|
|
1865
|
+
const suffix = query.size > 0 ? `?${query}` : "";
|
|
1866
|
+
const response = await this.client.fetch(`/v1/intelligence/reports${suffix}`);
|
|
1867
|
+
const body = await response.text();
|
|
1868
|
+
if (!response.ok) throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1869
|
+
return JSON.parse(body).reports;
|
|
1870
|
+
}
|
|
1871
|
+
async waitForReport(jobId, options = {}) {
|
|
1872
|
+
const deadline = Date.now() + (options.timeoutMs ?? 6e4);
|
|
1873
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
1874
|
+
while (true) {
|
|
1875
|
+
const report = await this.getReport(jobId);
|
|
1876
|
+
if (report.status === "completed" || report.status === "failed") return report;
|
|
1877
|
+
if (Date.now() >= deadline) throw new TimeoutError(options.timeoutMs ?? 6e4);
|
|
1878
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, Math.max(0, deadline - Date.now()))));
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
/**
|
|
1883
|
+
* Client for browsing available development environments.
|
|
1884
|
+
*/
|
|
1885
|
+
var EnvironmentsClient = class {
|
|
1886
|
+
constructor(client) {
|
|
1887
|
+
this.client = client;
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* List available development environments.
|
|
1891
|
+
*
|
|
1892
|
+
* @returns Array of available environments with metadata
|
|
1893
|
+
*
|
|
1894
|
+
* @example
|
|
1895
|
+
* ```typescript
|
|
1896
|
+
* const envs = await client.environments.list();
|
|
1897
|
+
* for (const env of envs) {
|
|
1898
|
+
* console.log(`${env.id}: ${env.description}`);
|
|
1899
|
+
* }
|
|
1900
|
+
* ```
|
|
1901
|
+
*/
|
|
1902
|
+
async list() {
|
|
1903
|
+
const response = await this.client.fetch("/v1/environments");
|
|
1904
|
+
if (!response.ok) {
|
|
1905
|
+
const body = await response.text();
|
|
1906
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1907
|
+
}
|
|
1908
|
+
return (await response.json()).environments ?? [];
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Get details for a specific environment.
|
|
1912
|
+
*
|
|
1913
|
+
* @param id - Environment identifier (e.g., "universal")
|
|
1914
|
+
*/
|
|
1915
|
+
async get(id) {
|
|
1916
|
+
return (await this.list()).find((e) => e.id === id) ?? null;
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
var PublicTemplatesClient = class {
|
|
1920
|
+
constructor(client) {
|
|
1921
|
+
this.client = client;
|
|
1922
|
+
}
|
|
1923
|
+
async list(options) {
|
|
1924
|
+
const params = new URLSearchParams();
|
|
1925
|
+
if (options?.query) params.set("q", options.query);
|
|
1926
|
+
if (options?.tag) params.set("tag", options.tag);
|
|
1927
|
+
if (options?.featuredOnly) params.set("featured", "true");
|
|
1928
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
1929
|
+
if (options?.offset !== void 0) params.set("offset", String(options.offset));
|
|
1930
|
+
const suffix = params.toString();
|
|
1931
|
+
const response = await this.client.fetch(`/v1/public-templates${suffix ? `?${suffix}` : ""}`);
|
|
1932
|
+
if (!response.ok) {
|
|
1933
|
+
const body = await response.text();
|
|
1934
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1935
|
+
}
|
|
1936
|
+
return (await response.json()).templates ?? [];
|
|
1937
|
+
}
|
|
1938
|
+
async featured() {
|
|
1939
|
+
const response = await this.client.fetch("/v1/public-templates/featured");
|
|
1940
|
+
if (!response.ok) {
|
|
1941
|
+
const body = await response.text();
|
|
1942
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1943
|
+
}
|
|
1944
|
+
return (await response.json()).templates ?? [];
|
|
1945
|
+
}
|
|
1946
|
+
async get(idOrSlug) {
|
|
1947
|
+
const response = await this.client.fetch(`/v1/public-templates/${encodeURIComponent(idOrSlug)}`);
|
|
1948
|
+
if (!response.ok) {
|
|
1949
|
+
const body = await response.text();
|
|
1950
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1951
|
+
}
|
|
1952
|
+
return (await response.json()).template;
|
|
1953
|
+
}
|
|
1954
|
+
async versions(idOrSlug) {
|
|
1955
|
+
const response = await this.client.fetch(`/v1/public-templates/${encodeURIComponent(idOrSlug)}/versions`);
|
|
1956
|
+
if (!response.ok) {
|
|
1957
|
+
const body = await response.text();
|
|
1958
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1959
|
+
}
|
|
1960
|
+
return (await response.json()).versions ?? [];
|
|
1961
|
+
}
|
|
1962
|
+
async publish(options) {
|
|
1963
|
+
const response = await this.client.fetch("/v1/public-templates", {
|
|
1964
|
+
method: "POST",
|
|
1965
|
+
body: JSON.stringify(options)
|
|
1966
|
+
});
|
|
1967
|
+
if (!response.ok) {
|
|
1968
|
+
const body = await response.text();
|
|
1969
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1970
|
+
}
|
|
1971
|
+
return (await response.json()).template;
|
|
1972
|
+
}
|
|
1973
|
+
async publishVersion(idOrSlug, options) {
|
|
1974
|
+
const response = await this.client.fetch(`/v1/public-templates/${encodeURIComponent(idOrSlug)}/versions`, {
|
|
1975
|
+
method: "POST",
|
|
1976
|
+
body: JSON.stringify(options)
|
|
1977
|
+
});
|
|
1978
|
+
if (!response.ok) {
|
|
1979
|
+
const body = await response.text();
|
|
1980
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
1981
|
+
}
|
|
1982
|
+
return (await response.json()).version;
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
/**
|
|
1986
|
+
* Client for managing teams and team-scoped sharing.
|
|
1987
|
+
*
|
|
1988
|
+
* Team resources are scoped to the authenticated user's membership —
|
|
1989
|
+
* the client never needs to pass an org id explicitly.
|
|
1990
|
+
*/
|
|
1991
|
+
var TeamsClient = class {
|
|
1992
|
+
constructor(client) {
|
|
1993
|
+
this.client = client;
|
|
1994
|
+
}
|
|
1995
|
+
async list() {
|
|
1996
|
+
const response = await this.client.fetch("/v1/teams");
|
|
1997
|
+
if (!response.ok) {
|
|
1998
|
+
const body = await response.text();
|
|
1999
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2000
|
+
}
|
|
2001
|
+
return (await response.json()).teams ?? [];
|
|
2002
|
+
}
|
|
2003
|
+
async create(options) {
|
|
2004
|
+
const response = await this.client.fetch("/v1/teams", {
|
|
2005
|
+
method: "POST",
|
|
2006
|
+
body: JSON.stringify(options)
|
|
2007
|
+
});
|
|
2008
|
+
if (!response.ok) {
|
|
2009
|
+
const body = await response.text();
|
|
2010
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2011
|
+
}
|
|
2012
|
+
return (await response.json()).team;
|
|
2013
|
+
}
|
|
2014
|
+
async get(teamId) {
|
|
2015
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}`);
|
|
2016
|
+
if (!response.ok) {
|
|
2017
|
+
const body = await response.text();
|
|
2018
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2019
|
+
}
|
|
2020
|
+
return (await response.json()).team;
|
|
2021
|
+
}
|
|
2022
|
+
async update(teamId, updates) {
|
|
2023
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}`, {
|
|
2024
|
+
method: "PATCH",
|
|
2025
|
+
body: JSON.stringify(updates)
|
|
2026
|
+
});
|
|
2027
|
+
if (!response.ok) {
|
|
2028
|
+
const body = await response.text();
|
|
2029
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2030
|
+
}
|
|
2031
|
+
return (await response.json()).team;
|
|
2032
|
+
}
|
|
2033
|
+
async delete(teamId) {
|
|
2034
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}`, { method: "DELETE" });
|
|
2035
|
+
if (!response.ok) {
|
|
2036
|
+
const body = await response.text();
|
|
2037
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
async leave(teamId) {
|
|
2041
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/leave`, { method: "POST" });
|
|
2042
|
+
if (!response.ok) {
|
|
2043
|
+
const body = await response.text();
|
|
2044
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
async transferOwnership(teamId, newOwnerCustomerId) {
|
|
2048
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/transfer`, {
|
|
2049
|
+
method: "POST",
|
|
2050
|
+
body: JSON.stringify({ newOwnerCustomerId })
|
|
2051
|
+
});
|
|
2052
|
+
if (!response.ok) {
|
|
2053
|
+
const body = await response.text();
|
|
2054
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
async listMembers(teamId) {
|
|
2058
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/members`);
|
|
2059
|
+
if (!response.ok) {
|
|
2060
|
+
const body = await response.text();
|
|
2061
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2062
|
+
}
|
|
2063
|
+
return (await response.json()).members ?? [];
|
|
2064
|
+
}
|
|
2065
|
+
async updateMember(teamId, memberId, updates) {
|
|
2066
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(memberId)}`, {
|
|
2067
|
+
method: "PATCH",
|
|
2068
|
+
body: JSON.stringify(updates)
|
|
2069
|
+
});
|
|
2070
|
+
if (!response.ok) {
|
|
2071
|
+
const body = await response.text();
|
|
2072
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2073
|
+
}
|
|
2074
|
+
return (await response.json()).member;
|
|
2075
|
+
}
|
|
2076
|
+
async removeMember(teamId, memberId) {
|
|
2077
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(memberId)}`, { method: "DELETE" });
|
|
2078
|
+
if (!response.ok) {
|
|
2079
|
+
const body = await response.text();
|
|
2080
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
async listInvitations(teamId) {
|
|
2084
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/invitations`);
|
|
2085
|
+
if (!response.ok) {
|
|
2086
|
+
const body = await response.text();
|
|
2087
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2088
|
+
}
|
|
2089
|
+
return (await response.json()).invitations ?? [];
|
|
2090
|
+
}
|
|
2091
|
+
async invite(teamId, options) {
|
|
2092
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/invitations`, {
|
|
2093
|
+
method: "POST",
|
|
2094
|
+
body: JSON.stringify(options)
|
|
2095
|
+
});
|
|
2096
|
+
if (!response.ok) {
|
|
2097
|
+
const body = await response.text();
|
|
2098
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2099
|
+
}
|
|
2100
|
+
return (await response.json()).invitation;
|
|
2101
|
+
}
|
|
2102
|
+
async revokeInvitation(invitationId) {
|
|
2103
|
+
const response = await this.client.fetch(`/v1/teams/invitations/${encodeURIComponent(invitationId)}`, { method: "DELETE" });
|
|
2104
|
+
if (!response.ok) {
|
|
2105
|
+
const body = await response.text();
|
|
2106
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
async acceptInvitation(token) {
|
|
2110
|
+
const response = await this.client.fetch(`/v1/teams/invitations/${encodeURIComponent(token)}/accept`, { method: "POST" });
|
|
2111
|
+
if (!response.ok) {
|
|
2112
|
+
const body = await response.text();
|
|
2113
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2114
|
+
}
|
|
2115
|
+
return (await response.json()).member;
|
|
2116
|
+
}
|
|
2117
|
+
async getInvitation(token) {
|
|
2118
|
+
const response = await this.client.fetch(`/v1/teams/invitations/${encodeURIComponent(token)}`);
|
|
2119
|
+
if (!response.ok) {
|
|
2120
|
+
const body = await response.text();
|
|
2121
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2122
|
+
}
|
|
2123
|
+
return (await response.json()).invitation;
|
|
2124
|
+
}
|
|
2125
|
+
async listSecrets(teamId) {
|
|
2126
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/secrets`);
|
|
2127
|
+
if (!response.ok) {
|
|
2128
|
+
const body = await response.text();
|
|
2129
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2130
|
+
}
|
|
2131
|
+
return (await response.json()).secrets ?? [];
|
|
2132
|
+
}
|
|
2133
|
+
async upsertSecret(teamId, name, value) {
|
|
2134
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/secrets/${encodeURIComponent(name)}`, {
|
|
2135
|
+
method: "PUT",
|
|
2136
|
+
body: JSON.stringify({ value })
|
|
2137
|
+
});
|
|
2138
|
+
if (!response.ok) {
|
|
2139
|
+
const body = await response.text();
|
|
2140
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2141
|
+
}
|
|
2142
|
+
return (await response.json()).secret;
|
|
2143
|
+
}
|
|
2144
|
+
async deleteSecret(teamId, name) {
|
|
2145
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/secrets/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
2146
|
+
if (!response.ok) {
|
|
2147
|
+
const body = await response.text();
|
|
2148
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
async revealSecret(teamId, name) {
|
|
2152
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/secrets/${encodeURIComponent(name)}/reveal`, { method: "POST" });
|
|
2153
|
+
if (!response.ok) {
|
|
2154
|
+
const body = await response.text();
|
|
2155
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2156
|
+
}
|
|
2157
|
+
return await response.json();
|
|
2158
|
+
}
|
|
2159
|
+
async listTemplates(teamId) {
|
|
2160
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/templates`);
|
|
2161
|
+
if (!response.ok) {
|
|
2162
|
+
const body = await response.text();
|
|
2163
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2164
|
+
}
|
|
2165
|
+
return (await response.json()).templates ?? [];
|
|
2166
|
+
}
|
|
2167
|
+
async createTemplate(teamId, options) {
|
|
2168
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/templates`, {
|
|
2169
|
+
method: "POST",
|
|
2170
|
+
body: JSON.stringify({
|
|
2171
|
+
name: options.name,
|
|
2172
|
+
snapshotId: options.snapshotId,
|
|
2173
|
+
description: options.description,
|
|
2174
|
+
environment: options.environment,
|
|
2175
|
+
config: options.config
|
|
2176
|
+
})
|
|
2177
|
+
});
|
|
2178
|
+
if (!response.ok) {
|
|
2179
|
+
const body = await response.text();
|
|
2180
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2181
|
+
}
|
|
2182
|
+
return (await response.json()).template;
|
|
2183
|
+
}
|
|
2184
|
+
async deleteTemplate(teamId, templateId) {
|
|
2185
|
+
const response = await this.client.fetch(`/v1/teams/${encodeURIComponent(teamId)}/templates/${encodeURIComponent(templateId)}`, { method: "DELETE" });
|
|
2186
|
+
if (!response.ok) {
|
|
2187
|
+
const body = await response.text();
|
|
2188
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
//#endregion
|
|
2193
|
+
export { DEFAULT_SANDBOX_SIZE as a, resolveSandboxResources as c, SandboxFleetClient as i, sandboxResourcesForSize as l, SandboxClient as n, SANDBOX_SIZE_PRESETS as o, SandboxFleet as r, SANDBOX_SIZE_PRESET_NAMES as s, IntelligenceClient as t };
|