@zenera/sandbox 1.1.58 → 1.2.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 +30 -11
- package/dist/bwrap.d.ts +143 -0
- package/dist/bwrap.js +707 -0
- package/dist/contract.d.ts +5 -6
- package/dist/fs.d.ts +0 -7
- package/dist/fs.js +1 -18
- package/dist/in-memory-fs.d.ts +0 -2
- package/dist/in-memory-fs.js +0 -12
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mounts.d.ts +4 -45
- package/dist/mounts.js +3 -45
- package/dist/podman.d.ts +4 -6
- package/dist/podman.js +10 -7
- package/dist/process.d.ts +2 -0
- package/dist/process.js +5 -1
- package/dist/rooted-fs.d.ts +41 -0
- package/dist/rooted-fs.js +169 -0
- package/dist/testing.js +4 -17
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,8 +10,7 @@ import { podmanSandbox } from '@zenera/sandbox';
|
|
|
10
10
|
|
|
11
11
|
const sandbox = podmanSandbox({ root: '/path/to/workspace', key: 'session-1' });
|
|
12
12
|
await sandbox.exec('python -V');
|
|
13
|
-
|
|
14
|
-
await view.fs!.writeFile(`${view.root}/notes.md`, 'hello');
|
|
13
|
+
await sandbox.fs.writeFile('/workspace/notes.md', 'hello'); // what `cat notes.md` sees
|
|
15
14
|
await sandbox.close();
|
|
16
15
|
```
|
|
17
16
|
|
|
@@ -19,14 +18,18 @@ await sandbox.close();
|
|
|
19
18
|
|
|
20
19
|
- **`Sandbox`** - `prepare` (pull the image, or build it from `build:`),
|
|
21
20
|
`exec`, background jobs (`startJob`, `readJob`, `stopJob`),
|
|
22
|
-
`close` at the end of a turn, and the files: `fs
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
`close` at the end of a turn, and the files: `fs`, an `FsDriver` in the
|
|
22
|
+
agent's own paths (`/workspace/a.py`) and contained to the sandbox's trees.
|
|
23
|
+
The invariant: **a file written through `fs` is the file `exec` sees under
|
|
24
|
+
the same name, and the reverse.**
|
|
26
25
|
- **`SandboxFactory`** - `(opts: SandboxOptions) => Sandbox`, how a host picks
|
|
27
26
|
where sandboxes run.
|
|
28
|
-
- **`FsDriver`** - the filesystem seam, usable without a sandbox. `
|
|
29
|
-
this machine
|
|
27
|
+
- **`FsDriver`** - the filesystem seam, usable without a sandbox. `RootedFs`
|
|
28
|
+
serves named trees from directories on this machine - `/workspace` from the
|
|
29
|
+
session's folder, `/assets` read-only - and is the one place a path is
|
|
30
|
+
contained: it must name a tree, its symlinks must resolve into one, and a
|
|
31
|
+
read-only tree refuses every change. `localFs` is this machine's disk as it
|
|
32
|
+
is; `InMemoryFs` keeps a tree in memory.
|
|
30
33
|
|
|
31
34
|
## The image
|
|
32
35
|
|
|
@@ -43,10 +46,26 @@ by name, across copies of this package).
|
|
|
43
46
|
## Implementations
|
|
44
47
|
|
|
45
48
|
- **`PodmanSandbox`** / `podmanSandbox` - a podman container on this machine,
|
|
46
|
-
binding host directories, with `fs
|
|
47
|
-
image step on its own.
|
|
49
|
+
binding host directories, with `fs` a `RootedFs` over them under the names
|
|
50
|
+
they are bound at. `ensurePodmanImage` is its image step on its own.
|
|
51
|
+
- **`BwrapSandbox`** / `bwrapSandbox` - for a host that is itself the
|
|
52
|
+
container, such as the agent image: each command runs under bubblewrap
|
|
53
|
+
(`/opt/zenera/bin/bwrap`, else `bwrap` on PATH) in its own user, pid, ipc and
|
|
54
|
+
uts namespaces, on a read-only view of the image, with the session's
|
|
55
|
+
directories bound in and the server's own paths (`/state`, `/run/secrets`)
|
|
56
|
+
hidden. bwrap is started with exactly the command's environment, never this
|
|
57
|
+
process's: the image's `ENV` (`/opt/zenera/etc/image-env.json`, written when
|
|
58
|
+
the agent image is built; else `PATH`, `LANG`, `LC_*`, `TZ`) plus the run's.
|
|
59
|
+
`/proc` is the command's own where the container lets one be
|
|
60
|
+
mounted, else the container's bound read-only (`probeBwrap()` says which, or
|
|
61
|
+
why bwrap cannot isolate anything; nothing falls back to running unisolated).
|
|
62
|
+
`prepare()` builds nothing: it checks that the container was built from the
|
|
63
|
+
project's Dockerfile (`ZEN_IMAGE_BUILD`) or image (`ZEN_IMAGE_BASE`), when
|
|
64
|
+
it says what it was built from.
|
|
65
|
+
`cpus`, `memory` and `persist` belong to the container and are ignored;
|
|
66
|
+
commands get `oom_score_adj` 1000, so they go before the server.
|
|
48
67
|
- A sandbox that keeps its files with the container (a sandbox service) serves
|
|
49
|
-
`fs` itself
|
|
68
|
+
`fs` itself, under the same names.
|
|
50
69
|
|
|
51
70
|
## Testing an implementation
|
|
52
71
|
|
package/dist/bwrap.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { type Mount } from './mounts.ts';
|
|
2
|
+
import { type Runner } from './process.ts';
|
|
3
|
+
import { RootedFs } from './rooted-fs.ts';
|
|
4
|
+
import { type BuildSpec, type ExecOptions, type ExecResult, type JobOutput, type JobStopped, type PrepareOptions, type Sandbox, type SandboxFactory, type SandboxHardening, type SandboxNetwork, type SandboxOptions } from './contract.ts';
|
|
5
|
+
/** Where the agent image keeps its bwrap: a static build, so any base runs it. */
|
|
6
|
+
export declare const ZENERA_BWRAP = "/opt/zenera/bin/bwrap";
|
|
7
|
+
/**
|
|
8
|
+
* The project image's own environment (its Dockerfile's `ENV` lines), written by the agent
|
|
9
|
+
* image's build as a JSON object: what a command gets to start from, as it would in that image.
|
|
10
|
+
*/
|
|
11
|
+
export declare const IMAGE_ENV_FILE = "/opt/zenera/etc/image-env.json";
|
|
12
|
+
/** Set by `zen image build`: what the image was built from. */
|
|
13
|
+
export declare const IMAGE_BUILD_ENV = "ZEN_IMAGE_BUILD";
|
|
14
|
+
export declare const IMAGE_BASE_ENV = "ZEN_IMAGE_BASE";
|
|
15
|
+
/** `--proc`: a `/proc` of the command's own, or the container's, bound read-only. */
|
|
16
|
+
export type ProcMode = 'fresh' | 'bind';
|
|
17
|
+
export interface BwrapProbe {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
/** the binary probed */
|
|
20
|
+
bwrap: string;
|
|
21
|
+
/** how `/proc` is given to commands, when `ok` */
|
|
22
|
+
proc?: ProcMode;
|
|
23
|
+
/** why not, when not `ok` */
|
|
24
|
+
error?: string;
|
|
25
|
+
hint?: string;
|
|
26
|
+
}
|
|
27
|
+
/** What a host can tell the sandbox about where it runs; the defaults are the agent image's. */
|
|
28
|
+
export interface BwrapHost {
|
|
29
|
+
/** the bwrap binary; `/opt/zenera/bin/bwrap`, else `bwrap` on PATH */
|
|
30
|
+
bwrap?: string;
|
|
31
|
+
/** where the base environment and secrets are read from; `process.env` */
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
/** more paths no command sees, besides `/state` and `/run/secrets` (always hidden) */
|
|
34
|
+
hidden?: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* What this image was built from, which `prepare` checks the spec against;
|
|
37
|
+
* from `ZEN_IMAGE_BUILD` / `ZEN_IMAGE_BASE` when unset. Neither known, or
|
|
38
|
+
* `false`: no check — the container is whatever its host made it.
|
|
39
|
+
*/
|
|
40
|
+
image?: false | {
|
|
41
|
+
build?: string;
|
|
42
|
+
base?: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* What a command's environment starts from: the image's own `ENV` (IMAGE_ENV_FILE, when the
|
|
46
|
+
* container has it), else PATH, LANG, LC_* and TZ of this process. Never the rest of it.
|
|
47
|
+
*/
|
|
48
|
+
imageEnv?: Record<string, string>;
|
|
49
|
+
/** how processes are spawned — the seam tests replace */
|
|
50
|
+
exec?: Runner;
|
|
51
|
+
/** the host's filesystem as the root is built from it — the seam tests replace */
|
|
52
|
+
tree?: HostTree;
|
|
53
|
+
}
|
|
54
|
+
/** The host's filesystem, as far as building a command's root needs it. */
|
|
55
|
+
export interface HostTree {
|
|
56
|
+
kind(path: string): 'dir' | 'file' | 'symlink' | 'missing';
|
|
57
|
+
list(path: string): string[];
|
|
58
|
+
link(path: string): string;
|
|
59
|
+
}
|
|
60
|
+
export declare const hostTree: HostTree;
|
|
61
|
+
interface Resolved {
|
|
62
|
+
root: string;
|
|
63
|
+
key: string;
|
|
64
|
+
image: string;
|
|
65
|
+
build?: BuildSpec;
|
|
66
|
+
network: SandboxNetwork;
|
|
67
|
+
workdir: string;
|
|
68
|
+
timeout: number;
|
|
69
|
+
hardening: SandboxHardening;
|
|
70
|
+
user?: {
|
|
71
|
+
uid: string;
|
|
72
|
+
gid?: string;
|
|
73
|
+
};
|
|
74
|
+
env: Record<string, string>;
|
|
75
|
+
secrets: readonly string[];
|
|
76
|
+
readOnly: boolean;
|
|
77
|
+
mounts: readonly Mount[];
|
|
78
|
+
execEnv: Record<string, string>;
|
|
79
|
+
execPaths: Record<string, readonly string[]>;
|
|
80
|
+
}
|
|
81
|
+
export declare class BwrapSandbox implements Sandbox {
|
|
82
|
+
#private;
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly fs: RootedFs;
|
|
85
|
+
readonly spec: Resolved;
|
|
86
|
+
constructor(opts: SandboxOptions, host?: BwrapHost);
|
|
87
|
+
get started(): boolean;
|
|
88
|
+
/** Checks that this container is the image the project describes; builds nothing. */
|
|
89
|
+
prepare(opts?: PrepareOptions): Promise<void>;
|
|
90
|
+
start(): Promise<void>;
|
|
91
|
+
exec(command: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
92
|
+
/**
|
|
93
|
+
* Starts a command and returns. Its log is a file of this process's, outside
|
|
94
|
+
* the command's view; its exit code is the one bwrap reports.
|
|
95
|
+
*/
|
|
96
|
+
startJob(command: string, opts?: ExecOptions): Promise<string>;
|
|
97
|
+
readJob(id: string, from?: number): Promise<JobOutput>;
|
|
98
|
+
/**
|
|
99
|
+
* Ends bwrap, and with it everything the job started: the command is the
|
|
100
|
+
* first process of a pid namespace, and a namespace whose first process
|
|
101
|
+
* dies takes the rest with it.
|
|
102
|
+
*/
|
|
103
|
+
stopJob(id: string): Promise<JobStopped>;
|
|
104
|
+
/** Ends every command and job; the files stay where the mounts keep them. */
|
|
105
|
+
close(): Promise<void>;
|
|
106
|
+
/** A path inside the sandbox, contained to the working directory. */
|
|
107
|
+
inside(path?: string): string;
|
|
108
|
+
}
|
|
109
|
+
export declare const bwrapSandbox: SandboxFactory;
|
|
110
|
+
export interface BwrapArgs {
|
|
111
|
+
tree: HostTree;
|
|
112
|
+
hidden: readonly string[];
|
|
113
|
+
/** shallowest first */
|
|
114
|
+
binds: readonly {
|
|
115
|
+
at: string;
|
|
116
|
+
host: string;
|
|
117
|
+
readOnly: boolean;
|
|
118
|
+
}[];
|
|
119
|
+
proc: ProcMode;
|
|
120
|
+
network: SandboxNetwork;
|
|
121
|
+
strict: boolean;
|
|
122
|
+
user?: {
|
|
123
|
+
uid: string;
|
|
124
|
+
gid?: string;
|
|
125
|
+
};
|
|
126
|
+
cwd: string;
|
|
127
|
+
/** more mounts, after `/tmp` exists */
|
|
128
|
+
extra?: readonly string[];
|
|
129
|
+
command: readonly string[];
|
|
130
|
+
}
|
|
131
|
+
export declare function bwrapArgs(a: BwrapArgs): string[];
|
|
132
|
+
/**
|
|
133
|
+
* The host's root, bound piece by piece: `targets` are mounted later, `omit`
|
|
134
|
+
* is never shown. A directory is opened only for a mount point it lacks;
|
|
135
|
+
* whatever it holds is then bound entry by entry, so this stays rare — under
|
|
136
|
+
* SELinux a bind of something the runtime itself mounted can be refused.
|
|
137
|
+
*/
|
|
138
|
+
export declare function rootArgs(tree: HostTree, targets: readonly string[], omit: readonly string[]): string[];
|
|
139
|
+
/** What is hidden inside a bound directory is covered: an empty tmpfs, or /dev/null. */
|
|
140
|
+
export declare function hiddenArgs(tree: HostTree, hidden: readonly string[]): string[];
|
|
141
|
+
export declare function probeBwrap(bwrap?: string, run?: Runner): Promise<BwrapProbe>;
|
|
142
|
+
export {};
|
|
143
|
+
//# sourceMappingURL=bwrap.d.ts.map
|