withruntime 0.8.4 → 0.10.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.
Files changed (132) hide show
  1. package/BLAXEL.md +4 -4
  2. package/COMPATIBILITY.md +145 -0
  3. package/DAYTONA.md +13 -7
  4. package/E2B.md +34 -20
  5. package/README.md +22 -11
  6. package/README.src.md +22 -11
  7. package/VERCEL.md +13 -3
  8. package/compatibility-lock.json +357 -0
  9. package/dist/ai-harness/index.js +2 -2
  10. package/dist/api-defaults.d.ts +17 -0
  11. package/dist/api-defaults.js +22 -0
  12. package/dist/blaxel/errors.js +1 -1
  13. package/dist/blaxel/lookup.d.ts +3 -2
  14. package/dist/blaxel/lookup.js +3 -2
  15. package/dist/blaxel/pagination.d.ts +2 -2
  16. package/dist/blaxel/pagination.js +45 -17
  17. package/dist/blaxel/preview.d.ts +3 -2
  18. package/dist/blaxel/preview.js +16 -14
  19. package/dist/blaxel/process.d.ts +1 -1
  20. package/dist/blaxel/process.js +66 -11
  21. package/dist/blaxel/sandbox.d.ts +2 -0
  22. package/dist/blaxel/sandbox.js +2 -0
  23. package/dist/blaxel/snapshot.js +3 -5
  24. package/dist/cli-words.d.ts +27 -0
  25. package/dist/cli-words.js +135 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +351 -85
  28. package/dist/client.d.ts +33 -4
  29. package/dist/client.js +47 -12
  30. package/dist/cloudflare/identity.d.ts +7 -0
  31. package/dist/cloudflare/identity.js +13 -0
  32. package/dist/cloudflare/index.d.ts +303 -0
  33. package/dist/cloudflare/index.js +592 -0
  34. package/dist/cloudflare/worker.d.ts +419 -0
  35. package/dist/cloudflare/worker.js +447 -0
  36. package/dist/codesandbox/guest-service.d.ts +31 -0
  37. package/dist/codesandbox/guest-service.js +490 -0
  38. package/dist/codesandbox/index.d.ts +386 -0
  39. package/dist/codesandbox/index.js +1347 -0
  40. package/dist/compat/core.d.ts +40 -0
  41. package/dist/compat/core.js +238 -0
  42. package/dist/compat/files.d.ts +26 -0
  43. package/dist/compat/files.js +133 -0
  44. package/dist/compat/shell.d.ts +4 -0
  45. package/dist/compat/shell.js +165 -0
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +107 -38
  48. package/dist/daytona/context.js +2 -1
  49. package/dist/daytona/daytona.js +11 -4
  50. package/dist/daytona/filesystem.d.ts +17 -4
  51. package/dist/daytona/filesystem.js +121 -38
  52. package/dist/daytona/index.d.ts +1 -1
  53. package/dist/daytona/process.d.ts +1 -1
  54. package/dist/daytona/process.js +27 -4
  55. package/dist/daytona/sandbox.d.ts +1 -1
  56. package/dist/daytona/sandbox.js +16 -7
  57. package/dist/e2b/client.js +1 -1
  58. package/dist/e2b/code-interpreter.d.ts +4 -5
  59. package/dist/e2b/code-interpreter.js +54 -56
  60. package/dist/e2b/commands.d.ts +35 -12
  61. package/dist/e2b/commands.js +169 -73
  62. package/dist/e2b/errors.js +1 -1
  63. package/dist/e2b/filesystem.d.ts +8 -6
  64. package/dist/e2b/filesystem.js +127 -28
  65. package/dist/e2b/index.d.ts +1 -0
  66. package/dist/e2b/index.js +1 -0
  67. package/dist/e2b/pty.d.ts +24 -0
  68. package/dist/e2b/pty.js +80 -0
  69. package/dist/e2b/sandbox.d.ts +6 -4
  70. package/dist/e2b/sandbox.js +59 -25
  71. package/dist/errors.d.ts +8 -0
  72. package/dist/errors.js +15 -2
  73. package/dist/file-source.d.ts +16 -0
  74. package/dist/file-source.js +162 -0
  75. package/dist/freestyle/index.d.ts +309 -0
  76. package/dist/freestyle/index.js +343 -0
  77. package/dist/index.d.ts +3 -1
  78. package/dist/login.js +16 -10
  79. package/dist/mcp.js +2 -2
  80. package/dist/modal/index.d.ts +205 -0
  81. package/dist/modal/index.js +486 -0
  82. package/dist/network-cli.js +112 -44
  83. package/dist/openai-agents/index.js +18 -71
  84. package/dist/page.d.ts +1 -1
  85. package/dist/page.js +2 -2
  86. package/dist/products/audit.d.ts +3 -0
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/interpreter.d.ts +11 -4
  90. package/dist/products/interpreter.js +33 -9
  91. package/dist/products/jobs.js +3 -2
  92. package/dist/products/limits.d.ts +10 -0
  93. package/dist/products/observability.d.ts +44 -0
  94. package/dist/products/observability.js +6 -0
  95. package/dist/products/previews.d.ts +18 -4
  96. package/dist/products/previews.js +14 -5
  97. package/dist/products/tailscale.d.ts +57 -0
  98. package/dist/products/tailscale.js +17 -0
  99. package/dist/products/volumes.d.ts +42 -0
  100. package/dist/products/volumes.js +36 -0
  101. package/dist/products/watch.d.ts +25 -4
  102. package/dist/products/watch.js +132 -18
  103. package/dist/proxy.js +3 -1
  104. package/dist/runloop/index.d.ts +326 -0
  105. package/dist/runloop/index.js +535 -0
  106. package/dist/runloop/pagination.d.ts +46 -0
  107. package/dist/runloop/pagination.js +82 -0
  108. package/dist/runloop/snapshots.d.ts +52 -0
  109. package/dist/runloop/snapshots.js +244 -0
  110. package/dist/sandbox.d.ts +76 -16
  111. package/dist/sandbox.js +696 -135
  112. package/dist/snapshots.d.ts +20 -5
  113. package/dist/snapshots.js +11 -1
  114. package/dist/sprites/index.d.ts +251 -0
  115. package/dist/sprites/index.js +588 -0
  116. package/dist/tar.d.ts +10 -1
  117. package/dist/tar.js +311 -35
  118. package/dist/tools/index.js +2 -1
  119. package/dist/transport.d.ts +23 -8
  120. package/dist/transport.js +247 -127
  121. package/dist/types.d.ts +52 -3
  122. package/dist/vercel/command.d.ts +1 -1
  123. package/dist/vercel/command.js +15 -3
  124. package/dist/vercel/errors.d.ts +2 -2
  125. package/dist/vercel/errors.js +19 -2
  126. package/dist/vercel/filesystem.d.ts +21 -18
  127. package/dist/vercel/filesystem.js +95 -48
  128. package/dist/vercel/sandbox.d.ts +8 -8
  129. package/dist/vercel/sandbox.js +159 -64
  130. package/dist/vercel/snapshot.d.ts +3 -2
  131. package/dist/vercel/snapshot.js +9 -5
  132. package/package.json +46 -3
