withruntime 0.8.3 → 0.9.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 (92) hide show
  1. package/COMPATIBILITY.md +145 -0
  2. package/E2B.md +23 -13
  3. package/README.md +8 -5
  4. package/README.src.md +8 -5
  5. package/compatibility-lock.json +281 -0
  6. package/dist/ai-harness/index.js +2 -2
  7. package/dist/api-defaults.d.ts +17 -0
  8. package/dist/api-defaults.js +22 -0
  9. package/dist/blaxel/preview.d.ts +1 -1
  10. package/dist/blaxel/preview.js +1 -1
  11. package/dist/blaxel/process.d.ts +1 -1
  12. package/dist/blaxel/process.js +11 -2
  13. package/dist/cli-words.d.ts +27 -0
  14. package/dist/cli-words.js +135 -0
  15. package/dist/cli.js +162 -31
  16. package/dist/client.d.ts +4 -1
  17. package/dist/client.js +8 -2
  18. package/dist/cloudflare/index.d.ts +294 -0
  19. package/dist/cloudflare/index.js +554 -0
  20. package/dist/cloudflare/worker.d.ts +415 -0
  21. package/dist/cloudflare/worker.js +297 -0
  22. package/dist/codesandbox/guest-service.d.ts +31 -0
  23. package/dist/codesandbox/guest-service.js +453 -0
  24. package/dist/codesandbox/index.d.ts +386 -0
  25. package/dist/codesandbox/index.js +1313 -0
  26. package/dist/compat/core.d.ts +39 -0
  27. package/dist/compat/core.js +199 -0
  28. package/dist/compat/files.d.ts +26 -0
  29. package/dist/compat/files.js +115 -0
  30. package/dist/compat/shell.d.ts +4 -0
  31. package/dist/compat/shell.js +165 -0
  32. package/dist/daytona/context.js +2 -1
  33. package/dist/daytona/process.d.ts +1 -1
  34. package/dist/daytona/process.js +27 -4
  35. package/dist/e2b/code-interpreter.d.ts +4 -5
  36. package/dist/e2b/code-interpreter.js +54 -56
  37. package/dist/e2b/commands.d.ts +5 -4
  38. package/dist/e2b/commands.js +37 -29
  39. package/dist/e2b/errors.js +1 -1
  40. package/dist/e2b/filesystem.d.ts +8 -6
  41. package/dist/e2b/filesystem.js +127 -28
  42. package/dist/e2b/sandbox.d.ts +4 -3
  43. package/dist/e2b/sandbox.js +10 -10
  44. package/dist/errors.d.ts +8 -0
  45. package/dist/errors.js +15 -2
  46. package/dist/freestyle/index.d.ts +301 -0
  47. package/dist/freestyle/index.js +310 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/mcp.js +2 -2
  50. package/dist/modal/index.d.ts +205 -0
  51. package/dist/modal/index.js +485 -0
  52. package/dist/network-cli.js +1 -1
  53. package/dist/openai-agents/index.js +9 -26
  54. package/dist/products/audit.d.ts +3 -0
  55. package/dist/products/index.d.ts +2 -0
  56. package/dist/products/index.js +2 -0
  57. package/dist/products/interpreter.d.ts +11 -4
  58. package/dist/products/interpreter.js +33 -9
  59. package/dist/products/jobs.d.ts +1 -1
  60. package/dist/products/jobs.js +5 -4
  61. package/dist/products/limits.d.ts +10 -0
  62. package/dist/products/previews.d.ts +10 -2
  63. package/dist/products/previews.js +3 -2
  64. package/dist/products/tailscale.d.ts +57 -0
  65. package/dist/products/tailscale.js +17 -0
  66. package/dist/products/watch.d.ts +25 -4
  67. package/dist/products/watch.js +75 -14
  68. package/dist/runloop/index.d.ts +340 -0
  69. package/dist/runloop/index.js +438 -0
  70. package/dist/runloop/pagination.d.ts +46 -0
  71. package/dist/runloop/pagination.js +82 -0
  72. package/dist/runloop/snapshots.d.ts +52 -0
  73. package/dist/runloop/snapshots.js +244 -0
  74. package/dist/sandbox.d.ts +40 -9
  75. package/dist/sandbox.js +273 -76
  76. package/dist/snapshots.d.ts +11 -1
  77. package/dist/snapshots.js +11 -1
  78. package/dist/sprites/index.d.ts +251 -0
  79. package/dist/sprites/index.js +552 -0
  80. package/dist/tar.d.ts +8 -1
  81. package/dist/tar.js +185 -35
  82. package/dist/tools/index.js +2 -1
  83. package/dist/transport.d.ts +20 -2
  84. package/dist/transport.js +48 -11
  85. package/dist/types.d.ts +52 -3
  86. package/dist/vercel/command.js +12 -1
  87. package/dist/vercel/errors.d.ts +2 -2
  88. package/dist/vercel/errors.js +12 -1
  89. package/dist/vercel/filesystem.d.ts +21 -18
  90. package/dist/vercel/filesystem.js +95 -48
  91. package/dist/vercel/sandbox.js +5 -5
  92. package/package.json +45 -3
