withruntime 0.4.0 → 0.5.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/DAYTONA.md +129 -0
- package/E2B.md +2 -2
- package/README.md +47 -9
- package/VERCEL.md +127 -0
- package/dist/ai/index.d.ts +33 -0
- package/dist/ai/index.js +38 -0
- package/dist/claude-agent-sdk/index.d.ts +7 -0
- package/dist/claude-agent-sdk/index.js +65 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +939 -102
- package/dist/client.d.ts +25 -2
- package/dist/client.js +14 -1
- package/dist/credentials.d.ts +10 -0
- package/dist/credentials.js +56 -1
- package/dist/daytona/client.d.ts +44 -0
- package/dist/daytona/client.js +40 -0
- package/dist/daytona/context.d.ts +26 -0
- package/dist/daytona/context.js +22 -0
- package/dist/daytona/daytona.d.ts +185 -0
- package/dist/daytona/daytona.js +416 -0
- package/dist/daytona/errors.d.ts +106 -0
- package/dist/daytona/errors.js +182 -0
- package/dist/daytona/filesystem.d.ts +84 -0
- package/dist/daytona/filesystem.js +206 -0
- package/dist/daytona/git.d.ts +33 -0
- package/dist/daytona/git.js +128 -0
- package/dist/daytona/image.d.ts +45 -0
- package/dist/daytona/image.js +140 -0
- package/dist/daytona/index.d.ts +50 -0
- package/dist/daytona/index.js +50 -0
- package/dist/daytona/process.d.ts +86 -0
- package/dist/daytona/process.js +339 -0
- package/dist/daytona/sandbox.d.ts +196 -0
- package/dist/daytona/sandbox.js +448 -0
- package/dist/e2b/sandbox.d.ts +22 -2
- package/dist/e2b/sandbox.js +39 -7
- package/dist/errors.d.ts +10 -4
- package/dist/errors.js +26 -6
- package/dist/image-context.d.ts +39 -0
- package/dist/image-context.js +176 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +17 -2
- package/dist/keys.d.ts +43 -0
- package/dist/keys.js +196 -0
- package/dist/login.js +14 -2
- package/dist/openai-agents/index.d.ts +132 -0
- package/dist/openai-agents/index.js +791 -0
- package/dist/products/audit.d.ts +48 -0
- package/dist/products/audit.js +10 -0
- package/dist/products/images.d.ts +113 -7
- package/dist/products/images.js +186 -27
- package/dist/products/index.d.ts +11 -0
- package/dist/products/index.js +17 -1
- package/dist/products/observability.d.ts +186 -0
- package/dist/products/observability.js +117 -0
- package/dist/products/secrets.d.ts +40 -0
- package/dist/products/secrets.js +24 -0
- package/dist/products/switching.d.ts +89 -0
- package/dist/products/switching.js +25 -0
- package/dist/sandbox.d.ts +26 -1
- package/dist/sandbox.js +101 -1
- package/dist/ssh.d.ts +21 -0
- package/dist/ssh.js +312 -0
- package/dist/tar.d.ts +1 -0
- package/dist/tar.js +4 -4
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.js +180 -0
- package/dist/transport.d.ts +12 -2
- package/dist/transport.js +31 -4
- package/dist/tunnel.d.ts +67 -0
- package/dist/tunnel.js +311 -0
- package/dist/types.d.ts +57 -1
- package/dist/vercel/client.d.ts +40 -0
- package/dist/vercel/client.js +39 -0
- package/dist/vercel/command.d.ts +70 -0
- package/dist/vercel/command.js +148 -0
- package/dist/vercel/errors.d.ts +53 -0
- package/dist/vercel/errors.js +113 -0
- package/dist/vercel/filesystem.d.ts +150 -0
- package/dist/vercel/filesystem.js +248 -0
- package/dist/vercel/index.d.ts +30 -0
- package/dist/vercel/index.js +38 -0
- package/dist/vercel/sandbox.d.ts +317 -0
- package/dist/vercel/sandbox.js +726 -0
- package/dist/vercel/snapshot.d.ts +93 -0
- package/dist/vercel/snapshot.js +123 -0
- package/package.json +56 -4
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/** Runtime Cloud sandboxes for the OpenAI Agents SDK (JavaScript).
|
|
2
|
+
*
|
|
3
|
+
* `RuntimeCloudSandboxClient` is a sandbox client for `SandboxAgent`: the
|
|
4
|
+
* agent's shell (with PTY sessions and stdin), file edits, image views and
|
|
5
|
+
* manifest all live in one Runtime Cloud microVM, and the agent definition
|
|
6
|
+
* does not change:
|
|
7
|
+
*
|
|
8
|
+
* import { run } from "@openai/agents";
|
|
9
|
+
* import { SandboxAgent } from "@openai/agents/sandbox";
|
|
10
|
+
* import { RuntimeCloudSandboxClient } from "withruntime/openai-agents";
|
|
11
|
+
*
|
|
12
|
+
* const agent = new SandboxAgent({ name: "Coder", instructions: "Fix the failing test." });
|
|
13
|
+
* await run(agent, "Go.", {
|
|
14
|
+
* sandbox: { client: new RuntimeCloudSandboxClient({ create: { funding: "trial" } }) },
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* Needs `@openai/agents` 0.18 or newer beside this package. The key comes from
|
|
18
|
+
* RUNTIME_API_KEY or this machine's `runtime login`. Plain function tools for
|
|
19
|
+
* an ordinary `Agent` are `runtimeFunctionTools(sandbox)`. */
|
|
20
|
+
import { applyDiff, tool, UserError, } from "@openai/agents-core";
|
|
21
|
+
import { SandboxProviderError, SandboxUnsupportedFeatureError, normalizeSandboxClientCreateArgs, } from "@openai/agents-core/sandbox";
|
|
22
|
+
import { deserializeManifest, elapsedSeconds, formatExecResponse, imageOutputFromBytes, materializeEnvironment, mergeManifestDelta, mergeManifestEntryDelta, serializeManifestRecord, shellQuote, truncateOutput, } from "@openai/agents-core/sandbox/internal";
|
|
23
|
+
import { Runtime } from "../client.js";
|
|
24
|
+
import { NotFoundError, RuntimeError } from "../errors.js";
|
|
25
|
+
import { sandboxTools } from "../tools/index.js";
|
|
26
|
+
export const BACKEND_ID = "runtime_cloud";
|
|
27
|
+
const PROVIDER = "RuntimeCloudSandboxClient";
|
|
28
|
+
const FILES_HOME = "/workspace";
|
|
29
|
+
const STAGING = "/workspace/.openai-agents-staging";
|
|
30
|
+
const MAX_EXEC_MS = 86_400_000;
|
|
31
|
+
const YIELD_MIN_MS = 250;
|
|
32
|
+
const YIELD_EMPTY_MIN_MS = 5_000;
|
|
33
|
+
const YIELD_MAX_MS = 30_000;
|
|
34
|
+
const MAX_PROCESSES = 64;
|
|
35
|
+
const isRuntimeError = (error) => error instanceof RuntimeError;
|
|
36
|
+
const clampYield = (ms) => Math.max(YIELD_MIN_MS, Math.min(YIELD_MAX_MS, ms));
|
|
37
|
+
const inFilesApi = (path) => path.startsWith(`${FILES_HOME}/`);
|
|
38
|
+
function normalize(path) {
|
|
39
|
+
const parts = [];
|
|
40
|
+
for (const part of path.split("/")) {
|
|
41
|
+
if (!part || part === ".")
|
|
42
|
+
continue;
|
|
43
|
+
if (part === "..")
|
|
44
|
+
parts.pop();
|
|
45
|
+
else
|
|
46
|
+
parts.push(part);
|
|
47
|
+
}
|
|
48
|
+
return `/${parts.join("/")}`;
|
|
49
|
+
}
|
|
50
|
+
function providerError(message, error, details = {}) {
|
|
51
|
+
return new SandboxProviderError(message, {
|
|
52
|
+
provider: BACKEND_ID,
|
|
53
|
+
...details,
|
|
54
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
55
|
+
...(isRuntimeError(error) ? { code: error.code } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** One Runtime Cloud sandbox as an Agents SDK sandbox session. */
|
|
59
|
+
export class RuntimeCloudSandboxSession {
|
|
60
|
+
state;
|
|
61
|
+
#runtime;
|
|
62
|
+
#sandbox;
|
|
63
|
+
#paused = false;
|
|
64
|
+
#processes = new Map();
|
|
65
|
+
constructor(args) {
|
|
66
|
+
this.state = args.state;
|
|
67
|
+
this.#runtime = args.runtime;
|
|
68
|
+
this.#sandbox = args.sandbox;
|
|
69
|
+
}
|
|
70
|
+
/** The Runtime sandbox, created (or woken) on first use. */
|
|
71
|
+
async sandbox() {
|
|
72
|
+
if (this.#sandbox && this.#paused) {
|
|
73
|
+
await this.#sandbox.wake();
|
|
74
|
+
this.#paused = false;
|
|
75
|
+
}
|
|
76
|
+
if (!this.#sandbox) {
|
|
77
|
+
this.#sandbox = await this.#runtime.sandboxes.create(this.state.create);
|
|
78
|
+
this.state.sandboxId = this.#sandbox.id;
|
|
79
|
+
}
|
|
80
|
+
return this.#sandbox;
|
|
81
|
+
}
|
|
82
|
+
/** Runs `operation`, waking the sandbox once if it paused itself when idle. */
|
|
83
|
+
async #call(operation) {
|
|
84
|
+
const sandbox = await this.sandbox();
|
|
85
|
+
try {
|
|
86
|
+
return await operation(sandbox);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!isRuntimeError(error) || error.code !== "sandbox_paused")
|
|
90
|
+
throw error;
|
|
91
|
+
await sandbox.wake();
|
|
92
|
+
return await operation(sandbox);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Paths.
|
|
96
|
+
/** An absolute path inside the workspace root or a granted path; anything else is refused. */
|
|
97
|
+
resolvePath(path) {
|
|
98
|
+
const root = this.state.manifest.root;
|
|
99
|
+
const resolved = normalize(path.startsWith("/") ? path : `${root}/${path}`);
|
|
100
|
+
const allowed = [root, ...this.state.manifest.extraPathGrants.map((grant) => grant.path)];
|
|
101
|
+
if (!allowed.some((base) => resolved === base || resolved.startsWith(`${base === "/" ? "" : base}/`)))
|
|
102
|
+
throw new UserError(`Path is outside the sandbox workspace: ${path}`);
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
#workdir(workdir) {
|
|
106
|
+
return workdir ? this.resolvePath(workdir) : this.state.manifest.root;
|
|
107
|
+
}
|
|
108
|
+
// Commands.
|
|
109
|
+
supportsPty() {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
#argv(args) {
|
|
113
|
+
const shellPath = args.shell ?? "/bin/sh";
|
|
114
|
+
const flag = args.shell ? ((args.login ?? true) ? "-lc" : "-c") : "-c";
|
|
115
|
+
const command = [shellPath, flag, args.cmd];
|
|
116
|
+
if (!args.runAs)
|
|
117
|
+
return command;
|
|
118
|
+
const env = Object.entries(this.state.environment).map(([key, value]) => `${key}=${value}`);
|
|
119
|
+
return ["sudo", "-n", "-u", args.runAs, "--", "env", ...env, ...command];
|
|
120
|
+
}
|
|
121
|
+
async exec(args) {
|
|
122
|
+
const start = Date.now();
|
|
123
|
+
const result = await this.#call((sbx) => sbx.exec(this.#argv(args), {
|
|
124
|
+
cwd: this.#workdir(args.workdir),
|
|
125
|
+
env: this.state.environment,
|
|
126
|
+
timeoutMs: Math.min(MAX_EXEC_MS, args.yieldTimeMs ?? this.state.execTimeoutMs),
|
|
127
|
+
}));
|
|
128
|
+
return {
|
|
129
|
+
output: result.stdout + result.stderr,
|
|
130
|
+
stdout: result.stdout,
|
|
131
|
+
stderr: result.stderr,
|
|
132
|
+
wallTimeSeconds: elapsedSeconds(start),
|
|
133
|
+
exitCode: result.timedOut ? null : result.exitCode,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Starts the command and returns its output so far: finished, with its exit
|
|
137
|
+
* code, or still running, with a session id for `writeStdin`. */
|
|
138
|
+
async execCommand(args) {
|
|
139
|
+
const start = Date.now();
|
|
140
|
+
let process;
|
|
141
|
+
try {
|
|
142
|
+
process = await this.#call((sbx) => sbx.spawn(this.#argv(args), {
|
|
143
|
+
cwd: this.#workdir(args.workdir),
|
|
144
|
+
env: this.state.environment,
|
|
145
|
+
timeoutMs: Math.min(MAX_EXEC_MS, this.state.execTimeoutMs),
|
|
146
|
+
...(args.tty ? { pty: { cols: 80, rows: 24 } } : {}),
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw providerError(`${PROVIDER} could not start the command.`, error, {
|
|
151
|
+
sandboxId: this.state.sandboxId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const entry = {
|
|
155
|
+
process,
|
|
156
|
+
tty: Boolean(args.tty),
|
|
157
|
+
chunks: [],
|
|
158
|
+
exitCode: undefined,
|
|
159
|
+
done: false,
|
|
160
|
+
lastUsed: Date.now(),
|
|
161
|
+
};
|
|
162
|
+
entry.pump = this.#pump(entry);
|
|
163
|
+
const sessionId = await this.#register(entry);
|
|
164
|
+
return this.#collect(sessionId, entry, clampYield(args.yieldTimeMs ?? 10_000), start, args.maxOutputTokens);
|
|
165
|
+
}
|
|
166
|
+
async writeStdin(args) {
|
|
167
|
+
const start = Date.now();
|
|
168
|
+
const entry = this.#processes.get(args.sessionId);
|
|
169
|
+
if (!entry)
|
|
170
|
+
throw new UserError(`No running process with session ID ${args.sessionId}.`);
|
|
171
|
+
const chars = args.chars ?? "";
|
|
172
|
+
if (chars) {
|
|
173
|
+
if (!entry.tty)
|
|
174
|
+
throw new UserError("This process has no stdin; start it with tty: true to type into it.");
|
|
175
|
+
await entry.process.write(chars);
|
|
176
|
+
}
|
|
177
|
+
entry.lastUsed = Date.now();
|
|
178
|
+
const yieldMs = clampYield(args.yieldTimeMs ?? 250);
|
|
179
|
+
return this.#collect(args.sessionId, entry, chars ? yieldMs : Math.max(yieldMs, YIELD_EMPTY_MIN_MS), start, args.maxOutputTokens);
|
|
180
|
+
}
|
|
181
|
+
async #register(entry) {
|
|
182
|
+
if (this.#processes.size >= MAX_PROCESSES) {
|
|
183
|
+
const oldest = [...this.#processes.entries()].sort((a, b) => a[1].lastUsed - b[1].lastUsed)[0];
|
|
184
|
+
if (oldest) {
|
|
185
|
+
this.#processes.delete(oldest[0]);
|
|
186
|
+
await this.#terminate(oldest[1]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let id;
|
|
190
|
+
do
|
|
191
|
+
id = 1_000 + Math.floor(Math.random() * 99_000);
|
|
192
|
+
while (this.#processes.has(id));
|
|
193
|
+
this.#processes.set(id, entry);
|
|
194
|
+
return id;
|
|
195
|
+
}
|
|
196
|
+
/** Moves the process's output into `entry` until it exits; a dropped stream resumes a few times. */
|
|
197
|
+
async #pump(entry) {
|
|
198
|
+
let cursor = 0;
|
|
199
|
+
let failures = 0;
|
|
200
|
+
try {
|
|
201
|
+
for (;;) {
|
|
202
|
+
try {
|
|
203
|
+
for await (const event of entry.process.output({ cursor })) {
|
|
204
|
+
failures = 0;
|
|
205
|
+
if (event.type === "stdout" || event.type === "stderr") {
|
|
206
|
+
cursor = event.offset + Buffer.byteLength(event.data);
|
|
207
|
+
entry.chunks.push(event.data);
|
|
208
|
+
entry.wake?.();
|
|
209
|
+
}
|
|
210
|
+
else if (event.type === "exit")
|
|
211
|
+
entry.exitCode = event.exitCode ?? -1;
|
|
212
|
+
}
|
|
213
|
+
if (entry.exitCode === undefined) {
|
|
214
|
+
const info = await entry.process.refresh();
|
|
215
|
+
if (info.state !== "running")
|
|
216
|
+
entry.exitCode = info.exitCode ?? -1;
|
|
217
|
+
}
|
|
218
|
+
if (entry.exitCode !== undefined)
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
failures += 1;
|
|
223
|
+
if (failures > 4 || (isRuntimeError(error) && !error.retryable)) {
|
|
224
|
+
entry.chunks.push(`\n[Runtime: the output stream failed: ${String(error)}]\n`);
|
|
225
|
+
entry.exitCode = -1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 200 * failures));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
entry.done = true;
|
|
234
|
+
entry.wake?.();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async #collect(sessionId, entry, yieldMs, start, maxOutputTokens) {
|
|
238
|
+
const deadline = Date.now() + yieldMs;
|
|
239
|
+
while (!entry.done && Date.now() < deadline) {
|
|
240
|
+
await new Promise((resolve) => {
|
|
241
|
+
const timer = setTimeout(resolve, deadline - Date.now());
|
|
242
|
+
entry.wake = () => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
resolve();
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
entry.wake = undefined;
|
|
248
|
+
}
|
|
249
|
+
const output = truncateOutput(entry.chunks.splice(0).join(""), maxOutputTokens);
|
|
250
|
+
const finished = entry.done && entry.exitCode !== undefined;
|
|
251
|
+
if (finished)
|
|
252
|
+
this.#processes.delete(sessionId);
|
|
253
|
+
return formatExecResponse({
|
|
254
|
+
output: output.text,
|
|
255
|
+
wallTimeSeconds: elapsedSeconds(start),
|
|
256
|
+
...(finished ? { exitCode: entry.exitCode } : { sessionId }),
|
|
257
|
+
...(output.originalTokenCount === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { originalTokenCount: output.originalTokenCount }),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async #terminate(entry) {
|
|
263
|
+
if (!entry.done)
|
|
264
|
+
await entry.process.kill("SIGKILL").catch(() => undefined);
|
|
265
|
+
}
|
|
266
|
+
// Files.
|
|
267
|
+
async #readBytes(path) {
|
|
268
|
+
if (inFilesApi(path))
|
|
269
|
+
return this.#call((sbx) => sbx.files.read(path));
|
|
270
|
+
// The Files API serves /workspace; another path is copied there first.
|
|
271
|
+
const staging = `${STAGING}/${crypto.randomUUID()}`;
|
|
272
|
+
try {
|
|
273
|
+
const copied = await this.#call((sbx) => sbx.exec([
|
|
274
|
+
"sh",
|
|
275
|
+
"-c",
|
|
276
|
+
'mkdir -p -- "$(dirname -- "$2")" && cat -- "$1" > "$2"',
|
|
277
|
+
"sh",
|
|
278
|
+
path,
|
|
279
|
+
staging,
|
|
280
|
+
]));
|
|
281
|
+
if (copied.exitCode !== 0) {
|
|
282
|
+
if (copied.stderr.includes("No such file"))
|
|
283
|
+
throw new NotFoundError({
|
|
284
|
+
message: copied.stderr.trim(),
|
|
285
|
+
code: "file_not_found",
|
|
286
|
+
status: 404,
|
|
287
|
+
});
|
|
288
|
+
throw new SandboxProviderError(`${PROVIDER} could not read ${path}.`, {
|
|
289
|
+
stderr: copied.stderr,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return await this.#call((sbx) => sbx.files.read(staging));
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
await this.#call((sbx) => sbx.files.remove(staging)).catch(() => undefined);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async #writeBytes(path, data) {
|
|
299
|
+
if (inFilesApi(path)) {
|
|
300
|
+
await this.#call((sbx) => sbx.files.write(path, data));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const staging = `${STAGING}/${crypto.randomUUID()}`;
|
|
304
|
+
try {
|
|
305
|
+
await this.#call((sbx) => sbx.files.write(staging, data));
|
|
306
|
+
const copied = await this.#call((sbx) => sbx.exec([
|
|
307
|
+
"sh",
|
|
308
|
+
"-c",
|
|
309
|
+
'mkdir -p -- "$(dirname -- "$2")" && cat -- "$1" > "$2"',
|
|
310
|
+
"sh",
|
|
311
|
+
staging,
|
|
312
|
+
path,
|
|
313
|
+
]));
|
|
314
|
+
if (copied.exitCode !== 0)
|
|
315
|
+
throw new SandboxProviderError(`${PROVIDER} could not write ${path}.`, {
|
|
316
|
+
stderr: copied.stderr,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
await this.#call((sbx) => sbx.files.remove(staging)).catch(() => undefined);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
#filesystemRunAs(runAs) {
|
|
324
|
+
if (runAs)
|
|
325
|
+
throw new SandboxUnsupportedFeatureError(`${PROVIDER} reads and writes files as the sandbox user; run commands with runAs instead.`, { provider: BACKEND_ID, feature: "filesystem.runAs" });
|
|
326
|
+
}
|
|
327
|
+
async readFile(args) {
|
|
328
|
+
this.#filesystemRunAs(args.runAs);
|
|
329
|
+
const bytes = await this.#readBytes(this.resolvePath(args.path));
|
|
330
|
+
return typeof args.maxBytes === "number" && bytes.byteLength > args.maxBytes
|
|
331
|
+
? bytes.subarray(0, args.maxBytes)
|
|
332
|
+
: bytes;
|
|
333
|
+
}
|
|
334
|
+
async viewImage(args) {
|
|
335
|
+
this.#filesystemRunAs(args.runAs);
|
|
336
|
+
return imageOutputFromBytes(args.path, await this.#readBytes(this.resolvePath(args.path)));
|
|
337
|
+
}
|
|
338
|
+
async listDir(args) {
|
|
339
|
+
const path = this.resolvePath(args.path);
|
|
340
|
+
const result = await this.#call((sbx) => sbx.exec(["find", path, "-mindepth", "1", "-maxdepth", "1", "-printf", "%y\\t%f\\n"]));
|
|
341
|
+
if (result.exitCode !== 0)
|
|
342
|
+
throw new SandboxProviderError(`${PROVIDER} could not list ${path}.`, {
|
|
343
|
+
stderr: result.stderr,
|
|
344
|
+
});
|
|
345
|
+
return result.stdout
|
|
346
|
+
.split("\n")
|
|
347
|
+
.filter(Boolean)
|
|
348
|
+
.map((line) => {
|
|
349
|
+
const [kind = "", ...rest] = line.split("\t");
|
|
350
|
+
const name = rest.join("\t");
|
|
351
|
+
return {
|
|
352
|
+
name,
|
|
353
|
+
path: `${path === "/" ? "" : path}/${name}`,
|
|
354
|
+
type: kind === "f" ? "file" : kind === "d" ? "dir" : "other",
|
|
355
|
+
};
|
|
356
|
+
})
|
|
357
|
+
.filter((entry) => entry.path !== STAGING)
|
|
358
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
359
|
+
}
|
|
360
|
+
async #test(flag, path, runAs) {
|
|
361
|
+
const argv = ["test", flag, this.resolvePath(path)];
|
|
362
|
+
const result = await this.#call((sbx) => sbx.exec(runAs ? ["sudo", "-n", "-u", runAs, "--", ...argv] : argv));
|
|
363
|
+
return result.exitCode === 0;
|
|
364
|
+
}
|
|
365
|
+
pathExists(path, runAs) {
|
|
366
|
+
return this.#test("-e", path, runAs);
|
|
367
|
+
}
|
|
368
|
+
directoryExists(path, runAs) {
|
|
369
|
+
return this.#test("-d", path, runAs);
|
|
370
|
+
}
|
|
371
|
+
createEditor(runAs) {
|
|
372
|
+
this.#filesystemRunAs(runAs);
|
|
373
|
+
const read = async (path) => new TextDecoder().decode(await this.#readBytes(this.resolvePath(path)));
|
|
374
|
+
const exists = (path) => this.pathExists(path);
|
|
375
|
+
return {
|
|
376
|
+
createFile: async (operation) => {
|
|
377
|
+
if (await exists(operation.path))
|
|
378
|
+
throw new UserError(`Cannot create file because it already exists: ${operation.path}`);
|
|
379
|
+
await this.#writeBytes(this.resolvePath(operation.path), applyDiff("", operation.diff, "create"));
|
|
380
|
+
return {};
|
|
381
|
+
},
|
|
382
|
+
updateFile: async (operation) => {
|
|
383
|
+
const source = this.resolvePath(operation.path);
|
|
384
|
+
const target = operation.moveTo ? this.resolvePath(operation.moveTo) : source;
|
|
385
|
+
await this.#writeBytes(target, applyDiff(await read(operation.path), operation.diff));
|
|
386
|
+
if (target !== source)
|
|
387
|
+
await this.#call((sbx) => sbx.exec(["rm", "-f", "--", source]));
|
|
388
|
+
return {};
|
|
389
|
+
},
|
|
390
|
+
deleteFile: async (operation) => {
|
|
391
|
+
const path = this.resolvePath(operation.path);
|
|
392
|
+
const result = await this.#call((sbx) => sbx.exec(["rm", "-f", "--", path]));
|
|
393
|
+
if (result.exitCode !== 0)
|
|
394
|
+
throw new UserError(`Cannot delete ${operation.path}: ${result.stderr}`);
|
|
395
|
+
return {};
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
// The manifest.
|
|
400
|
+
async prepareWorkspaceRoot() {
|
|
401
|
+
const root = this.state.manifest.root;
|
|
402
|
+
// The root may sit where the sandbox user cannot write (/app); make it as root and hand it over.
|
|
403
|
+
const result = await this.#call((sbx) => sbx.exec([
|
|
404
|
+
"sh",
|
|
405
|
+
"-c",
|
|
406
|
+
'mkdir -p -- "$1" 2>/dev/null || { sudo -n mkdir -p -- "$1" && sudo -n chown "$(id -u):$(id -g)" -- "$1"; }',
|
|
407
|
+
"sh",
|
|
408
|
+
root,
|
|
409
|
+
], { cwd: "/" }));
|
|
410
|
+
if (result.exitCode !== 0)
|
|
411
|
+
throw new SandboxProviderError(`${PROVIDER} could not prepare the workspace root ${root}.`, {
|
|
412
|
+
stderr: result.stderr,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async applyManifest(manifest, runAs) {
|
|
416
|
+
this.#filesystemRunAs(runAs);
|
|
417
|
+
for (const group of manifest.groups)
|
|
418
|
+
await this.#root(["groupadd", "-f", group.name]);
|
|
419
|
+
const users = new Set([
|
|
420
|
+
...manifest.users.map((user) => user.name),
|
|
421
|
+
...manifest.groups.flatMap((group) => (group.users ?? []).map((user) => user.name)),
|
|
422
|
+
]);
|
|
423
|
+
for (const name of users)
|
|
424
|
+
await this.#root([
|
|
425
|
+
"sh",
|
|
426
|
+
"-c",
|
|
427
|
+
'id -u "$1" >/dev/null 2>&1 || useradd -U -M -s /usr/sbin/nologin "$1"',
|
|
428
|
+
"sh",
|
|
429
|
+
name,
|
|
430
|
+
]);
|
|
431
|
+
for (const group of manifest.groups)
|
|
432
|
+
for (const user of group.users ?? [])
|
|
433
|
+
await this.#root(["usermod", "-aG", group.name, user.name]);
|
|
434
|
+
for (const [path, entry] of Object.entries(manifest.entries))
|
|
435
|
+
await this.#materialize(this.resolvePath(path), entry);
|
|
436
|
+
this.state.environment = await materializeEnvironment(manifest, this.state.environment);
|
|
437
|
+
this.state.manifest = mergeManifestDelta(this.state.manifest, manifest);
|
|
438
|
+
}
|
|
439
|
+
async materializeEntry(args) {
|
|
440
|
+
this.#filesystemRunAs(args.runAs);
|
|
441
|
+
await this.#materialize(this.resolvePath(args.path), args.entry);
|
|
442
|
+
const relative = this.resolvePath(args.path)
|
|
443
|
+
.slice(this.state.manifest.root.length)
|
|
444
|
+
.replace(/^\//, "");
|
|
445
|
+
this.state.manifest = mergeManifestEntryDelta(this.state.manifest, relative || args.path, args.entry);
|
|
446
|
+
}
|
|
447
|
+
async #root(argv) {
|
|
448
|
+
const result = await this.#call((sbx) => sbx.exec(["sudo", "-n", ...argv], { cwd: "/" }));
|
|
449
|
+
if (result.exitCode !== 0)
|
|
450
|
+
throw new SandboxProviderError(`${PROVIDER} could not run ${argv[0]}.`, {
|
|
451
|
+
stderr: result.stderr,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async #materialize(path, entry) {
|
|
455
|
+
if ("permissions" in entry && entry.permissions !== undefined)
|
|
456
|
+
throw new SandboxUnsupportedFeatureError(`${PROVIDER} does not set manifest permissions yet.`, {
|
|
457
|
+
provider: BACKEND_ID,
|
|
458
|
+
feature: "entry.permissions",
|
|
459
|
+
path,
|
|
460
|
+
});
|
|
461
|
+
switch (entry.type) {
|
|
462
|
+
case "file":
|
|
463
|
+
await this.#writeBytes(path, entry.content);
|
|
464
|
+
return;
|
|
465
|
+
case "dir": {
|
|
466
|
+
const made = await this.#call((sbx) => sbx.exec(["mkdir", "-p", "--", path]));
|
|
467
|
+
if (made.exitCode !== 0)
|
|
468
|
+
throw new SandboxProviderError(`${PROVIDER} could not make ${path}.`, {
|
|
469
|
+
stderr: made.stderr,
|
|
470
|
+
});
|
|
471
|
+
for (const [name, child] of Object.entries(entry.children ?? {}))
|
|
472
|
+
await this.#materialize(normalize(`${path}/${name}`), child);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
case "local_file": {
|
|
476
|
+
const { readFile } = await import("node:fs/promises");
|
|
477
|
+
await this.#writeBytes(path, await readFile(entry.src));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
case "local_dir": {
|
|
481
|
+
// One gzipped tar, staged inside /workspace where uploads of any size are accepted.
|
|
482
|
+
const { packDirectory } = await import("../tar.js");
|
|
483
|
+
const staging = `${STAGING}/${crypto.randomUUID()}.tar.gz`;
|
|
484
|
+
const archive = await packDirectory(entry.src);
|
|
485
|
+
try {
|
|
486
|
+
await this.#call((sbx) => sbx.files.write(staging, archive));
|
|
487
|
+
const unpacked = await this.#call((sbx) => sbx.exec([
|
|
488
|
+
"sh",
|
|
489
|
+
"-c",
|
|
490
|
+
'mkdir -p -- "$1" && tar -xzf "$2" -C "$1"',
|
|
491
|
+
"sh",
|
|
492
|
+
path,
|
|
493
|
+
staging,
|
|
494
|
+
]));
|
|
495
|
+
if (unpacked.exitCode !== 0)
|
|
496
|
+
throw new SandboxProviderError(`${PROVIDER} could not place ${entry.src}.`, {
|
|
497
|
+
stderr: unpacked.stderr,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
finally {
|
|
501
|
+
await this.#call((sbx) => sbx.files.remove(staging)).catch(() => undefined);
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
case "git_repo": {
|
|
506
|
+
const url = /^[a-z]+:\/\//.test(entry.repo)
|
|
507
|
+
? entry.repo
|
|
508
|
+
: `https://${entry.host ?? "github.com"}/${entry.repo.replace(/\.git$/, "")}.git`;
|
|
509
|
+
const script = [
|
|
510
|
+
"set -e",
|
|
511
|
+
"tmp=$(mktemp -d)",
|
|
512
|
+
`git clone --depth 1 ${entry.ref ? `--branch ${shellQuote(entry.ref)} ` : ""}${shellQuote(url)} "$tmp/repo"`,
|
|
513
|
+
'mkdir -p -- "$(dirname -- "$1")" && rm -rf -- "$1"',
|
|
514
|
+
'mv -- "$tmp/repo/$2" "$1"',
|
|
515
|
+
'rm -rf -- "$tmp"',
|
|
516
|
+
].join("\n");
|
|
517
|
+
const subpath = (entry.subpath ?? "").replace(/^\/+/, "");
|
|
518
|
+
const cloned = await this.#call((sbx) => sbx.exec(["sh", "-c", script, "sh", path, subpath], { timeoutMs: 600_000 }));
|
|
519
|
+
if (cloned.exitCode !== 0)
|
|
520
|
+
throw new SandboxProviderError(`${PROVIDER} could not clone ${entry.repo}.`, {
|
|
521
|
+
stderr: cloned.stderr,
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
default:
|
|
526
|
+
throw new SandboxUnsupportedFeatureError(`${PROVIDER} does not attach ${entry.type} entries; mount storage from a command instead.`, { provider: BACKEND_ID, feature: entry.type, path });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Ports.
|
|
530
|
+
async resolveExposedPort(port) {
|
|
531
|
+
const configured = this.state.configuredExposedPorts;
|
|
532
|
+
if (configured && !configured.includes(port))
|
|
533
|
+
throw new UserError(`Port ${port} is not in exposedPorts (${configured.join(", ")}).`);
|
|
534
|
+
let preview;
|
|
535
|
+
try {
|
|
536
|
+
preview = await this.#call((sbx) => sbx.previews.create(port, { visibility: this.state.previewVisibility }));
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
throw providerError(`${PROVIDER} could not share port ${port}.`, error, { port });
|
|
540
|
+
}
|
|
541
|
+
const url = new URL(preview.url);
|
|
542
|
+
const tls = url.protocol === "https:";
|
|
543
|
+
return {
|
|
544
|
+
host: url.hostname,
|
|
545
|
+
port: url.port ? Number(url.port) : tls ? 443 : 80,
|
|
546
|
+
tls,
|
|
547
|
+
query: preview.token ? `runtime_preview_token=${preview.token}` : "",
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
// Workspace archives.
|
|
551
|
+
async persistWorkspace() {
|
|
552
|
+
const root = this.state.manifest.root;
|
|
553
|
+
const staging = `${STAGING}/${crypto.randomUUID()}.tar`;
|
|
554
|
+
const skip = root === FILES_HOME ? ["--exclude=./.openai-agents-staging"] : [];
|
|
555
|
+
try {
|
|
556
|
+
const packed = await this.#call((sbx) => sbx.exec([
|
|
557
|
+
"sh",
|
|
558
|
+
"-c",
|
|
559
|
+
`mkdir -p -- ${STAGING} && tar ${skip.join(" ")} -C "$1" -cf - . > "$2"`,
|
|
560
|
+
"sh",
|
|
561
|
+
root,
|
|
562
|
+
staging,
|
|
563
|
+
], {
|
|
564
|
+
cwd: "/",
|
|
565
|
+
timeoutMs: this.state.execTimeoutMs,
|
|
566
|
+
}));
|
|
567
|
+
if (packed.exitCode !== 0)
|
|
568
|
+
throw new SandboxProviderError(`${PROVIDER} could not archive the workspace.`, {
|
|
569
|
+
stderr: packed.stderr,
|
|
570
|
+
});
|
|
571
|
+
return await this.#call((sbx) => sbx.files.read(staging));
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
await this.#call((sbx) => sbx.files.remove(staging)).catch(() => undefined);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async hydrateWorkspace(data) {
|
|
578
|
+
const bytes = typeof data === "string"
|
|
579
|
+
? new TextEncoder().encode(data)
|
|
580
|
+
: data instanceof Uint8Array
|
|
581
|
+
? data
|
|
582
|
+
: new Uint8Array(data);
|
|
583
|
+
const staging = `${STAGING}/${crypto.randomUUID()}.tar`;
|
|
584
|
+
try {
|
|
585
|
+
await this.#call((sbx) => sbx.files.write(staging, bytes));
|
|
586
|
+
const unpacked = await this.#call((sbx) => sbx.exec([
|
|
587
|
+
"sh",
|
|
588
|
+
"-c",
|
|
589
|
+
'mkdir -p -- "$1" && tar -C "$1" -xf "$2"',
|
|
590
|
+
"sh",
|
|
591
|
+
this.state.manifest.root,
|
|
592
|
+
staging,
|
|
593
|
+
], {
|
|
594
|
+
cwd: "/",
|
|
595
|
+
timeoutMs: this.state.execTimeoutMs,
|
|
596
|
+
}));
|
|
597
|
+
if (unpacked.exitCode !== 0)
|
|
598
|
+
throw new SandboxProviderError(`${PROVIDER} could not restore the workspace.`, {
|
|
599
|
+
stderr: unpacked.stderr,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
finally {
|
|
603
|
+
await this.#call((sbx) => sbx.files.remove(staging)).catch(() => undefined);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Lifecycle.
|
|
607
|
+
async running() {
|
|
608
|
+
if (!this.#sandbox || this.#paused)
|
|
609
|
+
return false;
|
|
610
|
+
try {
|
|
611
|
+
await this.#sandbox.refresh();
|
|
612
|
+
return this.#sandbox.state === "running";
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/** Ends every process the agent started. */
|
|
619
|
+
async shutdown() {
|
|
620
|
+
const entries = [...this.#processes.values()];
|
|
621
|
+
this.#processes.clear();
|
|
622
|
+
await Promise.all(entries.map((entry) => this.#terminate(entry)));
|
|
623
|
+
}
|
|
624
|
+
/** Stops the sandbox, or pauses it with `pauseOnExit`. */
|
|
625
|
+
async delete() {
|
|
626
|
+
await this.shutdown();
|
|
627
|
+
const sandbox = this.#sandbox;
|
|
628
|
+
if (!sandbox || this.#paused)
|
|
629
|
+
return;
|
|
630
|
+
if (this.state.pauseOnExit) {
|
|
631
|
+
try {
|
|
632
|
+
await sandbox.pause();
|
|
633
|
+
this.#paused = true;
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
// Stop it rather than leave it running.
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
await sandbox.stop();
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
if (!(error instanceof NotFoundError))
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
this.#sandbox = undefined;
|
|
648
|
+
}
|
|
649
|
+
async close() {
|
|
650
|
+
await this.delete();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/** Creates, resumes and deletes Runtime Cloud sandbox sessions. */
|
|
654
|
+
export class RuntimeCloudSandboxClient {
|
|
655
|
+
backendId = BACKEND_ID;
|
|
656
|
+
runtime;
|
|
657
|
+
#options;
|
|
658
|
+
constructor(options = {}) {
|
|
659
|
+
const { runtime, ...rest } = options;
|
|
660
|
+
this.runtime = runtime ?? new Runtime();
|
|
661
|
+
this.#options = rest;
|
|
662
|
+
}
|
|
663
|
+
async create(args, manifestOptions) {
|
|
664
|
+
const normalized = normalizeSandboxClientCreateArgs(args, manifestOptions);
|
|
665
|
+
if (normalized.snapshot && normalized.snapshot.type !== "noop")
|
|
666
|
+
throw new SandboxUnsupportedFeatureError(`${PROVIDER} keeps no SDK snapshots; use pauseOnExit to resume the same machine, or create from a Runtime snapshot.`, { provider: BACKEND_ID, feature: "snapshot" });
|
|
667
|
+
const options = { ...this.#options, ...normalized.options };
|
|
668
|
+
const manifest = normalized.manifest;
|
|
669
|
+
const state = {
|
|
670
|
+
manifest,
|
|
671
|
+
sandboxId: "",
|
|
672
|
+
create: { ...options.create },
|
|
673
|
+
environment: await materializeEnvironment(manifest, options.env),
|
|
674
|
+
...(options.exposedPorts ? { configuredExposedPorts: options.exposedPorts } : {}),
|
|
675
|
+
previewVisibility: options.previewVisibility ?? "private",
|
|
676
|
+
pauseOnExit: options.pauseOnExit ?? false,
|
|
677
|
+
execTimeoutMs: Math.min(MAX_EXEC_MS, options.execTimeoutMs ?? 3_600_000),
|
|
678
|
+
};
|
|
679
|
+
const session = new RuntimeCloudSandboxSession({ state, runtime: this.runtime });
|
|
680
|
+
await session.sandbox();
|
|
681
|
+
try {
|
|
682
|
+
await session.prepareWorkspaceRoot();
|
|
683
|
+
await session.applyManifest(manifest);
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
state.pauseOnExit = false;
|
|
687
|
+
await session.delete().catch(() => undefined);
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
return session;
|
|
691
|
+
}
|
|
692
|
+
/** Reattaches to the same sandbox while it runs (waking it when paused);
|
|
693
|
+
* otherwise starts a new one and applies the manifest again. */
|
|
694
|
+
async resume(state) {
|
|
695
|
+
if (state.sandboxId) {
|
|
696
|
+
try {
|
|
697
|
+
const sandbox = await this.runtime.sandboxes.get(state.sandboxId);
|
|
698
|
+
if (sandbox.state === "paused" || sandbox.state === "pausing")
|
|
699
|
+
await sandbox.wake();
|
|
700
|
+
else if (sandbox.state === "starting" || sandbox.state === "resuming")
|
|
701
|
+
await sandbox.waitFor("running");
|
|
702
|
+
if (sandbox.state === "running")
|
|
703
|
+
return new RuntimeCloudSandboxSession({ state, runtime: this.runtime, sandbox });
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
if (!(error instanceof NotFoundError) && !(isRuntimeError(error) && error.status === 409))
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const session = await this.create(state.manifest, {
|
|
711
|
+
create: state.create,
|
|
712
|
+
env: state.environment,
|
|
713
|
+
...(state.configuredExposedPorts ? { exposedPorts: state.configuredExposedPorts } : {}),
|
|
714
|
+
previewVisibility: state.previewVisibility,
|
|
715
|
+
pauseOnExit: state.pauseOnExit,
|
|
716
|
+
execTimeoutMs: state.execTimeoutMs,
|
|
717
|
+
});
|
|
718
|
+
Object.assign(state, session.state);
|
|
719
|
+
return new RuntimeCloudSandboxSession({
|
|
720
|
+
state,
|
|
721
|
+
runtime: this.runtime,
|
|
722
|
+
sandbox: await session.sandbox(),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
async delete(state) {
|
|
726
|
+
if (!state.sandboxId)
|
|
727
|
+
return;
|
|
728
|
+
try {
|
|
729
|
+
const sandbox = await this.runtime.sandboxes.get(state.sandboxId);
|
|
730
|
+
if (state.pauseOnExit)
|
|
731
|
+
await sandbox.pause();
|
|
732
|
+
else
|
|
733
|
+
await sandbox.stop();
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
if (!(error instanceof NotFoundError))
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
canPersistOwnedSessionState(state) {
|
|
741
|
+
return state.pauseOnExit;
|
|
742
|
+
}
|
|
743
|
+
/** The state without secrets: the environment is materialized again on the way back. */
|
|
744
|
+
async serializeSessionState(state) {
|
|
745
|
+
return {
|
|
746
|
+
sandboxId: state.sandboxId,
|
|
747
|
+
create: state.create,
|
|
748
|
+
configuredExposedPorts: state.configuredExposedPorts,
|
|
749
|
+
previewVisibility: state.previewVisibility,
|
|
750
|
+
pauseOnExit: state.pauseOnExit,
|
|
751
|
+
execTimeoutMs: state.execTimeoutMs,
|
|
752
|
+
manifest: serializeManifestRecord(state.manifest),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async deserializeSessionState(record) {
|
|
756
|
+
const manifest = deserializeManifest(record.manifest);
|
|
757
|
+
return {
|
|
758
|
+
manifest,
|
|
759
|
+
sandboxId: typeof record.sandboxId === "string" ? record.sandboxId : "",
|
|
760
|
+
create: record.create ?? {},
|
|
761
|
+
environment: await materializeEnvironment(manifest, this.#options.env),
|
|
762
|
+
...(Array.isArray(record.configuredExposedPorts)
|
|
763
|
+
? { configuredExposedPorts: record.configuredExposedPorts }
|
|
764
|
+
: {}),
|
|
765
|
+
previewVisibility: record.previewVisibility === "public" ? "public" : "private",
|
|
766
|
+
pauseOnExit: record.pauseOnExit === true,
|
|
767
|
+
execTimeoutMs: typeof record.execTimeoutMs === "number" ? record.execTimeoutMs : 3_600_000,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/** The four sandbox tools as function tools for an ordinary `Agent`
|
|
772
|
+
* (no SandboxAgent): runtime_exec, runtime_read_file, runtime_write_file and
|
|
773
|
+
* runtime_list_files, bound to `sandbox`. */
|
|
774
|
+
export function runtimeFunctionTools(sandbox, options = {}) {
|
|
775
|
+
return sandboxTools(sandbox, options).map((spec) => tool({
|
|
776
|
+
name: spec.name,
|
|
777
|
+
description: spec.description,
|
|
778
|
+
// Non-strict: the optional fields stay optional.
|
|
779
|
+
parameters: {
|
|
780
|
+
type: "object",
|
|
781
|
+
properties: spec.inputSchema.properties,
|
|
782
|
+
required: spec.inputSchema.required,
|
|
783
|
+
additionalProperties: true,
|
|
784
|
+
},
|
|
785
|
+
strict: false,
|
|
786
|
+
execute: async (input) => {
|
|
787
|
+
const output = await spec.execute(input);
|
|
788
|
+
return typeof output === "string" ? output : JSON.stringify(output);
|
|
789
|
+
},
|
|
790
|
+
}));
|
|
791
|
+
}
|