withruntime 0.6.0 → 0.6.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 CHANGED
@@ -110,7 +110,9 @@ package list every mapping and gap.
110
110
  ## Agent frameworks
111
111
 
112
112
  `withruntime/openai-agents` is a sandbox client for the OpenAI Agents SDK's
113
- `SandboxAgent`. `withruntime/ai` gives Vercel AI SDK and Mastra agents four
113
+ `SandboxAgent`. `withruntime/ai-harness` is a sandbox provider for the Vercel
114
+ AI SDK's `HarnessAgent` (Claude Code, Codex, OpenCode, Pi and the rest).
115
+ `withruntime/ai` gives Vercel AI SDK and Mastra agents four
114
116
  sandbox tools, `withruntime/claude-agent-sdk` gives them to the Claude Agent
115
117
  SDK as an MCP server, and `withruntime/tools` gives them in no framework's
116
118
  format. Each needs its framework installed beside this package. See
@@ -143,4 +145,5 @@ the browser, or send a bearer key.
143
145
  Docs: https://withruntime.com/docs/javascript and https://withruntime.com/docs/cli.
144
146
 
145
147
  The package was called `@withruntime/cloud` until 0.3.0. That name, and
146
- `runtime-cloud` and `withruntime-cloud`, still install this one.
148
+ `runtime-cloud` and `withruntime-cloud`, stopped at 0.5.1 and get no new
149
+ releases.
@@ -0,0 +1,194 @@
1
+ /** Runtime Cloud sandboxes for the Vercel AI SDK's harnesses (`@ai-sdk/harness`).
2
+ *
3
+ * `createRuntimeSandbox()` is a `HarnessV1SandboxProvider`, like
4
+ * `@ai-sdk/sandbox-vercel`'s `createVercelSandbox()`: a `HarnessAgent` runs its
5
+ * harness (Claude Code, Codex, OpenCode, Pi, ...) in a Runtime microVM, and
6
+ * the agent definition does not change:
7
+ *
8
+ * import { HarnessAgent } from "@ai-sdk/harness/agent";
9
+ * import { claudeCode } from "@ai-sdk/harness-claude-code";
10
+ * import { createRuntimeSandbox } from "withruntime/ai-harness";
11
+ *
12
+ * const agent = new HarnessAgent({
13
+ * harness: claudeCode,
14
+ * sandbox: createRuntimeSandbox({ ports: [4000], create: { funding: "trial" } }),
15
+ * });
16
+ *
17
+ * Commands run under `bash -c` as the sandbox user, streamed. Exposed ports
18
+ * are Runtime previews, private by default: the endpoint carries the preview
19
+ * token. The key comes from RUNTIME_API_KEY or this machine's `runtime login`.
20
+ * Needs `@ai-sdk/harness` 1.x beside this package. */
21
+ import { type HarnessV1NetworkPolicy, type HarnessV1NetworkSandboxSession, type HarnessV1PortEndpoint, type HarnessV1SandboxProvider } from "@ai-sdk/harness";
22
+ import { Runtime } from "../client.js";
23
+ import type { NetworkRules } from "../products/network.js";
24
+ import type { Sandbox } from "../sandbox.js";
25
+ import type { CreateSandbox } from "../types.js";
26
+ /** The AI SDK's plain sandbox session (files, `run`, `spawn`), as the harness
27
+ * package declares it. */
28
+ export type SandboxSession = ReturnType<HarnessV1NetworkSandboxSession["restricted"]>;
29
+ export type SandboxProcess = Awaited<ReturnType<SandboxSession["spawn"]>>;
30
+ type ProcessOptions = Parameters<SandboxSession["run"]>[0];
31
+ /** What `run` and `spawn` take beyond the AI SDK's options. The harness never
32
+ * passes these; code that holds the session may. */
33
+ export type RuntimeProcessOptions = ProcessOptions & {
34
+ /** Given to standard input, then closed. `spawn` takes text only. */
35
+ stdin?: string | Uint8Array;
36
+ /** The longest the command may run; this session's `commandTimeoutMs` when left out. */
37
+ timeoutMs?: number;
38
+ };
39
+ export declare const PROVIDER_ID = "runtime-sandbox";
40
+ /** The preview token's header. The edge also takes it as `runtime_preview_token` in the query. */
41
+ export declare const PREVIEW_TOKEN_HEADER = "x-runtime-preview-token";
42
+ export type RuntimeSandboxSettings = {
43
+ /** The client to use. Default: one from RUNTIME_API_KEY or this machine's `runtime login`. */
44
+ runtime?: Runtime;
45
+ /** Wrap a sandbox you created. You own it: the session's `stop()` and
46
+ * `destroy()` leave it running, and `create` is not used. */
47
+ sandbox?: Sandbox;
48
+ /** How to create each session's sandbox: `funding`, `image`, `snapshot`,
49
+ * `region`, `vcpu`, `memoryMiB`, `timeoutSeconds`, `network`, `labels`, ...
50
+ * With a harness session id the sandbox is named after it (for
51
+ * `resumeSession`), and that name wins over `create.name`. */
52
+ create?: CreateSandbox;
53
+ /** Set for every command, under the command's own `env`. Runtime's create
54
+ * takes no environment, so the provider holds it. */
55
+ env?: Record<string, string>;
56
+ /** Ports to share as previews. Bridge harnesses (Claude Code, Codex,
57
+ * OpenCode, Deep Agents) use the first one. */
58
+ ports?: number[];
59
+ /** `private` (default): the endpoint carries a preview token. `public`:
60
+ * anyone with the address; a browser sees a one-time warning page. */
61
+ previewVisibility?: "private" | "public";
62
+ /** How long each endpoint's token lasts, 60 s to 7 days. Default: the API's (1 day). */
63
+ previewTtlSeconds?: number;
64
+ /** The longest a `run` command may take. Default one hour; 24 hours at most. */
65
+ commandTimeoutMs?: number;
66
+ /** `stop()` pauses the sandbox instead of stopping it, so `resumeSession`
67
+ * wakes the same machine with its files, memory and processes. A paused
68
+ * sandbox is billed as paused storage until it is woken or stopped.
69
+ * Default false. `destroy()` always stops. */
70
+ pauseOnStop?: boolean;
71
+ };
72
+ /** A harness sandbox provider backed by Runtime Cloud. Construct it once, at
73
+ * module scope; nothing is created until a session is. */
74
+ export declare function createRuntimeSandbox(settings?: RuntimeSandboxSettings): RuntimeSandboxProvider;
75
+ /** The sandbox name a harness session id maps to: `ai-harness-<id>`, or a
76
+ * digest of the id when it would not make a valid name. */
77
+ export declare function sessionSandboxName(sessionId: string): Promise<string>;
78
+ export declare class RuntimeSandboxProvider implements HarnessV1SandboxProvider {
79
+ #private;
80
+ private readonly settings;
81
+ readonly specificationVersion: "harness-sandbox-v1";
82
+ readonly providerId = "runtime-sandbox";
83
+ constructor(settings?: RuntimeSandboxSettings);
84
+ get runtime(): Runtime;
85
+ /** A new sandbox (named after `sessionId` when given), or the wrapped one.
86
+ * `onFirstCreate` runs on a new sandbox before this returns; Runtime keeps
87
+ * no template for `identity`, so it runs for every new sandbox. */
88
+ createSession: (options?: {
89
+ sessionId?: string;
90
+ abortSignal?: AbortSignal;
91
+ identity?: string;
92
+ onFirstCreate?: (session: SandboxSession, opts: {
93
+ abortSignal?: AbortSignal;
94
+ }) => Promise<void>;
95
+ }) => Promise<RuntimeNetworkSandboxSession>;
96
+ /** The sandbox `createSession` made for `sessionId`, woken if it is paused.
97
+ * A stopped one cannot be resumed: its memory and disk are gone. */
98
+ resumeSession: (options: {
99
+ sessionId: string;
100
+ abortSignal?: AbortSignal;
101
+ }) => Promise<RuntimeNetworkSandboxSession>;
102
+ }
103
+ /** Files and processes in one Runtime sandbox: what `restricted()` hands to
104
+ * tools, with nothing that stops the sandbox or changes its network. */
105
+ export declare class RuntimeSandboxSession implements SandboxSession {
106
+ #private;
107
+ readonly sandbox: Sandbox;
108
+ protected readonly settings: Pick<RuntimeSandboxSettings, "env" | "commandTimeoutMs">;
109
+ /** Where relative paths and commands without a `workingDirectory` start. */
110
+ defaultWorkingDirectory: string;
111
+ constructor(sandbox: Sandbox, settings?: Pick<RuntimeSandboxSettings, "env" | "commandTimeoutMs">);
112
+ get description(): string;
113
+ /** Runs `command` under `bash -c` and returns once it exits, with all of
114
+ * its output. A timeout exits 124, with a note on standard error. */
115
+ run: (options: RuntimeProcessOptions) => Promise<{
116
+ exitCode: number;
117
+ stdout: string;
118
+ stderr: string;
119
+ }>;
120
+ /** Starts `command` and returns at once, with its output as streams. */
121
+ spawn: (options: RuntimeProcessOptions) => Promise<SandboxProcess>;
122
+ readBinaryFile: (options: {
123
+ path: string;
124
+ abortSignal?: AbortSignal;
125
+ }) => Promise<Uint8Array | null>;
126
+ readFile: (options: {
127
+ path: string;
128
+ abortSignal?: AbortSignal;
129
+ }) => Promise<ReadableStream<Uint8Array> | null>;
130
+ readTextFile: (options: {
131
+ path: string;
132
+ encoding?: string;
133
+ startLine?: number;
134
+ endLine?: number;
135
+ abortSignal?: AbortSignal;
136
+ }) => Promise<string | null>;
137
+ /** Writes atomically, making parent directories. Past 1 MiB a file must be under /workspace. */
138
+ writeBinaryFile: (options: {
139
+ path: string;
140
+ content: Uint8Array;
141
+ abortSignal?: AbortSignal;
142
+ }) => Promise<void>;
143
+ writeFile: (options: {
144
+ path: string;
145
+ content: ReadableStream<Uint8Array>;
146
+ abortSignal?: AbortSignal;
147
+ }) => Promise<void>;
148
+ writeTextFile: (options: {
149
+ path: string;
150
+ content: string;
151
+ encoding?: string;
152
+ abortSignal?: AbortSignal;
153
+ }) => Promise<void>;
154
+ }
155
+ /** The session a harness keeps: files and processes, plus ports as previews,
156
+ * network rules, and stopping. */
157
+ export declare class RuntimeNetworkSandboxSession extends RuntimeSandboxSession implements HarnessV1NetworkSandboxSession {
158
+ #private;
159
+ protected readonly settings: RuntimeSandboxSettings;
160
+ readonly ownsLifecycle: boolean;
161
+ constructor(sandbox: Sandbox, settings: RuntimeSandboxSettings, ownsLifecycle: boolean);
162
+ /** The sandbox's id: `resumeSession` finds it again by the name it was given. */
163
+ get id(): string;
164
+ get ports(): ReadonlyArray<number>;
165
+ get description(): string;
166
+ restricted: () => SandboxSession;
167
+ /** Where to reach `port`. A private preview's token is in the URL's query and
168
+ * in `headers`. For `ws` the URL has no token: the preview edge answers a GET
169
+ * whose query carries one with a redirect that sets a cookie, which a
170
+ * WebSocket client does not follow, so the header carries it. */
171
+ getPortEndpoint: (options: {
172
+ port: number;
173
+ protocol?: "http" | "https" | "ws";
174
+ }) => Promise<HarnessV1PortEndpoint>;
175
+ /** @deprecated As the AI SDK's: use `getPortEndpoint`. The URL carries the token in its query. */
176
+ getPortUrl: (options: {
177
+ port: number;
178
+ protocol?: "http" | "https" | "ws";
179
+ }) => Promise<string>;
180
+ /** Shares exactly `ports`: new ones become previews, dropped ones stop being shared. */
181
+ setPorts: (ports: ReadonlyArray<number>, options?: {
182
+ abortSignal?: AbortSignal;
183
+ }) => Promise<void>;
184
+ /** Outbound rules: `allow-all` is the public web, `deny-all` nothing, and
185
+ * `custom` only the listed hosts and ranges, with `deniedCIDRs` refused. */
186
+ setNetworkPolicy: (policy: HarnessV1NetworkPolicy) => Promise<void>;
187
+ /** Stops the sandbox (or pauses it, with `pauseOnStop`). A wrapped sandbox is left alone. */
188
+ stop: () => Promise<void>;
189
+ /** Stops the sandbox, whatever `pauseOnStop` says: its files and memory are gone. */
190
+ destroy: () => Promise<void>;
191
+ }
192
+ /** The harness's network policy as a Runtime sandbox's network rules. */
193
+ export declare function networkRules(policy: HarnessV1NetworkPolicy): NetworkRules;
194
+ export {};
@@ -0,0 +1,504 @@
1
+ /** Runtime Cloud sandboxes for the Vercel AI SDK's harnesses (`@ai-sdk/harness`).
2
+ *
3
+ * `createRuntimeSandbox()` is a `HarnessV1SandboxProvider`, like
4
+ * `@ai-sdk/sandbox-vercel`'s `createVercelSandbox()`: a `HarnessAgent` runs its
5
+ * harness (Claude Code, Codex, OpenCode, Pi, ...) in a Runtime microVM, and
6
+ * the agent definition does not change:
7
+ *
8
+ * import { HarnessAgent } from "@ai-sdk/harness/agent";
9
+ * import { claudeCode } from "@ai-sdk/harness-claude-code";
10
+ * import { createRuntimeSandbox } from "withruntime/ai-harness";
11
+ *
12
+ * const agent = new HarnessAgent({
13
+ * harness: claudeCode,
14
+ * sandbox: createRuntimeSandbox({ ports: [4000], create: { funding: "trial" } }),
15
+ * });
16
+ *
17
+ * Commands run under `bash -c` as the sandbox user, streamed. Exposed ports
18
+ * are Runtime previews, private by default: the endpoint carries the preview
19
+ * token. The key comes from RUNTIME_API_KEY or this machine's `runtime login`.
20
+ * Needs `@ai-sdk/harness` 1.x beside this package. */
21
+ import { HarnessCapabilityUnsupportedError, HarnessSandboxAuthenticationError, } from "@ai-sdk/harness";
22
+ import { Runtime } from "../client.js";
23
+ import { AuthenticationError, NotFoundError, PermissionDeniedError, RuntimeError, } from "../errors.js";
24
+ export const PROVIDER_ID = "runtime-sandbox";
25
+ /** The preview token's header. The edge also takes it as `runtime_preview_token` in the query. */
26
+ export const PREVIEW_TOKEN_HEADER = "x-runtime-preview-token";
27
+ const TOKEN_QUERY = "runtime_preview_token";
28
+ const SESSION_NAME_PREFIX = "ai-harness-";
29
+ const MAX_COMMAND_MS = 86_400_000;
30
+ const DEFAULT_COMMAND_MS = 3_600_000;
31
+ /** Exit codes for a command that ended without one, as coreutils `timeout` and SIGKILL give. */
32
+ const TIMED_OUT_EXIT = 124;
33
+ const KILLED_EXIT = 137;
34
+ /** A harness sandbox provider backed by Runtime Cloud. Construct it once, at
35
+ * module scope; nothing is created until a session is. */
36
+ export function createRuntimeSandbox(settings = {}) {
37
+ return new RuntimeSandboxProvider(settings);
38
+ }
39
+ /** The sandbox name a harness session id maps to: `ai-harness-<id>`, or a
40
+ * digest of the id when it would not make a valid name. */
41
+ export async function sessionSandboxName(sessionId) {
42
+ const name = `${SESSION_NAME_PREFIX}${sessionId}`;
43
+ if (/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(name))
44
+ return name;
45
+ const { createHash } = await import("node:crypto");
46
+ return `${SESSION_NAME_PREFIX}${createHash("sha256").update(sessionId).digest("hex").slice(0, 40)}`;
47
+ }
48
+ export class RuntimeSandboxProvider {
49
+ settings;
50
+ specificationVersion = "harness-sandbox-v1";
51
+ providerId = PROVIDER_ID;
52
+ #runtime;
53
+ constructor(settings = {}) {
54
+ this.settings = settings;
55
+ this.#runtime = settings.runtime;
56
+ }
57
+ get runtime() {
58
+ this.#runtime ??= new Runtime();
59
+ return this.#runtime;
60
+ }
61
+ /** A new sandbox (named after `sessionId` when given), or the wrapped one.
62
+ * `onFirstCreate` runs on a new sandbox before this returns; Runtime keeps
63
+ * no template for `identity`, so it runs for every new sandbox. */
64
+ createSession = async (options = {}) => {
65
+ options.abortSignal?.throwIfAborted();
66
+ if (this.settings.sandbox)
67
+ return this.#session(this.settings.sandbox, false, options.abortSignal);
68
+ const name = options.sessionId ? await sessionSandboxName(options.sessionId) : undefined;
69
+ const sandbox = await authenticated(() => this.runtime.sandboxes.create({ ...this.settings.create, ...(name ? { name } : {}) }, options.abortSignal ? { signal: options.abortSignal } : {}));
70
+ try {
71
+ const session = await this.#session(sandbox, true, options.abortSignal);
72
+ await options.onFirstCreate?.(session.restricted(), {
73
+ ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
74
+ });
75
+ return session;
76
+ }
77
+ catch (error) {
78
+ // A sandbox nobody holds would run until its lease ends.
79
+ await sandbox.stop({ wait: false }).catch(() => undefined);
80
+ throw error;
81
+ }
82
+ };
83
+ /** The sandbox `createSession` made for `sessionId`, woken if it is paused.
84
+ * A stopped one cannot be resumed: its memory and disk are gone. */
85
+ resumeSession = async (options) => {
86
+ options.abortSignal?.throwIfAborted();
87
+ if (this.settings.sandbox)
88
+ return this.#session(this.settings.sandbox, false, options.abortSignal);
89
+ const name = await sessionSandboxName(options.sessionId);
90
+ const found = await authenticated(async () => {
91
+ const page = await this.runtime.sandboxes.list({ name, limit: 1 });
92
+ return page.data[0];
93
+ });
94
+ if (!found)
95
+ throw new NotFoundError({
96
+ message: `No live Runtime sandbox is named ${name}, so harness session ${options.sessionId} cannot be resumed.`,
97
+ code: "not_found",
98
+ status: 404,
99
+ hint: "A stopped sandbox cannot be resumed. Set pauseOnStop: true so session.stop() pauses it instead.",
100
+ });
101
+ const request = options.abortSignal ? { signal: options.abortSignal } : {};
102
+ if (found.state === "paused" || found.state === "pausing")
103
+ await found.wake(request);
104
+ else if (found.state !== "running")
105
+ await found.waitFor("running", request);
106
+ return this.#session(found, true, options.abortSignal);
107
+ };
108
+ async #session(sandbox, ownsLifecycle, abortSignal) {
109
+ const session = new RuntimeNetworkSandboxSession(sandbox, this.settings, ownsLifecycle);
110
+ // The harness composes each session's directory under the sandbox's own
111
+ // working directory, read from the sandbox rather than assumed.
112
+ const pwd = await session.run({ command: "pwd", ...(abortSignal ? { abortSignal } : {}) });
113
+ const cwd = pwd.stdout.trim();
114
+ if (pwd.exitCode === 0 && cwd.startsWith("/"))
115
+ session.defaultWorkingDirectory = cwd;
116
+ if (this.settings.ports?.length)
117
+ await session.setPorts(this.settings.ports, abortSignal ? { abortSignal } : {});
118
+ return session;
119
+ }
120
+ }
121
+ /** Files and processes in one Runtime sandbox: what `restricted()` hands to
122
+ * tools, with nothing that stops the sandbox or changes its network. */
123
+ export class RuntimeSandboxSession {
124
+ sandbox;
125
+ settings;
126
+ /** Where relative paths and commands without a `workingDirectory` start. */
127
+ defaultWorkingDirectory = "/workspace";
128
+ constructor(sandbox, settings = {}) {
129
+ this.sandbox = sandbox;
130
+ this.settings = settings;
131
+ }
132
+ get description() {
133
+ return [
134
+ `Runtime Cloud sandbox ${this.sandbox.id}: a Firecracker microVM with its own Linux kernel.`,
135
+ `Commands run under bash as the user "runtime", with passwordless sudo. HOME and the default working directory are ${this.defaultWorkingDirectory}.`,
136
+ "Files persist for the life of the sandbox.",
137
+ ].join("\n");
138
+ }
139
+ #path(path) {
140
+ return resolvePosix(this.defaultWorkingDirectory, path);
141
+ }
142
+ #commandOptions(options) {
143
+ const env = { ...this.settings.env, ...options.env };
144
+ return {
145
+ // Without one, the sandbox's own working directory, which is what
146
+ // defaultWorkingDirectory was read from.
147
+ ...(options.workingDirectory === undefined
148
+ ? {}
149
+ : { cwd: this.#path(options.workingDirectory) }),
150
+ ...(Object.keys(env).length ? { env } : {}),
151
+ timeoutMs: Math.min(MAX_COMMAND_MS, options.timeoutMs ?? this.settings.commandTimeoutMs ?? DEFAULT_COMMAND_MS),
152
+ };
153
+ }
154
+ /** Runs `command` under `bash -c` and returns once it exits, with all of
155
+ * its output. A timeout exits 124, with a note on standard error. */
156
+ run = async (options) => {
157
+ const { abortSignal } = options;
158
+ abortSignal?.throwIfAborted();
159
+ const out = { stdout: "", stderr: "" };
160
+ let exit;
161
+ let processId;
162
+ try {
163
+ // Streamed, so no output is cut at the size a single answer holds.
164
+ for await (const event of this.sandbox.execStream(options.command, {
165
+ ...this.#commandOptions(options),
166
+ ...(options.stdin === undefined ? {} : { stdin: options.stdin }),
167
+ ...(abortSignal ? { signal: abortSignal } : {}),
168
+ })) {
169
+ if (event.type === "start")
170
+ processId = event.processId;
171
+ else if (event.type === "stdout" || event.type === "stderr")
172
+ out[event.type] += event.data;
173
+ else if (event.type === "exit")
174
+ exit = event;
175
+ }
176
+ }
177
+ catch (error) {
178
+ if (abortSignal?.aborted) {
179
+ // Aborting the request does not end the command; end it too.
180
+ if (processId)
181
+ await this.#kill(processId);
182
+ throw abortSignal.reason ?? error;
183
+ }
184
+ throw error;
185
+ }
186
+ if (abortSignal?.aborted)
187
+ throw abortSignal.reason;
188
+ const timedOut = exit?.timedOut ?? false;
189
+ return {
190
+ exitCode: exit?.exitCode ?? (timedOut ? TIMED_OUT_EXIT : KILLED_EXIT),
191
+ stdout: out.stdout,
192
+ stderr: timedOut
193
+ ? `${out.stderr}${out.stderr && !out.stderr.endsWith("\n") ? "\n" : ""}Timed out: the command ran past its limit and was ended.\n`
194
+ : out.stderr,
195
+ };
196
+ };
197
+ /** Starts `command` and returns at once, with its output as streams. */
198
+ spawn = async (options) => {
199
+ const { abortSignal } = options;
200
+ abortSignal?.throwIfAborted();
201
+ if (options.stdin instanceof Uint8Array)
202
+ throw new RuntimeError({
203
+ message: "spawn takes standard input as text; use run for bytes.",
204
+ code: "invalid_request",
205
+ status: 0,
206
+ });
207
+ const { timeoutMs, ...command } = this.#commandOptions(options);
208
+ const process = await this.sandbox.spawn(options.command, {
209
+ ...command,
210
+ // A spawned process is often a server: it runs until killed unless a limit is asked for.
211
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs }),
212
+ ...(options.stdin === undefined ? {} : { stdin: options.stdin }),
213
+ ...(abortSignal ? { signal: abortSignal } : {}),
214
+ });
215
+ return this.#process(process, abortSignal);
216
+ };
217
+ #process(process, abortSignal) {
218
+ const encoder = new TextEncoder();
219
+ const streams = {};
220
+ const stdout = new ReadableStream({ start: (c) => void (streams.stdout = c) });
221
+ const stderr = new ReadableStream({ start: (c) => void (streams.stderr = c) });
222
+ const stopReading = new AbortController();
223
+ const onAbort = () => {
224
+ stopReading.abort();
225
+ void this.#kill(process.id);
226
+ };
227
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
228
+ const drained = (async () => {
229
+ let exit;
230
+ try {
231
+ for await (const event of process.output({ signal: stopReading.signal })) {
232
+ if (event.type === "stdout" || event.type === "stderr")
233
+ streams[event.type].enqueue(encoder.encode(event.data));
234
+ else if (event.type === "exit")
235
+ exit = event;
236
+ }
237
+ streams.stdout.close();
238
+ streams.stderr.close();
239
+ }
240
+ catch (error) {
241
+ streams.stdout.error(error);
242
+ streams.stderr.error(error);
243
+ throw error;
244
+ }
245
+ finally {
246
+ abortSignal?.removeEventListener("abort", onAbort);
247
+ }
248
+ return exit;
249
+ })();
250
+ // Read or not, a failure is reported by wait(), never as an unhandled rejection.
251
+ drained.catch(() => undefined);
252
+ let killed = false;
253
+ return {
254
+ stdout,
255
+ stderr,
256
+ wait: async () => {
257
+ let exit;
258
+ try {
259
+ exit = await drained;
260
+ }
261
+ catch (error) {
262
+ if (abortSignal?.aborted)
263
+ throw abortSignal.reason ?? error;
264
+ throw error;
265
+ }
266
+ if (abortSignal?.aborted)
267
+ throw abortSignal.reason;
268
+ return {
269
+ exitCode: exit?.exitCode ?? (exit?.timedOut ? TIMED_OUT_EXIT : KILLED_EXIT),
270
+ };
271
+ },
272
+ kill: async () => {
273
+ if (killed)
274
+ return;
275
+ killed = true;
276
+ await this.#kill(process.id);
277
+ },
278
+ };
279
+ }
280
+ /** Ends a process; one that has already exited is not an error. */
281
+ async #kill(processId) {
282
+ try {
283
+ const process = await this.sandbox.processes.get(processId);
284
+ if (process.info.state === "running")
285
+ await process.kill("SIGKILL");
286
+ }
287
+ catch {
288
+ // Gone already, or the sandbox is: nothing is left to end.
289
+ }
290
+ }
291
+ readBinaryFile = async (options) => {
292
+ options.abortSignal?.throwIfAborted();
293
+ try {
294
+ return await this.sandbox.files.read(this.#path(options.path), options.abortSignal ? { signal: options.abortSignal } : {});
295
+ }
296
+ catch (error) {
297
+ if (error instanceof NotFoundError)
298
+ return null;
299
+ throw error;
300
+ }
301
+ };
302
+ readFile = async (options) => {
303
+ const bytes = await this.readBinaryFile(options);
304
+ if (!bytes)
305
+ return null;
306
+ return new ReadableStream({
307
+ start(controller) {
308
+ controller.enqueue(bytes);
309
+ controller.close();
310
+ },
311
+ });
312
+ };
313
+ readTextFile = async (options) => {
314
+ const bytes = await this.readBinaryFile(options);
315
+ if (!bytes)
316
+ return null;
317
+ const text = new TextDecoder(options.encoding ?? "utf-8").decode(bytes);
318
+ return lines(text, options.startLine, options.endLine);
319
+ };
320
+ /** Writes atomically, making parent directories. Past 1 MiB a file must be under /workspace. */
321
+ writeBinaryFile = async (options) => {
322
+ options.abortSignal?.throwIfAborted();
323
+ await this.sandbox.files.write(this.#path(options.path), options.content, options.abortSignal ? { signal: options.abortSignal } : {});
324
+ };
325
+ writeFile = async (options) => {
326
+ const content = new Uint8Array(await new Response(options.content).arrayBuffer());
327
+ await this.writeBinaryFile({ ...options, content });
328
+ };
329
+ writeTextFile = async (options) => {
330
+ const encoding = (options.encoding ?? "utf-8");
331
+ await this.writeBinaryFile({
332
+ ...options,
333
+ content: new Uint8Array(Buffer.from(options.content, encoding)),
334
+ });
335
+ };
336
+ }
337
+ /** The session a harness keeps: files and processes, plus ports as previews,
338
+ * network rules, and stopping. */
339
+ export class RuntimeNetworkSandboxSession extends RuntimeSandboxSession {
340
+ settings;
341
+ ownsLifecycle;
342
+ #ports = [];
343
+ constructor(sandbox, settings, ownsLifecycle) {
344
+ super(sandbox, settings);
345
+ this.settings = settings;
346
+ this.ownsLifecycle = ownsLifecycle;
347
+ }
348
+ /** The sandbox's id: `resumeSession` finds it again by the name it was given. */
349
+ get id() {
350
+ return this.sandbox.id;
351
+ }
352
+ get ports() {
353
+ return this.#ports;
354
+ }
355
+ get description() {
356
+ const ports = this.#ports.length
357
+ ? `\nPorts ${this.#ports.join(", ")} are shared at HTTPS preview addresses; listen on 0.0.0.0 or localhost.`
358
+ : "";
359
+ return `${super.description}${ports}`;
360
+ }
361
+ restricted = () => {
362
+ const session = new RuntimeSandboxSession(this.sandbox, this.settings);
363
+ session.defaultWorkingDirectory = this.defaultWorkingDirectory;
364
+ return session;
365
+ };
366
+ /** Where to reach `port`. A private preview's token is in the URL's query and
367
+ * in `headers`. For `ws` the URL has no token: the preview edge answers a GET
368
+ * whose query carries one with a redirect that sets a cookie, which a
369
+ * WebSocket client does not follow, so the header carries it. */
370
+ getPortEndpoint = async (options) => {
371
+ if (!this.#ports.includes(options.port))
372
+ throw new HarnessCapabilityUnsupportedError({
373
+ harnessId: PROVIDER_ID,
374
+ message: `Port ${options.port} is not exposed on this sandbox. Exposed ports: [${this.#ports.join(", ")}]. Pass it in createRuntimeSandbox({ ports }).`,
375
+ });
376
+ const preview = await this.#share(options.port);
377
+ const protocol = options.protocol ?? "https";
378
+ const url = new URL(preview.url);
379
+ url.protocol = protocol === "ws" ? "wss:" : "https:";
380
+ if (!preview.token)
381
+ return { url: url.toString() };
382
+ if (protocol !== "ws")
383
+ url.searchParams.set(TOKEN_QUERY, preview.token);
384
+ return { url: url.toString(), headers: { [PREVIEW_TOKEN_HEADER]: preview.token } };
385
+ };
386
+ /** @deprecated As the AI SDK's: use `getPortEndpoint`. The URL carries the token in its query. */
387
+ getPortUrl = async (options) => {
388
+ const endpoint = await this.getPortEndpoint({ ...options, protocol: "https" });
389
+ const url = new URL(endpoint.url);
390
+ if (options.protocol === "ws")
391
+ url.protocol = "wss:";
392
+ return url.toString();
393
+ };
394
+ /** Shares exactly `ports`: new ones become previews, dropped ones stop being shared. */
395
+ setPorts = async (ports, options = {}) => {
396
+ const wanted = [...new Set(ports)];
397
+ const request = options.abortSignal ? { signal: options.abortSignal } : {};
398
+ for (const port of this.#ports.filter((port) => !wanted.includes(port)))
399
+ await this.sandbox.previews.delete(port, request).catch((error) => {
400
+ if (!(error instanceof NotFoundError))
401
+ throw error;
402
+ });
403
+ for (const port of wanted.filter((port) => !this.#ports.includes(port)))
404
+ await this.#share(port, request);
405
+ this.#ports = wanted;
406
+ };
407
+ /** Creates the preview, or answers the existing one with a fresh token. */
408
+ #share(port, request = {}) {
409
+ return this.sandbox.previews.create(port, {
410
+ visibility: this.settings.previewVisibility ?? "private",
411
+ ...(this.settings.previewTtlSeconds ? { ttlSeconds: this.settings.previewTtlSeconds } : {}),
412
+ }, request);
413
+ }
414
+ /** Outbound rules: `allow-all` is the public web, `deny-all` nothing, and
415
+ * `custom` only the listed hosts and ranges, with `deniedCIDRs` refused. */
416
+ setNetworkPolicy = async (policy) => {
417
+ await this.sandbox.network.set(networkRules(policy));
418
+ };
419
+ /** Stops the sandbox (or pauses it, with `pauseOnStop`). A wrapped sandbox is left alone. */
420
+ stop = async () => {
421
+ if (!this.ownsLifecycle)
422
+ return;
423
+ if (this.settings.pauseOnStop) {
424
+ try {
425
+ await this.sandbox.pause();
426
+ return;
427
+ }
428
+ catch {
429
+ // Stop it rather than leave it running.
430
+ }
431
+ }
432
+ await this.#stop();
433
+ };
434
+ /** Stops the sandbox, whatever `pauseOnStop` says: its files and memory are gone. */
435
+ destroy = async () => {
436
+ if (!this.ownsLifecycle)
437
+ return;
438
+ await this.#stop();
439
+ };
440
+ async #stop() {
441
+ try {
442
+ await this.sandbox.stop();
443
+ }
444
+ catch (error) {
445
+ if (!(error instanceof NotFoundError))
446
+ throw error;
447
+ }
448
+ }
449
+ }
450
+ /** The harness's network policy as a Runtime sandbox's network rules. */
451
+ export function networkRules(policy) {
452
+ if (policy.mode === "allow-all")
453
+ return { internet: true };
454
+ if (policy.mode === "deny-all")
455
+ return { internet: false };
456
+ const allow = [...(policy.allowedHosts ?? []), ...(policy.allowedCIDRs ?? [])];
457
+ const deny = [...(policy.deniedCIDRs ?? [])];
458
+ return { internet: true, allow, ...(deny.length ? { deny } : {}) };
459
+ }
460
+ /** A missing or refused key as the harness's own error, so a framework can
461
+ * tell configuration from an outage. */
462
+ async function authenticated(operation) {
463
+ try {
464
+ return await operation();
465
+ }
466
+ catch (error) {
467
+ if (error instanceof AuthenticationError ||
468
+ error instanceof PermissionDeniedError ||
469
+ (error instanceof RuntimeError && error.code === "missing_api_key"))
470
+ throw new HarnessSandboxAuthenticationError({
471
+ message: `Runtime authentication failed: ${error.message} Set RUNTIME_API_KEY, run \`npx -y withruntime login\`, or pass createRuntimeSandbox({ runtime: new Runtime({ apiKey }) }).`,
472
+ sandboxProviderId: PROVIDER_ID,
473
+ cause: error,
474
+ });
475
+ throw error;
476
+ }
477
+ }
478
+ /** Lines `start` to `end` (1-based, inclusive) of `text`; past the end is the
479
+ * end. The file's own line ending is kept, as the AI SDK's `extractLines`. */
480
+ function lines(text, start, end) {
481
+ if (start === undefined && end === undefined)
482
+ return text;
483
+ const eol = text.includes("\r\n")
484
+ ? "\r\n"
485
+ : !text.includes("\n") && text.includes("\r")
486
+ ? "\r"
487
+ : "\n";
488
+ const all = text.split(eol);
489
+ const from = Math.max(1, start ?? 1) - 1;
490
+ const to = Math.min(all.length, end ?? all.length);
491
+ return all.slice(from, to).join(eol);
492
+ }
493
+ function resolvePosix(base, path) {
494
+ const parts = [];
495
+ for (const part of `${path.startsWith("/") ? "" : `${base}/`}${path}`.split("/")) {
496
+ if (part === "" || part === ".")
497
+ continue;
498
+ if (part === "..")
499
+ parts.pop();
500
+ else
501
+ parts.push(part);
502
+ }
503
+ return `/${parts.join("/")}`;
504
+ }
package/dist/cli.js CHANGED
@@ -238,6 +238,8 @@ function unknown(what, word, choices) {
238
238
  return usage(`Unknown ${what} ${word}.${meant ? ` Did you mean ${meant}?` : ""}`);
239
239
  }
240
240
  const COMMANDS = [
241
+ "account",
242
+ "audit",
241
243
  "login",
242
244
  "logout",
243
245
  "whoami",
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.6.0";
2
+ export declare const VERSION = "0.6.1";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  export type RequestOptions = {
5
5
  /** Sent as Idempotency-Key. Omit it: the SDK makes one per call and keeps
package/dist/transport.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ConnectionError, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
- export const VERSION = "0.6.0";
3
+ export const VERSION = "0.6.1";
4
4
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
5
5
  export function apiOrigin(value) {
6
6
  const url = new URL(value);
package/package.json CHANGED
@@ -1,45 +1,59 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "license": "Apache-2.0",
5
5
  "mcpName": "com.withruntime/runtime",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
- "import": "./dist/index.js"
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
11
12
  },
12
13
  "./e2b": {
13
14
  "types": "./dist/e2b/index.d.ts",
14
- "import": "./dist/e2b/index.js"
15
+ "import": "./dist/e2b/index.js",
16
+ "default": "./dist/e2b/index.js"
15
17
  },
16
18
  "./e2b/code-interpreter": {
17
19
  "types": "./dist/e2b/code-interpreter.d.ts",
18
- "import": "./dist/e2b/code-interpreter.js"
20
+ "import": "./dist/e2b/code-interpreter.js",
21
+ "default": "./dist/e2b/code-interpreter.js"
19
22
  },
20
23
  "./daytona": {
21
24
  "types": "./dist/daytona/index.d.ts",
22
- "import": "./dist/daytona/index.js"
25
+ "import": "./dist/daytona/index.js",
26
+ "default": "./dist/daytona/index.js"
23
27
  },
24
28
  "./vercel": {
25
29
  "types": "./dist/vercel/index.d.ts",
26
- "import": "./dist/vercel/index.js"
30
+ "import": "./dist/vercel/index.js",
31
+ "default": "./dist/vercel/index.js"
27
32
  },
28
33
  "./tools": {
29
34
  "types": "./dist/tools/index.d.ts",
30
- "import": "./dist/tools/index.js"
35
+ "import": "./dist/tools/index.js",
36
+ "default": "./dist/tools/index.js"
31
37
  },
32
38
  "./ai": {
33
39
  "types": "./dist/ai/index.d.ts",
34
- "import": "./dist/ai/index.js"
40
+ "import": "./dist/ai/index.js",
41
+ "default": "./dist/ai/index.js"
35
42
  },
36
43
  "./claude-agent-sdk": {
37
44
  "types": "./dist/claude-agent-sdk/index.d.ts",
38
- "import": "./dist/claude-agent-sdk/index.js"
45
+ "import": "./dist/claude-agent-sdk/index.js",
46
+ "default": "./dist/claude-agent-sdk/index.js"
39
47
  },
40
48
  "./openai-agents": {
41
49
  "types": "./dist/openai-agents/index.d.ts",
42
- "import": "./dist/openai-agents/index.js"
50
+ "import": "./dist/openai-agents/index.js",
51
+ "default": "./dist/openai-agents/index.js"
52
+ },
53
+ "./ai-harness": {
54
+ "types": "./dist/ai-harness/index.d.ts",
55
+ "import": "./dist/ai-harness/index.js",
56
+ "default": "./dist/ai-harness/index.js"
43
57
  }
44
58
  },
45
59
  "bin": {
@@ -69,18 +83,27 @@
69
83
  "test:go": "bun scripts/test-go.ts"
70
84
  },
71
85
  "devDependencies": {
86
+ "@ai-sdk/harness": "1.0.124",
87
+ "@anthropic-ai/claude-agent-sdk": "0.3.277",
88
+ "@anthropic-ai/sdk": "0.128.0",
89
+ "@google/genai": "2.24.0",
90
+ "@mastra/core": "1.71.0",
91
+ "@mistralai/mistralai": "2.7.0",
92
+ "@modelcontextprotocol/sdk": "^1.30.0",
93
+ "@openai/agents": "0.18.0",
94
+ "@openai/agents-core": "0.18.0",
95
+ "@openrouter/sdk": "1.3.27",
72
96
  "@runtime/eslint-config": "workspace:*",
73
97
  "@runtime/typescript-config": "workspace:*",
74
98
  "@types/bun": "^1.4.0",
75
99
  "@types/node": "^20",
100
+ "ai": "7.0.87",
76
101
  "eslint": "^9",
102
+ "groq-sdk": "1.6.0",
103
+ "openai": "7.23.0",
104
+ "together-ai": "0.55.0",
77
105
  "typescript": "^5",
78
- "@openai/agents": "0.18.0",
79
- "@openai/agents-core": "0.18.0",
80
- "@anthropic-ai/claude-agent-sdk": "0.3.277",
81
- "ai": "7.0.87",
82
- "zod": "^4.1.8",
83
- "@modelcontextprotocol/sdk": "^1.30.0"
106
+ "zod": "^4.1.8"
84
107
  },
85
108
  "publishConfig": {
86
109
  "access": "public",
@@ -100,6 +123,7 @@
100
123
  "undici": "^7.29.1"
101
124
  },
102
125
  "peerDependencies": {
126
+ "@ai-sdk/harness": "^1.0.0",
103
127
  "@anthropic-ai/claude-agent-sdk": ">=0.2.0",
104
128
  "@openai/agents": ">=0.18.0",
105
129
  "@openai/agents-core": ">=0.18.0",
@@ -107,6 +131,9 @@
107
131
  "zod": ">=3.25.0"
108
132
  },
109
133
  "peerDependenciesMeta": {
134
+ "@ai-sdk/harness": {
135
+ "optional": true
136
+ },
110
137
  "@anthropic-ai/claude-agent-sdk": {
111
138
  "optional": true
112
139
  },