withruntime 0.9.0 → 0.10.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 +4 -4
- package/DAYTONA.md +13 -7
- package/E2B.md +12 -8
- package/README.md +15 -7
- package/README.src.md +15 -7
- package/VERCEL.md +13 -3
- package/compatibility-lock.json +84 -8
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +2 -1
- package/dist/blaxel/preview.js +15 -13
- package/dist/blaxel/process.js +56 -10
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +189 -54
- package/dist/client.d.ts +29 -3
- package/dist/client.js +40 -11
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.js +47 -9
- package/dist/cloudflare/worker.d.ts +4 -0
- package/dist/cloudflare/worker.js +166 -16
- package/dist/codesandbox/guest-service.js +57 -20
- package/dist/codesandbox/index.js +42 -8
- package/dist/compat/core.d.ts +9 -8
- package/dist/compat/core.js +60 -21
- package/dist/compat/files.js +22 -4
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/daytona.js +11 -4
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +121 -38
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/sandbox.d.ts +1 -1
- package/dist/daytona/sandbox.js +16 -7
- package/dist/e2b/client.js +1 -1
- package/dist/e2b/commands.d.ts +30 -8
- package/dist/e2b/commands.js +142 -54
- package/dist/e2b/index.d.ts +1 -0
- package/dist/e2b/index.js +1 -0
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +80 -0
- package/dist/e2b/sandbox.d.ts +2 -1
- package/dist/e2b/sandbox.js +49 -15
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +9 -1
- package/dist/freestyle/index.js +38 -5
- package/dist/index.d.ts +2 -1
- package/dist/login.js +16 -10
- package/dist/modal/index.js +2 -1
- package/dist/network-cli.js +111 -43
- package/dist/openai-agents/index.js +9 -45
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/index.d.ts +12 -0
- package/dist/products/index.js +10 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +8 -2
- package/dist/products/previews.js +11 -3
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.js +61 -8
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +13 -27
- package/dist/runloop/index.js +149 -52
- package/dist/sandbox.d.ts +37 -8
- package/dist/sandbox.js +432 -68
- package/dist/snapshots.d.ts +9 -4
- package/dist/sprites/index.js +69 -33
- package/dist/tar.d.ts +3 -1
- package/dist/tar.js +140 -14
- package/dist/transport.d.ts +4 -7
- package/dist/transport.js +205 -122
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +3 -2
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +7 -1
- package/dist/vercel/sandbox.d.ts +8 -8
- package/dist/vercel/sandbox.js +154 -59
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +2 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
2
|
+
const CHUNK = 1_048_576;
|
|
3
|
+
function failure(message, code, cause) {
|
|
4
|
+
return new RuntimeError({ message, code, status: 0, ...(cause === undefined ? {} : { cause }) });
|
|
5
|
+
}
|
|
6
|
+
async function exact(handle, offset, length) {
|
|
7
|
+
const data = new Uint8Array(length);
|
|
8
|
+
let held = 0;
|
|
9
|
+
while (held < length) {
|
|
10
|
+
const read = await handle.read(data, held, length - held, offset + held);
|
|
11
|
+
if (read.bytesRead === 0)
|
|
12
|
+
throw failure("The local upload source changed or ended before its declared size.", "source_changed");
|
|
13
|
+
held += read.bytesRead;
|
|
14
|
+
}
|
|
15
|
+
return data;
|
|
16
|
+
}
|
|
17
|
+
/** ARCHITECTURE.md section 10, Folders over HTTP: unknown-length inputs use
|
|
18
|
+
* bounded private disk staging; known files and Blobs remain repeatable inputs.
|
|
19
|
+
* Size and digest are established before the remote upload can begin. */
|
|
20
|
+
export async function prepareFileSource(input, scope) {
|
|
21
|
+
const fs = await import("node:fs/promises");
|
|
22
|
+
const { createHash } = await import("node:crypto");
|
|
23
|
+
const { Readable } = await import("node:stream");
|
|
24
|
+
scope.check();
|
|
25
|
+
const hash = createHash("sha256");
|
|
26
|
+
let size = 0;
|
|
27
|
+
let handle;
|
|
28
|
+
let staging;
|
|
29
|
+
let iterator;
|
|
30
|
+
// What is used of a reader, so the code compiles against Node's stream types
|
|
31
|
+
// as well as the DOM's (packages/cloud compiles this file without DOM).
|
|
32
|
+
let reader;
|
|
33
|
+
const cleanup = async () => {
|
|
34
|
+
try {
|
|
35
|
+
await handle?.close();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (staging)
|
|
39
|
+
await fs.rm(staging, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
if (typeof input === "string") {
|
|
44
|
+
handle = await fs.open(input, "r");
|
|
45
|
+
const info = await handle.stat();
|
|
46
|
+
if (!info.isFile() || !Number.isSafeInteger(info.size))
|
|
47
|
+
throw failure("A streamed file upload requires a regular local file.", "invalid_upload_source");
|
|
48
|
+
size = info.size;
|
|
49
|
+
for (let offset = 0; offset < size; offset += CHUNK) {
|
|
50
|
+
scope.check();
|
|
51
|
+
hash.update(await scope.wait(exact(handle, offset, Math.min(CHUNK, size - offset))));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (input instanceof Blob) {
|
|
55
|
+
// Bun's types leave out Blob.slice when the DOM library is off, as it is
|
|
56
|
+
// where packages/cloud compiles this file; every runtime has it.
|
|
57
|
+
const blob = input;
|
|
58
|
+
size = blob.size;
|
|
59
|
+
for (let offset = 0; offset < size; offset += CHUNK) {
|
|
60
|
+
scope.check();
|
|
61
|
+
hash.update(new Uint8Array(await scope.wait(blob.slice(offset, offset + CHUNK).arrayBuffer())));
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
size,
|
|
65
|
+
sha256: hash.digest("hex"),
|
|
66
|
+
chunkAt: async (offset, length) => new Uint8Array(await blob.slice(offset, offset + length).arrayBuffer()),
|
|
67
|
+
close: async () => undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (!input ||
|
|
72
|
+
(typeof input.getReader !== "function" &&
|
|
73
|
+
typeof input[Symbol.asyncIterator] !== "function"))
|
|
74
|
+
throw failure("A streamed upload requires a byte stream or local file.", "invalid_upload_source");
|
|
75
|
+
const { tmpdir } = await import("node:os");
|
|
76
|
+
const path = await import("node:path");
|
|
77
|
+
staging = await fs.mkdtemp(path.join(tmpdir(), "runtime-upload-"));
|
|
78
|
+
await fs.chmod(staging, 0o700);
|
|
79
|
+
handle = await fs.open(path.join(staging, "source"), "wx+", 0o600);
|
|
80
|
+
const space = await fs.statfs(staging, { bigint: true });
|
|
81
|
+
const available = space.bavail * space.bsize;
|
|
82
|
+
if ("getReader" in input)
|
|
83
|
+
reader = input.getReader();
|
|
84
|
+
else
|
|
85
|
+
iterator = input[Symbol.asyncIterator]();
|
|
86
|
+
for (;;) {
|
|
87
|
+
scope.check();
|
|
88
|
+
const next = await scope.wait(reader ? reader.read() : iterator.next());
|
|
89
|
+
if (next.done)
|
|
90
|
+
break;
|
|
91
|
+
if (!(next.value instanceof Uint8Array))
|
|
92
|
+
throw failure("A streamed upload must produce byte chunks.", "invalid_upload_source");
|
|
93
|
+
for (let offset = 0; offset < next.value.length; offset += CHUNK) {
|
|
94
|
+
scope.check();
|
|
95
|
+
const part = next.value.subarray(offset, offset + CHUNK);
|
|
96
|
+
if (!Number.isSafeInteger(size + part.length))
|
|
97
|
+
throw failure("The streamed upload exceeds the native file size range.", "upload_too_large");
|
|
98
|
+
if (BigInt(size + part.length) > available)
|
|
99
|
+
throw failure("There is not enough local temporary disk space to stage the streamed upload.", "local_disk_full");
|
|
100
|
+
let written = 0;
|
|
101
|
+
while (written < part.length) {
|
|
102
|
+
scope.check();
|
|
103
|
+
const result = await handle.write(part, written, part.length - written, size + written);
|
|
104
|
+
if (result.bytesWritten === 0)
|
|
105
|
+
throw failure("The local temporary file accepted no upload bytes.", "local_disk_full");
|
|
106
|
+
written += result.bytesWritten;
|
|
107
|
+
}
|
|
108
|
+
size += part.length;
|
|
109
|
+
hash.update(part);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
scope.check();
|
|
114
|
+
const owned = handle;
|
|
115
|
+
if (!owned)
|
|
116
|
+
throw failure("The local upload file did not open.", "invalid_upload_source");
|
|
117
|
+
return {
|
|
118
|
+
size,
|
|
119
|
+
sha256: hash.digest("hex"),
|
|
120
|
+
chunkAt: (offset, length) => exact(owned, offset, length),
|
|
121
|
+
close: cleanup,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Calling return/cancel can itself throw synchronously. Cleanup still owns
|
|
126
|
+
// the file and directory, and must preserve the original source failure.
|
|
127
|
+
let timer;
|
|
128
|
+
try {
|
|
129
|
+
// Node's pending next() must settle before iterator.return() can finish.
|
|
130
|
+
// Web readers own cancellation directly; arbitrary iterators may not.
|
|
131
|
+
if (input instanceof Readable)
|
|
132
|
+
input.destroy();
|
|
133
|
+
const closing = reader ? reader.cancel(scope.signal?.reason) : iterator?.return?.();
|
|
134
|
+
if (closing)
|
|
135
|
+
await Promise.race([
|
|
136
|
+
closing,
|
|
137
|
+
new Promise((resolve) => {
|
|
138
|
+
timer = setTimeout(resolve, 1000);
|
|
139
|
+
}),
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* Preserve the caller's original failure. */
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
if (timer)
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
await cleanup().catch(() => undefined);
|
|
149
|
+
}
|
|
150
|
+
if (error?.code === "ENOSPC")
|
|
151
|
+
throw failure("There is not enough local temporary disk space to stage the streamed upload.", "local_disk_full", error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
try {
|
|
156
|
+
reader?.releaseLock();
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
/* Cancellation may leave a pending caller-owned read. */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -38,22 +38,29 @@ export declare class VmsNamespace {
|
|
|
38
38
|
id: string;
|
|
39
39
|
sourceVmId: string;
|
|
40
40
|
slug: string | null;
|
|
41
|
+
displayName: string | null;
|
|
41
42
|
public: boolean;
|
|
42
43
|
createdAt: string;
|
|
43
44
|
updatedAt: string;
|
|
44
45
|
ttlSeconds: number;
|
|
45
46
|
}>;
|
|
46
47
|
delete: (id: string) => Promise<void>;
|
|
47
|
-
list: (
|
|
48
|
+
list: (options?: {
|
|
49
|
+
sourceVmId?: string;
|
|
50
|
+
limit?: number;
|
|
51
|
+
offset?: number;
|
|
52
|
+
}) => Promise<{
|
|
48
53
|
snapshots: {
|
|
49
54
|
id: string;
|
|
50
55
|
sourceVmId: string;
|
|
51
56
|
slug: string | null;
|
|
57
|
+
displayName: string | null;
|
|
52
58
|
public: boolean;
|
|
53
59
|
createdAt: string;
|
|
54
60
|
updatedAt: string;
|
|
55
61
|
ttlSeconds: number;
|
|
56
62
|
}[];
|
|
63
|
+
totalCount: number;
|
|
57
64
|
}>;
|
|
58
65
|
};
|
|
59
66
|
constructor(runtime: Runtime);
|
|
@@ -292,6 +299,7 @@ export declare class Vm {
|
|
|
292
299
|
id: string;
|
|
293
300
|
sourceVmId: string;
|
|
294
301
|
slug: string | null;
|
|
302
|
+
displayName: string | null;
|
|
295
303
|
public: boolean;
|
|
296
304
|
createdAt: string;
|
|
297
305
|
updatedAt: string;
|
package/dist/freestyle/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { all, client, CompatibilityError, create, destroy, execOptions, lookup, only, ready, } from "../compat/core.js";
|
|
2
2
|
import { GuestFiles } from "../compat/files.js";
|
|
3
3
|
export { CompatibilityError };
|
|
4
|
+
function validateMetadata(metadata) {
|
|
5
|
+
for (const key of ["compat.provider", "fs.displayName"])
|
|
6
|
+
if (metadata !== undefined && Object.prototype.hasOwnProperty.call(metadata, key))
|
|
7
|
+
throw new CompatibilityError("Freestyle", `metadata key ${key} reserved by this adapter`);
|
|
8
|
+
}
|
|
4
9
|
const data = (s) => ({
|
|
5
10
|
id: s.id,
|
|
6
11
|
state: s.state,
|
|
@@ -18,6 +23,7 @@ const snapshotData = (s) => ({
|
|
|
18
23
|
id: s.id,
|
|
19
24
|
sourceVmId: s.sourceSandboxId,
|
|
20
25
|
slug: s.name,
|
|
26
|
+
displayName: s.labels?.["fs.displayName"] ?? null,
|
|
21
27
|
public: false,
|
|
22
28
|
createdAt: s.createdAt,
|
|
23
29
|
updatedAt: s.createdAt,
|
|
@@ -37,9 +43,32 @@ export class VmsNamespace {
|
|
|
37
43
|
this.snapshots = {
|
|
38
44
|
get: async (id) => snapshotData(await this.resolveSnapshot(id)),
|
|
39
45
|
delete: async (id) => runtime.snapshots.delete((await this.resolveSnapshot(id)).id),
|
|
40
|
-
list: async () =>
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
},
|
|
43
72
|
};
|
|
44
73
|
}
|
|
45
74
|
async resolveSnapshot(id) {
|
|
@@ -59,6 +88,7 @@ export class VmsNamespace {
|
|
|
59
88
|
"metadata",
|
|
60
89
|
"firewall",
|
|
61
90
|
]);
|
|
91
|
+
validateMetadata(options.metadata);
|
|
62
92
|
if (!options.firewall || !Array.isArray(options.firewall.rules))
|
|
63
93
|
throw new TypeError("firewall.rules is required");
|
|
64
94
|
only("Freestyle firewall", options.firewall, ["rules"]);
|
|
@@ -266,6 +296,7 @@ export class Vm {
|
|
|
266
296
|
}
|
|
267
297
|
async update(options) {
|
|
268
298
|
only("Freestyle update", options, ["slug", "displayName", "idleTimeoutSeconds", "metadata"]);
|
|
299
|
+
validateMetadata(options.metadata);
|
|
269
300
|
const s = await lookup(this.runtime, this.id);
|
|
270
301
|
await s.update({
|
|
271
302
|
name: options.slug,
|
|
@@ -298,8 +329,10 @@ export class Vm {
|
|
|
298
329
|
async snapshot(options = {}) {
|
|
299
330
|
only("Freestyle snapshot", options, ["slug", "displayName", "ttlSeconds"]);
|
|
300
331
|
if (options.ttlSeconds !== undefined &&
|
|
301
|
-
(options.ttlSeconds <= 0 ||
|
|
302
|
-
|
|
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");
|
|
303
336
|
const s = await (await lookup(this.runtime, this.id)).snapshot({
|
|
304
337
|
name: options.slug,
|
|
305
338
|
labels: options.displayName ? { "fs.displayName": options.displayName } : undefined,
|
package/dist/index.d.ts
CHANGED
|
@@ -15,13 +15,14 @@ export { Tunnel, TunnelStream, type PortForward } from "./tunnel.js";
|
|
|
15
15
|
export { Page } from "./page.js";
|
|
16
16
|
export type { KeyLimits } from "./products/limits.js";
|
|
17
17
|
export type { AuditEvent, AuditPage } from "./products/audit.js";
|
|
18
|
+
export type { UsageExportQuery, UsageExportRow, UsageExportPage, } from "./products/observability.js";
|
|
18
19
|
export type { JobSecret, Secret, SecretRule, SecretUse, SetSecret } from "./products/secrets.js";
|
|
19
20
|
export { SecretPartlyStoredError } from "./products/secrets.js";
|
|
20
21
|
export type { McpCatalogEntry, McpGateway, McpServerRequest } from "./products/mcp.js";
|
|
21
22
|
export type { FileEvent, FileEventType, WatchHandle, WatchNotice, WatchOptions, } from "./products/watch.js";
|
|
22
23
|
export type { Mount, MountBucket } from "./products/mounts.js";
|
|
23
24
|
export type { TailscaleJoin, TailscaleJoined, TailscaleNode, TailscaleStatus, } from "./products/tailscale.js";
|
|
24
|
-
export type { Volume, VolumeBackup, CreateVolume } from "./products/volumes.js";
|
|
25
|
+
export type { Volume, VolumeBackup, CreateVolume, VolumeAttachment } from "./products/volumes.js";
|
|
25
26
|
export type { CreateJob, Job, JobCompute, JobDefinition, JobLogs, JobRun, JobRunState, JobSchedule, } from "./products/jobs.js";
|
|
26
27
|
export type { CheckedDomain, DnsRecord, Domain, EgressAddress, TcpPort, Tunnel as PrivateTunnel, TunnelPeer, TunnelPeerCreated, SetUpstreamProxy, UpstreamProxy, } from "./products/network-products.js";
|
|
27
28
|
export { verifyWebhook, WebhookVerificationError, type MetricPoint, type MetricRange, type OtelExport, type RuntimeEvent, type SandboxMetrics, type Webhook, type WebhookDelivery, type WebhookEventType, } from "./products/observability.js";
|
package/dist/login.js
CHANGED
|
@@ -4,7 +4,7 @@ import { hostname } from "node:os";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { me, named } from "./cli-name.js";
|
|
6
6
|
import { Runtime } from "./client.js";
|
|
7
|
-
import { connectionOrigins, connectionStore } from "./credentials.js";
|
|
7
|
+
import { checkBrowserOrigins, connectionOrigins, connectionStore } from "./credentials.js";
|
|
8
8
|
import { describeRoute, envFetch } from "./proxy.js";
|
|
9
9
|
class ConnectionError extends Error {
|
|
10
10
|
status;
|
|
@@ -100,7 +100,16 @@ function connection(env, options) {
|
|
|
100
100
|
: response.status === 429
|
|
101
101
|
? "Too many connection attempts. Try again later."
|
|
102
102
|
: "Runtime could not complete this connection. Try again.");
|
|
103
|
-
|
|
103
|
+
try {
|
|
104
|
+
return (await response.json());
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// A complete malformed answer remains terminal. A failed body read can
|
|
108
|
+
// hide an issued token, so keep its private key and retry the same request.
|
|
109
|
+
if (error instanceof SyntaxError)
|
|
110
|
+
throw error;
|
|
111
|
+
throw new ConnectionError(503, "Runtime's connection response was interrupted. Retry the connection.");
|
|
112
|
+
}
|
|
104
113
|
}
|
|
105
114
|
return { origins, store, request, fetcher };
|
|
106
115
|
}
|
|
@@ -118,7 +127,7 @@ export async function authenticationCommand(args, env, options = {}) {
|
|
|
118
127
|
// A pasted key belongs to the website's key list: forget it here, and
|
|
119
128
|
// revoke it there if it should stop working everywhere.
|
|
120
129
|
if (saved.connectionId === PASTED) {
|
|
121
|
-
await store.remove();
|
|
130
|
+
await store.remove(saved.key);
|
|
122
131
|
return { disconnected: true };
|
|
123
132
|
}
|
|
124
133
|
try {
|
|
@@ -128,7 +137,7 @@ export async function authenticationCommand(args, env, options = {}) {
|
|
|
128
137
|
if (!(error instanceof ConnectionError && error.status === 401))
|
|
129
138
|
throw error;
|
|
130
139
|
}
|
|
131
|
-
await store.remove();
|
|
140
|
+
await store.remove(saved.key);
|
|
132
141
|
return { disconnected: true };
|
|
133
142
|
}
|
|
134
143
|
if (command !== "login")
|
|
@@ -160,11 +169,7 @@ export async function authenticationCommand(args, env, options = {}) {
|
|
|
160
169
|
agentName.length > 80 ||
|
|
161
170
|
[...agentName].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
162
171
|
throw new Error("Give the agent a name of up to 80 characters.");
|
|
163
|
-
|
|
164
|
-
const local = (origin) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname);
|
|
165
|
-
if (!((origins.auth === "https://withruntime.com" && origins.api === "https://api.withruntime.com") ||
|
|
166
|
-
(local(origins.auth) && local(origins.api))))
|
|
167
|
-
throw new Error("Browser login supports Runtime's production endpoints or local test endpoints. Check RUNTIME_AUTH_URL and RUNTIME_API_URL.");
|
|
172
|
+
checkBrowserOrigins(origins);
|
|
168
173
|
if (withKey) {
|
|
169
174
|
// The fallback to a browser approval: a key from the website, read from
|
|
170
175
|
// standard input so it never sits in a command line or shell history.
|
|
@@ -200,7 +205,7 @@ export async function authenticationCommand(args, env, options = {}) {
|
|
|
200
205
|
catch (error) {
|
|
201
206
|
if (!(error instanceof ConnectionError && error.status === 401))
|
|
202
207
|
throw error;
|
|
203
|
-
await store.remove();
|
|
208
|
+
await store.remove(saved.key);
|
|
204
209
|
}
|
|
205
210
|
}
|
|
206
211
|
const pending = await beginDeviceLogin(env, agentName, options);
|
|
@@ -245,6 +250,7 @@ export async function authenticationCommand(args, env, options = {}) {
|
|
|
245
250
|
* the agent, which shows it to the person it works for. */
|
|
246
251
|
export async function beginDeviceLogin(env, agentName, options = {}) {
|
|
247
252
|
const { origins, store, request } = connection(env, options);
|
|
253
|
+
checkBrowserOrigins(origins);
|
|
248
254
|
// A request this machine already started and nobody has answered yet: pick
|
|
249
255
|
// it up, so a command stopped while it waited (an agent's tool timing out)
|
|
250
256
|
// shows the same link and code again rather than a new one to approve.
|
package/dist/modal/index.js
CHANGED
|
@@ -149,6 +149,7 @@ export class SandboxService {
|
|
|
149
149
|
throw new CompatibilityError("Modal", "fractional CPU reservations");
|
|
150
150
|
if (params.blockNetwork && (params.outboundDomainAllowlist || params.outboundCidrAllowlist))
|
|
151
151
|
throw new TypeError("blockNetwork cannot be combined with allowlists");
|
|
152
|
+
const placement = region("Modal", params.regions?.[0]);
|
|
152
153
|
const built = await image.build(app);
|
|
153
154
|
let primary;
|
|
154
155
|
const s = await create(this.runtime, "modal", {
|
|
@@ -162,7 +163,7 @@ export class SandboxService {
|
|
|
162
163
|
cpu: "reserved",
|
|
163
164
|
vcpu: params.cpu,
|
|
164
165
|
memoryMiB: params.memoryMiB,
|
|
165
|
-
region:
|
|
166
|
+
region: placement,
|
|
166
167
|
timeoutSeconds: Math.ceil((params.timeoutMs ?? 300_000) / 1000),
|
|
167
168
|
idlePauseSeconds: 0,
|
|
168
169
|
onLeaseEnd: "stop",
|
package/dist/network-cli.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, rename, unlink } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
2
5
|
import { RuntimeError } from "./errors.js";
|
|
3
6
|
import { named } from "./cli-name.js";
|
|
4
7
|
import { generateWireGuardKeyPair } from "./wireguard.js";
|
|
@@ -92,15 +95,63 @@ function portNumber(product, text) {
|
|
|
92
95
|
throw usage(product, "Give a port from 1 to 65535.");
|
|
93
96
|
return value;
|
|
94
97
|
}
|
|
98
|
+
/** A fresh private inode prevents disclosure through permissive files, symlinks
|
|
99
|
+
* and hard links. Rename publishes complete contents without a readable window. */
|
|
100
|
+
async function privateOutput(path, text) {
|
|
101
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
102
|
+
try {
|
|
103
|
+
const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
104
|
+
try {
|
|
105
|
+
await handle.writeFile(text);
|
|
106
|
+
await handle.sync();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await handle.close();
|
|
110
|
+
}
|
|
111
|
+
return temporary;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
await unlink(temporary).catch(() => undefined);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Check and stage before changing a remote peer. If the request or final write
|
|
119
|
+
* fails, this private draft preserves the newly generated key for recovery. */
|
|
120
|
+
async function prepareOutput(path, privateKey) {
|
|
121
|
+
try {
|
|
122
|
+
const info = await lstat(path);
|
|
123
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
124
|
+
throw usage("tunnel", "The output must be a regular file, not a directory or symbolic link.");
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error.code !== "ENOENT")
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const recovery = await privateOutput(path, `# DRAFT: request or output incomplete. Keep this file for recovery.\n[Interface]\nPrivateKey = ${privateKey ?? "<your private key>"}\n`);
|
|
131
|
+
return {
|
|
132
|
+
recovery,
|
|
133
|
+
async publish(config) {
|
|
134
|
+
const temporary = await privateOutput(path, config);
|
|
135
|
+
try {
|
|
136
|
+
await rename(temporary, path);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await unlink(temporary).catch(() => undefined);
|
|
140
|
+
}
|
|
141
|
+
await unlink(recovery);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
95
145
|
/** Writes a wg-quick file readable by its owner alone, or prints it. */
|
|
96
|
-
async function deliver(out, config, path, hint, ready = true) {
|
|
146
|
+
async function deliver(out, config, path, hint, ready = true, prepared) {
|
|
97
147
|
if (!config) {
|
|
98
148
|
out.error(hint);
|
|
99
149
|
return;
|
|
100
150
|
}
|
|
101
151
|
if (path) {
|
|
102
|
-
|
|
103
|
-
|
|
152
|
+
if (!prepared)
|
|
153
|
+
throw new Error("Tunnel output was not prepared.");
|
|
154
|
+
await prepared.publish(config);
|
|
104
155
|
out.error(ready
|
|
105
156
|
? `Wrote ${path}. Bring it up with: sudo wg-quick up ${path.startsWith("/") ? path : `./${path}`}`
|
|
106
157
|
: `Wrote ${path}. ${hint}`);
|
|
@@ -276,52 +327,69 @@ export async function networkProductCommand(product, argv, args, out, client) {
|
|
|
276
327
|
case "peers": {
|
|
277
328
|
const action = rest[0];
|
|
278
329
|
if (action === "add" || action === "rotate") {
|
|
330
|
+
const argument = need(rest[1], action === "add" ? "the peer's name, such as office" : "a peer id");
|
|
279
331
|
// The pair is made here unless a key is given: the private key never
|
|
280
332
|
// leaves this machine, and goes only into the file written below.
|
|
281
333
|
const given = flag("public-key");
|
|
282
334
|
const pair = given ? undefined : generateWireGuardKeyPair();
|
|
283
335
|
const publicKey = given ?? pair.publicKey;
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
336
|
+
const path = out.json ? undefined : (flag("out") ?? (pair ? "runtime.conf" : undefined));
|
|
337
|
+
const prepared = path ? await prepareOutput(path, pair?.privateKey) : undefined;
|
|
338
|
+
try {
|
|
339
|
+
const answer = action === "add"
|
|
340
|
+
? await runtime.tunnel.addPeer({
|
|
341
|
+
name: argument,
|
|
342
|
+
publicKey,
|
|
343
|
+
routes: args.flags.get("route") ?? [],
|
|
344
|
+
})
|
|
345
|
+
: await runtime.tunnel.rotatePeer(argument, { publicKey });
|
|
346
|
+
const configReady = answer.configReady ?? !!answer.config;
|
|
347
|
+
const draftHint = "Keep this file: it preserves your private key. Read runtime tunnel get when the gateway is ready, replace PublicKey in [Peer] with gatewayPublicKey, then run wg-quick up.";
|
|
348
|
+
// Older servers returned no config while the gateway was starting.
|
|
349
|
+
// Preserve the locally generated key in a draft rather than discard it.
|
|
350
|
+
const supplied = answer.config ??
|
|
351
|
+
[
|
|
352
|
+
"# DRAFT: gateway key missing. Keep this file; it preserves your private key.",
|
|
353
|
+
"[Interface]",
|
|
354
|
+
`PrivateKey = ${pair?.privateKey ?? "<your private key>"}`,
|
|
355
|
+
`Address = ${answer.peer.address}/32`,
|
|
356
|
+
"",
|
|
357
|
+
"[Peer]",
|
|
358
|
+
`PublicKey = ${answer.tunnel.gatewayPublicKey ?? "<gatewayPublicKey from runtime tunnel get>"}`,
|
|
359
|
+
`Endpoint = ${answer.tunnel.endpoint}`,
|
|
360
|
+
`AllowedIPs = ${answer.tunnel.subnet}`,
|
|
361
|
+
"PersistentKeepalive = 25",
|
|
362
|
+
"",
|
|
363
|
+
].join("\n");
|
|
364
|
+
const privateKeyRow = /^[ \t]*PrivateKey[ \t]*=[^\r\n]*$/gm;
|
|
365
|
+
if (pair && (supplied.match(privateKeyRow)?.length ?? 0) !== 1)
|
|
366
|
+
throw new RuntimeError({
|
|
367
|
+
code: "invalid_response",
|
|
368
|
+
status: 0,
|
|
369
|
+
message: "Runtime returned a tunnel configuration without exactly one private-key field.",
|
|
370
|
+
});
|
|
371
|
+
const config = pair
|
|
372
|
+
? supplied.replace(privateKeyRow, `PrivateKey = ${pair.privateKey}`)
|
|
373
|
+
: supplied;
|
|
374
|
+
if (out.json) {
|
|
375
|
+
print("", {
|
|
376
|
+
...answer,
|
|
377
|
+
config,
|
|
378
|
+
configReady,
|
|
379
|
+
...(!configReady ? { hint: draftHint } : {}),
|
|
380
|
+
});
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
if (answer.tunnel.funded === false)
|
|
384
|
+
out.error(fundingState(answer.tunnel));
|
|
385
|
+
await deliver(out, config, path, configReady ? answer.hint : draftHint, configReady, prepared);
|
|
319
386
|
return 0;
|
|
320
387
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
388
|
+
catch (error) {
|
|
389
|
+
if (prepared)
|
|
390
|
+
out.error(`The peer request or output did not finish. Your private draft is saved at ${prepared.recovery}; keep it for recovery.`);
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
325
393
|
}
|
|
326
394
|
if (action === "rm" || action === "remove") {
|
|
327
395
|
const after = await runtime.tunnel.removePeer(need(rest[1], "a peer id"));
|
|
@@ -531,29 +531,11 @@ export class RuntimeCloudSandboxSession {
|
|
|
531
531
|
// Workspace archives.
|
|
532
532
|
async persistWorkspace() {
|
|
533
533
|
const root = this.state.manifest.root;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
"-c",
|
|
540
|
-
`mkdir -p -- ${STAGING} && tar ${skip.join(" ")} -C "$1" -cf - . > "$2"`,
|
|
541
|
-
"sh",
|
|
542
|
-
root,
|
|
543
|
-
staging,
|
|
544
|
-
], {
|
|
545
|
-
cwd: "/",
|
|
546
|
-
timeoutMs: this.state.execTimeoutMs,
|
|
547
|
-
}));
|
|
548
|
-
if (packed.exitCode !== 0)
|
|
549
|
-
throw new SandboxProviderError(`${PROVIDER} could not archive the workspace.`, {
|
|
550
|
-
stderr: packed.stderr,
|
|
551
|
-
});
|
|
552
|
-
return await this.#call((sbx) => sbx.files.read(staging));
|
|
553
|
-
}
|
|
554
|
-
finally {
|
|
555
|
-
await this.#call((sbx) => sbx.files.remove(staging)).catch(() => undefined);
|
|
556
|
-
}
|
|
534
|
+
return this.#call((sbx) => sbx.files.archive(root, {
|
|
535
|
+
gzip: false,
|
|
536
|
+
...(root === FILES_HOME ? { exclude: [".openai-agents-staging"] } : {}),
|
|
537
|
+
timeoutMs: this.state.execTimeoutMs,
|
|
538
|
+
}));
|
|
557
539
|
}
|
|
558
540
|
async hydrateWorkspace(data) {
|
|
559
541
|
const bytes = typeof data === "string"
|
|
@@ -561,28 +543,10 @@ export class RuntimeCloudSandboxSession {
|
|
|
561
543
|
: data instanceof Uint8Array
|
|
562
544
|
? data
|
|
563
545
|
: new Uint8Array(data);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
"sh",
|
|
569
|
-
"-c",
|
|
570
|
-
'mkdir -p -- "$1" && tar -C "$1" -xf "$2"',
|
|
571
|
-
"sh",
|
|
572
|
-
this.state.manifest.root,
|
|
573
|
-
staging,
|
|
574
|
-
], {
|
|
575
|
-
cwd: "/",
|
|
576
|
-
timeoutMs: this.state.execTimeoutMs,
|
|
577
|
-
}));
|
|
578
|
-
if (unpacked.exitCode !== 0)
|
|
579
|
-
throw new SandboxProviderError(`${PROVIDER} could not restore the workspace.`, {
|
|
580
|
-
stderr: unpacked.stderr,
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
finally {
|
|
584
|
-
await this.#call((sbx) => sbx.files.remove(staging)).catch(() => undefined);
|
|
585
|
-
}
|
|
546
|
+
await this.#call((sbx) => sbx.files.unarchive(this.state.manifest.root, bytes, {
|
|
547
|
+
gzip: false,
|
|
548
|
+
timeoutMs: this.state.execTimeoutMs,
|
|
549
|
+
}));
|
|
586
550
|
}
|
|
587
551
|
// Lifecycle.
|
|
588
552
|
async running() {
|