@sandblocks/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/agents.js +209 -0
- package/dist/bin.js +59 -0
- package/dist/index.js +1018 -0
- package/dist/output.js +54 -0
- package/dist/providers/custom.js +49 -0
- package/dist/providers/docker.js +257 -0
- package/dist/providers/podman.js +257 -0
- package/dist/providers/remote.js +341 -0
- package/dist/providers/unsafe-host.js +126 -0
- package/dist/types/agents.d.ts +63 -0
- package/dist/types/agents.d.ts.map +1 -0
- package/dist/types/bin.d.ts +3 -0
- package/dist/types/bin.d.ts.map +1 -0
- package/dist/types/index.d.ts +262 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/init.d.ts +6 -0
- package/dist/types/init.d.ts.map +1 -0
- package/dist/types/orchestration.d.ts +94 -0
- package/dist/types/orchestration.d.ts.map +1 -0
- package/dist/types/output.d.ts +12 -0
- package/dist/types/output.d.ts.map +1 -0
- package/dist/types/process.d.ts +6 -0
- package/dist/types/process.d.ts.map +1 -0
- package/dist/types/providers/custom.d.ts +15 -0
- package/dist/types/providers/custom.d.ts.map +1 -0
- package/dist/types/providers/docker.d.ts +7 -0
- package/dist/types/providers/docker.d.ts.map +1 -0
- package/dist/types/providers/oci.d.ts +37 -0
- package/dist/types/providers/oci.d.ts.map +1 -0
- package/dist/types/providers/podman.d.ts +7 -0
- package/dist/types/providers/podman.d.ts.map +1 -0
- package/dist/types/providers/remote.d.ts +31 -0
- package/dist/types/providers/remote.d.ts.map +1 -0
- package/dist/types/providers/unsafe-host.d.ts +14 -0
- package/dist/types/providers/unsafe-host.d.ts.map +1 -0
- package/dist/types/types.d.ts +196 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
// src/agents.ts
|
|
2
|
+
class CommandAgent {
|
|
3
|
+
options;
|
|
4
|
+
id;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
this.id = options.id ?? "command";
|
|
8
|
+
}
|
|
9
|
+
async run(context) {
|
|
10
|
+
const startedAt = new Date().toISOString();
|
|
11
|
+
const base = typeof this.options.command === "function" ? this.options.command(context) : [...this.options.command];
|
|
12
|
+
const promptMode = this.options.prompt ?? "stdin";
|
|
13
|
+
const command = promptMode === "argument" ? [...base, context.prompt] : base;
|
|
14
|
+
const idle = new AbortController;
|
|
15
|
+
const completed = new AbortController;
|
|
16
|
+
let idleTimer;
|
|
17
|
+
let completionTimer;
|
|
18
|
+
const resetIdle = () => {
|
|
19
|
+
if (idleTimer)
|
|
20
|
+
clearTimeout(idleTimer);
|
|
21
|
+
idleTimer = setTimeout(() => idle.abort(new Error(`agent idle timeout after ${context.idleTimeoutSeconds}s`)), context.idleTimeoutSeconds * 1000);
|
|
22
|
+
};
|
|
23
|
+
resetIdle();
|
|
24
|
+
let output = "";
|
|
25
|
+
let stdout = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
let lineBuffer = "";
|
|
28
|
+
let completionSignal;
|
|
29
|
+
const consume = async (chunk, stream) => {
|
|
30
|
+
resetIdle();
|
|
31
|
+
output += chunk;
|
|
32
|
+
if (stream === "stdout")
|
|
33
|
+
stdout += chunk;
|
|
34
|
+
else
|
|
35
|
+
stderr += chunk;
|
|
36
|
+
lineBuffer += chunk;
|
|
37
|
+
const lines = lineBuffer.split(/\r?\n/);
|
|
38
|
+
lineBuffer = lines.pop() ?? "";
|
|
39
|
+
for (const line of lines)
|
|
40
|
+
await this.emitLine(line, stream, context);
|
|
41
|
+
completionSignal ??= context.completionSignal.find((signal) => output.includes(signal));
|
|
42
|
+
if (completionSignal && !completionTimer) {
|
|
43
|
+
completionTimer = setTimeout(() => completed.abort(new Error("agent did not exit after its completion signal")), context.completionTimeoutSeconds * 1000);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
const signals = [context.signal, idle.signal, completed.signal].filter(Boolean);
|
|
48
|
+
try {
|
|
49
|
+
const result = await context.runtime.exec(command, {
|
|
50
|
+
env: this.options.env,
|
|
51
|
+
stdin: promptMode === "stdin" ? context.prompt : undefined,
|
|
52
|
+
signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
|
|
53
|
+
onStdout: (chunk) => consume(chunk, "stdout"),
|
|
54
|
+
onStderr: (chunk) => consume(chunk, "stderr")
|
|
55
|
+
});
|
|
56
|
+
stdout = result.stdout;
|
|
57
|
+
stderr = result.stderr;
|
|
58
|
+
return this.result(context, startedAt, result.exitCode, stdout, stderr, completionSignal);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!completionSignal)
|
|
61
|
+
throw error;
|
|
62
|
+
return this.result(context, startedAt, 0, stdout, stderr, completionSignal);
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
if (idleTimer)
|
|
66
|
+
clearTimeout(idleTimer);
|
|
67
|
+
if (completionTimer)
|
|
68
|
+
clearTimeout(completionTimer);
|
|
69
|
+
if (lineBuffer)
|
|
70
|
+
await this.emitLine(lineBuffer, "stdout", context);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
result(context, startedAt, exitCode, stdout, stderr, completionSignal) {
|
|
74
|
+
const sessionId = this.options.sessionId?.(stdout, stderr);
|
|
75
|
+
return {
|
|
76
|
+
iteration: context.iteration,
|
|
77
|
+
exitCode,
|
|
78
|
+
stdout,
|
|
79
|
+
stderr,
|
|
80
|
+
...sessionId ? { sessionId } : {},
|
|
81
|
+
...completionSignal ? { completionSignal } : {},
|
|
82
|
+
startedAt,
|
|
83
|
+
finishedAt: new Date().toISOString()
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async emitLine(line, stream, context) {
|
|
87
|
+
const timestamp = new Date().toISOString();
|
|
88
|
+
const parsed = this.options.parseEvent?.(line, { iteration: context.iteration, timestamp });
|
|
89
|
+
const events = parsed ? Array.isArray(parsed) ? parsed : [parsed] : [
|
|
90
|
+
{
|
|
91
|
+
type: "raw",
|
|
92
|
+
line: stream === "stderr" ? `[stderr] ${line}` : line,
|
|
93
|
+
iteration: context.iteration,
|
|
94
|
+
timestamp
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
for (const event of events)
|
|
98
|
+
await context.emit(event);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class ClaudeCodeAgent extends CommandAgent {
|
|
103
|
+
constructor(options) {
|
|
104
|
+
super({
|
|
105
|
+
id: "claude-code",
|
|
106
|
+
env: credentialEnvironment(options.env, options.credential),
|
|
107
|
+
prompt: "argument",
|
|
108
|
+
command: (context) => [
|
|
109
|
+
options.binary ?? "claude",
|
|
110
|
+
"--print",
|
|
111
|
+
"--model",
|
|
112
|
+
options.model,
|
|
113
|
+
...options.effort ? ["--effort", options.effort] : [],
|
|
114
|
+
...options.extraArgs ?? []
|
|
115
|
+
],
|
|
116
|
+
sessionId: extractSessionId,
|
|
117
|
+
parseEvent: parseJsonAgentEvent
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class CodexAgent extends CommandAgent {
|
|
123
|
+
constructor(options) {
|
|
124
|
+
super({
|
|
125
|
+
id: "codex",
|
|
126
|
+
env: credentialEnvironment(options.env, options.credential),
|
|
127
|
+
prompt: "argument",
|
|
128
|
+
command: (context) => [
|
|
129
|
+
options.binary ?? "codex",
|
|
130
|
+
"exec",
|
|
131
|
+
"--model",
|
|
132
|
+
options.model,
|
|
133
|
+
...options.reasoningEffort ? ["--config", `model_reasoning_effort=${options.reasoningEffort}`] : [],
|
|
134
|
+
...options.extraArgs ?? []
|
|
135
|
+
],
|
|
136
|
+
sessionId: extractSessionId,
|
|
137
|
+
parseEvent: parseJsonAgentEvent
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class PiAgent extends CommandAgent {
|
|
143
|
+
constructor(options) {
|
|
144
|
+
super({
|
|
145
|
+
id: "pi",
|
|
146
|
+
env: credentialEnvironment(options.env, options.credential),
|
|
147
|
+
prompt: "argument",
|
|
148
|
+
command: (context) => [
|
|
149
|
+
options.binary ?? "pi",
|
|
150
|
+
"--print",
|
|
151
|
+
"--model",
|
|
152
|
+
options.model,
|
|
153
|
+
...options.skills?.flatMap((skill) => ["--skill", skill]) ?? [],
|
|
154
|
+
...options.extraArgs ?? []
|
|
155
|
+
],
|
|
156
|
+
sessionId: extractSessionId,
|
|
157
|
+
parseEvent: parseJsonAgentEvent
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
var claudeCode = (model, options = {}) => new ClaudeCodeAgent({ model, ...options });
|
|
162
|
+
var codex = (model, options = {}) => new CodexAgent({ model, ...options });
|
|
163
|
+
var pi = (model, options = {}) => new PiAgent({ model, ...options });
|
|
164
|
+
function parseJsonAgentEvent(line, context) {
|
|
165
|
+
let value;
|
|
166
|
+
try {
|
|
167
|
+
value = JSON.parse(line);
|
|
168
|
+
} catch {
|
|
169
|
+
return { type: "raw", line, ...context };
|
|
170
|
+
}
|
|
171
|
+
const type = String(value.type ?? "");
|
|
172
|
+
if (/tool/i.test(type)) {
|
|
173
|
+
return {
|
|
174
|
+
type: "toolCall",
|
|
175
|
+
name: String(value.name ?? value.tool ?? value.tool_name ?? "tool"),
|
|
176
|
+
...value.input !== undefined ? { input: value.input } : {},
|
|
177
|
+
...context
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const text = value.text ?? value.message ?? value.content;
|
|
181
|
+
return typeof text === "string" ? { type: "text", text, ...context } : { type: "raw", line, ...context };
|
|
182
|
+
}
|
|
183
|
+
function credentialEnvironment(env, credential) {
|
|
184
|
+
if (!credential)
|
|
185
|
+
return env;
|
|
186
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(credential.environment))
|
|
187
|
+
throw new Error("credential environment name is invalid");
|
|
188
|
+
const value = process.env[credential.environment];
|
|
189
|
+
if (value === undefined)
|
|
190
|
+
return env;
|
|
191
|
+
const target = credential.target ?? credential.environment;
|
|
192
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(target))
|
|
193
|
+
throw new Error("credential target name is invalid");
|
|
194
|
+
return { ...env, [target]: value };
|
|
195
|
+
}
|
|
196
|
+
function extractSessionId(stdout, stderr) {
|
|
197
|
+
const match = `${stdout}
|
|
198
|
+
${stderr}`.match(/(?:session[_ -]?id|session)[:=\s]+([A-Za-z0-9._-]{8,})/i);
|
|
199
|
+
return match?.[1];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/output.ts
|
|
203
|
+
var Output = {
|
|
204
|
+
string(input) {
|
|
205
|
+
return { kind: "string", tag: validTag(input.tag) };
|
|
206
|
+
},
|
|
207
|
+
object(input) {
|
|
208
|
+
return { kind: "object", tag: validTag(input.tag), schema: input.schema };
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
async function extractOutput(spec, text) {
|
|
212
|
+
const pattern = new RegExp(`<${escapeRegex(spec.tag)}>([\\s\\S]*?)<\\/${escapeRegex(spec.tag)}>`);
|
|
213
|
+
const match = text.match(pattern);
|
|
214
|
+
if (!match)
|
|
215
|
+
throw new Error(`structured output tag <${spec.tag}> was not found`);
|
|
216
|
+
const content = match[1]?.trim();
|
|
217
|
+
if (content === undefined)
|
|
218
|
+
throw new Error(`structured output tag <${spec.tag}> is empty`);
|
|
219
|
+
if (spec.kind === "string")
|
|
220
|
+
return content;
|
|
221
|
+
let value;
|
|
222
|
+
try {
|
|
223
|
+
value = JSON.parse(content);
|
|
224
|
+
} catch {
|
|
225
|
+
throw new Error(`structured output <${spec.tag}> is not valid JSON`);
|
|
226
|
+
}
|
|
227
|
+
const schema = spec.schema;
|
|
228
|
+
if (schema["~standard"]) {
|
|
229
|
+
const result = await schema["~standard"].validate(value);
|
|
230
|
+
if (result.issues?.length)
|
|
231
|
+
throw new Error(`structured output validation failed: ${JSON.stringify(result.issues)}`);
|
|
232
|
+
return result.value;
|
|
233
|
+
}
|
|
234
|
+
if (schema.safeParse) {
|
|
235
|
+
const result = schema.safeParse(value);
|
|
236
|
+
if (!result.success)
|
|
237
|
+
throw new Error(`structured output validation failed: ${String(result.error)}`);
|
|
238
|
+
return result.data;
|
|
239
|
+
}
|
|
240
|
+
if (schema.parse)
|
|
241
|
+
return schema.parse(value);
|
|
242
|
+
throw new Error("structured output schema is unsupported");
|
|
243
|
+
}
|
|
244
|
+
function validTag(tag) {
|
|
245
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(tag))
|
|
246
|
+
throw new Error("output tag is invalid");
|
|
247
|
+
return tag;
|
|
248
|
+
}
|
|
249
|
+
function escapeRegex(value) {
|
|
250
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/providers/custom.ts
|
|
254
|
+
function createSandboxProvider(options) {
|
|
255
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(options.kind))
|
|
256
|
+
throw new Error("custom provider kind is invalid");
|
|
257
|
+
const reconnect = options.reconnect;
|
|
258
|
+
return {
|
|
259
|
+
kind: options.kind,
|
|
260
|
+
capabilities: Object.freeze({ ...options.capabilities }),
|
|
261
|
+
create: (input) => options.create(input),
|
|
262
|
+
...reconnect ? { reconnect: (input) => reconnect(input) } : {}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function createBindMountSandboxProvider(options) {
|
|
266
|
+
return createSandboxProvider({
|
|
267
|
+
...options,
|
|
268
|
+
capabilities: {
|
|
269
|
+
bindMounts: true,
|
|
270
|
+
isolatedFilesystem: true,
|
|
271
|
+
persistent: true,
|
|
272
|
+
networks: false,
|
|
273
|
+
devices: false,
|
|
274
|
+
snapshots: false,
|
|
275
|
+
reconnect: Boolean(options.reconnect),
|
|
276
|
+
remote: false,
|
|
277
|
+
...options.capabilities
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function createIsolatedSandboxProvider(options) {
|
|
282
|
+
return createSandboxProvider({
|
|
283
|
+
...options,
|
|
284
|
+
capabilities: {
|
|
285
|
+
bindMounts: false,
|
|
286
|
+
isolatedFilesystem: true,
|
|
287
|
+
persistent: true,
|
|
288
|
+
networks: true,
|
|
289
|
+
devices: false,
|
|
290
|
+
snapshots: false,
|
|
291
|
+
reconnect: Boolean(options.reconnect),
|
|
292
|
+
remote: true,
|
|
293
|
+
...options.capabilities
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// src/orchestration.ts
|
|
298
|
+
import { randomUUID } from "node:crypto";
|
|
299
|
+
import { appendFile, cp, mkdir, readFile, rm } from "node:fs/promises";
|
|
300
|
+
import { dirname, resolve } from "node:path";
|
|
301
|
+
|
|
302
|
+
// src/process.ts
|
|
303
|
+
import { spawn } from "node:child_process";
|
|
304
|
+
async function executeProcess(command, options = {}) {
|
|
305
|
+
if (!command.length || command.some((part) => !part || part.includes("\x00"))) {
|
|
306
|
+
throw new Error("command must contain safe non-empty arguments");
|
|
307
|
+
}
|
|
308
|
+
const startedAt = new Date().toISOString();
|
|
309
|
+
const [executable, ...args] = command;
|
|
310
|
+
if (!executable)
|
|
311
|
+
throw new Error("command executable is required");
|
|
312
|
+
const child = spawn(executable, args, {
|
|
313
|
+
cwd: options.cwd ?? options.hostCwd,
|
|
314
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
315
|
+
stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
|
|
316
|
+
});
|
|
317
|
+
if (options.stdin !== undefined)
|
|
318
|
+
child.stdin?.end(options.stdin);
|
|
319
|
+
let timedOut = false;
|
|
320
|
+
let stdout = "";
|
|
321
|
+
let stderr = "";
|
|
322
|
+
let callbackQueue = Promise.resolve();
|
|
323
|
+
child.stdout?.on("data", (chunk) => {
|
|
324
|
+
const text = chunk.toString();
|
|
325
|
+
stdout = `${stdout}${text}`.slice(-4 * 1024 * 1024);
|
|
326
|
+
callbackQueue = callbackQueue.then(() => options.onStdout?.(text)).then(() => {
|
|
327
|
+
return;
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
child.stderr?.on("data", (chunk) => {
|
|
331
|
+
const text = chunk.toString();
|
|
332
|
+
stderr = `${stderr}${text}`.slice(-4 * 1024 * 1024);
|
|
333
|
+
callbackQueue = callbackQueue.then(() => options.onStderr?.(text)).then(() => {
|
|
334
|
+
return;
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
const stop = () => child.kill("SIGTERM");
|
|
338
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
339
|
+
const timer = options.timeoutMs ? setTimeout(() => {
|
|
340
|
+
timedOut = true;
|
|
341
|
+
child.kill("SIGKILL");
|
|
342
|
+
}, options.timeoutMs) : undefined;
|
|
343
|
+
try {
|
|
344
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
345
|
+
child.once("error", reject);
|
|
346
|
+
child.once("close", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
|
|
347
|
+
});
|
|
348
|
+
await callbackQueue;
|
|
349
|
+
if (options.signal?.aborted)
|
|
350
|
+
throw options.signal.reason ?? new Error("operation aborted");
|
|
351
|
+
return {
|
|
352
|
+
exitCode: timedOut ? 124 : exitCode,
|
|
353
|
+
stdout,
|
|
354
|
+
stderr,
|
|
355
|
+
timedOut,
|
|
356
|
+
startedAt,
|
|
357
|
+
finishedAt: new Date().toISOString()
|
|
358
|
+
};
|
|
359
|
+
} finally {
|
|
360
|
+
if (timer)
|
|
361
|
+
clearTimeout(timer);
|
|
362
|
+
options.signal?.removeEventListener("abort", stop);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function expandHome(path) {
|
|
366
|
+
if (path === "~")
|
|
367
|
+
return process.env.HOME ?? path;
|
|
368
|
+
if (path.startsWith("~/"))
|
|
369
|
+
return `${process.env.HOME ?? "~"}/${path.slice(2)}`;
|
|
370
|
+
return path;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/orchestration.ts
|
|
374
|
+
class SandblocksSandbox {
|
|
375
|
+
runtime;
|
|
376
|
+
options;
|
|
377
|
+
git;
|
|
378
|
+
id;
|
|
379
|
+
provider;
|
|
380
|
+
cwd;
|
|
381
|
+
failed = false;
|
|
382
|
+
disposed = false;
|
|
383
|
+
eventQueue = [];
|
|
384
|
+
eventWaiters = [];
|
|
385
|
+
constructor(runtime, options, git) {
|
|
386
|
+
this.runtime = runtime;
|
|
387
|
+
this.options = options;
|
|
388
|
+
this.git = git;
|
|
389
|
+
this.id = runtime.id;
|
|
390
|
+
this.provider = runtime.provider;
|
|
391
|
+
this.cwd = runtime.cwd;
|
|
392
|
+
}
|
|
393
|
+
exec(command, options = {}) {
|
|
394
|
+
return this.runtime.exec(command, options);
|
|
395
|
+
}
|
|
396
|
+
upload(source, destination) {
|
|
397
|
+
if (!this.runtime.upload)
|
|
398
|
+
throw new Error(`${this.provider} provider does not support upload`);
|
|
399
|
+
return this.runtime.upload(source, destination);
|
|
400
|
+
}
|
|
401
|
+
download(source, destination) {
|
|
402
|
+
if (!this.runtime.download)
|
|
403
|
+
throw new Error(`${this.provider} provider does not support download`);
|
|
404
|
+
return this.runtime.download(source, destination);
|
|
405
|
+
}
|
|
406
|
+
stop(reason) {
|
|
407
|
+
return this.runtime.stop(reason);
|
|
408
|
+
}
|
|
409
|
+
async* events(options = {}) {
|
|
410
|
+
while (!this.disposed || this.eventQueue.length) {
|
|
411
|
+
if (options.signal?.aborted)
|
|
412
|
+
return;
|
|
413
|
+
const queued = this.eventQueue.shift();
|
|
414
|
+
if (queued) {
|
|
415
|
+
yield queued;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const event = await new Promise((resolve2) => {
|
|
419
|
+
const abort = () => resolve2(undefined);
|
|
420
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
421
|
+
this.eventWaiters.push((value) => {
|
|
422
|
+
options.signal?.removeEventListener("abort", abort);
|
|
423
|
+
resolve2(value);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
if (!event)
|
|
427
|
+
return;
|
|
428
|
+
yield event;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async run(options) {
|
|
432
|
+
if (options.output && (options.maxIterations ?? 1) !== 1)
|
|
433
|
+
throw new Error("structured output requires maxIterations === 1");
|
|
434
|
+
const startedAt = new Date().toISOString();
|
|
435
|
+
const prompt = await resolvePrompt(options);
|
|
436
|
+
const signals = array(options.completionSignal ?? "<promise>COMPLETE</promise>");
|
|
437
|
+
const logger = createLogger(options.logging, this.git.hostCwd);
|
|
438
|
+
const baseline = await gitHead(this.git.runtimeCwd);
|
|
439
|
+
const iterations = [];
|
|
440
|
+
try {
|
|
441
|
+
await runSandboxHooks(this.runtime, options.hooks?.sandbox?.beforeAgent);
|
|
442
|
+
for (let iteration = 1;iteration <= (options.maxIterations ?? 1); iteration += 1) {
|
|
443
|
+
const result = await options.agent.run({
|
|
444
|
+
runtime: this.runtime,
|
|
445
|
+
prompt,
|
|
446
|
+
iteration,
|
|
447
|
+
signal: options.signal,
|
|
448
|
+
idleTimeoutSeconds: options.idleTimeoutSeconds ?? 600,
|
|
449
|
+
completionTimeoutSeconds: options.completionTimeoutSeconds ?? 60,
|
|
450
|
+
completionSignal: signals,
|
|
451
|
+
emit: async (event) => {
|
|
452
|
+
this.publish(event);
|
|
453
|
+
await logger(event);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
iterations.push(result);
|
|
457
|
+
if (result.exitCode !== 0)
|
|
458
|
+
throw new Error(result.stderr || `${options.agent.id} exited ${result.exitCode}`);
|
|
459
|
+
if (result.completionSignal)
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
await runSandboxHooks(this.runtime, options.hooks?.sandbox?.afterAgent);
|
|
463
|
+
await this.runtime.syncToHost?.();
|
|
464
|
+
const commits = await collectCommits(this.git.runtimeCwd, baseline, this.options.timeouts?.commitCollectionMs);
|
|
465
|
+
if (this.git.mergeToHead && this.git.branch)
|
|
466
|
+
await mergeBranch(this.git, this.options.timeouts?.mergeToHostMs);
|
|
467
|
+
const allOutput = iterations.map((item) => item.stdout).join(`
|
|
468
|
+
`);
|
|
469
|
+
const completionSignal = iterations.find((item) => item.completionSignal)?.completionSignal;
|
|
470
|
+
return {
|
|
471
|
+
sandboxId: this.id,
|
|
472
|
+
provider: this.provider,
|
|
473
|
+
iterations,
|
|
474
|
+
commits,
|
|
475
|
+
...this.git.branch ? { branch: this.git.branch } : {},
|
|
476
|
+
...completionSignal ? { completionSignal } : {},
|
|
477
|
+
...options.output ? { output: await extractOutput(options.output, allOutput) } : {},
|
|
478
|
+
startedAt,
|
|
479
|
+
finishedAt: new Date().toISOString()
|
|
480
|
+
};
|
|
481
|
+
} catch (error) {
|
|
482
|
+
this.failed = true;
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async destroy() {
|
|
487
|
+
if (this.disposed)
|
|
488
|
+
return;
|
|
489
|
+
this.disposed = true;
|
|
490
|
+
await runHostHooks(this.options.hooks?.host?.beforeDestroy, this.git.hostCwd);
|
|
491
|
+
const retention = this.options.retention ?? "destroy";
|
|
492
|
+
if (retention === "destroy" || retention === "on-failure" && !this.failed)
|
|
493
|
+
await this.runtime.destroy();
|
|
494
|
+
for (const waiter of this.eventWaiters.splice(0))
|
|
495
|
+
waiter(undefined);
|
|
496
|
+
if (this.git.worktree && retention !== "always") {
|
|
497
|
+
await executeProcess([
|
|
498
|
+
"git",
|
|
499
|
+
"-C",
|
|
500
|
+
this.git.hostCwd,
|
|
501
|
+
"worktree",
|
|
502
|
+
"remove",
|
|
503
|
+
"--force",
|
|
504
|
+
this.git.worktree
|
|
505
|
+
]);
|
|
506
|
+
await rm(this.git.worktree, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async[Symbol.asyncDispose]() {
|
|
510
|
+
await this.destroy();
|
|
511
|
+
}
|
|
512
|
+
publish(event) {
|
|
513
|
+
const waiter = this.eventWaiters.shift();
|
|
514
|
+
if (waiter)
|
|
515
|
+
waiter(event);
|
|
516
|
+
else
|
|
517
|
+
this.eventQueue.push(event);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function createSandbox(options) {
|
|
521
|
+
const provider = options.provider ?? options.sandbox;
|
|
522
|
+
if (!provider)
|
|
523
|
+
throw new Error("sandbox provider is required");
|
|
524
|
+
assertProviderCapabilities(provider, options);
|
|
525
|
+
const hostCwd = resolve(options.cwd ?? process.cwd());
|
|
526
|
+
await runHostHooks(options.hooks?.host?.beforeCreate, hostCwd);
|
|
527
|
+
const branchStrategy = options.branch ?? options.branchStrategy ?? { type: "head" };
|
|
528
|
+
if (options.copyToWorktree?.length && branchStrategy.type === "head") {
|
|
529
|
+
throw new Error("copyToWorktree is not supported with the head branch strategy");
|
|
530
|
+
}
|
|
531
|
+
const git = await prepareGit(hostCwd, branchStrategy, options.timeouts?.gitSetupMs);
|
|
532
|
+
await runHostHooks(options.hooks?.host?.onWorktreeReady, git.runtimeCwd);
|
|
533
|
+
for (const file of options.copyToWorktree ?? []) {
|
|
534
|
+
const source = resolve(hostCwd, file);
|
|
535
|
+
const destination = resolve(git.runtimeCwd, file);
|
|
536
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
537
|
+
await withTimeout(cp(source, destination, { recursive: true }), options.timeouts?.copyToWorktreeMs ?? 60000, `copying '${file}'`);
|
|
538
|
+
}
|
|
539
|
+
const id = options.id ?? randomUUID();
|
|
540
|
+
const runtime = await provider.create({
|
|
541
|
+
id,
|
|
542
|
+
cwd: git.runtimeCwd,
|
|
543
|
+
image: options.image,
|
|
544
|
+
env: options.env,
|
|
545
|
+
mounts: options.mounts,
|
|
546
|
+
network: options.network,
|
|
547
|
+
groups: options.groups,
|
|
548
|
+
devices: options.devices,
|
|
549
|
+
cpus: options.cpus,
|
|
550
|
+
memoryMb: options.memoryMb,
|
|
551
|
+
pids: options.pids,
|
|
552
|
+
tmpfsMb: options.tmpfsMb,
|
|
553
|
+
signal: options.signal,
|
|
554
|
+
idempotencyKey: options.idempotencyKey,
|
|
555
|
+
metadata: options.metadata
|
|
556
|
+
});
|
|
557
|
+
const sandbox = new SandblocksSandbox(runtime, options, git);
|
|
558
|
+
try {
|
|
559
|
+
await runHostHooks(options.hooks?.host?.onSandboxReady, hostCwd);
|
|
560
|
+
await runHostHooks(options.hooks?.host?.afterCreate, hostCwd);
|
|
561
|
+
await runSandboxHooks(runtime, options.hooks?.sandbox?.onSandboxReady);
|
|
562
|
+
await runSandboxHooks(runtime, options.hooks?.sandbox?.afterCreate);
|
|
563
|
+
return sandbox;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
await sandbox.destroy().catch(() => {
|
|
566
|
+
return;
|
|
567
|
+
});
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async function connectSandbox(options) {
|
|
572
|
+
if (!options.provider.reconnect)
|
|
573
|
+
throw new Error(`${options.provider.kind} provider does not support reconnect`);
|
|
574
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
575
|
+
const runtime = await options.provider.reconnect({ id: options.id, cwd });
|
|
576
|
+
return new SandblocksSandbox(runtime, { provider: options.provider, cwd, retention: options.retention ?? "always" }, { hostCwd: cwd, runtimeCwd: cwd });
|
|
577
|
+
}
|
|
578
|
+
async function run(options) {
|
|
579
|
+
const sandbox = await createSandbox(options);
|
|
580
|
+
try {
|
|
581
|
+
return await sandbox.run(options);
|
|
582
|
+
} finally {
|
|
583
|
+
await sandbox.destroy();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function resolvePrompt(options) {
|
|
587
|
+
if (Boolean(options.prompt) === Boolean(options.promptFile))
|
|
588
|
+
throw new Error("provide exactly one of prompt or promptFile");
|
|
589
|
+
const promptFile = options.promptFile;
|
|
590
|
+
let prompt = options.prompt ?? await readFile(resolve(process.cwd(), promptFile ?? ""), "utf8");
|
|
591
|
+
for (const [key, value] of Object.entries(options.promptArgs ?? {}))
|
|
592
|
+
prompt = prompt.replaceAll(`{{${key}}}`, value);
|
|
593
|
+
return prompt;
|
|
594
|
+
}
|
|
595
|
+
async function prepareGit(hostCwd, strategy, timeoutMs = 30000) {
|
|
596
|
+
if (strategy.type === "head")
|
|
597
|
+
return { hostCwd, runtimeCwd: hostCwd };
|
|
598
|
+
const originalHead = await gitHead(hostCwd);
|
|
599
|
+
if (strategy.type === "branch") {
|
|
600
|
+
await checked(["git", "-C", hostCwd, "switch", "-C", strategy.name], timeoutMs);
|
|
601
|
+
return { hostCwd, runtimeCwd: hostCwd, branch: strategy.name, originalHead };
|
|
602
|
+
}
|
|
603
|
+
const branch = strategy.name ?? `sandblocks/${randomUUID().slice(0, 8)}`;
|
|
604
|
+
const worktree = resolve(hostCwd, ".sandblocks", "worktrees", branch.replace(/[^A-Za-z0-9_.-]/g, "-"));
|
|
605
|
+
await mkdir(dirname(worktree), { recursive: true });
|
|
606
|
+
await checked(["git", "-C", hostCwd, "worktree", "add", "-B", branch, worktree, "HEAD"], timeoutMs);
|
|
607
|
+
return {
|
|
608
|
+
hostCwd,
|
|
609
|
+
runtimeCwd: worktree,
|
|
610
|
+
branch,
|
|
611
|
+
worktree,
|
|
612
|
+
mergeToHead: strategy.type === "merge-to-head",
|
|
613
|
+
originalHead
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async function mergeBranch(git, timeoutMs = 60000) {
|
|
617
|
+
if (!git.branch)
|
|
618
|
+
throw new Error("merge branch is unavailable");
|
|
619
|
+
await checked(["git", "-C", git.hostCwd, "merge", "--no-edit", git.branch], timeoutMs);
|
|
620
|
+
}
|
|
621
|
+
async function gitHead(cwd) {
|
|
622
|
+
const result = await executeProcess(["git", "-C", cwd, "rev-parse", "HEAD"]);
|
|
623
|
+
return result.exitCode === 0 ? result.stdout.trim() : "";
|
|
624
|
+
}
|
|
625
|
+
async function collectCommits(cwd, baseline, timeoutMs = 30000) {
|
|
626
|
+
if (!baseline)
|
|
627
|
+
return [];
|
|
628
|
+
const result = await executeProcess(["git", "-C", cwd, "log", "--format=%H%x00%s", `${baseline}..HEAD`], {
|
|
629
|
+
timeoutMs
|
|
630
|
+
});
|
|
631
|
+
if (result.exitCode !== 0 || !result.stdout.trim())
|
|
632
|
+
return [];
|
|
633
|
+
return result.stdout.trim().split(`
|
|
634
|
+
`).map((line) => {
|
|
635
|
+
const [sha, message] = line.split("\x00");
|
|
636
|
+
if (!sha)
|
|
637
|
+
throw new Error("Git returned an invalid commit record");
|
|
638
|
+
return { sha, ...message ? { message } : {} };
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
async function checked(command, timeoutMs) {
|
|
642
|
+
const result = await executeProcess(command, { timeoutMs });
|
|
643
|
+
if (result.exitCode !== 0)
|
|
644
|
+
throw new Error(`${command[0]} failed: ${result.stderr || result.stdout}`);
|
|
645
|
+
}
|
|
646
|
+
async function runHostHooks(hooks, cwd) {
|
|
647
|
+
for (const hook of hooks ?? []) {
|
|
648
|
+
const command = hookCommand(hook);
|
|
649
|
+
const result = await executeProcess(command, {
|
|
650
|
+
cwd: resolve(cwd, hook.cwd ?? "."),
|
|
651
|
+
env: hook.env,
|
|
652
|
+
timeoutMs: hook.timeoutMs
|
|
653
|
+
});
|
|
654
|
+
if (result.exitCode !== 0)
|
|
655
|
+
throw new Error(`host hook failed: ${result.stderr || result.stdout}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function runSandboxHooks(runtime, hooks) {
|
|
659
|
+
for (const hook of hooks ?? []) {
|
|
660
|
+
const result = await runtime.exec(hookCommand(hook), {
|
|
661
|
+
cwd: hook.cwd,
|
|
662
|
+
env: hook.env,
|
|
663
|
+
timeoutMs: hook.timeoutMs
|
|
664
|
+
});
|
|
665
|
+
if (result.exitCode !== 0)
|
|
666
|
+
throw new Error(`sandbox hook failed: ${result.stderr || result.stdout}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function hookCommand(hook) {
|
|
670
|
+
if (Boolean(hook.command) === Boolean(hook.shell))
|
|
671
|
+
throw new Error("hook requires exactly one of command or shell");
|
|
672
|
+
if (hook.command)
|
|
673
|
+
return hook.command;
|
|
674
|
+
if (!hook.shell)
|
|
675
|
+
throw new Error("hook shell command is required");
|
|
676
|
+
return ["sh", "-lc", hook.shell];
|
|
677
|
+
}
|
|
678
|
+
function createLogger(options, cwd) {
|
|
679
|
+
const config = options ?? { type: "file" };
|
|
680
|
+
const file = resolve(cwd, config.path ?? `.sandblocks/logs/${Date.now()}.log`);
|
|
681
|
+
return async (event) => {
|
|
682
|
+
const line = `${JSON.stringify(event)}
|
|
683
|
+
`;
|
|
684
|
+
if (config.type === "stdout")
|
|
685
|
+
process.stdout.write(event.type === "text" ? event.text : event.type === "raw" ? `${event.line}
|
|
686
|
+
` : line);
|
|
687
|
+
if (config.type !== "stdout" && config.type !== "silent") {
|
|
688
|
+
await mkdir(dirname(file), { recursive: true });
|
|
689
|
+
await appendFile(file, line);
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
await config.onAgentStreamEvent?.(event);
|
|
693
|
+
} catch {}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
697
|
+
let timer;
|
|
698
|
+
try {
|
|
699
|
+
return await Promise.race([
|
|
700
|
+
promise,
|
|
701
|
+
new Promise((_, reject) => {
|
|
702
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
703
|
+
})
|
|
704
|
+
]);
|
|
705
|
+
} finally {
|
|
706
|
+
if (timer)
|
|
707
|
+
clearTimeout(timer);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function assertProviderCapabilities(provider, options) {
|
|
711
|
+
const capabilities = provider.capabilities;
|
|
712
|
+
if (options.mounts?.length && !capabilities.bindMounts)
|
|
713
|
+
throw new Error(`${provider.kind} provider does not support bind mounts`);
|
|
714
|
+
if (options.network && !capabilities.networks)
|
|
715
|
+
throw new Error(`${provider.kind} provider does not support network selection`);
|
|
716
|
+
if (options.devices?.length && !capabilities.devices)
|
|
717
|
+
throw new Error(`${provider.kind} provider does not support devices`);
|
|
718
|
+
if ((options.retention === "always" || options.retention === "on-failure") && !capabilities.persistent)
|
|
719
|
+
throw new Error(`${provider.kind} provider does not support retained sandboxes`);
|
|
720
|
+
}
|
|
721
|
+
function array(value) {
|
|
722
|
+
return Array.isArray(value) ? value : [value];
|
|
723
|
+
}
|
|
724
|
+
// src/init.ts
|
|
725
|
+
import { access, mkdir as mkdir2, writeFile } from "node:fs/promises";
|
|
726
|
+
import { resolve as resolve2 } from "node:path";
|
|
727
|
+
async function initSdk(options = {}) {
|
|
728
|
+
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
729
|
+
const root = resolve2(cwd, ".sandblocks");
|
|
730
|
+
const files = {
|
|
731
|
+
"main.ts": `import { ClaudeCodeAgent, createSandbox } from "@sandblocks/sdk";
|
|
732
|
+
import { DockerProvider } from "@sandblocks/sdk/providers/docker";
|
|
733
|
+
|
|
734
|
+
await using sandbox = await createSandbox({
|
|
735
|
+
provider: new DockerProvider({ image: "node:22-bookworm" }),
|
|
736
|
+
cwd: process.cwd(),
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const result = await sandbox.run({
|
|
740
|
+
agent: new ClaudeCodeAgent({ model: "claude-sonnet-4-6" }),
|
|
741
|
+
promptFile: ".sandblocks/prompts/implement.md",
|
|
742
|
+
logging: { type: "stdout" },
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
console.log(result);
|
|
746
|
+
`,
|
|
747
|
+
"prompts/implement.md": `Implement the requested change, run the relevant checks, and commit the result.
|
|
748
|
+
|
|
749
|
+
<promise>COMPLETE</promise>
|
|
750
|
+
`,
|
|
751
|
+
".env.example": `# Prefer Sandblocks managed secrets for remote providers.
|
|
752
|
+
CLAUDE_CODE_OAUTH_TOKEN=
|
|
753
|
+
# ANTHROPIC_API_KEY=
|
|
754
|
+
SANDBLOCKS_API_URL=
|
|
755
|
+
SANDBLOCKS_API_KEY=
|
|
756
|
+
`,
|
|
757
|
+
"config.json": `${JSON.stringify({ provider: "docker", image: "node:22-bookworm", agent: "claude-code", model: "claude-sonnet-4-6", branchStrategy: "head" }, null, 2)}
|
|
758
|
+
`
|
|
759
|
+
};
|
|
760
|
+
const written = [];
|
|
761
|
+
for (const [relative, content] of Object.entries(files)) {
|
|
762
|
+
const path = resolve2(root, relative);
|
|
763
|
+
await mkdir2(resolve2(path, ".."), { recursive: true });
|
|
764
|
+
if (!options.force && await access(path).then(() => true).catch(() => false))
|
|
765
|
+
continue;
|
|
766
|
+
await writeFile(path, content, { mode: relative.includes(".env") ? 384 : 420 });
|
|
767
|
+
written.push(path);
|
|
768
|
+
}
|
|
769
|
+
return written;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/index.ts
|
|
773
|
+
class SandblocksClient {
|
|
774
|
+
options;
|
|
775
|
+
baseUrl;
|
|
776
|
+
fetchImpl;
|
|
777
|
+
constructor(options) {
|
|
778
|
+
this.options = options;
|
|
779
|
+
if (!options.apiKey.trim())
|
|
780
|
+
throw new Error("Sandblocks API key is required");
|
|
781
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
782
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
783
|
+
}
|
|
784
|
+
listSandboxes(projectId) {
|
|
785
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes`);
|
|
786
|
+
}
|
|
787
|
+
recordSandbox(projectId, sandbox) {
|
|
788
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes`, {
|
|
789
|
+
method: "POST",
|
|
790
|
+
body: JSON.stringify(sandbox)
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
deployServices(input) {
|
|
794
|
+
return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/deployments`, {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: { "idempotency-key": input.idempotencyKey },
|
|
797
|
+
body: JSON.stringify(input)
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
destroyServices(input) {
|
|
801
|
+
return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/deployments/${encodeURIComponent(input.deploymentId)}`, {
|
|
802
|
+
method: "DELETE",
|
|
803
|
+
headers: { "idempotency-key": input.idempotencyKey }
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
issueSandboxLease(input) {
|
|
807
|
+
return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/sandboxes/${encodeURIComponent(input.sandboxId)}/leases`, {
|
|
808
|
+
method: "POST",
|
|
809
|
+
body: JSON.stringify({ subject: input.subject, scopes: input.scopes, ttlSeconds: input.ttlSeconds })
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
listSandboxLeases(projectId, sandboxId) {
|
|
813
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/leases`);
|
|
814
|
+
}
|
|
815
|
+
revokeSandboxLease(projectId, sandboxId, leaseId) {
|
|
816
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/leases/${encodeURIComponent(leaseId)}`, { method: "DELETE" });
|
|
817
|
+
}
|
|
818
|
+
listRoutingAliases(projectId) {
|
|
819
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-aliases`);
|
|
820
|
+
}
|
|
821
|
+
listRoutingHistory(projectId) {
|
|
822
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-aliases/history`);
|
|
823
|
+
}
|
|
824
|
+
promoteRoutingAliases(projectId, sandboxId) {
|
|
825
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-aliases/promote`, {
|
|
826
|
+
method: "POST",
|
|
827
|
+
body: JSON.stringify({ sandboxId })
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
getRoutingPolicy(projectId, stack, deploymentType) {
|
|
831
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(deploymentType)}`);
|
|
832
|
+
}
|
|
833
|
+
setRoutingPolicy(projectId, stack, deploymentType, policy) {
|
|
834
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-policies/${encodeURIComponent(stack)}/${encodeURIComponent(deploymentType)}`, {
|
|
835
|
+
method: "PUT",
|
|
836
|
+
body: JSON.stringify(policy)
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
rollbackRoutingAliases(projectId, sandboxId) {
|
|
840
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/routing-aliases/rollback`, {
|
|
841
|
+
method: "POST",
|
|
842
|
+
body: JSON.stringify({ sandboxId })
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
getSandbox(projectId, sandboxId) {
|
|
846
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}`);
|
|
847
|
+
}
|
|
848
|
+
createWorkspace(input) {
|
|
849
|
+
return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/workspaces`, {
|
|
850
|
+
method: "POST",
|
|
851
|
+
headers: {
|
|
852
|
+
"idempotency-key": input.idempotencyKey,
|
|
853
|
+
...input.gitCredential ? { "x-sandblocks-git-credential": input.gitCredential } : {}
|
|
854
|
+
},
|
|
855
|
+
body: JSON.stringify({
|
|
856
|
+
workspaceId: input.workspaceId,
|
|
857
|
+
repositoryUrl: input.repositoryUrl,
|
|
858
|
+
revision: input.revision,
|
|
859
|
+
placement: input.placement
|
|
860
|
+
})
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
executeWorkspace(input) {
|
|
864
|
+
return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(input.workspaceId)}/exec`, {
|
|
865
|
+
method: "POST",
|
|
866
|
+
headers: { "idempotency-key": input.idempotencyKey },
|
|
867
|
+
body: JSON.stringify({ executable: input.executable, args: input.args })
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
destroyWorkspace(input) {
|
|
871
|
+
return this.request(`/v1/projects/${encodeURIComponent(input.projectId)}/workspaces/${encodeURIComponent(input.workspaceId)}`, { method: "DELETE", headers: { "idempotency-key": input.idempotencyKey } });
|
|
872
|
+
}
|
|
873
|
+
getOperation(operationId) {
|
|
874
|
+
return this.request(`/v1/operations/${encodeURIComponent(operationId)}`);
|
|
875
|
+
}
|
|
876
|
+
listOperations(projectId, sandboxId) {
|
|
877
|
+
const query = sandboxId ? `?sandboxId=${encodeURIComponent(sandboxId)}` : "";
|
|
878
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/operations${query}`);
|
|
879
|
+
}
|
|
880
|
+
listOperationEvents(operationId) {
|
|
881
|
+
return this.request(`/v1/operations/${encodeURIComponent(operationId)}/events`);
|
|
882
|
+
}
|
|
883
|
+
cancelOperation(operationId) {
|
|
884
|
+
return this.request(`/v1/operations/${encodeURIComponent(operationId)}/cancel`, { method: "POST" });
|
|
885
|
+
}
|
|
886
|
+
cancelSandboxOperations(projectId, sandboxId) {
|
|
887
|
+
return this.request(`/v1/projects/${encodeURIComponent(projectId)}/sandboxes/${encodeURIComponent(sandboxId)}/operations/cancel`, { method: "POST" });
|
|
888
|
+
}
|
|
889
|
+
async waitForOperation(operationId, options = {}) {
|
|
890
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120000);
|
|
891
|
+
while (Date.now() < deadline) {
|
|
892
|
+
const { operation } = await this.getOperation(operationId);
|
|
893
|
+
if (operation.state === "succeeded")
|
|
894
|
+
return operation;
|
|
895
|
+
if (["failed", "cancelled"].includes(operation.state)) {
|
|
896
|
+
throw new Error(operation.failureReason ?? `Sandblocks operation ${operation.state}`);
|
|
897
|
+
}
|
|
898
|
+
await new Promise((resolve3) => setTimeout(resolve3, options.pollMs ?? 500));
|
|
899
|
+
}
|
|
900
|
+
throw new Error("Sandblocks operation timed out");
|
|
901
|
+
}
|
|
902
|
+
async request(path, init2 = {}) {
|
|
903
|
+
const headers = new Headers(init2.headers);
|
|
904
|
+
headers.set("x-sandblocks-api-key", this.options.apiKey);
|
|
905
|
+
if (init2.body)
|
|
906
|
+
headers.set("content-type", "application/json");
|
|
907
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, { ...init2, headers });
|
|
908
|
+
const body = await response.json().catch(() => ({}));
|
|
909
|
+
if (!response.ok)
|
|
910
|
+
throw new Error(body.error ?? `Sandblocks request failed (${response.status})`);
|
|
911
|
+
return body;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
class Sandblocks extends SandblocksClient {
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
class SandblocksSandboxClient {
|
|
919
|
+
baseUrl;
|
|
920
|
+
fetchImpl;
|
|
921
|
+
leaseToken;
|
|
922
|
+
sandboxId;
|
|
923
|
+
constructor(options) {
|
|
924
|
+
if (!options.leaseToken.startsWith("sbs_"))
|
|
925
|
+
throw new Error("Sandblocks sandbox lease token is required");
|
|
926
|
+
if (!/^[a-f0-9-]{36}$/.test(options.sandboxId))
|
|
927
|
+
throw new Error("Sandblocks sandbox ID is required");
|
|
928
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
929
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
930
|
+
this.leaseToken = options.leaseToken;
|
|
931
|
+
this.sandboxId = options.sandboxId;
|
|
932
|
+
}
|
|
933
|
+
getSession() {
|
|
934
|
+
return this.request("/v1/sandbox-session");
|
|
935
|
+
}
|
|
936
|
+
readFile(repository, path, idempotencyKey) {
|
|
937
|
+
return this.workspaceRequest(repository, "read", { path }, idempotencyKey);
|
|
938
|
+
}
|
|
939
|
+
search(repository, query, input, idempotencyKey) {
|
|
940
|
+
return this.workspaceRequest(repository, "search", { query, ...input }, idempotencyKey);
|
|
941
|
+
}
|
|
942
|
+
writeFile(repository, path, content, idempotencyKey) {
|
|
943
|
+
return this.workspaceRequest(repository, "write", { path, content }, idempotencyKey);
|
|
944
|
+
}
|
|
945
|
+
execute(repository, executable, args, idempotencyKey) {
|
|
946
|
+
return this.workspaceRequest(repository, "exec", { executable, args }, idempotencyKey);
|
|
947
|
+
}
|
|
948
|
+
git(repository, input, idempotencyKey) {
|
|
949
|
+
const { gitCredential, ...body } = input;
|
|
950
|
+
return this.workspaceRequest(repository, "git", body, idempotencyKey, gitCredential);
|
|
951
|
+
}
|
|
952
|
+
getOperation(operationId) {
|
|
953
|
+
return this.request(`/v1/sandbox-operations/${encodeURIComponent(operationId)}`);
|
|
954
|
+
}
|
|
955
|
+
listOperationEvents(operationId, after = 0) {
|
|
956
|
+
return this.request(`/v1/sandbox-operations/${encodeURIComponent(operationId)}/events?after=${after}`);
|
|
957
|
+
}
|
|
958
|
+
cancelOperation(operationId) {
|
|
959
|
+
return this.request(`/v1/sandbox-operations/${encodeURIComponent(operationId)}/cancel`, {
|
|
960
|
+
method: "POST"
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
async waitForOperation(operationId, options = {}) {
|
|
964
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120000);
|
|
965
|
+
while (Date.now() < deadline) {
|
|
966
|
+
const { operation } = await this.getOperation(operationId);
|
|
967
|
+
if (operation.state === "succeeded")
|
|
968
|
+
return operation;
|
|
969
|
+
if (["failed", "cancelled"].includes(operation.state))
|
|
970
|
+
throw new Error(operation.failureReason ?? `Sandblocks operation ${operation.state}`);
|
|
971
|
+
await new Promise((resolve3) => setTimeout(resolve3, options.pollMs ?? 500));
|
|
972
|
+
}
|
|
973
|
+
throw new Error("Sandblocks sandbox operation timed out");
|
|
974
|
+
}
|
|
975
|
+
workspaceRequest(repository, action, body, idempotencyKey, gitCredential) {
|
|
976
|
+
return this.request(`/v1/sandboxes/${encodeURIComponent(this.sandboxId)}/workspaces/${encodeURIComponent(repository)}/${action}`, {
|
|
977
|
+
method: "POST",
|
|
978
|
+
headers: {
|
|
979
|
+
"idempotency-key": idempotencyKey,
|
|
980
|
+
...gitCredential ? { "x-sandblocks-git-credential": gitCredential } : {}
|
|
981
|
+
},
|
|
982
|
+
body: JSON.stringify(body)
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
async request(path, init2 = {}) {
|
|
986
|
+
const headers = new Headers(init2.headers);
|
|
987
|
+
headers.set("authorization", `Bearer ${this.leaseToken}`);
|
|
988
|
+
if (init2.body)
|
|
989
|
+
headers.set("content-type", "application/json");
|
|
990
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, { ...init2, headers });
|
|
991
|
+
const body = await response.json().catch(() => ({}));
|
|
992
|
+
if (!response.ok)
|
|
993
|
+
throw new Error(body.error ?? `Sandblocks sandbox request failed (${response.status})`);
|
|
994
|
+
return body;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
export {
|
|
998
|
+
run,
|
|
999
|
+
pi,
|
|
1000
|
+
initSdk,
|
|
1001
|
+
extractOutput,
|
|
1002
|
+
createSandboxProvider,
|
|
1003
|
+
createSandbox,
|
|
1004
|
+
createIsolatedSandboxProvider,
|
|
1005
|
+
createBindMountSandboxProvider,
|
|
1006
|
+
connectSandbox,
|
|
1007
|
+
codex,
|
|
1008
|
+
claudeCode,
|
|
1009
|
+
SandblocksSandboxClient,
|
|
1010
|
+
SandblocksSandbox,
|
|
1011
|
+
SandblocksClient,
|
|
1012
|
+
Sandblocks,
|
|
1013
|
+
PiAgent,
|
|
1014
|
+
Output,
|
|
1015
|
+
CommandAgent,
|
|
1016
|
+
CodexAgent,
|
|
1017
|
+
ClaudeCodeAgent
|
|
1018
|
+
};
|