@blaxel/eve-sandbox 0.3.12-preview.250

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Beamlit, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @blaxel/eve-sandbox
2
+
3
+ [Blaxel](https://blaxel.ai) sandbox backend for [eve](https://eve.dev).
4
+
5
+ Run eve's built-in shell and file tools in secure, durable Blaxel sandboxes. Each durable eve session reconnects to the same sandbox after an application restart without relying on preview-only Blaxel APIs.
6
+
7
+ ## Quick start
8
+
9
+ You need Node.js 24 or later and an existing [eve project](https://eve.dev/docs/installation).
10
+
11
+ ### 1. Install the backend
12
+
13
+ ```bash
14
+ npm install @blaxel/eve-sandbox
15
+ ```
16
+
17
+ The package expects the `eve` and `ai` versions already installed by your eve project.
18
+
19
+ ### 2. Authenticate with Blaxel
20
+
21
+ For local development, sign in with the [Blaxel CLI](https://docs.blaxel.ai/cli-reference/overview):
22
+
23
+ ```bash
24
+ bl login YOUR-WORKSPACE
25
+ ```
26
+
27
+ In CI and production, provide `BL_WORKSPACE` and `BL_API_KEY` to the application runtime. Do not copy those credentials into the sandbox.
28
+
29
+ ### 3. Select Blaxel in eve
30
+
31
+ ```ts
32
+ // agent/sandbox.ts
33
+ import { blaxel } from "@blaxel/eve-sandbox";
34
+ import { defineSandbox } from "eve/sandbox";
35
+
36
+ export default defineSandbox({
37
+ backend: blaxel({
38
+ image: "blaxel/ts-app:latest",
39
+ lifecycle: {
40
+ expirationPolicies: [{ type: "ttl-idle", value: "1d", action: "delete" }],
41
+ },
42
+ memory: 4096,
43
+ }),
44
+ async onSession({ use }) {
45
+ const sandbox = await use({
46
+ networkPolicy: { allow: ["github.com", "*.npmjs.org"] },
47
+ });
48
+ await sandbox.run({ command: "mkdir -p /workspace/project" });
49
+ },
50
+ });
51
+ ```
52
+
53
+ Start eve as usual:
54
+
55
+ ```bash
56
+ npm run dev
57
+ ```
58
+
59
+ The built-in `bash`, `read_file`, `write_file`, `glob`, and `grep` tools now operate inside the Blaxel sandbox. To smoke-test the setup in eve's terminal UI, ask the agent to create `/workspace/hello.txt` and read it back.
60
+
61
+ A copy-ready sandbox definition is available in [`examples/agent/sandbox.ts`](./examples/agent/sandbox.ts).
62
+
63
+ ## What works
64
+
65
+ - All five built-in eve sandbox tools
66
+ - Blocking commands and streaming background processes
67
+ - Text, binary, and streaming file operations
68
+ - Durable filesystem and process state across turns and app restarts
69
+ - Custom Blaxel images, regions, resources, volumes, environment variables, and lifecycle policies
70
+ - Per-session setup through `onSession()`
71
+ - Domain egress policies and secret-backed header transforms
72
+ - Deterministic reconnection through eve's captured session metadata
73
+
74
+ ## Generally available capability scope
75
+
76
+ The backend intentionally uses only generally available Blaxel sandbox APIs. It does not call Blaxel snapshot or fork APIs.
77
+
78
+ eve's `bootstrap()` hook and files under `agent/sandbox/workspace` require a backend to clone a build-time template. This backend does not implement template prewarming. If either feature is configured, it fails with actionable guidance before serving traffic.
79
+
80
+ Put reusable system dependencies and base files in a [custom Blaxel image](https://docs.blaxel.ai/Sandboxes/Templates). Put setup that must run once for each durable eve session in `onSession()`.
81
+
82
+ ## Lifecycle
83
+
84
+ - `create()` reconnects a saved sandbox by name or creates a new sandbox from the configured image.
85
+ - `onSession()` performs one-time setup for a new durable eve session.
86
+ - Session names include the backend configuration identity, so a changed sandbox definition gets a new sandbox.
87
+ - `captureState()` persists the Blaxel sandbox name in eve's durable session state.
88
+ - `shutdown()` closes active log streams. Blaxel then moves the idle sandbox into standby while preserving memory, processes, and files.
89
+
90
+ Session discovery uses Blaxel resources and does not depend on an in-process registry.
91
+
92
+ ## Network policy
93
+
94
+ The backend supports:
95
+
96
+ - `"allow-all"`
97
+ - `"deny-all"`
98
+ - Domain allow-lists
99
+ - eve header transforms through Blaxel's secret-backed egress proxy
100
+
101
+ The backend rejects subnet rules, request match conditions, and `forwardURL`. It does not silently weaken unsupported policies.
102
+
103
+ The default is `"allow-all"`. For production or untrusted workloads, select `"deny-all"` or an explicit allow-list before the first command runs.
104
+
105
+ ## Options
106
+
107
+ `blaxel()` accepts standard `SandboxCreateConfiguration` fields such as `image`, `memory`, `region`, `ports`, `envs`, `volumes`, `ttl`, `lifecycle`, and `labels`.
108
+
109
+ | Option | Purpose | Default |
110
+ | --- | --- | --- |
111
+ | `backendName` | Stable eve backend identifier | `blaxel` |
112
+ | `namePrefix` | Prefix for deterministic Blaxel resources | `eve` |
113
+ | `networkPolicy` | Egress policy applied when a session sandbox is created | `"allow-all"` |
114
+ | `processOutputBufferBytes` | Maximum unread stdout or stderr per process stream | 1 MiB |
115
+ | `startupTimeoutMs` | Maximum cold-image readiness wait | 60 seconds |
116
+
117
+ ## Compatibility
118
+
119
+ | Package | eve | AI SDK | Node.js | Default image |
120
+ | --- | --- | --- | --- | --- |
121
+ | `@blaxel/eve-sandbox` 0.2.x | 0.31.3 to less than 0.32 | 7.0.38 to less than 8 | 24 or later | `blaxel/ts-app:latest` |
122
+
123
+ Each release must pass the adapter contract suite and a live eve agent evaluation against its declared range. A new eve minor requires a tested peer-range update.
124
+
125
+ ## Cleanup
126
+
127
+ Blaxel sandboxes remain durable until their configured lifecycle removes them. Set `lifecycle` or `ttl` when the application requires automatic deletion of inactive sessions.
@@ -0,0 +1,23 @@
1
+ import { type SandboxCreateConfiguration } from "@blaxel/core";
2
+ import { type SandboxBackend, type SandboxNetworkPolicy } from "eve/sandbox";
3
+ export type BlaxelEveUseOptions = {
4
+ readonly networkPolicy?: SandboxNetworkPolicy;
5
+ };
6
+ type BlaxelEveSandboxCreateOptions = Pick<SandboxCreateConfiguration, "envs" | "expires" | "extraArgs" | "image" | "labels" | "lifecycle" | "memory" | "ports" | "region" | "ttl" | "volumes">;
7
+ export type BlaxelEveBackendOptions = BlaxelEveSandboxCreateOptions & {
8
+ /** Stable eve backend name. Changing it invalidates persisted backend state. */
9
+ readonly backendName?: string;
10
+ /** Prefix used for deterministic Blaxel resource names. */
11
+ readonly namePrefix?: string;
12
+ /** Initial policy for new session sandboxes. */
13
+ readonly networkPolicy?: SandboxNetworkPolicy;
14
+ /** Maximum buffered stdout or stderr bytes for an unread process stream. */
15
+ readonly processOutputBufferBytes?: number;
16
+ /** Maximum time to wait for a cold sandbox image to accept commands. */
17
+ readonly startupTimeoutMs?: number;
18
+ };
19
+ /** Create a first-party Blaxel implementation of eve's public SandboxBackend. */
20
+ export declare function blaxel(options?: BlaxelEveBackendOptions): SandboxBackend<BlaxelEveUseOptions, BlaxelEveUseOptions>;
21
+ export { createBlaxelEveSession } from "./session.js";
22
+ export { toBlaxelNetworkPolicy } from "./network-policy.js";
23
+ export default blaxel;
package/dist/index.js ADDED
@@ -0,0 +1,251 @@
1
+ import { createHash } from "node:crypto";
2
+ import { ResponseError, SandboxInstance, isGatewayError, } from "@blaxel/core";
3
+ import { toBlaxelNetworkPolicy } from "./network-policy.js";
4
+ import { createBlaxelEveSession } from "./session.js";
5
+ const DEFAULT_BACKEND_NAME = "blaxel";
6
+ const DEFAULT_NAME_PREFIX = "eve";
7
+ const DEFAULT_IMAGE = "blaxel/ts-app:latest";
8
+ const DEFAULT_STARTUP_TIMEOUT_MS = 60_000;
9
+ const MAX_SANDBOX_NAME_LENGTH = 49;
10
+ /** Create a first-party Blaxel implementation of eve's public SandboxBackend. */
11
+ export function blaxel(options = {}) {
12
+ const { backendName = DEFAULT_BACKEND_NAME, namePrefix = DEFAULT_NAME_PREFIX, networkPolicy, processOutputBufferBytes, startupTimeoutMs = DEFAULT_STARTUP_TIMEOUT_MS, ...sandboxOptions } = options;
13
+ validateOptions({
14
+ backendName,
15
+ namePrefix,
16
+ processOutputBufferBytes,
17
+ startupTimeoutMs,
18
+ });
19
+ const normalizedPrefix = normalizeNamePrefix(namePrefix);
20
+ const configurationIdentity = stableHash({
21
+ sandboxOptions,
22
+ networkPolicy: networkPolicy ?? "allow-all",
23
+ });
24
+ return {
25
+ name: backendName,
26
+ prewarm(input) {
27
+ return Promise.reject(templatePrewarmUnsupported(input.templateKey));
28
+ },
29
+ async create(input) {
30
+ try {
31
+ if (input.templateKey !== null) {
32
+ throw templatePrewarmUnsupported(input.templateKey);
33
+ }
34
+ const labels = resourceLabels("session", input.tags, sandboxOptions.labels);
35
+ const existing = await findExistingSession(input);
36
+ if (existing) {
37
+ await SandboxInstance.updateMetadata(existing.metadata.name, { labels });
38
+ if (networkPolicy !== undefined)
39
+ await applyNetworkPolicy(existing, networkPolicy);
40
+ return createHandle(existing, input.sessionKey);
41
+ }
42
+ const sessionName = resourceName(normalizedPrefix, "session", `${input.sessionKey}\0${configurationIdentity}`);
43
+ const sandbox = await SandboxInstance.createIfNotExists({
44
+ ...sandboxOptions,
45
+ image: sandboxOptions.image ?? DEFAULT_IMAGE,
46
+ labels,
47
+ name: sessionName,
48
+ network: initialNetwork(networkPolicy),
49
+ });
50
+ await ensureBaseRuntime(sandbox, startupTimeoutMs);
51
+ return createHandle(sandbox, input.sessionKey);
52
+ }
53
+ catch (error) {
54
+ throw new Error(`Blaxel backend failed to open sandbox session "${input.sessionKey}": ${errorMessage(error)}`, { cause: error });
55
+ }
56
+ },
57
+ };
58
+ async function findExistingSession(input) {
59
+ const persistedName = readString(input.existingMetadata, "sandboxName");
60
+ const deterministicName = resourceName(normalizedPrefix, "session", `${input.sessionKey}\0${configurationIdentity}`);
61
+ const names = [...new Set([persistedName, deterministicName].filter(Boolean))];
62
+ for (const name of names) {
63
+ const sandbox = await tryGetSandbox(name);
64
+ if (sandbox)
65
+ return sandbox;
66
+ }
67
+ return null;
68
+ }
69
+ function createHandle(sandbox, sessionKey) {
70
+ const openProcessStreams = new Set();
71
+ const session = createBlaxelEveSession({
72
+ id: sessionKey,
73
+ processOutputBufferBytes,
74
+ sandbox,
75
+ registerProcessStream(close) {
76
+ openProcessStreams.add(close);
77
+ return () => openProcessStreams.delete(close);
78
+ },
79
+ });
80
+ return {
81
+ session,
82
+ async useSessionFn(useOptions) {
83
+ if (useOptions?.networkPolicy !== undefined) {
84
+ await applyNetworkPolicy(sandbox, useOptions.networkPolicy);
85
+ }
86
+ return session;
87
+ },
88
+ captureState() {
89
+ return Promise.resolve({
90
+ backendName,
91
+ metadata: { sandboxName: sandbox.metadata.name },
92
+ sessionKey,
93
+ });
94
+ },
95
+ shutdown() {
96
+ for (const close of openProcessStreams)
97
+ close();
98
+ openProcessStreams.clear();
99
+ // Blaxel automatically suspends an idle sandbox and preserves its full state.
100
+ return Promise.resolve();
101
+ },
102
+ };
103
+ }
104
+ }
105
+ export { createBlaxelEveSession } from "./session.js";
106
+ export { toBlaxelNetworkPolicy } from "./network-policy.js";
107
+ export default blaxel;
108
+ async function applyNetworkPolicy(sandbox, policy) {
109
+ await SandboxInstance.updateNetwork(sandbox.metadata.name, {
110
+ network: toBlaxelNetworkPolicy(policy),
111
+ });
112
+ }
113
+ function initialNetwork(policy) {
114
+ return policy === undefined ? undefined : toBlaxelNetworkPolicy(policy);
115
+ }
116
+ async function ensureBaseRuntime(sandbox, startupTimeoutMs) {
117
+ const deadline = Date.now() + startupTimeoutMs;
118
+ let retryDelayMs = 500;
119
+ for (;;) {
120
+ try {
121
+ const result = await sandbox.process.exec({
122
+ command: "mkdir -p /workspace && command -v bash >/dev/null",
123
+ waitForCompletion: true,
124
+ workingDir: "/",
125
+ });
126
+ if (result.exitCode !== 0) {
127
+ throw new Error("The Blaxel sandbox image must provide bash.");
128
+ }
129
+ return;
130
+ }
131
+ catch (error) {
132
+ if (!isRetryableStartupError(error) || Date.now() >= deadline)
133
+ throw error;
134
+ const remainingMs = deadline - Date.now();
135
+ await delay(Math.min(retryDelayMs, remainingMs));
136
+ retryDelayMs = Math.min(retryDelayMs * 2, 30_000);
137
+ }
138
+ }
139
+ }
140
+ async function tryGetSandbox(name) {
141
+ try {
142
+ return await SandboxInstance.get(name);
143
+ }
144
+ catch (error) {
145
+ if (isNotFoundError(error))
146
+ return null;
147
+ throw error;
148
+ }
149
+ }
150
+ function resourceName(prefix, kind, identity) {
151
+ const hashLength = MAX_SANDBOX_NAME_LENGTH - prefix.length - kind.length - 2;
152
+ return `${prefix}-${kind}-${stableHash(identity).slice(0, hashLength)}`;
153
+ }
154
+ function resourceLabels(kind, tags, configured) {
155
+ const labels = {
156
+ ...(configured ?? {}),
157
+ "blaxel-integration": "eve",
158
+ "eve-resource": kind,
159
+ };
160
+ for (const [key, value] of Object.entries(tags ?? {})) {
161
+ labels[`eve-${normalizeLabel(key, 59)}`] = normalizeLabel(value, 63);
162
+ }
163
+ return labels;
164
+ }
165
+ function normalizeNamePrefix(value) {
166
+ const normalized = value
167
+ .toLowerCase()
168
+ .replace(/[^a-z0-9-]+/g, "-")
169
+ .replace(/^-+|-+$/g, "")
170
+ .slice(0, 20);
171
+ if (!normalized)
172
+ throw new Error("namePrefix must contain a letter or number");
173
+ return normalized;
174
+ }
175
+ function normalizeLabel(value, maxLength) {
176
+ return value
177
+ .toLowerCase()
178
+ .replace(/[^a-z0-9._-]+/g, "-")
179
+ .replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "")
180
+ .slice(0, maxLength)
181
+ .replace(/[^a-z0-9]+$/g, "") || "unknown";
182
+ }
183
+ function stableHash(value) {
184
+ return createHash("sha256").update(stableSerialize(value)).digest("hex");
185
+ }
186
+ function stableSerialize(value) {
187
+ if (value instanceof Date)
188
+ return JSON.stringify(value.toISOString());
189
+ if (Array.isArray(value))
190
+ return `[${value.map(stableSerialize).join(",")}]`;
191
+ if (value && typeof value === "object") {
192
+ const entries = Object.entries(value)
193
+ .filter(([, item]) => item !== undefined)
194
+ .sort(([left], [right]) => left.localeCompare(right));
195
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableSerialize(item)}`).join(",")}}`;
196
+ }
197
+ return JSON.stringify(value) ?? "null";
198
+ }
199
+ function readString(record, key) {
200
+ const value = record?.[key];
201
+ return typeof value === "string" && value.length > 0 ? value : undefined;
202
+ }
203
+ function validateOptions(input) {
204
+ if (!input.backendName.trim())
205
+ throw new Error("backendName cannot be empty");
206
+ normalizeNamePrefix(input.namePrefix);
207
+ if (!Number.isFinite(input.startupTimeoutMs) || input.startupTimeoutMs <= 0) {
208
+ throw new Error("startupTimeoutMs must be a positive finite number");
209
+ }
210
+ if (input.processOutputBufferBytes !== undefined &&
211
+ (!Number.isInteger(input.processOutputBufferBytes) || input.processOutputBufferBytes <= 0)) {
212
+ throw new Error("processOutputBufferBytes must be a positive integer");
213
+ }
214
+ }
215
+ function isRetryableStartupError(error) {
216
+ if (isGatewayError(error))
217
+ return true;
218
+ return (error instanceof ResponseError &&
219
+ error.status === 404 &&
220
+ /WORKLOAD_UNAVAILABLE|currently not available/iu.test(error.message));
221
+ }
222
+ function isNotFoundError(error) {
223
+ if (!error || typeof error !== "object")
224
+ return false;
225
+ const candidate = error;
226
+ return candidate.code === 404 || candidate.code === "404" || candidate.status === 404 || candidate.statusCode === 404;
227
+ }
228
+ function templatePrewarmUnsupported(templateKey) {
229
+ return new Error(`Blaxel eve template prewarming is not supported (template "${templateKey}"). ` +
230
+ "Use a custom Blaxel image for reusable dependencies and files, omit eve bootstrap() " +
231
+ "and agent/sandbox/workspace seed files, and perform one-time session setup in onSession().");
232
+ }
233
+ function errorMessage(error) {
234
+ if (error instanceof Error)
235
+ return error.message;
236
+ if (error && typeof error === "object") {
237
+ const { message } = error;
238
+ if (typeof message === "string")
239
+ return message;
240
+ try {
241
+ return JSON.stringify(error);
242
+ }
243
+ catch {
244
+ return "Unknown error object";
245
+ }
246
+ }
247
+ return String(error);
248
+ }
249
+ function delay(milliseconds) {
250
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
251
+ }
@@ -0,0 +1,4 @@
1
+ import type { SandboxNetwork } from "@blaxel/core";
2
+ import type { SandboxNetworkPolicy } from "eve/sandbox";
3
+ /** Convert eve's firewall shape into Blaxel's domain proxy configuration. */
4
+ export declare function toBlaxelNetworkPolicy(policy: SandboxNetworkPolicy): SandboxNetwork;
@@ -0,0 +1,53 @@
1
+ /** Convert eve's firewall shape into Blaxel's domain proxy configuration. */
2
+ export function toBlaxelNetworkPolicy(policy) {
3
+ if (policy === "allow-all")
4
+ return {};
5
+ if (policy === "deny-all") {
6
+ return { proxy: { allowedDomains: [], routing: [] } };
7
+ }
8
+ const input = policy;
9
+ if ((input.subnets?.allow?.length ?? 0) > 0 || (input.subnets?.deny?.length ?? 0) > 0) {
10
+ throw new Error("The Blaxel eve backend does not support eve subnet allow or deny rules.");
11
+ }
12
+ const entries = normalizeAllowEntries(input.allow);
13
+ const allowAll = entries.some(([domain]) => domain === "*");
14
+ const allowedDomains = allowAll ? undefined : entries.map(([domain]) => domain);
15
+ const routing = entries.flatMap(([domain, rules], domainIndex) => {
16
+ const headers = {};
17
+ const secrets = {};
18
+ rules.forEach((rule, ruleIndex) => {
19
+ if (rule.match !== undefined) {
20
+ throw new Error(`The Blaxel eve backend cannot apply a request match condition for ${domain}.`);
21
+ }
22
+ if (rule.forwardURL !== undefined) {
23
+ throw new Error(`The Blaxel eve backend cannot forward requests for ${domain} to another URL.`);
24
+ }
25
+ rule.transform?.forEach((transform, transformIndex) => {
26
+ Object.entries(transform.headers ?? {}).forEach(([header, value], headerIndex) => {
27
+ const secretName = `eve-${domainIndex}-${ruleIndex}-${transformIndex}-${headerIndex}`;
28
+ headers[header] = `{{SECRET:${secretName}}}`;
29
+ secrets[secretName] = value;
30
+ });
31
+ });
32
+ });
33
+ if (Object.keys(headers).length === 0)
34
+ return [];
35
+ return [{ destinations: [domain], headers, secrets }];
36
+ });
37
+ return {
38
+ proxy: {
39
+ ...(allowedDomains === undefined ? {} : { allowedDomains }),
40
+ routing,
41
+ },
42
+ };
43
+ }
44
+ function normalizeAllowEntries(allow) {
45
+ if (allow === undefined)
46
+ return [];
47
+ if (Array.isArray(allow))
48
+ return allow.map((domain) => [domain, []]);
49
+ return Object.entries(allow).map(([domain, rules]) => [
50
+ domain,
51
+ Array.isArray(rules) ? rules : [],
52
+ ]);
53
+ }
@@ -0,0 +1,10 @@
1
+ import { SandboxInstance } from "@blaxel/core";
2
+ import type { SandboxSession } from "eve/sandbox";
3
+ export type CreateBlaxelEveSessionOptions = {
4
+ readonly id: string;
5
+ readonly processOutputBufferBytes?: number;
6
+ readonly registerProcessStream?: (close: () => void) => () => void;
7
+ readonly sandbox: SandboxInstance;
8
+ };
9
+ /** Build eve's public sandbox session over one Blaxel sandbox. */
10
+ export declare function createBlaxelEveSession(options: CreateBlaxelEveSessionOptions): SandboxSession;
@@ -0,0 +1,376 @@
1
+ import { posix as path } from "node:path";
2
+ import { randomUUID } from "node:crypto";
3
+ import { SandboxInstance } from "@blaxel/core";
4
+ import { toBlaxelNetworkPolicy } from "./network-policy.js";
5
+ const WORKSPACE_ROOT = "/workspace";
6
+ const DEFAULT_PROCESS_OUTPUT_BUFFER_BYTES = 1024 * 1024;
7
+ /** Build eve's public sandbox session over one Blaxel sandbox. */
8
+ export function createBlaxelEveSession(options) {
9
+ const { sandbox } = options;
10
+ const processOutputBufferBytes = options.processOutputBufferBytes ?? DEFAULT_PROCESS_OUTPUT_BUFFER_BYTES;
11
+ const session = {
12
+ id: options.id,
13
+ resolvePath,
14
+ async run(runOptions) {
15
+ const { completion, process } = await spawnInternal(runOptions);
16
+ const [streamedStdout, streamedStderr, result] = await Promise.all([
17
+ streamToString(process.stdout),
18
+ streamToString(process.stderr),
19
+ completion,
20
+ ]);
21
+ return {
22
+ exitCode: result.exitCode,
23
+ stdout: result.stdout ?? streamedStdout,
24
+ stderr: result.stderr ?? streamedStderr,
25
+ };
26
+ },
27
+ spawn,
28
+ async readFile(readOptions) {
29
+ throwIfAborted(readOptions.abortSignal);
30
+ try {
31
+ const blob = await sandbox.fs.readBinary(resolvePath(readOptions.path));
32
+ throwIfAborted(readOptions.abortSignal);
33
+ return bytesToStream(new Uint8Array(await blob.arrayBuffer()));
34
+ }
35
+ catch (error) {
36
+ if (isNotFoundError(error))
37
+ return null;
38
+ throw error;
39
+ }
40
+ },
41
+ async readBinaryFile(readOptions) {
42
+ const stream = await session.readFile(readOptions);
43
+ return stream === null ? null : streamToBytes(stream);
44
+ },
45
+ async readTextFile(readOptions) {
46
+ validateReadTextFileOptions(readOptions);
47
+ const bytes = await session.readBinaryFile(readOptions);
48
+ if (bytes === null)
49
+ return null;
50
+ return applyLineRange(decodeBytes(bytes, readOptions.encoding ?? "utf-8"), readOptions);
51
+ },
52
+ async writeFile(writeOptions) {
53
+ throwIfAborted(writeOptions.abortSignal);
54
+ const target = resolvePath(writeOptions.path);
55
+ await ensureParentDirectory(sandbox, target);
56
+ const bytes = await streamToBytes(writeOptions.content);
57
+ throwIfAborted(writeOptions.abortSignal);
58
+ await sandbox.fs.writeBinary(target, bytes);
59
+ },
60
+ async writeBinaryFile(writeOptions) {
61
+ throwIfAborted(writeOptions.abortSignal);
62
+ const target = resolvePath(writeOptions.path);
63
+ await ensureParentDirectory(sandbox, target);
64
+ await sandbox.fs.writeBinary(target, toUint8Array(writeOptions.content));
65
+ throwIfAborted(writeOptions.abortSignal);
66
+ },
67
+ async writeTextFile(writeOptions) {
68
+ throwIfAborted(writeOptions.abortSignal);
69
+ const target = resolvePath(writeOptions.path);
70
+ await ensureParentDirectory(sandbox, target);
71
+ const encoding = writeOptions.encoding ?? "utf-8";
72
+ if (encoding === "utf-8" || encoding === "utf8") {
73
+ await sandbox.fs.write(target, writeOptions.content);
74
+ }
75
+ else {
76
+ await sandbox.fs.writeBinary(target, Buffer.from(writeOptions.content, encoding));
77
+ }
78
+ throwIfAborted(writeOptions.abortSignal);
79
+ },
80
+ async removePath(removeOptions) {
81
+ throwIfAborted(removeOptions.abortSignal);
82
+ try {
83
+ await sandbox.fs.rm(resolvePath(removeOptions.path), removeOptions.recursive ?? false);
84
+ }
85
+ catch (error) {
86
+ if (removeOptions.force && isNotFoundError(error))
87
+ return;
88
+ throw error;
89
+ }
90
+ throwIfAborted(removeOptions.abortSignal);
91
+ },
92
+ async setNetworkPolicy(policy) {
93
+ await SandboxInstance.updateNetwork(sandbox.metadata.name, {
94
+ network: toBlaxelNetworkPolicy(policy),
95
+ });
96
+ },
97
+ };
98
+ return session;
99
+ async function spawn(spawnOptions) {
100
+ return (await spawnInternal(spawnOptions)).process;
101
+ }
102
+ async function spawnInternal(spawnOptions) {
103
+ throwIfAborted(spawnOptions.abortSignal);
104
+ const result = await sandbox.process.exec({
105
+ command: spawnOptions.command,
106
+ env: spawnOptions.env,
107
+ keepAlive: false,
108
+ name: `eve-${randomUUID()}`,
109
+ timeout: 0,
110
+ waitForCompletion: false,
111
+ workingDir: spawnOptions.workingDirectory === undefined
112
+ ? WORKSPACE_ROOT
113
+ : resolvePath(spawnOptions.workingDirectory),
114
+ });
115
+ const pid = result.pid;
116
+ const stdout = createBoundedByteStream(processOutputBufferBytes);
117
+ const stderr = createBoundedByteStream(processOutputBufferBytes);
118
+ let state = "running";
119
+ let exitCode = 1;
120
+ let terminalError;
121
+ let closeStream = () => { };
122
+ const stream = sandbox.process.streamLogs(pid, {
123
+ onStdout: (value) => {
124
+ const error = stdout.push(new TextEncoder().encode(value));
125
+ if (error)
126
+ failProcess(error);
127
+ },
128
+ onStderr: (value) => {
129
+ const error = stderr.push(new TextEncoder().encode(value));
130
+ if (error)
131
+ failProcess(error);
132
+ },
133
+ onError: failProcess,
134
+ });
135
+ closeStream = stream.close;
136
+ const unregister = options.registerProcessStream?.(stream.close) ?? (() => { });
137
+ const kill = async () => {
138
+ if (state !== "running")
139
+ return;
140
+ state = "killed";
141
+ exitCode = 137;
142
+ stream.close();
143
+ await sandbox.process.kill(pid).catch((error) => {
144
+ if (!isNotFoundError(error))
145
+ throw error;
146
+ });
147
+ stdout.finish();
148
+ stderr.finish();
149
+ };
150
+ const abort = () => void kill().catch(failProcess);
151
+ spawnOptions.abortSignal?.addEventListener("abort", abort, { once: true });
152
+ const completed = (async () => {
153
+ try {
154
+ await stream.wait();
155
+ if (terminalError)
156
+ throw terminalError;
157
+ if (state === "killed")
158
+ return { exitCode };
159
+ let final = await sandbox.process.get(pid);
160
+ while (final.status === "running") {
161
+ await delay(250);
162
+ final = await sandbox.process.get(pid);
163
+ }
164
+ exitCode = final.exitCode ?? 1;
165
+ state = final.status === "killed" || final.status === "stopped" ? "killed" : "exited";
166
+ stdout.finish();
167
+ stderr.finish();
168
+ return { exitCode, stderr: final.stderr, stdout: final.stdout };
169
+ }
170
+ catch (error) {
171
+ const normalized = asError(error);
172
+ terminalError ??= normalized;
173
+ stdout.finish(normalized);
174
+ stderr.finish(normalized);
175
+ throw normalized;
176
+ }
177
+ finally {
178
+ spawnOptions.abortSignal?.removeEventListener("abort", abort);
179
+ unregister();
180
+ }
181
+ })();
182
+ void completed.catch(() => undefined);
183
+ const process = {
184
+ stdout: stdout.stream,
185
+ stderr: stderr.stream,
186
+ wait: async () => ({ exitCode: (await completed).exitCode }),
187
+ kill,
188
+ };
189
+ return { completion: completed, process };
190
+ function failProcess(error) {
191
+ if (terminalError)
192
+ return;
193
+ terminalError = error;
194
+ closeStream();
195
+ void sandbox.process.kill(pid).catch(() => undefined);
196
+ stdout.finish(error);
197
+ stderr.finish(error);
198
+ }
199
+ }
200
+ }
201
+ function resolvePath(input) {
202
+ return input.startsWith("/") ? input : `${WORKSPACE_ROOT}/${input}`;
203
+ }
204
+ async function ensureParentDirectory(sandbox, filePath) {
205
+ const parent = path.dirname(filePath);
206
+ if (parent === "/" || parent === WORKSPACE_ROOT)
207
+ return;
208
+ const result = await sandbox.process.exec({
209
+ command: `mkdir -p -- ${shellQuote(parent)}`,
210
+ waitForCompletion: true,
211
+ workingDir: "/",
212
+ });
213
+ if (result.exitCode !== 0) {
214
+ throw new Error(`Failed to create parent directory ${parent}: ${result.stderr}`);
215
+ }
216
+ }
217
+ function createBoundedByteStream(maxBufferedBytes) {
218
+ if (!Number.isInteger(maxBufferedBytes) || maxBufferedBytes <= 0) {
219
+ throw new Error("processOutputBufferBytes must be a positive integer");
220
+ }
221
+ const queue = [];
222
+ let bufferedBytes = 0;
223
+ let finished = false;
224
+ let terminalError;
225
+ let pendingController;
226
+ const stream = new ReadableStream({
227
+ pull(controller) {
228
+ if (queue.length > 0) {
229
+ const chunk = queue.shift();
230
+ bufferedBytes -= chunk.byteLength;
231
+ controller.enqueue(chunk);
232
+ return;
233
+ }
234
+ if (terminalError) {
235
+ controller.error(terminalError);
236
+ return;
237
+ }
238
+ if (finished) {
239
+ controller.close();
240
+ return;
241
+ }
242
+ pendingController = controller;
243
+ },
244
+ cancel() {
245
+ queue.length = 0;
246
+ bufferedBytes = 0;
247
+ pendingController = undefined;
248
+ finished = true;
249
+ },
250
+ });
251
+ return {
252
+ stream,
253
+ push(chunk) {
254
+ if (finished)
255
+ return terminalError;
256
+ if (pendingController) {
257
+ const controller = pendingController;
258
+ pendingController = undefined;
259
+ controller.enqueue(chunk);
260
+ return undefined;
261
+ }
262
+ if (bufferedBytes + chunk.byteLength > maxBufferedBytes) {
263
+ const error = new Error(`Blaxel process output exceeded the ${maxBufferedBytes}-byte buffer; consume output while the process runs.`);
264
+ this.finish(error);
265
+ return error;
266
+ }
267
+ queue.push(chunk);
268
+ bufferedBytes += chunk.byteLength;
269
+ return undefined;
270
+ },
271
+ finish(error) {
272
+ if (finished)
273
+ return;
274
+ finished = true;
275
+ terminalError = error;
276
+ if (!pendingController)
277
+ return;
278
+ const controller = pendingController;
279
+ pendingController = undefined;
280
+ if (error)
281
+ controller.error(error);
282
+ else
283
+ controller.close();
284
+ },
285
+ };
286
+ }
287
+ function bytesToStream(bytes) {
288
+ return new ReadableStream({
289
+ start(controller) {
290
+ controller.enqueue(bytes);
291
+ controller.close();
292
+ },
293
+ });
294
+ }
295
+ async function streamToBytes(stream) {
296
+ const reader = stream.getReader();
297
+ const chunks = [];
298
+ let size = 0;
299
+ try {
300
+ for (;;) {
301
+ const { done, value } = await reader.read();
302
+ if (done)
303
+ break;
304
+ chunks.push(value);
305
+ size += value.byteLength;
306
+ }
307
+ }
308
+ finally {
309
+ reader.releaseLock();
310
+ }
311
+ const result = new Uint8Array(size);
312
+ let offset = 0;
313
+ for (const chunk of chunks) {
314
+ result.set(chunk, offset);
315
+ offset += chunk.byteLength;
316
+ }
317
+ return result;
318
+ }
319
+ async function streamToString(stream) {
320
+ return new TextDecoder().decode(await streamToBytes(stream));
321
+ }
322
+ function toUint8Array(content) {
323
+ if (content instanceof Uint8Array)
324
+ return content;
325
+ if (content instanceof ArrayBuffer)
326
+ return new Uint8Array(content);
327
+ return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
328
+ }
329
+ function decodeBytes(bytes, encoding) {
330
+ if (encoding === "utf-8" || encoding === "utf8") {
331
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
332
+ }
333
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString(encoding);
334
+ }
335
+ function validateReadTextFileOptions(options) {
336
+ const { startLine, endLine } = options;
337
+ if (startLine !== undefined && (!Number.isInteger(startLine) || startLine < 1)) {
338
+ throw new Error("startLine must be a positive integer (1-based).");
339
+ }
340
+ if (endLine !== undefined && (!Number.isInteger(endLine) || endLine < 1)) {
341
+ throw new Error("endLine must be a positive integer (1-based).");
342
+ }
343
+ if (startLine !== undefined && endLine !== undefined && startLine > endLine) {
344
+ throw new Error("startLine must not be greater than endLine.");
345
+ }
346
+ }
347
+ function applyLineRange(text, options) {
348
+ if (options.startLine === undefined && options.endLine === undefined)
349
+ return text;
350
+ const lines = text.match(/.*(?:\r\n|\r|\n)|.+$/g) ?? [];
351
+ const start = options.startLine ?? 1;
352
+ const end = Math.min(options.endLine ?? lines.length, lines.length);
353
+ return start > lines.length ? "" : lines.slice(start - 1, end).join("");
354
+ }
355
+ function isNotFoundError(error) {
356
+ if (!error || typeof error !== "object")
357
+ return false;
358
+ const candidate = error;
359
+ return candidate.code === 404 || candidate.code === "404" || candidate.status === 404 || candidate.statusCode === 404;
360
+ }
361
+ function throwIfAborted(signal) {
362
+ if (!signal?.aborted)
363
+ return;
364
+ if (signal.reason instanceof Error)
365
+ throw signal.reason;
366
+ throw new DOMException("The operation was aborted", "AbortError");
367
+ }
368
+ function shellQuote(value) {
369
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
370
+ }
371
+ function asError(error) {
372
+ return error instanceof Error ? error : new Error(String(error));
373
+ }
374
+ function delay(milliseconds) {
375
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
376
+ }
@@ -0,0 +1,21 @@
1
+ import { blaxel } from "@blaxel/eve-sandbox";
2
+ import { defineSandbox } from "eve/sandbox";
3
+
4
+ export default defineSandbox({
5
+ backend: blaxel({
6
+ image: "blaxel/ts-app:latest",
7
+ labels: { application: "eve" },
8
+ memory: 4096,
9
+ }),
10
+ async onSession({ use }) {
11
+ const sandbox = await use({
12
+ networkPolicy: {
13
+ allow: ["api.github.com", "github.com", "registry.npmjs.org"],
14
+ },
15
+ });
16
+ const result = await sandbox.run({ command: "mkdir -p /workspace/project" });
17
+ if (result.exitCode !== 0) {
18
+ throw new Error(`Blaxel session setup failed: ${result.stderr}`);
19
+ }
20
+ },
21
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["es2023"],
4
+ "module": "nodenext",
5
+ "target": "es2022",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "moduleResolution": "nodenext",
11
+ "noEmit": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["./**/*.ts"]
15
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@blaxel/eve-sandbox",
3
+ "version": "0.3.12-preview.250",
4
+ "description": "Durable Blaxel sandbox backend for eve agents",
5
+ "license": "MIT",
6
+ "author": "Blaxel, INC (https://blaxel.ai)",
7
+ "homepage": "https://blaxel.ai",
8
+ "type": "module",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/blaxel-ai/sdk-typescript",
12
+ "directory": "@blaxel/eve-sandbox"
13
+ },
14
+ "engines": {
15
+ "node": ">=24"
16
+ },
17
+ "keywords": [
18
+ "agent",
19
+ "ai-agent",
20
+ "blaxel",
21
+ "eve",
22
+ "sandbox",
23
+ "sandbox-backend",
24
+ "vercel"
25
+ ],
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "typesVersions": {
37
+ "*": {
38
+ ".": [
39
+ "./dist/index.d.ts"
40
+ ]
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "examples",
46
+ "README.md"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "sideEffects": false,
52
+ "scripts": {
53
+ "lint": "eslint src/ examples/ test/",
54
+ "typecheck:examples": "tsc --noEmit -p examples/tsconfig.json",
55
+ "dev": "tsc --watch",
56
+ "build": "NODE_OPTIONS='--max-old-space-size=8192' tsc -p tsconfig.json"
57
+ },
58
+ "dependencies": {
59
+ "@blaxel/core": "0.3.12-preview.250"
60
+ },
61
+ "peerDependencies": {
62
+ "ai": "^7.0.38",
63
+ "eve": "^0.31.3"
64
+ },
65
+ "devDependencies": {
66
+ "@eslint/js": "^9.39.5",
67
+ "ai": "^7.0.38",
68
+ "eslint": "9.39.2",
69
+ "eve": "^0.31.3",
70
+ "typescript": "^5.9.3",
71
+ "typescript-eslint": "^8.64.0"
72
+ }
73
+ }