withruntime 0.7.0 → 0.7.2
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/E2B.md +1 -1
- package/dist/ai-harness/index.js +5 -0
- package/dist/cli.js +49 -12
- package/dist/e2b/commands.js +11 -2
- package/dist/e2b/errors.d.ts +5 -0
- package/dist/e2b/errors.js +3 -0
- package/dist/login.d.ts +9 -36
- package/dist/login.js +0 -22
- package/dist/sandbox.d.ts +3 -2
- package/dist/sandbox.js +7 -6
- package/dist/tar.js +72 -14
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/types.d.ts +14 -1
- package/package.json +14 -3
package/E2B.md
CHANGED
|
@@ -58,7 +58,7 @@ machine. An E2B key (`e2b_...`) is never sent anywhere. If one is passed as
|
|
|
58
58
|
| `envs` | Given to every command and code run through this object. |
|
|
59
59
|
| `allowInternetAccess: false` | `network: { internet: false }`. |
|
|
60
60
|
| `lifecycle.onTimeout` | `onLeaseEnd`: `kill` is `stop`, `pause` is `pause`. |
|
|
61
|
-
| `commands.run(cmd, { cwd, envs, timeoutMs, onStdout, onStderr })` | `exec`, streamed, so the whole output comes back however large. Default timeout 60 s. `0` means no limit (Runtime's longest, 24 hours).
|
|
61
|
+
| `commands.run(cmd, { cwd, envs, timeoutMs, onStdout, onStderr })` | `exec`, streamed, so the whole output comes back however large; output lost before it was read sets `truncated` and warns. Default timeout 60 s. `0` means no limit (Runtime's longest, 24 hours). |
|
|
62
62
|
| Non-zero exit | Throws `CommandExitError` with `exitCode`, `stdout`, `stderr` and `error` (`exit status N`). A command killed by a signal has `exitCode` -1. |
|
|
63
63
|
| Command past its timeout | Throws `TimeoutError`. |
|
|
64
64
|
| `commands.run(cmd, { background: true })` | `spawn`. Returns a `CommandHandle` with `pid`, `wait()`, `kill()` (SIGKILL), `sendStdin`, `closeStdin`, `disconnect`, `stdout`, `stderr`, `exitCode`. |
|
package/dist/ai-harness/index.js
CHANGED
|
@@ -467,6 +467,11 @@ export function networkRules(policy) {
|
|
|
467
467
|
if (policy.mode === "deny-all")
|
|
468
468
|
return { internet: false };
|
|
469
469
|
const allow = [...(policy.allowedHosts ?? []), ...(policy.allowedCIDRs ?? [])];
|
|
470
|
+
// `custom` is an allow list; one that allows nothing is `deny-all` in the
|
|
471
|
+
// harness's own terms. Runtime reads an empty allow as no allow list at all,
|
|
472
|
+
// which would open the whole internet.
|
|
473
|
+
if (!allow.length)
|
|
474
|
+
return { internet: false };
|
|
470
475
|
const deny = [...(policy.deniedCIDRs ?? [])];
|
|
471
476
|
return { internet: true, allow, ...(deny.length ? { deny } : {}) };
|
|
472
477
|
}
|
package/dist/cli.js
CHANGED
|
@@ -140,7 +140,7 @@ const SANDBOX_HELP = `runtime sandbox <command>
|
|
|
140
140
|
[--option SERVER.NAME=value]... [--port 8765] [--replace]
|
|
141
141
|
Start them; prints each URL and the header to send
|
|
142
142
|
mcp <id> [status] | mcp <id> stop Their state and fresh URLs; stop them
|
|
143
|
-
preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address
|
|
143
|
+
preview <id> <port> [--public] [--ttl <s>] Share a port at an HTTPS address (--public: paid only)
|
|
144
144
|
preview rotate <id> <port> Refuse every token given out for a private port so
|
|
145
145
|
far; prints the new token
|
|
146
146
|
previews <id> Every shared port
|
|
@@ -494,7 +494,10 @@ function remote(spec) {
|
|
|
494
494
|
return match ? { id: match[1], path: match[2] } : undefined;
|
|
495
495
|
}
|
|
496
496
|
export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
497
|
-
|
|
497
|
+
// Only what comes before -- is the CLI's: a --json after it is the
|
|
498
|
+
// command's, whether or not the CLI was given its own.
|
|
499
|
+
const dash = argv.indexOf("--");
|
|
500
|
+
const [first, ...remaining] = argv.filter((arg, index) => arg !== "--json" || (dash !== -1 && index > dash));
|
|
498
501
|
// Sandbox is one product of several, so its commands live under it
|
|
499
502
|
// (AGENTS.md, 23 September 2026): `runtime sandbox run`, `sandbox ssh` and
|
|
500
503
|
// `sandbox port-forward`. The old top-level forms were removed, not aliased.
|
|
@@ -598,11 +601,17 @@ export async function run(argv, env = process.env, out = defaultOut(argv)) {
|
|
|
598
601
|
if (command === "login" || command === "logout") {
|
|
599
602
|
const { authenticationCommand } = await import("./login.js");
|
|
600
603
|
const result = await authenticationCommand([command, ...rest], env);
|
|
604
|
+
// Which account it connected to, so a person with several can tell. The
|
|
605
|
+
// connection stands whether or not this read answers.
|
|
606
|
+
const who = command === "login" && "connected" in result && result.connected
|
|
607
|
+
? await (await client(env)).me().catch(() => undefined)
|
|
608
|
+
: undefined;
|
|
609
|
+
const account = who ? (who.orgName ? `${who.orgName} (${who.orgId})` : who.orgId) : undefined;
|
|
601
610
|
print(result.pending
|
|
602
611
|
? `Not connected yet. When it is approved, run any command, or \`${me} login --wait\`; it uses this link.`
|
|
603
612
|
: command === "login"
|
|
604
|
-
? `Connected${"agentName" in result && result.agentName ? ` as ${String(result.agentName)}` : ""}.`
|
|
605
|
-
: "Disconnected.", result);
|
|
613
|
+
? `Connected${"agentName" in result && result.agentName ? ` as ${String(result.agentName)}` : ""}${account ? ` to ${account}` : ""}.`
|
|
614
|
+
: "Disconnected.", who ? { ...result, orgId: who.orgId, orgName: who.orgName ?? null } : result);
|
|
606
615
|
return 0;
|
|
607
616
|
}
|
|
608
617
|
if (command === "whoami") {
|
|
@@ -974,6 +983,14 @@ function usageSummary(u) {
|
|
|
974
983
|
"free trial",
|
|
975
984
|
`${hours(u.trial.availableMs)} of ${hours(u.trial.totalMs)} hours left${u.trial.reservedMs > 0 ? `, ${hours(u.trial.reservedMs)} held by running sandboxes` : ""}`,
|
|
976
985
|
]);
|
|
986
|
+
if (u.outbound) {
|
|
987
|
+
const gib = (bytes) => `${(bytes / 1_073_741_824).toLocaleString("en-US", { maximumFractionDigits: 1 })} GiB`;
|
|
988
|
+
const left = Math.max(0, u.outbound.allowanceBytes - u.outbound.freeBytes);
|
|
989
|
+
rows.push([
|
|
990
|
+
"outbound traffic this month",
|
|
991
|
+
`${gib(u.outbound.sentBytes)} sent, ${gib(left)} of ${gib(u.outbound.allowanceBytes)} free left, ${dollars(micros(u.outbound.chargedMicros))} charged`,
|
|
992
|
+
]);
|
|
993
|
+
}
|
|
977
994
|
const kinds = new Map();
|
|
978
995
|
for (const resource of u.resources ?? []) {
|
|
979
996
|
const kind = typeof resource.kind === "string" ? resource.kind : "other";
|
|
@@ -1038,13 +1055,14 @@ async function outputSoFar(runtime, sandboxId, processId) {
|
|
|
1038
1055
|
chunks,
|
|
1039
1056
|
};
|
|
1040
1057
|
}
|
|
1041
|
-
/** Dollars from integer microdollars: cents from a dollar up,
|
|
1042
|
-
*
|
|
1058
|
+
/** Dollars from integer microdollars: cents from a dollar up, four places
|
|
1059
|
+
* below one, and every place below a hundredth of a cent, so a short trial
|
|
1060
|
+
* test never reads as $0.0000. */
|
|
1043
1061
|
function dollars(micros) {
|
|
1044
1062
|
const value = BigInt(micros);
|
|
1045
1063
|
const sign = value < 0n ? "-" : "";
|
|
1046
1064
|
const size = value < 0n ? -value : value;
|
|
1047
|
-
const places = size
|
|
1065
|
+
const places = size >= 1000000n ? 2 : size >= 100n || size === 0n ? 4 : 6;
|
|
1048
1066
|
const unit = 10n ** BigInt(6 - places);
|
|
1049
1067
|
const rounded = (size + unit / 2n) / unit;
|
|
1050
1068
|
const whole = rounded / 10n ** BigInt(places);
|
|
@@ -1106,11 +1124,28 @@ function switchingLine(summary, provider) {
|
|
|
1106
1124
|
: `${recorded}; its first top-up was not matched. Ask support if you think that is wrong.${once}`;
|
|
1107
1125
|
}
|
|
1108
1126
|
}
|
|
1127
|
+
/** Running time in the largest unit that keeps it above zero: a short
|
|
1128
|
+
* trial test is seconds, not "0 hours". */
|
|
1129
|
+
function runningTime(seconds) {
|
|
1130
|
+
const [amount, unit] = seconds < 60
|
|
1131
|
+
? [seconds, "second"]
|
|
1132
|
+
: seconds < 3600
|
|
1133
|
+
? [seconds / 60, "minute"]
|
|
1134
|
+
: [seconds / 3600, "hour"];
|
|
1135
|
+
const shown = amount.toLocaleString("en-US", {
|
|
1136
|
+
maximumFractionDigits: unit === "second" ? 1 : 2,
|
|
1137
|
+
});
|
|
1138
|
+
return `${shown} ${unit}${shown === "1" ? "" : "s"}`;
|
|
1139
|
+
}
|
|
1109
1140
|
/** The sandboxes the free trial paid for, said beside what was priced. */
|
|
1110
|
-
function trialNote(trial, all) {
|
|
1141
|
+
function trialNote(trial, all, trialSeconds) {
|
|
1111
1142
|
if (!trial)
|
|
1112
1143
|
return "";
|
|
1113
|
-
const which = trial === all
|
|
1144
|
+
const which = trial === all
|
|
1145
|
+
? all === 1
|
|
1146
|
+
? "It"
|
|
1147
|
+
: "All of them"
|
|
1148
|
+
: `${trial} of them, ${runningTime(trialSeconds)} of that time,`;
|
|
1114
1149
|
return ` ${which} ran on the free trial, which charged nothing; Runtime's side prices ${trial === 1 ? "it" : "them"} at the standard rates, what the same work costs on paid credit.`;
|
|
1115
1150
|
}
|
|
1116
1151
|
async function switching(command, argv, env, out) {
|
|
@@ -1172,13 +1207,13 @@ async function switching(command, argv, env, out) {
|
|
|
1172
1207
|
const lines = c.basis === "usage"
|
|
1173
1208
|
? [
|
|
1174
1209
|
`Your last ${c.window.days} days: ${figures}${pace}`,
|
|
1175
|
-
`Priced: ${c.usage.sandboxes.toLocaleString("en-US")} sandbox${c.usage.sandboxes === 1 ? "" : "es"}, ${(c.usage.runSeconds
|
|
1210
|
+
`Priced: ${c.usage.sandboxes.toLocaleString("en-US")} sandbox${c.usage.sandboxes === 1 ? "" : "es"}, ${runningTime(c.usage.runSeconds)} running.${trialNote(c.usage.trialSandboxes, c.usage.sandboxes, c.usage.trialRunSeconds)}${c.usage.unpricedSandboxes ? ` ${c.usage.unpricedSandboxes} left out: ${c.rival.name} publishes no price for their size.` : ""}`,
|
|
1176
1211
|
]
|
|
1177
1212
|
: [
|
|
1178
1213
|
`No settled sandboxes in your last ${c.window.days} days yet, so this prices an example: ${c.usage.sandboxes.toLocaleString("en-US")} runs of ${(c.usage.runSeconds / c.usage.sandboxes).toLocaleString("en-US")} seconds using ${(c.usage.activeCpuSeconds / c.usage.sandboxes).toLocaleString("en-US")} CPU-seconds each.`,
|
|
1179
1214
|
`The example: ${figures}.`,
|
|
1180
1215
|
];
|
|
1181
|
-
lines.push(`${c.rival.name}'s published rates, checked ${longDate(`${c.rival.checked}T12:00:00Z`)}: ${c.rival.rates.join("; ")}. Compute only: storage, network, plan fees and free allowances are left out.`);
|
|
1216
|
+
lines.push(`${c.rival.name}'s published rates, checked ${longDate(`${c.rival.checked}T12:00:00Z`)}: ${c.rival.rates.join("; ")}. Compute only, with the disk where a rival bills it while running: storage at rest, network, plan fees and free allowances are left out.`);
|
|
1182
1217
|
const line = switchingLine(c.switching, Object.hasOwn(SWITCH_FROM, from) ? SWITCH_FROM[from] : undefined);
|
|
1183
1218
|
if (line)
|
|
1184
1219
|
lines.push("", line);
|
|
@@ -2636,8 +2671,10 @@ ownInterrupt = true) {
|
|
|
2636
2671
|
}
|
|
2637
2672
|
if (out.json)
|
|
2638
2673
|
out.write(JSON.stringify(result));
|
|
2674
|
+
// Here the output streamed, whose timeout is 24 hours unless one was given
|
|
2675
|
+
// (Sandbox.exec); `--json` answers the result and prints no such line.
|
|
2639
2676
|
else if (result.timedOut)
|
|
2640
|
-
out.error(`runtime: timed out after ${timeout
|
|
2677
|
+
out.error(`runtime: timed out after ${timeout ? `${timeout} s` : "24 h"}; the command's process group was killed.`);
|
|
2641
2678
|
// Never drop output silently: say so, and do not exit 0.
|
|
2642
2679
|
const lost = extras.lostOutput(result, sbx.id);
|
|
2643
2680
|
if (lost && !out.json)
|
package/dist/e2b/commands.js
CHANGED
|
@@ -28,7 +28,8 @@ function timedOut(timeoutMs) {
|
|
|
28
28
|
"Pass a larger 'timeoutMs', or 0 for no limit.");
|
|
29
29
|
}
|
|
30
30
|
/** A Runtime result as E2B's: a command killed by a signal has no exit code on
|
|
31
|
-
* Runtime; E2B reports -1.
|
|
31
|
+
* Runtime; E2B reports -1. E2B never loses output, so a result that did says
|
|
32
|
+
* so in `truncated` rather than passing as whole. */
|
|
32
33
|
function toResult(result) {
|
|
33
34
|
const exitCode = result.exitCode ?? -1;
|
|
34
35
|
return {
|
|
@@ -40,12 +41,15 @@ function toResult(result) {
|
|
|
40
41
|
: {
|
|
41
42
|
error: result.exitCode === null ? "terminated by a signal" : `exit status ${exitCode}`,
|
|
42
43
|
}),
|
|
44
|
+
...(result.truncated ? { truncated: true } : {}),
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
function settle(result, timeoutMs) {
|
|
46
48
|
if (result.timedOut)
|
|
47
49
|
throw timedOut(timeoutMs);
|
|
48
50
|
const out = toResult(result);
|
|
51
|
+
if (out.truncated && typeof process !== "undefined" && process.emitWarning)
|
|
52
|
+
process.emitWarning("Part of the command's output was dropped before it was read, so stdout and stderr are incomplete (result.truncated). Write large output to a file and read it with sandbox.files.read.", { code: "RUNTIME_E2B_OUTPUT_TRUNCATED" });
|
|
49
53
|
if (out.exitCode !== 0)
|
|
50
54
|
throw new CommandExitError(out);
|
|
51
55
|
return out;
|
|
@@ -80,7 +84,7 @@ export class Commands {
|
|
|
80
84
|
...(onStderr ? { onStderr: (text) => void onStderr(text) } : {}),
|
|
81
85
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
82
86
|
}));
|
|
83
|
-
return settle(result, timeoutMs);
|
|
87
|
+
return settle({ ...result, truncated: result.stdoutTruncated === true || result.stderrTruncated === true }, timeoutMs);
|
|
84
88
|
}
|
|
85
89
|
async #start(cmd, opts, timeoutMs) {
|
|
86
90
|
const process = await guard("sandbox", () => this.#ctx.runtime.spawn(cmd, {
|
|
@@ -166,6 +170,7 @@ export class CommandHandle {
|
|
|
166
170
|
#stdout = "";
|
|
167
171
|
#stderr = "";
|
|
168
172
|
#exit;
|
|
173
|
+
#truncated = false;
|
|
169
174
|
#failure;
|
|
170
175
|
#disconnected = false;
|
|
171
176
|
constructor(process, options) {
|
|
@@ -189,6 +194,9 @@ export class CommandHandle {
|
|
|
189
194
|
this.#stderr += event.data;
|
|
190
195
|
await options.onStderr?.(event.data);
|
|
191
196
|
}
|
|
197
|
+
else if (event.type === "truncated") {
|
|
198
|
+
this.#truncated = true;
|
|
199
|
+
}
|
|
192
200
|
else if (event.type === "exit") {
|
|
193
201
|
this.#exit = { exitCode: event.exitCode, timedOut: event.timedOut };
|
|
194
202
|
}
|
|
@@ -237,6 +245,7 @@ export class CommandHandle {
|
|
|
237
245
|
stdout: this.#stdout,
|
|
238
246
|
stderr: this.#stderr,
|
|
239
247
|
timedOut: this.#exit.timedOut,
|
|
248
|
+
truncated: this.#truncated,
|
|
240
249
|
}, this.#timeoutMs);
|
|
241
250
|
}
|
|
242
251
|
/** Stops receiving output; the command keeps running. */
|
package/dist/e2b/errors.d.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface CommandResult {
|
|
|
52
52
|
error?: string;
|
|
53
53
|
stdout: string;
|
|
54
54
|
stderr: string;
|
|
55
|
+
/** Runtime's addition, present only when true: some of the command's
|
|
56
|
+
* output was dropped before it was read, so `stdout` and `stderr` are
|
|
57
|
+
* missing part of it. A warning says so too. */
|
|
58
|
+
truncated?: true;
|
|
55
59
|
}
|
|
56
60
|
/** Thrown by `commands.run` and `CommandHandle.wait` when the command exits
|
|
57
61
|
* with a code other than 0. It carries the result, as in E2B. */
|
|
@@ -62,6 +66,7 @@ export declare class CommandExitError extends SandboxError implements CommandRes
|
|
|
62
66
|
get error(): string | undefined;
|
|
63
67
|
get stdout(): string;
|
|
64
68
|
get stderr(): string;
|
|
69
|
+
get truncated(): true | undefined;
|
|
65
70
|
}
|
|
66
71
|
/** Which object a call was about, to name a 404 the way E2B would. */
|
|
67
72
|
export type Subject = "sandbox" | "file" | "other";
|
package/dist/e2b/errors.js
CHANGED
|
@@ -92,6 +92,9 @@ export class CommandExitError extends SandboxError {
|
|
|
92
92
|
get stderr() {
|
|
93
93
|
return this.#result.stderr;
|
|
94
94
|
}
|
|
95
|
+
get truncated() {
|
|
96
|
+
return this.#result.truncated;
|
|
97
|
+
}
|
|
95
98
|
}
|
|
96
99
|
/** Turns a Runtime SDK error into the E2B error a caller written for E2B
|
|
97
100
|
* expects. Anything that is not a Runtime error passes through unchanged. */
|
package/dist/login.d.ts
CHANGED
|
@@ -15,34 +15,11 @@ type LoginOptions = {
|
|
|
15
15
|
* at the same machine to press Connect agent. */
|
|
16
16
|
export declare const AGENT_WAIT_MS = 50000;
|
|
17
17
|
export declare function authenticationCommand(args: string[], env: NodeJS.ProcessEnv, options?: LoginOptions): Promise<{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
reason?: undefined;
|
|
21
|
-
agentName?: undefined;
|
|
22
|
-
orgId?: undefined;
|
|
23
|
-
disconnected?: undefined;
|
|
24
|
-
pending?: undefined;
|
|
25
|
-
url?: undefined;
|
|
26
|
-
userCode?: undefined;
|
|
27
|
-
expiresAt?: undefined;
|
|
28
|
-
} | {
|
|
29
|
-
connected: boolean;
|
|
30
|
-
source?: undefined;
|
|
31
|
-
reason?: undefined;
|
|
18
|
+
disconnected: boolean;
|
|
19
|
+
connected?: undefined;
|
|
32
20
|
agentName?: undefined;
|
|
33
21
|
orgId?: undefined;
|
|
34
|
-
disconnected?: undefined;
|
|
35
|
-
pending?: undefined;
|
|
36
|
-
url?: undefined;
|
|
37
|
-
userCode?: undefined;
|
|
38
|
-
expiresAt?: undefined;
|
|
39
|
-
} | {
|
|
40
|
-
connected: boolean;
|
|
41
|
-
reason: string;
|
|
42
22
|
source?: undefined;
|
|
43
|
-
agentName?: undefined;
|
|
44
|
-
orgId?: undefined;
|
|
45
|
-
disconnected?: undefined;
|
|
46
23
|
pending?: undefined;
|
|
47
24
|
url?: undefined;
|
|
48
25
|
userCode?: undefined;
|
|
@@ -51,18 +28,16 @@ export declare function authenticationCommand(args: string[], env: NodeJS.Proces
|
|
|
51
28
|
connected: boolean;
|
|
52
29
|
agentName: string;
|
|
53
30
|
orgId: string;
|
|
54
|
-
source
|
|
55
|
-
reason?: undefined;
|
|
31
|
+
source: string;
|
|
56
32
|
disconnected?: undefined;
|
|
57
33
|
pending?: undefined;
|
|
58
34
|
url?: undefined;
|
|
59
35
|
userCode?: undefined;
|
|
60
36
|
expiresAt?: undefined;
|
|
61
37
|
} | {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
reason?: undefined;
|
|
38
|
+
connected: boolean;
|
|
39
|
+
source: string;
|
|
40
|
+
disconnected?: undefined;
|
|
66
41
|
agentName?: undefined;
|
|
67
42
|
orgId?: undefined;
|
|
68
43
|
pending?: undefined;
|
|
@@ -73,9 +48,8 @@ export declare function authenticationCommand(args: string[], env: NodeJS.Proces
|
|
|
73
48
|
connected: boolean;
|
|
74
49
|
agentName: string;
|
|
75
50
|
orgId: string;
|
|
76
|
-
source: string;
|
|
77
|
-
reason?: undefined;
|
|
78
51
|
disconnected?: undefined;
|
|
52
|
+
source?: undefined;
|
|
79
53
|
pending?: undefined;
|
|
80
54
|
url?: undefined;
|
|
81
55
|
userCode?: undefined;
|
|
@@ -86,11 +60,10 @@ export declare function authenticationCommand(args: string[], env: NodeJS.Proces
|
|
|
86
60
|
url: string;
|
|
87
61
|
userCode: string;
|
|
88
62
|
expiresAt: string;
|
|
89
|
-
|
|
90
|
-
reason?: undefined;
|
|
63
|
+
disconnected?: undefined;
|
|
91
64
|
agentName?: undefined;
|
|
92
65
|
orgId?: undefined;
|
|
93
|
-
|
|
66
|
+
source?: undefined;
|
|
94
67
|
}>;
|
|
95
68
|
export type PendingLogin = {
|
|
96
69
|
/** Open this in a browser, where the account owner approves the agent. */
|
package/dist/login.js
CHANGED
|
@@ -109,28 +109,6 @@ export async function authenticationCommand(args, env, options = {}) {
|
|
|
109
109
|
const notify = options.notify ?? ((message) => process.stderr.write(`${message}\n`));
|
|
110
110
|
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
111
111
|
const [command, ...rest] = args;
|
|
112
|
-
if (command === "auth" && rest.length === 1 && rest[0] === "status") {
|
|
113
|
-
if (env.RUNTIME_API_KEY) {
|
|
114
|
-
await new Runtime({
|
|
115
|
-
apiKey: env.RUNTIME_API_KEY,
|
|
116
|
-
baseUrl: origins.api,
|
|
117
|
-
fetch: fetcher,
|
|
118
|
-
}).usage();
|
|
119
|
-
return { connected: true, source: "environment" };
|
|
120
|
-
}
|
|
121
|
-
const saved = await store.read();
|
|
122
|
-
if (!saved)
|
|
123
|
-
return { connected: false };
|
|
124
|
-
try {
|
|
125
|
-
await request("confirm", undefined, saved.key);
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
if (error instanceof ConnectionError && error.status === 401)
|
|
129
|
-
return { connected: false, reason: "revoked" };
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
return { connected: true, agentName: saved.agentName, orgId: saved.orgId };
|
|
133
|
-
}
|
|
134
112
|
if (command === "logout" && rest.length === 0) {
|
|
135
113
|
if (env.RUNTIME_API_KEY)
|
|
136
114
|
throw new Error("This shell uses RUNTIME_API_KEY. Remove that override before disconnecting the saved connection.");
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -218,8 +218,9 @@ export declare class Files {
|
|
|
218
218
|
*/
|
|
219
219
|
readStream(path: string, options?: RequestOptions): Promise<ReadableStream<Uint8Array>>;
|
|
220
220
|
readText(path: string, options?: RequestOptions): Promise<string>;
|
|
221
|
-
/** Writes a file
|
|
222
|
-
* ones
|
|
221
|
+
/** Writes a file, atomically, making parent directories. Under /workspace
|
|
222
|
+
* any size, large ones in parallel 1 MiB chunks checked against their
|
|
223
|
+
* SHA-256; elsewhere, with the sandbox user's rights, up to 1 MiB. `mode`
|
|
223
224
|
* sets its permissions (0o755 for a program); 0o644 when left out. */
|
|
224
225
|
write(path: string, data: string | Uint8Array, writeOptions?: RequestOptions & {
|
|
225
226
|
mode?: number;
|
package/dist/sandbox.js
CHANGED
|
@@ -408,10 +408,6 @@ export class Sandbox {
|
|
|
408
408
|
return this.#info;
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
-
/** A streamed command keeps all its output unless the reader fell more than the
|
|
412
|
-
* process's output buffer behind, which the server marks with a "truncated"
|
|
413
|
-
* event. That event does not say which stream lost bytes, so both flags carry
|
|
414
|
-
* it; without one, both are false, as a synchronous result's are. */
|
|
415
411
|
/** Where a reader of a process's output is, in bytes of stdout and stderr
|
|
416
412
|
* together. Output it did not receive, named by the server or not, becomes a
|
|
417
413
|
* `truncated` event: the next chunk starting past the cursor means the bytes
|
|
@@ -448,6 +444,10 @@ class OutputCursor {
|
|
|
448
444
|
function cutOff(error) {
|
|
449
445
|
return !(error instanceof RuntimeError) || error instanceof ConnectionError;
|
|
450
446
|
}
|
|
447
|
+
/** A streamed command keeps all its output unless the reader fell more than the
|
|
448
|
+
* process's output buffer behind, which the server marks with a "truncated"
|
|
449
|
+
* event. That event does not say which stream lost bytes, so both flags carry
|
|
450
|
+
* it; without one, both are false, as a synchronous result's are. */
|
|
451
451
|
function truncation(dropped) {
|
|
452
452
|
return { stdoutTruncated: dropped, stderrTruncated: dropped };
|
|
453
453
|
}
|
|
@@ -718,8 +718,9 @@ export class Files {
|
|
|
718
718
|
async readText(path, options = {}) {
|
|
719
719
|
return new TextDecoder().decode(await this.read(path, options));
|
|
720
720
|
}
|
|
721
|
-
/** Writes a file
|
|
722
|
-
* ones
|
|
721
|
+
/** Writes a file, atomically, making parent directories. Under /workspace
|
|
722
|
+
* any size, large ones in parallel 1 MiB chunks checked against their
|
|
723
|
+
* SHA-256; elsewhere, with the sandbox user's rights, up to 1 MiB. `mode`
|
|
723
724
|
* sets its permissions (0o755 for a program); 0o644 when left out. */
|
|
724
725
|
async write(path, data, writeOptions = {}) {
|
|
725
726
|
const { mode, ...options } = writeOptions;
|
package/dist/tar.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile,
|
|
2
|
-
import { join, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { chmod, lstat, mkdir, readdir, readFile, realpath, symlink, unlink, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
4
4
|
/* A small ustar writer and reader for directory uploads and downloads, so the
|
|
5
5
|
SDK needs no dependency. Regular files, directories and symbolic links;
|
|
@@ -68,17 +68,62 @@ export async function packDirectory(root) {
|
|
|
68
68
|
parts.push(new Uint8Array(1024));
|
|
69
69
|
return gzipSync(Buffer.concat(parts));
|
|
70
70
|
}
|
|
71
|
+
/* The archive comes from a sandbox, whose contents the customer's untrusted code
|
|
72
|
+
controls, so nothing in it may write outside `target`. A lexical check on each
|
|
73
|
+
name is not enough: a link `x -> ../outside` followed by `x/file` passes it and
|
|
74
|
+
writes through the link. So no write ever passes through a link, links are made
|
|
75
|
+
only after every file and directory is in place, and each link is then walked
|
|
76
|
+
through the finished tree and removed, failing the unpack, if it leads out. */
|
|
77
|
+
async function linkAt(path) {
|
|
78
|
+
try {
|
|
79
|
+
return (await lstat(path)).isSymbolicLink();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Throws unless `destination` is inside `root` and no part of the way to it,
|
|
86
|
+
itself included, is a link. */
|
|
87
|
+
async function assertPlain(root, destination, name) {
|
|
88
|
+
if (destination !== root && !destination.startsWith(root + sep))
|
|
89
|
+
throw new Error(`Refusing an archive entry outside the target: ${name}`);
|
|
90
|
+
let path = root;
|
|
91
|
+
for (const part of relative(root, destination).split(sep).filter(Boolean)) {
|
|
92
|
+
path = join(path, part);
|
|
93
|
+
if (await linkAt(path))
|
|
94
|
+
throw new Error(`Refusing an archive entry that passes through a link: ${name}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Where following `link` from `directory` leads, or undefined when the way
|
|
98
|
+
leaves `root` or turns at a link before its last step. The last step may
|
|
99
|
+
be a link; that link is checked on its own. */
|
|
100
|
+
async function linkStaysInside(root, directory, link) {
|
|
101
|
+
if (isAbsolute(link))
|
|
102
|
+
return false;
|
|
103
|
+
const parts = link.split("/").filter((part) => part && part !== ".");
|
|
104
|
+
let path = directory;
|
|
105
|
+
for (const [index, part] of parts.entries()) {
|
|
106
|
+
path = part === ".." ? dirname(path) : join(path, part);
|
|
107
|
+
if (path !== root && !path.startsWith(root + sep))
|
|
108
|
+
return false;
|
|
109
|
+
if (index < parts.length - 1 && part !== ".." && (await linkAt(path)))
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
71
114
|
/** Unpacks into `target`, refusing any entry that would land outside it. */
|
|
72
115
|
export async function unpackArchive(archive, target) {
|
|
73
116
|
const data = gunzipSync(archive);
|
|
74
|
-
|
|
75
|
-
|
|
117
|
+
await mkdir(resolve(target), { recursive: true });
|
|
118
|
+
const root = await realpath(resolve(target));
|
|
76
119
|
const text = (start, length) => {
|
|
77
120
|
const slice = data.subarray(start, start + length);
|
|
78
121
|
const end = slice.indexOf(0);
|
|
79
122
|
return new TextDecoder().decode(end < 0 ? slice : slice.subarray(0, end));
|
|
80
123
|
};
|
|
124
|
+
const links = [];
|
|
81
125
|
let longName;
|
|
126
|
+
let longLink;
|
|
82
127
|
for (let offset = 0; offset + 512 <= data.length;) {
|
|
83
128
|
if (data.subarray(offset, offset + 512).every((byte) => byte === 0))
|
|
84
129
|
break;
|
|
@@ -86,31 +131,44 @@ export async function unpackArchive(archive, target) {
|
|
|
86
131
|
const type = String.fromCharCode(data[offset + 156] ?? 48);
|
|
87
132
|
const prefix = text(offset + 345, 155);
|
|
88
133
|
let name = longName ?? (prefix ? `${prefix}/${text(offset, 100)}` : text(offset, 100));
|
|
134
|
+
const link = longLink ?? text(offset + 157, 100);
|
|
89
135
|
longName = undefined;
|
|
136
|
+
longLink = undefined;
|
|
90
137
|
const mode = parseInt(text(offset + 100, 8).trim() || "644", 8);
|
|
91
|
-
const link = text(offset + 157, 100);
|
|
92
138
|
const body = data.subarray(offset + 512, offset + 512 + size);
|
|
93
139
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
94
|
-
if (type === "L") {
|
|
95
|
-
|
|
140
|
+
if (type === "L" || type === "K") {
|
|
141
|
+
const value = new TextDecoder().decode(body).replace(/\0.*$/s, "");
|
|
142
|
+
if (type === "L")
|
|
143
|
+
longName = value;
|
|
144
|
+
else
|
|
145
|
+
longLink = value;
|
|
96
146
|
continue;
|
|
97
147
|
}
|
|
98
148
|
name = name.replace(/^\.\//, "");
|
|
99
149
|
if (!name || name === "." || name === "./")
|
|
100
150
|
continue;
|
|
101
151
|
const destination = resolve(root, name);
|
|
102
|
-
|
|
103
|
-
throw new Error(`Refusing an archive entry outside the target: ${name}`);
|
|
152
|
+
await assertPlain(root, destination, name);
|
|
104
153
|
if (type === "5")
|
|
105
154
|
await mkdir(destination, { recursive: true });
|
|
106
|
-
else if (type === "2")
|
|
107
|
-
|
|
108
|
-
await symlink(link, destination).catch(() => undefined);
|
|
109
|
-
}
|
|
155
|
+
else if (type === "2")
|
|
156
|
+
links.push({ destination, link, name });
|
|
110
157
|
else if (type === "0" || type === "\0" || type === "7") {
|
|
111
|
-
await mkdir(
|
|
158
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
112
159
|
await writeFile(destination, body);
|
|
113
160
|
await chmod(destination, mode & 0o777);
|
|
114
161
|
}
|
|
115
162
|
}
|
|
163
|
+
for (const { destination, link, name } of links) {
|
|
164
|
+
await assertPlain(root, destination, name);
|
|
165
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
166
|
+
await symlink(link, destination).catch(() => undefined);
|
|
167
|
+
}
|
|
168
|
+
for (const { destination, link, name } of links) {
|
|
169
|
+
if (await linkStaysInside(root, dirname(destination), link))
|
|
170
|
+
continue;
|
|
171
|
+
await unlink(destination).catch(() => undefined);
|
|
172
|
+
throw new Error(`Refusing an archive link that leads outside the target: ${name} -> ${link}`);
|
|
173
|
+
}
|
|
116
174
|
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
-
export declare const VERSION = "0.7.
|
|
2
|
+
export declare const VERSION = "0.7.2";
|
|
3
3
|
export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
4
4
|
export type RequestOptions = {
|
|
5
5
|
/** Sent as Idempotency-Key. Omit it: the SDK makes one per call and keeps
|
package/dist/transport.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConnectionError, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
|
|
2
2
|
import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
|
|
3
|
-
export const VERSION = "0.7.
|
|
3
|
+
export const VERSION = "0.7.2";
|
|
4
4
|
export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
5
5
|
export function apiOrigin(value) {
|
|
6
6
|
const url = new URL(value);
|
package/dist/types.d.ts
CHANGED
|
@@ -118,7 +118,8 @@ export type ExecOptions = {
|
|
|
118
118
|
env?: Record<string, string>;
|
|
119
119
|
/** Given to standard input, then closed. */
|
|
120
120
|
stdin?: string | Uint8Array;
|
|
121
|
-
/** Default 60 000
|
|
121
|
+
/** Default 60 000, or 24 hours when the output streams (onStdout, onStderr or
|
|
122
|
+
* execStream); up to 24 hours. A timeout is a result (timedOut), not an error. */
|
|
122
123
|
timeoutMs?: number;
|
|
123
124
|
onStdout?: (text: string) => void;
|
|
124
125
|
onStderr?: (text: string) => void;
|
|
@@ -201,6 +202,18 @@ export type Usage = {
|
|
|
201
202
|
reservedMs: number;
|
|
202
203
|
availableMs: number;
|
|
203
204
|
} | null;
|
|
205
|
+
/** Outbound traffic this calendar month, UTC: the first `allowanceBytes`
|
|
206
|
+
* (100 GiB) an account sends are free, the rest $0.02 per decimal GB. */
|
|
207
|
+
outbound?: {
|
|
208
|
+
month: string;
|
|
209
|
+
sentBytes: number;
|
|
210
|
+
freeBytes: number;
|
|
211
|
+
billableBytes: number;
|
|
212
|
+
allowanceBytes: number;
|
|
213
|
+
chargedMicros: string;
|
|
214
|
+
/** What the balance or a spending limit could not cover; never charged. */
|
|
215
|
+
writtenOffMicros: string;
|
|
216
|
+
} | null;
|
|
204
217
|
resources: Array<Record<string, unknown>>;
|
|
205
218
|
[key: string]: unknown;
|
|
206
219
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "withruntime",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"mcpName": "com.withruntime/runtime",
|
|
6
6
|
"type": "module",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"prepack": "bun run build",
|
|
77
77
|
"check-types": "tsc --noEmit",
|
|
78
78
|
"lint": "eslint",
|
|
79
|
-
"test": "bun test && bun scripts/test-python.ts && bun scripts/test-go.ts && bun ../../sdks/java/scripts/check.ts && bun ../../sdks/ruby/scripts/check.ts",
|
|
79
|
+
"test": "bun test --parallel && bun scripts/test-python.ts && bun scripts/test-go.ts && bun ../../sdks/java/scripts/check.ts && bun ../../sdks/ruby/scripts/check.ts",
|
|
80
80
|
"test:java": "bun ../../sdks/java/scripts/check.ts",
|
|
81
81
|
"test:ruby": "bun ../../sdks/ruby/scripts/check.ts",
|
|
82
82
|
"test:python": "bun scripts/test-python.ts",
|
|
@@ -110,14 +110,25 @@
|
|
|
110
110
|
"registry": "https://registry.npmjs.org/"
|
|
111
111
|
},
|
|
112
112
|
"description": "Runtime Cloud: one client, CLI and MCP bridge for every Runtime product. Sandboxes first.",
|
|
113
|
+
"homepage": "https://withruntime.com",
|
|
114
|
+
"repository": {
|
|
115
|
+
"type": "git",
|
|
116
|
+
"url": "git+https://github.com/withruntime/runtime.git",
|
|
117
|
+
"directory": "packages/cloud-sdk"
|
|
118
|
+
},
|
|
119
|
+
"bugs": "https://github.com/withruntime/runtime/issues",
|
|
113
120
|
"sideEffects": false,
|
|
114
121
|
"keywords": [
|
|
115
122
|
"sandbox",
|
|
116
123
|
"cloud",
|
|
117
124
|
"agents",
|
|
125
|
+
"ai-agents",
|
|
118
126
|
"mcp",
|
|
119
127
|
"microvm",
|
|
120
|
-
"
|
|
128
|
+
"firecracker",
|
|
129
|
+
"code-execution",
|
|
130
|
+
"code-interpreter",
|
|
131
|
+
"llm"
|
|
121
132
|
],
|
|
122
133
|
"dependencies": {
|
|
123
134
|
"undici": "^7.29.1"
|