withruntime 0.11.2 → 0.14.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 +11 -10
- package/README.src.md +10 -9
- package/VERCEL.md +43 -5
- package/compatibility-lock.json +58 -49
- package/dist/billing-cli.d.ts +1 -1
- package/dist/billing-cli.js +85 -5
- 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 +15 -6
- package/dist/blaxel/sandbox.js +64 -8
- 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-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 +22 -3
- package/dist/cli.js +647 -273
- 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/credentials.d.ts +3 -0
- package/dist/credentials.js +15 -0
- package/dist/daytona/computer-use.d.ts +13 -4
- package/dist/daytona/computer-use.js +66 -8
- package/dist/daytona/daytona.d.ts +7 -1
- package/dist/daytona/daytona.js +31 -4
- 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 +28 -22
- package/dist/e2b/sandbox.js +34 -8
- package/dist/e2b/unsupported.js +1 -1
- package/dist/errors.d.ts +11 -2
- package/dist/errors.js +17 -5
- 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 +3 -1
- 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 +27 -4
- package/dist/machine.js +46 -14
- package/dist/mcp.js +144 -20
- 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/api-keys.d.ts +73 -0
- package/dist/products/api-keys.js +23 -0
- package/dist/products/apps.d.ts +157 -0
- package/dist/products/apps.js +60 -0
- package/dist/products/balancers.d.ts +87 -0
- package/dist/products/balancers.js +8 -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 +156 -0
- package/dist/products/builds.js +227 -0
- package/dist/products/index.d.ts +5 -16
- package/dist/products/index.js +5 -14
- package/dist/products/jobs.d.ts +19 -2
- package/dist/products/jobs.js +28 -11
- package/dist/products/logs.d.ts +245 -0
- package/dist/products/logs.js +274 -0
- package/dist/products/network-products.d.ts +38 -7
- package/dist/products/network-products.js +31 -18
- package/dist/products/observability.d.ts +8 -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/resources.d.ts +54 -0
- package/dist/products/resources.js +122 -0
- package/dist/products/secrets.d.ts +29 -3
- package/dist/products/secrets.js +47 -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 +6 -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 +7 -2
- package/dist/sandbox.js +41 -11
- 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 +18 -1
- package/dist/transport.js +80 -6
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +71 -4
- package/dist/vercel/sandbox.d.ts +1 -1
- package/dist/vercel/sandbox.js +6 -4
- package/dist/vercel/snapshot.js +10 -2
- package/dist/volumes/index.d.ts +9 -0
- package/dist/volumes/index.js +10 -0
- package/dist/wallet.d.ts +4 -1
- package/dist/wallet.js +2 -1
- package/package.json +48 -14
package/dist/freestyle/index.js
CHANGED
|
@@ -1,11 +1,195 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ConnectionError, RuntimeError } from "../errors.js";
|
|
2
|
+
import { all, client, CompatibilityError, create, destroy, execOptions, lookup, only, quote, ready, refuse, refused, validateEnvironment, } from "../compat/core.js";
|
|
2
3
|
import { GuestFiles } from "../compat/files.js";
|
|
3
4
|
export { CompatibilityError };
|
|
5
|
+
/** What every failed Runtime call becomes, as Freestyle throws it for a
|
|
6
|
+
* non-2xx response: `code` is Runtime's error code in upper case
|
|
7
|
+
* (`NOT_FOUND`), `status` its HTTP status, and `cause` the RuntimeError. */
|
|
8
|
+
export class FreestyleApiError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
status;
|
|
11
|
+
path;
|
|
12
|
+
constructor(status, body, path) {
|
|
13
|
+
super(body.message || `Freestyle API error: ${body.code}`);
|
|
14
|
+
this.name = "FreestyleApiError";
|
|
15
|
+
this.code = body.code;
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.path = path;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** What a call that got no answer from Runtime becomes, as Freestyle 0.2.17
|
|
21
|
+
* throws a failed fetch: a TypeError whose `cause` is the RuntimeError
|
|
22
|
+
* ConnectionError and whose `errors` carry it as one entry. */
|
|
23
|
+
export class FreestyleTransportError extends TypeError {
|
|
24
|
+
errors;
|
|
25
|
+
constructor(cause, errors) {
|
|
26
|
+
super(`${cause instanceof Error ? cause.message : "fetch failed"} (${errors.map((e) => e.code ?? e.name ?? "Error").join("; ")})`, { cause });
|
|
27
|
+
this.name = "FreestyleTransportError";
|
|
28
|
+
this.errors = errors;
|
|
29
|
+
}
|
|
30
|
+
toJSON() {
|
|
31
|
+
return { name: this.name, message: this.message, errors: this.errors };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function api(call) {
|
|
35
|
+
try {
|
|
36
|
+
return await call();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof ConnectionError)
|
|
40
|
+
throw new FreestyleTransportError(error, [
|
|
41
|
+
{ name: error.name, message: error.message, code: error.code },
|
|
42
|
+
]);
|
|
43
|
+
if (!(error instanceof RuntimeError))
|
|
44
|
+
throw error;
|
|
45
|
+
const translated = new FreestyleApiError(error.status, {
|
|
46
|
+
code: error.code.toUpperCase(),
|
|
47
|
+
message: error.message,
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(translated, "cause", { value: error });
|
|
50
|
+
throw translated;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const notFound = (what) => new FreestyleApiError(404, { code: "NOT_FOUND", message: `${what} not found` });
|
|
54
|
+
export class FirewallRuleValidationError extends Error {
|
|
55
|
+
path;
|
|
56
|
+
constructor(path, message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.path = path;
|
|
59
|
+
this.name = "FirewallRuleValidationError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class TlsRuleValidationError extends Error {
|
|
63
|
+
path;
|
|
64
|
+
constructor(path, message) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.path = path;
|
|
67
|
+
this.name = "TlsRuleValidationError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class TlsForwardAuthValidationError extends Error {
|
|
71
|
+
path;
|
|
72
|
+
constructor(path, message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.path = path;
|
|
75
|
+
this.name = "TlsForwardAuthValidationError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Freestyle products Runtime does not sell as they are --------------------------
|
|
79
|
+
const FIREWALL = "give a VM's outbound rules at create (firewall.rules to public or CIDR destinations), or use sandbox.network";
|
|
80
|
+
const TLS = "serve HTTPS from a sandbox with Runtime previews (sandbox.previews)";
|
|
81
|
+
const TUNNELS = "reach a sandbox's ports with sandbox.tunnel()";
|
|
82
|
+
const VPC = "let sandboxes reach each other with Runtime private networks (runtime.networks.private)";
|
|
83
|
+
export const FirewallNamespace = refused("Freestyle", "FirewallNamespace", FIREWALL);
|
|
84
|
+
export const FirewallRulesNamespace = refused("Freestyle", "FirewallRulesNamespace", FIREWALL);
|
|
85
|
+
export const validateFirewallEndpoint = refused("Freestyle", "validateFirewallEndpoint", FIREWALL);
|
|
86
|
+
export const validateFirewallRule = refused("Freestyle", "validateFirewallRule", FIREWALL);
|
|
87
|
+
export const validateFirewallSpec = refused("Freestyle", "validateFirewallSpec", FIREWALL);
|
|
88
|
+
export const TlsNamespace = refused("Freestyle", "TlsNamespace", TLS);
|
|
89
|
+
export const TlsRulesNamespace = refused("Freestyle", "TlsRulesNamespace", TLS);
|
|
90
|
+
export const TlsForwardAuthNamespace = refused("Freestyle", "TlsForwardAuthNamespace", TLS);
|
|
91
|
+
export const hasTlsIdentity = refused("Freestyle", "hasTlsIdentity", TLS);
|
|
92
|
+
export const resolveTlsProtocol = refused("Freestyle", "resolveTlsProtocol", TLS);
|
|
93
|
+
export const validateTlsDomain = refused("Freestyle", "validateTlsDomain", TLS);
|
|
94
|
+
export const validateTlsEndpoint = refused("Freestyle", "validateTlsEndpoint", TLS);
|
|
95
|
+
export const validateTlsForwardAuth = refused("Freestyle", "validateTlsForwardAuth", TLS);
|
|
96
|
+
export const validateTlsMailRoute = refused("Freestyle", "validateTlsMailRoute", TLS);
|
|
97
|
+
export const validateTlsRule = refused("Freestyle", "validateTlsRule", TLS);
|
|
98
|
+
export const validateTlsSpec = refused("Freestyle", "validateTlsSpec", TLS);
|
|
99
|
+
export const TunnelsNamespace = refused("Freestyle", "TunnelsNamespace", TUNNELS);
|
|
100
|
+
export const VpcNamespace = refused("Freestyle", "VpcNamespace", VPC);
|
|
101
|
+
export const Vpc = refused("Freestyle", "Vpc", VPC);
|
|
102
|
+
export const VpcTunnelNamespace = refused("Freestyle", "VpcTunnelNamespace", VPC);
|
|
103
|
+
const ADDRESS = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,2}))?$/;
|
|
104
|
+
/** An IPv4 address or CIDR, as Runtime's network allow list takes it. */
|
|
105
|
+
function cidrOf(value) {
|
|
106
|
+
const match = ADDRESS.exec(value);
|
|
107
|
+
if (!match || match.slice(1, 5).some((octet) => Number(octet) > 255))
|
|
108
|
+
return undefined;
|
|
109
|
+
if (match[5] !== undefined && Number(match[5]) > 32)
|
|
110
|
+
return undefined;
|
|
111
|
+
return match[5] === undefined ? `${value}/32` : value;
|
|
112
|
+
}
|
|
113
|
+
/** The network settings for a VM's inline rules: every rule allows this VM
|
|
114
|
+
* outbound, to the public internet or to a CIDR, over the web ports Runtime
|
|
115
|
+
* opens (any, 80 or 443 over TCP). Runtime opens 80 and 443 together, so a
|
|
116
|
+
* destination named with one port alone refuses rather than gets the other.
|
|
117
|
+
* Anything else refuses before allocation. */
|
|
118
|
+
function networkFor(rules) {
|
|
119
|
+
const allow = [];
|
|
120
|
+
let open = false;
|
|
121
|
+
const ports = new Map();
|
|
122
|
+
for (const [index, rule] of rules.entries()) {
|
|
123
|
+
only("Freestyle firewall rule", rule, ["action", "source", "destination", "description"]);
|
|
124
|
+
const { destination } = rule;
|
|
125
|
+
if (rule.action !== "allow" || Object.keys(rule.source ?? {}).length)
|
|
126
|
+
throw new CompatibilityError("Freestyle", `firewall.rules[${index}]: rules other than this VM's outbound access`);
|
|
127
|
+
only("Freestyle firewall destination", destination ?? {}, [
|
|
128
|
+
"public",
|
|
129
|
+
"cidr",
|
|
130
|
+
"port",
|
|
131
|
+
"protocol",
|
|
132
|
+
]);
|
|
133
|
+
if ((destination.port !== undefined && destination.port !== 80 && destination.port !== 443) ||
|
|
134
|
+
(destination.protocol !== undefined && destination.protocol !== "tcp") ||
|
|
135
|
+
(destination.port !== undefined && destination.protocol === undefined))
|
|
136
|
+
throw new CompatibilityError("Freestyle", `firewall.rules[${index}]: ports other than 80 and 443 over TCP; open host:port pairs with sandbox.network.set({ connect })`);
|
|
137
|
+
let target = "public";
|
|
138
|
+
if (destination.public === true && destination.cidr === undefined)
|
|
139
|
+
open = true;
|
|
140
|
+
else if (destination.public === undefined && typeof destination.cidr === "string") {
|
|
141
|
+
const cidr = cidrOf(destination.cidr);
|
|
142
|
+
if (!cidr)
|
|
143
|
+
throw new FirewallRuleValidationError(`firewall.rules[${index}].destination.cidr`, `firewall.rules[${index}].destination.cidr \`${destination.cidr}\` is not an IPv4 CIDR`);
|
|
144
|
+
if (!allow.includes(cidr))
|
|
145
|
+
allow.push(cidr);
|
|
146
|
+
target = cidr;
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
throw new CompatibilityError("Freestyle", `firewall.rules[${index}]: destinations other than public or one CIDR`);
|
|
150
|
+
ports.set(target, (ports.get(target) ?? new Set()).add(destination.port));
|
|
151
|
+
}
|
|
152
|
+
for (const [target, named] of ports)
|
|
153
|
+
if (!named.has(undefined) && !(named.has(80) && named.has(443)))
|
|
154
|
+
throw new CompatibilityError("Freestyle", `firewall rules opening ${target} on port ${[...named].join(" or ")} alone; Runtime opens 80 and 443 together`);
|
|
155
|
+
if (open)
|
|
156
|
+
return { internet: true };
|
|
157
|
+
if (allow.length)
|
|
158
|
+
return { internet: true, allow };
|
|
159
|
+
return { internet: false };
|
|
160
|
+
}
|
|
161
|
+
function ruleData(vmId, index, destination, at) {
|
|
162
|
+
return {
|
|
163
|
+
id: `runtime-network-${vmId}-${index}`,
|
|
164
|
+
action: "allow",
|
|
165
|
+
source: { vmId },
|
|
166
|
+
destination,
|
|
167
|
+
description: null,
|
|
168
|
+
dependencies: [],
|
|
169
|
+
createdAt: at,
|
|
170
|
+
updatedAt: at,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
4
173
|
function validateMetadata(metadata) {
|
|
5
174
|
for (const key of ["compat.provider", "fs.displayName"])
|
|
6
175
|
if (metadata !== undefined && Object.prototype.hasOwnProperty.call(metadata, key))
|
|
7
176
|
throw new CompatibilityError("Freestyle", `metadata key ${key} reserved by this adapter`);
|
|
8
177
|
}
|
|
178
|
+
const LINUX_USER = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
179
|
+
function validateLinuxUser(user) {
|
|
180
|
+
if (typeof user !== "string" || !LINUX_USER.test(user))
|
|
181
|
+
throw new TypeError("linuxUser must be a Linux user name: lowercase letters, digits, _ and -");
|
|
182
|
+
return user;
|
|
183
|
+
}
|
|
184
|
+
/** Runs `command` as `user` through sudo. The environment travels in the
|
|
185
|
+
* exec's own environment and sudo keeps it by name: values never enter the
|
|
186
|
+
* command line, which the process list and Runtime's records show. */
|
|
187
|
+
function asUser(user, command, env = {}) {
|
|
188
|
+
validateEnvironment(env);
|
|
189
|
+
const names = Object.keys(env);
|
|
190
|
+
const keep = names.length ? ` --preserve-env=${names.join(",")}` : "";
|
|
191
|
+
return `exec sudo -n -H${keep} -u ${quote(user)} -- bash -c ${quote(command)}`;
|
|
192
|
+
}
|
|
9
193
|
const data = (s) => ({
|
|
10
194
|
id: s.id,
|
|
11
195
|
state: s.state,
|
|
@@ -19,59 +203,62 @@ const data = (s) => ({
|
|
|
19
203
|
createdAt: s.info.createdAt,
|
|
20
204
|
updatedAt: s.info.createdAt,
|
|
21
205
|
});
|
|
206
|
+
/** Labels the snapshot API returns beside the typed fields. */
|
|
207
|
+
const labelsOf = (s) => s.labels ?? {};
|
|
22
208
|
const snapshotData = (s) => ({
|
|
23
209
|
id: s.id,
|
|
24
210
|
sourceVmId: s.sourceSandboxId,
|
|
25
211
|
slug: s.name,
|
|
26
|
-
displayName: s
|
|
212
|
+
displayName: labelsOf(s)["fs.displayName"] ?? null,
|
|
27
213
|
public: false,
|
|
28
214
|
createdAt: s.createdAt,
|
|
29
215
|
updatedAt: s.createdAt,
|
|
30
|
-
ttlSeconds: s.retentionDays * 86400,
|
|
216
|
+
ttlSeconds: s.retentionDays === null ? null : s.retentionDays * 86400,
|
|
31
217
|
});
|
|
218
|
+
/** Freestyle keeps a snapshot for a time in seconds; Runtime for whole days. */
|
|
219
|
+
function retentionDays(ttlSeconds) {
|
|
220
|
+
if (ttlSeconds === undefined)
|
|
221
|
+
return undefined;
|
|
222
|
+
if (!(ttlSeconds > 0) ||
|
|
223
|
+
ttlSeconds > 365 * 86400 ||
|
|
224
|
+
ttlSeconds % 86400 !== 0 ||
|
|
225
|
+
!Number.isFinite(ttlSeconds))
|
|
226
|
+
throw new CompatibilityError("Freestyle", "snapshot retention outside 1 to 365 whole days");
|
|
227
|
+
return ttlSeconds / 86400;
|
|
228
|
+
}
|
|
32
229
|
export class Freestyle {
|
|
33
230
|
vms;
|
|
34
231
|
constructor(options = {}) {
|
|
35
|
-
|
|
232
|
+
const { teamId, stackAccessToken, identityAccessToken, ...rest } = options;
|
|
233
|
+
if (teamId !== undefined)
|
|
234
|
+
throw new CompatibilityError("Freestyle", "teamId; a Runtime API key is its organization's");
|
|
235
|
+
if (stackAccessToken !== undefined || identityAccessToken !== undefined)
|
|
236
|
+
throw new CompatibilityError("Freestyle", "stackAccessToken and identityAccessToken; sign in with a Runtime API key");
|
|
237
|
+
this.vms = new VmsNamespace(client(rest));
|
|
238
|
+
}
|
|
239
|
+
get firewall() {
|
|
240
|
+
throw new CompatibilityError("Freestyle", `freestyle.firewall; ${FIREWALL}`);
|
|
241
|
+
}
|
|
242
|
+
get tls() {
|
|
243
|
+
throw new CompatibilityError("Freestyle", `freestyle.tls; ${TLS}`);
|
|
244
|
+
}
|
|
245
|
+
get tunnels() {
|
|
246
|
+
throw new CompatibilityError("Freestyle", `freestyle.tunnels; ${TUNNELS}`);
|
|
247
|
+
}
|
|
248
|
+
get vpc() {
|
|
249
|
+
throw new CompatibilityError("Freestyle", `freestyle.vpc; ${VPC}`);
|
|
36
250
|
}
|
|
37
251
|
}
|
|
38
|
-
|
|
252
|
+
refuse("Freestyle", "Freestyle", Freestyle, {
|
|
253
|
+
fetch: "call Runtime's API with its own client (new Runtime())",
|
|
254
|
+
});
|
|
255
|
+
export class VmSnapshotsNamespace {
|
|
39
256
|
runtime;
|
|
40
|
-
snapshots;
|
|
41
257
|
constructor(runtime) {
|
|
42
258
|
this.runtime = runtime;
|
|
43
|
-
this.snapshots = {
|
|
44
|
-
get: async (id) => snapshotData(await this.resolveSnapshot(id)),
|
|
45
|
-
delete: async (id) => runtime.snapshots.delete((await this.resolveSnapshot(id)).id),
|
|
46
|
-
list: async (options = {}) => {
|
|
47
|
-
only("Freestyle snapshot list", options, ["sourceVmId", "limit", "offset"]);
|
|
48
|
-
if (options.sourceVmId !== undefined && typeof options.sourceVmId !== "string")
|
|
49
|
-
throw new TypeError("sourceVmId must be a string");
|
|
50
|
-
if (options.limit !== undefined &&
|
|
51
|
-
(!Number.isSafeInteger(options.limit) || options.limit <= 0))
|
|
52
|
-
throw new TypeError("limit must be a positive safe integer");
|
|
53
|
-
if (options.offset !== undefined &&
|
|
54
|
-
(!Number.isSafeInteger(options.offset) || options.offset < 0))
|
|
55
|
-
throw new TypeError("offset must be a nonnegative safe integer");
|
|
56
|
-
const offset = options.offset ?? 0;
|
|
57
|
-
const snapshots = [];
|
|
58
|
-
let totalCount = 0;
|
|
59
|
-
// Count the filtered catalog across every native page without retaining
|
|
60
|
-
// snapshots outside the requested page or Page.toArray's default cap.
|
|
61
|
-
const page = await runtime.snapshots.list(options.sourceVmId === undefined ? {} : { sandboxId: options.sourceVmId });
|
|
62
|
-
for await (const snapshot of page) {
|
|
63
|
-
if (options.sourceVmId !== undefined && snapshot.sourceSandboxId !== options.sourceVmId)
|
|
64
|
-
continue;
|
|
65
|
-
if (totalCount >= offset &&
|
|
66
|
-
(options.limit === undefined || snapshots.length < options.limit))
|
|
67
|
-
snapshots.push(snapshotData(snapshot));
|
|
68
|
-
totalCount++;
|
|
69
|
-
}
|
|
70
|
-
return { snapshots, totalCount };
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
259
|
}
|
|
74
|
-
|
|
260
|
+
/** A snapshot by id or by its slug (Runtime's snapshot name). */
|
|
261
|
+
async resolve(id) {
|
|
75
262
|
if (/^[a-f0-9-]{36}$/i.test(id))
|
|
76
263
|
return this.runtime.snapshots.get(id);
|
|
77
264
|
const found = (await this.runtime.snapshots.list({ name: id })).data.find((s) => s.name === id);
|
|
@@ -79,6 +266,70 @@ export class VmsNamespace {
|
|
|
79
266
|
throw new CompatibilityError("Freestyle", `snapshot ${id}; import the source environment into Runtime first`);
|
|
80
267
|
return found;
|
|
81
268
|
}
|
|
269
|
+
get(snapshotId) {
|
|
270
|
+
return api(async () => snapshotData(await this.resolve(snapshotId)));
|
|
271
|
+
}
|
|
272
|
+
delete(snapshotId) {
|
|
273
|
+
return api(async () => this.runtime.snapshots.delete((await this.resolve(snapshotId)).id));
|
|
274
|
+
}
|
|
275
|
+
async list(options = {}) {
|
|
276
|
+
only("Freestyle snapshot list", options, ["sourceVmId", "limit", "offset"]);
|
|
277
|
+
if (options.sourceVmId !== undefined && typeof options.sourceVmId !== "string")
|
|
278
|
+
throw new TypeError("sourceVmId must be a string");
|
|
279
|
+
if (options.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit <= 0))
|
|
280
|
+
throw new TypeError("limit must be a positive safe integer");
|
|
281
|
+
if (options.offset !== undefined &&
|
|
282
|
+
(!Number.isSafeInteger(options.offset) || options.offset < 0))
|
|
283
|
+
throw new TypeError("offset must be a nonnegative safe integer");
|
|
284
|
+
return api(async () => {
|
|
285
|
+
const offset = options.offset ?? 0;
|
|
286
|
+
const snapshots = [];
|
|
287
|
+
let totalCount = 0;
|
|
288
|
+
// Count the filtered catalog across every native page without retaining
|
|
289
|
+
// snapshots outside the requested page or Page.toArray's default cap.
|
|
290
|
+
const page = await this.runtime.snapshots.list(options.sourceVmId === undefined ? {} : { sandboxId: options.sourceVmId });
|
|
291
|
+
for await (const snapshot of page) {
|
|
292
|
+
if (options.sourceVmId !== undefined && snapshot.sourceSandboxId !== options.sourceVmId)
|
|
293
|
+
continue;
|
|
294
|
+
if (totalCount >= offset &&
|
|
295
|
+
(options.limit === undefined || snapshots.length < options.limit))
|
|
296
|
+
snapshots.push(snapshotData(snapshot));
|
|
297
|
+
totalCount++;
|
|
298
|
+
}
|
|
299
|
+
return { snapshots, totalCount };
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/** Renames, relabels and re-times a snapshot: slug is its Runtime name,
|
|
303
|
+
* ttlSeconds whole days from now. */
|
|
304
|
+
async update(snapshotId, options) {
|
|
305
|
+
only("Freestyle snapshot update", options, ["slug", "displayName", "ttlSeconds"]);
|
|
306
|
+
const days = retentionDays(options.ttlSeconds);
|
|
307
|
+
if (options.slug !== undefined && (typeof options.slug !== "string" || !options.slug))
|
|
308
|
+
throw new TypeError("slug must be a nonempty string");
|
|
309
|
+
if (options.displayName !== undefined && typeof options.displayName !== "string")
|
|
310
|
+
throw new TypeError("displayName must be a string");
|
|
311
|
+
return api(async () => {
|
|
312
|
+
let snapshot = await this.resolve(snapshotId);
|
|
313
|
+
if (options.slug !== undefined || options.displayName !== undefined)
|
|
314
|
+
snapshot = await this.runtime.snapshots.update(snapshot.id, {
|
|
315
|
+
...(options.slug === undefined ? {} : { name: options.slug }),
|
|
316
|
+
...(options.displayName === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: { labels: { ...labelsOf(snapshot), "fs.displayName": options.displayName } }),
|
|
319
|
+
});
|
|
320
|
+
if (days !== undefined)
|
|
321
|
+
snapshot = await this.runtime.snapshots.extend(snapshot.id, days);
|
|
322
|
+
return snapshotData(snapshot);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export class VmsNamespace {
|
|
327
|
+
runtime;
|
|
328
|
+
snapshots;
|
|
329
|
+
constructor(runtime) {
|
|
330
|
+
this.runtime = runtime;
|
|
331
|
+
this.snapshots = new VmSnapshotsNamespace(runtime);
|
|
332
|
+
}
|
|
82
333
|
async create(options) {
|
|
83
334
|
only("Freestyle create", options, [
|
|
84
335
|
"snapshotId",
|
|
@@ -92,47 +343,38 @@ export class VmsNamespace {
|
|
|
92
343
|
if (!options.firewall || !Array.isArray(options.firewall.rules))
|
|
93
344
|
throw new TypeError("firewall.rules is required");
|
|
94
345
|
only("Freestyle firewall", options.firewall, ["rules"]);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
:
|
|
114
|
-
|
|
346
|
+
const network = networkFor(options.firewall.rules);
|
|
347
|
+
return api(async () => {
|
|
348
|
+
const snapshot = options.snapshotId
|
|
349
|
+
? (await this.snapshots.resolve(options.snapshotId)).id
|
|
350
|
+
: undefined;
|
|
351
|
+
const s = await create(this.runtime, "freestyle", {
|
|
352
|
+
snapshot,
|
|
353
|
+
name: options.slug ?? undefined,
|
|
354
|
+
labels: { ...options.metadata, "fs.displayName": options.displayName ?? "" },
|
|
355
|
+
persistent: true,
|
|
356
|
+
idlePauseSeconds: options.idleTimeoutSeconds === -1 || options.idleTimeoutSeconds == null
|
|
357
|
+
? 0
|
|
358
|
+
: options.idleTimeoutSeconds,
|
|
359
|
+
network,
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
vm: new Vm(this.runtime, s.id),
|
|
363
|
+
vmId: s.id,
|
|
364
|
+
data: data(s),
|
|
365
|
+
firewallRules: options.firewall.rules.map((rule, index) => ruleData(s.id, index, rule.destination, s.info.createdAt)),
|
|
366
|
+
tlsRules: [],
|
|
367
|
+
};
|
|
115
368
|
});
|
|
116
|
-
return {
|
|
117
|
-
vm: new Vm(this.runtime, s.id),
|
|
118
|
-
vmId: s.id,
|
|
119
|
-
data: data(s),
|
|
120
|
-
firewallRules: options.firewall.rules.map((rule) => ({
|
|
121
|
-
...rule,
|
|
122
|
-
id: `runtime-network-${s.id}`,
|
|
123
|
-
source: { vmId: s.id },
|
|
124
|
-
})),
|
|
125
|
-
tlsRules: [],
|
|
126
|
-
};
|
|
127
369
|
}
|
|
128
370
|
ref(id) {
|
|
129
371
|
return new Vm(this.runtime, id);
|
|
130
372
|
}
|
|
131
|
-
|
|
132
|
-
return data(await lookup(this.runtime, id));
|
|
373
|
+
get(id) {
|
|
374
|
+
return api(async () => data(await lookup(this.runtime, id)));
|
|
133
375
|
}
|
|
134
|
-
|
|
135
|
-
|
|
376
|
+
delete(id) {
|
|
377
|
+
return api(async () => destroy(await lookup(this.runtime, id)));
|
|
136
378
|
}
|
|
137
379
|
async list(options = {}) {
|
|
138
380
|
only("Freestyle list", options, ["state", "slug", "metadata", "limit", "offset"]);
|
|
@@ -140,7 +382,7 @@ export class VmsNamespace {
|
|
|
140
382
|
const i = x.indexOf(":");
|
|
141
383
|
return [x.slice(0, i), x.slice(i + 1)];
|
|
142
384
|
}) ?? [];
|
|
143
|
-
const values = (await all(this.runtime, "freestyle")).filter((s) => (!options.state || s.state === options.state) &&
|
|
385
|
+
const values = (await api(() => all(this.runtime, "freestyle"))).filter((s) => (!options.state || s.state === options.state) &&
|
|
144
386
|
(!options.slug || s.info.name === options.slug) &&
|
|
145
387
|
filters.every(([k, v]) => s.info.labels[k] === v));
|
|
146
388
|
return {
|
|
@@ -164,7 +406,8 @@ export class VmFilesystem {
|
|
|
164
406
|
this.files = new GuestFiles(get, "/");
|
|
165
407
|
}
|
|
166
408
|
async readFile(path, options = {}) {
|
|
167
|
-
|
|
409
|
+
const stream = await this.readFileStream(path, options);
|
|
410
|
+
return api(async () => new Uint8Array(await new Response(stream).arrayBuffer()));
|
|
168
411
|
}
|
|
169
412
|
async readTextFile(path, options = {}) {
|
|
170
413
|
return new TextDecoder().decode(await this.readFile(path, options));
|
|
@@ -175,7 +418,7 @@ export class VmFilesystem {
|
|
|
175
418
|
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
|
|
176
419
|
throw new TypeError("File range must contain nonnegative safe integers");
|
|
177
420
|
options.signal?.throwIfAborted();
|
|
178
|
-
const source = await (await this.get()).files.readStream(this.files.path(path), { signal: options.signal });
|
|
421
|
+
const source = await api(async () => (await this.get()).files.readStream(this.files.path(path), { signal: options.signal }));
|
|
179
422
|
if (options.offset === undefined && options.length === undefined)
|
|
180
423
|
return source;
|
|
181
424
|
const reader = source.getReader();
|
|
@@ -231,113 +474,382 @@ export class VmFilesystem {
|
|
|
231
474
|
: content instanceof Uint8Array
|
|
232
475
|
? content
|
|
233
476
|
: new Uint8Array(await content.arrayBuffer());
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
mode
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
477
|
+
await api(async () => {
|
|
478
|
+
let mode = options.mode;
|
|
479
|
+
if (mode === undefined)
|
|
480
|
+
mode = (await this.files.exists(path))
|
|
481
|
+
? parseInt((await this.files.stat(path)).mode, 8)
|
|
482
|
+
: 0o600;
|
|
483
|
+
await this.files.writeFile(path, bytes, { mode, signal: options.signal });
|
|
484
|
+
});
|
|
240
485
|
options.onProgress?.({ completedBytes: bytes.length, totalBytes: bytes.length });
|
|
241
486
|
}
|
|
242
487
|
writeTextFile(path, content, options = {}) {
|
|
243
488
|
return this.writeFile(path, content, options);
|
|
244
489
|
}
|
|
245
490
|
mkdir(path) {
|
|
246
|
-
return this.files.mkdir(path, true);
|
|
491
|
+
return api(() => this.files.mkdir(path, true));
|
|
247
492
|
}
|
|
248
493
|
remove(path) {
|
|
249
|
-
return this.files.remove(path, true);
|
|
494
|
+
return api(() => this.files.remove(path, true));
|
|
250
495
|
}
|
|
251
496
|
exists(path) {
|
|
252
|
-
return this.files.exists(path);
|
|
497
|
+
return api(() => this.files.exists(path));
|
|
253
498
|
}
|
|
254
|
-
|
|
255
|
-
return (await this.files.readdir(path)).map((e) => ({ name: e.name, kind: e.type }));
|
|
499
|
+
readDir(path) {
|
|
500
|
+
return api(async () => (await this.files.readdir(path)).map((e) => ({ name: e.name, kind: e.type })));
|
|
256
501
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
502
|
+
stat(path) {
|
|
503
|
+
return api(async () => {
|
|
504
|
+
const e = await this.files.stat(path);
|
|
505
|
+
const s = await this.get();
|
|
506
|
+
const r = await s.exec(["stat", "-c", "%U:%G", "--", this.files.path(path)], { check: true });
|
|
507
|
+
const [owner, group] = r.stdout.trim().split(":");
|
|
508
|
+
return {
|
|
509
|
+
size: e.size,
|
|
510
|
+
isFile: e.type === "file",
|
|
511
|
+
isDirectory: e.type === "directory",
|
|
512
|
+
isSymlink: e.type === "symlink",
|
|
513
|
+
permissions: e.mode,
|
|
514
|
+
owner: owner,
|
|
515
|
+
group: group,
|
|
516
|
+
modified: e.modifiedAt,
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const PTY = /^: freestyle-pty slug=([a-z0-9-]*) user=([a-z0-9_-]*) cols=(\d+) rows=(\d+); /;
|
|
522
|
+
const SLUG = /^(?=.*[a-z-])[a-z0-9-]{1,63}$/;
|
|
523
|
+
/** A session's number: FNV-1a of its process id, 31 bits, never 0. */
|
|
524
|
+
export function sessionNumber(processId) {
|
|
525
|
+
let hash = 0x811c9dc5;
|
|
526
|
+
for (const char of processId)
|
|
527
|
+
hash = Math.imul(hash ^ char.charCodeAt(0), 0x01000193);
|
|
528
|
+
return hash >>> 1 || 1;
|
|
529
|
+
}
|
|
530
|
+
function session(process) {
|
|
531
|
+
const mark = PTY.exec(process.command);
|
|
532
|
+
if (!process.pty || !mark)
|
|
533
|
+
return undefined;
|
|
534
|
+
const running = process.state === "running";
|
|
535
|
+
return {
|
|
536
|
+
processId: process.id,
|
|
537
|
+
running,
|
|
538
|
+
info: {
|
|
539
|
+
sessionId: sessionNumber(process.id),
|
|
540
|
+
state: running ? "running" : "exited",
|
|
541
|
+
createdUnix: Math.floor(Date.parse(process.startedAt) / 1000),
|
|
542
|
+
cols: Number(mark[3]),
|
|
543
|
+
rows: Number(mark[4]),
|
|
544
|
+
...(running ? {} : { exitCode: process.exitCode }),
|
|
545
|
+
linuxUser: mark[2] || null,
|
|
546
|
+
// A slug is released when its shell exits.
|
|
547
|
+
slug: running && mark[1] ? mark[1] : null,
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const dimension = (value, fallback, name) => {
|
|
552
|
+
if (value === undefined)
|
|
553
|
+
return fallback;
|
|
554
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1000)
|
|
555
|
+
throw new TypeError(`${name} must be a whole number from 1 to 1000`);
|
|
556
|
+
return value;
|
|
557
|
+
};
|
|
558
|
+
const PTY_OPEN = [
|
|
559
|
+
"exec",
|
|
560
|
+
"cols",
|
|
561
|
+
"rows",
|
|
562
|
+
"linuxUser",
|
|
563
|
+
"slug",
|
|
564
|
+
"onData",
|
|
565
|
+
"onExit",
|
|
566
|
+
"onClose",
|
|
567
|
+
"onError",
|
|
568
|
+
];
|
|
569
|
+
export class PtySession {
|
|
570
|
+
terminal;
|
|
571
|
+
sandbox;
|
|
572
|
+
processId;
|
|
573
|
+
sessionId;
|
|
574
|
+
slug;
|
|
575
|
+
created;
|
|
576
|
+
#readyState = 1;
|
|
577
|
+
constructor(terminal, sandbox, processId, sessionId, slug, created, events) {
|
|
578
|
+
this.terminal = terminal;
|
|
579
|
+
this.sandbox = sandbox;
|
|
580
|
+
this.processId = processId;
|
|
581
|
+
this.sessionId = sessionId;
|
|
582
|
+
this.slug = slug;
|
|
583
|
+
this.created = created;
|
|
584
|
+
void terminal.exited.then((exitCode) => {
|
|
585
|
+
this.#readyState = 3;
|
|
586
|
+
if (exitCode !== null)
|
|
587
|
+
events.onExit?.(exitCode);
|
|
588
|
+
events.onClose?.({ wasClean: true, code: 1000, reason: "" });
|
|
589
|
+
});
|
|
590
|
+
this.onError = events.onError;
|
|
591
|
+
}
|
|
592
|
+
onError;
|
|
593
|
+
write(data) {
|
|
594
|
+
this.terminal.write(data);
|
|
595
|
+
}
|
|
596
|
+
resize(options) {
|
|
597
|
+
this.terminal.resize(dimension(options.cols, 80, "cols"), dimension(options.rows, 24, "rows"));
|
|
598
|
+
}
|
|
599
|
+
signal(sig) {
|
|
600
|
+
if (sig !== "sigint" && sig !== "sigkill")
|
|
601
|
+
throw new TypeError('signal must be "sigint" or "sigkill"');
|
|
602
|
+
void (async () => {
|
|
603
|
+
const process = await this.sandbox.processes.get(this.processId);
|
|
604
|
+
await process.kill(sig === "sigint" ? "SIGINT" : "SIGKILL");
|
|
605
|
+
})().catch((error) => this.onError?.(error));
|
|
606
|
+
}
|
|
607
|
+
/** Closes the connection; the shell keeps running and attach() finds it. */
|
|
608
|
+
detach() {
|
|
609
|
+
this.#readyState = 2;
|
|
610
|
+
this.terminal.close();
|
|
611
|
+
}
|
|
612
|
+
get readyState() {
|
|
613
|
+
return this.#readyState;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
export class VmPty {
|
|
617
|
+
get;
|
|
618
|
+
constructor(get) {
|
|
619
|
+
this.get = get;
|
|
620
|
+
}
|
|
621
|
+
async sessions(sandbox) {
|
|
622
|
+
return (await sandbox.processes.list()).flatMap((process) => session(process) ?? []);
|
|
623
|
+
}
|
|
624
|
+
async find(sandbox, selector, linuxUser) {
|
|
625
|
+
if (typeof selector !== "number" && typeof selector !== "string")
|
|
626
|
+
throw new TypeError("session must be a session id or a slug");
|
|
627
|
+
const found = (await this.sessions(sandbox))
|
|
628
|
+
.filter((s) => typeof selector === "number" ? s.info.sessionId === selector : s.info.slug === selector)
|
|
629
|
+
.sort((a, b) => a.info.createdUnix - b.info.createdUnix)[0];
|
|
630
|
+
if (!found)
|
|
631
|
+
throw notFound(`PTY session ${selector}`);
|
|
632
|
+
if (linuxUser !== undefined && found.info.linuxUser !== linuxUser)
|
|
633
|
+
throw notFound(`PTY session ${selector} of ${linuxUser}`);
|
|
634
|
+
return found;
|
|
635
|
+
}
|
|
636
|
+
async connect(sandbox, found, created, events) {
|
|
637
|
+
const terminal = await sandbox.terminal({ processId: found.processId, onData: events.onData });
|
|
638
|
+
return new PtySession(terminal, sandbox, found.processId, found.info.sessionId, found.info.slug ?? undefined, created, events);
|
|
639
|
+
}
|
|
640
|
+
async open(options = {}) {
|
|
641
|
+
only("Freestyle pty open", options, PTY_OPEN);
|
|
642
|
+
const cols = dimension(options.cols, 80, "cols");
|
|
643
|
+
const rows = dimension(options.rows, 24, "rows");
|
|
644
|
+
const user = options.linuxUser === undefined ? "" : validateLinuxUser(options.linuxUser);
|
|
645
|
+
if (options.slug !== undefined &&
|
|
646
|
+
(typeof options.slug !== "string" || !SLUG.test(options.slug)))
|
|
647
|
+
throw new TypeError("slug must be lowercase letters, digits and hyphens, not all digits");
|
|
648
|
+
if (options.exec !== undefined &&
|
|
649
|
+
(typeof options.exec !== "string" || options.exec.includes("\0")))
|
|
650
|
+
throw new TypeError("exec must be a command without NUL bytes");
|
|
651
|
+
return api(async () => {
|
|
652
|
+
const sandbox = await this.get();
|
|
653
|
+
if (options.slug !== undefined) {
|
|
654
|
+
const existing = (await this.sessions(sandbox)).find((s) => s.info.slug === options.slug);
|
|
655
|
+
// Get-or-create: a running session with the slug is handed back, exec unrun.
|
|
656
|
+
if (existing)
|
|
657
|
+
return this.connect(sandbox, existing, false, options);
|
|
658
|
+
}
|
|
659
|
+
const shell = options.exec === undefined
|
|
660
|
+
? user
|
|
661
|
+
? `exec sudo -n -H -u ${quote(user)} -- bash -l`
|
|
662
|
+
: "exec bash -l"
|
|
663
|
+
: user
|
|
664
|
+
? `exec sudo -n -H -u ${quote(user)} -- bash -c ${quote(options.exec)}`
|
|
665
|
+
: options.exec;
|
|
666
|
+
const process = await sandbox.spawn(`: freestyle-pty slug=${options.slug ?? ""} user=${user} cols=${cols} rows=${rows}; ${shell}`, { pty: { cols, rows } });
|
|
667
|
+
const started = session(process.toJSON());
|
|
668
|
+
if (!started)
|
|
669
|
+
throw new Error("The started PTY session has no session line");
|
|
670
|
+
return this.connect(sandbox, started, true, options);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
async attach(options) {
|
|
674
|
+
only("Freestyle pty attach", options, [
|
|
675
|
+
"session",
|
|
676
|
+
"linuxUser",
|
|
677
|
+
"onData",
|
|
678
|
+
"onExit",
|
|
679
|
+
"onClose",
|
|
680
|
+
"onError",
|
|
681
|
+
]);
|
|
682
|
+
if (options.linuxUser !== undefined)
|
|
683
|
+
validateLinuxUser(options.linuxUser);
|
|
684
|
+
return api(async () => {
|
|
685
|
+
const sandbox = await this.get();
|
|
686
|
+
return this.connect(sandbox, await this.find(sandbox, options.session, options.linuxUser), false, options);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
async list(options = {}) {
|
|
690
|
+
only("Freestyle pty list", options, ["linuxUser"]);
|
|
691
|
+
if (options.linuxUser !== undefined)
|
|
692
|
+
validateLinuxUser(options.linuxUser);
|
|
693
|
+
return api(async () => ({
|
|
694
|
+
sessions: (await this.sessions(await this.get()))
|
|
695
|
+
.filter((s) => options.linuxUser === undefined || s.info.linuxUser === options.linuxUser)
|
|
696
|
+
.map((s) => s.info),
|
|
697
|
+
}));
|
|
698
|
+
}
|
|
699
|
+
/** Ends a session's shell. An exited session stays in the list. */
|
|
700
|
+
async close(session, options = {}) {
|
|
701
|
+
only("Freestyle pty close", options, ["linuxUser"]);
|
|
702
|
+
if (options.linuxUser !== undefined)
|
|
703
|
+
validateLinuxUser(options.linuxUser);
|
|
704
|
+
return api(async () => {
|
|
705
|
+
const sandbox = await this.get();
|
|
706
|
+
const found = await this.find(sandbox, session, options.linuxUser);
|
|
707
|
+
if (!found.running)
|
|
708
|
+
return { sessionId: found.info.sessionId, exitCode: found.info.exitCode };
|
|
709
|
+
await (await sandbox.processes.get(found.processId)).kill("SIGKILL");
|
|
710
|
+
return { sessionId: found.info.sessionId };
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
export class VmLinuxUserPty {
|
|
715
|
+
pty;
|
|
716
|
+
linuxUser;
|
|
717
|
+
constructor(pty, linuxUser) {
|
|
718
|
+
this.pty = pty;
|
|
719
|
+
this.linuxUser = linuxUser;
|
|
720
|
+
}
|
|
721
|
+
async open(options = {}) {
|
|
722
|
+
only("Freestyle pty open", options, PTY_OPEN.filter((key) => key !== "linuxUser"));
|
|
723
|
+
return this.pty.open({ ...options, linuxUser: this.linuxUser });
|
|
724
|
+
}
|
|
725
|
+
attach(options) {
|
|
726
|
+
return this.pty.attach({ ...options, linuxUser: this.linuxUser });
|
|
727
|
+
}
|
|
728
|
+
list() {
|
|
729
|
+
return this.pty.list({ linuxUser: this.linuxUser });
|
|
730
|
+
}
|
|
731
|
+
close(session) {
|
|
732
|
+
return this.pty.close(session, { linuxUser: this.linuxUser });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
export class VmLinuxUser {
|
|
736
|
+
vm;
|
|
737
|
+
linuxUser;
|
|
738
|
+
pty;
|
|
739
|
+
constructor(vm, linuxUser) {
|
|
740
|
+
this.vm = vm;
|
|
741
|
+
this.linuxUser = linuxUser;
|
|
742
|
+
validateLinuxUser(linuxUser);
|
|
743
|
+
this.pty = new VmLinuxUserPty(vm.pty, linuxUser);
|
|
744
|
+
}
|
|
745
|
+
exec(options) {
|
|
746
|
+
const request = typeof options === "string" ? { command: options } : options;
|
|
747
|
+
return this.vm.exec({ ...request, linuxUser: this.linuxUser });
|
|
272
748
|
}
|
|
273
749
|
}
|
|
274
750
|
export class Vm {
|
|
275
751
|
runtime;
|
|
276
752
|
id;
|
|
277
753
|
fs;
|
|
754
|
+
pty;
|
|
278
755
|
constructor(runtime, id) {
|
|
279
756
|
this.runtime = runtime;
|
|
280
757
|
this.id = id;
|
|
281
758
|
this.fs = new VmFilesystem(() => lookup(runtime, id));
|
|
759
|
+
this.pty = new VmPty(() => lookup(runtime, id));
|
|
760
|
+
}
|
|
761
|
+
data() {
|
|
762
|
+
return api(async () => data(await lookup(this.runtime, this.id)));
|
|
763
|
+
}
|
|
764
|
+
start() {
|
|
765
|
+
return api(async () => data(await ready(await lookup(this.runtime, this.id))));
|
|
282
766
|
}
|
|
283
|
-
|
|
284
|
-
return
|
|
767
|
+
pause() {
|
|
768
|
+
return api(async () => {
|
|
769
|
+
const s = await lookup(this.runtime, this.id);
|
|
770
|
+
await s.pause();
|
|
771
|
+
return data(s);
|
|
772
|
+
});
|
|
285
773
|
}
|
|
286
|
-
|
|
287
|
-
return
|
|
774
|
+
delete() {
|
|
775
|
+
return api(async () => destroy(await lookup(this.runtime, this.id)));
|
|
288
776
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
777
|
+
/** The VM's outbound rules, read back from its network settings. */
|
|
778
|
+
firewallRules() {
|
|
779
|
+
return api(async () => {
|
|
780
|
+
const s = await lookup(this.runtime, this.id);
|
|
781
|
+
const policy = await s.network.get();
|
|
782
|
+
if (policy.deny.length || policy.connect.length)
|
|
783
|
+
throw new CompatibilityError("Freestyle", "network rules that deny or open ports; read them with sandbox.network.get()");
|
|
784
|
+
const destinations = !policy.internet
|
|
785
|
+
? []
|
|
786
|
+
: !policy.allow.length
|
|
787
|
+
? [{ public: true }]
|
|
788
|
+
: policy.allow.map((entry) => {
|
|
789
|
+
const cidr = cidrOf(entry);
|
|
790
|
+
if (!cidr)
|
|
791
|
+
throw new CompatibilityError("Freestyle", "network rules that name hosts; read them with sandbox.network.get()");
|
|
792
|
+
return { cidr };
|
|
793
|
+
});
|
|
794
|
+
const at = policy.updatedAt ?? s.info.createdAt;
|
|
795
|
+
const rules = destinations.map((destination, index) => ruleData(s.id, index, destination, at));
|
|
796
|
+
return { rules, totalCount: rules.length };
|
|
797
|
+
});
|
|
293
798
|
}
|
|
294
|
-
|
|
295
|
-
|
|
799
|
+
linuxUser(linuxUser) {
|
|
800
|
+
return new VmLinuxUser(this, linuxUser);
|
|
296
801
|
}
|
|
297
802
|
async update(options) {
|
|
298
803
|
only("Freestyle update", options, ["slug", "displayName", "idleTimeoutSeconds", "metadata"]);
|
|
299
804
|
validateMetadata(options.metadata);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
805
|
+
return api(async () => {
|
|
806
|
+
const s = await lookup(this.runtime, this.id);
|
|
807
|
+
await s.update({
|
|
808
|
+
name: options.slug,
|
|
809
|
+
idlePauseSeconds: options.idleTimeoutSeconds === -1 ? 0 : options.idleTimeoutSeconds,
|
|
810
|
+
labels: {
|
|
811
|
+
...s.info.labels,
|
|
812
|
+
...options.metadata,
|
|
813
|
+
...(options.displayName === undefined ? {} : { "fs.displayName": options.displayName }),
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
return data(s);
|
|
309
817
|
});
|
|
310
|
-
return data(s);
|
|
311
818
|
}
|
|
312
819
|
async exec(options) {
|
|
313
820
|
const o = typeof options === "string" ? { command: options } : options;
|
|
314
821
|
only("Freestyle exec", o, ["command", "linuxUser", "timeoutMs", "env", "stdin"]);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
822
|
+
const user = o.linuxUser === undefined ? undefined : validateLinuxUser(o.linuxUser);
|
|
823
|
+
return api(async () => {
|
|
824
|
+
const s = await lookup(this.runtime, this.id);
|
|
825
|
+
const native = await execOptions(s, {
|
|
826
|
+
onStdout: () => undefined,
|
|
827
|
+
onStderr: () => undefined,
|
|
828
|
+
timeoutMs: o.timeoutMs,
|
|
829
|
+
env: o.env,
|
|
830
|
+
stdin: o.stdin === undefined
|
|
831
|
+
? undefined
|
|
832
|
+
: Uint8Array.from(atob(o.stdin), (c) => c.charCodeAt(0)),
|
|
833
|
+
});
|
|
834
|
+
const r = user
|
|
835
|
+
? await s.exec(asUser(user, o.command, native.env), native)
|
|
836
|
+
: await s.exec(o.command, native);
|
|
837
|
+
return { stdout: r.stdout, stderr: r.stderr, statusCode: r.timedOut ? null : r.exitCode };
|
|
838
|
+
});
|
|
328
839
|
}
|
|
329
840
|
async snapshot(options = {}) {
|
|
330
841
|
only("Freestyle snapshot", options, ["slug", "displayName", "ttlSeconds"]);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
options.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
retentionDays: options.ttlSeconds === undefined ? undefined : options.ttlSeconds / 86400,
|
|
842
|
+
const days = retentionDays(options.ttlSeconds);
|
|
843
|
+
return api(async () => {
|
|
844
|
+
const s = await (await lookup(this.runtime, this.id)).snapshot({
|
|
845
|
+
name: options.slug,
|
|
846
|
+
labels: options.displayName ? { "fs.displayName": options.displayName } : undefined,
|
|
847
|
+
retentionDays: days,
|
|
848
|
+
});
|
|
849
|
+
return { snapshotId: s.id, sourceVmId: s.sourceSandboxId, snapshot: snapshotData(s) };
|
|
340
850
|
});
|
|
341
|
-
return { snapshotId: s.id, sourceVmId: s.sourceSandboxId, snapshot: snapshotData(s) };
|
|
342
851
|
}
|
|
343
852
|
}
|
|
853
|
+
refuse("Freestyle", "Vm", Vm, {
|
|
854
|
+
resize: "Runtime resizes by restarting, which ends running processes: call sandbox.resize() on the native client",
|
|
855
|
+
});
|