@@ -0,0 +1,40 @@
1
+ import { Runtime, type CreateOptions, type RuntimeOptions } from "../client.js";
2
+ import type { Sandbox } from "../sandbox.js";
3
+ import type { CreateSandbox, ExecOptions } from "../types.js";
4
+ import type { RequestOptions } from "../transport.js";
5
+ export type ClientOptions = RuntimeOptions & {
6
+ client?: Runtime;
7
+ };
8
+ export declare const client: ({ client: supplied, ...options }?: ClientOptions) => Runtime;
9
+ /** Reject before allocation: accepting an option that does nothing is not compatibility. */
10
+ export declare class CompatibilityError extends Error {
11
+ readonly provider: string;
12
+ readonly feature: string;
13
+ readonly code = "unsupported_compatibility_option";
14
+ constructor(provider: string, feature: string);
15
+ }
16
+ export declare function only(provider: string, value: object, keys: readonly string[]): void;
17
+ export declare function region(provider: string, value?: string | null): string | undefined;
18
+ export declare const quote: (value: string) => string;
19
+ export declare const pathAt: (path: string, cwd?: string) => string;
20
+ export declare function environment(sandbox: Sandbox, options?: RequestOptions): Promise<Record<string, string>>;
21
+ export declare function saveEnvironment(sandbox: Sandbox, env: Record<string, string>, options?: RequestOptions): Promise<void>;
22
+ export declare function validateEnvironment(env: Record<string, string>): void;
23
+ export declare function execOptions(sandbox: Sandbox, options?: ExecOptions, request?: RequestOptions): Promise<ExecOptions>;
24
+ export declare function create(runtime: Runtime, provider: string, input: CreateSandbox, env?: Record<string, string> | undefined, setup?: (sandbox: Sandbox) => Promise<void>, options?: CreateOptions): Promise<Sandbox>;
25
+ export declare function destroy(sandbox: Sandbox, options?: RequestOptions): Promise<void>;
26
+ export declare function ready(sandbox: Sandbox): Promise<Sandbox>;
27
+ export declare function lookup(runtime: Runtime, id: string, options?: RequestOptions): Promise<Sandbox>;
28
+ export declare function all(runtime: Runtime, provider: string): Promise<Sandbox[]>;
29
+ export declare function image(runtime: Runtime, ref: string): Promise<string>;
30
+ export declare function checked(sandbox: Sandbox, argv: string[], options?: ExecOptions): Promise<void>;
31
+ export declare function missing(path: string): never;
32
+ /** Foreground commands share a resident Bash process. Background commands start
33
+ * from the last completed exported environment/cwd snapshot without blocking it. */
34
+ export declare function shellCommand(sandbox: Sandbox, command: string, name?: string | null, overrides?: {
35
+ env?: Record<string, string>;
36
+ cwd?: string;
37
+ persist?: boolean;
38
+ }, request?: RequestOptions): Promise<string | string[]>;
39
+ export declare function closeShell(sandbox: Sandbox, name: string): Promise<void>;
40
+ export declare function shellRoot(name: string): Promise<string>;
@@ -0,0 +1,238 @@
1
+ import { Runtime } from "../client.js";
2
+ import { RuntimeError, NotFoundError } from "../errors.js";
3
+ import { SHELL_BROKER } from "./shell.js";
4
+ export const client = ({ client: supplied, ...options } = {}) => {
5
+ if (supplied)
6
+ return supplied;
7
+ if (options.baseUrl && !options.fetch) {
8
+ const host = new URL(options.baseUrl).hostname;
9
+ if (host !== "withruntime.com" &&
10
+ !host.endsWith(".withruntime.com") &&
11
+ !["localhost", "127.0.0.1", "[::1]"].includes(host))
12
+ throw new CompatibilityError("Runtime", "a competitor API endpoint; configure the Runtime endpoint or inject a native Runtime client");
13
+ }
14
+ // Vendor credentials must never leave the caller's machine for Runtime.
15
+ const apiKey = options.apiKey?.startsWith("rtcloud_") ? options.apiKey : undefined;
16
+ return new Runtime({ ...options, apiKey });
17
+ };
18
+ /** Reject before allocation: accepting an option that does nothing is not compatibility. */
19
+ export class CompatibilityError extends Error {
20
+ provider;
21
+ feature;
22
+ code = "unsupported_compatibility_option";
23
+ constructor(provider, feature) {
24
+ super(`${provider}: ${feature} is not supported by this Runtime compatibility adapter.`);
25
+ this.provider = provider;
26
+ this.feature = feature;
27
+ this.name = "CompatibilityError";
28
+ }
29
+ }
30
+ export function only(provider, value, keys) {
31
+ for (const [key, item] of Object.entries(value))
32
+ if (item !== undefined && item !== null && !keys.includes(key))
33
+ throw new CompatibilityError(provider, key);
34
+ }
35
+ export function region(provider, value) {
36
+ if (value && value !== "us-east")
37
+ throw new CompatibilityError(provider, `region ${value}; select Runtime's us-east region`);
38
+ return value ?? undefined;
39
+ }
40
+ export const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
41
+ export const pathAt = (path, cwd = "/workspace") => path.startsWith("/") ? path : `${cwd.replace(/\/$/, "")}/${path}`;
42
+ const ENV_PATH = "/workspace/.runtime-compat/environment.json";
43
+ export async function environment(sandbox, options = {}) {
44
+ options.signal?.throwIfAborted();
45
+ try {
46
+ const text = await sandbox.files.readText(ENV_PATH, options);
47
+ options.signal?.throwIfAborted();
48
+ return JSON.parse(text);
49
+ }
50
+ catch (error) {
51
+ if (error instanceof NotFoundError)
52
+ return {};
53
+ throw error;
54
+ }
55
+ }
56
+ export async function saveEnvironment(sandbox, env, options = {}) {
57
+ options.signal?.throwIfAborted();
58
+ validateEnvironment(env);
59
+ await sandbox.files.write(ENV_PATH, JSON.stringify(env), {
60
+ mode: 0o600,
61
+ signal: options.signal,
62
+ timeoutMs: options.timeoutMs,
63
+ });
64
+ }
65
+ export function validateEnvironment(env) {
66
+ for (const key of Object.keys(env))
67
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
68
+ throw new TypeError(`Invalid environment variable: ${key}`);
69
+ for (const value of Object.values(env))
70
+ if (typeof value !== "string" || value.includes("\0"))
71
+ throw new TypeError("Environment values must be strings without NUL bytes");
72
+ }
73
+ export async function execOptions(sandbox, options = {}, request = {}) {
74
+ const requestOptions = { ...request, signal: request.signal ?? options.signal };
75
+ return { ...options, env: { ...(await environment(sandbox, requestOptions)), ...options.env } };
76
+ }
77
+ export async function create(runtime, provider, input, env = undefined, setup, options = {}) {
78
+ options.signal?.throwIfAborted();
79
+ if (env !== undefined)
80
+ validateEnvironment(env);
81
+ const sandbox = await runtime.sandboxes.create({
82
+ ...input,
83
+ labels: { ...input.labels, "compat.provider": provider },
84
+ }, options);
85
+ try {
86
+ options.signal?.throwIfAborted();
87
+ if (env !== undefined || !input.snapshot)
88
+ await saveEnvironment(sandbox, env ?? {}, options);
89
+ options.signal?.throwIfAborted();
90
+ await setup?.(sandbox);
91
+ options.signal?.throwIfAborted();
92
+ return sandbox;
93
+ }
94
+ catch (error) {
95
+ // Cancellation cannot authorize deleting a remote resource: the response
96
+ // may be a retry/reuse of a sandbox that already has somebody's work.
97
+ // Legacy failed-initialization rollback is limited to an ordinary create
98
+ // with a new SDK-generated mutation key and no replay/reuse marker.
99
+ if (options.signal?.aborted ||
100
+ (error instanceof RuntimeError && error.code === "timeout") ||
101
+ input.getOrCreate === true ||
102
+ options.idempotencyKey !== undefined ||
103
+ sandbox.info.replayed === true ||
104
+ sandbox.info.reused === true)
105
+ throw error;
106
+ try {
107
+ await destroy(sandbox);
108
+ }
109
+ catch (cleanup) {
110
+ throw new AggregateError([error, cleanup], `Initialization failed and sandbox ${sandbox.id} could not be cleaned up.`);
111
+ }
112
+ throw error;
113
+ }
114
+ }
115
+ export async function destroy(sandbox, options = {}) {
116
+ // Stop before releasing persistence: a paused sandbox cannot change its disk
117
+ // meter, and must never be woken solely to delete it. The stopped update queues
118
+ // disk destruction; see ARCHITECTURE.md section 10 and migration 0181.
119
+ options.signal?.throwIfAborted();
120
+ if (options.idempotencyKey !== undefined)
121
+ throw new CompatibilityError("Runtime", "caller idempotency keys for compound sandbox deletion");
122
+ await sandbox.stop(options);
123
+ if (sandbox.info.persistent)
124
+ await sandbox.update({ persistent: false }, options);
125
+ }
126
+ export async function ready(sandbox) {
127
+ if (sandbox.state === "paused")
128
+ await sandbox.wake();
129
+ else if (sandbox.state === "stopped")
130
+ await sandbox.restart();
131
+ else if (sandbox.state !== "running")
132
+ await sandbox.waitFor("running");
133
+ return sandbox;
134
+ }
135
+ export async function lookup(runtime, id, options = {}) {
136
+ options.signal?.throwIfAborted();
137
+ if (/^[a-f0-9-]{36}$/i.test(id))
138
+ return runtime.sandboxes.get(id, options);
139
+ const page = await runtime.sandboxes.list({ name: id, includeStopped: true }, options);
140
+ for await (const sandbox of page) {
141
+ options.signal?.throwIfAborted();
142
+ if (sandbox.info.name === id)
143
+ return sandbox;
144
+ }
145
+ // IDs are not necessarily UUIDs in local or self-hosted servers.
146
+ return runtime.sandboxes.get(id, options);
147
+ }
148
+ export async function all(runtime, provider) {
149
+ const result = [];
150
+ for await (const sandbox of await runtime.sandboxes.list({
151
+ labels: { "compat.provider": provider },
152
+ includeStopped: true,
153
+ }))
154
+ result.push(sandbox);
155
+ return result;
156
+ }
157
+ export async function image(runtime, ref) {
158
+ if (ref.startsWith("runtime:"))
159
+ return (await runtime.images.resolve(ref.slice(8))).id;
160
+ return (await runtime.images.build({ image: ref })).id;
161
+ }
162
+ export async function checked(sandbox, argv, options = {}) {
163
+ await sandbox.exec(argv, { ...options, check: true });
164
+ }
165
+ export function missing(path) {
166
+ throw new RuntimeError({
167
+ message: `${path} does not exist`,
168
+ code: "file_not_found",
169
+ status: 404,
170
+ });
171
+ }
172
+ /** Foreground commands share a resident Bash process. Background commands start
173
+ * from the last completed exported environment/cwd snapshot without blocking it. */
174
+ export async function shellCommand(sandbox, command, name, overrides = {}, request = {}) {
175
+ request.signal?.throwIfAborted();
176
+ if (!name)
177
+ return command;
178
+ const root = await shellRoot(name);
179
+ await sandbox.files.mkdir(root, {
180
+ parents: true,
181
+ signal: request.signal,
182
+ timeoutMs: request.timeoutMs,
183
+ });
184
+ if (overrides.persist !== false) {
185
+ const script = "/workspace/.runtime-compat/shell.py";
186
+ await sandbox.files.write(script, SHELL_BROKER, {
187
+ mode: 0o600,
188
+ signal: request.signal,
189
+ timeoutMs: request.timeoutMs,
190
+ });
191
+ return [
192
+ "python3",
193
+ script,
194
+ "client",
195
+ root,
196
+ JSON.stringify({
197
+ command,
198
+ env: overrides.env ?? {},
199
+ cwd: overrides.cwd,
200
+ baseEnv: await environment(sandbox, request),
201
+ }),
202
+ ];
203
+ }
204
+ return [
205
+ ...(overrides.persist === false ? [] : ["flock", `${root}/lock`]),
206
+ "bash",
207
+ "-c",
208
+ 'readonly __runtime_state="$1" __runtime_command="$2" __runtime_cwd="$3" __runtime_persist="$4"; shift 4; __runtime_env=(); for __runtime_key in "$@"; do __runtime_env+=("$__runtime_key=${!__runtime_key}"); done; if [ -f "$__runtime_state" ]; then source "$__runtime_state"; fi; for __runtime_entry in "${__runtime_env[@]}"; do export "$__runtime_entry"; done; if [ -n "$__runtime_cwd" ]; then cd -- "$__runtime_cwd" || exit; fi; if [ "$__runtime_persist" = yes ]; then trap \'__runtime_status=$?; (umask 077; export -p | sed "/^declare -x __runtime_/d" > "$__runtime_state.next"; printf "cd -- %q\\n" "$PWD" >> "$__runtime_state.next"; mv -- "$__runtime_state.next" "$__runtime_state"); exit "$__runtime_status"\' EXIT; fi; eval "$__runtime_command"',
209
+ "bash",
210
+ `${root}/state`,
211
+ command,
212
+ overrides.cwd ?? "",
213
+ overrides.persist === false ? "no" : "yes",
214
+ ...Object.keys(overrides.env ?? {}),
215
+ ];
216
+ }
217
+ export async function closeShell(sandbox, name) {
218
+ const root = await shellRoot(name);
219
+ const script = "/workspace/.runtime-compat/shell.py";
220
+ if (await sandbox.files.exists(script)) {
221
+ const result = await sandbox.exec([
222
+ "python3",
223
+ script,
224
+ "client",
225
+ root,
226
+ JSON.stringify({ action: "destroy" }),
227
+ ]);
228
+ if (result.exitCode !== 0)
229
+ throw new Error(`Could not close named shell: ${result.stderr}`);
230
+ }
231
+ if (await sandbox.files.exists(root))
232
+ await sandbox.files.remove(root, { recursive: true });
233
+ }
234
+ export async function shellRoot(name) {
235
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(name));
236
+ const key = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
237
+ return `/workspace/.runtime-compat/shells/${key}`;
238
+ }
@@ -0,0 +1,26 @@
1
+ import type { Sandbox } from "../sandbox.js";
2
+ /** Guest operations use argv, never interpolated shell paths. */
3
+ export declare class GuestFiles {
4
+ private readonly get;
5
+ readonly cwd: string;
6
+ constructor(get: () => Promise<Sandbox>, cwd?: string);
7
+ path(path: string): string;
8
+ readFile(path: string): Promise<Uint8Array>;
9
+ readTextFile(path: string): Promise<string>;
10
+ writeFile(path: string, content: string | Uint8Array, options?: {
11
+ mode?: number;
12
+ signal?: AbortSignal;
13
+ }): Promise<void>;
14
+ writeTextFile(path: string, content: string): Promise<void>;
15
+ mkdir(path: string, recursive?: boolean): Promise<void>;
16
+ remove(path: string, recursive?: boolean): Promise<void>;
17
+ rename(from: string, to: string, overwrite?: boolean): Promise<void>;
18
+ copy(from: string, to: string, recursive?: boolean, overwrite?: boolean): Promise<void>;
19
+ exists(path: string): Promise<boolean>;
20
+ stat(path: string): Promise<import("../types.js").FileEntry & {
21
+ exists: true;
22
+ }>;
23
+ readdir(path: string): Promise<import("../types.js").FileEntry[]>;
24
+ chmod(path: string, mode: number, recursive?: boolean): Promise<void>;
25
+ appendFile(path: string, content: string | Uint8Array): Promise<void>;
26
+ }
@@ -0,0 +1,133 @@
1
+ import { checked, missing, pathAt } from "./core.js";
2
+ /** Guest operations use argv, never interpolated shell paths. */
3
+ export class GuestFiles {
4
+ get;
5
+ cwd;
6
+ constructor(get, cwd = "/workspace") {
7
+ this.get = get;
8
+ this.cwd = cwd;
9
+ }
10
+ path(path) {
11
+ return pathAt(path, this.cwd);
12
+ }
13
+ async readFile(path) {
14
+ return (await this.get()).files.read(this.path(path));
15
+ }
16
+ async readTextFile(path) {
17
+ return (await this.get()).files.readText(this.path(path));
18
+ }
19
+ async writeFile(path, content, options = {}) {
20
+ const sandbox = await this.get();
21
+ const target = this.path(path);
22
+ const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
23
+ if (bytes.length <= 1_048_576 || target.startsWith("/workspace/")) {
24
+ await sandbox.files.write(target, content, options);
25
+ return;
26
+ }
27
+ const staging = `/workspace/.runtime-compat/write-${crypto.randomUUID()}`;
28
+ try {
29
+ await sandbox.files.write(staging, bytes, { mode: 0o600, signal: options.signal });
30
+ await checked(sandbox, [
31
+ "bash",
32
+ "-c",
33
+ 'set -e; mkdir -p -- "$(dirname -- "$2")"; temp="$2.runtime-$4"; trap \'rm -f -- "$temp"\' EXIT; cp -- "$1" "$temp"; chmod "$3" "$temp"; mv -f -- "$temp" "$2"',
34
+ "bash",
35
+ staging,
36
+ target,
37
+ (options.mode ?? 0o644).toString(8),
38
+ crypto.randomUUID(),
39
+ ], { signal: options.signal });
40
+ }
41
+ finally {
42
+ // Cleanup must preserve the write's outcome, including its original
43
+ // failure; a successful destination write must not invite a replay.
44
+ await sandbox.files.remove(staging).catch(() => undefined);
45
+ }
46
+ }
47
+ writeTextFile(path, content) {
48
+ return this.writeFile(path, content);
49
+ }
50
+ async mkdir(path, recursive = false) {
51
+ await (await this.get()).files.mkdir(this.path(path), { parents: recursive });
52
+ }
53
+ async remove(path, recursive = false) {
54
+ await (await this.get()).files.remove(this.path(path), { recursive });
55
+ }
56
+ async rename(from, to, overwrite = false) {
57
+ await (await this.get()).files.rename(this.path(from), this.path(to), { overwrite });
58
+ }
59
+ async copy(from, to, recursive = false, overwrite = false) {
60
+ const sandbox = await this.get();
61
+ if (!overwrite && (await sandbox.files.exists(this.path(to))))
62
+ throw new Error(`Destination exists: ${to}`);
63
+ if (!overwrite) {
64
+ // cp -n alone reports success when it skips a raced destination. Stage
65
+ // privately, then publish without replacement and verify it moved.
66
+ await checked(sandbox, [
67
+ "bash",
68
+ "-c",
69
+ 'set -e; parent=$(dirname -- "$2"); stage="$parent/.runtime-copy-$4"; mkdir -m 700 -- "$stage"; trap \'rm -rf -- "$stage"\' EXIT; if [ "$3" = true ]; then cp -R -n -- "$1" "$stage/item"; else cp -n -- "$1" "$stage/item"; fi; mv -n -T -- "$stage/item" "$2"; if [ -e "$stage/item" ] || [ -L "$stage/item" ]; then printf "Destination exists: %s\\n" "$2" >&2; exit 73; fi',
70
+ "bash",
71
+ this.path(from),
72
+ this.path(to),
73
+ String(recursive),
74
+ crypto.randomUUID(),
75
+ ]);
76
+ return;
77
+ }
78
+ await checked(sandbox, [
79
+ "cp",
80
+ ...(recursive ? ["-R"] : []),
81
+ "--",
82
+ this.path(from),
83
+ this.path(to),
84
+ ]);
85
+ }
86
+ async exists(path) {
87
+ return (await this.get()).files.exists(this.path(path));
88
+ }
89
+ async stat(path) {
90
+ const entry = await (await this.get()).files.stat(this.path(path));
91
+ if (!entry.exists)
92
+ missing(path);
93
+ return entry;
94
+ }
95
+ async readdir(path) {
96
+ return (await this.get()).files.list(this.path(path), { hidden: true });
97
+ }
98
+ async chmod(path, mode, recursive = false) {
99
+ await checked(await this.get(), [
100
+ "chmod",
101
+ ...(recursive ? ["-R"] : []),
102
+ (mode & 0o7777).toString(8),
103
+ "--",
104
+ this.path(path),
105
+ ]);
106
+ }
107
+ async appendFile(path, content) {
108
+ const sandbox = await this.get();
109
+ const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
110
+ if (bytes.byteLength <= 1_048_576) {
111
+ await checked(sandbox, ["bash", "-c", 'cat >> "$1"', "bash", this.path(path)], {
112
+ stdin: bytes,
113
+ });
114
+ return;
115
+ }
116
+ const staging = `/workspace/.runtime-compat/append-${crypto.randomUUID()}`;
117
+ try {
118
+ await sandbox.files.write(staging, bytes, { mode: 0o600 });
119
+ await checked(sandbox, [
120
+ "bash",
121
+ "-c",
122
+ 'cat -- "$1" >> "$2"',
123
+ "bash",
124
+ staging,
125
+ this.path(path),
126
+ ]);
127
+ }
128
+ finally {
129
+ // A cleanup failure after success must not invite appending twice.
130
+ await sandbox.files.remove(staging).catch(() => undefined);
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,4 @@
1
+ /** Guest-side resident shell. The socket is private to the sandbox user. Native
2
+ * sandbox leases bound its lifetime; cancellation closes the connection and
3
+ * kills the affected shell before another command may start. */
4
+ export declare const SHELL_BROKER: string;
@@ -0,0 +1,165 @@
1
+ /** Guest-side resident shell. The socket is private to the sandbox user. Native
2
+ * sandbox leases bound its lifetime; cancellation closes the connection and
3
+ * kills the affected shell before another command may start. */
4
+ export const SHELL_BROKER = String.raw `import base64, fcntl, json, os, select, selectors, shlex, signal, socket, subprocess, sys, time
5
+
6
+ root = sys.argv[2]
7
+ sockpath = os.path.join(root, 'socket')
8
+ statepath = os.path.join(root, 'state')
9
+
10
+ def send(conn, event):
11
+ conn.sendall((json.dumps(event) + '\n').encode())
12
+
13
+ def stop_shell(shell):
14
+ if shell is not None:
15
+ try: os.killpg(shell.pid, signal.SIGKILL)
16
+ except ProcessLookupError: pass
17
+ shell.wait()
18
+
19
+ def server():
20
+ os.umask(0o077)
21
+ try: os.unlink(sockpath)
22
+ except FileNotFoundError: pass
23
+ listener = socket.socket(socket.AF_UNIX)
24
+ listener.bind(sockpath)
25
+ listener.listen(32)
26
+ shell = None
27
+ status_read = None
28
+ baseline = {}
29
+ try:
30
+ while True:
31
+ conn, _ = listener.accept()
32
+ with conn:
33
+ try:
34
+ data = b''
35
+ while not data.endswith(b'\n'):
36
+ chunk = conn.recv(65536)
37
+ if not chunk: raise ConnectionError('Client disconnected before command')
38
+ data += chunk
39
+ if len(data) > 16 * 1024 * 1024: raise ValueError('Shell request exceeds 16 MiB')
40
+ req = json.loads(data)
41
+ if select.select([conn], [], [], 0)[0] and not conn.recv(1, socket.MSG_PEEK):
42
+ raise ConnectionError('Client disconnected before execution')
43
+ if req.get('action') == 'destroy':
44
+ stop_shell(shell)
45
+ shell = None
46
+ send(conn, {'type':'exit', 'code':0})
47
+ return
48
+ if shell is None or shell.poll() is not None:
49
+ if status_read is not None: os.close(status_read)
50
+ status_read, status_write = os.pipe()
51
+ env = req['initialEnv']
52
+ shell = subprocess.Popen(['bash', '--noprofile', '--norc'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=req['initialCwd'], pass_fds=(status_write,), start_new_session=True)
53
+ baseline = req['baseEnv']
54
+ shell.stdin.write(b'shopt -s expand_aliases\n')
55
+ # Export/cwd snapshots allow upgrades and background processes to reconnect.
56
+ if os.path.exists(statepath):
57
+ shell.stdin.write(('builtin source ' + shlex.quote(statepath) + '\n').encode())
58
+ os.close(status_write)
59
+ lines = ['builtin unset -- ' + shlex.quote(key) for key in baseline if key not in req['baseEnv']]
60
+ for key, value in req['baseEnv'].items():
61
+ if baseline.get(key) != value: lines.append('builtin export -- ' + shlex.quote(key + '=' + value))
62
+ baseline = req['baseEnv']
63
+ for key, value in req['env'].items(): lines.append('builtin export -- ' + shlex.quote(key + '=' + value))
64
+ if req.get('cwd'): lines.append('builtin cd -- ' + shlex.quote(req['cwd']) + ' &&')
65
+ commandpath = os.path.join(root, 'command')
66
+ with open(commandpath, 'w') as source: source.write(req['command'] + '\n')
67
+ lines.append('builtin source ' + shlex.quote(commandpath))
68
+ # All bookkeeping happens after evaluation. No shell variables can
69
+ # overwrite the requested command before it runs.
70
+ script = '\n'.join(lines) + '\n__runtime_status=$?\n'
71
+ script += '(builtin umask 077; builtin export -p | /usr/bin/sed "/^declare -x __runtime_/d" > ' + shlex.quote(statepath + '.next') + '; builtin printf "cd -- %q\\n" "$PWD" >> ' + shlex.quote(statepath + '.next') + '; /bin/mv -- ' + shlex.quote(statepath + '.next') + ' ' + shlex.quote(statepath) + ')\n'
72
+ script += '(umask 077; builtin printf "%s" "$PWD" >| ' + shlex.quote(os.path.join(root, 'cwd')) + ')\n'
73
+ script += 'builtin printf "%s\\n" "$__runtime_status" >&' + str(status_write) + '\n'
74
+ shell.stdin.write(script.encode())
75
+ shell.stdin.flush()
76
+ status = b''
77
+ with selectors.DefaultSelector() as sel:
78
+ sel.register(shell.stdout, selectors.EVENT_READ, 'stdout')
79
+ sel.register(shell.stderr, selectors.EVENT_READ, 'stderr')
80
+ sel.register(status_read, selectors.EVENT_READ, 'status')
81
+ sel.register(conn, selectors.EVENT_READ, 'client')
82
+ while True:
83
+ events = sel.select(0.1)
84
+ # Drain output before acknowledging completion, even when
85
+ # the completion pipe and output become readable together.
86
+ for key, _ in sorted(events, key=lambda event: event[0].data == 'status'):
87
+ kind = key.data
88
+ if kind == 'client':
89
+ if not conn.recv(1, socket.MSG_PEEK): raise ConnectionError('Client disconnected')
90
+ continue
91
+ chunk = os.read(key.fd, 65536)
92
+ if not chunk:
93
+ sel.unregister(key.fileobj)
94
+ continue
95
+ if kind == 'status': status += chunk
96
+ else: send(conn, {'type':kind, 'base64':base64.b64encode(chunk).decode()})
97
+ if b'\n' in status:
98
+ # Pipe readiness can race: exhaust currently buffered output.
99
+ for pipe, kind in [(shell.stdout,'stdout'),(shell.stderr,'stderr')]:
100
+ os.set_blocking(pipe.fileno(), False)
101
+ try:
102
+ while True:
103
+ chunk = os.read(pipe.fileno(), 65536)
104
+ if not chunk: break
105
+ send(conn, {'type':kind,'base64':base64.b64encode(chunk).decode()})
106
+ except BlockingIOError: pass
107
+ finally: os.set_blocking(pipe.fileno(), True)
108
+ send(conn, {'type':'exit','code':int(status.split(b'\n')[0])})
109
+ break
110
+ if shell.poll() is not None:
111
+ # A terminated shell starts fresh next time, not from old exports.
112
+ for stale in (statepath, os.path.join(root, 'cwd')):
113
+ try: os.unlink(stale)
114
+ except FileNotFoundError: pass
115
+ send(conn, {'type':'exit','code':shell.returncode if shell.returncode >= 0 else 128-shell.returncode, 'terminated':True})
116
+ break
117
+ except (BrokenPipeError, ConnectionError, OSError):
118
+ stop_shell(shell)
119
+ shell = None
120
+ except Exception as error:
121
+ stop_shell(shell)
122
+ shell = None
123
+ try: send(conn, {'type':'error','message':str(error)})
124
+ except OSError: pass
125
+ finally:
126
+ stop_shell(shell)
127
+ listener.close()
128
+ try: os.unlink(sockpath)
129
+ except FileNotFoundError: pass
130
+
131
+ if sys.argv[1] == 'server':
132
+ server()
133
+ else:
134
+ os.makedirs(root, mode=0o700, exist_ok=True)
135
+ conn = socket.socket(socket.AF_UNIX)
136
+ with open(os.path.join(root, 'startup.lock'), 'a') as lock:
137
+ fcntl.flock(lock, fcntl.LOCK_EX)
138
+ try: conn.connect(sockpath)
139
+ except (FileNotFoundError, ConnectionRefusedError):
140
+ if json.loads(sys.argv[3]).get('action') == 'destroy': sys.exit(0)
141
+ with open(os.path.join(root, 'startup.log'), 'wb') as log:
142
+ daemon = subprocess.Popen([sys.executable, __file__, 'server', root], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=log, start_new_session=True)
143
+ for _ in range(300):
144
+ try:
145
+ conn.connect(sockpath)
146
+ break
147
+ except (FileNotFoundError, ConnectionRefusedError):
148
+ if daemon.poll() is not None:
149
+ with open(os.path.join(root, 'startup.log')) as log: raise RuntimeError('Resident shell startup: ' + log.read())
150
+ time.sleep(0.01)
151
+ else: raise RuntimeError('Resident shell failed to start')
152
+ with conn:
153
+ req = json.loads(sys.argv[3])
154
+ req['initialEnv'] = dict(os.environ)
155
+ req['initialCwd'] = os.getcwd()
156
+ send(conn, req)
157
+ for line in conn.makefile('rb'):
158
+ event = json.loads(line)
159
+ if event['type'] == 'exit': sys.exit(event['code'])
160
+ if event['type'] == 'error': raise RuntimeError(event['message'])
161
+ target = sys.stdout.buffer if event['type'] == 'stdout' else sys.stderr.buffer
162
+ target.write(base64.b64decode(event['base64']))
163
+ target.flush()
164
+ raise RuntimeError('Resident shell disconnected without exit status')
165
+ `;
@@ -14,6 +14,9 @@ export declare function connectionOrigins(env: NodeJS.ProcessEnv): {
14
14
  api: string;
15
15
  auth: string;
16
16
  };
17
+ /** Browser-issued production credentials must only reach the production API.
18
+ * CLI and MCP share this boundary, including resumed device requests. */
19
+ export declare function checkBrowserOrigins(origins: ReturnType<typeof connectionOrigins>): void;
17
20
  /** Private user storage, outside project files. Nothing reading this module
18
21
  * returns a secret to CLI output. Store names bind credentials to both origins. */
19
22
  export declare function connectionStore(env: NodeJS.ProcessEnv, directory?: string): {
@@ -27,7 +30,9 @@ export declare function connectionStore(env: NodeJS.ProcessEnv, directory?: stri
27
30
  /** Use another saved account's connection, by account id or name. The one
28
31
  * in use moves to the saved accounts. Null when none matches. */
29
32
  use(which: string): Promise<SavedConnection | null>;
30
- remove(): Promise<void>;
33
+ /** Forget the expected credential after a remote revoke or refusal. A
34
+ * concurrent login may have saved another connection during that request. */
35
+ remove(expectedKey?: string): Promise<void>;
31
36
  /** A browser approval that was started and not yet answered, so a command
32
37
  * that was stopped while it waited picks up the same link and code rather
33
38
  * than asking the person to approve a new one. Unreadable or stale ones