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/sprites/index.js
CHANGED
|
@@ -1,9 +1,244 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { PassThrough, Writable } from "node:stream";
|
|
3
|
-
import { RuntimeError } from "../errors.js";
|
|
4
|
-
import {
|
|
3
|
+
import { NotFoundError, RuntimeError } from "../errors.js";
|
|
4
|
+
import { timeoutSignal } from "../transport.js";
|
|
5
|
+
import { all, client, CompatibilityError, create, destroy, execOptions, lookup, only, ready, refuse, region, } from "../compat/core.js";
|
|
5
6
|
import { GuestFiles } from "../compat/files.js";
|
|
6
7
|
export { CompatibilityError };
|
|
8
|
+
export const ERR_CODE_CREATION_RATE_LIMITED = "sprite_creation_rate_limited";
|
|
9
|
+
export const ERR_CODE_CONCURRENT_LIMIT_EXCEEDED = "concurrent_sprite_limit_exceeded";
|
|
10
|
+
/** A Sprites progress or event stream: the work behind it runs whether or
|
|
11
|
+
* not anyone reads, as the vendor's server-side operation does; `next()`
|
|
12
|
+
* answers null at the end, `close()` stops reading (and stops a watch). */
|
|
13
|
+
class Feed {
|
|
14
|
+
#items = [];
|
|
15
|
+
#waiters = [];
|
|
16
|
+
#ended = false;
|
|
17
|
+
#closed = false;
|
|
18
|
+
#error;
|
|
19
|
+
constructor(source) {
|
|
20
|
+
if (source)
|
|
21
|
+
void (async () => {
|
|
22
|
+
try {
|
|
23
|
+
for await (const item of source)
|
|
24
|
+
this.push(item);
|
|
25
|
+
this.end();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
this.end(error);
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
32
|
+
push(item) {
|
|
33
|
+
if (this.#ended || this.#closed)
|
|
34
|
+
return;
|
|
35
|
+
this.#items.push(item);
|
|
36
|
+
this.#wake();
|
|
37
|
+
}
|
|
38
|
+
end(error) {
|
|
39
|
+
if (this.#ended)
|
|
40
|
+
return;
|
|
41
|
+
this.#ended = true;
|
|
42
|
+
this.#error = error;
|
|
43
|
+
this.#wake();
|
|
44
|
+
}
|
|
45
|
+
/** Called once by close(); a watch stops its native watch here. */
|
|
46
|
+
stop() { }
|
|
47
|
+
#wake() {
|
|
48
|
+
for (const waiter of this.#waiters.splice(0))
|
|
49
|
+
waiter();
|
|
50
|
+
}
|
|
51
|
+
async next() {
|
|
52
|
+
while (!this.#items.length && !this.#ended && !this.#closed)
|
|
53
|
+
await new Promise((resolve) => this.#waiters.push(resolve));
|
|
54
|
+
if (this.#closed)
|
|
55
|
+
return null;
|
|
56
|
+
if (this.#items.length)
|
|
57
|
+
return this.#items.shift();
|
|
58
|
+
if (this.#error !== undefined) {
|
|
59
|
+
const error = this.#error;
|
|
60
|
+
this.#error = undefined;
|
|
61
|
+
throw error instanceof Error ? error : new Error("The stream failed", { cause: error });
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
async processAll(handler) {
|
|
66
|
+
for await (const item of this)
|
|
67
|
+
await handler(item);
|
|
68
|
+
}
|
|
69
|
+
close() {
|
|
70
|
+
if (this.#closed)
|
|
71
|
+
return;
|
|
72
|
+
this.#closed = true;
|
|
73
|
+
this.stop();
|
|
74
|
+
this.#wake();
|
|
75
|
+
}
|
|
76
|
+
isClosed() {
|
|
77
|
+
return this.#closed || (this.#ended && !this.#items.length);
|
|
78
|
+
}
|
|
79
|
+
async *[Symbol.asyncIterator]() {
|
|
80
|
+
let item;
|
|
81
|
+
while ((item = await this.next()) !== null)
|
|
82
|
+
yield item;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class CheckpointStream extends Feed {
|
|
86
|
+
}
|
|
87
|
+
export class RestoreStream extends Feed {
|
|
88
|
+
}
|
|
89
|
+
export class ServiceLogStream extends Feed {
|
|
90
|
+
}
|
|
91
|
+
export class SessionKillStream extends Feed {
|
|
92
|
+
}
|
|
93
|
+
export class SpriteListStream extends Feed {
|
|
94
|
+
}
|
|
95
|
+
export class APIError extends Error {
|
|
96
|
+
errorCode;
|
|
97
|
+
statusCode;
|
|
98
|
+
limit;
|
|
99
|
+
windowSeconds;
|
|
100
|
+
retryAfterSeconds;
|
|
101
|
+
currentCount;
|
|
102
|
+
upgradeAvailable;
|
|
103
|
+
upgradeUrl;
|
|
104
|
+
retryAfterHeader;
|
|
105
|
+
rateLimitLimit;
|
|
106
|
+
rateLimitRemaining;
|
|
107
|
+
rateLimitReset;
|
|
108
|
+
constructor(message, options = {}) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "APIError";
|
|
111
|
+
Object.assign(this, options);
|
|
112
|
+
}
|
|
113
|
+
isRateLimitError() {
|
|
114
|
+
return this.statusCode === 429;
|
|
115
|
+
}
|
|
116
|
+
isCreationRateLimited() {
|
|
117
|
+
return this.errorCode === ERR_CODE_CREATION_RATE_LIMITED;
|
|
118
|
+
}
|
|
119
|
+
isConcurrentLimitExceeded() {
|
|
120
|
+
return this.errorCode === ERR_CODE_CONCURRENT_LIMIT_EXCEEDED;
|
|
121
|
+
}
|
|
122
|
+
getRetryAfterSeconds() {
|
|
123
|
+
if (this.retryAfterSeconds !== undefined && this.retryAfterSeconds > 0)
|
|
124
|
+
return this.retryAfterSeconds;
|
|
125
|
+
return this.retryAfterHeader;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** The vendor's parser, for callers that read Sprites-shaped answers themselves. */
|
|
129
|
+
export function parseAPIError(statusCode, body, headers = {}) {
|
|
130
|
+
if (statusCode < 400)
|
|
131
|
+
return undefined;
|
|
132
|
+
const header = (name) => {
|
|
133
|
+
const value = headers[name.toLowerCase()] ?? headers[name];
|
|
134
|
+
const number = value ? parseInt(value, 10) : NaN;
|
|
135
|
+
return Number.isNaN(number) ? undefined : number;
|
|
136
|
+
};
|
|
137
|
+
const retryAfterHeader = header("Retry-After");
|
|
138
|
+
let message = "";
|
|
139
|
+
let data = {};
|
|
140
|
+
if (body)
|
|
141
|
+
try {
|
|
142
|
+
data = JSON.parse(body);
|
|
143
|
+
message = data.message || "";
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
message = body;
|
|
147
|
+
}
|
|
148
|
+
const errorCode = data.error;
|
|
149
|
+
const limit = data.limit;
|
|
150
|
+
const windowSeconds = data.window_seconds;
|
|
151
|
+
const retryAfterSeconds = data.retry_after_seconds;
|
|
152
|
+
const currentCount = data.current_count;
|
|
153
|
+
const upgradeUrl = data.upgrade_url;
|
|
154
|
+
if (statusCode === 429 && message) {
|
|
155
|
+
const details = [];
|
|
156
|
+
if (limit !== undefined && windowSeconds !== undefined)
|
|
157
|
+
details.push(`limit: ${limit} per ${windowSeconds}s`);
|
|
158
|
+
if (currentCount !== undefined)
|
|
159
|
+
details.push(`current: ${currentCount}`);
|
|
160
|
+
const retry = retryAfterSeconds ?? retryAfterHeader;
|
|
161
|
+
if (retry !== undefined)
|
|
162
|
+
details.push(`retry after: ${retry}s`);
|
|
163
|
+
if (upgradeUrl)
|
|
164
|
+
details.push(`upgrade: ${upgradeUrl}`);
|
|
165
|
+
if (details.length)
|
|
166
|
+
message = `${message} (${details.join(", ")})`;
|
|
167
|
+
}
|
|
168
|
+
return new APIError(message || errorCode || `API error (status ${statusCode})`, {
|
|
169
|
+
errorCode,
|
|
170
|
+
statusCode,
|
|
171
|
+
limit,
|
|
172
|
+
windowSeconds,
|
|
173
|
+
retryAfterSeconds,
|
|
174
|
+
currentCount,
|
|
175
|
+
upgradeAvailable: data.upgrade_available,
|
|
176
|
+
upgradeUrl,
|
|
177
|
+
retryAfterHeader,
|
|
178
|
+
rateLimitLimit: header("X-RateLimit-Limit"),
|
|
179
|
+
rateLimitRemaining: header("X-RateLimit-Remaining"),
|
|
180
|
+
rateLimitReset: header("X-RateLimit-Reset"),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** Runtime's refusal as the APIError the vendor's client throws; limits on
|
|
184
|
+
* running sandboxes are its concurrent limit, the API's rate limit on a
|
|
185
|
+
* create its creation limit. */
|
|
186
|
+
async function api(run, creating = false) {
|
|
187
|
+
try {
|
|
188
|
+
return await run();
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (!(error instanceof RuntimeError) || error.status < 400)
|
|
192
|
+
throw error;
|
|
193
|
+
const errorCode = ["no_credit_running_limit", "quota_exceeded"].includes(error.code)
|
|
194
|
+
? ERR_CODE_CONCURRENT_LIMIT_EXCEEDED
|
|
195
|
+
: creating && error.code === "rate_limited"
|
|
196
|
+
? ERR_CODE_CREATION_RATE_LIMITED
|
|
197
|
+
: error.code;
|
|
198
|
+
const retry = error.retryAfterMs === undefined ? undefined : Math.ceil(error.retryAfterMs / 1000);
|
|
199
|
+
throw Object.assign(new APIError(error.message, {
|
|
200
|
+
errorCode,
|
|
201
|
+
statusCode: error.status,
|
|
202
|
+
retryAfterSeconds: retry,
|
|
203
|
+
retryAfterHeader: retry,
|
|
204
|
+
}), { cause: error });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** "30s", "500ms", "2m" or "1h", as Sprites' durations are written. */
|
|
208
|
+
function duration(value, name) {
|
|
209
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/.exec(value);
|
|
210
|
+
if (!match)
|
|
211
|
+
throw new TypeError(`${name} must be a duration such as "10s"`);
|
|
212
|
+
return Number(match[1]) * { ms: 1, s: 1000, m: 60_000, h: 3_600_000 }[match[2]];
|
|
213
|
+
}
|
|
214
|
+
const SIGNALS = [
|
|
215
|
+
"SIGTERM",
|
|
216
|
+
"SIGKILL",
|
|
217
|
+
"SIGINT",
|
|
218
|
+
"SIGHUP",
|
|
219
|
+
"SIGQUIT",
|
|
220
|
+
"SIGUSR1",
|
|
221
|
+
"SIGUSR2",
|
|
222
|
+
];
|
|
223
|
+
function signalName(signal) {
|
|
224
|
+
const name = (signal.startsWith("SIG") ? signal : `SIG${signal}`);
|
|
225
|
+
if (!SIGNALS.includes(name))
|
|
226
|
+
throw new TypeError(`Unsupported signal ${signal}`);
|
|
227
|
+
return name;
|
|
228
|
+
}
|
|
229
|
+
/** maxRunAfterDisconnect is left out: a Runtime process runs until it ends
|
|
230
|
+
* or is killed, whoever is connected. */
|
|
231
|
+
const SPAWN_KEYS = [
|
|
232
|
+
"cwd",
|
|
233
|
+
"env",
|
|
234
|
+
"tty",
|
|
235
|
+
"rows",
|
|
236
|
+
"cols",
|
|
237
|
+
"detachable",
|
|
238
|
+
"sessionId",
|
|
239
|
+
"controlMode",
|
|
240
|
+
];
|
|
241
|
+
const EXEC_KEYS = [...SPAWN_KEYS, "encoding", "maxBuffer", "signal", "timeout"];
|
|
7
242
|
function validateURLSettings(settings) {
|
|
8
243
|
only("Sprites URL settings", settings, ["auth"]);
|
|
9
244
|
if (settings.auth !== "public" && settings.auth !== "sprite")
|
|
@@ -48,19 +283,58 @@ async function fileOperation(path, syscall, run) {
|
|
|
48
283
|
}
|
|
49
284
|
}
|
|
50
285
|
export class ExecError extends Error {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
stderr;
|
|
54
|
-
constructor(message, exitCode, stdout, stderr) {
|
|
286
|
+
result;
|
|
287
|
+
constructor(message, result) {
|
|
55
288
|
super(message);
|
|
56
|
-
this.
|
|
57
|
-
this.stdout = stdout;
|
|
58
|
-
this.stderr = stderr;
|
|
289
|
+
this.result = result;
|
|
59
290
|
this.name = "ExecError";
|
|
60
291
|
}
|
|
292
|
+
get exitCode() {
|
|
293
|
+
return this.result.exitCode;
|
|
294
|
+
}
|
|
295
|
+
get stdout() {
|
|
296
|
+
return this.result.stdout;
|
|
297
|
+
}
|
|
298
|
+
get stderr() {
|
|
299
|
+
return this.result.stderr;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/** The vendor's control-mode wire protocol: multiplexed operations over one
|
|
303
|
+
* WebSocket. Runtime's own API multiplexes commands without it. */
|
|
304
|
+
const CONTROL = "Runtime runs each command over its own API; use spawn, exec or execFile";
|
|
305
|
+
export class OpConn extends EventEmitter {
|
|
61
306
|
}
|
|
307
|
+
refuse("Sprites", "OpConn", OpConn, Object.fromEntries([
|
|
308
|
+
"write",
|
|
309
|
+
"sendEOF",
|
|
310
|
+
"resize",
|
|
311
|
+
"signal",
|
|
312
|
+
"handleData",
|
|
313
|
+
"handleText",
|
|
314
|
+
"complete",
|
|
315
|
+
"close",
|
|
316
|
+
"isClosed",
|
|
317
|
+
"getExitCode",
|
|
318
|
+
].map((name) => [name, CONTROL])), { sync: true });
|
|
319
|
+
refuse("Sprites", "OpConn", OpConn, { wait: CONTROL });
|
|
320
|
+
export class ControlConnection extends EventEmitter {
|
|
321
|
+
}
|
|
322
|
+
refuse("Sprites", "ControlConnection", ControlConnection, Object.fromEntries(["isActive", "setActive", "clearOpConn", "sendData", "close", "isClosed"].map((name) => [
|
|
323
|
+
name,
|
|
324
|
+
CONTROL,
|
|
325
|
+
])), { sync: true });
|
|
326
|
+
refuse("Sprites", "ControlConnection", ControlConnection, {
|
|
327
|
+
connect: CONTROL,
|
|
328
|
+
startOp: CONTROL,
|
|
329
|
+
});
|
|
62
330
|
export class SpritesClient {
|
|
63
331
|
runtime;
|
|
332
|
+
/** The Runtime API the calls go to. */
|
|
333
|
+
baseURL;
|
|
334
|
+
/** The Runtime key given, if any; otherwise RUNTIME_API_KEY or the saved login is used. */
|
|
335
|
+
token;
|
|
336
|
+
/** Always false: true is refused (control mode). */
|
|
337
|
+
controlMode = false;
|
|
64
338
|
constructor(token, options = {}) {
|
|
65
339
|
only("Sprites client", options, [
|
|
66
340
|
"apiKey",
|
|
@@ -82,6 +356,8 @@ export class SpritesClient {
|
|
|
82
356
|
baseUrl: options.baseUrl ?? options.baseURL,
|
|
83
357
|
timeoutMs: options.timeoutMs ?? options.timeout,
|
|
84
358
|
});
|
|
359
|
+
this.baseURL = this.runtime.transport?.baseUrl ?? options.baseUrl ?? options.baseURL ?? "";
|
|
360
|
+
this.token = options.apiKey ?? token ?? "";
|
|
85
361
|
}
|
|
86
362
|
sprite(name) {
|
|
87
363
|
return new Sprite(name, this);
|
|
@@ -105,7 +381,7 @@ export class SpritesClient {
|
|
|
105
381
|
validateLabels(o.labels);
|
|
106
382
|
const c = o.config ?? {};
|
|
107
383
|
only("Sprites config", c, ["ramMB", "cpus", "region", "storageGB"]);
|
|
108
|
-
const s = await create(this.runtime, "sprites", {
|
|
384
|
+
const s = await api(() => create(this.runtime, "sprites", {
|
|
109
385
|
name,
|
|
110
386
|
persistent: true,
|
|
111
387
|
vcpu: c.cpus,
|
|
@@ -113,9 +389,10 @@ export class SpritesClient {
|
|
|
113
389
|
diskMiB: c.storageGB === undefined ? undefined : Math.ceil((c.storageGB * 1e9) / 1048576),
|
|
114
390
|
region: region("Sprites", c.region),
|
|
115
391
|
labels: { "compat.labels": JSON.stringify(o.labels ?? []) },
|
|
116
|
-
}, o.environment, undefined, o.waitForCapacity === false ? { waitForCapacityMs: 0 } : {});
|
|
392
|
+
}, o.environment, undefined, o.waitForCapacity === false ? { waitForCapacityMs: 0 } : {}), true);
|
|
117
393
|
const sprite = new Sprite(name, this);
|
|
118
394
|
sprite.hydrate(s);
|
|
395
|
+
sprite.environment = { ...o.environment };
|
|
119
396
|
if (o.urlSettings) {
|
|
120
397
|
try {
|
|
121
398
|
await sprite.updateURLSettings(o.urlSettings);
|
|
@@ -129,15 +406,15 @@ export class SpritesClient {
|
|
|
129
406
|
}
|
|
130
407
|
async getSprite(name) {
|
|
131
408
|
const sprite = this.sprite(name);
|
|
132
|
-
sprite.hydrate(await lookup(this.runtime, name));
|
|
409
|
+
sprite.hydrate(await api(() => lookup(this.runtime, name)));
|
|
133
410
|
return sprite;
|
|
134
411
|
}
|
|
135
412
|
async deleteSprite(name) {
|
|
136
|
-
await destroy(await lookup(this.runtime, name));
|
|
413
|
+
await api(async () => destroy(await lookup(this.runtime, name)));
|
|
137
414
|
}
|
|
138
415
|
async listSprites(options = {}) {
|
|
139
416
|
only("Sprites list", options, ["prefix", "maxResults", "continuationToken", "bulkLoad"]);
|
|
140
|
-
const values = (await all(this.runtime, "sprites")).filter((s) => (s.info.name ?? s.id).startsWith(options.prefix ?? ""));
|
|
417
|
+
const values = (await api(() => all(this.runtime, "sprites"))).filter((s) => (s.info.name ?? s.id).startsWith(options.prefix ?? ""));
|
|
141
418
|
const offset = Number(options.continuationToken ?? 0), limit = options.maxResults ?? 100;
|
|
142
419
|
return {
|
|
143
420
|
sprites: values.slice(offset, offset + limit).map((s) => ({
|
|
@@ -158,7 +435,7 @@ export class SpritesClient {
|
|
|
158
435
|
};
|
|
159
436
|
}
|
|
160
437
|
async listAllSprites(prefix) {
|
|
161
|
-
return (await all(this.runtime, "sprites"))
|
|
438
|
+
return (await api(() => all(this.runtime, "sprites")))
|
|
162
439
|
.filter((s) => (s.info.name ?? s.id).startsWith(prefix ?? ""))
|
|
163
440
|
.map((s) => {
|
|
164
441
|
const sprite = this.sprite(s.info.name ?? s.id);
|
|
@@ -167,25 +444,65 @@ export class SpritesClient {
|
|
|
167
444
|
});
|
|
168
445
|
}
|
|
169
446
|
async restartSprite(name) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
447
|
+
return api(async () => {
|
|
448
|
+
const s = await lookup(this.runtime, name);
|
|
449
|
+
await s.stop();
|
|
450
|
+
await s.restart();
|
|
451
|
+
return { spriteName: name, machineId: s.id, message: "Restarted" };
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/** Its state as Runtime reports it now; `elapsed` is the lookup's milliseconds. */
|
|
455
|
+
async checkSprite(name) {
|
|
456
|
+
const started = performance.now();
|
|
457
|
+
const s = await api(() => lookup(this.runtime, name));
|
|
458
|
+
return {
|
|
459
|
+
spriteName: name,
|
|
460
|
+
spriteId: s.id,
|
|
461
|
+
status: s.state,
|
|
462
|
+
checkedAt: new Date(),
|
|
463
|
+
elapsed: Math.round(performance.now() - started),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/** Runtime keeps each sandbox's system current itself, so there is nothing
|
|
467
|
+
* to move to: this checks that the Sprite exists, as the vendor's call does. */
|
|
468
|
+
async upgradeSprite(name) {
|
|
469
|
+
await api(() => lookup(this.runtime, name));
|
|
470
|
+
}
|
|
471
|
+
async updateSprite(name, options) {
|
|
472
|
+
return (await this.getSprite(name)).update(options);
|
|
174
473
|
}
|
|
175
474
|
updateURLSettings(name, settings) {
|
|
176
475
|
return this.sprite(name).updateURLSettings(settings);
|
|
177
476
|
}
|
|
178
477
|
}
|
|
478
|
+
refuse("Sprites", "SpritesClient", SpritesClient, {
|
|
479
|
+
watchSprites: "watch sandbox state with runtime.events or signed webhooks",
|
|
480
|
+
});
|
|
481
|
+
refuse("Sprites", "SpritesClient", { prototype: SpritesClient }, {
|
|
482
|
+
createToken: "Fly macaroons are not Runtime credentials; create a Runtime API key in the console",
|
|
483
|
+
});
|
|
179
484
|
export class Sprite {
|
|
180
485
|
name;
|
|
181
486
|
client;
|
|
182
487
|
id;
|
|
488
|
+
/** Runtime does not send the organization's name with a sandbox. */
|
|
489
|
+
organizationName;
|
|
183
490
|
status;
|
|
184
491
|
config;
|
|
492
|
+
/** Set by createSprite; Runtime keeps it in the sandbox, not in its listing. */
|
|
493
|
+
environment;
|
|
185
494
|
createdAt;
|
|
186
495
|
updatedAt;
|
|
187
496
|
url;
|
|
497
|
+
urlSettings;
|
|
498
|
+
/** Vendor storage and version fields; Runtime has no equivalent to report. */
|
|
499
|
+
bucketName;
|
|
500
|
+
primaryRegion;
|
|
501
|
+
version;
|
|
502
|
+
environmentVersion;
|
|
188
503
|
labels = [];
|
|
504
|
+
lastRunningAt;
|
|
505
|
+
lastWarmingAt;
|
|
189
506
|
constructor(name, client) {
|
|
190
507
|
this.name = name;
|
|
191
508
|
this.client = client;
|
|
@@ -199,6 +516,7 @@ export class Sprite {
|
|
|
199
516
|
region: s.info.region,
|
|
200
517
|
storageGB: (s.info.diskMiB * 1048576) / 1e9,
|
|
201
518
|
};
|
|
519
|
+
this.primaryRegion = s.info.region;
|
|
202
520
|
this.createdAt = new Date(s.info.createdAt);
|
|
203
521
|
this.updatedAt = this.createdAt;
|
|
204
522
|
this.labels = JSON.parse(s.info.labels["compat.labels"] ?? "[]");
|
|
@@ -209,36 +527,49 @@ export class Sprite {
|
|
|
209
527
|
return s;
|
|
210
528
|
}
|
|
211
529
|
spawn(command, args = [], options = {}) {
|
|
530
|
+
only("Sprites spawn", options, EXEC_KEYS);
|
|
212
531
|
return new SpriteCommand(this, command, args, options);
|
|
213
532
|
}
|
|
214
|
-
exec(command, options) {
|
|
533
|
+
exec(command, options = {}) {
|
|
534
|
+
only("Sprites exec", options, EXEC_KEYS);
|
|
215
535
|
return this.execFile("bash", ["-c", command], options);
|
|
216
536
|
}
|
|
217
|
-
|
|
218
|
-
options
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
only("Sprites exec", options, [
|
|
537
|
+
execFile(file, args = [], options = {}) {
|
|
538
|
+
only("Sprites exec", options, EXEC_KEYS);
|
|
539
|
+
return this.#execute(file, args, options);
|
|
540
|
+
}
|
|
541
|
+
/** Runtime's commands are HTTP calls already; `input` is written to stdin
|
|
542
|
+
* and stdin closed, and without it stdin is closed at once. */
|
|
543
|
+
execFileHTTP(file, args = [], options = {}) {
|
|
544
|
+
only("Sprites HTTP exec", options, [
|
|
226
545
|
"cwd",
|
|
227
546
|
"env",
|
|
228
|
-
"tty",
|
|
229
|
-
"rows",
|
|
230
|
-
"cols",
|
|
231
|
-
"detachable",
|
|
232
|
-
"sessionId",
|
|
233
|
-
"controlMode",
|
|
234
547
|
"encoding",
|
|
235
548
|
"maxBuffer",
|
|
236
549
|
"signal",
|
|
237
550
|
"timeout",
|
|
551
|
+
"input",
|
|
238
552
|
]);
|
|
553
|
+
const { input, ...rest } = options;
|
|
554
|
+
return this.#execute(file, args, rest, input ?? "");
|
|
555
|
+
}
|
|
556
|
+
async #execute(file, args, options, input) {
|
|
557
|
+
options.signal?.throwIfAborted();
|
|
558
|
+
if (options.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout < 0))
|
|
559
|
+
throw new TypeError("timeout must be a non-negative finite number");
|
|
560
|
+
if (options.encoding &&
|
|
561
|
+
options.encoding !== "buffer" &&
|
|
562
|
+
!Buffer.isEncoding(options.encoding))
|
|
563
|
+
throw new TypeError(`Unknown encoding: ${String(options.encoding)}`);
|
|
239
564
|
if (options.controlMode)
|
|
240
565
|
throw new CompatibilityError("Sprites", "control-mode wire protocol");
|
|
241
566
|
const cmd = this.spawn(file, args, options);
|
|
567
|
+
if (input !== undefined) {
|
|
568
|
+
if (input.length)
|
|
569
|
+
cmd.stdin.end(input);
|
|
570
|
+
else
|
|
571
|
+
cmd.stdin.end();
|
|
572
|
+
}
|
|
242
573
|
const out = [], err = [];
|
|
243
574
|
const maxBuffer = options.maxBuffer || 10 * 1024 * 1024;
|
|
244
575
|
let stdoutBytes = 0;
|
|
@@ -292,7 +623,7 @@ export class Sprite {
|
|
|
292
623
|
exitCode,
|
|
293
624
|
};
|
|
294
625
|
if (exitCode !== 0)
|
|
295
|
-
throw new ExecError(`Command exited with code ${exitCode}`,
|
|
626
|
+
throw new ExecError(`Command exited with code ${exitCode}`, r);
|
|
296
627
|
return r;
|
|
297
628
|
}
|
|
298
629
|
finally {
|
|
@@ -301,15 +632,14 @@ export class Sprite {
|
|
|
301
632
|
options.signal?.removeEventListener("abort", abort);
|
|
302
633
|
}
|
|
303
634
|
}
|
|
304
|
-
execFileHTTP(file, args, options) {
|
|
305
|
-
return this.execFile(file, args, options);
|
|
306
|
-
}
|
|
307
635
|
createSession(command, args = [], options = {}) {
|
|
636
|
+
only("Sprites session", options, SPAWN_KEYS);
|
|
308
637
|
// The pinned SDK's session wrapper forces a terminal, even when a caller
|
|
309
638
|
// supplies tty:false. Attaching an existing session detects its own PTY.
|
|
310
639
|
return this.spawn(command, args, { ...options, detachable: true, tty: true });
|
|
311
640
|
}
|
|
312
641
|
attachSession(sessionId, options = {}) {
|
|
642
|
+
only("Sprites session", options, SPAWN_KEYS);
|
|
313
643
|
return this.spawn("", [], { ...options, sessionId });
|
|
314
644
|
}
|
|
315
645
|
async listSessions() {
|
|
@@ -336,19 +666,149 @@ export class Sprite {
|
|
|
336
666
|
filesystem(workingDir = "/") {
|
|
337
667
|
return new SpriteFilesystem(() => this.native(), workingDir);
|
|
338
668
|
}
|
|
339
|
-
|
|
340
|
-
|
|
669
|
+
/** Ends a session: `signal` (SIGTERM by default; "TERM" works too), then
|
|
670
|
+
* SIGKILL if it has not exited within `timeout` ("10s" by default). */
|
|
671
|
+
async killSession(sessionId, signal = "SIGTERM", timeout = "10s") {
|
|
672
|
+
const name = signalName(signal);
|
|
673
|
+
const ms = duration(timeout, "timeout");
|
|
674
|
+
const process = await (await this.native()).processes.get(sessionId);
|
|
675
|
+
const kill = (async function* () {
|
|
676
|
+
try {
|
|
677
|
+
await process.kill(name);
|
|
678
|
+
yield { type: "signal", signal: name };
|
|
679
|
+
try {
|
|
680
|
+
const result = await process.wait({ signal: timeoutSignal(ms) });
|
|
681
|
+
yield { type: "exited", exitCode: result.exitCode ?? undefined };
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
if (!(error instanceof Error) || !["TimeoutError", "AbortError"].includes(error.name))
|
|
685
|
+
throw error;
|
|
686
|
+
yield { type: "timeout", message: `Still running after ${timeout}` };
|
|
687
|
+
await process.kill("SIGKILL");
|
|
688
|
+
yield { type: "killed", signal: "SIGKILL" };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
if (!(error instanceof RuntimeError))
|
|
693
|
+
throw error;
|
|
694
|
+
yield { type: "error", message: error.message };
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
yield { type: "complete" };
|
|
698
|
+
})();
|
|
699
|
+
return new SessionKillStream(kill);
|
|
700
|
+
}
|
|
701
|
+
/** Ports listening in the Sprite now, then each one opened or closed. */
|
|
702
|
+
async watchPorts() {
|
|
703
|
+
const watcher = new PortWatcher(this.client, this.id ?? this.name);
|
|
704
|
+
await watcher.connect();
|
|
705
|
+
return watcher;
|
|
341
706
|
}
|
|
342
|
-
|
|
343
|
-
|
|
707
|
+
/** Runtime keeps each sandbox's system current itself; this checks it exists. */
|
|
708
|
+
async upgrade() {
|
|
709
|
+
await this.client.upgradeSprite(this.id ?? this.name);
|
|
344
710
|
}
|
|
345
|
-
|
|
346
|
-
|
|
711
|
+
check() {
|
|
712
|
+
return this.client.checkSprite(this.id ?? this.name);
|
|
347
713
|
}
|
|
714
|
+
/** A checkpoint is a Runtime disk snapshot: the files, not the running
|
|
715
|
+
* processes. The Sprite is paused for the moment it takes, then woken.
|
|
716
|
+
* Progress arrives as the vendor's stream; a refusal is an `error` message. */
|
|
717
|
+
async createCheckpoint(comment) {
|
|
718
|
+
if (comment !== undefined && typeof comment !== "string")
|
|
719
|
+
throw new TypeError("comment must be a string");
|
|
720
|
+
const s = await this.native();
|
|
721
|
+
const taken = s.snapshot({
|
|
722
|
+
mode: "disk",
|
|
723
|
+
labels: { "compat.sprite": s.id, ...(comment ? { "compat.comment": comment } : {}) },
|
|
724
|
+
});
|
|
725
|
+
taken.catch(() => undefined);
|
|
726
|
+
return new CheckpointStream((async function* () {
|
|
727
|
+
yield { type: "info", data: "Creating checkpoint" };
|
|
728
|
+
try {
|
|
729
|
+
const snapshot = await taken;
|
|
730
|
+
yield { type: "info", data: `Checkpoint ${snapshot.id} created` };
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
if (!(error instanceof RuntimeError))
|
|
734
|
+
throw error;
|
|
735
|
+
yield { type: "error", error: error.message };
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
yield { type: "complete" };
|
|
739
|
+
})());
|
|
740
|
+
}
|
|
741
|
+
/** This Sprite's disk snapshots, newest first. */
|
|
742
|
+
async listCheckpoints(historyFilter) {
|
|
743
|
+
if (historyFilter !== undefined)
|
|
744
|
+
throw new CompatibilityError("Sprites", "checkpoint history filters");
|
|
745
|
+
const s = await this.native();
|
|
746
|
+
const found = [];
|
|
747
|
+
for await (const snapshot of await this.client.runtime.snapshots.list({
|
|
748
|
+
sandboxId: s.id,
|
|
749
|
+
state: "ready",
|
|
750
|
+
}))
|
|
751
|
+
if (snapshot.mode === "disk" && snapshot.sourceSandboxId === s.id)
|
|
752
|
+
found.push(checkpoint(snapshot));
|
|
753
|
+
return found.sort((a, b) => b.createTime.getTime() - a.createTime.getTime());
|
|
754
|
+
}
|
|
755
|
+
async getCheckpoint(id) {
|
|
756
|
+
const s = await this.native();
|
|
757
|
+
let snapshot;
|
|
758
|
+
try {
|
|
759
|
+
snapshot = await this.client.runtime.snapshots.get(id);
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
if (error instanceof NotFoundError)
|
|
763
|
+
throw new Error(`Checkpoint not found: ${id}`);
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
if (snapshot.mode !== "disk" || snapshot.sourceSandboxId !== s.id)
|
|
767
|
+
throw new Error(`Checkpoint not found: ${id}`);
|
|
768
|
+
return checkpoint(snapshot);
|
|
769
|
+
}
|
|
770
|
+
/** Forwards localhost:`localPort` here to `remotePort` in the Sprite. */
|
|
771
|
+
proxyPort(localPort, remotePort, remoteHost) {
|
|
772
|
+
return proxyPort(this.client, this.id ?? this.name, localPort, remotePort, remoteHost);
|
|
773
|
+
}
|
|
774
|
+
proxyPorts(mappings) {
|
|
775
|
+
return proxyPorts(this.client, this.id ?? this.name, mappings);
|
|
776
|
+
}
|
|
777
|
+
/** Runtime's network rules as Sprites rules: `* deny` closes the internet,
|
|
778
|
+
* allow rules are the only destinations reachable, deny rules are refused. */
|
|
779
|
+
async getNetworkPolicy() {
|
|
780
|
+
const policy = await (await this.native()).network.get();
|
|
781
|
+
if (!policy.internet)
|
|
782
|
+
return { rules: [{ domain: "*", action: "deny" }] };
|
|
783
|
+
const rules = [
|
|
784
|
+
...policy.allow.map((domain) => ({ domain, action: "allow" })),
|
|
785
|
+
...policy.deny.map((domain) => ({ domain, action: "deny" })),
|
|
786
|
+
];
|
|
787
|
+
if (policy.allow.length)
|
|
788
|
+
rules.push({ domain: "*", action: "deny" });
|
|
789
|
+
return { rules };
|
|
790
|
+
}
|
|
791
|
+
/** Replaces the rules, keeping any host:port openings set through Runtime. */
|
|
792
|
+
async updateNetworkPolicy(policy) {
|
|
793
|
+
const rules = networkRules(policy);
|
|
794
|
+
const s = await this.native();
|
|
795
|
+
const current = await s.network.get();
|
|
796
|
+
await s.network.set({ ...rules, connect: current.connect });
|
|
797
|
+
}
|
|
798
|
+
/** Control mode is never on: Runtime refuses `controlMode: true`. */
|
|
799
|
+
useControlMode() {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
hasControlConnection() {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
/** Nothing to close: no control connection is ever opened. */
|
|
806
|
+
closeControlConnection() { }
|
|
348
807
|
async updateURLSettings(settings) {
|
|
349
808
|
validateURLSettings(settings);
|
|
350
809
|
const p = await (await this.native()).previews.create(8080, { visibility: settings.auth === "public" ? "public" : "private" });
|
|
351
810
|
this.url = p.urlWithToken ?? p.url;
|
|
811
|
+
this.urlSettings = { auth: settings.auth };
|
|
352
812
|
}
|
|
353
813
|
async update(options) {
|
|
354
814
|
only("Sprites update", options, ["labels", "urlSettings"]);
|
|
@@ -368,6 +828,289 @@ export class Sprite {
|
|
|
368
828
|
return this;
|
|
369
829
|
}
|
|
370
830
|
}
|
|
831
|
+
const SERVICES = "Runtime has no service manager; run it as a process with spawn, or as an image's start command";
|
|
832
|
+
refuse("Sprites", "Sprite", Sprite, {
|
|
833
|
+
restoreCheckpoint: "Runtime has no in-place restore; start a new sandbox from the checkpoint with runtime.sandboxes.create({ snapshot: id })",
|
|
834
|
+
getControlConnection: CONTROL,
|
|
835
|
+
listServices: SERVICES,
|
|
836
|
+
getService: SERVICES,
|
|
837
|
+
createService: SERVICES,
|
|
838
|
+
deleteService: SERVICES,
|
|
839
|
+
startService: SERVICES,
|
|
840
|
+
stopService: SERVICES,
|
|
841
|
+
restartService: SERVICES,
|
|
842
|
+
getServiceLogs: SERVICES,
|
|
843
|
+
signalService: SERVICES,
|
|
844
|
+
getPrivilegesPolicy: "Runtime sets one privilege profile for every sandbox: root through sudo",
|
|
845
|
+
updatePrivilegesPolicy: "Runtime sets one privilege profile for every sandbox: root through sudo",
|
|
846
|
+
deletePrivilegesPolicy: "Runtime sets one privilege profile for every sandbox: root through sudo",
|
|
847
|
+
getResourcesPolicy: "read the sandbox's memory size from runtime.sandboxes.get",
|
|
848
|
+
updateResourcesPolicy: "change memory with the native sandbox's resize(), which restarts its programs",
|
|
849
|
+
deleteResourcesPolicy: "change memory with the native sandbox's resize(), which restarts its programs",
|
|
850
|
+
});
|
|
851
|
+
function checkpoint(snapshot) {
|
|
852
|
+
const labels = (snapshot.labels ?? {});
|
|
853
|
+
return {
|
|
854
|
+
id: snapshot.id,
|
|
855
|
+
createTime: new Date(snapshot.createdAt),
|
|
856
|
+
comment: labels["compat.comment"],
|
|
857
|
+
isAuto: false,
|
|
858
|
+
health: snapshot.state === "ready" ? "" : snapshot.state,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/** Sprites lets the more specific rule win and reads `rules: []` as no
|
|
862
|
+
* limit; Runtime's allow list is the only destinations reachable and its deny
|
|
863
|
+
* list always wins. A policy whose meaning would differ is refused. */
|
|
864
|
+
function networkRules(policy) {
|
|
865
|
+
if (!policy || typeof policy !== "object" || !Array.isArray(policy.rules))
|
|
866
|
+
throw new TypeError("policy.rules must be an array");
|
|
867
|
+
only("Sprites network policy", policy, ["rules"]);
|
|
868
|
+
const allow = [];
|
|
869
|
+
const deny = [];
|
|
870
|
+
let everything;
|
|
871
|
+
for (const rule of policy.rules) {
|
|
872
|
+
if (!rule || typeof rule !== "object")
|
|
873
|
+
throw new TypeError("Each rule must be an object");
|
|
874
|
+
only("Sprites network rule", rule, ["domain", "action", "include"]);
|
|
875
|
+
if (rule.include !== undefined)
|
|
876
|
+
throw new CompatibilityError("Sprites", `the network preset ${String(rule.include)}; list its domains as allow rules`);
|
|
877
|
+
if (typeof rule.domain !== "string" || !rule.domain.trim())
|
|
878
|
+
throw new TypeError("Each rule needs a domain");
|
|
879
|
+
if (rule.action !== "allow" && rule.action !== "deny")
|
|
880
|
+
throw new TypeError("action must be allow or deny");
|
|
881
|
+
const domain = rule.domain.trim().toLowerCase();
|
|
882
|
+
if (domain === "*") {
|
|
883
|
+
if (everything && everything !== rule.action)
|
|
884
|
+
throw new TypeError("* is both allowed and denied");
|
|
885
|
+
everything = rule.action;
|
|
886
|
+
}
|
|
887
|
+
else
|
|
888
|
+
(rule.action === "allow" ? allow : deny).push(domain);
|
|
889
|
+
}
|
|
890
|
+
const covers = (wide, narrow) => wide === narrow || (wide.startsWith("*.") && narrow.endsWith(wide.slice(1)));
|
|
891
|
+
for (const allowed of allow)
|
|
892
|
+
for (const denied of deny)
|
|
893
|
+
if (covers(denied, allowed))
|
|
894
|
+
throw new CompatibilityError("Sprites", `allowing ${allowed} inside the denied ${denied}; Runtime's deny rules always win`);
|
|
895
|
+
if (everything === "deny" && !allow.length)
|
|
896
|
+
return { internet: false, allow: [], deny };
|
|
897
|
+
return { internet: true, allow: everything === "allow" ? [] : allow, deny };
|
|
898
|
+
}
|
|
899
|
+
/** A local listener that carries each connection to a port in the Sprite,
|
|
900
|
+
* over Runtime's tunnel. Node and Bun. */
|
|
901
|
+
export class ProxySession extends EventEmitter {
|
|
902
|
+
localPort;
|
|
903
|
+
remotePort;
|
|
904
|
+
remoteHost;
|
|
905
|
+
#sprite;
|
|
906
|
+
#forward;
|
|
907
|
+
#closed = false;
|
|
908
|
+
#done;
|
|
909
|
+
#finish;
|
|
910
|
+
constructor(client, spriteName, mapping) {
|
|
911
|
+
super();
|
|
912
|
+
only("Sprites port mapping", mapping, ["localPort", "remotePort", "remoteHost"]);
|
|
913
|
+
for (const [name, port] of [
|
|
914
|
+
["localPort", mapping.localPort],
|
|
915
|
+
["remotePort", mapping.remotePort],
|
|
916
|
+
])
|
|
917
|
+
if (!Number.isInteger(port) ||
|
|
918
|
+
port < 0 ||
|
|
919
|
+
port > 65535 ||
|
|
920
|
+
(name === "remotePort" && port === 0))
|
|
921
|
+
throw new TypeError(`${name} must be a port number`);
|
|
922
|
+
const host = mapping.remoteHost ?? "localhost";
|
|
923
|
+
if (!["localhost", "127.0.0.1", "::1"].includes(host))
|
|
924
|
+
throw new CompatibilityError("Sprites", `proxying to ${host}; forward a port of the Sprite itself (localhost)`);
|
|
925
|
+
this.localPort = mapping.localPort;
|
|
926
|
+
this.remotePort = mapping.remotePort;
|
|
927
|
+
this.remoteHost = host;
|
|
928
|
+
const owner = client instanceof SpritesClient
|
|
929
|
+
? client
|
|
930
|
+
: new SpritesClient(client.token, { baseURL: client.baseURL });
|
|
931
|
+
this.#sprite = owner.sprite(spriteName);
|
|
932
|
+
this.#done = new Promise((resolve) => (this.#finish = resolve));
|
|
933
|
+
}
|
|
934
|
+
async start() {
|
|
935
|
+
if (this.#closed)
|
|
936
|
+
throw new Error("Proxy session is closed");
|
|
937
|
+
try {
|
|
938
|
+
const s = await ready(await this.#sprite.native());
|
|
939
|
+
this.#forward = await s.forwardPort(this.remotePort, {
|
|
940
|
+
localPort: this.localPort,
|
|
941
|
+
host: "localhost",
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
if (this.listenerCount("error"))
|
|
946
|
+
this.emit("error", error);
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
949
|
+
if (this.#closed)
|
|
950
|
+
await this.#forward.close();
|
|
951
|
+
}
|
|
952
|
+
localAddr() {
|
|
953
|
+
return this.#forward && !this.#closed ? `localhost:${this.#forward.localPort}` : null;
|
|
954
|
+
}
|
|
955
|
+
close() {
|
|
956
|
+
if (this.#closed)
|
|
957
|
+
return;
|
|
958
|
+
this.#closed = true;
|
|
959
|
+
void this.#forward?.close().catch(() => undefined);
|
|
960
|
+
this.#finish();
|
|
961
|
+
this.emit("close");
|
|
962
|
+
}
|
|
963
|
+
wait() {
|
|
964
|
+
return this.#done;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
export class ProxyManager {
|
|
968
|
+
#sessions = [];
|
|
969
|
+
addSession(session) {
|
|
970
|
+
this.#sessions.push(session);
|
|
971
|
+
}
|
|
972
|
+
closeAll() {
|
|
973
|
+
for (const session of this.#sessions)
|
|
974
|
+
session.close();
|
|
975
|
+
}
|
|
976
|
+
async waitAll() {
|
|
977
|
+
await Promise.all(this.#sessions.map((session) => session.wait()));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
export async function proxyPort(client, spriteName, localPort, remotePort, remoteHost) {
|
|
981
|
+
const session = new ProxySession(client, spriteName, { localPort, remotePort, remoteHost });
|
|
982
|
+
await session.start();
|
|
983
|
+
return session;
|
|
984
|
+
}
|
|
985
|
+
/** All or none: a mapping that cannot start closes the ones before it. */
|
|
986
|
+
export async function proxyPorts(client, spriteName, mappings) {
|
|
987
|
+
const sessions = [];
|
|
988
|
+
try {
|
|
989
|
+
for (const mapping of mappings) {
|
|
990
|
+
const session = new ProxySession(client, spriteName, mapping);
|
|
991
|
+
sessions.push(session);
|
|
992
|
+
await session.start();
|
|
993
|
+
}
|
|
994
|
+
return sessions;
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
for (const session of sessions)
|
|
998
|
+
session.close();
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/** Polls the Sprite's listening TCP sockets once a second with `ss` (as
|
|
1003
|
+
* Sprites' own watcher keeps a connection open, this keeps it awake). */
|
|
1004
|
+
export class PortWatcher extends Feed {
|
|
1005
|
+
#timer;
|
|
1006
|
+
#stopped = false;
|
|
1007
|
+
#sprite;
|
|
1008
|
+
constructor(client, spriteName) {
|
|
1009
|
+
super();
|
|
1010
|
+
this.#sprite = client.sprite(spriteName);
|
|
1011
|
+
}
|
|
1012
|
+
async connect() {
|
|
1013
|
+
const s = await ready(await this.#sprite.native());
|
|
1014
|
+
let known = await listening(s);
|
|
1015
|
+
this.push({ type: "port_list", ports: [...known.values()] });
|
|
1016
|
+
const tick = async () => {
|
|
1017
|
+
if (this.#stopped)
|
|
1018
|
+
return;
|
|
1019
|
+
try {
|
|
1020
|
+
const now = await listening(s);
|
|
1021
|
+
for (const [key, port] of known)
|
|
1022
|
+
if (!now.has(key))
|
|
1023
|
+
this.push({ ...port, type: "port_closed" });
|
|
1024
|
+
for (const [key, port] of now)
|
|
1025
|
+
if (!known.has(key))
|
|
1026
|
+
this.push(port);
|
|
1027
|
+
known = now;
|
|
1028
|
+
}
|
|
1029
|
+
catch (error) {
|
|
1030
|
+
this.end(error);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (!this.#stopped)
|
|
1034
|
+
this.#timer = setTimeout(() => void tick(), 1000);
|
|
1035
|
+
};
|
|
1036
|
+
this.#timer = setTimeout(() => void tick(), 1000);
|
|
1037
|
+
}
|
|
1038
|
+
stop() {
|
|
1039
|
+
this.#stopped = true;
|
|
1040
|
+
if (this.#timer)
|
|
1041
|
+
clearTimeout(this.#timer);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function listening(s) {
|
|
1045
|
+
const result = await s.exec(["ss", "-Hltnp"]);
|
|
1046
|
+
if (result.exitCode !== 0)
|
|
1047
|
+
throw new Error(`Cannot list ports in the Sprite: ${result.stderr.trim() || "ss failed"}`);
|
|
1048
|
+
const ports = new Map();
|
|
1049
|
+
for (const line of result.stdout.split("\n")) {
|
|
1050
|
+
const local = line.trim().split(/\s+/)[3];
|
|
1051
|
+
if (!local)
|
|
1052
|
+
continue;
|
|
1053
|
+
const at = local.lastIndexOf(":");
|
|
1054
|
+
const port = Number(local.slice(at + 1));
|
|
1055
|
+
if (!Number.isInteger(port))
|
|
1056
|
+
continue;
|
|
1057
|
+
const address = local
|
|
1058
|
+
.slice(0, at)
|
|
1059
|
+
.replace(/^\[|\]$/g, "")
|
|
1060
|
+
.replace(/%.*$/, "");
|
|
1061
|
+
const pid = Number(/pid=(\d+)/.exec(line)?.[1] ?? 0);
|
|
1062
|
+
ports.set(`${address}:${port}`, { type: "port_opened", port, address, pid });
|
|
1063
|
+
}
|
|
1064
|
+
return ports;
|
|
1065
|
+
}
|
|
1066
|
+
/** Runtime's file watch, one per path, as Sprites' watch events. */
|
|
1067
|
+
export class FilesystemWatcher extends Feed {
|
|
1068
|
+
sandbox;
|
|
1069
|
+
paths;
|
|
1070
|
+
recursive;
|
|
1071
|
+
#handles = [];
|
|
1072
|
+
#stopped = false;
|
|
1073
|
+
#files;
|
|
1074
|
+
constructor(sandbox, paths, workingDir, recursive) {
|
|
1075
|
+
super();
|
|
1076
|
+
this.sandbox = sandbox;
|
|
1077
|
+
this.paths = paths;
|
|
1078
|
+
this.recursive = recursive;
|
|
1079
|
+
this.#files = new GuestFiles(sandbox, workingDir);
|
|
1080
|
+
}
|
|
1081
|
+
async connect() {
|
|
1082
|
+
const s = await this.sandbox();
|
|
1083
|
+
const paths = this.paths.map((path) => this.#files.path(path));
|
|
1084
|
+
try {
|
|
1085
|
+
for (const path of paths) {
|
|
1086
|
+
const handle = await s.files.watch(path, (event) => {
|
|
1087
|
+
if (event.type === "access")
|
|
1088
|
+
return;
|
|
1089
|
+
this.push({
|
|
1090
|
+
type: "event",
|
|
1091
|
+
path: event.path,
|
|
1092
|
+
event: event.type,
|
|
1093
|
+
isDir: event.isDir,
|
|
1094
|
+
timestamp: new Date().toISOString(),
|
|
1095
|
+
});
|
|
1096
|
+
}, { recursive: this.recursive, timeoutMs: 0 });
|
|
1097
|
+
this.#handles.push(handle);
|
|
1098
|
+
if (this.#stopped)
|
|
1099
|
+
void handle.stop().catch(() => undefined);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
catch (error) {
|
|
1103
|
+
this.stop();
|
|
1104
|
+
throw error;
|
|
1105
|
+
}
|
|
1106
|
+
this.push({ type: "subscribed", paths });
|
|
1107
|
+
}
|
|
1108
|
+
stop() {
|
|
1109
|
+
this.#stopped = true;
|
|
1110
|
+
for (const handle of this.#handles.splice(0))
|
|
1111
|
+
void handle.stop().catch(() => undefined);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
371
1114
|
export class SpriteCommand extends EventEmitter {
|
|
372
1115
|
stdout = new PassThrough();
|
|
373
1116
|
stderr = new PassThrough();
|
|
@@ -378,20 +1121,7 @@ export class SpriteCommand extends EventEmitter {
|
|
|
378
1121
|
controller = new AbortController();
|
|
379
1122
|
constructor(sprite, command, args = [], options = {}) {
|
|
380
1123
|
super();
|
|
381
|
-
only("Sprites spawn", options,
|
|
382
|
-
"cwd",
|
|
383
|
-
"env",
|
|
384
|
-
"tty",
|
|
385
|
-
"rows",
|
|
386
|
-
"cols",
|
|
387
|
-
"detachable",
|
|
388
|
-
"sessionId",
|
|
389
|
-
"controlMode",
|
|
390
|
-
"encoding",
|
|
391
|
-
"maxBuffer",
|
|
392
|
-
"signal",
|
|
393
|
-
"timeout",
|
|
394
|
-
]);
|
|
1124
|
+
only("Sprites spawn", options, EXEC_KEYS);
|
|
395
1125
|
if (options.controlMode)
|
|
396
1126
|
throw new CompatibilityError("Sprites", "control-mode wire protocol");
|
|
397
1127
|
this.process = (async () => {
|
|
@@ -479,9 +1209,11 @@ export class SpriteCommand extends EventEmitter {
|
|
|
479
1209
|
}
|
|
480
1210
|
export class SpriteFilesystem {
|
|
481
1211
|
get;
|
|
1212
|
+
cwd;
|
|
482
1213
|
files;
|
|
483
1214
|
constructor(get, cwd = "/") {
|
|
484
1215
|
this.get = get;
|
|
1216
|
+
this.cwd = cwd;
|
|
485
1217
|
this.files = new GuestFiles(get, cwd);
|
|
486
1218
|
}
|
|
487
1219
|
async readFile(path, encoding) {
|
|
@@ -527,6 +1259,8 @@ export class SpriteFilesystem {
|
|
|
527
1259
|
async rm(path, options = {}) {
|
|
528
1260
|
if (!options.force && !(await this.files.exists(path)))
|
|
529
1261
|
throw new FilesystemError(`Path not found: ${path}`, "ENOENT", path, "rm");
|
|
1262
|
+
if (options.asRoot)
|
|
1263
|
+
return this.#root(path, "rm", ["rm", "-f", ...(options.recursive ? ["-r"] : []), "--", this.files.path(path)], true);
|
|
530
1264
|
await fileOperation(path, "rm", () => this.files.remove(path, options.recursive));
|
|
531
1265
|
}
|
|
532
1266
|
async stat(path) {
|
|
@@ -542,15 +1276,85 @@ export class SpriteFilesystem {
|
|
|
542
1276
|
isSymbolicLink: () => e.type === "symlink",
|
|
543
1277
|
};
|
|
544
1278
|
}
|
|
545
|
-
rename(from, to) {
|
|
1279
|
+
async rename(from, to, options = {}) {
|
|
1280
|
+
if (options.asRoot)
|
|
1281
|
+
return this.#root(from, "rename", ["mv", "-f", "-T", "--", this.files.path(from), this.files.path(to)], true);
|
|
546
1282
|
return fileOperation(from, "rename", () => this.files.rename(from, to, true));
|
|
547
1283
|
}
|
|
548
|
-
copyFile(from, to, options = {}) {
|
|
1284
|
+
async copyFile(from, to, options = {}) {
|
|
1285
|
+
if (options.asRoot || options.preserveAttrs)
|
|
1286
|
+
return this.#root(from, "copyFile", [
|
|
1287
|
+
"cp",
|
|
1288
|
+
...(options.recursive ? ["-R"] : []),
|
|
1289
|
+
...(options.preserveAttrs ? ["--preserve=mode,ownership,timestamps"] : []),
|
|
1290
|
+
"--",
|
|
1291
|
+
this.files.path(from),
|
|
1292
|
+
this.files.path(to),
|
|
1293
|
+
], options.asRoot === true);
|
|
549
1294
|
return fileOperation(from, "copyFile", () => this.files.copy(from, to, options.recursive, true));
|
|
550
1295
|
}
|
|
551
|
-
chmod(path, mode, options = {}) {
|
|
1296
|
+
async chmod(path, mode, options = {}) {
|
|
1297
|
+
if (options.asRoot) {
|
|
1298
|
+
if (!Number.isInteger(mode) || mode < 0)
|
|
1299
|
+
throw new TypeError("mode must be a file mode");
|
|
1300
|
+
return this.#root(path, "chmod", [
|
|
1301
|
+
"chmod",
|
|
1302
|
+
...(options.recursive ? ["-R"] : []),
|
|
1303
|
+
(mode & 0o7777).toString(8),
|
|
1304
|
+
"--",
|
|
1305
|
+
this.files.path(path),
|
|
1306
|
+
], true);
|
|
1307
|
+
}
|
|
552
1308
|
return fileOperation(path, "chmod", () => this.files.chmod(path, mode, options.recursive));
|
|
553
1309
|
}
|
|
1310
|
+
/** By number or name, as the Sprite's user unless `asRoot` (sudo). */
|
|
1311
|
+
async chown(path, options) {
|
|
1312
|
+
only("Sprites chown", options ?? {}, ["uid", "gid", "recursive", "asRoot"]);
|
|
1313
|
+
if (options?.uid === undefined && options?.gid === undefined)
|
|
1314
|
+
throw new TypeError("uid or gid is required");
|
|
1315
|
+
for (const id of [options.uid, options.gid])
|
|
1316
|
+
if (id !== undefined &&
|
|
1317
|
+
!(typeof id === "number" ? Number.isInteger(id) && id >= 0 : /^[A-Za-z0-9._-]+$/.test(id)))
|
|
1318
|
+
throw new TypeError("uid and gid must be ids or user and group names");
|
|
1319
|
+
const owner = `${options.uid ?? ""}${options.gid === undefined ? "" : `:${options.gid}`}`;
|
|
1320
|
+
return this.#root(path, "chown", ["chown", ...(options.recursive ? ["-R"] : []), owner, "--", this.files.path(path)], options.asRoot === true);
|
|
1321
|
+
}
|
|
1322
|
+
/** Changes to these paths until the watcher is closed. */
|
|
1323
|
+
async watch(paths, options = {}) {
|
|
1324
|
+
only("Sprites watch", options, ["recursive"]);
|
|
1325
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
1326
|
+
if (!list.length || list.some((path) => typeof path !== "string" || !path))
|
|
1327
|
+
throw new TypeError("paths must name at least one path");
|
|
1328
|
+
const watcher = new FilesystemWatcher(this.get, list, this.cwd, options.recursive === true);
|
|
1329
|
+
await watcher.connect();
|
|
1330
|
+
return watcher;
|
|
1331
|
+
}
|
|
1332
|
+
/** A guest command for what the file API does not offer: as root through
|
|
1333
|
+
* sudo when asked, and its failure as the vendor's FilesystemError. */
|
|
1334
|
+
async #root(path, syscall, argv, asRoot) {
|
|
1335
|
+
const result = await (await this.get()).exec(asRoot ? ["sudo", "-n", "--", ...argv] : argv);
|
|
1336
|
+
if (result.exitCode === 0)
|
|
1337
|
+
return;
|
|
1338
|
+
const message = result.stderr.trim() || `${syscall} failed`;
|
|
1339
|
+
const code = /No such file/i.test(message)
|
|
1340
|
+
? "ENOENT"
|
|
1341
|
+
: /File exists/i.test(message)
|
|
1342
|
+
? "EEXIST"
|
|
1343
|
+
: /Not a directory/i.test(message)
|
|
1344
|
+
? "ENOTDIR"
|
|
1345
|
+
: /Is a directory/i.test(message)
|
|
1346
|
+
? "EISDIR"
|
|
1347
|
+
: /Directory not empty/i.test(message)
|
|
1348
|
+
? "ENOTEMPTY"
|
|
1349
|
+
: /Operation not permitted|password is required|sudo:/i.test(message)
|
|
1350
|
+
? "EPERM"
|
|
1351
|
+
: /Permission denied/i.test(message)
|
|
1352
|
+
? "EACCES"
|
|
1353
|
+
: /invalid/i.test(message)
|
|
1354
|
+
? "EINVAL"
|
|
1355
|
+
: "UNKNOWN";
|
|
1356
|
+
throw new FilesystemError(message, code, path, syscall);
|
|
1357
|
+
}
|
|
554
1358
|
exists(path) {
|
|
555
1359
|
return this.files.exists(path);
|
|
556
1360
|
}
|
|
@@ -564,25 +1368,3 @@ export class SpriteFilesystem {
|
|
|
564
1368
|
return this.writeFile(path, JSON.stringify(data, null, options.spaces));
|
|
565
1369
|
}
|
|
566
1370
|
}
|
|
567
|
-
export class CheckpointStream {
|
|
568
|
-
messages;
|
|
569
|
-
index = 0;
|
|
570
|
-
constructor(messages) {
|
|
571
|
-
this.messages = messages;
|
|
572
|
-
}
|
|
573
|
-
async next() {
|
|
574
|
-
return this.messages[this.index++] ?? null;
|
|
575
|
-
}
|
|
576
|
-
async processAll(handler) {
|
|
577
|
-
for await (const message of this)
|
|
578
|
-
await handler(message);
|
|
579
|
-
}
|
|
580
|
-
close() {
|
|
581
|
-
this.index = this.messages.length;
|
|
582
|
-
}
|
|
583
|
-
async *[Symbol.asyncIterator]() {
|
|
584
|
-
let message;
|
|
585
|
-
while ((message = await this.next()))
|
|
586
|
-
yield message;
|
|
587
|
-
}
|
|
588
|
-
}
|