@lizard-build/sdk 0.1.49 → 0.1.52
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 +25 -6
- package/dist/index.d.mts +392 -123
- package/dist/index.d.ts +392 -123
- package/dist/index.js +660 -267
- package/dist/index.mjs +659 -267
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
LizardError: () => LizardError,
|
|
37
37
|
MetricsAPI: () => MetricsAPI,
|
|
38
38
|
NotFoundError: () => NotFoundError,
|
|
39
|
+
PaymentRequiredError: () => PaymentRequiredError,
|
|
39
40
|
PlatformClient: () => PlatformClient,
|
|
40
41
|
ProjectsAPI: () => ProjectsAPI,
|
|
41
42
|
RegionsAPI: () => RegionsAPI,
|
|
@@ -111,18 +112,44 @@ var TimeoutError = class extends LizardError {
|
|
|
111
112
|
this.name = "TimeoutError";
|
|
112
113
|
}
|
|
113
114
|
};
|
|
115
|
+
var PaymentRequiredError = class extends LizardError {
|
|
116
|
+
constructor(message = "Payment required") {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = "PaymentRequiredError";
|
|
119
|
+
}
|
|
120
|
+
/** The one page to open next for this {@link paymentStatus}. */
|
|
121
|
+
get url() {
|
|
122
|
+
const s = this.paymentStatus;
|
|
123
|
+
if (this.invoiceUrl) return this.invoiceUrl;
|
|
124
|
+
if ((s === "trial_available" || s === "subscription_required") && this.subscribeUrl) return this.subscribeUrl;
|
|
125
|
+
if (s && PREPAID_STATUSES.has(s) && this.topupUrl) return this.topupUrl;
|
|
126
|
+
return this.billingUrl ?? this.subscribeUrl ?? this.topupUrl;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var PREPAID_STATUSES = /* @__PURE__ */ new Set(["grace", "frozen", "card_required", "credits_required"]);
|
|
130
|
+
var ERROR_CODE = /^[A-Z][A-Z0-9_]*$/;
|
|
114
131
|
async function handleApiError(res) {
|
|
115
|
-
let
|
|
116
|
-
let code;
|
|
132
|
+
let body = {};
|
|
117
133
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
if (typeof body.code === "string") code = body.code;
|
|
134
|
+
const parsed = await res.json();
|
|
135
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) body = parsed;
|
|
121
136
|
} catch {
|
|
122
|
-
message = res.statusText;
|
|
123
137
|
}
|
|
138
|
+
const text = (key) => typeof body[key] === "string" && body[key] ? body[key] : void 0;
|
|
139
|
+
const error = text("error");
|
|
140
|
+
const errorIsCode = !!error && ERROR_CODE.test(error);
|
|
141
|
+
const message = (errorIsCode ? text("message") ?? error : error ?? text("message")) ?? res.statusText;
|
|
142
|
+
const code = text("code") ?? (errorIsCode ? error : void 0);
|
|
124
143
|
let err;
|
|
125
|
-
if (res.status ===
|
|
144
|
+
if (res.status === 402) {
|
|
145
|
+
const paid = new PaymentRequiredError(message);
|
|
146
|
+
paid.paymentStatus = text("status");
|
|
147
|
+
paid.subscribeUrl = text("subscribeUrl");
|
|
148
|
+
paid.billingUrl = text("billingUrl");
|
|
149
|
+
paid.topupUrl = text("topupUrl");
|
|
150
|
+
paid.invoiceUrl = text("invoiceUrl");
|
|
151
|
+
err = paid;
|
|
152
|
+
} else if (res.status === 401 || res.status === 403) err = new AuthenticationError(message);
|
|
126
153
|
else if (res.status === 404) err = new NotFoundError(message);
|
|
127
154
|
else if (res.status === 409) err = new ConflictError(message);
|
|
128
155
|
else if (res.status === 408 || res.status === 504) err = new TimeoutError(message);
|
|
@@ -548,7 +575,9 @@ var Process = class {
|
|
|
548
575
|
envs: opts?.envs,
|
|
549
576
|
user: opts?.user,
|
|
550
577
|
workdir: opts?.workdir,
|
|
551
|
-
timeoutMs: opts?.timeoutMs
|
|
578
|
+
timeoutMs: opts?.timeoutMs,
|
|
579
|
+
// The pid event is opt-in: older CLIs print any unknown data line as output.
|
|
580
|
+
pidEvent: opts?.onPid ? true : void 0
|
|
552
581
|
}),
|
|
553
582
|
signal: AbortSignal.timeout(opts?.timeoutMs ?? 6e4)
|
|
554
583
|
});
|
|
@@ -556,11 +585,6 @@ var Process = class {
|
|
|
556
585
|
if (!streaming) return res.json();
|
|
557
586
|
return this.consumeStream(res, opts);
|
|
558
587
|
}
|
|
559
|
-
/**
|
|
560
|
-
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
561
|
-
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
562
|
-
* `{exitCode}` at the end.
|
|
563
|
-
*/
|
|
564
588
|
/**
|
|
565
589
|
* List the processes this sandbox is currently running.
|
|
566
590
|
*
|
|
@@ -584,8 +608,8 @@ var Process = class {
|
|
|
584
608
|
/**
|
|
585
609
|
* Signal a running process. Defaults to `SIGTERM`.
|
|
586
610
|
*
|
|
587
|
-
* The signal goes to the process
|
|
588
|
-
* killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
|
|
611
|
+
* The signal goes to the whole process tree, so a shell's children die with
|
|
612
|
+
* it — killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
|
|
589
613
|
*
|
|
590
614
|
* The pid comes from {@link list}, or from the `pid` event at the start of a
|
|
591
615
|
* streaming `exec`.
|
|
@@ -604,6 +628,11 @@ var Process = class {
|
|
|
604
628
|
});
|
|
605
629
|
if (!res.ok) await handleApiError(res);
|
|
606
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
633
|
+
* still accumulating the full result. Events are `{pid}` first (when asked for
|
|
634
|
+
* with `pidEvent`), `{stream, line}` for output and `{exitCode}` at the end.
|
|
635
|
+
*/
|
|
607
636
|
async consumeStream(res, opts) {
|
|
608
637
|
const reader = res.body?.getReader();
|
|
609
638
|
if (!reader) return { stdout: "", stderr: "", exitCode: 0 };
|
|
@@ -654,33 +683,37 @@ var Fs = class {
|
|
|
654
683
|
/**
|
|
655
684
|
* Write a file into the sandbox filesystem.
|
|
656
685
|
*
|
|
657
|
-
* Creates parent directories automatically if they don't exist.
|
|
658
|
-
*
|
|
686
|
+
* Creates parent directories automatically if they don't exist. A string is
|
|
687
|
+
* written as UTF-8; bytes (`Uint8Array`, `Buffer` or `ArrayBuffer`) are written
|
|
688
|
+
* exactly as given, so binary files round-trip with {@link readBytes}.
|
|
689
|
+
*
|
|
690
|
+
* With `opts.user` the file is written as (and owned by) that user.
|
|
659
691
|
*
|
|
660
692
|
* @example
|
|
661
693
|
* ```ts
|
|
662
694
|
* await sandbox.fs.write('/app/index.js', 'console.log("hello")')
|
|
663
695
|
* ```
|
|
664
696
|
*
|
|
665
|
-
* @example Write
|
|
697
|
+
* @example Write binary data:
|
|
666
698
|
* ```ts
|
|
667
|
-
* await sandbox.fs.write('/app/
|
|
699
|
+
* await sandbox.fs.write('/app/logo.png', await readFile('logo.png'))
|
|
668
700
|
* ```
|
|
669
701
|
*/
|
|
670
702
|
async write(path, data, opts) {
|
|
671
|
-
const
|
|
703
|
+
const body = typeof data === "string" ? { path, content: data, user: opts?.user } : { path, content: bytesToBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data), encoding: "base64", user: opts?.user };
|
|
672
704
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
|
|
673
705
|
method: "POST",
|
|
674
706
|
headers: this.config.headers,
|
|
675
|
-
body: JSON.stringify(
|
|
707
|
+
body: JSON.stringify(body)
|
|
676
708
|
});
|
|
677
709
|
if (!res.ok) await handleApiError(res);
|
|
678
710
|
}
|
|
679
711
|
/**
|
|
680
712
|
* Read a file from the sandbox filesystem.
|
|
681
713
|
*
|
|
682
|
-
* @returns The file contents as a UTF-8 string.
|
|
714
|
+
* @returns The file contents as a UTF-8 string. Use {@link readBytes} for binary files.
|
|
683
715
|
*
|
|
716
|
+
* Throws `NotFoundError` if the path does not exist.
|
|
684
717
|
* @example
|
|
685
718
|
* ```ts
|
|
686
719
|
* const content = await sandbox.fs.read('/app/index.js')
|
|
@@ -748,9 +781,7 @@ var Fs = class {
|
|
|
748
781
|
* console.log(info.size, info.type)
|
|
749
782
|
* ```
|
|
750
783
|
*
|
|
751
|
-
* Throws `NotFoundError` if the path does not exist.
|
|
752
|
-
* this shipped run a guest agent without it and throw `LizardError` (501) —
|
|
753
|
-
* recreate the sandbox to use it.
|
|
784
|
+
* Throws `NotFoundError` if the path does not exist.
|
|
754
785
|
*/
|
|
755
786
|
async stat(path, opts) {
|
|
756
787
|
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/stat`);
|
|
@@ -767,9 +798,6 @@ var Fs = class {
|
|
|
767
798
|
* ```ts
|
|
768
799
|
* await sandbox.fs.move('/tmp/build.log', '/app/logs/build.log')
|
|
769
800
|
* ```
|
|
770
|
-
*
|
|
771
|
-
* Sandboxes created before this shipped run a guest agent without it and throw
|
|
772
|
-
* `LizardError` (501) — recreate the sandbox to use it.
|
|
773
801
|
*/
|
|
774
802
|
async move(from, to) {
|
|
775
803
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/move`, {
|
|
@@ -798,8 +826,7 @@ var Fs = class {
|
|
|
798
826
|
* }, 1000)
|
|
799
827
|
* ```
|
|
800
828
|
*
|
|
801
|
-
*
|
|
802
|
-
* `LizardError` (501) — recreate the sandbox to use it.
|
|
829
|
+
* Not available on Firecracker sandboxes yet: the call throws `LizardError` (501).
|
|
803
830
|
*/
|
|
804
831
|
async watch(path, opts) {
|
|
805
832
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch`, {
|
|
@@ -845,6 +872,12 @@ var Watcher = class {
|
|
|
845
872
|
if (!res.ok) await handleApiError(res);
|
|
846
873
|
}
|
|
847
874
|
};
|
|
875
|
+
function bytesToBase64(bytes) {
|
|
876
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
877
|
+
let s = "";
|
|
878
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
879
|
+
return btoa(s);
|
|
880
|
+
}
|
|
848
881
|
|
|
849
882
|
// src/sandbox/desktop.ts
|
|
850
883
|
var BUTTONS = { left: "1", middle: "2", right: "3" };
|
|
@@ -1145,11 +1178,17 @@ var Sandbox = class extends SandboxClient {
|
|
|
1145
1178
|
async kill(opts) {
|
|
1146
1179
|
return SandboxClient.killSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
1147
1180
|
}
|
|
1148
|
-
/**
|
|
1181
|
+
/**
|
|
1182
|
+
* Pause the sandbox: its memory, running processes and files are kept and
|
|
1183
|
+
* compute stops. Wait with `waitForStatus('paused')` before resuming.
|
|
1184
|
+
*/
|
|
1149
1185
|
async pause(opts) {
|
|
1150
1186
|
return SandboxClient.pauseSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
1151
1187
|
}
|
|
1152
|
-
/**
|
|
1188
|
+
/**
|
|
1189
|
+
* Resume a paused sandbox exactly where it stopped, processes included. Wait
|
|
1190
|
+
* with `waitForStatus('running')` before running commands.
|
|
1191
|
+
*/
|
|
1153
1192
|
async resume(opts) {
|
|
1154
1193
|
return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
1155
1194
|
}
|
|
@@ -1182,33 +1221,82 @@ var Sandbox = class extends SandboxClient {
|
|
|
1182
1221
|
return SandboxClient.setTimeoutSandbox(this.sandboxId, timeoutMs, this.resolveOpts(opts));
|
|
1183
1222
|
}
|
|
1184
1223
|
/**
|
|
1185
|
-
*
|
|
1224
|
+
* Publish a port on a public HTTPS hostname and return the hostname without a scheme.
|
|
1186
1225
|
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1226
|
+
* The port is private: requests need {@link accessToken} (set by this call) as
|
|
1227
|
+
* the `X-Lizard-Access-Token` header, or `?lizard_token=<token>` in a browser.
|
|
1228
|
+
* {@link exposePort} returns the token and a ready-made browser URL together.
|
|
1189
1229
|
*
|
|
1190
1230
|
* @example
|
|
1191
1231
|
* ```ts
|
|
1192
1232
|
* // Start an HTTP server on port 3000 before exposing it.
|
|
1193
1233
|
* const hostname = await sandbox.getHost(3000)
|
|
1194
|
-
*
|
|
1234
|
+
* const res = await fetch(`https://${hostname}/`, {
|
|
1235
|
+
* headers: { 'X-Lizard-Access-Token': sandbox.accessToken! },
|
|
1236
|
+
* })
|
|
1195
1237
|
* ```
|
|
1196
1238
|
*/
|
|
1197
1239
|
async getHost(port, opts) {
|
|
1198
|
-
|
|
1199
|
-
return hostname;
|
|
1240
|
+
return (await this.exposePort(port, opts)).hostname;
|
|
1200
1241
|
}
|
|
1201
|
-
/**
|
|
1202
|
-
|
|
1203
|
-
|
|
1242
|
+
/**
|
|
1243
|
+
* Publish a port and return everything needed to reach it: the hostname, a
|
|
1244
|
+
* browser `url` that carries the token, and the `accessToken` for API clients.
|
|
1245
|
+
*
|
|
1246
|
+
* @example
|
|
1247
|
+
* ```ts
|
|
1248
|
+
* const { url, hostname, accessToken } = await sandbox.exposePort(3000)
|
|
1249
|
+
* console.log(url) // open in a browser
|
|
1250
|
+
* await fetch(`https://${hostname}/api`, { headers: { 'X-Lizard-Access-Token': accessToken } })
|
|
1251
|
+
* ```
|
|
1252
|
+
*/
|
|
1253
|
+
async exposePort(port, opts) {
|
|
1254
|
+
const exposed = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
|
|
1255
|
+
if (exposed.accessToken) this.accessToken = exposed.accessToken;
|
|
1256
|
+
return exposed;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Clone this running sandbox `count` times (1-10). Each fork is a copy of the
|
|
1260
|
+
* microVM at this instant -- memory, running processes and files -- and is
|
|
1261
|
+
* billed like its source. The source keeps running.
|
|
1262
|
+
*
|
|
1263
|
+
* Returns one entry per requested fork, in order: `{ sandbox, info }`, or
|
|
1264
|
+
* `{ error }` for a copy that could not be made (e.g. the account's sandbox
|
|
1265
|
+
* limit). A sandbox with a volume attached cannot be forked (`ConflictError`, 409).
|
|
1266
|
+
*
|
|
1267
|
+
* @param opts.timeoutMs Lifetime of each fork; defaults to the source's timeout.
|
|
1268
|
+
*
|
|
1269
|
+
* @example
|
|
1270
|
+
* ```ts
|
|
1271
|
+
* const forks = await sandbox.fork({ count: 2 })
|
|
1272
|
+
* for (const f of forks) if (f.sandbox) await f.sandbox.process.exec('echo hi')
|
|
1273
|
+
* ```
|
|
1274
|
+
*/
|
|
1275
|
+
async fork(opts = {}) {
|
|
1276
|
+
const result = await new PlatformClient(this.resolveOpts()).post(
|
|
1277
|
+
`/api/sandboxes/${this.sandboxId}/fork`,
|
|
1278
|
+
{ count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 }
|
|
1279
|
+
);
|
|
1280
|
+
const Ctor = this.constructor;
|
|
1281
|
+
return (Array.isArray(result) ? result : []).map((entry) => {
|
|
1282
|
+
const id = entry.sandbox?.sandboxId ?? entry.sandbox?.id;
|
|
1283
|
+
if (!entry.sandbox || !id) return { error: entry.error ?? "fork failed" };
|
|
1284
|
+
return { sandbox: new Ctor({ ...this.resolveOpts(), sandboxId: id }), info: { ...entry.sandbox, sandboxId: id } };
|
|
1285
|
+
});
|
|
1204
1286
|
}
|
|
1205
|
-
/**
|
|
1287
|
+
/**
|
|
1288
|
+
* Save this sandbox -- memory, running processes and files -- as a private
|
|
1289
|
+
* snapshot. A Firecracker snapshot is `ready` as soon as this returns (about 2 s);
|
|
1290
|
+
* start copies of it with {@link Sandbox.restore} (about 0.4 s each). The source
|
|
1291
|
+
* keeps running. `poolSize` applies to container sandboxes only.
|
|
1292
|
+
*/
|
|
1206
1293
|
snapshot(name = `Snapshot ${this.sandboxId}`, opts = {}) {
|
|
1207
1294
|
return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name, poolSize: opts.poolSize ?? 5 });
|
|
1208
1295
|
}
|
|
1209
1296
|
unexpose(port) {
|
|
1210
1297
|
return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
|
|
1211
1298
|
}
|
|
1299
|
+
/** Stream the sandbox's logs. Not available on Firecracker sandboxes yet (`LizardError`, 501). */
|
|
1212
1300
|
logs(opts = {}) {
|
|
1213
1301
|
return new PlatformClient(this.resolveOpts()).events(query(`/api/sandboxes/${this.sandboxId}/logs`, { tail: opts.tail }), { signal: opts.signal });
|
|
1214
1302
|
}
|
|
@@ -1218,7 +1306,7 @@ var Sandbox = class extends SandboxClient {
|
|
|
1218
1306
|
static getSnapshot(snapshotId, opts) {
|
|
1219
1307
|
return new PlatformClient(opts ?? {}).get(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`);
|
|
1220
1308
|
}
|
|
1221
|
-
/** Start a sandbox from
|
|
1309
|
+
/** Start a new sandbox from a snapshot, in the snapshot's project and region. */
|
|
1222
1310
|
static async restore(snapshotId, opts) {
|
|
1223
1311
|
const client = new PlatformClient(opts ?? {});
|
|
1224
1312
|
const snapshot = await this.getSnapshot(snapshotId, opts);
|
|
@@ -1233,7 +1321,7 @@ var Sandbox = class extends SandboxClient {
|
|
|
1233
1321
|
static setSnapshotWarmPool(snapshotId, poolSize, opts) {
|
|
1234
1322
|
return new PlatformClient(opts ?? {}).patch(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`, { poolSize });
|
|
1235
1323
|
}
|
|
1236
|
-
/** Release idle warm copies; retain saved state and existing claimed sandboxes. */
|
|
1324
|
+
/** Release idle warm copies (container snapshots); retain saved state and existing claimed sandboxes. */
|
|
1237
1325
|
static pauseSnapshot(snapshotId, opts) {
|
|
1238
1326
|
return new PlatformClient(opts ?? {}).post(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}/pause`, {});
|
|
1239
1327
|
}
|
|
@@ -1611,88 +1699,54 @@ var RegionsAPI = class {
|
|
|
1611
1699
|
}
|
|
1612
1700
|
};
|
|
1613
1701
|
|
|
1614
|
-
// src/cli.ts
|
|
1615
|
-
var import_node_child_process = require("child_process");
|
|
1616
|
-
var LizardCLI = class {
|
|
1617
|
-
constructor(opts = {}) {
|
|
1618
|
-
this.opts = opts;
|
|
1619
|
-
}
|
|
1620
|
-
run(args, opts = {}) {
|
|
1621
|
-
return new Promise((resolve, reject) => {
|
|
1622
|
-
const env = { ...process.env };
|
|
1623
|
-
if (this.opts.apiKey) {
|
|
1624
|
-
env.LIZARD_API_KEY = this.opts.apiKey;
|
|
1625
|
-
env.LIZARD_TOKEN = this.opts.apiKey;
|
|
1626
|
-
}
|
|
1627
|
-
if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
|
|
1628
|
-
const child = (0, import_node_child_process.spawn)(this.opts.executable ?? "lizard", ["--json", ...args], {
|
|
1629
|
-
cwd: this.opts.cwd,
|
|
1630
|
-
env,
|
|
1631
|
-
shell: false,
|
|
1632
|
-
stdio: "pipe",
|
|
1633
|
-
signal: opts.signal,
|
|
1634
|
-
timeout: this.opts.timeoutMs ?? 6e5
|
|
1635
|
-
});
|
|
1636
|
-
child.stdout.setEncoding("utf8");
|
|
1637
|
-
child.stderr.setEncoding("utf8");
|
|
1638
|
-
let stdout = "", stderr = "", bytes = 0;
|
|
1639
|
-
const collect = (stream, data) => {
|
|
1640
|
-
bytes += Buffer.byteLength(data);
|
|
1641
|
-
if (bytes > 16 * 1024 * 1024) {
|
|
1642
|
-
child.kill();
|
|
1643
|
-
reject(new Error("CLI output exceeds 16 MiB"));
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
|
-
if (stream === "stdout") stdout += data;
|
|
1647
|
-
else stderr += data;
|
|
1648
|
-
};
|
|
1649
|
-
child.stdout.on("data", (data) => collect("stdout", data));
|
|
1650
|
-
child.stderr.on("data", (data) => collect("stderr", data));
|
|
1651
|
-
child.on("error", reject);
|
|
1652
|
-
child.on("close", (code, signal) => {
|
|
1653
|
-
if (signal) {
|
|
1654
|
-
reject(new Error(`CLI ended with signal ${signal}`));
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
let events = [];
|
|
1658
|
-
try {
|
|
1659
|
-
events = [JSON.parse(stdout)];
|
|
1660
|
-
} catch {
|
|
1661
|
-
events = stdout.split("\n").filter(Boolean).flatMap((line) => {
|
|
1662
|
-
try {
|
|
1663
|
-
return [JSON.parse(line)];
|
|
1664
|
-
} catch {
|
|
1665
|
-
return [];
|
|
1666
|
-
}
|
|
1667
|
-
});
|
|
1668
|
-
}
|
|
1669
|
-
resolve({ code: code ?? 1, stdout, stderr, events });
|
|
1670
|
-
});
|
|
1671
|
-
child.stdin.on("error", (error) => {
|
|
1672
|
-
if (error.code !== "EPIPE") reject(error);
|
|
1673
|
-
});
|
|
1674
|
-
child.stdin.end(opts.stdin);
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
};
|
|
1678
|
-
|
|
1679
1702
|
// src/platform/billing.ts
|
|
1680
1703
|
var BillingAPI = class {
|
|
1681
1704
|
constructor(client) {
|
|
1682
1705
|
this.client = client;
|
|
1683
1706
|
}
|
|
1684
|
-
/**
|
|
1707
|
+
/**
|
|
1708
|
+
* The plan: trial, this month's credits, overage, next charge, cancel date. With
|
|
1709
|
+
* `workspaceId`, the plan of that workspace's owner (`isOwner: false`, no card or
|
|
1710
|
+
* invoice fields).
|
|
1711
|
+
*/
|
|
1712
|
+
subscription(opts) {
|
|
1713
|
+
return this.client.get(query("/api/billing/subscription", { workspaceId: opts?.workspaceId }));
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Opens Stripe Checkout for Pro: the trial when the account can have one, otherwise
|
|
1717
|
+
* Pro at once ($19 today). Returns the page for the user to finish; a second call
|
|
1718
|
+
* while it is open returns the same session.
|
|
1719
|
+
*/
|
|
1720
|
+
startCheckout(opts = {}) {
|
|
1721
|
+
return this.client.post("/api/billing/subscription/checkout", { returnUrl: opts.returnUrl });
|
|
1722
|
+
}
|
|
1723
|
+
/** Ends the trial now: charges $19 and starts the first paid month with $19 of credits. */
|
|
1724
|
+
startProNow() {
|
|
1725
|
+
return this.client.post("/api/billing/subscription/start-now", {});
|
|
1726
|
+
}
|
|
1727
|
+
/** Cancels Pro at the end of the current month or trial. Cancelling in the trial costs nothing. */
|
|
1728
|
+
cancel() {
|
|
1729
|
+
return this.client.post("/api/billing/subscription/cancel", {});
|
|
1730
|
+
}
|
|
1731
|
+
/** Undoes {@link cancel}: Pro renews as usual. */
|
|
1732
|
+
resume() {
|
|
1733
|
+
return this.client.post("/api/billing/subscription/resume", {});
|
|
1734
|
+
}
|
|
1735
|
+
/** Redeems a promo code: a longer trial with more trial credits. Works only before the first payment. */
|
|
1736
|
+
redeemPromo(code) {
|
|
1737
|
+
return this.client.post("/api/billing/promo/redeem", { code });
|
|
1738
|
+
}
|
|
1739
|
+
/** Prepaid credits balance, status, burn rate and runway (old `payg` accounts and enterprise). */
|
|
1685
1740
|
balance() {
|
|
1686
1741
|
return this.client.get("/api/billing/balance");
|
|
1687
1742
|
}
|
|
1688
|
-
/** A page of balance transactions, newest first. */
|
|
1743
|
+
/** A page of balance transactions, newest first (old `payg` accounts and enterprise). */
|
|
1689
1744
|
transactions(opts) {
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
return this.client.get(`/api/billing/transactions${q ? `?${q}` : ""}`);
|
|
1745
|
+
return this.client.get(query("/api/billing/transactions", {
|
|
1746
|
+
limit: opts?.limit,
|
|
1747
|
+
cursor: opts?.cursor || void 0,
|
|
1748
|
+
includeUsage: opts?.includeUsage ? "1" : void 0
|
|
1749
|
+
}));
|
|
1696
1750
|
}
|
|
1697
1751
|
/** Cost summary for the current billing period, broken down by resource. */
|
|
1698
1752
|
summary() {
|
|
@@ -1702,46 +1756,13 @@ var BillingAPI = class {
|
|
|
1702
1756
|
live() {
|
|
1703
1757
|
return this.client.get("/api/billing/live");
|
|
1704
1758
|
}
|
|
1759
|
+
/** Saved cards. Cards are added in Checkout or on the Billing page. */
|
|
1705
1760
|
paymentMethods() {
|
|
1706
1761
|
return this.client.get("/api/billing/payment-methods");
|
|
1707
1762
|
}
|
|
1708
|
-
setupPaymentMethod(returnUrl) {
|
|
1709
|
-
return this.client.post("/api/billing/payment-methods/setup", { returnUrl });
|
|
1710
|
-
}
|
|
1711
1763
|
removePaymentMethod(id) {
|
|
1712
1764
|
return this.client.delete(`/api/billing/payment-methods/${encodeURIComponent(id)}`);
|
|
1713
1765
|
}
|
|
1714
|
-
purchase(creditCents, opts = {}) {
|
|
1715
|
-
if (!Number.isSafeInteger(creditCents) || creditCents <= 0) throw new Error("creditCents must be a positive integer");
|
|
1716
|
-
return this.client.post("/api/billing/purchase", { creditCents, paymentMethod: opts.paymentMethod ?? "card", returnUrl: opts.returnUrl });
|
|
1717
|
-
}
|
|
1718
|
-
autoTopup() {
|
|
1719
|
-
return this.client.get("/api/billing/auto-topup");
|
|
1720
|
-
}
|
|
1721
|
-
setAutoTopup(opts) {
|
|
1722
|
-
return this.client.put("/api/billing/auto-topup", opts);
|
|
1723
|
-
}
|
|
1724
|
-
runAutoTopup() {
|
|
1725
|
-
return this.client.post("/api/billing/auto-topup/run", {});
|
|
1726
|
-
}
|
|
1727
|
-
redeemPromo(code) {
|
|
1728
|
-
return this.client.post("/api/billing/promo/redeem", { code });
|
|
1729
|
-
}
|
|
1730
|
-
x402Quote(creditCents) {
|
|
1731
|
-
return this.client.post("/api/billing/purchase/x402/quote", { creditCents });
|
|
1732
|
-
}
|
|
1733
|
-
paymentStatus(attemptId) {
|
|
1734
|
-
return this.client.get(`/api/billing/purchase/x402/${encodeURIComponent(attemptId)}`);
|
|
1735
|
-
}
|
|
1736
|
-
/** Pay with the CLI's durable x402 journal. Requires CLI >= 4.0.8. Amounts are cents. */
|
|
1737
|
-
async payX402(creditCents, opts) {
|
|
1738
|
-
if (![creditCents, opts.maxTotalCents].every((n) => Number.isSafeInteger(n) && n > 0)) throw new Error("Payment amounts must be positive integer cents");
|
|
1739
|
-
const args = ["credits", "topup", (creditCents / 100).toFixed(2), "--method", "x402", "--max-total", (opts.maxTotalCents / 100).toFixed(2), "--yes"];
|
|
1740
|
-
if (opts.requestId) args.push("--request-id", opts.requestId);
|
|
1741
|
-
const result = await new LizardCLI({ apiKey: this.client.config.apiKey, apiUrl: this.client.config.apiUrl, executable: opts.executable }).run(args);
|
|
1742
|
-
if (result.code !== 0) throw new Error(`x402 CLI exited with code ${result.code}; inspect the payment journal or query paymentStatus before retrying`);
|
|
1743
|
-
return result.events[0];
|
|
1744
|
-
}
|
|
1745
1766
|
};
|
|
1746
1767
|
|
|
1747
1768
|
// src/volume/index.ts
|
|
@@ -1772,7 +1793,7 @@ var Volume = class _Volume {
|
|
|
1772
1793
|
* "the scratch disk for this task" no longer has to store an id between runs.
|
|
1773
1794
|
*
|
|
1774
1795
|
* An existing volume is returned as-is — `sizeGb` applies only to a fresh create,
|
|
1775
|
-
* so an existing volume keeps its size.
|
|
1796
|
+
* so an existing volume keeps its size.
|
|
1776
1797
|
*/
|
|
1777
1798
|
static async getOrCreate(projectId, name, opts) {
|
|
1778
1799
|
const config = new ConnectionConfig(opts);
|
|
@@ -1815,11 +1836,15 @@ var Volume = class _Volume {
|
|
|
1815
1836
|
/**
|
|
1816
1837
|
* Resize a volume, by name or by id, to `sizeGb`.
|
|
1817
1838
|
*
|
|
1818
|
-
*
|
|
1819
|
-
*
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1839
|
+
* **Firecracker volumes cannot be resized yet**: volumes created for the default
|
|
1840
|
+
* Firecracker sandboxes throw `LizardError` (400) with `code: 'volume_not_resizable'`.
|
|
1841
|
+
* Create the volume at the size you need, or copy the data to a new, larger one.
|
|
1842
|
+
*
|
|
1843
|
+
* On a container volume the change is in place and online: the size is a quota,
|
|
1844
|
+
* so no data is copied and a mounted sandbox sees the new size immediately. Both
|
|
1845
|
+
* growing and shrinking are allowed, but a shrink must leave at least 10% of the
|
|
1846
|
+
* new size free — otherwise it throws `ConflictError` with
|
|
1847
|
+
* `code: 'volume_too_full_to_shrink'`.
|
|
1823
1848
|
*
|
|
1824
1849
|
* Other failures carry a `code` too: `invalid_volume_size`, `volume_not_resizable`,
|
|
1825
1850
|
* `volume_resize_in_progress`, `volume_not_provisioned`,
|
|
@@ -1845,7 +1870,7 @@ var Volume = class _Volume {
|
|
|
1845
1870
|
if (!res.ok) await handleApiError(res);
|
|
1846
1871
|
return res.json();
|
|
1847
1872
|
}
|
|
1848
|
-
/** Resize this volume
|
|
1873
|
+
/** Resize this volume — see the static {@link Volume.resize} (not supported for Firecracker volumes yet). */
|
|
1849
1874
|
async resize(projectId, sizeGb) {
|
|
1850
1875
|
return _Volume.resize(projectId, this.volumeId, sizeGb, {
|
|
1851
1876
|
apiKey: this.config.apiKey,
|
|
@@ -1968,7 +1993,7 @@ var Execution = class {
|
|
|
1968
1993
|
this.stderr = "";
|
|
1969
1994
|
/** Rich output items (images, JSON, HTML, plain values). */
|
|
1970
1995
|
this.results = [];
|
|
1971
|
-
/** Monotonically increasing counter for this context. */
|
|
1996
|
+
/** Monotonically increasing counter for this context (the Python kernel's `In[n]`). */
|
|
1972
1997
|
this.executionCount = 0;
|
|
1973
1998
|
}
|
|
1974
1999
|
get success() {
|
|
@@ -1979,25 +2004,317 @@ var Execution = class {
|
|
|
1979
2004
|
}
|
|
1980
2005
|
};
|
|
1981
2006
|
|
|
2007
|
+
// src/code-interpreter/runner.ts
|
|
2008
|
+
var RUNNER_SOURCE = String.raw`# Lizard code runner: executes code for CodeSandbox.run_code / runCode inside the sandbox.
|
|
2009
|
+
# Python runs in a persistent Jupyter kernel per context when the template has one
|
|
2010
|
+
# (the interpreter template does); everything else runs as a fresh process.
|
|
2011
|
+
# Events are JSON lines on stdout; an event over 24000 chars is sent as "part" lines.
|
|
2012
|
+
import base64, json, os, re, shutil, signal, subprocess, sys, threading, time
|
|
2013
|
+
ROOT = "/tmp/.lizard-code"
|
|
2014
|
+
LANGS = {"python": "python3", "javascript": "node", "bash": "bash"}
|
|
2015
|
+
ALIASES = {"py": "python", "python3": "python", "js": "javascript", "node": "javascript", "sh": "bash"}
|
|
2016
|
+
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
|
2017
|
+
LOCK = threading.Lock()
|
|
2018
|
+
|
|
2019
|
+
|
|
2020
|
+
def emit(ev):
|
|
2021
|
+
s = json.dumps(ev)
|
|
2022
|
+
with LOCK:
|
|
2023
|
+
if len(s) <= 24000:
|
|
2024
|
+
print(s, flush=True)
|
|
2025
|
+
return
|
|
2026
|
+
n = (len(s) + 23999) // 24000
|
|
2027
|
+
for i in range(n):
|
|
2028
|
+
print(json.dumps({"type": "part", "data": s[i * 24000:(i + 1) * 24000], "end": i == n - 1}), flush=True)
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
def fail(msg):
|
|
2032
|
+
emit({"type": "fatal", "message": msg})
|
|
2033
|
+
sys.exit(3)
|
|
2034
|
+
|
|
2035
|
+
|
|
2036
|
+
def meta_path(ctx):
|
|
2037
|
+
return os.path.join(ROOT, ctx, "meta.json")
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
def load(ctx):
|
|
2041
|
+
try:
|
|
2042
|
+
with open(meta_path(ctx)) as f:
|
|
2043
|
+
return json.load(f)
|
|
2044
|
+
except OSError:
|
|
2045
|
+
return None
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
def save(m):
|
|
2049
|
+
os.makedirs(os.path.join(ROOT, m["id"]), exist_ok=True)
|
|
2050
|
+
with open(meta_path(m["id"]), "w") as f:
|
|
2051
|
+
json.dump(m, f)
|
|
2052
|
+
|
|
2053
|
+
|
|
2054
|
+
def alive(pid):
|
|
2055
|
+
try:
|
|
2056
|
+
os.kill(pid, 0)
|
|
2057
|
+
except OSError:
|
|
2058
|
+
return False
|
|
2059
|
+
try:
|
|
2060
|
+
with open("/proc/%d/stat" % pid) as f:
|
|
2061
|
+
return f.read().split(") ")[-1][:1] != "Z"
|
|
2062
|
+
except OSError:
|
|
2063
|
+
return False
|
|
2064
|
+
|
|
2065
|
+
|
|
2066
|
+
def kernel_pid(m):
|
|
2067
|
+
try:
|
|
2068
|
+
with open(os.path.join(ROOT, m["id"], "kernel.pid")) as f:
|
|
2069
|
+
return int(f.read().strip())
|
|
2070
|
+
except (OSError, ValueError):
|
|
2071
|
+
return 0
|
|
2072
|
+
|
|
2073
|
+
|
|
2074
|
+
def stop_kernel(m):
|
|
2075
|
+
pid = kernel_pid(m)
|
|
2076
|
+
if pid and alive(pid):
|
|
2077
|
+
try:
|
|
2078
|
+
os.killpg(pid, signal.SIGKILL)
|
|
2079
|
+
except OSError:
|
|
2080
|
+
pass
|
|
2081
|
+
for name in ("kernel.pid", "kernel.json"):
|
|
2082
|
+
try:
|
|
2083
|
+
os.remove(os.path.join(ROOT, m["id"], name))
|
|
2084
|
+
except OSError:
|
|
2085
|
+
pass
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
def start_kernel(m):
|
|
2089
|
+
d = os.path.join(ROOT, m["id"])
|
|
2090
|
+
conn = os.path.join(d, "kernel.json")
|
|
2091
|
+
stop_kernel(m)
|
|
2092
|
+
os.makedirs(m["cwd"], exist_ok=True)
|
|
2093
|
+
log = open(os.path.join(d, "kernel.log"), "ab")
|
|
2094
|
+
p = subprocess.Popen([sys.executable, "-m", "ipykernel_launcher", "-f", conn], cwd=m["cwd"],
|
|
2095
|
+
stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True,
|
|
2096
|
+
env=dict(os.environ, PYTHONUNBUFFERED="1"))
|
|
2097
|
+
with open(os.path.join(d, "kernel.pid"), "w") as f:
|
|
2098
|
+
f.write(str(p.pid))
|
|
2099
|
+
end = time.time() + 30
|
|
2100
|
+
while time.time() < end:
|
|
2101
|
+
if os.path.exists(conn) and os.path.getsize(conn) > 0:
|
|
2102
|
+
return
|
|
2103
|
+
if p.poll() is not None:
|
|
2104
|
+
break
|
|
2105
|
+
time.sleep(0.05)
|
|
2106
|
+
with open(os.path.join(d, "kernel.log"), "rb") as f:
|
|
2107
|
+
tail = f.read()[-2000:].decode("utf-8", "replace")
|
|
2108
|
+
fail("python kernel failed to start: " + (tail or "no output"))
|
|
2109
|
+
|
|
2110
|
+
|
|
2111
|
+
def has_kernel():
|
|
2112
|
+
try:
|
|
2113
|
+
import jupyter_client, ipykernel # noqa: F401
|
|
2114
|
+
return True
|
|
2115
|
+
except ImportError:
|
|
2116
|
+
return False
|
|
2117
|
+
|
|
2118
|
+
|
|
2119
|
+
def client(m):
|
|
2120
|
+
from jupyter_client import BlockingKernelClient
|
|
2121
|
+
if not (kernel_pid(m) and alive(kernel_pid(m))):
|
|
2122
|
+
start_kernel(m)
|
|
2123
|
+
kc = BlockingKernelClient()
|
|
2124
|
+
kc.load_connection_file(os.path.join(ROOT, m["id"], "kernel.json"))
|
|
2125
|
+
kc.start_channels()
|
|
2126
|
+
kc.wait_for_ready(timeout=30)
|
|
2127
|
+
return kc
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
def shell_reply(kc, msg_id, timeout):
|
|
2131
|
+
end = time.time() + timeout
|
|
2132
|
+
while time.time() < end:
|
|
2133
|
+
try:
|
|
2134
|
+
r = kc.get_shell_msg(timeout=max(0.01, end - time.time()))
|
|
2135
|
+
except Exception:
|
|
2136
|
+
return None
|
|
2137
|
+
if r["parent_header"].get("msg_id") == msg_id:
|
|
2138
|
+
return r
|
|
2139
|
+
return None
|
|
2140
|
+
|
|
2141
|
+
|
|
2142
|
+
def silent(kc, code):
|
|
2143
|
+
shell_reply(kc, kc.execute(code, silent=True, store_history=False), 10)
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
BEST = ("image/png", "image/jpeg", "image/svg+xml", "text/html", "text/markdown", "application/json", "text/latex", "text/plain")
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
def run_python(m, code, envs, timeout):
|
|
2150
|
+
kc = client(m)
|
|
2151
|
+
if envs:
|
|
2152
|
+
silent(kc, "import os as _lz_os\n_lz_saved = {k: _lz_os.environ.get(k) for k in %r}\n_lz_os.environ.update(%r)" % (list(envs), envs))
|
|
2153
|
+
msg_id = kc.execute(code, store_history=True, allow_stdin=False)
|
|
2154
|
+
end = time.time() + timeout
|
|
2155
|
+
timed_out = False
|
|
2156
|
+
while True:
|
|
2157
|
+
left = end - time.time()
|
|
2158
|
+
if left <= 0:
|
|
2159
|
+
timed_out = True
|
|
2160
|
+
break
|
|
2161
|
+
try:
|
|
2162
|
+
msg = kc.get_iopub_msg(timeout=left)
|
|
2163
|
+
except Exception:
|
|
2164
|
+
continue
|
|
2165
|
+
if msg["parent_header"].get("msg_id") != msg_id:
|
|
2166
|
+
continue
|
|
2167
|
+
t, c = msg["msg_type"], msg["content"]
|
|
2168
|
+
if t == "stream":
|
|
2169
|
+
emit({"type": "stderr" if c.get("name") == "stderr" else "stdout", "data": c.get("text", "")})
|
|
2170
|
+
elif t in ("execute_result", "display_data"):
|
|
2171
|
+
data = c.get("data", {})
|
|
2172
|
+
mime = next((k for k in BEST if k in data), next(iter(data), None))
|
|
2173
|
+
if mime:
|
|
2174
|
+
v = data[mime]
|
|
2175
|
+
emit({"type": "result", "mime": mime, "data": v if isinstance(v, str) else json.dumps(v)})
|
|
2176
|
+
elif t == "error":
|
|
2177
|
+
emit({"type": "error", "name": c.get("ename", "Error"), "message": c.get("evalue", ""),
|
|
2178
|
+
"traceback": ANSI.sub("", "\n".join(c.get("traceback", [])))})
|
|
2179
|
+
elif t == "status" and c.get("execution_state") == "idle":
|
|
2180
|
+
break
|
|
2181
|
+
count = 0
|
|
2182
|
+
if timed_out:
|
|
2183
|
+
pid = kernel_pid(m)
|
|
2184
|
+
if pid:
|
|
2185
|
+
try:
|
|
2186
|
+
os.kill(pid, signal.SIGINT)
|
|
2187
|
+
except OSError:
|
|
2188
|
+
pass
|
|
2189
|
+
emit({"type": "error", "name": "TimeoutError", "message": "execution timed out after %gs; the kernel was interrupted" % timeout, "traceback": ""})
|
|
2190
|
+
else:
|
|
2191
|
+
r = shell_reply(kc, msg_id, 10)
|
|
2192
|
+
count = (r or {}).get("content", {}).get("execution_count") or 0
|
|
2193
|
+
if envs and not timed_out:
|
|
2194
|
+
silent(kc, "for _lz_k, _lz_v in _lz_saved.items():\n _lz_os.environ.pop(_lz_k, None) if _lz_v is None else _lz_os.environ.__setitem__(_lz_k, _lz_v)")
|
|
2195
|
+
kc.stop_channels()
|
|
2196
|
+
emit({"type": "done", "execution_count": count})
|
|
2197
|
+
|
|
2198
|
+
|
|
2199
|
+
def run_process(m, code, envs, timeout):
|
|
2200
|
+
interp = LANGS[m["language"]]
|
|
2201
|
+
if shutil.which(interp) is None:
|
|
2202
|
+
emit({"type": "error", "name": "LanguageUnavailable", "message": "%s is not installed in this sandbox" % interp, "traceback": ""})
|
|
2203
|
+
emit({"type": "done", "execution_count": 0})
|
|
2204
|
+
return
|
|
2205
|
+
d = os.path.join(ROOT, m["id"])
|
|
2206
|
+
src = os.path.join(d, "cell" + {"node": ".js", "bash": ".sh"}.get(interp, ".py"))
|
|
2207
|
+
with open(src, "w") as f:
|
|
2208
|
+
f.write(code)
|
|
2209
|
+
os.makedirs(m["cwd"], exist_ok=True)
|
|
2210
|
+
p = subprocess.Popen([interp, src], cwd=m["cwd"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
2211
|
+
stderr=subprocess.PIPE, env=dict(os.environ, **(envs or {})), start_new_session=True)
|
|
2212
|
+
err_tail = []
|
|
2213
|
+
|
|
2214
|
+
def pump(stream, kind):
|
|
2215
|
+
for line in iter(stream.readline, b""):
|
|
2216
|
+
text = line.decode("utf-8", "replace")
|
|
2217
|
+
if kind == "stderr":
|
|
2218
|
+
err_tail.append(text)
|
|
2219
|
+
del err_tail[:-50]
|
|
2220
|
+
emit({"type": kind, "data": text})
|
|
2221
|
+
|
|
2222
|
+
ts = [threading.Thread(target=pump, args=(p.stdout, "stdout")), threading.Thread(target=pump, args=(p.stderr, "stderr"))]
|
|
2223
|
+
for t in ts:
|
|
2224
|
+
t.start()
|
|
2225
|
+
try:
|
|
2226
|
+
rc = p.wait(timeout=timeout)
|
|
2227
|
+
except subprocess.TimeoutExpired:
|
|
2228
|
+
os.killpg(p.pid, signal.SIGKILL)
|
|
2229
|
+
rc = None
|
|
2230
|
+
for t in ts:
|
|
2231
|
+
t.join(5)
|
|
2232
|
+
m["count"] = m.get("count", 0) + 1
|
|
2233
|
+
save(m)
|
|
2234
|
+
if rc is None:
|
|
2235
|
+
emit({"type": "error", "name": "TimeoutError", "message": "execution timed out after %gs" % timeout, "traceback": ""})
|
|
2236
|
+
elif rc != 0:
|
|
2237
|
+
emit({"type": "error", "name": "ExitError", "message": "exited with code %d" % rc, "traceback": "".join(err_tail)})
|
|
2238
|
+
emit({"type": "done", "execution_count": m["count"]})
|
|
2239
|
+
|
|
2240
|
+
|
|
2241
|
+
def ctx_for(a):
|
|
2242
|
+
lang = (a.get("language") or "python").lower()
|
|
2243
|
+
lang = ALIASES.get(lang, lang)
|
|
2244
|
+
if lang not in LANGS:
|
|
2245
|
+
fail("unsupported language %r; use python, javascript or bash" % lang)
|
|
2246
|
+
return lang
|
|
2247
|
+
|
|
2248
|
+
|
|
2249
|
+
def main():
|
|
2250
|
+
a = json.loads(base64.b64decode(sys.argv[1]))
|
|
2251
|
+
act = a["action"]
|
|
2252
|
+
os.makedirs(ROOT, exist_ok=True)
|
|
2253
|
+
if act == "create" or (act == "run" and not a.get("context")):
|
|
2254
|
+
lang = ctx_for(a)
|
|
2255
|
+
cid = a.get("context") or ("default-" + lang)
|
|
2256
|
+
m = load(cid) if act == "run" else None
|
|
2257
|
+
if m is None:
|
|
2258
|
+
m = {"id": cid if act == "run" else a["id"], "language": lang, "cwd": a.get("cwd") or "/home/user"}
|
|
2259
|
+
save(m)
|
|
2260
|
+
if lang == "python" and act == "create" and has_kernel():
|
|
2261
|
+
client(m).stop_channels()
|
|
2262
|
+
if act == "create":
|
|
2263
|
+
emit({"type": "context", "id": m["id"], "language": m["language"], "cwd": m["cwd"]})
|
|
2264
|
+
return
|
|
2265
|
+
elif act == "list":
|
|
2266
|
+
out = []
|
|
2267
|
+
for name in sorted(os.listdir(ROOT)):
|
|
2268
|
+
m = load(name)
|
|
2269
|
+
if m and not name.startswith("default-"):
|
|
2270
|
+
out.append({"id": m["id"], "language": m["language"], "cwd": m["cwd"]})
|
|
2271
|
+
emit({"type": "contexts", "data": out})
|
|
2272
|
+
return
|
|
2273
|
+
else:
|
|
2274
|
+
m = load(a["context"])
|
|
2275
|
+
if m is None:
|
|
2276
|
+
fail("context %r not found" % a["context"])
|
|
2277
|
+
if act == "delete":
|
|
2278
|
+
stop_kernel(m)
|
|
2279
|
+
shutil.rmtree(os.path.join(ROOT, m["id"]), ignore_errors=True)
|
|
2280
|
+
emit({"type": "ok"})
|
|
2281
|
+
return
|
|
2282
|
+
if act == "restart":
|
|
2283
|
+
if m["language"] == "python" and has_kernel():
|
|
2284
|
+
start_kernel(m)
|
|
2285
|
+
m["count"] = 0
|
|
2286
|
+
save(m)
|
|
2287
|
+
emit({"type": "ok"})
|
|
2288
|
+
return
|
|
2289
|
+
code = a.get("code")
|
|
2290
|
+
if code is None:
|
|
2291
|
+
with open(a["codeFile"]) as f:
|
|
2292
|
+
code = f.read()
|
|
2293
|
+
os.remove(a["codeFile"])
|
|
2294
|
+
if m["language"] == "python" and has_kernel():
|
|
2295
|
+
run_python(m, code, a.get("envs") or {}, a.get("timeout") or 60)
|
|
2296
|
+
else:
|
|
2297
|
+
run_process(m, code, a.get("envs") or {}, a.get("timeout") or 60)
|
|
2298
|
+
|
|
2299
|
+
|
|
2300
|
+
main()
|
|
2301
|
+
`;
|
|
2302
|
+
|
|
1982
2303
|
// src/code-interpreter/index.ts
|
|
1983
|
-
var
|
|
2304
|
+
var DEFAULT_RUN_TIMEOUT_MS = 6e4;
|
|
2305
|
+
var RUNNER_OVERHEAD_MS = 45e3;
|
|
2306
|
+
var MAX_INLINE_ARGS = 1e5;
|
|
1984
2307
|
var CodeSandbox = class extends Sandbox {
|
|
1985
|
-
get serverUrl() {
|
|
1986
|
-
return this.getHost(CODE_INTERPRETER_PORT).then((h) => `https://${h}`);
|
|
1987
|
-
}
|
|
1988
2308
|
/**
|
|
1989
|
-
* Execute code
|
|
2309
|
+
* Execute code and wait for it to finish.
|
|
1990
2310
|
*
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
2311
|
+
* Python keeps its state per context (the default context, per language,
|
|
2312
|
+
* when none is given). Code errors appear in `execution.error`; request
|
|
2313
|
+
* failures throw.
|
|
1993
2314
|
*
|
|
1994
2315
|
* @param code Source code to run.
|
|
1995
2316
|
* @param opts Language, context, env vars, timeout, and streaming callbacks.
|
|
1996
2317
|
*
|
|
1997
|
-
* @returns Execution result with stdout, stderr, results, and any error.
|
|
1998
|
-
*
|
|
1999
|
-
* Code errors appear in `execution.error`; request failures throw.
|
|
2000
|
-
*
|
|
2001
2318
|
* @example
|
|
2002
2319
|
* ```ts
|
|
2003
2320
|
* const result = await sandbox.runCode(`
|
|
@@ -2011,63 +2328,42 @@ var CodeSandbox = class extends Sandbox {
|
|
|
2011
2328
|
if (opts?.context && opts?.language) {
|
|
2012
2329
|
throw new Error("Provide context or language, not both");
|
|
2013
2330
|
}
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
} else if (item.type === "stderr") {
|
|
2042
|
-
execution.stderr += item.data;
|
|
2043
|
-
opts?.onStderr?.(item.data);
|
|
2044
|
-
} else if (item.type === "result") {
|
|
2045
|
-
const out = item;
|
|
2046
|
-
execution.results.push(out);
|
|
2047
|
-
opts?.onResult?.(out);
|
|
2048
|
-
} else if (item.type === "error") {
|
|
2049
|
-
const err = new ExecutionError(
|
|
2050
|
-
item.name,
|
|
2051
|
-
item.message,
|
|
2052
|
-
item.traceback
|
|
2053
|
-
);
|
|
2054
|
-
execution.error = err;
|
|
2055
|
-
opts?.onError?.(err);
|
|
2056
|
-
} else if (item.type === "done") {
|
|
2057
|
-
execution.executionCount = item.execution_count ?? 0;
|
|
2058
|
-
break;
|
|
2059
|
-
}
|
|
2331
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
|
|
2332
|
+
const args = {
|
|
2333
|
+
action: "run",
|
|
2334
|
+
code,
|
|
2335
|
+
envs: opts?.envs ?? {},
|
|
2336
|
+
timeout: timeoutMs / 1e3
|
|
2337
|
+
};
|
|
2338
|
+
if (opts?.context) args.context = opts.context.id;
|
|
2339
|
+
else args.language = opts?.language ?? "python";
|
|
2340
|
+
const execution = new Execution();
|
|
2341
|
+
await this.runner(args, timeoutMs, (item) => {
|
|
2342
|
+
if (item.type === "stdout") {
|
|
2343
|
+
execution.stdout += item.data;
|
|
2344
|
+
opts?.onStdout?.(item.data);
|
|
2345
|
+
} else if (item.type === "stderr") {
|
|
2346
|
+
execution.stderr += item.data;
|
|
2347
|
+
opts?.onStderr?.(item.data);
|
|
2348
|
+
} else if (item.type === "result") {
|
|
2349
|
+
const out = { type: "result", mime: item.mime, data: item.data };
|
|
2350
|
+
execution.results.push(out);
|
|
2351
|
+
opts?.onResult?.(out);
|
|
2352
|
+
} else if (item.type === "error") {
|
|
2353
|
+
const err = new ExecutionError(item.name, item.message, item.traceback);
|
|
2354
|
+
execution.error = err;
|
|
2355
|
+
opts?.onError?.(err);
|
|
2356
|
+
} else if (item.type === "done") {
|
|
2357
|
+
execution.executionCount = item.execution_count ?? 0;
|
|
2060
2358
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
clearTimeout(timer);
|
|
2064
|
-
}
|
|
2359
|
+
});
|
|
2360
|
+
return execution;
|
|
2065
2361
|
}
|
|
2066
2362
|
/**
|
|
2067
2363
|
* Create a new isolated execution context.
|
|
2068
2364
|
*
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2365
|
+
* A Python context is its own kernel: its own variables, and `cwd` as its
|
|
2366
|
+
* working directory. Other languages get the working directory only.
|
|
2071
2367
|
*
|
|
2072
2368
|
* @example
|
|
2073
2369
|
* ```ts
|
|
@@ -2077,40 +2373,80 @@ var CodeSandbox = class extends Sandbox {
|
|
|
2077
2373
|
* ```
|
|
2078
2374
|
*/
|
|
2079
2375
|
async createContext(opts) {
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2376
|
+
const id = `ctx-${randomId()}`;
|
|
2377
|
+
let ctx;
|
|
2378
|
+
await this.runner(
|
|
2379
|
+
{ action: "create", id, language: opts?.language ?? "python", cwd: opts?.cwd ?? "/home/user" },
|
|
2380
|
+
DEFAULT_RUN_TIMEOUT_MS,
|
|
2381
|
+
(ev) => {
|
|
2382
|
+
if (ev.type === "context") ctx = { id: ev.id, language: ev.language, cwd: ev.cwd };
|
|
2383
|
+
}
|
|
2384
|
+
);
|
|
2385
|
+
if (!ctx) throw new LizardError("Failed to create context");
|
|
2386
|
+
return ctx;
|
|
2090
2387
|
}
|
|
2091
|
-
/**
|
|
2092
|
-
* List all active execution contexts in this sandbox.
|
|
2093
|
-
*/
|
|
2388
|
+
/** List the contexts created with {@link createContext} in this sandbox. */
|
|
2094
2389
|
async listContexts() {
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2390
|
+
let out = [];
|
|
2391
|
+
await this.runner({ action: "list" }, DEFAULT_RUN_TIMEOUT_MS, (ev) => {
|
|
2392
|
+
if (ev.type === "contexts") out = ev.data;
|
|
2393
|
+
});
|
|
2394
|
+
return out;
|
|
2098
2395
|
}
|
|
2099
|
-
/**
|
|
2100
|
-
* Delete an execution context and free its resources.
|
|
2101
|
-
*/
|
|
2396
|
+
/** Delete an execution context, stopping its kernel. */
|
|
2102
2397
|
async deleteContext(context) {
|
|
2103
2398
|
const id = typeof context === "string" ? context : context.id;
|
|
2104
|
-
|
|
2105
|
-
|
|
2399
|
+
await this.runner({ action: "delete", context: id }, DEFAULT_RUN_TIMEOUT_MS, () => {
|
|
2400
|
+
});
|
|
2106
2401
|
}
|
|
2107
|
-
/**
|
|
2108
|
-
* Restart a context, clearing all variables and state.
|
|
2109
|
-
*/
|
|
2402
|
+
/** Restart a context, clearing all variables and state. */
|
|
2110
2403
|
async restartContext(context) {
|
|
2111
2404
|
const id = typeof context === "string" ? context : context.id;
|
|
2112
|
-
|
|
2113
|
-
|
|
2405
|
+
await this.runner({ action: "restart", context: id }, DEFAULT_RUN_TIMEOUT_MS, () => {
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
/** Run the in-sandbox runner with `args` and hand each of its events to `onEvent`. */
|
|
2409
|
+
async runner(args, timeoutMs, onEvent) {
|
|
2410
|
+
let encoded = toBase64(JSON.stringify(args));
|
|
2411
|
+
if (encoded.length > MAX_INLINE_ARGS && typeof args.code === "string") {
|
|
2412
|
+
const codeFile = `/tmp/.lizard-code/in-${randomId()}`;
|
|
2413
|
+
await this.fs.write(codeFile, args.code);
|
|
2414
|
+
const { code: _code, ...rest } = args;
|
|
2415
|
+
encoded = toBase64(JSON.stringify({ ...rest, codeFile }));
|
|
2416
|
+
}
|
|
2417
|
+
let part = "";
|
|
2418
|
+
let fatal;
|
|
2419
|
+
let sawEvent = false;
|
|
2420
|
+
const stderr = [];
|
|
2421
|
+
const result = await this.process.exec(["python3", "-c", RUNNER_SOURCE, encoded], {
|
|
2422
|
+
timeoutMs: timeoutMs + RUNNER_OVERHEAD_MS,
|
|
2423
|
+
onStdout: (line) => {
|
|
2424
|
+
let ev;
|
|
2425
|
+
try {
|
|
2426
|
+
ev = JSON.parse(line);
|
|
2427
|
+
if (ev.type === "part") {
|
|
2428
|
+
part += ev.data;
|
|
2429
|
+
if (!ev.end) return;
|
|
2430
|
+
ev = JSON.parse(part);
|
|
2431
|
+
part = "";
|
|
2432
|
+
}
|
|
2433
|
+
} catch {
|
|
2434
|
+
part = "";
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
sawEvent = true;
|
|
2438
|
+
if (ev.type === "fatal") fatal = ev.message;
|
|
2439
|
+
else onEvent(ev);
|
|
2440
|
+
},
|
|
2441
|
+
onStderr: (line) => {
|
|
2442
|
+
stderr.push(line);
|
|
2443
|
+
}
|
|
2444
|
+
});
|
|
2445
|
+
if (fatal) throw new LizardError(fatal);
|
|
2446
|
+
if (result.exitCode !== 0 || !sawEvent) {
|
|
2447
|
+
const detail = stderr.slice(-20).join("\n").trim() || `exit code ${result.exitCode}`;
|
|
2448
|
+
throw new LizardError(`Code runner failed (python3 is required in the sandbox): ${detail}`);
|
|
2449
|
+
}
|
|
2114
2450
|
}
|
|
2115
2451
|
static async create(templateOrOpts, opts) {
|
|
2116
2452
|
return super.create(templateOrOpts, opts);
|
|
@@ -2119,27 +2455,83 @@ var CodeSandbox = class extends Sandbox {
|
|
|
2119
2455
|
return super.connect(sandboxId, opts);
|
|
2120
2456
|
}
|
|
2121
2457
|
};
|
|
2122
|
-
CodeSandbox.defaultTemplate = "
|
|
2123
|
-
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2458
|
+
CodeSandbox.defaultTemplate = "interpreter";
|
|
2459
|
+
function randomId() {
|
|
2460
|
+
const c = globalThis.crypto;
|
|
2461
|
+
return c?.randomUUID ? c.randomUUID().replace(/-/g, "").slice(0, 16) : Math.random().toString(16).slice(2, 18);
|
|
2462
|
+
}
|
|
2463
|
+
function toBase64(text) {
|
|
2464
|
+
if (typeof Buffer !== "undefined") return Buffer.from(text, "utf8").toString("base64");
|
|
2465
|
+
const bytes = new TextEncoder().encode(text);
|
|
2466
|
+
let s = "";
|
|
2467
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
2468
|
+
return btoa(s);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/cli.ts
|
|
2472
|
+
var import_node_child_process = require("child_process");
|
|
2473
|
+
var LizardCLI = class {
|
|
2474
|
+
constructor(opts = {}) {
|
|
2475
|
+
this.opts = opts;
|
|
2476
|
+
}
|
|
2477
|
+
run(args, opts = {}) {
|
|
2478
|
+
return new Promise((resolve, reject) => {
|
|
2479
|
+
const env = { ...process.env };
|
|
2480
|
+
if (this.opts.apiKey) {
|
|
2481
|
+
env.LIZARD_API_KEY = this.opts.apiKey;
|
|
2482
|
+
env.LIZARD_TOKEN = this.opts.apiKey;
|
|
2136
2483
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2484
|
+
if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
|
|
2485
|
+
const child = (0, import_node_child_process.spawn)(this.opts.executable ?? "lizard", ["--json", ...args], {
|
|
2486
|
+
cwd: this.opts.cwd,
|
|
2487
|
+
env,
|
|
2488
|
+
shell: false,
|
|
2489
|
+
stdio: "pipe",
|
|
2490
|
+
signal: opts.signal,
|
|
2491
|
+
timeout: this.opts.timeoutMs ?? 6e5
|
|
2492
|
+
});
|
|
2493
|
+
child.stdout.setEncoding("utf8");
|
|
2494
|
+
child.stderr.setEncoding("utf8");
|
|
2495
|
+
let stdout = "", stderr = "", bytes = 0;
|
|
2496
|
+
const collect = (stream, data) => {
|
|
2497
|
+
bytes += Buffer.byteLength(data);
|
|
2498
|
+
if (bytes > 16 * 1024 * 1024) {
|
|
2499
|
+
child.kill();
|
|
2500
|
+
reject(new Error("CLI output exceeds 16 MiB"));
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
if (stream === "stdout") stdout += data;
|
|
2504
|
+
else stderr += data;
|
|
2505
|
+
};
|
|
2506
|
+
child.stdout.on("data", (data) => collect("stdout", data));
|
|
2507
|
+
child.stderr.on("data", (data) => collect("stderr", data));
|
|
2508
|
+
child.on("error", reject);
|
|
2509
|
+
child.on("close", (code, signal) => {
|
|
2510
|
+
if (signal) {
|
|
2511
|
+
reject(new Error(`CLI ended with signal ${signal}`));
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
let events = [];
|
|
2515
|
+
try {
|
|
2516
|
+
events = [JSON.parse(stdout)];
|
|
2517
|
+
} catch {
|
|
2518
|
+
events = stdout.split("\n").filter(Boolean).flatMap((line) => {
|
|
2519
|
+
try {
|
|
2520
|
+
return [JSON.parse(line)];
|
|
2521
|
+
} catch {
|
|
2522
|
+
return [];
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
resolve({ code: code ?? 1, stdout, stderr, events });
|
|
2527
|
+
});
|
|
2528
|
+
child.stdin.on("error", (error) => {
|
|
2529
|
+
if (error.code !== "EPIPE") reject(error);
|
|
2530
|
+
});
|
|
2531
|
+
child.stdin.end(opts.stdin);
|
|
2532
|
+
});
|
|
2141
2533
|
}
|
|
2142
|
-
}
|
|
2534
|
+
};
|
|
2143
2535
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2144
2536
|
0 && (module.exports = {
|
|
2145
2537
|
AddonsAPI,
|
|
@@ -2158,6 +2550,7 @@ async function* readLines(body) {
|
|
|
2158
2550
|
LizardError,
|
|
2159
2551
|
MetricsAPI,
|
|
2160
2552
|
NotFoundError,
|
|
2553
|
+
PaymentRequiredError,
|
|
2161
2554
|
PlatformClient,
|
|
2162
2555
|
ProjectsAPI,
|
|
2163
2556
|
RegionsAPI,
|