withruntime 0.11.2 → 0.12.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 +2 -2
- package/COMPATIBILITY.md +74 -9
- package/DAYTONA.md +33 -5
- package/E2B.md +85 -24
- package/README.md +9 -8
- package/README.src.md +8 -7
- package/VERCEL.md +43 -5
- package/compatibility-lock.json +58 -49
- package/dist/billing-cli.d.ts +1 -1
- package/dist/billing-cli.js +77 -1
- package/dist/blaxel/errors.d.ts +14 -0
- package/dist/blaxel/errors.js +29 -0
- package/dist/blaxel/index.d.ts +2 -2
- package/dist/blaxel/index.js +4 -4
- package/dist/blaxel/interpreter.d.ts +2 -5
- package/dist/blaxel/sandbox.d.ts +14 -5
- package/dist/blaxel/sandbox.js +63 -7
- package/dist/blaxel/snapshot.d.ts +4 -0
- package/dist/blaxel/snapshot.js +4 -0
- package/dist/cli/logs.d.ts +21 -0
- package/dist/cli/logs.js +188 -0
- package/dist/cli/logs.test.d.ts +1 -0
- package/dist/cli/logs.test.js +272 -0
- package/dist/cli-extras.d.ts +3 -3
- package/dist/cli-extras.js +17 -12
- package/dist/cli-words.d.ts +10 -1
- package/dist/cli-words.js +18 -0
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +405 -204
- package/dist/client.d.ts +2 -4
- package/dist/client.js +27 -11
- package/dist/cloudflare/errors.d.ts +143 -0
- package/dist/cloudflare/errors.js +167 -0
- package/dist/cloudflare/index.d.ts +175 -6
- package/dist/cloudflare/index.js +416 -26
- package/dist/cloudflare/interpreter.d.ts +119 -0
- package/dist/cloudflare/interpreter.js +223 -0
- package/dist/cloudflare/streams.d.ts +38 -0
- package/dist/cloudflare/streams.js +145 -0
- package/dist/cloudflare/watch.d.ts +9 -0
- package/dist/cloudflare/watch.js +24 -0
- package/dist/codesandbox/index.d.ts +216 -28
- package/dist/codesandbox/index.js +478 -78
- package/dist/compat/core.d.ts +18 -0
- package/dist/compat/core.js +29 -0
- package/dist/compat/files.d.ts +1 -0
- package/dist/compat/files.js +25 -5
- package/dist/daytona/computer-use.d.ts +13 -4
- package/dist/daytona/computer-use.js +66 -8
- package/dist/daytona/daytona.d.ts +6 -0
- package/dist/daytona/daytona.js +30 -3
- package/dist/daytona/errors.d.ts +11 -0
- package/dist/daytona/errors.js +18 -0
- package/dist/daytona/filesystem.js +2 -1
- package/dist/daytona/image.d.ts +13 -9
- package/dist/daytona/image.js +18 -12
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/sandbox.d.ts +8 -2
- package/dist/daytona/sandbox.js +16 -2
- package/dist/domains/index.d.ts +9 -0
- package/dist/domains/index.js +10 -0
- package/dist/e2b/commands.d.ts +1 -1
- package/dist/e2b/commands.js +6 -7
- package/dist/e2b/errors.d.ts +17 -0
- package/dist/e2b/errors.js +40 -1
- package/dist/e2b/index.d.ts +2 -2
- package/dist/e2b/index.js +1 -1
- package/dist/e2b/sandbox.d.ts +27 -21
- package/dist/e2b/sandbox.js +34 -8
- package/dist/e2b/unsupported.js +1 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +16 -4
- package/dist/freestyle/index.d.ts +285 -37
- package/dist/freestyle/index.js +656 -144
- package/dist/identity.d.ts +3 -0
- package/dist/identity.js +27 -0
- package/dist/images/index.d.ts +9 -0
- package/dist/images/index.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/ips/index.d.ts +9 -0
- package/dist/ips/index.js +10 -0
- package/dist/jobs/index.d.ts +9 -0
- package/dist/jobs/index.js +10 -0
- package/dist/keys.js +2 -0
- package/dist/login.js +2 -0
- package/dist/machine.d.ts +12 -3
- package/dist/machine.js +20 -3
- package/dist/mcp.js +1 -1
- package/dist/modal/index.d.ts +339 -61
- package/dist/modal/index.js +781 -121
- package/dist/network-cli.d.ts +3 -1
- package/dist/network-cli.js +78 -36
- package/dist/networks/index.d.ts +11 -0
- package/dist/networks/index.js +10 -0
- package/dist/ports/index.d.ts +9 -0
- package/dist/ports/index.js +10 -0
- package/dist/product-names.d.ts +52 -0
- package/dist/product-names.js +164 -0
- package/dist/product-renames.d.ts +103 -0
- package/dist/product-renames.js +189 -0
- package/dist/products/apps.d.ts +176 -0
- package/dist/products/apps.js +165 -0
- package/dist/products/billing.d.ts +41 -0
- package/dist/products/billing.js +13 -0
- package/dist/products/browser.d.ts +10 -0
- package/dist/products/browsers.d.ts +71 -0
- package/dist/products/browsers.js +102 -0
- package/dist/products/builds.d.ts +126 -0
- package/dist/products/builds.js +221 -0
- package/dist/products/index.d.ts +3 -16
- package/dist/products/index.js +3 -14
- package/dist/products/jobs.d.ts +17 -0
- package/dist/products/jobs.js +27 -10
- package/dist/products/logs.d.ts +245 -0
- package/dist/products/logs.js +274 -0
- package/dist/products/logs.test.d.ts +1 -0
- package/dist/products/logs.test.js +343 -0
- package/dist/products/network-products.d.ts +38 -7
- package/dist/products/network-products.js +31 -18
- package/dist/products/observability.d.ts +3 -2
- package/dist/products/private-network.js +1 -1
- package/dist/products/queues.d.ts +149 -0
- package/dist/products/queues.js +213 -0
- package/dist/products/referrals.d.ts +21 -7
- package/dist/products/referrals.js +3 -3
- package/dist/products/secrets.d.ts +7 -3
- package/dist/products/secrets.js +39 -18
- package/dist/products/signed-requests.d.ts +21 -0
- package/dist/products/signed-requests.js +11 -0
- package/dist/products/streams.d.ts +73 -0
- package/dist/products/streams.js +17 -0
- package/dist/products/switching.d.ts +35 -7
- package/dist/products/switching.js +4 -3
- package/dist/products/volumes.d.ts +3 -0
- package/dist/record-cli.d.ts +23 -0
- package/dist/record-cli.js +308 -0
- package/dist/renamed-products.d.ts +38 -0
- package/dist/renamed-products.js +34 -0
- package/dist/runloop/index.d.ts +9 -2
- package/dist/runloop/index.js +29 -8
- package/dist/runloop/sdk.d.ts +131 -0
- package/dist/runloop/sdk.js +167 -0
- package/dist/sandbox.d.ts +3 -2
- package/dist/sandbox.js +14 -9
- package/dist/sandboxes/index.d.ts +11 -0
- package/dist/sandboxes/index.js +12 -0
- package/dist/snapshots/index.d.ts +9 -0
- package/dist/snapshots/index.js +11 -0
- package/dist/snapshots.d.ts +7 -4
- package/dist/sprites/index.d.ts +343 -31
- package/dist/sprites/index.js +866 -84
- package/dist/ssh.d.ts +1 -1
- package/dist/ssh.js +11 -11
- package/dist/terminal.d.ts +24 -0
- package/dist/terminal.js +32 -0
- package/dist/transport.d.ts +15 -1
- package/dist/transport.js +56 -3
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +63 -0
- package/dist/vercel/sandbox.js +5 -3
- package/dist/vercel/snapshot.js +10 -2
- package/dist/volumes/index.d.ts +9 -0
- package/dist/volumes/index.js +10 -0
- package/package.json +48 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { all, client, CompatibilityError, create, destroy, execOptions, only, quote, ready, } from "../compat/core.js";
|
|
1
|
+
import { all, client, CompatibilityError, create, destroy, execOptions, only, quote, ready, refuse, } from "../compat/core.js";
|
|
2
2
|
import { GuestFiles } from "../compat/files.js";
|
|
3
3
|
import { ManagedTasks } from "./guest-service.js";
|
|
4
4
|
export { CompatibilityError };
|
|
@@ -41,10 +41,38 @@ export class VMTier {
|
|
|
41
41
|
}
|
|
42
42
|
export class CodeSandbox {
|
|
43
43
|
sandboxes;
|
|
44
|
+
hosts = new HostTokens();
|
|
44
45
|
constructor(apiToken, options = {}) {
|
|
45
|
-
|
|
46
|
+
const { headers, tracer, ...rest } = options;
|
|
47
|
+
if (headers && Object.keys(headers).length)
|
|
48
|
+
throw new CompatibilityError("CodeSandbox", "custom API headers");
|
|
49
|
+
if (tracer)
|
|
50
|
+
throw new CompatibilityError("CodeSandbox", "an OpenTelemetry tracer; export Runtime's sandbox telemetry with runtime.observability");
|
|
51
|
+
this.sandboxes = new Sandboxes(client({ ...rest, apiKey: rest.apiKey ?? apiToken }));
|
|
46
52
|
}
|
|
47
53
|
}
|
|
54
|
+
/** CodeSandbox's host tokens open private previews. Runtime's private
|
|
55
|
+
* previews carry their own signed token: `sandbox.previews.create(port)`. */
|
|
56
|
+
export class HostTokens {
|
|
57
|
+
}
|
|
58
|
+
const PREVIEW_TOKENS = "use Runtime's private previews, sandbox.previews.create(port), which carry a signed token";
|
|
59
|
+
refuse("CodeSandbox", "HostTokens", HostTokens, {
|
|
60
|
+
createToken: PREVIEW_TOKENS,
|
|
61
|
+
listTokens: PREVIEW_TOKENS,
|
|
62
|
+
revokeToken: PREVIEW_TOKENS,
|
|
63
|
+
revokeAllTokens: PREVIEW_TOKENS,
|
|
64
|
+
updateToken: PREVIEW_TOKENS,
|
|
65
|
+
});
|
|
66
|
+
refuse("CodeSandbox", "HostTokens", HostTokens, { getUrl: PREVIEW_TOKENS, getHeaders: PREVIEW_TOKENS, getCookies: PREVIEW_TOKENS }, { sync: true });
|
|
67
|
+
/** Passing it to create() makes a fresh sandbox from Runtime's default image. */
|
|
68
|
+
const DEFAULT_TEMPLATE = "runtime-default";
|
|
69
|
+
function wakeup(config) {
|
|
70
|
+
if (!config)
|
|
71
|
+
return undefined;
|
|
72
|
+
if (config.http !== config.websocket)
|
|
73
|
+
throw new CompatibilityError("CodeSandbox", "waking on HTTP but not WebSocket requests, or the reverse; Runtime wakes on both");
|
|
74
|
+
return config.http;
|
|
75
|
+
}
|
|
48
76
|
const metadata = (s) => ({
|
|
49
77
|
id: s.id,
|
|
50
78
|
createdAt: new Date(s.info.createdAt),
|
|
@@ -67,6 +95,9 @@ export class Sandboxes {
|
|
|
67
95
|
constructor(runtime) {
|
|
68
96
|
this.runtime = runtime;
|
|
69
97
|
}
|
|
98
|
+
get defaultTemplateId() {
|
|
99
|
+
return DEFAULT_TEMPLATE;
|
|
100
|
+
}
|
|
70
101
|
async create(options = {}) {
|
|
71
102
|
only("CodeSandbox create", options, [
|
|
72
103
|
"id",
|
|
@@ -76,22 +107,33 @@ export class Sandboxes {
|
|
|
76
107
|
"privacy",
|
|
77
108
|
"vmTier",
|
|
78
109
|
"hibernationTimeoutSeconds",
|
|
110
|
+
"automaticWakeupConfig",
|
|
111
|
+
"ipcountry",
|
|
79
112
|
]);
|
|
113
|
+
if (options.ipcountry)
|
|
114
|
+
throw new CompatibilityError("CodeSandbox", "ipcountry; Runtime's sandboxes run in us-east");
|
|
80
115
|
validateHibernationTimeout(options.hibernationTimeoutSeconds);
|
|
116
|
+
const autoWake = wakeup(options.automaticWakeupConfig);
|
|
81
117
|
const labels = {
|
|
82
118
|
"compat.provider": "codesandbox",
|
|
83
119
|
"cs.tags": JSON.stringify([...new Set([...(options.tags ?? []), "sdk"])]),
|
|
84
120
|
"cs.privacy": options.privacy ?? "public-hosts",
|
|
85
121
|
"cs.description": options.description ?? "",
|
|
86
122
|
};
|
|
87
|
-
if (options.id) {
|
|
88
|
-
if (options.vmTier)
|
|
89
|
-
throw new CompatibilityError("CodeSandbox", "changing a fork's VM tier");
|
|
123
|
+
if (options.id && options.id !== DEFAULT_TEMPLATE) {
|
|
90
124
|
const source = await this.runtime.sandboxes.get(options.id);
|
|
91
125
|
const fork = await source.fork({ name: options.title, labels });
|
|
92
|
-
if (options.hibernationTimeoutSeconds !== undefined)
|
|
93
|
-
await fork.update({
|
|
94
|
-
|
|
126
|
+
if (options.hibernationTimeoutSeconds !== undefined || autoWake !== undefined)
|
|
127
|
+
await fork.update({
|
|
128
|
+
...(options.hibernationTimeoutSeconds !== undefined
|
|
129
|
+
? { idlePauseSeconds: options.hibernationTimeoutSeconds }
|
|
130
|
+
: {}),
|
|
131
|
+
...(autoWake !== undefined ? { autoWake } : {}),
|
|
132
|
+
});
|
|
133
|
+
const sandbox = new Sandbox(fork, "FORK");
|
|
134
|
+
if (options.vmTier)
|
|
135
|
+
await sandbox.updateTier(options.vmTier);
|
|
136
|
+
return sandbox;
|
|
95
137
|
}
|
|
96
138
|
const tier = options.vmTier ?? VMTier.Pico;
|
|
97
139
|
const sandbox = await create(this.runtime, "codesandbox", {
|
|
@@ -102,6 +144,7 @@ export class Sandboxes {
|
|
|
102
144
|
diskMiB: Math.ceil((tier.diskGB * 1e9) / 1048576),
|
|
103
145
|
idlePauseSeconds: options.hibernationTimeoutSeconds,
|
|
104
146
|
persistent: true,
|
|
147
|
+
...(autoWake !== undefined ? { autoWake } : {}),
|
|
105
148
|
}, {}, async (sandbox) => {
|
|
106
149
|
await sandbox.exec(["sudo", "mkdir", "-p", "/project/sandbox"], { check: true });
|
|
107
150
|
await sandbox.exec("sudo chown $(id -u):$(id -g) /project/sandbox", { check: true });
|
|
@@ -135,6 +178,26 @@ export class Sandboxes {
|
|
|
135
178
|
fork(id, options = {}) {
|
|
136
179
|
return this.create({ ...options, id });
|
|
137
180
|
}
|
|
181
|
+
/** The account's running CodeSandbox-adapter sandboxes. Runtime has no
|
|
182
|
+
* per-account count of running machines, so the limit is null. */
|
|
183
|
+
async listRunning() {
|
|
184
|
+
const running = (await all(this.runtime, "codesandbox")).filter((s) => s.state === "running");
|
|
185
|
+
return {
|
|
186
|
+
concurrentVmCount: running.length,
|
|
187
|
+
concurrentVmLimit: null,
|
|
188
|
+
vms: running.map((s) => ({
|
|
189
|
+
id: s.id,
|
|
190
|
+
creditBasis: undefined,
|
|
191
|
+
lastActiveAt: s.info.lastActiveAt ? new Date(s.info.lastActiveAt) : undefined,
|
|
192
|
+
sessionStartedAt: s.info.readyAt ? new Date(s.info.readyAt) : undefined,
|
|
193
|
+
specs: {
|
|
194
|
+
cpu: s.info.vcpu,
|
|
195
|
+
memory: s.info.memoryMiB / 1024,
|
|
196
|
+
storage: s.info.diskMiB / 1024,
|
|
197
|
+
},
|
|
198
|
+
})),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
138
201
|
async get(id) {
|
|
139
202
|
return metadata(await this.runtime.sandboxes.get(id));
|
|
140
203
|
}
|
|
@@ -196,6 +259,14 @@ export class Sandbox {
|
|
|
196
259
|
get cluster() {
|
|
197
260
|
return this.native.info.region;
|
|
198
261
|
}
|
|
262
|
+
/** Runtime's guest agent is updated with the host: always current. */
|
|
263
|
+
get isUpToDate() {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
/** Resizes to the tier's CPUs and memory; a running sandbox restarts, as CodeSandbox's does. */
|
|
267
|
+
async updateTier(tier) {
|
|
268
|
+
await this.native.resize({ vcpu: tier.cpuCores, memoryMiB: tier.memoryGiB * 1024 });
|
|
269
|
+
}
|
|
199
270
|
async updateHibernationTimeout(timeoutSeconds) {
|
|
200
271
|
await this.native.update({ idlePauseSeconds: timeoutSeconds });
|
|
201
272
|
}
|
|
@@ -208,6 +279,10 @@ export class Sandbox {
|
|
|
208
279
|
return client;
|
|
209
280
|
}
|
|
210
281
|
}
|
|
282
|
+
refuse("CodeSandbox", "Sandbox", Sandbox, {
|
|
283
|
+
createSession: "use sandbox.connect(); a browser reaches a Runtime sandbox with a session token from the native sandbox.sessions.create()",
|
|
284
|
+
createBrowserSession: "make a session token with the native sandbox.sessions.create() and open it with Runtime's Sandbox.fromSession",
|
|
285
|
+
});
|
|
211
286
|
export class CommandError extends Error {
|
|
212
287
|
exitCode;
|
|
213
288
|
output;
|
|
@@ -420,6 +495,34 @@ export class FileSystem {
|
|
|
420
495
|
isSymlink: e.type === "symlink",
|
|
421
496
|
}));
|
|
422
497
|
}
|
|
498
|
+
/** Changes under a path, as CodeSandbox's add, change and remove events. */
|
|
499
|
+
async watch(path, options = {}) {
|
|
500
|
+
only("CodeSandbox watch", options, ["recursive", "excludes"]);
|
|
501
|
+
const events = emitter();
|
|
502
|
+
const handle = await this.native.files.watch(this.files.path(path), (e) => {
|
|
503
|
+
if (e.type === "rename") {
|
|
504
|
+
if (e.oldPath)
|
|
505
|
+
events.fire({ paths: [e.oldPath], type: "remove" });
|
|
506
|
+
events.fire({ paths: [e.path], type: "add" });
|
|
507
|
+
}
|
|
508
|
+
else
|
|
509
|
+
events.fire({
|
|
510
|
+
paths: [e.path],
|
|
511
|
+
type: e.type === "create" ? "add" : e.type === "remove" ? "remove" : "change",
|
|
512
|
+
});
|
|
513
|
+
}, {
|
|
514
|
+
recursive: options.recursive ?? false,
|
|
515
|
+
...(options.excludes?.length ? { exclude: [...options.excludes] } : {}),
|
|
516
|
+
timeoutMs: 0,
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
onEvent: events.event,
|
|
520
|
+
dispose: () => {
|
|
521
|
+
events.clear();
|
|
522
|
+
void handle.stop().catch(() => undefined);
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
423
526
|
async stat(path) {
|
|
424
527
|
const e = await this.files.stat(path);
|
|
425
528
|
const r = await this.native.exec(["stat", "-c", "%X %Y %Z", "--", this.files.path(path)], {
|
|
@@ -436,19 +539,350 @@ export class FileSystem {
|
|
|
436
539
|
};
|
|
437
540
|
}
|
|
438
541
|
}
|
|
542
|
+
refuse("CodeSandbox", "FileSystem", FileSystem, {
|
|
543
|
+
download: "read it with fs.readFile, or with the native sandbox.files.download(); Runtime has no signed download links",
|
|
544
|
+
});
|
|
439
545
|
const commandPath = (id) => `/workspace/.runtime-compat/codesandbox/commands/${encodeURIComponent(id)}.json`;
|
|
546
|
+
/** A shell CodeSandbox lists: a command's name is JSON whose type is "command". */
|
|
547
|
+
export function isCommandShell(shell) {
|
|
548
|
+
try {
|
|
549
|
+
return JSON.parse(shell.name).type === "command";
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
export class SandboxCommands {
|
|
556
|
+
session;
|
|
557
|
+
constructor(session) {
|
|
558
|
+
this.session = session;
|
|
559
|
+
}
|
|
560
|
+
async runBackground(command, options = {}) {
|
|
561
|
+
only("CodeSandbox command", options, ["env", "cwd", "name", "dimensions", "asGlobalSession"]);
|
|
562
|
+
if (options.asGlobalSession)
|
|
563
|
+
throw new CompatibilityError("CodeSandbox", "global editor sessions");
|
|
564
|
+
const { native, env, workspacePath } = this.session;
|
|
565
|
+
this.session.assertConnected();
|
|
566
|
+
const { stdin: _stdin, ...spawnOptions } = await execOptions(native, {
|
|
567
|
+
env: { ...env, ...options.env },
|
|
568
|
+
cwd: options.cwd ?? workspacePath,
|
|
569
|
+
});
|
|
570
|
+
// The published SDK treats arrays as sequential shell commands, not argv.
|
|
571
|
+
const saved = {
|
|
572
|
+
version: 1,
|
|
573
|
+
command: Array.isArray(command) ? command.join(" && ") : command,
|
|
574
|
+
name: options.name,
|
|
575
|
+
options: {
|
|
576
|
+
...spawnOptions,
|
|
577
|
+
env: { ...spawnOptions.env },
|
|
578
|
+
pty: { ...(options.dimensions ?? { cols: 128, rows: 24 }) },
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
return this.session.attach(await this.session.start(saved), saved);
|
|
582
|
+
}
|
|
583
|
+
async run(command, options) {
|
|
584
|
+
return (await this.runBackground(command, options)).waitUntilComplete();
|
|
585
|
+
}
|
|
586
|
+
async getAll() {
|
|
587
|
+
const { native } = this.session;
|
|
588
|
+
this.session.assertConnected();
|
|
589
|
+
const commands = [];
|
|
590
|
+
for (const saved of await savedShells(native))
|
|
591
|
+
if (saved.taskId === undefined && saved.kind !== "terminal")
|
|
592
|
+
commands.push(this.session.attach(await native.processes.get(saved.processId), saved));
|
|
593
|
+
return commands;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/** The running processes this adapter started, with what it saved about each. */
|
|
597
|
+
async function savedShells(native) {
|
|
598
|
+
const shells = [];
|
|
599
|
+
for (const process of await native.processes.list()) {
|
|
600
|
+
if (process.state !== "running" || !(await native.files.exists(commandPath(process.id))))
|
|
601
|
+
continue;
|
|
602
|
+
const saved = JSON.parse(await native.files.readText(commandPath(process.id)));
|
|
603
|
+
if (saved.version !== 1 || typeof saved.command !== "string" || !saved.options)
|
|
604
|
+
throw new Error(`Invalid saved CodeSandbox command: ${process.id}`);
|
|
605
|
+
shells.push({ ...saved, processId: process.id });
|
|
606
|
+
}
|
|
607
|
+
return shells;
|
|
608
|
+
}
|
|
609
|
+
/** The ports listening in the sandbox, from the kernel's socket tables. */
|
|
610
|
+
const LISTENING = String.raw `import sys
|
|
611
|
+
ports=set()
|
|
612
|
+
for name in ("/proc/net/tcp","/proc/net/tcp6"):
|
|
613
|
+
try:
|
|
614
|
+
for line in open(name).readlines()[1:]:
|
|
615
|
+
f=line.split()
|
|
616
|
+
if f[3]=="0A": ports.add(int(f[1].rsplit(":",1)[1],16))
|
|
617
|
+
except FileNotFoundError: pass
|
|
618
|
+
print(" ".join(str(p) for p in sorted(ports)))`;
|
|
619
|
+
export class Ports {
|
|
620
|
+
session;
|
|
621
|
+
opened = emitter();
|
|
622
|
+
closed = emitter();
|
|
623
|
+
watching;
|
|
624
|
+
known;
|
|
625
|
+
polling = false;
|
|
626
|
+
constructor(session) {
|
|
627
|
+
this.session = session;
|
|
628
|
+
}
|
|
629
|
+
/** A port's events start a watch of the sandbox's listening ports, once a second. */
|
|
630
|
+
get onDidPortOpen() {
|
|
631
|
+
return (listener) => this.subscribe(this.opened.event(listener));
|
|
632
|
+
}
|
|
633
|
+
get onDidPortClose() {
|
|
634
|
+
return (listener) => this.subscribe(this.closed.event(listener));
|
|
635
|
+
}
|
|
636
|
+
subscribe(subscription) {
|
|
637
|
+
this.watching ??= setInterval(() => void this.poll(), 1000);
|
|
638
|
+
void this.poll();
|
|
639
|
+
return {
|
|
640
|
+
dispose: () => {
|
|
641
|
+
subscription.dispose();
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/** The ports listening now. */
|
|
646
|
+
async listening() {
|
|
647
|
+
const r = await this.session.native.exec(["python3", "-c", LISTENING], { check: true });
|
|
648
|
+
return r.stdout.trim() ? r.stdout.trim().split(" ").map(Number) : [];
|
|
649
|
+
}
|
|
650
|
+
async poll() {
|
|
651
|
+
if (this.polling)
|
|
652
|
+
return;
|
|
653
|
+
this.polling = true;
|
|
654
|
+
try {
|
|
655
|
+
const now = new Set(await this.listening());
|
|
656
|
+
const before = this.known;
|
|
657
|
+
this.known = now;
|
|
658
|
+
if (!before)
|
|
659
|
+
return;
|
|
660
|
+
for (const port of now)
|
|
661
|
+
if (!before.has(port))
|
|
662
|
+
this.opened.fire(await this.get(port));
|
|
663
|
+
for (const port of before)
|
|
664
|
+
if (!now.has(port))
|
|
665
|
+
this.closed.fire(port);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// A failed look is tried again at the next tick.
|
|
669
|
+
}
|
|
670
|
+
finally {
|
|
671
|
+
this.polling = false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
dispose() {
|
|
675
|
+
clearInterval(this.watching);
|
|
676
|
+
this.watching = undefined;
|
|
677
|
+
this.opened.clear();
|
|
678
|
+
this.closed.clear();
|
|
679
|
+
}
|
|
680
|
+
async get(port) {
|
|
681
|
+
const { native, previewURLs } = this.session;
|
|
682
|
+
this.session.assertConnected();
|
|
683
|
+
if (native.info.labels["cs.privacy"] === "private")
|
|
684
|
+
throw new CompatibilityError("CodeSandbox", "private port host authentication; use Runtime signed preview URLs");
|
|
685
|
+
const p = await native.previews.create(port, { visibility: "public" });
|
|
686
|
+
previewURLs.set(port, p.urlWithToken ?? p.url);
|
|
687
|
+
return { port, host: new URL(p.urlWithToken ?? p.url).host };
|
|
688
|
+
}
|
|
689
|
+
async getAll() {
|
|
690
|
+
this.session.assertConnected();
|
|
691
|
+
return (await this.session.native.previews.list()).map((p) => {
|
|
692
|
+
this.session.previewURLs.set(p.port, p.urlWithToken ?? p.url);
|
|
693
|
+
return { port: p.port, host: new URL(p.url).host };
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
async waitForPort(port, options = {}) {
|
|
697
|
+
only("CodeSandbox waitForPort", options, ["timeoutMs"]);
|
|
698
|
+
this.session.assertConnected();
|
|
699
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
700
|
+
throw new TypeError("Invalid port");
|
|
701
|
+
await this.session.native.exec([
|
|
702
|
+
"python3",
|
|
703
|
+
"-c",
|
|
704
|
+
'import socket,sys,time\nport=int(sys.argv[1]); end=time.monotonic()+float(sys.argv[2])/1000\nwhile True:\n try:\n s=socket.create_connection(("127.0.0.1",port),timeout=0.25); s.close(); break\n except OSError:\n if time.monotonic() >= end: raise TimeoutError("Port did not open")\n time.sleep(0.1)',
|
|
705
|
+
String(port),
|
|
706
|
+
String(options.timeoutMs ?? 30000),
|
|
707
|
+
], { check: true, timeoutMs: (options.timeoutMs ?? 30000) + 1000 });
|
|
708
|
+
return this.get(port);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/** URLs of the ports this client exposed with ports.get or listed with ports.getAll. */
|
|
712
|
+
export class Hosts {
|
|
713
|
+
session;
|
|
714
|
+
constructor(session) {
|
|
715
|
+
this.session = session;
|
|
716
|
+
}
|
|
717
|
+
getUrl(port, protocol = "https") {
|
|
718
|
+
const url = this.session.previewURLs.get(port);
|
|
719
|
+
if (!url)
|
|
720
|
+
throw new Error(`Port ${port} has not been exposed; call ports.get(${port}) first`);
|
|
721
|
+
const at = new URL(url);
|
|
722
|
+
at.protocol = `${protocol}:`;
|
|
723
|
+
return at.toString();
|
|
724
|
+
}
|
|
725
|
+
/** Public previews need no token: nothing to send. */
|
|
726
|
+
getHeaders() {
|
|
727
|
+
return {};
|
|
728
|
+
}
|
|
729
|
+
getCookies() {
|
|
730
|
+
return {};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/** Interactive shells, each a process with a terminal of its own. */
|
|
734
|
+
export class Terminals {
|
|
735
|
+
session;
|
|
736
|
+
constructor(session) {
|
|
737
|
+
this.session = session;
|
|
738
|
+
}
|
|
739
|
+
async create(command = "bash", options = {}) {
|
|
740
|
+
only("CodeSandbox terminal", options, ["env", "cwd", "name", "dimensions", "asGlobalSession"]);
|
|
741
|
+
if (options.asGlobalSession)
|
|
742
|
+
throw new CompatibilityError("CodeSandbox", "global editor sessions");
|
|
743
|
+
if (!["bash", "zsh", "fish", "ksh", "dash"].includes(command))
|
|
744
|
+
throw new TypeError(`Unknown shell: ${command}`);
|
|
745
|
+
const { native, env, workspacePath } = this.session;
|
|
746
|
+
this.session.assertConnected();
|
|
747
|
+
const { stdin: _stdin, ...spawnOptions } = await execOptions(native, {
|
|
748
|
+
env: { ...env, ...options.env },
|
|
749
|
+
cwd: options.cwd ?? workspacePath,
|
|
750
|
+
});
|
|
751
|
+
const saved = {
|
|
752
|
+
version: 1,
|
|
753
|
+
kind: "terminal",
|
|
754
|
+
command,
|
|
755
|
+
name: options.name,
|
|
756
|
+
options: {
|
|
757
|
+
...spawnOptions,
|
|
758
|
+
env: { ...spawnOptions.env },
|
|
759
|
+
stdin: "pipe",
|
|
760
|
+
pty: { ...(options.dimensions ?? { cols: 80, rows: 24 }) },
|
|
761
|
+
},
|
|
762
|
+
};
|
|
763
|
+
const process = await this.session.start(saved);
|
|
764
|
+
return new Terminal(process, options.name ?? command);
|
|
765
|
+
}
|
|
766
|
+
async get(shellId) {
|
|
767
|
+
return (await this.getAll()).find((terminal) => terminal.id === shellId);
|
|
768
|
+
}
|
|
769
|
+
async getAll() {
|
|
770
|
+
const { native } = this.session;
|
|
771
|
+
this.session.assertConnected();
|
|
772
|
+
const terminals = [];
|
|
773
|
+
for (const saved of await savedShells(native))
|
|
774
|
+
if (saved.kind === "terminal")
|
|
775
|
+
terminals.push(new Terminal(await native.processes.get(saved.processId), saved.name ?? saved.command));
|
|
776
|
+
return terminals;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
export class Terminal {
|
|
780
|
+
process;
|
|
781
|
+
name;
|
|
782
|
+
outputEvent = emitter();
|
|
783
|
+
onOutput = this.outputEvent.event;
|
|
784
|
+
output = [];
|
|
785
|
+
following;
|
|
786
|
+
constructor(process, name) {
|
|
787
|
+
this.process = process;
|
|
788
|
+
this.name = name;
|
|
789
|
+
}
|
|
790
|
+
get id() {
|
|
791
|
+
return this.process.id;
|
|
792
|
+
}
|
|
793
|
+
/** Its output so far, and what it prints from now on through onOutput. */
|
|
794
|
+
async open(dimensions = { cols: 80, rows: 24 }) {
|
|
795
|
+
await this.process.resize(dimensions.cols, dimensions.rows);
|
|
796
|
+
if (!this.following) {
|
|
797
|
+
const info = await this.process.refresh();
|
|
798
|
+
const end = info.outputBytes;
|
|
799
|
+
const signal = (this.following = new AbortController()).signal;
|
|
800
|
+
let replayed = "";
|
|
801
|
+
let caughtUp;
|
|
802
|
+
const ready = new Promise((resolve) => (caughtUp = resolve));
|
|
803
|
+
if (end <= info.firstOffset)
|
|
804
|
+
caughtUp();
|
|
805
|
+
void (async () => {
|
|
806
|
+
try {
|
|
807
|
+
for await (const e of this.process.output({ signal }))
|
|
808
|
+
if (e.type === "stdout" || e.type === "stderr") {
|
|
809
|
+
if (e.offset < end)
|
|
810
|
+
replayed += e.data;
|
|
811
|
+
else {
|
|
812
|
+
this.output.push(e.data);
|
|
813
|
+
this.outputEvent.fire(e.data);
|
|
814
|
+
}
|
|
815
|
+
if (e.offset + new TextEncoder().encode(e.data).length >= end)
|
|
816
|
+
caughtUp();
|
|
817
|
+
}
|
|
818
|
+
else if (e.type === "exit")
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
// The terminal ended, or this client let go of it.
|
|
823
|
+
}
|
|
824
|
+
finally {
|
|
825
|
+
caughtUp();
|
|
826
|
+
}
|
|
827
|
+
})();
|
|
828
|
+
await ready;
|
|
829
|
+
this.output.unshift(replayed);
|
|
830
|
+
}
|
|
831
|
+
return this.output.join("");
|
|
832
|
+
}
|
|
833
|
+
async write(input, dimensions) {
|
|
834
|
+
if (dimensions)
|
|
835
|
+
await this.process.resize(dimensions.cols, dimensions.rows);
|
|
836
|
+
await this.process.write(input);
|
|
837
|
+
}
|
|
838
|
+
run(input, dimensions) {
|
|
839
|
+
return this.write(`${input}\n`, dimensions);
|
|
840
|
+
}
|
|
841
|
+
async kill() {
|
|
842
|
+
this.following?.abort();
|
|
843
|
+
await this.process.kill("SIGKILL");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/** One-off JavaScript and Python snippets, run as CodeSandbox runs them: the
|
|
847
|
+
* last line's value is printed. */
|
|
848
|
+
export class Interpreters {
|
|
849
|
+
commands;
|
|
850
|
+
constructor(commands) {
|
|
851
|
+
this.commands = commands;
|
|
852
|
+
}
|
|
853
|
+
javascript(code) {
|
|
854
|
+
const body = code
|
|
855
|
+
.split("\n")
|
|
856
|
+
.map((line, i, lines) => i === lines.length - 1 && !line.startsWith("return") ? `return ${line}` : line)
|
|
857
|
+
.join("\n");
|
|
858
|
+
return this.commands.run(`node -p "$(cat <<'EOF'\n(() => {${body}})()\nEOF\n)"`, {
|
|
859
|
+
env: { NO_COLOR: "true" },
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
python(code) {
|
|
863
|
+
const body = code
|
|
864
|
+
.split("\n")
|
|
865
|
+
.map((line, i, lines) => i === lines.length - 1 && !line.startsWith("print") ? `print(${line})` : line)
|
|
866
|
+
.join("\n");
|
|
867
|
+
return this.commands.run(`python3 <<'PYCODE'\n${body}\nPYCODE\n`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
440
870
|
export class SandboxClient {
|
|
441
871
|
native;
|
|
442
872
|
env;
|
|
443
873
|
fs;
|
|
444
874
|
commands;
|
|
445
875
|
ports;
|
|
876
|
+
terminals;
|
|
877
|
+
interpreters;
|
|
878
|
+
hosts;
|
|
446
879
|
tasks;
|
|
447
880
|
setup;
|
|
448
881
|
workspacePath = "/project/sandbox";
|
|
449
882
|
disconnected = false;
|
|
450
883
|
attached = new Set();
|
|
451
884
|
previewURLs = new Map();
|
|
885
|
+
stateEvent = emitter();
|
|
452
886
|
stopKeepAlive;
|
|
453
887
|
constructor(native, env) {
|
|
454
888
|
this.native = native;
|
|
@@ -456,77 +890,31 @@ export class SandboxClient {
|
|
|
456
890
|
this.fs = new FileSystem(native);
|
|
457
891
|
this.tasks = new Tasks(this, native);
|
|
458
892
|
this.setup = new Setup(this, native);
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
cwd: options.cwd ?? this.workspacePath,
|
|
468
|
-
});
|
|
469
|
-
// The published SDK treats arrays as sequential shell commands, not argv.
|
|
470
|
-
const saved = {
|
|
471
|
-
version: 1,
|
|
472
|
-
command: Array.isArray(command) ? command.join(" && ") : command,
|
|
473
|
-
name: options.name,
|
|
474
|
-
options: {
|
|
475
|
-
...spawnOptions,
|
|
476
|
-
env: { ...spawnOptions.env },
|
|
477
|
-
pty: { ...(options.dimensions ?? { cols: 128, rows: 24 }) },
|
|
478
|
-
},
|
|
479
|
-
};
|
|
480
|
-
return this.attach(await this.start(saved), saved);
|
|
481
|
-
};
|
|
482
|
-
this.commands = {
|
|
483
|
-
runBackground,
|
|
484
|
-
run: async (command, options) => (await runBackground(command, options)).waitUntilComplete(),
|
|
485
|
-
getAll: async () => {
|
|
486
|
-
this.assertConnected();
|
|
487
|
-
const commands = [];
|
|
488
|
-
for (const process of await native.processes.list()) {
|
|
489
|
-
if (process.state !== "running" || !(await native.files.exists(commandPath(process.id))))
|
|
490
|
-
continue;
|
|
491
|
-
const saved = JSON.parse(await native.files.readText(commandPath(process.id)));
|
|
492
|
-
if (saved.version !== 1 || typeof saved.command !== "string" || !saved.options)
|
|
493
|
-
throw new Error(`Invalid saved CodeSandbox command: ${process.id}`);
|
|
494
|
-
if (saved.taskId === undefined)
|
|
495
|
-
commands.push(this.attach(await native.processes.get(process.id), saved));
|
|
496
|
-
}
|
|
497
|
-
return commands;
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
this.ports = {
|
|
501
|
-
get: async (port) => {
|
|
502
|
-
this.assertConnected();
|
|
503
|
-
if (native.info.labels["cs.privacy"] === "private")
|
|
504
|
-
throw new CompatibilityError("CodeSandbox", "private port host authentication; use Runtime signed preview URLs");
|
|
505
|
-
const p = await native.previews.create(port, { visibility: "public" });
|
|
506
|
-
this.previewURLs.set(port, p.urlWithToken ?? p.url);
|
|
507
|
-
return { port, host: new URL(p.urlWithToken ?? p.url).host };
|
|
508
|
-
},
|
|
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
|
-
},
|
|
516
|
-
waitForPort: async (port, options = {}) => {
|
|
517
|
-
this.assertConnected();
|
|
518
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
519
|
-
throw new TypeError("Invalid port");
|
|
520
|
-
await native.exec([
|
|
521
|
-
"python3",
|
|
522
|
-
"-c",
|
|
523
|
-
'import socket,sys,time\nport=int(sys.argv[1]); end=time.monotonic()+float(sys.argv[2])/1000\nwhile True:\n try:\n s=socket.create_connection(("127.0.0.1",port),timeout=0.25); s.close(); break\n except OSError:\n if time.monotonic() >= end: raise TimeoutError("Port did not open")\n time.sleep(0.1)',
|
|
524
|
-
String(port),
|
|
525
|
-
String(options.timeoutMs ?? 30000),
|
|
526
|
-
], { check: true, timeoutMs: (options.timeoutMs ?? 30000) + 1000 });
|
|
527
|
-
return this.ports.get(port);
|
|
528
|
-
},
|
|
893
|
+
const session = {
|
|
894
|
+
native,
|
|
895
|
+
env,
|
|
896
|
+
workspacePath: this.workspacePath,
|
|
897
|
+
assertConnected: () => this.assertConnected(),
|
|
898
|
+
start: (saved) => this.start(saved),
|
|
899
|
+
attach: (process, saved) => this.attach(process, saved),
|
|
900
|
+
previewURLs: this.previewURLs,
|
|
529
901
|
};
|
|
902
|
+
this.commands = new SandboxCommands(session);
|
|
903
|
+
this.ports = new Ports(session);
|
|
904
|
+
this.terminals = new Terminals(session);
|
|
905
|
+
this.interpreters = new Interpreters(this.commands);
|
|
906
|
+
this.hosts = new Hosts(session);
|
|
907
|
+
}
|
|
908
|
+
/** Runtime's guest agent is updated with the host: always current. */
|
|
909
|
+
get isUpToDate() {
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
/** The sandbox's page in Runtime's console. */
|
|
913
|
+
get editorUrl() {
|
|
914
|
+
return `https://withruntime.com/account/sandboxes/${encodeURIComponent(this.native.id)}`;
|
|
915
|
+
}
|
|
916
|
+
get onStateChange() {
|
|
917
|
+
return this.stateEvent.event;
|
|
530
918
|
}
|
|
531
919
|
async initialize() {
|
|
532
920
|
await this.tasks.getAll();
|
|
@@ -575,7 +963,10 @@ export class SandboxClient {
|
|
|
575
963
|
return this.disconnected ? "DISCONNECTED" : "CONNECTED";
|
|
576
964
|
}
|
|
577
965
|
async disconnect() {
|
|
966
|
+
if (!this.disconnected)
|
|
967
|
+
this.stateEvent.fire("DISCONNECTED");
|
|
578
968
|
this.disconnected = true;
|
|
969
|
+
this.ports.dispose();
|
|
579
970
|
this.tasks.dispose();
|
|
580
971
|
this.setup.dispose();
|
|
581
972
|
for (const command of this.attached)
|
|
@@ -585,6 +976,8 @@ export class SandboxClient {
|
|
|
585
976
|
}
|
|
586
977
|
async reconnect() {
|
|
587
978
|
await ready(await this.native.refresh());
|
|
979
|
+
if (this.disconnected)
|
|
980
|
+
this.stateEvent.fire("CONNECTED");
|
|
588
981
|
this.disconnected = false;
|
|
589
982
|
await this.tasks.reconnect();
|
|
590
983
|
await this.setup.reconnect();
|
|
@@ -595,15 +988,22 @@ export class SandboxClient {
|
|
|
595
988
|
this.stopKeepAlive = this.native.keepAlive();
|
|
596
989
|
}
|
|
597
990
|
dispose() {
|
|
991
|
+
if (!this.disconnected)
|
|
992
|
+
this.stateEvent.fire("DISCONNECTED");
|
|
598
993
|
this.disconnected = true;
|
|
994
|
+
this.ports.dispose();
|
|
599
995
|
this.tasks.dispose();
|
|
600
996
|
this.setup.dispose();
|
|
601
997
|
for (const command of this.attached)
|
|
602
998
|
command.detach();
|
|
603
999
|
this.attached.clear();
|
|
604
1000
|
this.stopKeepAlive?.();
|
|
1001
|
+
this.stateEvent.clear();
|
|
605
1002
|
}
|
|
606
1003
|
}
|
|
1004
|
+
refuse("CodeSandbox", "SandboxClient", { prototype: SandboxClient }, {
|
|
1005
|
+
create: "connect with sandbox.connect(); Runtime has no separate session handshake",
|
|
1006
|
+
});
|
|
607
1007
|
/** Configured tasks share durable native processes between client connections. */
|
|
608
1008
|
export class Tasks {
|
|
609
1009
|
client;
|