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.
Files changed (171) hide show
  1. package/BLAXEL.md +2 -2
  2. package/COMPATIBILITY.md +74 -9
  3. package/DAYTONA.md +33 -5
  4. package/E2B.md +85 -24
  5. package/README.md +11 -10
  6. package/README.src.md +10 -9
  7. package/VERCEL.md +43 -5
  8. package/compatibility-lock.json +58 -49
  9. package/dist/billing-cli.d.ts +1 -1
  10. package/dist/billing-cli.js +85 -5
  11. package/dist/blaxel/errors.d.ts +14 -0
  12. package/dist/blaxel/errors.js +29 -0
  13. package/dist/blaxel/index.d.ts +2 -2
  14. package/dist/blaxel/index.js +4 -4
  15. package/dist/blaxel/interpreter.d.ts +2 -5
  16. package/dist/blaxel/sandbox.d.ts +15 -6
  17. package/dist/blaxel/sandbox.js +64 -8
  18. package/dist/blaxel/snapshot.d.ts +4 -0
  19. package/dist/blaxel/snapshot.js +4 -0
  20. package/dist/cli/logs.d.ts +21 -0
  21. package/dist/cli/logs.js +188 -0
  22. package/dist/cli-extras.d.ts +3 -3
  23. package/dist/cli-extras.js +17 -12
  24. package/dist/cli-words.d.ts +10 -1
  25. package/dist/cli-words.js +18 -0
  26. package/dist/cli.d.ts +22 -3
  27. package/dist/cli.js +647 -273
  28. package/dist/client.d.ts +2 -4
  29. package/dist/client.js +27 -11
  30. package/dist/cloudflare/errors.d.ts +143 -0
  31. package/dist/cloudflare/errors.js +167 -0
  32. package/dist/cloudflare/index.d.ts +175 -6
  33. package/dist/cloudflare/index.js +416 -26
  34. package/dist/cloudflare/interpreter.d.ts +119 -0
  35. package/dist/cloudflare/interpreter.js +223 -0
  36. package/dist/cloudflare/streams.d.ts +38 -0
  37. package/dist/cloudflare/streams.js +145 -0
  38. package/dist/cloudflare/watch.d.ts +9 -0
  39. package/dist/cloudflare/watch.js +24 -0
  40. package/dist/codesandbox/index.d.ts +216 -28
  41. package/dist/codesandbox/index.js +478 -78
  42. package/dist/compat/core.d.ts +18 -0
  43. package/dist/compat/core.js +29 -0
  44. package/dist/compat/files.d.ts +1 -0
  45. package/dist/compat/files.js +25 -5
  46. package/dist/credentials.d.ts +3 -0
  47. package/dist/credentials.js +15 -0
  48. package/dist/daytona/computer-use.d.ts +13 -4
  49. package/dist/daytona/computer-use.js +66 -8
  50. package/dist/daytona/daytona.d.ts +7 -1
  51. package/dist/daytona/daytona.js +31 -4
  52. package/dist/daytona/errors.d.ts +11 -0
  53. package/dist/daytona/errors.js +18 -0
  54. package/dist/daytona/filesystem.js +2 -1
  55. package/dist/daytona/image.d.ts +13 -9
  56. package/dist/daytona/image.js +18 -12
  57. package/dist/daytona/index.d.ts +1 -1
  58. package/dist/daytona/sandbox.d.ts +8 -2
  59. package/dist/daytona/sandbox.js +16 -2
  60. package/dist/domains/index.d.ts +9 -0
  61. package/dist/domains/index.js +10 -0
  62. package/dist/e2b/commands.d.ts +1 -1
  63. package/dist/e2b/commands.js +6 -7
  64. package/dist/e2b/errors.d.ts +17 -0
  65. package/dist/e2b/errors.js +40 -1
  66. package/dist/e2b/index.d.ts +2 -2
  67. package/dist/e2b/index.js +1 -1
  68. package/dist/e2b/sandbox.d.ts +28 -22
  69. package/dist/e2b/sandbox.js +34 -8
  70. package/dist/e2b/unsupported.js +1 -1
  71. package/dist/errors.d.ts +11 -2
  72. package/dist/errors.js +17 -5
  73. package/dist/freestyle/index.d.ts +285 -37
  74. package/dist/freestyle/index.js +656 -144
  75. package/dist/identity.d.ts +3 -0
  76. package/dist/identity.js +27 -0
  77. package/dist/images/index.d.ts +9 -0
  78. package/dist/images/index.js +10 -0
  79. package/dist/index.d.ts +3 -1
  80. package/dist/ips/index.d.ts +9 -0
  81. package/dist/ips/index.js +10 -0
  82. package/dist/jobs/index.d.ts +9 -0
  83. package/dist/jobs/index.js +10 -0
  84. package/dist/keys.js +2 -0
  85. package/dist/login.js +2 -0
  86. package/dist/machine.d.ts +27 -4
  87. package/dist/machine.js +46 -14
  88. package/dist/mcp.js +144 -20
  89. package/dist/modal/index.d.ts +339 -61
  90. package/dist/modal/index.js +781 -121
  91. package/dist/network-cli.d.ts +3 -1
  92. package/dist/network-cli.js +78 -36
  93. package/dist/networks/index.d.ts +11 -0
  94. package/dist/networks/index.js +10 -0
  95. package/dist/ports/index.d.ts +9 -0
  96. package/dist/ports/index.js +10 -0
  97. package/dist/product-names.d.ts +52 -0
  98. package/dist/product-names.js +164 -0
  99. package/dist/product-renames.d.ts +103 -0
  100. package/dist/product-renames.js +189 -0
  101. package/dist/products/api-keys.d.ts +73 -0
  102. package/dist/products/api-keys.js +23 -0
  103. package/dist/products/apps.d.ts +157 -0
  104. package/dist/products/apps.js +60 -0
  105. package/dist/products/balancers.d.ts +87 -0
  106. package/dist/products/balancers.js +8 -0
  107. package/dist/products/billing.d.ts +41 -0
  108. package/dist/products/billing.js +13 -0
  109. package/dist/products/browser.d.ts +10 -0
  110. package/dist/products/browsers.d.ts +71 -0
  111. package/dist/products/browsers.js +102 -0
  112. package/dist/products/builds.d.ts +156 -0
  113. package/dist/products/builds.js +227 -0
  114. package/dist/products/index.d.ts +5 -16
  115. package/dist/products/index.js +5 -14
  116. package/dist/products/jobs.d.ts +19 -2
  117. package/dist/products/jobs.js +28 -11
  118. package/dist/products/logs.d.ts +245 -0
  119. package/dist/products/logs.js +274 -0
  120. package/dist/products/network-products.d.ts +38 -7
  121. package/dist/products/network-products.js +31 -18
  122. package/dist/products/observability.d.ts +8 -2
  123. package/dist/products/private-network.js +1 -1
  124. package/dist/products/queues.d.ts +149 -0
  125. package/dist/products/queues.js +213 -0
  126. package/dist/products/referrals.d.ts +21 -7
  127. package/dist/products/referrals.js +3 -3
  128. package/dist/products/resources.d.ts +54 -0
  129. package/dist/products/resources.js +122 -0
  130. package/dist/products/secrets.d.ts +29 -3
  131. package/dist/products/secrets.js +47 -18
  132. package/dist/products/signed-requests.d.ts +21 -0
  133. package/dist/products/signed-requests.js +11 -0
  134. package/dist/products/streams.d.ts +73 -0
  135. package/dist/products/streams.js +17 -0
  136. package/dist/products/switching.d.ts +35 -7
  137. package/dist/products/switching.js +4 -3
  138. package/dist/products/volumes.d.ts +6 -0
  139. package/dist/record-cli.d.ts +23 -0
  140. package/dist/record-cli.js +308 -0
  141. package/dist/renamed-products.d.ts +38 -0
  142. package/dist/renamed-products.js +34 -0
  143. package/dist/runloop/index.d.ts +9 -2
  144. package/dist/runloop/index.js +29 -8
  145. package/dist/runloop/sdk.d.ts +131 -0
  146. package/dist/runloop/sdk.js +167 -0
  147. package/dist/sandbox.d.ts +7 -2
  148. package/dist/sandbox.js +41 -11
  149. package/dist/sandboxes/index.d.ts +11 -0
  150. package/dist/sandboxes/index.js +12 -0
  151. package/dist/snapshots/index.d.ts +9 -0
  152. package/dist/snapshots/index.js +11 -0
  153. package/dist/snapshots.d.ts +7 -4
  154. package/dist/sprites/index.d.ts +343 -31
  155. package/dist/sprites/index.js +866 -84
  156. package/dist/ssh.d.ts +1 -1
  157. package/dist/ssh.js +11 -11
  158. package/dist/terminal.d.ts +24 -0
  159. package/dist/terminal.js +32 -0
  160. package/dist/transport.d.ts +18 -1
  161. package/dist/transport.js +80 -6
  162. package/dist/tunnel.js +1 -1
  163. package/dist/types.d.ts +71 -4
  164. package/dist/vercel/sandbox.d.ts +1 -1
  165. package/dist/vercel/sandbox.js +6 -4
  166. package/dist/vercel/snapshot.js +10 -2
  167. package/dist/volumes/index.d.ts +9 -0
  168. package/dist/volumes/index.js +10 -0
  169. package/dist/wallet.d.ts +4 -1
  170. package/dist/wallet.js +2 -1
  171. package/package.json +48 -14
