@evelandhq/sandbox-bwrap 0.3.0 → 0.4.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/README.md +101 -28
- package/dist/backend-contract.d.ts +49 -0
- package/dist/backend-contract.js +1 -0
- package/dist/backend.d.ts +4 -3
- package/dist/backend.js +23 -86
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +20 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +5 -1
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +18 -2
- package/dist/provider-definition.d.ts +49 -0
- package/dist/provider-definition.js +212 -0
- package/dist/provider.d.ts +18 -0
- package/dist/provider.js +20 -0
- package/dist/runtime.d.ts +45 -0
- package/dist/runtime.js +90 -0
- package/dist/session.d.ts +14 -2
- package/dist/session.js +3 -6
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @evelandhq/sandbox-bwrap
|
|
2
2
|
|
|
3
|
-
A [bubblewrap](https://github.com/containers/bubblewrap)-based
|
|
4
|
-
[eve](https://www.npmjs.com/package/eve) agents
|
|
3
|
+
A [bubblewrap](https://github.com/containers/bubblewrap)-based sandbox for
|
|
4
|
+
[eve](https://www.npmjs.com/package/eve) agents: a sandbox provider for eve 0.64 and later,
|
|
5
|
+
and a `SandboxBackend` for eve 0.62 and 0.63. It gives agent-executed code a real
|
|
5
6
|
Linux sandbox — actual binaries, isolated filesystem, coarse network control — without
|
|
6
7
|
requiring a Docker daemon or KVM.
|
|
7
8
|
|
|
@@ -16,7 +17,8 @@ namespaces.
|
|
|
16
17
|
|
|
17
18
|
## Usage
|
|
18
19
|
|
|
19
|
-
**Deployed on eveland:** you do nothing.
|
|
20
|
+
**Deployed on eveland:** you do nothing. (What follows describes the integration for eve
|
|
21
|
+
0.63 and earlier.) eveland's Docker and systemd runtimes generate
|
|
20
22
|
the sandbox module into the release directory at build time — `agent/sandbox.js` for a flat
|
|
21
23
|
agent, or `agent/sandbox/sandbox.js` when a sandbox folder exists, recursively for every
|
|
22
24
|
subagent — and vendors this package's built output beside it, so agent projects never declare
|
|
@@ -35,7 +37,47 @@ the eveland build pipeline, so it falls back to eve's default backend chain (usu
|
|
|
35
37
|
build log looks like and what happens when the sandbox does not work on the host.
|
|
36
38
|
|
|
37
39
|
**Standalone use of this package** (outside eveland, or in any project that manages its
|
|
38
|
-
own `agent/sandbox.ts`)
|
|
40
|
+
own `agent/sandbox.ts`) depends on which sandbox API your eve has.
|
|
41
|
+
|
|
42
|
+
### eve 0.64 and later: `BwrapSandbox`
|
|
43
|
+
|
|
44
|
+
eve 0.64 replaced sandbox backends with providers. Import the provider from the
|
|
45
|
+
`/provider` entry point and export its environment:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// agent/sandbox.ts
|
|
49
|
+
import { defineSandbox } from "eve/sandbox";
|
|
50
|
+
import { BwrapSandbox } from "@evelandhq/sandbox-bwrap/provider";
|
|
51
|
+
|
|
52
|
+
export const environment = BwrapSandbox.environment({
|
|
53
|
+
// Optional: setup every new sandbox inherits. Runs once, during `eve build`.
|
|
54
|
+
prepare: async (sandbox) => {
|
|
55
|
+
const result = await sandbox.run({ command: "pip install --user requests" });
|
|
56
|
+
if (result.exitCode !== 0) throw new Error(result.stderr);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export default defineSandbox(() => environment.open());
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `eve build` prepares the template: it writes the workspace and skill trees, then runs
|
|
64
|
+
`prepare` inside bwrap, so a build that runs `prepare` needs bwrap on the build host. The
|
|
65
|
+
template lives under `.eve/sandbox-cache/bwrap/templates` in the built app, and eve records
|
|
66
|
+
its absolute path. Serve the build from where it was built, or rebuild on the deploy host;
|
|
67
|
+
a missing template fails the first sandbox access with a rebuild message.
|
|
68
|
+
- eve records which provider prepared each sandbox and refuses to open it with a different
|
|
69
|
+
one. Choose the provider from something that is the same at build and run time, not from
|
|
70
|
+
`isBwrapAvailable()` on hosts that differ.
|
|
71
|
+
- `environment.open({ networkPolicy: "deny-all" })` sets a session's initial network policy.
|
|
72
|
+
It is recorded in the session state and applied again after a restart.
|
|
73
|
+
- eve resumes a session from its recorded state alone. The template is not needed, so with a
|
|
74
|
+
shared `cacheDir` a session keeps its workspace when it moves to a newer deployment.
|
|
75
|
+
- The `/provider` entry point imports `eve/sandbox/provider`, which exists only from eve
|
|
76
|
+
0.64 on. The package root imports nothing from eve at runtime and also exports the plain
|
|
77
|
+
provider definition, `createBwrapSandboxProviderDefinition()`, for callers that call eve's
|
|
78
|
+
`defineSandboxProvider` themselves.
|
|
79
|
+
|
|
80
|
+
### eve 0.62 and 0.63: `bwrap()`
|
|
39
81
|
|
|
40
82
|
```ts
|
|
41
83
|
// agent/sandbox.ts
|
|
@@ -50,20 +92,23 @@ export default defineSandbox({
|
|
|
50
92
|
|
|
51
93
|
### eve version requirement
|
|
52
94
|
|
|
53
|
-
This package requires `eve` `>=0.
|
|
95
|
+
This package requires `eve` `>=0.62.0 <1.0.0`.
|
|
54
96
|
|
|
55
97
|
The range is deliberately wide. eve's 0.x releases use caret-incompatible minor bumps,
|
|
56
98
|
so a package that pins a narrow window has to republish for every eve minor — which is
|
|
57
99
|
churn for consumers, not safety, when the surface actually consumed is one small
|
|
58
|
-
interface
|
|
100
|
+
interface that changes rarely. The range spans two such interfaces: `SandboxBackend` for
|
|
101
|
+
eve 0.62 and 0.63, and the sandbox provider contract from eve 0.64 on. Rather than
|
|
59
102
|
re-declaring the window, CI keeps the claim honest from both ends:
|
|
60
103
|
`src/eve-compatibility.test.ts` typechecks the backend against the range's exact floor
|
|
61
|
-
(0.
|
|
62
|
-
the
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
first time authored code calls
|
|
104
|
+
(0.62.0; 0.63.0, the last eve that calls backends, ships identical sandbox types), and
|
|
105
|
+
typechecks the provider against the newest verified release; a scheduled workflow re-runs the suite against
|
|
106
|
+
`eve@latest` so a breaking eve minor shows up as a red build here instead of a bug report
|
|
107
|
+
from your deployment. That is not theoretical: eve 0.32.0 added a required `stop()` to the
|
|
108
|
+
backend handle. This package implements it; 0.1.0 does not, and pairing that release with
|
|
109
|
+
eve `>=0.32.0` resolves cleanly and then fails at runtime the first time authored code calls
|
|
110
|
+
`ctx.getSandbox().stop()`. Likewise, releases up to 0.3.0 import a value that eve 0.64
|
|
111
|
+
removed, so they cannot load at all on eve 0.64 and later.
|
|
67
112
|
|
|
68
113
|
The backend implements both handle lifecycle methods by killing every process the session
|
|
69
114
|
has spawned that has not yet exited: `shutdown()`, which eve calls at server teardown and
|
|
@@ -74,30 +119,55 @@ because the processes are the compute. Neither method touches the session's work
|
|
|
74
119
|
directory — it is durable state. Cleanup closes that compute generation to new commands;
|
|
75
120
|
the next backend `create()` opens a fresh generation over the same workspace. Repeated
|
|
76
121
|
`create()` calls through one backend instance share the live generation, so one handle
|
|
77
|
-
cannot race a new spawn past another handle's cleanup barrier.
|
|
122
|
+
cannot race a new spawn past another handle's cleanup barrier. The provider's
|
|
123
|
+
`onSessionStop()` and `onRuntimeShutdown()` do the same, and its `resume()` shares the live
|
|
124
|
+
generation the same way.
|
|
78
125
|
|
|
79
126
|
### Options
|
|
80
127
|
|
|
81
|
-
| Option | Default | Meaning
|
|
82
|
-
| ------------------------ | ------------------------------------ |
|
|
83
|
-
| `env` | `{}` | Environment variables set for every sandboxed command.
|
|
84
|
-
| `networkPolicy` | `"allow-all"` | `"allow-all"` shares the host network; `"deny-all"` runs each command with no network (`--unshare-net`). `setNetworkPolicy` can switch between the two at run time; granular domain policies are rejected (use the Vercel backend for those).
|
|
85
|
-
| `hidePaths` | `[]` | Extra host paths hidden from the sandbox (each covered by an empty tmpfs).
|
|
86
|
-
| `bwrapPath` | `"bwrap"` | bwrap executable to invoke.
|
|
87
|
-
| `cacheDir` | `<appRoot>/.eve/sandbox-cache/bwrap` | Absolute directory holding
|
|
88
|
-
| `templateRevision` | `null` | Optional immutable release identity included in the template cache key but not the session path. Change it when seed files change so new Sessions use a fresh template without overwriting durable workspaces. Eveland sets it from its internal `EVELAND_SANDBOX_TEMPLATE_REVISION`.
|
|
89
|
-
| `runTimeoutMs` | `600000` | Hard wall-clock limit for one `run()` command. Timeout aborts the command and kills its complete bwrap process group. Set `null` to disable it. The limit deliberately does not apply to `spawn()`, which is the API for long-running processes.
|
|
90
|
-
| `maxConcurrentProcesses` | `64` | Maximum live `run()`/`spawn()` commands admitted in one compute generation. A live `spawn()` counts until it actually exits, even when its caller never waits. Set `null` to disable.
|
|
91
|
-
| `maxOutputBytes` | `16777216` | Maximum combined stdout and stderr retained by one `run()` call. Exceeding it aborts and reaps the complete process group. Set `null` to disable. Streaming `spawn()` output is not retained by this backend.
|
|
92
|
-
| `onEvent` | `undefined` | Best-effort structured lifecycle sink for generation, command, and cleanup events. Sink failures are ignored so telemetry cannot change command behavior.
|
|
93
|
-
|
|
94
|
-
|
|
128
|
+
| Option | Default | Meaning |
|
|
129
|
+
| ------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
130
|
+
| `env` | `{}` | Environment variables set for every sandboxed command. |
|
|
131
|
+
| `networkPolicy` | `"allow-all"` | `"allow-all"` shares the host network; `"deny-all"` runs each command with no network (`--unshare-net`). `setNetworkPolicy` can switch between the two at run time; granular domain policies are rejected (use the Vercel backend for those). |
|
|
132
|
+
| `hidePaths` | `[]` | Extra host paths hidden from the sandbox (each covered by an empty tmpfs). |
|
|
133
|
+
| `bwrapPath` | `"bwrap"` | bwrap executable to invoke. |
|
|
134
|
+
| `cacheDir` | `<appRoot>/.eve/sandbox-cache/bwrap` | Absolute directory holding durable session workspaces, and, for the backend, templates. The provider always keeps templates in eve's build storage, where eve records them. Pin this outside the release directory so a redeploy does not discard durable session state: since eve 0.22.0, eve keys session sandboxes per durable session, not per deployment, so an `appRoot`-derived default would silently destroy every session's `/workspace` on the next redeploy. The generated eveland module always sets this from `EVELAND_SANDBOX_CACHE_DIR`. |
|
|
135
|
+
| `templateRevision` | `null` | Backend only. Optional immutable release identity included in the template cache key but not the session path. Change it when seed files change so new Sessions use a fresh template without overwriting durable workspaces. Eveland sets it from its internal `EVELAND_SANDBOX_TEMPLATE_REVISION`. |
|
|
136
|
+
| `runTimeoutMs` | `600000` | Hard wall-clock limit for one `run()` command. Timeout aborts the command and kills its complete bwrap process group. Set `null` to disable it. The limit deliberately does not apply to `spawn()`, which is the API for long-running processes. |
|
|
137
|
+
| `maxConcurrentProcesses` | `64` | Maximum live `run()`/`spawn()` commands admitted in one compute generation. A live `spawn()` counts until it actually exits, even when its caller never waits. Set `null` to disable. |
|
|
138
|
+
| `maxOutputBytes` | `16777216` | Maximum combined stdout and stderr retained by one `run()` call. Exceeding it aborts and reaps the complete process group. Set `null` to disable. Streaming `spawn()` output is not retained by this backend. |
|
|
139
|
+
| `onEvent` | `undefined` | Best-effort structured lifecycle sink for generation, command, and cleanup events. Sink failures are ignored so telemetry cannot change command behavior. |
|
|
140
|
+
|
|
141
|
+
`BwrapSandbox.environment()` takes the same options except `templateRevision`, which the
|
|
142
|
+
provider does not need: eve's source revision and resource keys already key its templates.
|
|
143
|
+
It adds `prepare`, described above.
|
|
144
|
+
|
|
145
|
+
With the backend, both lifecycle callbacks may call `use({ networkPolicy: "allow-all" | "deny-all" })`. The
|
|
95
146
|
policy is applied before `use()` returns the template or live Session, so subsequent commands in
|
|
96
147
|
that callback use the requested network boundary. Calling `use()` without options keeps the
|
|
97
148
|
backend's configured policy.
|
|
98
149
|
|
|
99
150
|
## How it works
|
|
100
151
|
|
|
152
|
+
The provider (eve 0.64 and later):
|
|
153
|
+
|
|
154
|
+
- **prepare** (`eve build`): writes eve's workspace tree and skill tree
|
|
155
|
+
(`$HOME/.agents/skills/**` lands at `/workspace/.agents/skills/**`) into a staging
|
|
156
|
+
directory, runs the authored `prepare` inside bwrap, stops anything it left running, and
|
|
157
|
+
atomically renames the result into `<storage>/bwrap/templates/<hash>`, where `<storage>`
|
|
158
|
+
is eve's sandbox storage directory, `<appRoot>/.eve/sandbox-cache`. The key covers eve's
|
|
159
|
+
source revision, the resource keys, the options, and the source of `prepare`.
|
|
160
|
+
- **start** (first sandbox access of a session): clones the template into
|
|
161
|
+
`<cacheDir>/sessions/<hash>`, keyed by eve's session id, and returns the session state eve
|
|
162
|
+
persists: the session key and the network policy.
|
|
163
|
+
- **resume** (every later access, and after restarts): reopens the recorded workspace and
|
|
164
|
+
applies the recorded network policy to a new compute generation. It fails instead of
|
|
165
|
+
recreating a workspace that is gone.
|
|
166
|
+
- **onSessionDelete**: kills the session's processes and removes its workspace and
|
|
167
|
+
metadata; the template survives.
|
|
168
|
+
|
|
169
|
+
The backend (eve 0.62 and 0.63):
|
|
170
|
+
|
|
101
171
|
- **prewarm** (build time): resolves Eve's `$HOME/.agents/skills/**` seed paths to
|
|
102
172
|
`/workspace/.agents/skills/**`, writes every seed into a staging directory, then runs the
|
|
103
173
|
authored `bootstrap` inside bwrap so it can consume those canonical inputs, before atomically
|
|
@@ -123,7 +193,10 @@ backend's configured policy.
|
|
|
123
193
|
## Disk usage and cache management
|
|
124
194
|
|
|
125
195
|
Session and template directories persist under `<cacheDir>/{sessions,templates}` across
|
|
126
|
-
process restarts and reconnects, enabling fast reattach.
|
|
196
|
+
process restarts and reconnects, enabling fast reattach. The provider keeps its templates
|
|
197
|
+
under eve's storage directory instead (`<appRoot>/.eve/sandbox-cache/bwrap/templates`), so
|
|
198
|
+
they go away with the build that prepared them; pass that `appRoot` without `cacheDir` to the
|
|
199
|
+
APIs below to inspect them. `stop()` and `shutdown()` never
|
|
127
200
|
delete durable state, and this package never schedules automatic deletion. On Eveland,
|
|
128
201
|
the cache lives at `EVELAND_SANDBOX_CACHE_DIR` outside every release directory, so a
|
|
129
202
|
redeploy does not touch it.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { BwrapSandboxUseOptions } from "./options.js";
|
|
2
|
+
import type { BwrapSession } from "./session.js";
|
|
3
|
+
export interface BwrapSeedFile {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly content: string | Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
export interface BwrapBackendRuntimeContext {
|
|
8
|
+
readonly appRoot: string;
|
|
9
|
+
}
|
|
10
|
+
export interface BwrapBackendCreateInput {
|
|
11
|
+
readonly templateKey: string | null;
|
|
12
|
+
readonly sessionKey: string;
|
|
13
|
+
readonly existingMetadata?: Record<string, unknown>;
|
|
14
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
15
|
+
readonly runtimeContext: BwrapBackendRuntimeContext;
|
|
16
|
+
}
|
|
17
|
+
export interface BwrapBackendBootstrapContext {
|
|
18
|
+
use(options?: BwrapSandboxUseOptions): Promise<BwrapSession>;
|
|
19
|
+
}
|
|
20
|
+
export interface BwrapBackendPrewarmInput {
|
|
21
|
+
readonly templateKey: string;
|
|
22
|
+
readonly bootstrap?: (input: BwrapBackendBootstrapContext) => void | Promise<void>;
|
|
23
|
+
readonly log?: (message: string) => void;
|
|
24
|
+
readonly runtimeContext: BwrapBackendRuntimeContext;
|
|
25
|
+
readonly seedFiles: ReadonlyArray<BwrapSeedFile>;
|
|
26
|
+
}
|
|
27
|
+
export interface BwrapBackendSessionState {
|
|
28
|
+
readonly backendName: string;
|
|
29
|
+
readonly metadata: Record<string, unknown>;
|
|
30
|
+
readonly sessionKey: string;
|
|
31
|
+
}
|
|
32
|
+
export interface BwrapBackendHandle {
|
|
33
|
+
readonly session: BwrapSession;
|
|
34
|
+
readonly useSessionFn: (options?: BwrapSandboxUseOptions) => Promise<BwrapSession>;
|
|
35
|
+
captureState(): Promise<BwrapBackendSessionState>;
|
|
36
|
+
delete(options?: {
|
|
37
|
+
readonly abortSignal?: AbortSignal;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
shutdown(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** The bubblewrap sandbox backend, for eve 0.62 and 0.63. */
|
|
43
|
+
export interface BwrapSandboxBackend {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
create(input: BwrapBackendCreateInput): Promise<BwrapBackendHandle>;
|
|
46
|
+
prewarm(input: BwrapBackendPrewarmInput): Promise<{
|
|
47
|
+
readonly reused: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { BwrapSandboxCreateOptions
|
|
1
|
+
import type { BwrapSandboxBackend } from "./backend-contract.js";
|
|
2
|
+
import type { BwrapSandboxCreateOptions } from "./options.js";
|
|
3
3
|
import type { ProcessRunner } from "./process.js";
|
|
4
4
|
import { type BwrapDirectoryCopier } from "./cache.js";
|
|
5
5
|
/**
|
|
@@ -14,4 +14,5 @@ export interface CreateBwrapSandboxBackendInput {
|
|
|
14
14
|
/** Injectable clone primitive for filesystem-capability tests. */
|
|
15
15
|
readonly copyDirectory?: BwrapDirectoryCopier;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
/** The backend for eve 0.62 and 0.63. eve 0.64 and later use `BwrapSandbox` instead. */
|
|
18
|
+
export declare function createBwrapSandboxBackend(input?: CreateBwrapSandboxBackendInput): BwrapSandboxBackend;
|
package/dist/backend.js
CHANGED
|
@@ -2,92 +2,21 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { mkdir, rename, rm } from "node:fs/promises";
|
|
4
4
|
import { basename } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { BwrapTemplateNotProvisionedError } from "./errors.js";
|
|
6
6
|
import { createBwrapOptionsHash, resolveBwrapSandboxOptions } from "./options.js";
|
|
7
|
-
import { resolveBwrapCacheRoot, resolveSessionPath, resolveTemplatePath
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { cloneDirectoryAtomically, createBwrapCacheLease, registerActiveCachePath, removeCacheMetadata, touchCacheMetadata, } from "./cache.js";
|
|
11
|
-
const EVE_MODEL_SKILL_ROOT = "$HOME/.agents/skills";
|
|
7
|
+
import { resolveBwrapCacheRoot, resolveSessionPath, resolveTemplatePath } from "./paths.js";
|
|
8
|
+
import { createBwrapRuntime, writeSeedFiles } from "./runtime.js";
|
|
9
|
+
import { cloneDirectoryAtomically, removeCacheMetadata, touchCacheMetadata, } from "./cache.js";
|
|
12
10
|
/**
|
|
13
11
|
* Stable backend name. Participates in eve's template/session cache-key
|
|
14
12
|
* derivation and persisted reconnect state — never change it.
|
|
15
13
|
*/
|
|
16
14
|
export const BWRAP_BACKEND_NAME = "bwrap";
|
|
15
|
+
/** The backend for eve 0.62 and 0.63. eve 0.64 and later use `BwrapSandbox` instead. */
|
|
17
16
|
export function createBwrapSandboxBackend(input = {}) {
|
|
18
17
|
const options = resolveBwrapSandboxOptions(input.createOptions);
|
|
19
18
|
const optionsHash = createBwrapOptionsHash(options);
|
|
20
|
-
const
|
|
21
|
-
const generations = new Map();
|
|
22
|
-
// Probe only when running against the real bwrap; injected runners skip it.
|
|
23
|
-
const shouldProbe = input.runner === undefined;
|
|
24
|
-
let probed = false;
|
|
25
|
-
function assertBwrapAvailable() {
|
|
26
|
-
if (!shouldProbe || probed)
|
|
27
|
-
return;
|
|
28
|
-
const missing = describeMissingPrereqs({
|
|
29
|
-
bwrapPresent: isBwrapAvailable(options.bwrapPath),
|
|
30
|
-
workspaceMountpointPresent: existsSync(WORKSPACE_ROOT),
|
|
31
|
-
bwrapPath: options.bwrapPath,
|
|
32
|
-
});
|
|
33
|
-
if (missing)
|
|
34
|
-
throw new Error(missing);
|
|
35
|
-
probed = true;
|
|
36
|
-
}
|
|
37
|
-
function openSession(id, workspaceDir, appRoot, tags, generationId, onStopped) {
|
|
38
|
-
return createBwrapSession({
|
|
39
|
-
id,
|
|
40
|
-
workspaceDir,
|
|
41
|
-
appRoot,
|
|
42
|
-
runner,
|
|
43
|
-
options,
|
|
44
|
-
tags,
|
|
45
|
-
generationId,
|
|
46
|
-
onStopped,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
async function openRuntimeSession(id, workspaceDir, appRoot, tags) {
|
|
50
|
-
const current = generations.get(workspaceDir);
|
|
51
|
-
if (current && current.lifecycleState() !== "stopped")
|
|
52
|
-
return current;
|
|
53
|
-
const releaseActive = registerActiveCachePath(workspaceDir);
|
|
54
|
-
const cacheRoot = resolveBwrapCacheRoot(appRoot, options.cacheDir);
|
|
55
|
-
const activeLease = await createBwrapCacheLease({
|
|
56
|
-
cacheRoot,
|
|
57
|
-
sessionId: basename(workspaceDir),
|
|
58
|
-
});
|
|
59
|
-
let session;
|
|
60
|
-
try {
|
|
61
|
-
session = openSession(id, workspaceDir, appRoot, tags, activeLease.lease.generationId, async () => {
|
|
62
|
-
releaseActive();
|
|
63
|
-
await activeLease.release();
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
releaseActive();
|
|
68
|
-
await activeLease.release();
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
generations.set(workspaceDir, session);
|
|
72
|
-
return session;
|
|
73
|
-
}
|
|
74
|
-
function resolveSeedPath(seedPath) {
|
|
75
|
-
if (seedPath === EVE_MODEL_SKILL_ROOT || seedPath.startsWith(`${EVE_MODEL_SKILL_ROOT}/`)) {
|
|
76
|
-
return `${WORKSPACE_ROOT}/.agents/skills${seedPath.slice(EVE_MODEL_SKILL_ROOT.length)}`;
|
|
77
|
-
}
|
|
78
|
-
return seedPath;
|
|
79
|
-
}
|
|
80
|
-
async function writeSeedFiles(session, seedFiles) {
|
|
81
|
-
for (const seed of seedFiles) {
|
|
82
|
-
const seedPath = resolveSeedPath(seed.path);
|
|
83
|
-
if (typeof seed.content === "string") {
|
|
84
|
-
await session.writeTextFile({ path: seedPath, content: seed.content });
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
await session.writeBinaryFile({ path: seedPath, content: seed.content });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
19
|
+
const runtime = createBwrapRuntime({ options, runner: input.runner });
|
|
91
20
|
async function useSession(session, useOptions) {
|
|
92
21
|
if (useOptions?.networkPolicy !== undefined) {
|
|
93
22
|
await session.setNetworkPolicy(useOptions.networkPolicy);
|
|
@@ -97,7 +26,7 @@ export function createBwrapSandboxBackend(input = {}) {
|
|
|
97
26
|
return {
|
|
98
27
|
name: BWRAP_BACKEND_NAME,
|
|
99
28
|
async prewarm({ templateKey, bootstrap, seedFiles, log, runtimeContext }) {
|
|
100
|
-
assertBwrapAvailable();
|
|
29
|
+
runtime.assertBwrapAvailable();
|
|
101
30
|
const templatePath = resolveTemplatePath(runtimeContext.appRoot, templateKey, optionsHash, options.cacheDir);
|
|
102
31
|
const touchTemplate = async () => await touchCacheMetadata({
|
|
103
32
|
cacheRoot: resolveBwrapCacheRoot(runtimeContext.appRoot, options.cacheDir),
|
|
@@ -113,7 +42,11 @@ export function createBwrapSandboxBackend(input = {}) {
|
|
|
113
42
|
const stagingPath = `${templatePath}.staging-${randomUUID()}`;
|
|
114
43
|
await mkdir(stagingPath, { recursive: true });
|
|
115
44
|
try {
|
|
116
|
-
const session =
|
|
45
|
+
const session = runtime.openTemplateSession({
|
|
46
|
+
id: templateKey,
|
|
47
|
+
workspaceDir: stagingPath,
|
|
48
|
+
cacheRoots: [resolveBwrapCacheRoot(runtimeContext.appRoot, options.cacheDir)],
|
|
49
|
+
});
|
|
117
50
|
await writeSeedFiles(session, seedFiles);
|
|
118
51
|
if (bootstrap) {
|
|
119
52
|
await bootstrap({ use: async (useOptions) => await useSession(session, useOptions) });
|
|
@@ -133,7 +66,7 @@ export function createBwrapSandboxBackend(input = {}) {
|
|
|
133
66
|
return { reused: false };
|
|
134
67
|
},
|
|
135
68
|
async create({ templateKey, sessionKey, runtimeContext, tags }) {
|
|
136
|
-
assertBwrapAvailable();
|
|
69
|
+
runtime.assertBwrapAvailable();
|
|
137
70
|
const sessionPath = resolveSessionPath(runtimeContext.appRoot, sessionKey, options.cacheDir);
|
|
138
71
|
let cloneStrategy = "existing";
|
|
139
72
|
if (!existsSync(sessionPath)) {
|
|
@@ -144,10 +77,7 @@ export function createBwrapSandboxBackend(input = {}) {
|
|
|
144
77
|
else {
|
|
145
78
|
const templatePath = resolveTemplatePath(runtimeContext.appRoot, templateKey, optionsHash, options.cacheDir);
|
|
146
79
|
if (!existsSync(templatePath)) {
|
|
147
|
-
throw new
|
|
148
|
-
backendName: BWRAP_BACKEND_NAME,
|
|
149
|
-
templateKey,
|
|
150
|
-
});
|
|
80
|
+
throw new BwrapTemplateNotProvisionedError({ templateKey });
|
|
151
81
|
}
|
|
152
82
|
cloneStrategy = await cloneDirectoryAtomically({
|
|
153
83
|
sourcePath: templatePath,
|
|
@@ -163,7 +93,14 @@ export function createBwrapSandboxBackend(input = {}) {
|
|
|
163
93
|
tags,
|
|
164
94
|
cloneStrategy,
|
|
165
95
|
});
|
|
166
|
-
const
|
|
96
|
+
const cacheRoot = resolveBwrapCacheRoot(runtimeContext.appRoot, options.cacheDir);
|
|
97
|
+
const { session } = await runtime.openRuntimeSession({
|
|
98
|
+
id: sessionKey,
|
|
99
|
+
workspaceDir: sessionPath,
|
|
100
|
+
cacheRoots: [cacheRoot],
|
|
101
|
+
leaseRoot: cacheRoot,
|
|
102
|
+
tags,
|
|
103
|
+
});
|
|
167
104
|
return {
|
|
168
105
|
session,
|
|
169
106
|
useSessionFn: async (useOptions) => await useSession(session, useOptions),
|
|
@@ -195,7 +132,7 @@ export function createBwrapSandboxBackend(input = {}) {
|
|
|
195
132
|
async delete(deleteOptions) {
|
|
196
133
|
deleteOptions?.abortSignal?.throwIfAborted();
|
|
197
134
|
await session.killAll();
|
|
198
|
-
|
|
135
|
+
runtime.forgetRuntimeSession(sessionPath);
|
|
199
136
|
await rm(sessionPath, { force: true, recursive: true });
|
|
200
137
|
await removeCacheMetadata({
|
|
201
138
|
cacheRoot: resolveBwrapCacheRoot(runtimeContext.appRoot, options.cacheDir),
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a session needs a template that was never prepared.
|
|
3
|
+
*
|
|
4
|
+
* eve recognizes this error structurally rather than by class, so this package
|
|
5
|
+
* can throw it without importing eve at runtime. `name` and `templateKey` are
|
|
6
|
+
* checked by every eve line; `backendName` is what eve 0.62 and 0.63 look for,
|
|
7
|
+
* and `providerName` is what 0.64 and later look for. It carries both, so
|
|
8
|
+
* the same error works for the backend and the provider.
|
|
9
|
+
*/
|
|
10
|
+
export declare class BwrapTemplateNotProvisionedError extends Error {
|
|
11
|
+
readonly name = "SandboxTemplateNotProvisionedError";
|
|
12
|
+
readonly backendName = "bwrap";
|
|
13
|
+
readonly providerName = "bwrap";
|
|
14
|
+
readonly templateKey: string;
|
|
15
|
+
constructor(input: {
|
|
16
|
+
readonly templateKey: string;
|
|
17
|
+
});
|
|
18
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a session needs a template that was never prepared.
|
|
3
|
+
*
|
|
4
|
+
* eve recognizes this error structurally rather than by class, so this package
|
|
5
|
+
* can throw it without importing eve at runtime. `name` and `templateKey` are
|
|
6
|
+
* checked by every eve line; `backendName` is what eve 0.62 and 0.63 look for,
|
|
7
|
+
* and `providerName` is what 0.64 and later look for. It carries both, so
|
|
8
|
+
* the same error works for the backend and the provider.
|
|
9
|
+
*/
|
|
10
|
+
export class BwrapTemplateNotProvisionedError extends Error {
|
|
11
|
+
name = "SandboxTemplateNotProvisionedError";
|
|
12
|
+
backendName = "bwrap";
|
|
13
|
+
providerName = "bwrap";
|
|
14
|
+
templateKey;
|
|
15
|
+
constructor(input) {
|
|
16
|
+
super(`Sandbox template "${input.templateKey}" is not provisioned for bwrap. ` +
|
|
17
|
+
"Run `eve build` before serving traffic.");
|
|
18
|
+
this.templateKey = input.templateKey;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { BwrapSandboxCreateOptions
|
|
1
|
+
import type { BwrapSandboxBackend } from "./backend-contract.js";
|
|
2
|
+
import type { BwrapSandboxCreateOptions } from "./options.js";
|
|
3
3
|
export { BWRAP_BACKEND_NAME, createBwrapSandboxBackend, type CreateBwrapSandboxBackendInput, } from "./backend.js";
|
|
4
|
+
export type { BwrapBackendBootstrapContext, BwrapBackendCreateInput, BwrapBackendHandle, BwrapBackendPrewarmInput, BwrapBackendRuntimeContext, BwrapBackendSessionState, BwrapSandboxBackend, BwrapSeedFile, } from "./backend-contract.js";
|
|
5
|
+
export { BwrapTemplateNotProvisionedError } from "./errors.js";
|
|
6
|
+
export { BWRAP_PROVIDER_STATE_PROTOCOL_VERSION, createBwrapSandboxProviderDefinition, type BwrapPreparedArtifact, type BwrapSandboxEnvironmentOptions, type BwrapSandboxOpenOptions, type BwrapSandboxProviderDefinition, type BwrapSessionState, type CreateBwrapSandboxProviderDefinitionInput, } from "./provider-definition.js";
|
|
7
|
+
export type { BwrapSession } from "./session.js";
|
|
4
8
|
export type { BwrapNetworkPolicy, BwrapSandboxCreateOptions, BwrapSandboxUseOptions, } from "./options.js";
|
|
5
9
|
export type { BwrapCommandFinishReason, BwrapSandboxEvent, BwrapSandboxEventSink, } from "./events.js";
|
|
6
10
|
export { listBwrapCache, listBwrapCacheLeases, pruneBwrapCache } from "./cache.js";
|
|
@@ -9,7 +13,9 @@ export { DEFAULT_MAX_CONCURRENT_PROCESSES, DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_RUN
|
|
|
9
13
|
export { isBwrapAvailable } from "./process.js";
|
|
10
14
|
export type { ProcessRunner, SpawnedProcess } from "./process.js";
|
|
11
15
|
/**
|
|
12
|
-
* Creates the bubblewrap sandbox backend for `defineSandbox({ backend })
|
|
16
|
+
* Creates the bubblewrap sandbox backend for `defineSandbox({ backend })`, the
|
|
17
|
+
* sandbox API of eve 0.62 and 0.63. On eve 0.64 and later use `BwrapSandbox`
|
|
18
|
+
* from `@evelandhq/sandbox-bwrap/provider` instead.
|
|
13
19
|
*
|
|
14
20
|
* ```ts
|
|
15
21
|
* // agent/sandbox.ts
|
|
@@ -21,4 +27,4 @@ export type { ProcessRunner, SpawnedProcess } from "./process.js";
|
|
|
21
27
|
* });
|
|
22
28
|
* ```
|
|
23
29
|
*/
|
|
24
|
-
export declare function bwrap(options?: BwrapSandboxCreateOptions):
|
|
30
|
+
export declare function bwrap(options?: BwrapSandboxCreateOptions): BwrapSandboxBackend;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { createBwrapSandboxBackend } from "./backend.js";
|
|
2
2
|
export { BWRAP_BACKEND_NAME, createBwrapSandboxBackend, } from "./backend.js";
|
|
3
|
+
export { BwrapTemplateNotProvisionedError } from "./errors.js";
|
|
4
|
+
export { BWRAP_PROVIDER_STATE_PROTOCOL_VERSION, createBwrapSandboxProviderDefinition, } from "./provider-definition.js";
|
|
3
5
|
export { listBwrapCache, listBwrapCacheLeases, pruneBwrapCache } from "./cache.js";
|
|
4
6
|
export { DEFAULT_MAX_CONCURRENT_PROCESSES, DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_RUN_TIMEOUT_MS, } from "./options.js";
|
|
5
7
|
export { isBwrapAvailable } from "./process.js";
|
|
6
8
|
/**
|
|
7
|
-
* Creates the bubblewrap sandbox backend for `defineSandbox({ backend })
|
|
9
|
+
* Creates the bubblewrap sandbox backend for `defineSandbox({ backend })`, the
|
|
10
|
+
* sandbox API of eve 0.62 and 0.63. On eve 0.64 and later use `BwrapSandbox`
|
|
11
|
+
* from `@evelandhq/sandbox-bwrap/provider` instead.
|
|
8
12
|
*
|
|
9
13
|
* ```ts
|
|
10
14
|
* // agent/sandbox.ts
|
package/dist/paths.d.ts
CHANGED
|
@@ -8,6 +8,16 @@ export declare const WORKSPACE_ROOT = "/workspace";
|
|
|
8
8
|
export declare function resolveBwrapCacheRoot(appRoot: string, cacheDir?: string | null): string;
|
|
9
9
|
export declare function resolveTemplatePath(appRoot: string, templateKey: string, optionsHash: string, cacheDir?: string | null): string;
|
|
10
10
|
export declare function resolveSessionPath(appRoot: string, sessionKey: string, cacheDir?: string | null): string;
|
|
11
|
+
/**
|
|
12
|
+
* The provider's cache root inside eve's sandbox storage directory
|
|
13
|
+
* (`<appRoot>/.eve/sandbox-cache`), the same place the backend's default
|
|
14
|
+
* cache root resolves to. eve prepares templates there at build time and
|
|
15
|
+
* records their path, so templates always live here, even when `cacheDir`
|
|
16
|
+
* moves session workspaces elsewhere.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveProviderCacheRoot(storagePath: string): string;
|
|
19
|
+
export declare function templatePathIn(cacheRoot: string, templateKey: string, optionsHash: string): string;
|
|
20
|
+
export declare function sessionPathIn(cacheRoot: string, sessionKey: string): string;
|
|
11
21
|
/** Anchors a sandbox-relative path to /workspace; absolute paths pass through. */
|
|
12
22
|
export declare function resolveWorkspacePath(path: string): string;
|
|
13
23
|
/**
|
package/dist/paths.js
CHANGED
|
@@ -15,10 +15,26 @@ function keyDigest(value) {
|
|
|
15
15
|
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
16
16
|
}
|
|
17
17
|
export function resolveTemplatePath(appRoot, templateKey, optionsHash, cacheDir) {
|
|
18
|
-
return
|
|
18
|
+
return templatePathIn(resolveBwrapCacheRoot(appRoot, cacheDir), templateKey, optionsHash);
|
|
19
19
|
}
|
|
20
20
|
export function resolveSessionPath(appRoot, sessionKey, cacheDir) {
|
|
21
|
-
return
|
|
21
|
+
return sessionPathIn(resolveBwrapCacheRoot(appRoot, cacheDir), sessionKey);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The provider's cache root inside eve's sandbox storage directory
|
|
25
|
+
* (`<appRoot>/.eve/sandbox-cache`), the same place the backend's default
|
|
26
|
+
* cache root resolves to. eve prepares templates there at build time and
|
|
27
|
+
* records their path, so templates always live here, even when `cacheDir`
|
|
28
|
+
* moves session workspaces elsewhere.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveProviderCacheRoot(storagePath) {
|
|
31
|
+
return join(storagePath, "bwrap");
|
|
32
|
+
}
|
|
33
|
+
export function templatePathIn(cacheRoot, templateKey, optionsHash) {
|
|
34
|
+
return join(cacheRoot, "templates", `${keyDigest(templateKey)}-${optionsHash}`);
|
|
35
|
+
}
|
|
36
|
+
export function sessionPathIn(cacheRoot, sessionKey) {
|
|
37
|
+
return join(cacheRoot, "sessions", keyDigest(sessionKey));
|
|
22
38
|
}
|
|
23
39
|
/** Anchors a sandbox-relative path to /workspace; absolute paths pass through. */
|
|
24
40
|
export function resolveWorkspacePath(path) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MutableNetworkSandboxSession } from "eve/sandbox";
|
|
2
|
+
import type { SandboxProviderDefinition } from "eve/sandbox/provider";
|
|
3
|
+
import { type BwrapDirectoryCopier } from "./cache.js";
|
|
4
|
+
import type { BwrapNetworkPolicy, BwrapSandboxCreateOptions } from "./options.js";
|
|
5
|
+
import type { ProcessRunner } from "./process.js";
|
|
6
|
+
/**
|
|
7
|
+
* Version of the session state this provider persists through eve. eve refuses
|
|
8
|
+
* to resume state written under a different version, so bump it only together
|
|
9
|
+
* with a reader for the old shape.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BWRAP_PROVIDER_STATE_PROTOCOL_VERSION = 1;
|
|
12
|
+
/** Options for `BwrapSandbox.environment(...)`. */
|
|
13
|
+
export type BwrapSandboxEnvironmentOptions = Omit<BwrapSandboxCreateOptions, "templateRevision"> & {
|
|
14
|
+
/**
|
|
15
|
+
* Setup every new sandbox inherits. eve runs it once, when `eve build`
|
|
16
|
+
* prepares the template, not for each session. Commands it runs execute in
|
|
17
|
+
* bwrap, so the build host needs bwrap too.
|
|
18
|
+
*/
|
|
19
|
+
readonly prepare?: (sandbox: MutableNetworkSandboxSession) => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
/** Options for `environment.open(...)`, applied when a session's sandbox is first started. */
|
|
22
|
+
export interface BwrapSandboxOpenOptions {
|
|
23
|
+
readonly networkPolicy?: BwrapNetworkPolicy;
|
|
24
|
+
}
|
|
25
|
+
/** What `eve build` records for a prepared template. */
|
|
26
|
+
export type BwrapPreparedArtifact = {
|
|
27
|
+
readonly version: 1;
|
|
28
|
+
readonly templatePath: string;
|
|
29
|
+
};
|
|
30
|
+
/** What eve persists for one durable session's sandbox. */
|
|
31
|
+
export type BwrapSessionState = {
|
|
32
|
+
readonly version: 1;
|
|
33
|
+
readonly sessionKey: string;
|
|
34
|
+
readonly networkPolicy: BwrapNetworkPolicy;
|
|
35
|
+
};
|
|
36
|
+
export interface CreateBwrapSandboxProviderDefinitionInput {
|
|
37
|
+
/** Injectable process launcher so provider logic is testable without bwrap. */
|
|
38
|
+
readonly runner?: ProcessRunner;
|
|
39
|
+
/** Injectable clone primitive for filesystem-capability tests. */
|
|
40
|
+
readonly copyDirectory?: BwrapDirectoryCopier;
|
|
41
|
+
}
|
|
42
|
+
export type BwrapSandboxProviderDefinition = SandboxProviderDefinition<BwrapSandboxEnvironmentOptions, BwrapSandboxOpenOptions, BwrapPreparedArtifact, BwrapSessionState, MutableNetworkSandboxSession>;
|
|
43
|
+
/**
|
|
44
|
+
* The bubblewrap sandbox provider for eve 0.64 and later, as a plain
|
|
45
|
+
* definition. Pass it to eve's `defineSandboxProvider`, or import the ready
|
|
46
|
+
* `BwrapSandbox` from `@evelandhq/sandbox-bwrap/provider`. This module imports
|
|
47
|
+
* nothing from eve at runtime, so it loads on every eve in the peer range.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createBwrapSandboxProviderDefinition(input?: CreateBwrapSandboxProviderDefinitionInput): BwrapSandboxProviderDefinition;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
import { cloneDirectoryAtomically, removeCacheMetadata, touchCacheMetadata, } from "./cache.js";
|
|
6
|
+
import { BwrapTemplateNotProvisionedError } from "./errors.js";
|
|
7
|
+
import { createBwrapOptionsHash, resolveBwrapSandboxOptions } from "./options.js";
|
|
8
|
+
import { resolveProviderCacheRoot, sessionPathIn, templatePathIn } from "./paths.js";
|
|
9
|
+
import { createBwrapRuntime, writeSeedFiles } from "./runtime.js";
|
|
10
|
+
/**
|
|
11
|
+
* Version of the session state this provider persists through eve. eve refuses
|
|
12
|
+
* to resume state written under a different version, so bump it only together
|
|
13
|
+
* with a reader for the old shape.
|
|
14
|
+
*/
|
|
15
|
+
export const BWRAP_PROVIDER_STATE_PROTOCOL_VERSION = 1;
|
|
16
|
+
/**
|
|
17
|
+
* The bubblewrap sandbox provider for eve 0.64 and later, as a plain
|
|
18
|
+
* definition. Pass it to eve's `defineSandboxProvider`, or import the ready
|
|
19
|
+
* `BwrapSandbox` from `@evelandhq/sandbox-bwrap/provider`. This module imports
|
|
20
|
+
* nothing from eve at runtime, so it loads on every eve in the peer range.
|
|
21
|
+
*/
|
|
22
|
+
export function createBwrapSandboxProviderDefinition(input = {}) {
|
|
23
|
+
return {
|
|
24
|
+
name: "bwrap",
|
|
25
|
+
stateProtocolVersion: BWRAP_PROVIDER_STATE_PROTOCOL_VERSION,
|
|
26
|
+
environment(environmentOptions) {
|
|
27
|
+
const { prepare, ...createOptions } = environmentOptions ?? {};
|
|
28
|
+
const options = resolveBwrapSandboxOptions(createOptions);
|
|
29
|
+
const optionsHash = createBwrapOptionsHash(options);
|
|
30
|
+
const runtime = createBwrapRuntime({ options, runner: input.runner });
|
|
31
|
+
// Templates live beside eve's own prepared artifacts; sessions follow
|
|
32
|
+
// `cacheDir` when it is set, so they outlive the release that made them.
|
|
33
|
+
const cacheRoots = (storagePath) => {
|
|
34
|
+
const templateRoot = resolveProviderCacheRoot(storagePath);
|
|
35
|
+
const sessionRoot = options.cacheDir ?? templateRoot;
|
|
36
|
+
return { templateRoot, sessionRoot, hidden: [templateRoot, sessionRoot] };
|
|
37
|
+
};
|
|
38
|
+
async function openHandle(ctx, sessionKey, cloneStrategy, applyNetworkPolicy) {
|
|
39
|
+
const roots = cacheRoots(ctx.storagePath);
|
|
40
|
+
const sessionPath = sessionPathIn(roots.sessionRoot, sessionKey);
|
|
41
|
+
await touchCacheMetadata({
|
|
42
|
+
cacheRoot: roots.sessionRoot,
|
|
43
|
+
kind: "session",
|
|
44
|
+
id: basename(sessionPath),
|
|
45
|
+
tags: { sessionId: ctx.session.id },
|
|
46
|
+
cloneStrategy,
|
|
47
|
+
});
|
|
48
|
+
const { session, created } = await runtime.openRuntimeSession({
|
|
49
|
+
id: ctx.session.id,
|
|
50
|
+
workspaceDir: sessionPath,
|
|
51
|
+
cacheRoots: roots.hidden,
|
|
52
|
+
leaseRoot: roots.sessionRoot,
|
|
53
|
+
});
|
|
54
|
+
await applyNetworkPolicy(session, created);
|
|
55
|
+
return {
|
|
56
|
+
sandbox: session,
|
|
57
|
+
// The processes are the compute and the workspace directory is the
|
|
58
|
+
// durable session: stopping kills the processes and keeps the files.
|
|
59
|
+
async onSessionStop() {
|
|
60
|
+
await session.killAll();
|
|
61
|
+
},
|
|
62
|
+
async onRuntimeShutdown() {
|
|
63
|
+
await session.killAll();
|
|
64
|
+
},
|
|
65
|
+
// The session's own workspace is disposable; the template it was
|
|
66
|
+
// cloned from is shared by every other session and must survive.
|
|
67
|
+
async onSessionDelete(deleteOptions) {
|
|
68
|
+
deleteOptions?.abortSignal?.throwIfAborted();
|
|
69
|
+
await session.killAll();
|
|
70
|
+
runtime.forgetRuntimeSession(sessionPath);
|
|
71
|
+
await rm(sessionPath, { force: true, recursive: true });
|
|
72
|
+
await removeCacheMetadata({
|
|
73
|
+
cacheRoot: roots.sessionRoot,
|
|
74
|
+
kind: "session",
|
|
75
|
+
id: basename(sessionPath),
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
async prepare(ctx) {
|
|
82
|
+
const templateKey = JSON.stringify({
|
|
83
|
+
version: 1,
|
|
84
|
+
sourceRevision: ctx.sourceRevision,
|
|
85
|
+
workspace: ctx.resources.workspace?.key ?? null,
|
|
86
|
+
skills: ctx.resources.skills?.key ?? null,
|
|
87
|
+
// eve's source revision covers the sandbox module, not helpers it
|
|
88
|
+
// imports, so the preparation's own source joins the key.
|
|
89
|
+
prepare: prepare?.toString() ?? null,
|
|
90
|
+
});
|
|
91
|
+
const roots = cacheRoots(ctx.storagePath);
|
|
92
|
+
const templatePath = templatePathIn(roots.templateRoot, templateKey, optionsHash);
|
|
93
|
+
const artifact = { version: 1, templatePath };
|
|
94
|
+
const touchTemplate = async () => await touchCacheMetadata({
|
|
95
|
+
cacheRoot: roots.templateRoot,
|
|
96
|
+
kind: "template",
|
|
97
|
+
id: basename(templatePath),
|
|
98
|
+
});
|
|
99
|
+
if (existsSync(templatePath)) {
|
|
100
|
+
await touchTemplate();
|
|
101
|
+
ctx.log?.("bwrap: reusing the prepared template");
|
|
102
|
+
return artifact;
|
|
103
|
+
}
|
|
104
|
+
ctx.log?.("bwrap: capturing the template");
|
|
105
|
+
const stagingPath = `${templatePath}.staging-${randomUUID()}`;
|
|
106
|
+
await mkdir(stagingPath, { recursive: true });
|
|
107
|
+
try {
|
|
108
|
+
const session = runtime.openTemplateSession({
|
|
109
|
+
id: basename(templatePath),
|
|
110
|
+
workspaceDir: stagingPath,
|
|
111
|
+
cacheRoots: roots.hidden,
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
await writeSeedFiles(session, resourceSeeds(ctx.resources));
|
|
115
|
+
if (prepare) {
|
|
116
|
+
runtime.assertBwrapAvailable();
|
|
117
|
+
ctx.log?.("bwrap: running sandbox preparation");
|
|
118
|
+
await prepare(session);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
// Nothing authored preparation started may outlive the capture.
|
|
123
|
+
await session.killAll();
|
|
124
|
+
}
|
|
125
|
+
await rename(stagingPath, templatePath);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
await rm(stagingPath, { force: true, recursive: true }).catch(() => { });
|
|
129
|
+
// A concurrent preparation winning the race is reuse, not failure.
|
|
130
|
+
if (existsSync(templatePath)) {
|
|
131
|
+
await touchTemplate();
|
|
132
|
+
return artifact;
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
await touchTemplate();
|
|
137
|
+
return artifact;
|
|
138
|
+
},
|
|
139
|
+
async start(ctx, openOptions, preparedArtifact) {
|
|
140
|
+
runtime.assertBwrapAvailable();
|
|
141
|
+
const { templatePath } = requireArtifact(preparedArtifact);
|
|
142
|
+
const sessionKey = `session:${ctx.session.id}`;
|
|
143
|
+
const sessionPath = sessionPathIn(cacheRoots(ctx.storagePath).sessionRoot, sessionKey);
|
|
144
|
+
let cloneStrategy = "existing";
|
|
145
|
+
if (!existsSync(sessionPath)) {
|
|
146
|
+
if (!existsSync(templatePath)) {
|
|
147
|
+
throw new BwrapTemplateNotProvisionedError({ templateKey: templatePath });
|
|
148
|
+
}
|
|
149
|
+
cloneStrategy = await cloneDirectoryAtomically({
|
|
150
|
+
sourcePath: templatePath,
|
|
151
|
+
targetPath: sessionPath,
|
|
152
|
+
copyDirectory: input.copyDirectory,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const networkPolicy = openOptions?.networkPolicy ?? options.networkPolicy;
|
|
156
|
+
const handle = await openHandle(ctx, sessionKey, cloneStrategy, async (session) => {
|
|
157
|
+
await session.setNetworkPolicy(networkPolicy);
|
|
158
|
+
});
|
|
159
|
+
return { handle, state: { version: 1, sessionKey, networkPolicy } };
|
|
160
|
+
},
|
|
161
|
+
// eve resumes at every durable step, and after a restart or a move to
|
|
162
|
+
// another deployment. Only the session workspace is needed, never the
|
|
163
|
+
// template, so the artifact passed here may belong to a newer build.
|
|
164
|
+
async resume(ctx, preparedArtifact, sessionState) {
|
|
165
|
+
runtime.assertBwrapAvailable();
|
|
166
|
+
requireArtifact(preparedArtifact);
|
|
167
|
+
const state = requireSessionState(sessionState);
|
|
168
|
+
const sessionPath = sessionPathIn(cacheRoots(ctx.storagePath).sessionRoot, state.sessionKey);
|
|
169
|
+
if (!existsSync(sessionPath)) {
|
|
170
|
+
throw new Error(`bwrap sandbox: session workspace ${sessionPath} no longer exists, so the sandbox cannot be resumed`);
|
|
171
|
+
}
|
|
172
|
+
// A live generation keeps whatever policy the session set since;
|
|
173
|
+
// only a new generation starts over from the recorded one.
|
|
174
|
+
return await openHandle(ctx, state.sessionKey, "existing", async (session, created) => {
|
|
175
|
+
if (created)
|
|
176
|
+
await session.setNetworkPolicy(state.networkPolicy);
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** eve's workspace and skill trees as seeds at their sandbox target paths. */
|
|
184
|
+
function resourceSeeds(resources) {
|
|
185
|
+
return [resources.workspace, resources.skills].flatMap((tree) => tree === undefined
|
|
186
|
+
? []
|
|
187
|
+
: tree.files.map((file) => ({
|
|
188
|
+
path: `${tree.targetPath}/${file.relativePath}`,
|
|
189
|
+
content: file.content,
|
|
190
|
+
})));
|
|
191
|
+
}
|
|
192
|
+
function isRecord(value) {
|
|
193
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
194
|
+
}
|
|
195
|
+
function requireArtifact(value) {
|
|
196
|
+
if (!isRecord(value) || value.version !== 1 || typeof value.templatePath !== "string") {
|
|
197
|
+
throw new Error("bwrap sandbox: invalid prepared artifact; rebuild the agent with `eve build`");
|
|
198
|
+
}
|
|
199
|
+
return { version: 1, templatePath: value.templatePath };
|
|
200
|
+
}
|
|
201
|
+
function isNetworkPolicy(value) {
|
|
202
|
+
return value === "allow-all" || value === "deny-all";
|
|
203
|
+
}
|
|
204
|
+
function requireSessionState(value) {
|
|
205
|
+
if (!isRecord(value) ||
|
|
206
|
+
value.version !== 1 ||
|
|
207
|
+
typeof value.sessionKey !== "string" ||
|
|
208
|
+
!isNetworkPolicy(value.networkPolicy)) {
|
|
209
|
+
throw new Error("bwrap sandbox: invalid session state");
|
|
210
|
+
}
|
|
211
|
+
return { version: 1, sessionKey: value.sessionKey, networkPolicy: value.networkPolicy };
|
|
212
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { BWRAP_PROVIDER_STATE_PROTOCOL_VERSION, createBwrapSandboxProviderDefinition, type BwrapPreparedArtifact, type BwrapSandboxEnvironmentOptions, type BwrapSandboxOpenOptions, type BwrapSandboxProviderDefinition, type BwrapSessionState, type CreateBwrapSandboxProviderDefinitionInput, } from "./provider-definition.js";
|
|
2
|
+
export { isBwrapAvailable } from "./process.js";
|
|
3
|
+
/**
|
|
4
|
+
* The bubblewrap sandbox provider for eve 0.64 and later.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // agent/sandbox.ts
|
|
8
|
+
* import { defineSandbox } from "eve/sandbox";
|
|
9
|
+
* import { BwrapSandbox } from "@evelandhq/sandbox-bwrap/provider";
|
|
10
|
+
*
|
|
11
|
+
* export const environment = BwrapSandbox.environment();
|
|
12
|
+
* export default defineSandbox(() => environment.open());
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* This entry point imports `eve/sandbox/provider`, which exists only from eve
|
|
16
|
+
* 0.64 on. On eve 0.62 and 0.63 use `bwrap()` from the package root.
|
|
17
|
+
*/
|
|
18
|
+
export declare const BwrapSandbox: import("eve/sandbox/provider").SandboxProvider<import("./provider-definition.js").BwrapSandboxEnvironmentOptions, import("./provider-definition.js").BwrapSandboxOpenOptions, import("eve/sandbox").MutableNetworkSandboxSession>;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineSandboxProvider } from "eve/sandbox/provider";
|
|
2
|
+
import { createBwrapSandboxProviderDefinition } from "./provider-definition.js";
|
|
3
|
+
export { BWRAP_PROVIDER_STATE_PROTOCOL_VERSION, createBwrapSandboxProviderDefinition, } from "./provider-definition.js";
|
|
4
|
+
export { isBwrapAvailable } from "./process.js";
|
|
5
|
+
/**
|
|
6
|
+
* The bubblewrap sandbox provider for eve 0.64 and later.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // agent/sandbox.ts
|
|
10
|
+
* import { defineSandbox } from "eve/sandbox";
|
|
11
|
+
* import { BwrapSandbox } from "@evelandhq/sandbox-bwrap/provider";
|
|
12
|
+
*
|
|
13
|
+
* export const environment = BwrapSandbox.environment();
|
|
14
|
+
* export default defineSandbox(() => environment.open());
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* This entry point imports `eve/sandbox/provider`, which exists only from eve
|
|
18
|
+
* 0.64 on. On eve 0.62 and 0.63 use `bwrap()` from the package root.
|
|
19
|
+
*/
|
|
20
|
+
export const BwrapSandbox = defineSandboxProvider(createBwrapSandboxProviderDefinition());
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ResolvedBwrapSandboxOptions } from "./options.js";
|
|
2
|
+
import type { ProcessRunner } from "./process.js";
|
|
3
|
+
import type { BwrapSession } from "./session.js";
|
|
4
|
+
export interface BwrapSeed {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly content: string | Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
export interface BwrapSessionLocation {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly workspaceDir: string;
|
|
11
|
+
/** Hidden from every sandboxed command; see `CreateBwrapSessionInput.cacheRoots`. */
|
|
12
|
+
readonly cacheRoots: readonly string[];
|
|
13
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The session machinery the backend and the provider share: one live compute
|
|
17
|
+
* generation per workspace directory, each holding a cache lease so pruning
|
|
18
|
+
* never removes a workspace in use.
|
|
19
|
+
*/
|
|
20
|
+
export interface BwrapRuntime {
|
|
21
|
+
/** Throws a setup message when the host cannot run bwrap. Probed once. */
|
|
22
|
+
assertBwrapAvailable(): void;
|
|
23
|
+
/** A throwaway session over a template being captured; it holds no lease. */
|
|
24
|
+
openTemplateSession(location: BwrapSessionLocation): BwrapSession;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the live generation for this workspace, or starts a new one.
|
|
27
|
+
* `created` tells the caller whether per-generation state such as the
|
|
28
|
+
* network policy still needs to be applied.
|
|
29
|
+
*/
|
|
30
|
+
openRuntimeSession(location: BwrapSessionLocation & {
|
|
31
|
+
readonly leaseRoot: string;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
readonly session: BwrapSession;
|
|
34
|
+
readonly created: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
/** Drops a deleted workspace's generation so the next open starts fresh. */
|
|
37
|
+
forgetRuntimeSession(workspaceDir: string): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createBwrapRuntime(input: {
|
|
40
|
+
readonly options: ResolvedBwrapSandboxOptions;
|
|
41
|
+
/** Injectable process launcher so logic is testable without bwrap. */
|
|
42
|
+
readonly runner?: ProcessRunner;
|
|
43
|
+
}): BwrapRuntime;
|
|
44
|
+
/** Writes seeds into a session; eve's `$HOME/.agents/skills` lands under the sandbox HOME. */
|
|
45
|
+
export declare function writeSeedFiles(session: BwrapSession, seedFiles: ReadonlyArray<BwrapSeed>): Promise<void>;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { createBwrapCacheLease, registerActiveCachePath } from "./cache.js";
|
|
4
|
+
import { WORKSPACE_ROOT } from "./paths.js";
|
|
5
|
+
import { createNodeProcessRunner, describeMissingPrereqs, isBwrapAvailable } from "./process.js";
|
|
6
|
+
import { createBwrapSession } from "./session.js";
|
|
7
|
+
const EVE_MODEL_SKILL_ROOT = "$HOME/.agents/skills";
|
|
8
|
+
export function createBwrapRuntime(input) {
|
|
9
|
+
const { options } = input;
|
|
10
|
+
const runner = input.runner ?? createNodeProcessRunner();
|
|
11
|
+
const generations = new Map();
|
|
12
|
+
// Probe only when running against the real bwrap; injected runners skip it.
|
|
13
|
+
const shouldProbe = input.runner === undefined;
|
|
14
|
+
let probed = false;
|
|
15
|
+
function openSession(location, generationId, onStopped) {
|
|
16
|
+
return createBwrapSession({
|
|
17
|
+
id: location.id,
|
|
18
|
+
workspaceDir: location.workspaceDir,
|
|
19
|
+
cacheRoots: location.cacheRoots,
|
|
20
|
+
runner,
|
|
21
|
+
options,
|
|
22
|
+
tags: location.tags,
|
|
23
|
+
generationId,
|
|
24
|
+
onStopped,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
assertBwrapAvailable() {
|
|
29
|
+
if (!shouldProbe || probed)
|
|
30
|
+
return;
|
|
31
|
+
const missing = describeMissingPrereqs({
|
|
32
|
+
bwrapPresent: isBwrapAvailable(options.bwrapPath),
|
|
33
|
+
workspaceMountpointPresent: existsSync(WORKSPACE_ROOT),
|
|
34
|
+
bwrapPath: options.bwrapPath,
|
|
35
|
+
});
|
|
36
|
+
if (missing)
|
|
37
|
+
throw new Error(missing);
|
|
38
|
+
probed = true;
|
|
39
|
+
},
|
|
40
|
+
openTemplateSession(location) {
|
|
41
|
+
return openSession(location);
|
|
42
|
+
},
|
|
43
|
+
async openRuntimeSession(location) {
|
|
44
|
+
const current = generations.get(location.workspaceDir);
|
|
45
|
+
if (current && current.lifecycleState() !== "stopped") {
|
|
46
|
+
return { session: current, created: false };
|
|
47
|
+
}
|
|
48
|
+
const releaseActive = registerActiveCachePath(location.workspaceDir);
|
|
49
|
+
const activeLease = await createBwrapCacheLease({
|
|
50
|
+
cacheRoot: location.leaseRoot,
|
|
51
|
+
sessionId: basename(location.workspaceDir),
|
|
52
|
+
});
|
|
53
|
+
let session;
|
|
54
|
+
try {
|
|
55
|
+
session = openSession(location, activeLease.lease.generationId, async () => {
|
|
56
|
+
releaseActive();
|
|
57
|
+
await activeLease.release();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
releaseActive();
|
|
62
|
+
await activeLease.release();
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
generations.set(location.workspaceDir, session);
|
|
66
|
+
return { session, created: true };
|
|
67
|
+
},
|
|
68
|
+
forgetRuntimeSession(workspaceDir) {
|
|
69
|
+
generations.delete(workspaceDir);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function resolveSeedPath(seedPath) {
|
|
74
|
+
if (seedPath === EVE_MODEL_SKILL_ROOT || seedPath.startsWith(`${EVE_MODEL_SKILL_ROOT}/`)) {
|
|
75
|
+
return `${WORKSPACE_ROOT}/.agents/skills${seedPath.slice(EVE_MODEL_SKILL_ROOT.length)}`;
|
|
76
|
+
}
|
|
77
|
+
return seedPath;
|
|
78
|
+
}
|
|
79
|
+
/** Writes seeds into a session; eve's `$HOME/.agents/skills` lands under the sandbox HOME. */
|
|
80
|
+
export async function writeSeedFiles(session, seedFiles) {
|
|
81
|
+
for (const seed of seedFiles) {
|
|
82
|
+
const seedPath = resolveSeedPath(seed.path);
|
|
83
|
+
if (typeof seed.content === "string") {
|
|
84
|
+
await session.writeTextFile({ path: seedPath, content: seed.content });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await session.writeBinaryFile({ path: seedPath, content: seed.content });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import type { SandboxSession } from "eve/sandbox";
|
|
1
|
+
import type { SandboxNetworkPolicy, SandboxSession } from "eve/sandbox";
|
|
2
2
|
import type { ResolvedBwrapSandboxOptions } from "./options.js";
|
|
3
3
|
import type { ProcessRunner } from "./process.js";
|
|
4
4
|
export interface CreateBwrapSessionInput {
|
|
5
5
|
readonly id: string;
|
|
6
6
|
readonly workspaceDir: string;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Host directories holding templates and session workspaces. Every one is
|
|
9
|
+
* masked with an empty tmpfs so a sandboxed command can never read another
|
|
10
|
+
* session's workspace or a template it was not cloned from.
|
|
11
|
+
*/
|
|
12
|
+
readonly cacheRoots: readonly string[];
|
|
8
13
|
readonly runner: ProcessRunner;
|
|
9
14
|
readonly options: ResolvedBwrapSandboxOptions;
|
|
10
15
|
readonly generationId?: string;
|
|
@@ -17,6 +22,13 @@ export interface CreateBwrapSessionInput {
|
|
|
17
22
|
* session tracks the processes it spawned and can terminate them on demand.
|
|
18
23
|
*/
|
|
19
24
|
export type BwrapSession = SandboxSession & {
|
|
25
|
+
/**
|
|
26
|
+
* Stable identifier of the durable session this handle wraps. eve 0.64
|
|
27
|
+
* dropped `id` from `SandboxSession`, but every earlier line requires it.
|
|
28
|
+
*/
|
|
29
|
+
readonly id: string;
|
|
30
|
+
/** Coarse egress switch; always present here, optional on eve 0.64's `SandboxSession`. */
|
|
31
|
+
setNetworkPolicy(policy: SandboxNetworkPolicy): Promise<void>;
|
|
20
32
|
/** Kills every process this session spawned that has not yet exited. Idempotent. */
|
|
21
33
|
killAll(): Promise<void>;
|
|
22
34
|
/** Internal compute-generation state used to coordinate repeated handles. */
|
package/dist/session.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Readable } from "node:stream";
|
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
import { createWriteStream } from "node:fs";
|
|
8
8
|
import { buildBwrapExecArgs, DEFAULT_SANDBOX_PATH } from "./args.js";
|
|
9
|
-
import { isWithinWorkspaceReal,
|
|
9
|
+
import { isWithinWorkspaceReal, resolveWorkspacePath, toHostPath, WORKSPACE_ROOT, } from "./paths.js";
|
|
10
10
|
function isMissingFileError(error) {
|
|
11
11
|
return (typeof error === "object" &&
|
|
12
12
|
error !== null &&
|
|
@@ -62,7 +62,7 @@ function createRunAbortSignal(callerSignal, timeoutMs, outputSignal) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
export function createBwrapSession(input) {
|
|
65
|
-
const { id, workspaceDir,
|
|
65
|
+
const { id, workspaceDir, cacheRoots, runner, options } = input;
|
|
66
66
|
const generationId = input.generationId ?? randomUUID();
|
|
67
67
|
const tags = input.tags ?? {};
|
|
68
68
|
let networkPolicy = options.networkPolicy;
|
|
@@ -205,10 +205,7 @@ export function createBwrapSession(input) {
|
|
|
205
205
|
...options.env,
|
|
206
206
|
...spawnOptions.env,
|
|
207
207
|
};
|
|
208
|
-
const hidePaths = [
|
|
209
|
-
resolveBwrapCacheRoot(appRoot, options.cacheDir),
|
|
210
|
-
...options.hidePaths,
|
|
211
|
-
].filter((path) => existsSync(path));
|
|
208
|
+
const hidePaths = [...new Set([...cacheRoots, ...options.hidePaths])].filter((path) => existsSync(path));
|
|
212
209
|
const argv = buildBwrapExecArgs({
|
|
213
210
|
bwrapPath: options.bwrapPath,
|
|
214
211
|
workspaceDir,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evelandhq/sandbox-bwrap",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "bubblewrap
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "bubblewrap sandbox for eve agents — real exec sandboxing without Docker or KVM",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
7
7
|
"bubblewrap",
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
"types": "./dist/index.d.ts",
|
|
26
26
|
"import": "./dist/index.js",
|
|
27
27
|
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./provider": {
|
|
30
|
+
"types": "./dist/provider.d.ts",
|
|
31
|
+
"import": "./dist/provider.js",
|
|
32
|
+
"default": "./dist/provider.js"
|
|
28
33
|
}
|
|
29
34
|
},
|
|
30
35
|
"scripts": {
|
|
@@ -40,8 +45,8 @@
|
|
|
40
45
|
"devDependencies": {
|
|
41
46
|
"@types/node": "^26.0.1",
|
|
42
47
|
"ai": "^7.0.44",
|
|
43
|
-
"eve": "0.
|
|
44
|
-
"eve-floor": "npm:eve@0.
|
|
48
|
+
"eve": "0.64.1",
|
|
49
|
+
"eve-floor": "npm:eve@0.62.0",
|
|
45
50
|
"oxfmt": "0.58.0",
|
|
46
51
|
"oxlint": "1.73.0",
|
|
47
52
|
"tsx": "^4.22.4",
|
|
@@ -49,7 +54,7 @@
|
|
|
49
54
|
"vitest": "^4.1.9"
|
|
50
55
|
},
|
|
51
56
|
"peerDependencies": {
|
|
52
|
-
"eve": ">=0.
|
|
57
|
+
"eve": ">=0.62.0 <1.0.0"
|
|
53
58
|
},
|
|
54
59
|
"engines": {
|
|
55
60
|
"node": ">=24.0.0"
|