@sandboxrouter/sdk 0.2.0 → 0.3.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/README.md +224 -48
- package/dist/bench.js +4 -4
- package/dist/catalog.d.ts +159 -0
- package/dist/catalog.js +448 -15
- package/dist/check.d.ts +28 -0
- package/dist/check.js +185 -0
- package/dist/cli.js +148 -11
- package/dist/computers.d.ts +55 -13
- package/dist/computers.js +132 -6
- package/dist/conformance.d.ts +46 -0
- package/dist/conformance.js +65 -0
- package/dist/cost.d.ts +67 -0
- package/dist/cost.js +153 -0
- package/dist/gc.d.ts +29 -0
- package/dist/gc.js +54 -0
- package/dist/index.d.ts +28 -2
- package/dist/index.js +28 -2
- package/dist/leaderboard.d.ts +6 -0
- package/dist/leaderboard.js +5 -4
- package/dist/ledger.d.ts +38 -0
- package/dist/ledger.js +89 -0
- package/dist/providers/agent37.d.ts +40 -0
- package/dist/providers/agent37.js +224 -0
- package/dist/providers/agentcore.d.ts +71 -0
- package/dist/providers/agentcore.js +446 -0
- package/dist/providers/alibaba.d.ts +18 -0
- package/dist/providers/alibaba.js +106 -0
- package/dist/providers/azure.d.ts +43 -0
- package/dist/providers/azure.js +330 -0
- package/dist/providers/beam.d.ts +11 -0
- package/dist/providers/beam.js +179 -0
- package/dist/providers/blaxel.d.ts +3 -1
- package/dist/providers/blaxel.js +32 -22
- package/dist/providers/boat.js +8 -3
- package/dist/providers/cloudflare.js +8 -3
- package/dist/providers/codesandbox.d.ts +59 -0
- package/dist/providers/codesandbox.js +287 -0
- package/dist/providers/daytona.d.ts +25 -4
- package/dist/providers/daytona.js +449 -28
- package/dist/providers/dedalus.d.ts +12 -0
- package/dist/providers/dedalus.js +244 -0
- package/dist/providers/deno.d.ts +50 -0
- package/dist/providers/deno.js +170 -0
- package/dist/providers/docker.js +11 -5
- package/dist/providers/e2b.d.ts +70 -3
- package/dist/providers/e2b.js +376 -8
- package/dist/providers/exedev.js +17 -5
- package/dist/providers/freestyle.d.ts +49 -0
- package/dist/providers/freestyle.js +261 -0
- package/dist/providers/hopx.js +27 -18
- package/dist/providers/instabox.d.ts +76 -0
- package/dist/providers/instabox.js +360 -0
- package/dist/providers/koyeb.d.ts +71 -0
- package/dist/providers/koyeb.js +379 -0
- package/dist/providers/lambda.d.ts +51 -0
- package/dist/providers/lambda.js +467 -0
- package/dist/providers/langsmith.d.ts +16 -0
- package/dist/providers/langsmith.js +149 -0
- package/dist/providers/limrun.d.ts +77 -0
- package/dist/providers/limrun.js +626 -0
- package/dist/providers/maritime.d.ts +5 -1
- package/dist/providers/maritime.js +39 -8
- package/dist/providers/mobilerun.d.ts +44 -0
- package/dist/providers/mobilerun.js +417 -0
- package/dist/providers/modal.js +12 -4
- package/dist/providers/morph.js +12 -4
- package/dist/providers/northflank.js +11 -13
- package/dist/providers/novita.d.ts +12 -0
- package/dist/providers/novita.js +81 -0
- package/dist/providers/openscale.d.ts +17 -0
- package/dist/providers/openscale.js +87 -0
- package/dist/providers/orgo.d.ts +45 -0
- package/dist/providers/orgo.js +414 -0
- package/dist/providers/sail.d.ts +32 -0
- package/dist/providers/sail.js +372 -0
- package/dist/providers/superserve.d.ts +25 -0
- package/dist/providers/superserve.js +337 -0
- package/dist/providers/tensorlake.js +17 -14
- package/dist/providers/upstash.d.ts +30 -0
- package/dist/providers/upstash.js +258 -0
- package/dist/providers/vercel.js +14 -3
- package/dist/providers/workspaces.d.ts +60 -0
- package/dist/providers/workspaces.js +662 -0
- package/dist/router.d.ts +36 -5
- package/dist/router.js +149 -19
- package/dist/server.d.ts +1 -1
- package/dist/server.js +22 -3
- package/dist/types.d.ts +13 -5
- package/dist/util.d.ts +28 -1
- package/dist/util.js +56 -7
- package/package.json +80 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLOUDFLARE_INSTANCES, DEFAULT_MEMORY_GB, DEFAULT_VCPU, cloudflareInstance } from "../catalog.js";
|
|
2
2
|
import { ProviderError } from "../types.js";
|
|
3
|
-
import { toBytes } from "../util.js";
|
|
3
|
+
import { DEFAULT_EXEC_TIMEOUT_S, killedByTimeout, timeoutError, toBytes } from "../util.js";
|
|
4
4
|
import { readSSE } from "../sse.js";
|
|
5
5
|
/**
|
|
6
6
|
* Cloudflare Sandboxes through a bridge Worker you deploy to your own account
|
|
@@ -99,7 +99,8 @@ class CloudflareSandbox {
|
|
|
99
99
|
}
|
|
100
100
|
async exec(cmd, opts = {}) {
|
|
101
101
|
const t0 = Date.now();
|
|
102
|
-
const
|
|
102
|
+
const seconds = opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S;
|
|
103
|
+
const body = { cmd, cwd: opts.cwd, env: opts.env, timeoutSeconds: seconds };
|
|
103
104
|
const streaming = !!(opts.onStdout || opts.onStderr);
|
|
104
105
|
const res = await this.p.request("POST", `${this.base}/exec`, {
|
|
105
106
|
json: body,
|
|
@@ -129,7 +130,11 @@ class CloudflareSandbox {
|
|
|
129
130
|
if (result.stderr)
|
|
130
131
|
opts.onStderr?.(result.stderr);
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
+
const durationMs = Date.now() - t0;
|
|
134
|
+
// The bridge runs the command under coreutils `timeout`: a kill at the deadline is exit 124 (137 after SIGKILL).
|
|
135
|
+
if (killedByTimeout(result.exitCode ?? -1, durationMs, seconds))
|
|
136
|
+
throw timeoutError("cloudflare", seconds);
|
|
137
|
+
return { exitCode: result.exitCode ?? -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "", durationMs };
|
|
133
138
|
}
|
|
134
139
|
async writeFile(path, content) {
|
|
135
140
|
await this.p.request("PUT", `${this.base}/files?path=${encodeURIComponent(path)}`, { bytes: toBytes(content) });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CODESANDBOX_TIERS, codesandboxTier } from "../catalog.js";
|
|
2
|
+
export { CODESANDBOX_TIERS, codesandboxTier };
|
|
3
|
+
import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
|
|
4
|
+
interface CsbCommand {
|
|
5
|
+
waitUntilComplete(): Promise<string>;
|
|
6
|
+
kill(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
interface CsbClient {
|
|
9
|
+
commands: {
|
|
10
|
+
runBackground(command: string, opts?: Record<string, unknown>): Promise<CsbCommand>;
|
|
11
|
+
};
|
|
12
|
+
fs: {
|
|
13
|
+
writeFile(path: string, content: Uint8Array, opts?: {
|
|
14
|
+
create?: boolean;
|
|
15
|
+
overwrite?: boolean;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
18
|
+
mkdir(path: string, recursive?: boolean): Promise<void>;
|
|
19
|
+
remove(path: string, recursive?: boolean): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
disconnect(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
interface CsbSandbox {
|
|
24
|
+
id: string;
|
|
25
|
+
connect(): Promise<CsbClient>;
|
|
26
|
+
}
|
|
27
|
+
interface CsbSdk {
|
|
28
|
+
sandboxes: {
|
|
29
|
+
create(opts: Record<string, unknown>): Promise<CsbSandbox>;
|
|
30
|
+
resume(id: string): Promise<CsbSandbox>;
|
|
31
|
+
hibernate(id: string): Promise<void>;
|
|
32
|
+
shutdown(id: string): Promise<void>;
|
|
33
|
+
delete(id: string): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The slice of `@codesandbox/sdk` this adapter uses. */
|
|
37
|
+
interface CsbModule {
|
|
38
|
+
CodeSandbox: new (apiToken?: string, opts?: {
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
}) => CsbSdk;
|
|
41
|
+
VMTier: {
|
|
42
|
+
fromName(name: string): unknown;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export declare class CodeSandboxProvider implements Provider {
|
|
46
|
+
private apiKey;
|
|
47
|
+
private baseUrl;
|
|
48
|
+
readonly id: "codesandbox";
|
|
49
|
+
private sdk?;
|
|
50
|
+
constructor(apiKey?: string | undefined, baseUrl?: string | undefined);
|
|
51
|
+
configured(): boolean;
|
|
52
|
+
/** @internal */
|
|
53
|
+
module(): Promise<CsbModule>;
|
|
54
|
+
/** @internal */
|
|
55
|
+
client(): Promise<CsbSdk>;
|
|
56
|
+
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
57
|
+
/** Reattach by id; a hibernated sandbox is resumed (shut-down ones boot clean). */
|
|
58
|
+
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { CODESANDBOX_TIERS, DEFAULT_MEMORY_GB, DEFAULT_VCPU, codesandboxTier } from "../catalog.js";
|
|
3
|
+
export { CODESANDBOX_TIERS, codesandboxTier };
|
|
4
|
+
import { ProviderError } from "../types.js";
|
|
5
|
+
import { dirname, optionalImport, shq, toBytes, wrapCommand } from "../util.js";
|
|
6
|
+
/**
|
|
7
|
+
* CodeSandbox SDK (Together AI's "code sandbox", legacy edition) via `@codesandbox/sdk` (checked
|
|
8
|
+
* against 2.4.2). Auth: CSB_API_KEY, a token from https://codesandbox.io/t/api.
|
|
9
|
+
* new CodeSandbox(key).sandboxes.create({ id: template, vmTier, hibernationTimeoutSeconds, privacy })
|
|
10
|
+
* sandboxes.resume(id) -> Sandbox (wakes a hibernated one), sandbox.connect() -> SandboxClient
|
|
11
|
+
* client.commands.runBackground(cmd) -> Command (.waitUntilComplete(), .kill())
|
|
12
|
+
* client.fs.writeFile / readFile / mkdir / remove
|
|
13
|
+
* sandboxes.hibernate(id) memory + disk snapshot; resume continues where it left off
|
|
14
|
+
* sandboxes.shutdown(id), sandboxes.delete(id)
|
|
15
|
+
* Sizes are fixed VM tiers billed in credits per minute (CODESANDBOX_TIERS below); the router asks
|
|
16
|
+
* for the smallest tier that fits. timeoutMinutes becomes hibernationTimeoutSeconds (60 s .. 1 day),
|
|
17
|
+
* which is an inactivity timeout that hibernates, not a hard kill.
|
|
18
|
+
* Commands run in a PTY that merges stdout and stderr, so exec redirects both to files in /tmp and
|
|
19
|
+
* reads them back; onStdout/onStderr are fed by polling those files. spec.env is written to
|
|
20
|
+
* $HOME/.sandboxrouter.env and sourced by every exec, so it survives connect().
|
|
21
|
+
* Successor: Together's `together-sandbox` SDK (https://docs.together.ai/docs/together-code-sandbox)
|
|
22
|
+
* needs sales enablement and publishes no prices, so it isn't used here.
|
|
23
|
+
* Docs: https://docs.together.ai/docs/together-code-sandbox-legacy, https://codesandbox.io/docs/sdk,
|
|
24
|
+
* https://github.com/codesandbox/codesandbox-sdk, https://codesandbox.io/pricing
|
|
25
|
+
*/
|
|
26
|
+
const ID = "codesandbox";
|
|
27
|
+
/**
|
|
28
|
+
* VM tiers and list prices ($0.01486 per credit), from
|
|
29
|
+
* https://docs.together.ai/docs/together-code-sandbox-legacy ("VM credit prices by VM size").
|
|
30
|
+
* Pico: the docs table says 2 cores / 1 GB while the SDK's VMTier says 1 CPU / 2 GiB, so it is only
|
|
31
|
+
* picked for requests that fit both (1 vCPU, 1 GB).
|
|
32
|
+
*/
|
|
33
|
+
/** Hibernation timeout bounds: below 60 s the SDK warns of unstable sessions; 86400 s is the max. */
|
|
34
|
+
const MIN_HIBERNATE_S = 60;
|
|
35
|
+
const MAX_HIBERNATE_S = 86_400;
|
|
36
|
+
const POLL_MS = 250;
|
|
37
|
+
const ENV_FILE = "$HOME/.sandboxrouter.env";
|
|
38
|
+
function fail(err, what) {
|
|
39
|
+
if (err instanceof ProviderError)
|
|
40
|
+
return err;
|
|
41
|
+
const status = typeof err?.status === "number" ? err.status : typeof err?.response?.status === "number" ? err.response.status : undefined;
|
|
42
|
+
return new ProviderError(ID, `${what}: ${err?.message ?? String(err)}`, status);
|
|
43
|
+
}
|
|
44
|
+
export class CodeSandboxProvider {
|
|
45
|
+
apiKey;
|
|
46
|
+
baseUrl;
|
|
47
|
+
id = ID;
|
|
48
|
+
sdk;
|
|
49
|
+
constructor(apiKey = process.env.CSB_API_KEY, baseUrl = process.env.CSB_BASE_URL) {
|
|
50
|
+
this.apiKey = apiKey;
|
|
51
|
+
this.baseUrl = baseUrl;
|
|
52
|
+
}
|
|
53
|
+
configured() {
|
|
54
|
+
return !!this.apiKey;
|
|
55
|
+
}
|
|
56
|
+
/** @internal */
|
|
57
|
+
async module() {
|
|
58
|
+
return optionalImport("@codesandbox/sdk", ID);
|
|
59
|
+
}
|
|
60
|
+
/** @internal */
|
|
61
|
+
async client() {
|
|
62
|
+
if (!this.apiKey)
|
|
63
|
+
throw new ProviderError(ID, "CSB_API_KEY is not set");
|
|
64
|
+
if (!this.sdk) {
|
|
65
|
+
const { CodeSandbox } = await this.module();
|
|
66
|
+
this.sdk = new CodeSandbox(this.apiKey, this.baseUrl ? { baseUrl: this.baseUrl } : undefined);
|
|
67
|
+
}
|
|
68
|
+
return this.sdk;
|
|
69
|
+
}
|
|
70
|
+
async create(spec) {
|
|
71
|
+
const [sdk, { VMTier }] = await Promise.all([this.client(), this.module()]);
|
|
72
|
+
const tier = codesandboxTier(spec.vcpu ?? DEFAULT_VCPU, spec.memoryGb ?? DEFAULT_MEMORY_GB);
|
|
73
|
+
const hibernate = Math.min(MAX_HIBERNATE_S, Math.max(MIN_HIBERNATE_S, Math.round((spec.timeoutMinutes ?? 30) * 60)));
|
|
74
|
+
let sandbox;
|
|
75
|
+
try {
|
|
76
|
+
sandbox = await sdk.sandboxes.create({
|
|
77
|
+
...(spec.image?.codesandbox ? { id: spec.image.codesandbox } : {}),
|
|
78
|
+
vmTier: VMTier.fromName(tier.name),
|
|
79
|
+
hibernationTimeoutSeconds: hibernate,
|
|
80
|
+
// The SDK defaults to "public-hosts" (open preview URLs); keep router sandboxes private.
|
|
81
|
+
privacy: "private",
|
|
82
|
+
tags: ["sdk", "sandboxrouter"],
|
|
83
|
+
...(spec.metadata?.name ? { title: spec.metadata.name } : {}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw fail(err, "create");
|
|
88
|
+
}
|
|
89
|
+
const box = new CodeSandboxBox(this, sandbox.id);
|
|
90
|
+
try {
|
|
91
|
+
await box.attach(sandbox);
|
|
92
|
+
if (spec.env && Object.keys(spec.env).length)
|
|
93
|
+
await box.saveEnv(spec.env);
|
|
94
|
+
return box;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
await box.kill().catch(() => { });
|
|
98
|
+
throw fail(err, "create");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Reattach by id; a hibernated sandbox is resumed (shut-down ones boot clean). */
|
|
102
|
+
async connect(nativeId) {
|
|
103
|
+
const box = new CodeSandboxBox(this, nativeId);
|
|
104
|
+
await box.session();
|
|
105
|
+
return box;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
class CodeSandboxBox {
|
|
109
|
+
p;
|
|
110
|
+
nativeId;
|
|
111
|
+
conn;
|
|
112
|
+
constructor(p, nativeId) {
|
|
113
|
+
this.p = p;
|
|
114
|
+
this.nativeId = nativeId;
|
|
115
|
+
}
|
|
116
|
+
/** @internal */
|
|
117
|
+
async attach(sandbox) {
|
|
118
|
+
this.conn = await sandbox.connect();
|
|
119
|
+
return this.conn;
|
|
120
|
+
}
|
|
121
|
+
/** The live session, resuming the sandbox first if this box has none (fresh connect or after pause). */
|
|
122
|
+
async session() {
|
|
123
|
+
if (this.conn)
|
|
124
|
+
return this.conn;
|
|
125
|
+
try {
|
|
126
|
+
const sdk = await this.p.client();
|
|
127
|
+
return await this.attach(await sdk.sandboxes.resume(this.nativeId));
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
throw fail(err, `resume ${this.nativeId}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** @internal Persist env vars as `export` lines in the sandbox; every exec sources the file. */
|
|
134
|
+
async saveEnv(env) {
|
|
135
|
+
const lines = Object.entries(env).map(([k, v]) => {
|
|
136
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k))
|
|
137
|
+
throw new ProviderError(ID, `invalid env var name: ${k}`);
|
|
138
|
+
return `export ${k}=${shq(v)}\n`;
|
|
139
|
+
});
|
|
140
|
+
const b64 = Buffer.from(lines.join("")).toString("base64");
|
|
141
|
+
const r = await this.exec(`echo ${b64} | base64 -d > "${ENV_FILE}"`, { timeoutSeconds: 30 });
|
|
142
|
+
if (r.exitCode !== 0)
|
|
143
|
+
throw new ProviderError(ID, `could not write env file: ${r.stderr.trim()}`);
|
|
144
|
+
}
|
|
145
|
+
async exec(cmd, opts = {}) {
|
|
146
|
+
const t0 = Date.now();
|
|
147
|
+
const client = await this.session();
|
|
148
|
+
const limit = (opts.timeoutSeconds ?? 60) * 1000;
|
|
149
|
+
const dir = `/tmp/.sbr-${randomBytes(8).toString("hex")}`;
|
|
150
|
+
const user = wrapCommand(cmd, { cwd: opts.cwd, env: opts.env });
|
|
151
|
+
const script = [
|
|
152
|
+
`mkdir -p ${dir} || exit 1`,
|
|
153
|
+
`[ -f "${ENV_FILE}" ] && . "${ENV_FILE}"`,
|
|
154
|
+
`( ${user}\n) > ${dir}/out 2> ${dir}/err < /dev/null`,
|
|
155
|
+
].join("\n");
|
|
156
|
+
// base64 so the script passes the SDK's own `bash -c '...'` quoting untouched.
|
|
157
|
+
const line = `bash -c "$(echo ${Buffer.from(script).toString("base64")} | base64 -d)"`;
|
|
158
|
+
let command;
|
|
159
|
+
try {
|
|
160
|
+
command = await client.commands.runBackground(line);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
throw fail(err, "exec");
|
|
164
|
+
}
|
|
165
|
+
let done = false;
|
|
166
|
+
const finished = command.waitUntilComplete().then(() => 0, (err) => {
|
|
167
|
+
// CommandError carries the exit code for a nonzero exit: that's a result, not a failure.
|
|
168
|
+
if (typeof err?.exitCode === "number")
|
|
169
|
+
return err.exitCode;
|
|
170
|
+
throw err;
|
|
171
|
+
});
|
|
172
|
+
finished.finally(() => (done = true)).catch(() => { });
|
|
173
|
+
const out = new Tail(client, `${dir}/out`, opts.onStdout);
|
|
174
|
+
const err = new Tail(client, `${dir}/err`, opts.onStderr);
|
|
175
|
+
const streaming = !!(opts.onStdout || opts.onStderr);
|
|
176
|
+
const poller = (async () => {
|
|
177
|
+
while (streaming && !done) {
|
|
178
|
+
await Promise.all([out.poll(), err.poll()]);
|
|
179
|
+
if (!done)
|
|
180
|
+
await sleep(POLL_MS);
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
let timer;
|
|
184
|
+
const timeout = new Promise((resolve) => (timer = setTimeout(() => resolve("timeout"), limit)));
|
|
185
|
+
try {
|
|
186
|
+
const r = await Promise.race([finished, timeout]);
|
|
187
|
+
if (r === "timeout") {
|
|
188
|
+
done = true;
|
|
189
|
+
finished.catch(() => { });
|
|
190
|
+
await command.kill().catch(() => { });
|
|
191
|
+
throw new ProviderError(ID, `command timed out after ${limit / 1000}s`);
|
|
192
|
+
}
|
|
193
|
+
await poller;
|
|
194
|
+
const [stdout, stderr] = await Promise.all([out.poll(), err.poll()]).then(() => [out.text(), err.text()]);
|
|
195
|
+
return { exitCode: r, stdout, stderr, durationMs: Date.now() - t0 };
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
throw fail(e, "exec");
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
done = true;
|
|
203
|
+
client.fs.remove(dir, true).catch(() => { });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async writeFile(path, content) {
|
|
207
|
+
const client = await this.session();
|
|
208
|
+
try {
|
|
209
|
+
const parent = dirname(path);
|
|
210
|
+
if (parent !== "/")
|
|
211
|
+
await client.fs.mkdir(parent, true).catch(() => { });
|
|
212
|
+
await client.fs.writeFile(path, toBytes(content), { create: true, overwrite: true });
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
throw fail(err, `write ${path}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async readFile(path) {
|
|
219
|
+
const client = await this.session();
|
|
220
|
+
try {
|
|
221
|
+
return await client.fs.readFile(path);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
throw fail(err, `read ${path}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Hibernate: memory and disk are snapshotted. router.get() (connect) resumes it. */
|
|
228
|
+
async pause() {
|
|
229
|
+
const sdk = await this.p.client();
|
|
230
|
+
await this.conn?.disconnect().catch(() => { });
|
|
231
|
+
this.conn = undefined;
|
|
232
|
+
try {
|
|
233
|
+
await sdk.sandboxes.hibernate(this.nativeId);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
throw fail(err, "hibernate");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Shut the VM down (billing stops), then delete the sandbox so it doesn't pile up in the workspace. */
|
|
240
|
+
async kill() {
|
|
241
|
+
const sdk = await this.p.client();
|
|
242
|
+
await this.conn?.disconnect().catch(() => { });
|
|
243
|
+
this.conn = undefined;
|
|
244
|
+
await sdk.sandboxes.shutdown(this.nativeId).catch(() => { });
|
|
245
|
+
try {
|
|
246
|
+
await sdk.sandboxes.delete(this.nativeId);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
throw fail(err, "delete");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Follows a file in the sandbox: each poll reads it and passes on the bytes added since the last. */
|
|
254
|
+
class Tail {
|
|
255
|
+
client;
|
|
256
|
+
path;
|
|
257
|
+
on;
|
|
258
|
+
bytes = new Uint8Array(0);
|
|
259
|
+
decoder = new TextDecoder();
|
|
260
|
+
constructor(client, path, on) {
|
|
261
|
+
this.client = client;
|
|
262
|
+
this.path = path;
|
|
263
|
+
this.on = on;
|
|
264
|
+
}
|
|
265
|
+
async poll() {
|
|
266
|
+
let data;
|
|
267
|
+
try {
|
|
268
|
+
data = await this.client.fs.readFile(this.path);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return; // not created yet
|
|
272
|
+
}
|
|
273
|
+
if (data.length <= this.bytes.length)
|
|
274
|
+
return;
|
|
275
|
+
const fresh = data.subarray(this.bytes.length);
|
|
276
|
+
this.bytes = data;
|
|
277
|
+
const text = this.decoder.decode(fresh, { stream: true });
|
|
278
|
+
if (text)
|
|
279
|
+
this.on?.(text);
|
|
280
|
+
}
|
|
281
|
+
text() {
|
|
282
|
+
return new TextDecoder().decode(this.bytes);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function sleep(ms) {
|
|
286
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
287
|
+
}
|
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { ComputerOs, ComputerProvider, ComputerSpec, ProviderComputer } from "../computers.js";
|
|
3
|
+
/**
|
|
4
|
+
* Daytona via `@daytonaio/sdk`. Default snapshot; set image.daytona to start from an image.
|
|
5
|
+
*
|
|
6
|
+
* Computers are Linux container sandboxes from a default snapshot (they ship Xvfb, xfce4,
|
|
7
|
+
* x11vnc and noVNC) with the desktop started through `computerUse.start()`. Windows sandboxes
|
|
8
|
+
* exist but need a Tier 3 organization plus a support request, so they aren't routed here.
|
|
9
|
+
* Pass `computerSnapshot` (e.g. "daytona-medium") for a bigger desktop than the 1 vCPU / 1 GiB default.
|
|
10
|
+
* Docs: https://www.daytona.io/docs/en/computer-use
|
|
11
|
+
*/
|
|
12
|
+
export declare class DaytonaProvider implements Provider, ComputerProvider {
|
|
4
13
|
private apiKey;
|
|
14
|
+
private computerSnapshot?;
|
|
5
15
|
readonly id: "daytona";
|
|
16
|
+
readonly computerOs: ComputerOs[];
|
|
6
17
|
private client?;
|
|
7
|
-
constructor(apiKey?: string | undefined);
|
|
18
|
+
constructor(apiKey?: string | undefined, computerSnapshot?: string | undefined);
|
|
8
19
|
configured(): boolean;
|
|
9
20
|
private daytona;
|
|
10
21
|
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
11
|
-
|
|
22
|
+
private discardTagged;
|
|
23
|
+
/**
|
|
24
|
+
* Reattach. Daytona stops, archives or pauses idle sandboxes on its own schedule (and pause()
|
|
25
|
+
* stops them), so whatever state the sandbox is in, it is brought back to started first.
|
|
26
|
+
*/
|
|
12
27
|
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
28
|
+
/** A new Linux desktop. `externalUserId` is stored as a label only: every call creates a new sandbox. */
|
|
29
|
+
createComputer(spec: ComputerSpec): Promise<ProviderComputer>;
|
|
30
|
+
/** Reattach; a stopped, paused or archived computer is started again, desktop included. */
|
|
31
|
+
connectComputer(nativeId: string): Promise<ProviderComputer>;
|
|
13
32
|
}
|
|
33
|
+
/** @internal "ctrl+Return" -> ["ctrl", "enter"]. */
|
|
34
|
+
export declare function daytonaKeys(chord: string): string[];
|