withruntime 0.9.0 → 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.
- package/BLAXEL.md +4 -4
- package/DAYTONA.md +13 -7
- package/E2B.md +12 -8
- package/README.md +15 -7
- package/README.src.md +15 -7
- package/VERCEL.md +13 -3
- package/compatibility-lock.json +84 -8
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +2 -1
- package/dist/blaxel/preview.js +15 -13
- package/dist/blaxel/process.js +56 -10
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +189 -54
- package/dist/client.d.ts +29 -3
- package/dist/client.js +40 -11
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.js +47 -9
- package/dist/cloudflare/worker.d.ts +4 -0
- package/dist/cloudflare/worker.js +166 -16
- package/dist/codesandbox/guest-service.js +57 -20
- package/dist/codesandbox/index.js +42 -8
- package/dist/compat/core.d.ts +9 -8
- package/dist/compat/core.js +60 -21
- package/dist/compat/files.js +22 -4
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/daytona.js +11 -4
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +121 -38
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/sandbox.d.ts +1 -1
- package/dist/daytona/sandbox.js +16 -7
- package/dist/e2b/client.js +1 -1
- package/dist/e2b/commands.d.ts +30 -8
- package/dist/e2b/commands.js +142 -54
- package/dist/e2b/index.d.ts +1 -0
- package/dist/e2b/index.js +1 -0
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +80 -0
- package/dist/e2b/sandbox.d.ts +2 -1
- package/dist/e2b/sandbox.js +49 -15
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +9 -1
- package/dist/freestyle/index.js +38 -5
- package/dist/index.d.ts +2 -1
- package/dist/login.js +16 -10
- package/dist/modal/index.js +2 -1
- package/dist/network-cli.js +111 -43
- package/dist/openai-agents/index.js +9 -45
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/index.d.ts +12 -0
- package/dist/products/index.js +10 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +8 -2
- package/dist/products/previews.js +11 -3
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.js +61 -8
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +13 -27
- package/dist/runloop/index.js +149 -52
- package/dist/sandbox.d.ts +37 -8
- package/dist/sandbox.js +432 -68
- package/dist/snapshots.d.ts +9 -4
- package/dist/sprites/index.js +69 -33
- package/dist/tar.d.ts +3 -1
- package/dist/tar.js +140 -14
- package/dist/transport.d.ts +4 -7
- package/dist/transport.js +205 -122
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +3 -2
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +7 -1
- package/dist/vercel/sandbox.d.ts +8 -8
- package/dist/vercel/sandbox.js +154 -59
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +2 -1
|
@@ -58,6 +58,10 @@ const metadata = (s) => ({
|
|
|
58
58
|
: (s.info.labels["cs.privacy"] ?? "private"),
|
|
59
59
|
tags: JSON.parse(s.info.labels["cs.tags"] ?? "[]"),
|
|
60
60
|
});
|
|
61
|
+
function validateHibernationTimeout(seconds) {
|
|
62
|
+
if (seconds !== undefined && (!Number.isSafeInteger(seconds) || seconds < 0 || seconds > 86_400))
|
|
63
|
+
throw new RangeError("hibernationTimeoutSeconds must be an integer from 0 to 86400.");
|
|
64
|
+
}
|
|
61
65
|
export class Sandboxes {
|
|
62
66
|
runtime;
|
|
63
67
|
constructor(runtime) {
|
|
@@ -73,6 +77,7 @@ export class Sandboxes {
|
|
|
73
77
|
"vmTier",
|
|
74
78
|
"hibernationTimeoutSeconds",
|
|
75
79
|
]);
|
|
80
|
+
validateHibernationTimeout(options.hibernationTimeoutSeconds);
|
|
76
81
|
const labels = {
|
|
77
82
|
"compat.provider": "codesandbox",
|
|
78
83
|
"cs.tags": JSON.stringify([...new Set([...(options.tags ?? []), "sdk"])]),
|
|
@@ -119,6 +124,7 @@ export class Sandboxes {
|
|
|
119
124
|
}
|
|
120
125
|
async restart(id, options = {}) {
|
|
121
126
|
only("CodeSandbox restart", options, ["hibernationTimeoutSeconds"]);
|
|
127
|
+
validateHibernationTimeout(options.hibernationTimeoutSeconds);
|
|
122
128
|
const s = await this.runtime.sandboxes.get(id);
|
|
123
129
|
await s.stop();
|
|
124
130
|
await s.restart();
|
|
@@ -143,19 +149,37 @@ export class Sandboxes {
|
|
|
143
149
|
]);
|
|
144
150
|
if (options.orderBy === "updated_at")
|
|
145
151
|
throw new CompatibilityError("CodeSandbox", "sorting by updated_at");
|
|
146
|
-
|
|
152
|
+
const limit = options.limit ?? 50;
|
|
153
|
+
let currentPage = options.pagination?.page ?? 1;
|
|
154
|
+
const pageSize = options.pagination?.pageSize ?? limit;
|
|
155
|
+
for (const [name, value] of [
|
|
156
|
+
["limit", limit],
|
|
157
|
+
["page", currentPage],
|
|
158
|
+
["pageSize", pageSize],
|
|
159
|
+
])
|
|
160
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
161
|
+
throw new RangeError(`${name} must be a positive safe integer.`);
|
|
162
|
+
const sandboxes = (await all(this.runtime, "codesandbox"))
|
|
147
163
|
.filter((s) => (!options.status || s.state === "running") &&
|
|
148
164
|
(options.tags ?? []).every((tag) => metadata(s).tags.includes(tag)))
|
|
149
165
|
.map(metadata);
|
|
150
166
|
sandboxes.sort((a, b) => (a.createdAt.getTime() - b.createdAt.getTime()) * (options.direction === "desc" ? -1 : 1));
|
|
151
|
-
const totalCount = sandboxes.length
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
const totalCount = sandboxes.length;
|
|
168
|
+
const selected = [];
|
|
169
|
+
let nextPage;
|
|
170
|
+
for (;;) {
|
|
171
|
+
const offset = (currentPage - 1) * pageSize;
|
|
172
|
+
selected.push(...sandboxes.slice(offset, offset + pageSize));
|
|
173
|
+
nextPage = offset + pageSize < totalCount ? currentPage + 1 : null;
|
|
174
|
+
if (nextPage === null || selected.length >= limit)
|
|
175
|
+
break;
|
|
176
|
+
currentPage = nextPage;
|
|
177
|
+
}
|
|
154
178
|
return {
|
|
155
|
-
sandboxes,
|
|
179
|
+
sandboxes: selected,
|
|
156
180
|
totalCount,
|
|
157
|
-
hasMore,
|
|
158
|
-
pagination: { currentPage
|
|
181
|
+
hasMore: totalCount > selected.length,
|
|
182
|
+
pagination: { currentPage, nextPage, pageSize },
|
|
159
183
|
};
|
|
160
184
|
}
|
|
161
185
|
}
|
|
@@ -475,14 +499,22 @@ export class SandboxClient {
|
|
|
475
499
|
};
|
|
476
500
|
this.ports = {
|
|
477
501
|
get: async (port) => {
|
|
502
|
+
this.assertConnected();
|
|
478
503
|
if (native.info.labels["cs.privacy"] === "private")
|
|
479
504
|
throw new CompatibilityError("CodeSandbox", "private port host authentication; use Runtime signed preview URLs");
|
|
480
505
|
const p = await native.previews.create(port, { visibility: "public" });
|
|
481
506
|
this.previewURLs.set(port, p.urlWithToken ?? p.url);
|
|
482
507
|
return { port, host: new URL(p.urlWithToken ?? p.url).host };
|
|
483
508
|
},
|
|
484
|
-
getAll: async () =>
|
|
509
|
+
getAll: async () => {
|
|
510
|
+
this.assertConnected();
|
|
511
|
+
return (await native.previews.list()).map((p) => ({
|
|
512
|
+
port: p.port,
|
|
513
|
+
host: new URL(p.url).host,
|
|
514
|
+
}));
|
|
515
|
+
},
|
|
485
516
|
waitForPort: async (port, options = {}) => {
|
|
517
|
+
this.assertConnected();
|
|
486
518
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
487
519
|
throw new TypeError("Invalid port");
|
|
488
520
|
await native.exec([
|
|
@@ -586,6 +618,8 @@ export class Tasks {
|
|
|
586
618
|
this.managed = new ManagedTasks(native);
|
|
587
619
|
}
|
|
588
620
|
getAll() {
|
|
621
|
+
if (this.client.state !== "CONNECTED")
|
|
622
|
+
return Promise.reject(new Error("Sandbox client is disconnected."));
|
|
589
623
|
return (this.cached ??= this.load().catch((error) => {
|
|
590
624
|
this.cached = undefined;
|
|
591
625
|
throw error;
|
package/dist/compat/core.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Runtime, type RuntimeOptions } from "../client.js";
|
|
1
|
+
import { Runtime, type CreateOptions, type RuntimeOptions } from "../client.js";
|
|
2
2
|
import type { Sandbox } from "../sandbox.js";
|
|
3
3
|
import type { CreateSandbox, ExecOptions } from "../types.js";
|
|
4
|
+
import type { RequestOptions } from "../transport.js";
|
|
4
5
|
export type ClientOptions = RuntimeOptions & {
|
|
5
6
|
client?: Runtime;
|
|
6
7
|
};
|
|
@@ -16,14 +17,14 @@ export declare function only(provider: string, value: object, keys: readonly str
|
|
|
16
17
|
export declare function region(provider: string, value?: string | null): string | undefined;
|
|
17
18
|
export declare const quote: (value: string) => string;
|
|
18
19
|
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
|
|
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>;
|
|
21
22
|
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
|
|
24
|
-
export declare function destroy(sandbox: Sandbox): Promise<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>;
|
|
25
26
|
export declare function ready(sandbox: Sandbox): Promise<Sandbox>;
|
|
26
|
-
export declare function lookup(runtime: Runtime, id: string): Promise<Sandbox>;
|
|
27
|
+
export declare function lookup(runtime: Runtime, id: string, options?: RequestOptions): Promise<Sandbox>;
|
|
27
28
|
export declare function all(runtime: Runtime, provider: string): Promise<Sandbox[]>;
|
|
28
29
|
export declare function image(runtime: Runtime, ref: string): Promise<string>;
|
|
29
30
|
export declare function checked(sandbox: Sandbox, argv: string[], options?: ExecOptions): Promise<void>;
|
|
@@ -34,6 +35,6 @@ export declare function shellCommand(sandbox: Sandbox, command: string, name?: s
|
|
|
34
35
|
env?: Record<string, string>;
|
|
35
36
|
cwd?: string;
|
|
36
37
|
persist?: boolean;
|
|
37
|
-
}): Promise<string | string[]>;
|
|
38
|
+
}, request?: RequestOptions): Promise<string | string[]>;
|
|
38
39
|
export declare function closeShell(sandbox: Sandbox, name: string): Promise<void>;
|
|
39
40
|
export declare function shellRoot(name: string): Promise<string>;
|
package/dist/compat/core.js
CHANGED
|
@@ -40,9 +40,12 @@ export function region(provider, value) {
|
|
|
40
40
|
export const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
41
41
|
export const pathAt = (path, cwd = "/workspace") => path.startsWith("/") ? path : `${cwd.replace(/\/$/, "")}/${path}`;
|
|
42
42
|
const ENV_PATH = "/workspace/.runtime-compat/environment.json";
|
|
43
|
-
export async function environment(sandbox) {
|
|
43
|
+
export async function environment(sandbox, options = {}) {
|
|
44
|
+
options.signal?.throwIfAborted();
|
|
44
45
|
try {
|
|
45
|
-
|
|
46
|
+
const text = await sandbox.files.readText(ENV_PATH, options);
|
|
47
|
+
options.signal?.throwIfAborted();
|
|
48
|
+
return JSON.parse(text);
|
|
46
49
|
}
|
|
47
50
|
catch (error) {
|
|
48
51
|
if (error instanceof NotFoundError)
|
|
@@ -50,9 +53,14 @@ export async function environment(sandbox) {
|
|
|
50
53
|
throw error;
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
|
-
export async function saveEnvironment(sandbox, env) {
|
|
56
|
+
export async function saveEnvironment(sandbox, env, options = {}) {
|
|
57
|
+
options.signal?.throwIfAborted();
|
|
54
58
|
validateEnvironment(env);
|
|
55
|
-
await sandbox.files.write(ENV_PATH, JSON.stringify(env), {
|
|
59
|
+
await sandbox.files.write(ENV_PATH, JSON.stringify(env), {
|
|
60
|
+
mode: 0o600,
|
|
61
|
+
signal: options.signal,
|
|
62
|
+
timeoutMs: options.timeoutMs,
|
|
63
|
+
});
|
|
56
64
|
}
|
|
57
65
|
export function validateEnvironment(env) {
|
|
58
66
|
for (const key of Object.keys(env))
|
|
@@ -62,23 +70,39 @@ export function validateEnvironment(env) {
|
|
|
62
70
|
if (typeof value !== "string" || value.includes("\0"))
|
|
63
71
|
throw new TypeError("Environment values must be strings without NUL bytes");
|
|
64
72
|
}
|
|
65
|
-
export async function execOptions(sandbox, options = {}) {
|
|
66
|
-
|
|
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 } };
|
|
67
76
|
}
|
|
68
|
-
export async function create(runtime, provider, input, env = undefined, setup) {
|
|
77
|
+
export async function create(runtime, provider, input, env = undefined, setup, options = {}) {
|
|
78
|
+
options.signal?.throwIfAborted();
|
|
69
79
|
if (env !== undefined)
|
|
70
80
|
validateEnvironment(env);
|
|
71
81
|
const sandbox = await runtime.sandboxes.create({
|
|
72
82
|
...input,
|
|
73
83
|
labels: { ...input.labels, "compat.provider": provider },
|
|
74
|
-
});
|
|
84
|
+
}, options);
|
|
75
85
|
try {
|
|
86
|
+
options.signal?.throwIfAborted();
|
|
76
87
|
if (env !== undefined || !input.snapshot)
|
|
77
|
-
await saveEnvironment(sandbox, env ?? {});
|
|
88
|
+
await saveEnvironment(sandbox, env ?? {}, options);
|
|
89
|
+
options.signal?.throwIfAborted();
|
|
78
90
|
await setup?.(sandbox);
|
|
91
|
+
options.signal?.throwIfAborted();
|
|
79
92
|
return sandbox;
|
|
80
93
|
}
|
|
81
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;
|
|
82
106
|
try {
|
|
83
107
|
await destroy(sandbox);
|
|
84
108
|
}
|
|
@@ -88,13 +112,16 @@ export async function create(runtime, provider, input, env = undefined, setup) {
|
|
|
88
112
|
throw error;
|
|
89
113
|
}
|
|
90
114
|
}
|
|
91
|
-
export async function destroy(sandbox) {
|
|
115
|
+
export async function destroy(sandbox, options = {}) {
|
|
92
116
|
// Stop before releasing persistence: a paused sandbox cannot change its disk
|
|
93
117
|
// meter, and must never be woken solely to delete it. The stopped update queues
|
|
94
118
|
// disk destruction; see ARCHITECTURE.md section 10 and migration 0181.
|
|
95
|
-
|
|
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);
|
|
96
123
|
if (sandbox.info.persistent)
|
|
97
|
-
await sandbox.update({ persistent: false });
|
|
124
|
+
await sandbox.update({ persistent: false }, options);
|
|
98
125
|
}
|
|
99
126
|
export async function ready(sandbox) {
|
|
100
127
|
if (sandbox.state === "paused")
|
|
@@ -105,15 +132,18 @@ export async function ready(sandbox) {
|
|
|
105
132
|
await sandbox.waitFor("running");
|
|
106
133
|
return sandbox;
|
|
107
134
|
}
|
|
108
|
-
export async function lookup(runtime, id) {
|
|
135
|
+
export async function lookup(runtime, id, options = {}) {
|
|
136
|
+
options.signal?.throwIfAborted();
|
|
109
137
|
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)
|
|
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();
|
|
113
142
|
if (sandbox.info.name === id)
|
|
114
143
|
return sandbox;
|
|
144
|
+
}
|
|
115
145
|
// IDs are not necessarily UUIDs in local or self-hosted servers.
|
|
116
|
-
return runtime.sandboxes.get(id);
|
|
146
|
+
return runtime.sandboxes.get(id, options);
|
|
117
147
|
}
|
|
118
148
|
export async function all(runtime, provider) {
|
|
119
149
|
const result = [];
|
|
@@ -141,14 +171,23 @@ export function missing(path) {
|
|
|
141
171
|
}
|
|
142
172
|
/** Foreground commands share a resident Bash process. Background commands start
|
|
143
173
|
* from the last completed exported environment/cwd snapshot without blocking it. */
|
|
144
|
-
export async function shellCommand(sandbox, command, name, overrides = {}) {
|
|
174
|
+
export async function shellCommand(sandbox, command, name, overrides = {}, request = {}) {
|
|
175
|
+
request.signal?.throwIfAborted();
|
|
145
176
|
if (!name)
|
|
146
177
|
return command;
|
|
147
178
|
const root = await shellRoot(name);
|
|
148
|
-
await sandbox.files.mkdir(root, {
|
|
179
|
+
await sandbox.files.mkdir(root, {
|
|
180
|
+
parents: true,
|
|
181
|
+
signal: request.signal,
|
|
182
|
+
timeoutMs: request.timeoutMs,
|
|
183
|
+
});
|
|
149
184
|
if (overrides.persist !== false) {
|
|
150
185
|
const script = "/workspace/.runtime-compat/shell.py";
|
|
151
|
-
await sandbox.files.write(script, SHELL_BROKER, {
|
|
186
|
+
await sandbox.files.write(script, SHELL_BROKER, {
|
|
187
|
+
mode: 0o600,
|
|
188
|
+
signal: request.signal,
|
|
189
|
+
timeoutMs: request.timeoutMs,
|
|
190
|
+
});
|
|
152
191
|
return [
|
|
153
192
|
"python3",
|
|
154
193
|
script,
|
|
@@ -158,7 +197,7 @@ export async function shellCommand(sandbox, command, name, overrides = {}) {
|
|
|
158
197
|
command,
|
|
159
198
|
env: overrides.env ?? {},
|
|
160
199
|
cwd: overrides.cwd,
|
|
161
|
-
baseEnv: await environment(sandbox),
|
|
200
|
+
baseEnv: await environment(sandbox, request),
|
|
162
201
|
}),
|
|
163
202
|
];
|
|
164
203
|
}
|
package/dist/compat/files.js
CHANGED
|
@@ -25,8 +25,8 @@ export class GuestFiles {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
const staging = `/workspace/.runtime-compat/write-${crypto.randomUUID()}`;
|
|
28
|
-
await sandbox.files.write(staging, bytes, { mode: 0o600, signal: options.signal });
|
|
29
28
|
try {
|
|
29
|
+
await sandbox.files.write(staging, bytes, { mode: 0o600, signal: options.signal });
|
|
30
30
|
await checked(sandbox, [
|
|
31
31
|
"bash",
|
|
32
32
|
"-c",
|
|
@@ -39,7 +39,9 @@ export class GuestFiles {
|
|
|
39
39
|
], { signal: options.signal });
|
|
40
40
|
}
|
|
41
41
|
finally {
|
|
42
|
-
|
|
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);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
writeTextFile(path, content) {
|
|
@@ -58,6 +60,21 @@ export class GuestFiles {
|
|
|
58
60
|
const sandbox = await this.get();
|
|
59
61
|
if (!overwrite && (await sandbox.files.exists(this.path(to))))
|
|
60
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
|
+
}
|
|
61
78
|
await checked(sandbox, [
|
|
62
79
|
"cp",
|
|
63
80
|
...(recursive ? ["-R"] : []),
|
|
@@ -97,8 +114,8 @@ export class GuestFiles {
|
|
|
97
114
|
return;
|
|
98
115
|
}
|
|
99
116
|
const staging = `/workspace/.runtime-compat/append-${crypto.randomUUID()}`;
|
|
100
|
-
await sandbox.files.write(staging, bytes, { mode: 0o600 });
|
|
101
117
|
try {
|
|
118
|
+
await sandbox.files.write(staging, bytes, { mode: 0o600 });
|
|
102
119
|
await checked(sandbox, [
|
|
103
120
|
"bash",
|
|
104
121
|
"-c",
|
|
@@ -109,7 +126,8 @@ export class GuestFiles {
|
|
|
109
126
|
]);
|
|
110
127
|
}
|
|
111
128
|
finally {
|
|
112
|
-
|
|
129
|
+
// A cleanup failure after success must not invite appending twice.
|
|
130
|
+
await sandbox.files.remove(staging).catch(() => undefined);
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
133
|
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/credentials.js
CHANGED
|
@@ -12,6 +12,14 @@ export function connectionOrigins(env) {
|
|
|
12
12
|
auth: apiOrigin(env.RUNTIME_AUTH_URL ?? "https://withruntime.com"),
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
/** Browser-issued production credentials must only reach the production API.
|
|
16
|
+
* CLI and MCP share this boundary, including resumed device requests. */
|
|
17
|
+
export function checkBrowserOrigins(origins) {
|
|
18
|
+
const local = (origin) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname);
|
|
19
|
+
if (!((origins.auth === "https://withruntime.com" && origins.api === "https://api.withruntime.com") ||
|
|
20
|
+
(local(origins.auth) && local(origins.api))))
|
|
21
|
+
throw new Error("Browser login supports Runtime's production endpoints or local test endpoints. Check RUNTIME_AUTH_URL and RUNTIME_API_URL.");
|
|
22
|
+
}
|
|
15
23
|
function defaultDirectory(env) {
|
|
16
24
|
const root = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
17
25
|
if (!isAbsolute(root))
|
|
@@ -93,8 +101,75 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
93
101
|
throw error;
|
|
94
102
|
});
|
|
95
103
|
}
|
|
104
|
+
const lockFile = join(directory, `${name}.lock`);
|
|
105
|
+
/** ARCHITECTURE.md section 3.9: concurrent CLI processes keep each account's
|
|
106
|
+
* connection. Serialize the entire read/modify/write, not only each rename.
|
|
107
|
+
* Never steal a lock: stale cleanup can otherwise delete a new live lock. */
|
|
108
|
+
async function updateAccounts(update) {
|
|
109
|
+
await privateDirectory(true);
|
|
110
|
+
const until = performance.now() + 10_000;
|
|
111
|
+
let handle;
|
|
112
|
+
for (;;) {
|
|
113
|
+
try {
|
|
114
|
+
handle = await open(lockFile, "wx", 0o600);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code !== "EEXIST")
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
const text = await readPrivate(lockFile, 1024, "credential update lock");
|
|
122
|
+
let pid;
|
|
123
|
+
try {
|
|
124
|
+
pid = JSON.parse(text ?? "null")?.pid;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// The holder may not have finished writing its process id yet.
|
|
128
|
+
}
|
|
129
|
+
if (typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code === "ESRCH")
|
|
135
|
+
throw new Error(`A previous Runtime credential update was interrupted. Remove ${lockFile} and retry.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (performance.now() >= until)
|
|
139
|
+
throw new Error(`Another Runtime command is updating saved accounts. Retry when it finishes. If no command is running, remove ${lockFile} and retry.`);
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid }));
|
|
144
|
+
return await update();
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
try {
|
|
148
|
+
await handle.close();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await unlink(lockFile);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
96
155
|
const pendingFile = join(directory, `${name}.pending.json`);
|
|
97
156
|
const accountsFile = join(directory, `${name}.accounts.json`);
|
|
157
|
+
/** Active and archived connections must satisfy the same contract: switching
|
|
158
|
+
* accounts cannot publish a credential the next command refuses to read. */
|
|
159
|
+
function validConnection(value) {
|
|
160
|
+
if (value === null || typeof value !== "object")
|
|
161
|
+
return false;
|
|
162
|
+
const saved = value;
|
|
163
|
+
return (saved.version === 1 &&
|
|
164
|
+
saved.apiOrigin === origins.api &&
|
|
165
|
+
saved.authOrigin === origins.auth &&
|
|
166
|
+
typeof saved.key === "string" &&
|
|
167
|
+
/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) &&
|
|
168
|
+
typeof saved.connectionId === "string" &&
|
|
169
|
+
typeof saved.orgId === "string" &&
|
|
170
|
+
typeof saved.agentName === "string" &&
|
|
171
|
+
(saved.orgName === undefined || typeof saved.orgName === "string"));
|
|
172
|
+
}
|
|
98
173
|
return {
|
|
99
174
|
async read() {
|
|
100
175
|
const text = await readPrivate(file, 4096, "saved connection");
|
|
@@ -107,15 +182,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
107
182
|
catch {
|
|
108
183
|
throw new Error("Runtime's saved connection is invalid. Connect again.");
|
|
109
184
|
}
|
|
110
|
-
if (saved
|
|
111
|
-
saved.apiOrigin !== origins.api ||
|
|
112
|
-
saved.authOrigin !== origins.auth ||
|
|
113
|
-
typeof saved.key !== "string" ||
|
|
114
|
-
!/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) ||
|
|
115
|
-
typeof saved.connectionId !== "string" ||
|
|
116
|
-
typeof saved.orgId !== "string" ||
|
|
117
|
-
typeof saved.agentName !== "string" ||
|
|
118
|
-
(saved.orgName !== undefined && typeof saved.orgName !== "string"))
|
|
185
|
+
if (!validConnection(saved))
|
|
119
186
|
throw new Error("Runtime's saved connection is invalid. Connect again.");
|
|
120
187
|
return saved;
|
|
121
188
|
},
|
|
@@ -125,12 +192,14 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
125
192
|
async save(connection) {
|
|
126
193
|
if (connection.apiOrigin !== origins.api || connection.authOrigin !== origins.auth)
|
|
127
194
|
throw new Error("Connection origins do not match.");
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
195
|
+
return updateAccounts(async () => {
|
|
196
|
+
const previous = await this.read().catch(() => null);
|
|
197
|
+
if (previous && previous.orgId !== connection.orgId) {
|
|
198
|
+
const others = (await this.others()).filter((one) => one.orgId !== previous.orgId && one.orgId !== connection.orgId);
|
|
199
|
+
await writePrivate(accountsFile, JSON.stringify([previous, ...others].slice(0, SAVED_ACCOUNTS)));
|
|
200
|
+
}
|
|
201
|
+
await writePrivate(file, JSON.stringify(connection));
|
|
202
|
+
});
|
|
134
203
|
},
|
|
135
204
|
/** The other accounts this machine has connected, newest first. */
|
|
136
205
|
async others() {
|
|
@@ -145,15 +214,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
145
214
|
return [];
|
|
146
215
|
try {
|
|
147
216
|
const list = JSON.parse(text);
|
|
148
|
-
return Array.isArray(list)
|
|
149
|
-
? list.filter((one) => one &&
|
|
150
|
-
one.version === 1 &&
|
|
151
|
-
one.apiOrigin === origins.api &&
|
|
152
|
-
one.authOrigin === origins.auth &&
|
|
153
|
-
typeof one.key === "string" &&
|
|
154
|
-
/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(one.key) &&
|
|
155
|
-
typeof one.orgId === "string")
|
|
156
|
-
: [];
|
|
217
|
+
return Array.isArray(list) ? list.filter(validConnection) : [];
|
|
157
218
|
}
|
|
158
219
|
catch {
|
|
159
220
|
return [];
|
|
@@ -162,21 +223,29 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
162
223
|
/** Use another saved account's connection, by account id or name. The one
|
|
163
224
|
* in use moves to the saved accounts. Null when none matches. */
|
|
164
225
|
async use(which) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
others.find((one) => one.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
return updateAccounts(async () => {
|
|
227
|
+
const others = await this.others();
|
|
228
|
+
const wanted = which.trim().toLowerCase();
|
|
229
|
+
const chosen = others.find((one) => one.orgId === which) ??
|
|
230
|
+
others.find((one) => one.orgName?.toLowerCase() === wanted) ??
|
|
231
|
+
null;
|
|
232
|
+
if (!chosen)
|
|
233
|
+
return null;
|
|
234
|
+
const current = await this.read().catch(() => null);
|
|
235
|
+
const rest = others.filter((one) => one.orgId !== chosen.orgId);
|
|
236
|
+
await writePrivate(accountsFile, JSON.stringify((current ? [current, ...rest] : rest).slice(0, SAVED_ACCOUNTS)));
|
|
237
|
+
await writePrivate(file, JSON.stringify(chosen));
|
|
238
|
+
return chosen;
|
|
239
|
+
});
|
|
177
240
|
},
|
|
178
|
-
|
|
179
|
-
|
|
241
|
+
/** Forget the expected credential after a remote revoke or refusal. A
|
|
242
|
+
* concurrent login may have saved another connection during that request. */
|
|
243
|
+
async remove(expectedKey) {
|
|
244
|
+
await updateAccounts(async () => {
|
|
245
|
+
if (expectedKey !== undefined && (await this.read())?.key !== expectedKey)
|
|
246
|
+
return;
|
|
247
|
+
await removePrivate(file);
|
|
248
|
+
});
|
|
180
249
|
},
|
|
181
250
|
/** A browser approval that was started and not yet answered, so a command
|
|
182
251
|
* that was stopped while it waited picks up the same link and code rather
|
package/dist/daytona/daytona.js
CHANGED
|
@@ -105,6 +105,8 @@ export class SnapshotService {
|
|
|
105
105
|
/** Builds a Runtime image named `name` from a registry image or a Daytona
|
|
106
106
|
* Image, streaming the build log to `onLogs`. */
|
|
107
107
|
async create(params, options = {}) {
|
|
108
|
+
if (params.regionId)
|
|
109
|
+
throw new NotSupportedError("Choosing a snapshot region (regionId)", "Omit regionId: Runtime places image builds on its available hosts.");
|
|
108
110
|
if (params.resources)
|
|
109
111
|
throw new NotSupportedError("Resources on a snapshot", "Pass resources when creating each sandbox from it: daytona.create({ snapshot, resources }).");
|
|
110
112
|
if (params.entrypoint)
|
|
@@ -183,8 +185,6 @@ export class Daytona {
|
|
|
183
185
|
const client = params.withruntime?.client ?? this.#client;
|
|
184
186
|
const lifecycle = lifecycleOf(params);
|
|
185
187
|
const resources = { ...DEFAULT_RESOURCES, ...params.resources };
|
|
186
|
-
if (resources.gpu)
|
|
187
|
-
throw new NotSupportedError("GPUs", "Runtime runs sandboxes on CPUs; remove gpu from resources.");
|
|
188
188
|
const source = await this.#source(client, params, options.onSnapshotCreateLogs);
|
|
189
189
|
const volumes = await this.#volumes(client, params.volumes);
|
|
190
190
|
const network = networkRules(params);
|
|
@@ -267,8 +267,6 @@ export class Daytona {
|
|
|
267
267
|
async #volumes(client, mounts) {
|
|
268
268
|
const out = [];
|
|
269
269
|
for (const mount of mounts ?? []) {
|
|
270
|
-
if (mount.subpath)
|
|
271
|
-
throw new NotSupportedError("Mounting part of a volume (subpath)", "Mount the whole volume.");
|
|
272
270
|
let id = mount.volumeId;
|
|
273
271
|
if (!UUID.test(id)) {
|
|
274
272
|
const found = (await guard("other", () => client.volumes.list({ name: id, limit: 1 })))
|
|
@@ -363,6 +361,15 @@ export class Daytona {
|
|
|
363
361
|
}
|
|
364
362
|
}
|
|
365
363
|
function refuseCreate(params) {
|
|
364
|
+
const gpuType = params.resources?.gpuType;
|
|
365
|
+
if (params.resources?.gpu ||
|
|
366
|
+
(gpuType !== undefined &&
|
|
367
|
+
gpuType !== null &&
|
|
368
|
+
gpuType !== "" &&
|
|
369
|
+
(!Array.isArray(gpuType) || gpuType.length > 0)))
|
|
370
|
+
throw new NotSupportedError("GPUs", "Runtime runs sandboxes on CPUs; remove gpu and gpuType from resources.");
|
|
371
|
+
if (params.volumes?.some((mount) => mount.subpath))
|
|
372
|
+
throw new NotSupportedError("Mounting part of a volume (subpath)", "Mount the whole volume.");
|
|
366
373
|
const refusals = [
|
|
367
374
|
["spot", "Spot GPU sandboxes (spot)", "Remove it: Runtime runs sandboxes on CPUs."],
|
|
368
375
|
[
|
|
@@ -49,6 +49,14 @@ export interface FileDownloadResponse {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
export type UploadSource = Buffer | Uint8Array | string | Readable | ReadableStream<Uint8Array>;
|
|
52
|
+
export type UploadProgress = {
|
|
53
|
+
bytesSent: number;
|
|
54
|
+
};
|
|
55
|
+
export type UploadStreamOptions = {
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
timeout?: number;
|
|
58
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
59
|
+
};
|
|
52
60
|
/** `sandbox.fs`: Daytona's file calls over Runtime's files API. Relative
|
|
53
61
|
* paths resolve from the working directory, /workspace on Runtime. */
|
|
54
62
|
export declare class FileSystem {
|
|
@@ -62,9 +70,12 @@ export declare class FileSystem {
|
|
|
62
70
|
downloadFile(remotePath: string, localPath: string, timeout?: number): Promise<void>;
|
|
63
71
|
/** The file as it arrives, without holding it in memory. It errors with
|
|
64
72
|
* `download_incomplete` if it ends short of the file's length. */
|
|
65
|
-
downloadFileStream(remotePath: string
|
|
73
|
+
downloadFileStream(remotePath: string, timeoutOrOptions?: number | {
|
|
74
|
+
timeout?: number;
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
}): Promise<Readable>;
|
|
66
77
|
/** Several files; a file that fails carries `error` instead of `result`. */
|
|
67
|
-
downloadFiles(files: FileDownloadRequest[]): Promise<FileDownloadResponse[]>;
|
|
78
|
+
downloadFiles(files: FileDownloadRequest[], timeout?: number): Promise<FileDownloadResponse[]>;
|
|
68
79
|
/** Lines matching `pattern` (a grep pattern) in files under `path`. */
|
|
69
80
|
findFiles(path: string, pattern: string): Promise<Match[]>;
|
|
70
81
|
getFileDetails(path: string): Promise<FileInfo>;
|
|
@@ -81,6 +92,8 @@ export declare class FileSystem {
|
|
|
81
92
|
* directories and replacing one that exists. */
|
|
82
93
|
uploadFile(file: Buffer, remotePath: string, timeout?: number): Promise<void>;
|
|
83
94
|
uploadFile(localPath: string, remotePath: string, timeout?: number): Promise<void>;
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
/** Uploads with bounded memory. Unknown-length streams stage in a private
|
|
96
|
+
* temporary file before the native atomic upload; local paths avoid staging. */
|
|
97
|
+
uploadFileStream(source: UploadSource, remotePath: string, options?: UploadStreamOptions): Promise<void>;
|
|
98
|
+
uploadFiles(files: FileUpload[], timeout?: number): Promise<void>;
|
|
86
99
|
}
|