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