withruntime 0.8.4 → 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.
- package/COMPATIBILITY.md +145 -0
- package/E2B.md +23 -13
- package/README.md +8 -5
- package/README.src.md +8 -5
- package/compatibility-lock.json +281 -0
- package/dist/ai-harness/index.js +2 -2
- package/dist/api-defaults.d.ts +17 -0
- package/dist/api-defaults.js +22 -0
- package/dist/blaxel/preview.d.ts +1 -1
- package/dist/blaxel/preview.js +1 -1
- package/dist/blaxel/process.d.ts +1 -1
- package/dist/blaxel/process.js +11 -2
- package/dist/cli-words.d.ts +27 -0
- package/dist/cli-words.js +135 -0
- package/dist/cli.js +162 -31
- package/dist/client.d.ts +4 -1
- package/dist/client.js +8 -2
- package/dist/cloudflare/index.d.ts +294 -0
- package/dist/cloudflare/index.js +554 -0
- package/dist/cloudflare/worker.d.ts +415 -0
- package/dist/cloudflare/worker.js +297 -0
- package/dist/codesandbox/guest-service.d.ts +31 -0
- package/dist/codesandbox/guest-service.js +453 -0
- package/dist/codesandbox/index.d.ts +386 -0
- package/dist/codesandbox/index.js +1313 -0
- package/dist/compat/core.d.ts +39 -0
- package/dist/compat/core.js +199 -0
- package/dist/compat/files.d.ts +26 -0
- package/dist/compat/files.js +115 -0
- package/dist/compat/shell.d.ts +4 -0
- package/dist/compat/shell.js +165 -0
- package/dist/daytona/context.js +2 -1
- package/dist/daytona/process.d.ts +1 -1
- package/dist/daytona/process.js +27 -4
- package/dist/e2b/code-interpreter.d.ts +4 -5
- package/dist/e2b/code-interpreter.js +54 -56
- package/dist/e2b/commands.d.ts +5 -4
- package/dist/e2b/commands.js +37 -29
- package/dist/e2b/errors.js +1 -1
- package/dist/e2b/filesystem.d.ts +8 -6
- package/dist/e2b/filesystem.js +127 -28
- package/dist/e2b/sandbox.d.ts +4 -3
- package/dist/e2b/sandbox.js +10 -10
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -2
- package/dist/freestyle/index.d.ts +301 -0
- package/dist/freestyle/index.js +310 -0
- package/dist/index.d.ts +1 -0
- package/dist/mcp.js +2 -2
- package/dist/modal/index.d.ts +205 -0
- package/dist/modal/index.js +485 -0
- package/dist/network-cli.js +1 -1
- package/dist/openai-agents/index.js +9 -26
- package/dist/products/audit.d.ts +3 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/interpreter.d.ts +11 -4
- package/dist/products/interpreter.js +33 -9
- package/dist/products/jobs.js +3 -2
- package/dist/products/limits.d.ts +10 -0
- package/dist/products/previews.d.ts +10 -2
- package/dist/products/previews.js +3 -2
- package/dist/products/tailscale.d.ts +57 -0
- package/dist/products/tailscale.js +17 -0
- package/dist/products/watch.d.ts +25 -4
- package/dist/products/watch.js +75 -14
- package/dist/runloop/index.d.ts +340 -0
- package/dist/runloop/index.js +438 -0
- package/dist/runloop/pagination.d.ts +46 -0
- package/dist/runloop/pagination.js +82 -0
- package/dist/runloop/snapshots.d.ts +52 -0
- package/dist/runloop/snapshots.js +244 -0
- package/dist/sandbox.d.ts +40 -9
- package/dist/sandbox.js +273 -76
- package/dist/snapshots.d.ts +11 -1
- package/dist/snapshots.js +11 -1
- package/dist/sprites/index.d.ts +251 -0
- package/dist/sprites/index.js +552 -0
- package/dist/tar.d.ts +8 -1
- package/dist/tar.js +185 -35
- package/dist/tools/index.js +2 -1
- package/dist/transport.d.ts +20 -2
- package/dist/transport.js +48 -11
- package/dist/types.d.ts +52 -3
- package/dist/vercel/command.js +12 -1
- package/dist/vercel/errors.d.ts +2 -2
- package/dist/vercel/errors.js +12 -1
- package/dist/vercel/filesystem.d.ts +21 -18
- package/dist/vercel/filesystem.js +95 -48
- package/dist/vercel/sandbox.js +5 -5
- 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,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
|
+
`;
|
package/dist/daytona/context.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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>;
|
package/dist/daytona/process.js
CHANGED
|
@@ -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 = '
|
|
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,
|
|
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
|
-
|
|
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);
|