@@ -1,11 +1,195 @@
1
- import { all, client, CompatibilityError, create, destroy, execOptions, lookup, only, ready, } from "../compat/core.js";
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.labels?.["fs.displayName"] ?? null,
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
- this.vms = new VmsNamespace(client(options));
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
- export class VmsNamespace {
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
- async resolveSnapshot(id) {
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
- for (const r of options.firewall.rules) {
96
- only("Freestyle firewall rule", r, ["action", "source", "destination"]);
97
- if (r.action !== "allow" ||
98
- Object.keys(r.source).length ||
99
- Object.keys(r.destination).length !== 1 ||
100
- r.destination.public !== true)
101
- throw new CompatibilityError("Freestyle", "firewall rules other than unrestricted outbound public access");
102
- }
103
- const snapshot = options.snapshotId
104
- ? (await this.resolveSnapshot(options.snapshotId)).id
105
- : undefined;
106
- const s = await create(this.runtime, "freestyle", {
107
- snapshot,
108
- name: options.slug ?? undefined,
109
- labels: { ...options.metadata, "fs.displayName": options.displayName ?? "" },
110
- persistent: true,
111
- idlePauseSeconds: options.idleTimeoutSeconds === -1 || options.idleTimeoutSeconds == null
112
- ? 0
113
- : options.idleTimeoutSeconds,
114
- network: { internet: options.firewall.rules.length > 0 },
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
- async get(id) {
132
- return data(await lookup(this.runtime, id));
373
+ get(id) {
374
+ return api(async () => data(await lookup(this.runtime, id)));
133
375
  }
134
- async delete(id) {
135
- await destroy(await lookup(this.runtime, id));
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
- return new Uint8Array(await new Response(await this.readFileStream(path, options)).arrayBuffer());
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
- let mode = options.mode;
235
- if (mode === undefined)
236
- mode = (await this.files.exists(path))
237
- ? parseInt((await this.files.stat(path)).mode, 8)
238
- : 0o600;
239
- await this.files.writeFile(path, bytes, { mode, signal: options.signal });
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
- async readDir(path) {
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
- async stat(path) {
258
- const e = await this.files.stat(path);
259
- const s = await this.get();
260
- const r = await s.exec(["stat", "-c", "%U:%G", "--", this.files.path(path)], { check: true });
261
- const [owner, group] = r.stdout.trim().split(":");
262
- return {
263
- size: e.size,
264
- isFile: e.type === "file",
265
- isDirectory: e.type === "directory",
266
- isSymlink: e.type === "symlink",
267
- permissions: e.mode,
268
- owner: owner,
269
- group: group,
270
- modified: e.modifiedAt,
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
- async data() {
284
- return data(await lookup(this.runtime, this.id));
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
- async start() {
287
- return data(await ready(await lookup(this.runtime, this.id)));
774
+ delete() {
775
+ return api(async () => destroy(await lookup(this.runtime, this.id)));
288
776
  }
289
- async pause() {
290
- const s = await lookup(this.runtime, this.id);
291
- await s.pause();
292
- return data(s);
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
- async delete() {
295
- await destroy(await lookup(this.runtime, this.id));
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
- const s = await lookup(this.runtime, this.id);
301
- await s.update({
302
- name: options.slug,
303
- idlePauseSeconds: options.idleTimeoutSeconds === -1 ? 0 : options.idleTimeoutSeconds,
304
- labels: {
305
- ...s.info.labels,
306
- ...options.metadata,
307
- ...(options.displayName === undefined ? {} : { "fs.displayName": options.displayName }),
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
- if (o.linuxUser)
316
- throw new CompatibilityError("Freestyle", "exec as a different Linux user");
317
- const s = await lookup(this.runtime, this.id);
318
- const r = await s.exec(o.command, await execOptions(s, {
319
- onStdout: () => undefined,
320
- onStderr: () => undefined,
321
- timeoutMs: o.timeoutMs,
322
- env: o.env,
323
- stdin: o.stdin === undefined
324
- ? undefined
325
- : Uint8Array.from(atob(o.stdin), (c) => c.charCodeAt(0)),
326
- }));
327
- return { stdout: r.stdout, stderr: r.stderr, statusCode: r.timedOut ? null : r.exitCode };
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
- if (options.ttlSeconds !== undefined &&
332
- (options.ttlSeconds <= 0 ||
333
- options.ttlSeconds > 365 * 86400 ||
334
- options.ttlSeconds % 86400 !== 0))
335
- throw new CompatibilityError("Freestyle", "snapshot retention outside 1 to 365 whole days");
336
- const s = await (await lookup(this.runtime, this.id)).snapshot({
337
- name: options.slug,
338
- labels: options.displayName ? { "fs.displayName": options.displayName } : undefined,
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
+ });