@@ -0,0 +1,39 @@
1
+ import { Runtime, type RuntimeOptions } from "../client.js";
2
+ import type { Sandbox } from "../sandbox.js";
3
+ import type { CreateSandbox, ExecOptions } from "../types.js";
4
+ export type ClientOptions = RuntimeOptions & {
5
+ client?: Runtime;
6
+ };
7
+ export declare const client: ({ client: supplied, ...options }?: ClientOptions) => Runtime;
8
+ /** Reject before allocation: accepting an option that does nothing is not compatibility. */
9
+ export declare class CompatibilityError extends Error {
10
+ readonly provider: string;
11
+ readonly feature: string;
12
+ readonly code = "unsupported_compatibility_option";
13
+ constructor(provider: string, feature: string);
14
+ }
15
+ export declare function only(provider: string, value: object, keys: readonly string[]): void;
16
+ export declare function region(provider: string, value?: string | null): string | undefined;
17
+ export declare const quote: (value: string) => string;
18
+ export declare const pathAt: (path: string, cwd?: string) => string;
19
+ export declare function environment(sandbox: Sandbox): Promise<Record<string, string>>;
20
+ export declare function saveEnvironment(sandbox: Sandbox, env: Record<string, string>): Promise<void>;
21
+ export declare function validateEnvironment(env: Record<string, string>): void;
22
+ export declare function execOptions(sandbox: Sandbox, options?: ExecOptions): Promise<ExecOptions>;
23
+ export declare function create(runtime: Runtime, provider: string, input: CreateSandbox, env?: Record<string, string> | undefined, setup?: (sandbox: Sandbox) => Promise<void>): Promise<Sandbox>;
24
+ export declare function destroy(sandbox: Sandbox): Promise<void>;
25
+ export declare function ready(sandbox: Sandbox): Promise<Sandbox>;
26
+ export declare function lookup(runtime: Runtime, id: string): Promise<Sandbox>;
27
+ export declare function all(runtime: Runtime, provider: string): Promise<Sandbox[]>;
28
+ export declare function image(runtime: Runtime, ref: string): Promise<string>;
29
+ export declare function checked(sandbox: Sandbox, argv: string[], options?: ExecOptions): Promise<void>;
30
+ export declare function missing(path: string): never;
31
+ /** Foreground commands share a resident Bash process. Background commands start
32
+ * from the last completed exported environment/cwd snapshot without blocking it. */
33
+ export declare function shellCommand(sandbox: Sandbox, command: string, name?: string | null, overrides?: {
34
+ env?: Record<string, string>;
35
+ cwd?: string;
36
+ persist?: boolean;
37
+ }): Promise<string | string[]>;
38
+ export declare function closeShell(sandbox: Sandbox, name: string): Promise<void>;
39
+ export declare function shellRoot(name: string): Promise<string>;
@@ -0,0 +1,199 @@
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) {
44
+ try {
45
+ return JSON.parse(await sandbox.files.readText(ENV_PATH));
46
+ }
47
+ catch (error) {
48
+ if (error instanceof NotFoundError)
49
+ return {};
50
+ throw error;
51
+ }
52
+ }
53
+ export async function saveEnvironment(sandbox, env) {
54
+ validateEnvironment(env);
55
+ await sandbox.files.write(ENV_PATH, JSON.stringify(env), { mode: 0o600 });
56
+ }
57
+ export function validateEnvironment(env) {
58
+ for (const key of Object.keys(env))
59
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
60
+ throw new TypeError(`Invalid environment variable: ${key}`);
61
+ for (const value of Object.values(env))
62
+ if (typeof value !== "string" || value.includes("\0"))
63
+ throw new TypeError("Environment values must be strings without NUL bytes");
64
+ }
65
+ export async function execOptions(sandbox, options = {}) {
66
+ return { ...options, env: { ...(await environment(sandbox)), ...options.env } };
67
+ }
68
+ export async function create(runtime, provider, input, env = undefined, setup) {
69
+ if (env !== undefined)
70
+ validateEnvironment(env);
71
+ const sandbox = await runtime.sandboxes.create({
72
+ ...input,
73
+ labels: { ...input.labels, "compat.provider": provider },
74
+ });
75
+ try {
76
+ if (env !== undefined || !input.snapshot)
77
+ await saveEnvironment(sandbox, env ?? {});
78
+ await setup?.(sandbox);
79
+ return sandbox;
80
+ }
81
+ catch (error) {
82
+ try {
83
+ await destroy(sandbox);
84
+ }
85
+ catch (cleanup) {
86
+ throw new AggregateError([error, cleanup], `Initialization failed and sandbox ${sandbox.id} could not be cleaned up.`);
87
+ }
88
+ throw error;
89
+ }
90
+ }
91
+ export async function destroy(sandbox) {
92
+ // Stop before releasing persistence: a paused sandbox cannot change its disk
93
+ // meter, and must never be woken solely to delete it. The stopped update queues
94
+ // disk destruction; see ARCHITECTURE.md section 10 and migration 0181.
95
+ await sandbox.stop();
96
+ if (sandbox.info.persistent)
97
+ await sandbox.update({ persistent: false });
98
+ }
99
+ export async function ready(sandbox) {
100
+ if (sandbox.state === "paused")
101
+ await sandbox.wake();
102
+ else if (sandbox.state === "stopped")
103
+ await sandbox.restart();
104
+ else if (sandbox.state !== "running")
105
+ await sandbox.waitFor("running");
106
+ return sandbox;
107
+ }
108
+ export async function lookup(runtime, id) {
109
+ if (/^[a-f0-9-]{36}$/i.test(id))
110
+ return runtime.sandboxes.get(id);
111
+ const page = await runtime.sandboxes.list({ name: id, includeStopped: true });
112
+ for await (const sandbox of page)
113
+ if (sandbox.info.name === id)
114
+ return sandbox;
115
+ // IDs are not necessarily UUIDs in local or self-hosted servers.
116
+ return runtime.sandboxes.get(id);
117
+ }
118
+ export async function all(runtime, provider) {
119
+ const result = [];
120
+ for await (const sandbox of await runtime.sandboxes.list({
121
+ labels: { "compat.provider": provider },
122
+ includeStopped: true,
123
+ }))
124
+ result.push(sandbox);
125
+ return result;
126
+ }
127
+ export async function image(runtime, ref) {
128
+ if (ref.startsWith("runtime:"))
129
+ return (await runtime.images.resolve(ref.slice(8))).id;
130
+ return (await runtime.images.build({ image: ref })).id;
131
+ }
132
+ export async function checked(sandbox, argv, options = {}) {
133
+ await sandbox.exec(argv, { ...options, check: true });
134
+ }
135
+ export function missing(path) {
136
+ throw new RuntimeError({
137
+ message: `${path} does not exist`,
138
+ code: "file_not_found",
139
+ status: 404,
140
+ });
141
+ }
142
+ /** Foreground commands share a resident Bash process. Background commands start
143
+ * from the last completed exported environment/cwd snapshot without blocking it. */
144
+ export async function shellCommand(sandbox, command, name, overrides = {}) {
145
+ if (!name)
146
+ return command;
147
+ const root = await shellRoot(name);
148
+ await sandbox.files.mkdir(root, { parents: true });
149
+ if (overrides.persist !== false) {
150
+ const script = "/workspace/.runtime-compat/shell.py";
151
+ await sandbox.files.write(script, SHELL_BROKER, { mode: 0o600 });
152
+ return [
153
+ "python3",
154
+ script,
155
+ "client",
156
+ root,
157
+ JSON.stringify({
158
+ command,
159
+ env: overrides.env ?? {},
160
+ cwd: overrides.cwd,
161
+ baseEnv: await environment(sandbox),
162
+ }),
163
+ ];
164
+ }
165
+ return [
166
+ ...(overrides.persist === false ? [] : ["flock", `${root}/lock`]),
167
+ "bash",
168
+ "-c",
169
+ '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"',
170
+ "bash",
171
+ `${root}/state`,
172
+ command,
173
+ overrides.cwd ?? "",
174
+ overrides.persist === false ? "no" : "yes",
175
+ ...Object.keys(overrides.env ?? {}),
176
+ ];
177
+ }
178
+ export async function closeShell(sandbox, name) {
179
+ const root = await shellRoot(name);
180
+ const script = "/workspace/.runtime-compat/shell.py";
181
+ if (await sandbox.files.exists(script)) {
182
+ const result = await sandbox.exec([
183
+ "python3",
184
+ script,
185
+ "client",
186
+ root,
187
+ JSON.stringify({ action: "destroy" }),
188
+ ]);
189
+ if (result.exitCode !== 0)
190
+ throw new Error(`Could not close named shell: ${result.stderr}`);
191
+ }
192
+ if (await sandbox.files.exists(root))
193
+ await sandbox.files.remove(root, { recursive: true });
194
+ }
195
+ export async function shellRoot(name) {
196
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(name));
197
+ const key = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
198
+ return `/workspace/.runtime-compat/shells/${key}`;
199
+ }
@@ -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,115 @@
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
+ await sandbox.files.write(staging, bytes, { mode: 0o600, signal: options.signal });
29
+ try {
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
+ await sandbox.files.remove(staging);
43
+ }
44
+ }
45
+ writeTextFile(path, content) {
46
+ return this.writeFile(path, content);
47
+ }
48
+ async mkdir(path, recursive = false) {
49
+ await (await this.get()).files.mkdir(this.path(path), { parents: recursive });
50
+ }
51
+ async remove(path, recursive = false) {
52
+ await (await this.get()).files.remove(this.path(path), { recursive });
53
+ }
54
+ async rename(from, to, overwrite = false) {
55
+ await (await this.get()).files.rename(this.path(from), this.path(to), { overwrite });
56
+ }
57
+ async copy(from, to, recursive = false, overwrite = false) {
58
+ const sandbox = await this.get();
59
+ if (!overwrite && (await sandbox.files.exists(this.path(to))))
60
+ throw new Error(`Destination exists: ${to}`);
61
+ await checked(sandbox, [
62
+ "cp",
63
+ ...(recursive ? ["-R"] : []),
64
+ "--",
65
+ this.path(from),
66
+ this.path(to),
67
+ ]);
68
+ }
69
+ async exists(path) {
70
+ return (await this.get()).files.exists(this.path(path));
71
+ }
72
+ async stat(path) {
73
+ const entry = await (await this.get()).files.stat(this.path(path));
74
+ if (!entry.exists)
75
+ missing(path);
76
+ return entry;
77
+ }
78
+ async readdir(path) {
79
+ return (await this.get()).files.list(this.path(path), { hidden: true });
80
+ }
81
+ async chmod(path, mode, recursive = false) {
82
+ await checked(await this.get(), [
83
+ "chmod",
84
+ ...(recursive ? ["-R"] : []),
85
+ (mode & 0o7777).toString(8),
86
+ "--",
87
+ this.path(path),
88
+ ]);
89
+ }
90
+ async appendFile(path, content) {
91
+ const sandbox = await this.get();
92
+ const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
93
+ if (bytes.byteLength <= 1_048_576) {
94
+ await checked(sandbox, ["bash", "-c", 'cat >> "$1"', "bash", this.path(path)], {
95
+ stdin: bytes,
96
+ });
97
+ return;
98
+ }
99
+ const staging = `/workspace/.runtime-compat/append-${crypto.randomUUID()}`;
100
+ await sandbox.files.write(staging, bytes, { mode: 0o600 });
101
+ try {
102
+ await checked(sandbox, [
103
+ "bash",
104
+ "-c",
105
+ 'cat -- "$1" >> "$2"',
106
+ "bash",
107
+ staging,
108
+ this.path(path),
109
+ ]);
110
+ }
111
+ finally {
112
+ await sandbox.files.remove(staging);
113
+ }
114
+ }
115
+ }
@@ -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
+ `;
@@ -17,7 +17,8 @@ export function resolvePath(path) {
17
17
  return `${HOME}/${path.slice(2)}`;
18
18
  if (path.startsWith("/"))
19
19
  return path;
20
- return `${HOME}/${path.replace(/^\.\/?/, "")}`.replace(/\/+$/, "") || HOME;
20
+ // Only "." components are dropped: ".env" and "..x" keep their dots.
21
+ return `${HOME}/${path.replace(/^(?:\.(?:\/+|$))+/, "")}`.replace(/\/+$/, "") || HOME;
21
22
  }
22
23
  /** Quotes a word for bash. */
23
24
  export function quote(word) {
@@ -63,7 +63,7 @@ export declare class Process {
63
63
  getSessionCommand(sessionId: string, commandId: string): Promise<Command>;
64
64
  /** Runs a command in the session. It waits and returns the output and exit
65
65
  * code, or with `runAsync: true` returns the cmdId at once. */
66
- executeSessionCommand(sessionId: string, req: SessionExecuteRequest, _timeout?: number): Promise<SessionExecuteResponse>;
66
+ executeSessionCommand(sessionId: string, req: SessionExecuteRequest, timeout?: number): Promise<SessionExecuteResponse>;
67
67
  /** A command's output so far, or with callbacks, its output from the start
68
68
  * as it happens until it ends. */
69
69
  getSessionCommandLogs(sessionId: string, commandId: string): Promise<SessionCommandLogsResponse>;
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { HOME, LONGEST_MS, quote, resolvePath, WHOLE_OUTPUT, } from "./context.js";
3
- import { DaytonaCommandAlreadyCompletedError, DaytonaConflictError, DaytonaNotFoundError, DaytonaProcessExecutionTimeoutError, DaytonaSessionEndedError, guard, NotSupportedError, } from "./errors.js";
3
+ import { DaytonaCommandAlreadyCompletedError, DaytonaConflictError, DaytonaConnectionError, DaytonaNotFoundError, DaytonaProcessExecutionTimeoutError, DaytonaSessionEndedError, guard, NotSupportedError, } from "./errors.js";
4
4
  export class CodeRunParams {
5
5
  argv;
6
6
  env;
@@ -11,7 +11,7 @@ const MARK = "\x1e";
11
11
  The command travels base64-encoded and runs through eval, keeping the
12
12
  shell's directory, variables and functions from one command to the next;
13
13
  the markers say where its output ends and what it exited with. */
14
- const PRELUDE = '__rt_run() { eval "$(printf %s "$1" | base64 -d)"; __rt_c=$?; ' +
14
+ const PRELUDE = '__rt_eval() { eval "$(printf %s "$1" | base64 -d)"; }; __rt_run() { __rt_eval "$1"; __rt_c=$?; ' +
15
15
  'printf \'\\036RT%s:%s\\036\' "$2" "$__rt_c"; printf \'\\036RT%s\\036\' "$2" >&2; return $__rt_c; }\n';
16
16
  const TAG = "daytona-session:";
17
17
  class SessionCommand {
@@ -78,6 +78,8 @@ export class Process {
78
78
  timeoutMs,
79
79
  ...WHOLE_OUTPUT,
80
80
  }));
81
+ if (result.stdoutTruncated || result.stderrTruncated)
82
+ throw new DaytonaConnectionError("Some command output is no longer available.", 502, undefined, "output_truncated");
81
83
  if (result.timedOut)
82
84
  throw new DaytonaProcessExecutionTimeoutError(`Command timed out after ${timeout} s. Pass a larger timeout, or 0 for none.`, 408);
83
85
  const output = result.stdout + result.stderr;
@@ -100,6 +102,8 @@ export class Process {
100
102
  timeoutMs,
101
103
  ...WHOLE_OUTPUT,
102
104
  }));
105
+ if (result.stdoutTruncated || result.stderrTruncated)
106
+ throw new DaytonaConnectionError("Some command output is no longer available.", 502, undefined, "output_truncated");
103
107
  if (result.timedOut)
104
108
  throw new DaytonaProcessExecutionTimeoutError(`Code run timed out after ${timeout} s.`, 408);
105
109
  return {
@@ -162,7 +166,7 @@ export class Process {
162
166
  }
163
167
  /** Runs a command in the session. It waits and returns the output and exit
164
168
  * code, or with `runAsync: true` returns the cmdId at once. */
165
- async executeSessionCommand(sessionId, req, _timeout) {
169
+ async executeSessionCommand(sessionId, req, timeout) {
166
170
  const session = await this.#session(sessionId);
167
171
  await this.#ctx.ensureHome(req.command);
168
172
  await this.#ctx.live();
@@ -192,7 +196,22 @@ export class Process {
192
196
  command.done.catch(() => undefined);
193
197
  return { cmdId: command.id };
194
198
  }
195
- await command.done;
199
+ if (timeout && timeout > 0) {
200
+ let timer;
201
+ try {
202
+ await Promise.race([
203
+ command.done,
204
+ new Promise((_, reject) => {
205
+ timer = setTimeout(() => reject(new DaytonaProcessExecutionTimeoutError(`Waiting for session command timed out after ${timeout} s. The command continues in the session.`, 408)), timeout * 1000);
206
+ }),
207
+ ]);
208
+ }
209
+ finally {
210
+ clearTimeout(timer);
211
+ }
212
+ }
213
+ else
214
+ await command.done;
196
215
  return {
197
216
  cmdId: command.id,
198
217
  output: command.output,
@@ -216,6 +235,10 @@ export class Process {
216
235
  };
217
236
  await guard("process", async () => {
218
237
  for await (const event of session.process.output({ cursor: session.cursor })) {
238
+ if (event.type === "truncated") {
239
+ session.ended = true;
240
+ throw new DaytonaConnectionError("Session command output was lost.", 502, undefined, "output_truncated");
241
+ }
219
242
  if (event.type === "exit") {
220
243
  session.ended = true;
221
244
  flush("stdout", buffer.stdout.length);