withruntime 0.18.0 → 0.21.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/COMPATIBILITY.md +32 -1
- package/README.md +19 -0
- package/README.src.md +19 -0
- package/dist/agents/index.d.ts +11 -0
- package/dist/agents/index.js +12 -0
- package/dist/agents-cli.d.ts +10 -0
- package/dist/agents-cli.js +362 -0
- package/dist/apps-cli.d.ts +2 -0
- package/dist/apps-cli.js +173 -0
- package/dist/builds/index.d.ts +10 -0
- package/dist/builds/index.js +11 -0
- package/dist/cli-words.js +1 -0
- package/dist/cli.js +88 -6
- package/dist/client.d.ts +19 -0
- package/dist/client.js +20 -0
- package/dist/cloudflare/errors.d.ts +23 -0
- package/dist/cloudflare/errors.js +16 -0
- package/dist/cloudflare/index.d.ts +82 -9
- package/dist/cloudflare/index.js +162 -38
- package/dist/cloudflare/interpreter.js +14 -5
- package/dist/cloudflare/mime.d.ts +1 -0
- package/dist/cloudflare/mime.js +66 -0
- package/dist/cloudflare/streams.d.ts +8 -0
- package/dist/cloudflare/streams.js +30 -0
- package/dist/cloudflare/types.d.ts +338 -0
- package/dist/cloudflare/types.js +80 -0
- package/dist/cloudflare/worker.d.ts +297 -10
- package/dist/cloudflare/worker.js +203 -17
- package/dist/containers/index.d.ts +10 -0
- package/dist/containers/index.js +11 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/product-names.js +3 -0
- package/dist/products/agents.d.ts +272 -0
- package/dist/products/agents.js +251 -0
- package/dist/products/containers.d.ts +71 -0
- package/dist/products/containers.js +6 -0
- package/dist/products/index.d.ts +6 -0
- package/dist/products/index.js +7 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/transport.d.ts +2 -2
- package/dist/transport.js +1 -1
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +11 -0
- package/package.json +16 -1
|
@@ -1,12 +1,47 @@
|
|
|
1
|
-
/** Workers conditional entry
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
import {
|
|
1
|
+
/** Workers conditional entry: what a Worker imports as `withruntime/cloudflare`.
|
|
2
|
+
* `Sandbox` is the Durable Object class the Worker exports and binds, as with
|
|
3
|
+
* @cloudflare/sandbox; it keeps the sandbox's name and preview tokens, and
|
|
4
|
+
* Runtime runs the sandbox. The Wrangler config drops its `containers` block. */
|
|
5
|
+
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|
6
|
+
import { only, refusal, CompatibilityError } from "../compat/core.js";
|
|
6
7
|
import { Runtime } from "../client.js";
|
|
7
8
|
import { workerSandboxIdentity } from "./identity.js";
|
|
8
|
-
import { Sandbox as RuntimeSandbox, sanitizeSandboxId, validateSandboxConfiguration, } from "./index.js";
|
|
9
|
+
import { Sandbox as RuntimeSandbox, SandboxSecurityError, sanitizeSandboxId, validateSandboxConfiguration, } from "./index.js";
|
|
9
10
|
export * from "./index.js";
|
|
11
|
+
/** Cloudflare's egress entrypoint for outbound interception, which Runtime
|
|
12
|
+
* does with network rules and secrets instead. */
|
|
13
|
+
export class ContainerProxy extends WorkerEntrypoint {
|
|
14
|
+
fetch() {
|
|
15
|
+
return Promise.reject(refusal("Cloudflare", "ContainerProxy", OUTBOUND));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const OUTBOUND = "set the sandbox's network rules with Runtime's network API, and store keys as Runtime secrets, which Runtime's proxy adds to the requests you name";
|
|
19
|
+
const TERMINAL = "open a terminal with the native Runtime sandbox's terminal()";
|
|
20
|
+
/** Members of Cloudflare's Durable Object a Runtime sandbox has no counterpart
|
|
21
|
+
* for, refused with what to use instead. */
|
|
22
|
+
const REFUSED = {
|
|
23
|
+
callTunnels: "use exposePort for a public URL",
|
|
24
|
+
setContainerTimeouts: "a Runtime sandbox has no container start to tune",
|
|
25
|
+
setTransport: "Runtime's API is the only transport",
|
|
26
|
+
containerFetch: "fetch the URL exposePort returns",
|
|
27
|
+
getContainerPlacementId: "a Runtime sandbox's id is its place",
|
|
28
|
+
createBackup: "snapshot the sandbox with Runtime's snapshots, or keep the folder on a volume",
|
|
29
|
+
restoreBackup: "create a sandbox from a Runtime snapshot",
|
|
30
|
+
terminal: TERMINAL,
|
|
31
|
+
};
|
|
32
|
+
/** Hooks and outbound settings a subclass of Cloudflare's class may set,
|
|
33
|
+
* which would otherwise be silently ignored. */
|
|
34
|
+
const SUBCLASS = {
|
|
35
|
+
onStart: "a Runtime sandbox has no Durable Object lifecycle hooks",
|
|
36
|
+
onStop: "a Runtime sandbox has no Durable Object lifecycle hooks",
|
|
37
|
+
onError: "a Runtime sandbox has no Durable Object lifecycle hooks",
|
|
38
|
+
onActivityExpired: "set sleepAfter or keepAlive instead",
|
|
39
|
+
interceptHttps: OUTBOUND,
|
|
40
|
+
enableInternet: OUTBOUND,
|
|
41
|
+
allowedHosts: OUTBOUND,
|
|
42
|
+
deniedHosts: OUTBOUND,
|
|
43
|
+
};
|
|
44
|
+
const STATIC_OUTBOUND = ["outboundByHost", "outbound", "outboundHandlers"];
|
|
10
45
|
const CONFIGURATION_KEY = "runtime.configuration";
|
|
11
46
|
const CONFIGURATION_INTENT_KEY = "runtime.configuration.pending";
|
|
12
47
|
/** Preserve the original failure even when durable recovery also fails. */
|
|
@@ -58,20 +93,46 @@ function sessionTarget(session) {
|
|
|
58
93
|
exec: (command, options) => session.exec(command, options),
|
|
59
94
|
execStream: (command, options) => session.execStream(command, options),
|
|
60
95
|
startProcess: async (command, options) => processTarget(await session.startProcess(command, options)),
|
|
96
|
+
listProcesses: async () => (await session.listProcesses()).map((p) => processTarget(p)),
|
|
97
|
+
getProcess: async (id) => {
|
|
98
|
+
const p = await session.getProcess(id);
|
|
99
|
+
return p ? processTarget(p) : null;
|
|
100
|
+
},
|
|
101
|
+
killProcess: (id, signal) => session.killProcess(id, signal),
|
|
102
|
+
killAllProcesses: () => session.killAllProcesses(),
|
|
103
|
+
cleanupCompletedProcesses: () => session.cleanupCompletedProcesses(),
|
|
104
|
+
getProcessLogs: (id) => session.getProcessLogs(id),
|
|
105
|
+
streamProcessLogs: (id) => session.streamProcessLogs(id),
|
|
61
106
|
writeFile: (...args) => session.writeFile(...args),
|
|
62
107
|
readFile,
|
|
108
|
+
readFileStream: (path) => session.readFileStream(path),
|
|
63
109
|
mkdir: (...args) => session.mkdir(...args),
|
|
64
110
|
deleteFile: (path) => session.deleteFile(path),
|
|
65
111
|
renameFile: (path, to) => session.renameFile(path, to),
|
|
66
112
|
moveFile: (path, to) => session.renameFile(path, to),
|
|
67
113
|
exists: (path) => session.exists(path),
|
|
68
114
|
listFiles: (...args) => session.listFiles(...args),
|
|
115
|
+
gitCheckout: (repoUrl, options) => session.gitCheckout(repoUrl, options),
|
|
116
|
+
watch: (path, options) => session.watch(path, options),
|
|
117
|
+
checkChanges: (path, options) => session.checkChanges(path, options),
|
|
69
118
|
setEnvVars: (...args) => session.setEnvVars(...args),
|
|
119
|
+
createCodeContext: (options) => session.createCodeContext(options),
|
|
120
|
+
runCode: (code, options) => session.runCode(code, options),
|
|
121
|
+
runCodeStream: (code, options) => session.runCodeStream(code, options),
|
|
122
|
+
listCodeContexts: () => session.listCodeContexts(),
|
|
123
|
+
deleteCodeContext: (id) => session.deleteCodeContext(id),
|
|
124
|
+
mountBucket: (bucket, path, options) => session.mountBucket(bucket, path, options),
|
|
125
|
+
unmountBucket: (path) => session.unmountBucket(path),
|
|
126
|
+
terminal: () => Promise.reject(refusal("Cloudflare", "ExecutionSession.terminal", TERMINAL)),
|
|
70
127
|
};
|
|
71
128
|
}
|
|
129
|
+
/** A Process as plain data and callable members, as Cloudflare's RPC returns
|
|
130
|
+
* one. `pid` is absent: Runtime does not report it (Cloudflare's type has it
|
|
131
|
+
* optional). */
|
|
72
132
|
function processTarget(process) {
|
|
73
133
|
return {
|
|
74
134
|
id: process.id,
|
|
135
|
+
pid: undefined,
|
|
75
136
|
command: process.command,
|
|
76
137
|
status: process.status,
|
|
77
138
|
startTime: process.startTime,
|
|
@@ -83,6 +144,7 @@ function processTarget(process) {
|
|
|
83
144
|
getLogs: () => process.getLogs(),
|
|
84
145
|
waitForExit: (timeout) => process.waitForExit(timeout),
|
|
85
146
|
waitForLog: (pattern, timeout) => process.waitForLog(pattern, timeout),
|
|
147
|
+
waitForPort: (port, options) => process.waitForPort(port, options),
|
|
86
148
|
};
|
|
87
149
|
}
|
|
88
150
|
export class Sandbox extends DurableObject {
|
|
@@ -126,6 +188,7 @@ export class Sandbox extends DurableObject {
|
|
|
126
188
|
throw new Error("Call getSandbox(namespace, id) before using the Durable Object");
|
|
127
189
|
if (!this.env.RUNTIME_API_KEY?.startsWith("rtcloud_"))
|
|
128
190
|
throw new Error("Set the RUNTIME_API_KEY Worker secret to a Runtime API key");
|
|
191
|
+
this.refuseSubclassSettings();
|
|
129
192
|
this.facade ??= new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), workerSandboxIdentity(this.ctx.id.toString(), this.configuration), { ...this.configuration });
|
|
130
193
|
return this.facade;
|
|
131
194
|
}
|
|
@@ -308,34 +371,99 @@ export class Sandbox extends DurableObject {
|
|
|
308
371
|
async setEnvVars(...args) {
|
|
309
372
|
return (await this.sandbox()).setEnvVars(...args);
|
|
310
373
|
}
|
|
374
|
+
// The rest of Cloudflare's 0.x Sandbox, each a call on the Runtime sandbox.
|
|
375
|
+
async setLabels(labels) {
|
|
376
|
+
return (await this.sandbox()).setLabels(labels);
|
|
377
|
+
}
|
|
378
|
+
async execWithSessionToken(command, sessionId, options) {
|
|
379
|
+
return (await this.sandbox(options?.signal)).execWithSessionToken(command, sessionId, options);
|
|
380
|
+
}
|
|
381
|
+
async execStreamWithSessionToken(command, sessionId, options) {
|
|
382
|
+
return (await this.sandbox(options?.signal)).execStreamWithSessionToken(command, sessionId, options);
|
|
383
|
+
}
|
|
384
|
+
async streamProcessLogs(processId, options) {
|
|
385
|
+
return (await this.sandbox(options?.signal)).streamProcessLogs(processId, options);
|
|
386
|
+
}
|
|
387
|
+
async cleanupCompletedProcesses(sessionId) {
|
|
388
|
+
return (await this.sandbox()).cleanupCompletedProcesses(sessionId);
|
|
389
|
+
}
|
|
390
|
+
async readFileStream(path, options) {
|
|
391
|
+
return (await this.sandbox()).readFileStream(path, options);
|
|
392
|
+
}
|
|
393
|
+
async gitCheckout(repoUrl, options) {
|
|
394
|
+
return (await this.sandbox()).gitCheckout(repoUrl, options);
|
|
395
|
+
}
|
|
396
|
+
async watch(path, options) {
|
|
397
|
+
return (await this.sandbox()).watch(path, options);
|
|
398
|
+
}
|
|
399
|
+
async checkChanges(path, options) {
|
|
400
|
+
return (await this.sandbox()).checkChanges(path, options);
|
|
401
|
+
}
|
|
402
|
+
async mountBucket(bucket, mountPath, options) {
|
|
403
|
+
return (await this.sandbox()).mountBucket(bucket, mountPath, options);
|
|
404
|
+
}
|
|
405
|
+
async unmountBucket(mountPath) {
|
|
406
|
+
return (await this.sandbox()).unmountBucket(mountPath);
|
|
407
|
+
}
|
|
408
|
+
async createCodeContext(options) {
|
|
409
|
+
return (await this.sandbox()).createCodeContext(options);
|
|
410
|
+
}
|
|
411
|
+
async runCode(code, options) {
|
|
412
|
+
return (await this.sandbox(options?.signal)).runCode(code, options);
|
|
413
|
+
}
|
|
414
|
+
async runCodeStream(code, options) {
|
|
415
|
+
return (await this.sandbox(options?.signal)).runCodeStream(code, options);
|
|
416
|
+
}
|
|
417
|
+
async listCodeContexts() {
|
|
418
|
+
return (await this.sandbox()).listCodeContexts();
|
|
419
|
+
}
|
|
420
|
+
async deleteCodeContext(contextId) {
|
|
421
|
+
return (await this.sandbox()).deleteCodeContext(contextId);
|
|
422
|
+
}
|
|
423
|
+
/** Stops the sandbox; its files stay, and the next call starts it again. */
|
|
424
|
+
async stop(signal) {
|
|
425
|
+
return (await this.sandbox()).stop(signal);
|
|
426
|
+
}
|
|
427
|
+
/** A subclass's Cloudflare-only hooks and outbound settings would do
|
|
428
|
+
* nothing here, so the first call refuses them by name. */
|
|
429
|
+
refuseSubclassSettings() {
|
|
430
|
+
for (const [member, alternative] of Object.entries(SUBCLASS))
|
|
431
|
+
if (this[member] !== undefined)
|
|
432
|
+
throw refusal("Cloudflare", `Sandbox.${member}`, alternative);
|
|
433
|
+
const statics = this.constructor;
|
|
434
|
+
for (const member of STATIC_OUTBOUND)
|
|
435
|
+
if (statics[member] !== undefined)
|
|
436
|
+
throw refusal("Cloudflare", `Sandbox.${member}`, OUTBOUND);
|
|
437
|
+
}
|
|
311
438
|
exposePort(port, options) {
|
|
312
439
|
return this.previewOperation(() => this.expose(port, options));
|
|
313
440
|
}
|
|
314
441
|
async expose(port, options) {
|
|
315
442
|
only("Cloudflare Worker exposePort", options, ["name", "hostname", "token"]);
|
|
316
443
|
if (!Number.isInteger(port) || port < 1024 || port > 65535 || port === 3000)
|
|
317
|
-
throw new
|
|
318
|
-
if (!/^[a-z0-9.-]
|
|
444
|
+
throw new SandboxSecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding 3000 (sandbox control plane).`, "INVALID_PORT");
|
|
445
|
+
if (!/^[a-z0-9.-]+(:\d{1,5})?$/i.test(options.hostname) ||
|
|
319
446
|
options.hostname.startsWith(".") ||
|
|
320
|
-
options.hostname
|
|
447
|
+
/\.(:|$)/.test(options.hostname))
|
|
321
448
|
throw new TypeError("Preview hostname must be a DNS hostname");
|
|
322
449
|
if (options.hostname.endsWith(".workers.dev"))
|
|
323
|
-
throw new Error("Port exposure requires a custom domain
|
|
450
|
+
throw new Error("Port exposure requires a custom domain. .workers.dev domains do not support wildcard subdomains required for port proxying.");
|
|
451
|
+
if (options.token !== undefined)
|
|
452
|
+
validateToken(options.token);
|
|
324
453
|
const existing = await this.ctx.storage.get(`runtime.preview.${port}`);
|
|
325
|
-
const token = options.token ?? existing?.token ??
|
|
454
|
+
const token = options.token ?? existing?.token ?? newToken();
|
|
326
455
|
const ports = await this.ctx.storage.list({
|
|
327
456
|
prefix: "runtime.preview.",
|
|
328
457
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
throw new TypeError("Preview token must contain 1-63 lowercase letters, digits or underscores");
|
|
458
|
+
const taken = [...ports.values()].find((p) => p.port !== port && p.token === token);
|
|
459
|
+
if (taken)
|
|
460
|
+
throw new SandboxSecurityError(`Token '${token}' is already in use by port ${taken.port}. Please use a different token.`, "TOKEN_IN_USE");
|
|
333
461
|
const sandbox = await this.sandbox();
|
|
334
462
|
const name = this.configuration.sandboxName.name;
|
|
335
463
|
if (name !== name.toLowerCase())
|
|
336
464
|
throw new CompatibilityError("Cloudflare", "uppercase sandbox IDs in preview hostnames; use normalizeId:true");
|
|
337
465
|
const native = await (await sandbox.native()).previews.create(port, { visibility: "private" });
|
|
338
|
-
const url =
|
|
466
|
+
const url = previewUrl(port, name, options.hostname, token);
|
|
339
467
|
await this.ctx.storage.put(`runtime.preview.${port}`, {
|
|
340
468
|
token,
|
|
341
469
|
url,
|
|
@@ -366,7 +494,7 @@ export class Sandbox extends DurableObject {
|
|
|
366
494
|
prefix: "runtime.preview.",
|
|
367
495
|
})).values(),
|
|
368
496
|
].map((p) => ({
|
|
369
|
-
url:
|
|
497
|
+
url: previewUrl(p.port, this.configuration.sandboxName.name, hostname, p.token),
|
|
370
498
|
port: p.port,
|
|
371
499
|
status: "active",
|
|
372
500
|
}));
|
|
@@ -410,6 +538,9 @@ export class Sandbox extends DurableObject {
|
|
|
410
538
|
}
|
|
411
539
|
}
|
|
412
540
|
export function getSandbox(namespace, id, options = {}) {
|
|
541
|
+
for (const [option, alternative] of Object.entries(OPTIONS))
|
|
542
|
+
if (options[option] !== undefined)
|
|
543
|
+
throw refusal("Cloudflare", `getSandbox ${option}`, alternative);
|
|
413
544
|
only("Cloudflare Worker getSandbox", options, ["normalizeId", "sleepAfter", "keepAlive"]);
|
|
414
545
|
sanitizeSandboxId(id);
|
|
415
546
|
const name = options.normalizeId ? id.toLowerCase() : id;
|
|
@@ -422,6 +553,16 @@ export function getSandbox(namespace, id, options = {}) {
|
|
|
422
553
|
// Await configuration for every operation so no request can race name setup.
|
|
423
554
|
return new Proxy(stub, {
|
|
424
555
|
get(target, key) {
|
|
556
|
+
// Cloudflare's getSandbox adds these on the Worker side; an RPC stub
|
|
557
|
+
// would answer every name, so refuse them here, by name.
|
|
558
|
+
if (key === "tunnels")
|
|
559
|
+
return tunnels;
|
|
560
|
+
if (key === "wsConnect")
|
|
561
|
+
return () => Promise.reject(refusal("Cloudflare", "Sandbox.wsConnect", "connect to the URL exposePort returns"));
|
|
562
|
+
if (key === "terminal")
|
|
563
|
+
return () => Promise.reject(refusal("Cloudflare", "Sandbox.terminal", TERMINAL));
|
|
564
|
+
if (key === "client")
|
|
565
|
+
throw refusal("Cloudflare", "Sandbox.client", "Runtime's API is the client");
|
|
425
566
|
const value = Reflect.get(target, key);
|
|
426
567
|
if (typeof value !== "function" || key === "then")
|
|
427
568
|
return value;
|
|
@@ -429,6 +570,51 @@ export function getSandbox(namespace, id, options = {}) {
|
|
|
429
570
|
},
|
|
430
571
|
});
|
|
431
572
|
}
|
|
573
|
+
for (const [member, alternative] of Object.entries(REFUSED))
|
|
574
|
+
Object.defineProperty(Sandbox.prototype, member, {
|
|
575
|
+
configurable: true,
|
|
576
|
+
writable: true,
|
|
577
|
+
value: () => Promise.reject(refusal("Cloudflare", `Sandbox.${member}`, alternative)),
|
|
578
|
+
});
|
|
579
|
+
/** getSandbox options Cloudflare's class reads that a Runtime sandbox has no
|
|
580
|
+
* counterpart for. */
|
|
581
|
+
const OPTIONS = {
|
|
582
|
+
containerTimeouts: "a Runtime sandbox has no container start to tune",
|
|
583
|
+
transport: "Runtime's API is the only transport",
|
|
584
|
+
enableDefaultSession: "commands share the default session; use createSession() for a separate shell",
|
|
585
|
+
labels: "call sandbox.setLabels(labels)",
|
|
586
|
+
};
|
|
587
|
+
const TUNNELS = "use exposePort for a URL to a port; Runtime serves it itself";
|
|
588
|
+
const tunnels = Object.fromEntries(["get", "list", "destroy"].map((method) => [
|
|
589
|
+
method,
|
|
590
|
+
() => Promise.reject(refusal("Cloudflare", `Sandbox.tunnels.${method}`, TUNNELS)),
|
|
591
|
+
]));
|
|
592
|
+
/** Cloudflare's preview URL for a port: `http` and the given port on a local
|
|
593
|
+
* hostname, `https` otherwise, built as its SDK builds it. */
|
|
594
|
+
function previewUrl(port, sandboxId, hostname, token) {
|
|
595
|
+
const id = sanitizeSandboxId(sandboxId).toLowerCase();
|
|
596
|
+
const [host = "", mainPort] = hostname.split(":");
|
|
597
|
+
const local = ["localhost", "127.0.0.1", "0.0.0.0"].includes(host);
|
|
598
|
+
const base = new URL(local ? `http://${host}:${mainPort || "80"}` : `https://${hostname}`);
|
|
599
|
+
base.hostname = `${port}-${id}-${token}.${host}`;
|
|
600
|
+
return base.toString();
|
|
601
|
+
}
|
|
602
|
+
function validateToken(token) {
|
|
603
|
+
if (token.length === 0)
|
|
604
|
+
throw new SandboxSecurityError("Custom token cannot be empty.", "INVALID_TOKEN");
|
|
605
|
+
if (token.length > 16)
|
|
606
|
+
throw new SandboxSecurityError(`Custom token too long. Maximum 16 characters allowed. Received: ${token.length} characters.`, "INVALID_TOKEN");
|
|
607
|
+
if (!/^[a-z0-9_]+$/.test(token))
|
|
608
|
+
throw new SandboxSecurityError("Custom token must contain only lowercase letters (a-z), numbers (0-9), and underscores (_). Invalid token provided.", "INVALID_TOKEN");
|
|
609
|
+
}
|
|
610
|
+
/** 16 characters of a-z, 0-9 and _, as Cloudflare's tokens are. */
|
|
611
|
+
function newToken() {
|
|
612
|
+
const bytes = crypto.getRandomValues(new Uint8Array(12));
|
|
613
|
+
return btoa(String.fromCharCode(...bytes))
|
|
614
|
+
.replace(/[+/]/g, "_")
|
|
615
|
+
.replace(/=/g, "")
|
|
616
|
+
.toLowerCase();
|
|
617
|
+
}
|
|
432
618
|
function previewRoute(url) {
|
|
433
619
|
const label = url.hostname.split(".")[0] ?? "";
|
|
434
620
|
const match = /^(\d{4,5})-([a-z0-9_-]+)-([a-z0-9_]{1,63})$/.exec(label);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** `withruntime/containers`: Containers alone. One OCI image's process on its
|
|
2
|
+
* own microVM, at the sandbox rates. In a private beta.
|
|
3
|
+
*
|
|
4
|
+
* import { containers } from "withruntime/containers";
|
|
5
|
+
* const client = containers(); // RUNTIME_API_KEY, or this machine's saved connection
|
|
6
|
+
*/
|
|
7
|
+
import { Runtime, type RuntimeOptions } from "../client.js";
|
|
8
|
+
export type * from "../products/containers.js";
|
|
9
|
+
/** `runtime.containers` of a client made with `options`. */
|
|
10
|
+
export declare function containers(options?: RuntimeOptions): Runtime["containers"];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** `withruntime/containers`: Containers alone. One OCI image's process on its
|
|
2
|
+
* own microVM, at the sandbox rates. In a private beta.
|
|
3
|
+
*
|
|
4
|
+
* import { containers } from "withruntime/containers";
|
|
5
|
+
* const client = containers(); // RUNTIME_API_KEY, or this machine's saved connection
|
|
6
|
+
*/
|
|
7
|
+
import { Runtime } from "../client.js";
|
|
8
|
+
/** `runtime.containers` of a client made with `options`. */
|
|
9
|
+
export function containers(options) {
|
|
10
|
+
return new Runtime(options).containers;
|
|
11
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -76,3 +76,6 @@ export { openWalletAccount, claimWalletAccount, rekeyWalletAccount, type OpenedW
|
|
|
76
76
|
export type { Topup } from "./products/billing.js";
|
|
77
77
|
export type { NewKey, LimitedKey, LimitedKeyAction, MadeKey, ListedKey, KeyUsage, } from "./products/api-keys.js";
|
|
78
78
|
export type { SsoConnection, SsoStatus } from "./products/sso.js";
|
|
79
|
+
export type { Agent, AgentDefinition, AgentEvent, AgentSession, AgentSessionHandle, AgentSessionState, AgentTemplate, AgentTemplateKey, AgentTrigger, AgentTriggerKind, AgentUse, CreateAgent, CreateAgentTrigger, StartAgentSession, } from "./products/agents.js";
|
|
80
|
+
export { AGENT_TEMPLATES, AGENT_USES } from "./products/agents.js";
|
|
81
|
+
export type { Container, ContainerState, CreateContainer, UpdateContainer, } from "./products/containers.js";
|
package/dist/index.js
CHANGED
package/dist/product-names.js
CHANGED
|
@@ -34,6 +34,8 @@ export const PRODUCT_NAMES = [
|
|
|
34
34
|
{ name: "jobs", title: "Jobs", engine: "machine", singular: "job", old: was("job") },
|
|
35
35
|
{ name: "workflows", title: "Workflows", engine: "machine", singular: "workflow", old: [] },
|
|
36
36
|
{ name: "builds", title: "Builds", engine: "machine", singular: "build", old: [] },
|
|
37
|
+
{ name: "agents", title: "Agents", engine: "machine", singular: "agent", old: [] },
|
|
38
|
+
{ name: "ci", title: "CI/CD", engine: "machine", old: [] },
|
|
37
39
|
// Disk
|
|
38
40
|
{ name: "volumes", title: "Volumes", engine: "disk", singular: "volume", old: was("volume") },
|
|
39
41
|
{
|
|
@@ -91,6 +93,7 @@ export const PRODUCT_NAMES = [
|
|
|
91
93
|
{ name: "usage", title: "Usage", engine: "meter", old: [] },
|
|
92
94
|
{ name: "billing", title: "Billing", engine: "meter", old: [] },
|
|
93
95
|
{ name: "metrics", title: "Metrics", engine: "meter", old: [] },
|
|
96
|
+
{ name: "plans", title: "Plans", engine: "meter", singular: "plan", old: [] },
|
|
94
97
|
// Database (not now: CLOUD.md section 2)
|
|
95
98
|
{ name: "postgres", title: "Postgres", engine: "database", old: [] },
|
|
96
99
|
{ name: "mysql", title: "MySQL", engine: "database", old: [] },
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** Agents: a coding or support agent's harness (Claude Code, Codex, Gemini
|
|
2
|
+
* CLI, or your own code on an agents SDK) in a sandbox of its own per
|
|
3
|
+
* session, on your own model key. Your app drives its sessions through this
|
|
4
|
+
* API and webhooks. In a private beta: outside it every call is refused with
|
|
5
|
+
* `product_in_beta`.
|
|
6
|
+
*
|
|
7
|
+
* const agent = await runtime.agents.create({
|
|
8
|
+
* name: "support",
|
|
9
|
+
* definition: {
|
|
10
|
+
* ...runtime.agents.templates["claude-code"].definition,
|
|
11
|
+
* ...runtime.agents.uses.support.definition,
|
|
12
|
+
* instructions: "Answer questions about our product. Be brief.",
|
|
13
|
+
* },
|
|
14
|
+
* });
|
|
15
|
+
* const session = await runtime.agents.start("support", { input: "Hi!", key: "customer:42" });
|
|
16
|
+
* for await (const event of session.events()) if (event.kind === "reply") console.log(event.body.text);
|
|
17
|
+
*
|
|
18
|
+
* A session's sandbox is billed while it runs, at $0.03 per active vCPU-hour
|
|
19
|
+
* and $0.01 per GiB-hour of memory; a waiting session's sandbox is paused.
|
|
20
|
+
* No session fee, no seats. */
|
|
21
|
+
import { Page } from "../page.js";
|
|
22
|
+
import type { RequestOptions, Transport } from "../transport.js";
|
|
23
|
+
export type AgentDefinition = {
|
|
24
|
+
/** What runs the harness, from the sandbox's home. */
|
|
25
|
+
command: string;
|
|
26
|
+
/** Where each session's sandbox starts: an image of yours, or a snapshot
|
|
27
|
+
* of a machine you set up by hand (a harness logged in to your own
|
|
28
|
+
* subscription included). The default image has Node and Python. */
|
|
29
|
+
from?: {
|
|
30
|
+
image: string;
|
|
31
|
+
} | {
|
|
32
|
+
snapshot: string;
|
|
33
|
+
};
|
|
34
|
+
/** 1, 2, 4, 8 or 16; default 4. */
|
|
35
|
+
vcpu?: number;
|
|
36
|
+
/** 512 to 65536, a multiple of 256; default 8192 (8 GiB). */
|
|
37
|
+
memoryMiB?: number;
|
|
38
|
+
/** The agent's standing words, at most 64 KiB. */
|
|
39
|
+
instructions?: string;
|
|
40
|
+
/** Settings the harness sees; never a secret. */
|
|
41
|
+
env?: Record<string, string>;
|
|
42
|
+
/** Names of your egress secrets the sandbox may use: it sees a placeholder,
|
|
43
|
+
* and the egress proxy adds the value toward the secret's hosts. Model
|
|
44
|
+
* keys of any provider go here (ANTHROPIC_API_KEY, OPENAI_API_KEY,
|
|
45
|
+
* GEMINI_API_KEY, OPENROUTER_API_KEY, or an OpenAI-compatible endpoint's
|
|
46
|
+
* key with env.OPENAI_BASE_URL). */
|
|
47
|
+
secrets?: string[];
|
|
48
|
+
/** MCP servers the harness may reach. */
|
|
49
|
+
mcp?: {
|
|
50
|
+
name: string;
|
|
51
|
+
url: string;
|
|
52
|
+
}[];
|
|
53
|
+
volumes?: {
|
|
54
|
+
volume: string;
|
|
55
|
+
path: string;
|
|
56
|
+
}[];
|
|
57
|
+
/** How long a session's sandbox runs with nothing happening before it
|
|
58
|
+
* pauses; default 60. */
|
|
59
|
+
idlePauseSeconds?: number;
|
|
60
|
+
/** A session's longest life; default 86400. */
|
|
61
|
+
maxSessionSeconds?: number;
|
|
62
|
+
/** Sessions running at once; more wait, queued. Default 10. */
|
|
63
|
+
maxConcurrent?: number;
|
|
64
|
+
/** The use it was set up for (`uses`). */
|
|
65
|
+
template?: "support" | "team";
|
|
66
|
+
};
|
|
67
|
+
export type Agent = {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
version: number;
|
|
71
|
+
/** With its defaults written in. */
|
|
72
|
+
definition: AgentDefinition;
|
|
73
|
+
labels: Record<string, string>;
|
|
74
|
+
/** `subscription_for_customers`: its secrets name CLAUDE_CODE_OAUTH_TOKEN
|
|
75
|
+
* and it is set up to answer your customers (`template: "support"`). A
|
|
76
|
+
* subscription is for your own team's agents; an agent that serves your
|
|
77
|
+
* customers uses an API key. */
|
|
78
|
+
warnings: "subscription_for_customers"[];
|
|
79
|
+
createdAt: string;
|
|
80
|
+
updatedAt: string;
|
|
81
|
+
};
|
|
82
|
+
export type AgentSessionState = "queued" | "running" | "waiting" | "done" | "failed" | "stopped";
|
|
83
|
+
/** One session. Subagents' parentSessionId and rootSessionId, its cost and
|
|
84
|
+
* treeCost, historyBytes and attachmentBytes join it as the API gains
|
|
85
|
+
* them. */
|
|
86
|
+
export type AgentSession = {
|
|
87
|
+
id: string;
|
|
88
|
+
agentId: string;
|
|
89
|
+
agentVersion: number;
|
|
90
|
+
key: string | null;
|
|
91
|
+
trigger: "api" | AgentTriggerKind;
|
|
92
|
+
state: AgentSessionState;
|
|
93
|
+
/** Why it ended. */
|
|
94
|
+
reason: string | null;
|
|
95
|
+
/** What the agent said it ended with: a pull request's address, an answer. */
|
|
96
|
+
result: string | null;
|
|
97
|
+
sandboxId: string | null;
|
|
98
|
+
lastSeq: number;
|
|
99
|
+
labels: Record<string, string>;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
startedAt: string | null;
|
|
102
|
+
endedAt: string | null;
|
|
103
|
+
};
|
|
104
|
+
export type AgentEvent = {
|
|
105
|
+
seq: number;
|
|
106
|
+
kind: "input" | "answer" | "reply" | "ask" | "status" | "result" | "error" | "note";
|
|
107
|
+
/** A key's principal id, slack:<user>, github:<login>, agent (the
|
|
108
|
+
* harness) or runtime. */
|
|
109
|
+
actor: string;
|
|
110
|
+
/** Words are body.text. */
|
|
111
|
+
body: {
|
|
112
|
+
text?: string;
|
|
113
|
+
} & Record<string, unknown>;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
};
|
|
116
|
+
export type AgentTriggerKind = "slack" | "github" | "schedule" | "webhook";
|
|
117
|
+
export type AgentTrigger = {
|
|
118
|
+
id: string;
|
|
119
|
+
agentId: string;
|
|
120
|
+
kind: AgentTriggerKind;
|
|
121
|
+
config: Record<string, unknown>;
|
|
122
|
+
pausedAt: string | null;
|
|
123
|
+
pausedWhy: string | null;
|
|
124
|
+
createdAt: string;
|
|
125
|
+
/** A webhook's secret, which each of its calls carries: answered once,
|
|
126
|
+
* when it is added, and never again. Keep it where you keep your keys. */
|
|
127
|
+
secret?: string;
|
|
128
|
+
};
|
|
129
|
+
export type CreateAgent = {
|
|
130
|
+
name: string;
|
|
131
|
+
definition: AgentDefinition;
|
|
132
|
+
labels?: Record<string, string>;
|
|
133
|
+
};
|
|
134
|
+
export type StartAgentSession = {
|
|
135
|
+
input: string;
|
|
136
|
+
/** Words with the same key join its open session: a Slack thread, a pull
|
|
137
|
+
* request, one of your customers (`customer:42`). */
|
|
138
|
+
key?: string;
|
|
139
|
+
labels?: Record<string, string>;
|
|
140
|
+
};
|
|
141
|
+
export type CreateAgentTrigger = {
|
|
142
|
+
kind: "slack";
|
|
143
|
+
config: {
|
|
144
|
+
team: string;
|
|
145
|
+
channel: string;
|
|
146
|
+
};
|
|
147
|
+
} | {
|
|
148
|
+
kind: "github";
|
|
149
|
+
config: {
|
|
150
|
+
installation: number;
|
|
151
|
+
repository: string;
|
|
152
|
+
events?: string[];
|
|
153
|
+
};
|
|
154
|
+
} | {
|
|
155
|
+
kind: "schedule";
|
|
156
|
+
config: {
|
|
157
|
+
cron: string;
|
|
158
|
+
timezone?: string;
|
|
159
|
+
input: string;
|
|
160
|
+
};
|
|
161
|
+
} | {
|
|
162
|
+
kind: "webhook";
|
|
163
|
+
config: Record<string, never>;
|
|
164
|
+
};
|
|
165
|
+
/** The five harnesses' names, so `templates["claude-code"]` is known to be there. */
|
|
166
|
+
export type AgentTemplateKey = "claude-code" | "codex" | "gemini-cli" | "claude-agent-sdk" | "openai-agents-sdk";
|
|
167
|
+
/** A harness, as data (packages/cloud-guide/agent-templates.json, the same
|
|
168
|
+
* in every SDK and the guide). `definition` is what an agent's definition
|
|
169
|
+
* takes from it; add a use's (`uses`) and your instructions. */
|
|
170
|
+
export type AgentTemplate = {
|
|
171
|
+
key: AgentTemplateKey;
|
|
172
|
+
title: string;
|
|
173
|
+
command: string;
|
|
174
|
+
/** The egress secret its API key is stored as, and the host it is for. */
|
|
175
|
+
apiKeySecret: string;
|
|
176
|
+
apiKeyHost: string;
|
|
177
|
+
/** How your own subscription logs it in, for your own team's agents, or
|
|
178
|
+
* null when it takes an API key alone. */
|
|
179
|
+
subscription: string | null;
|
|
180
|
+
definition: {
|
|
181
|
+
command: string;
|
|
182
|
+
secrets: string[];
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
/** A use: what an agent is for, merged into a template's definition. */
|
|
186
|
+
export type AgentUse = {
|
|
187
|
+
key: "support" | "team";
|
|
188
|
+
title: string;
|
|
189
|
+
/** How its sessions are keyed: the key your app passes per customer, or
|
|
190
|
+
* none. */
|
|
191
|
+
sessionKey: string | null;
|
|
192
|
+
triggers: ("api" | AgentTriggerKind)[];
|
|
193
|
+
definition: Partial<AgentDefinition> & {
|
|
194
|
+
template: "support" | "team";
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
/** The five harnesses (agents-contract.md); tests/agents.test.ts holds them
|
|
198
|
+
* to packages/cloud-guide/agent-templates.json. */
|
|
199
|
+
export declare const AGENT_TEMPLATES: Readonly<Record<AgentTemplateKey, AgentTemplate>>;
|
|
200
|
+
export declare const AGENT_USES: Readonly<Record<"support" | "team", AgentUse>>;
|
|
201
|
+
/** One session, to talk to and follow. */
|
|
202
|
+
export type AgentSessionHandle = {
|
|
203
|
+
readonly id: string;
|
|
204
|
+
/** The session as it was when started or read. */
|
|
205
|
+
readonly session: AgentSession;
|
|
206
|
+
/** Whether the start's words joined a session already open with its key. */
|
|
207
|
+
readonly joined: boolean;
|
|
208
|
+
/** The seq of the words that started or joined it. */
|
|
209
|
+
readonly seq: number;
|
|
210
|
+
/** Words in; answers their seq. */
|
|
211
|
+
send(text: string, options?: RequestOptions): Promise<{
|
|
212
|
+
seq: number;
|
|
213
|
+
}>;
|
|
214
|
+
/** Its log after `after` (0 by default), each event as it comes, waiting
|
|
215
|
+
* up to 30 seconds a call; ends when the session has ended. */
|
|
216
|
+
events(options?: {
|
|
217
|
+
after?: number;
|
|
218
|
+
signal?: AbortSignal;
|
|
219
|
+
}): AsyncGenerator<AgentEvent>;
|
|
220
|
+
stop(options?: RequestOptions): Promise<AgentSession>;
|
|
221
|
+
/** Until it has ended; answers it then. */
|
|
222
|
+
wait(options?: {
|
|
223
|
+
signal?: AbortSignal;
|
|
224
|
+
}): Promise<AgentSession>;
|
|
225
|
+
get(options?: RequestOptions): Promise<AgentSession>;
|
|
226
|
+
};
|
|
227
|
+
export declare function agents(t: Transport): {
|
|
228
|
+
/** An agent: a name and a definition its sessions start from. */
|
|
229
|
+
create: (input: CreateAgent, options?: RequestOptions) => Promise<Agent>;
|
|
230
|
+
/** By its id or its name. */
|
|
231
|
+
get: (agent: string, options?: RequestOptions) => Promise<Agent>;
|
|
232
|
+
/** Your agents, oldest first. Await for a page, or `for await` for all. */
|
|
233
|
+
list: (query?: {
|
|
234
|
+
cursor?: string;
|
|
235
|
+
limit?: number;
|
|
236
|
+
}, options?: RequestOptions) => Promise<Page<Agent>>;
|
|
237
|
+
/** A new version of the definition, whole; sessions already started keep theirs. */
|
|
238
|
+
update: (agent: string, definition: AgentDefinition, options?: RequestOptions) => Promise<Agent>;
|
|
239
|
+
/** Its triggers go and its open sessions stop. */
|
|
240
|
+
delete: (agent: string, options?: RequestOptions) => Promise<void>;
|
|
241
|
+
/** A session with its first words, or the open one with the same key joined. */
|
|
242
|
+
start(agent: string, input: StartAgentSession, options?: RequestOptions): Promise<AgentSessionHandle>;
|
|
243
|
+
/** A session you already have, by its id. */
|
|
244
|
+
session(id: string, options?: RequestOptions): Promise<AgentSessionHandle>;
|
|
245
|
+
sessions: {
|
|
246
|
+
get: (id: string, options?: RequestOptions) => Promise<AgentSession>;
|
|
247
|
+
/** Newest first; of one agent, or in some states. */
|
|
248
|
+
list: (query?: {
|
|
249
|
+
agent?: string;
|
|
250
|
+
state?: AgentSessionState[];
|
|
251
|
+
cursor?: string;
|
|
252
|
+
limit?: number;
|
|
253
|
+
}, options?: RequestOptions) => Promise<Page<AgentSession>>;
|
|
254
|
+
/** An ended session and its log go. Its history is storage like any
|
|
255
|
+
* other, kept until you delete it; an open session is refused
|
|
256
|
+
* (`session_open`): stop it first. */
|
|
257
|
+
delete: (id: string, options?: RequestOptions) => Promise<void>;
|
|
258
|
+
};
|
|
259
|
+
triggers: {
|
|
260
|
+
create: (agent: string, input: CreateAgentTrigger, options?: RequestOptions) => Promise<AgentTrigger>;
|
|
261
|
+
list: (agent: string, options?: RequestOptions) => Promise<AgentTrigger[]>;
|
|
262
|
+
/** Its events are refused, saying `why`; nothing else is held. */
|
|
263
|
+
pause: (id: string, why: string, options?: RequestOptions) => Promise<AgentTrigger>;
|
|
264
|
+
resume: (id: string, options?: RequestOptions) => Promise<AgentTrigger>;
|
|
265
|
+
delete: (id: string, options?: RequestOptions) => Promise<void>;
|
|
266
|
+
};
|
|
267
|
+
/** The harnesses, as data: claude-code, codex, gemini-cli,
|
|
268
|
+
* claude-agent-sdk, openai-agents-sdk. */
|
|
269
|
+
templates: Readonly<Record<AgentTemplateKey, AgentTemplate>>;
|
|
270
|
+
/** The uses, as data: support (your customers) and team (your team). */
|
|
271
|
+
uses: Readonly<Record<"team" | "support", AgentUse>>;
|
|
272
|
+
};
|