@zenera/sandbox 0.0.0-stage → 1.1.57
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 +21 -0
- package/README.md +60 -2
- package/dist/contract.d.ts +230 -0
- package/dist/contract.js +27 -0
- package/dist/fs.d.ts +46 -0
- package/dist/fs.js +39 -0
- package/dist/image.d.ts +62 -0
- package/dist/image.js +210 -0
- package/dist/in-memory-fs.d.ts +25 -0
- package/dist/in-memory-fs.js +165 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +12 -0
- package/dist/limits.d.ts +19 -0
- package/dist/limits.js +33 -0
- package/dist/mounts.d.ts +84 -0
- package/dist/mounts.js +63 -0
- package/dist/podman.d.ts +80 -0
- package/dist/podman.js +575 -0
- package/dist/process.d.ts +29 -0
- package/dist/process.js +122 -0
- package/dist/testing.d.ts +25 -0
- package/dist/testing.js +118 -0
- package/package.json +58 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrey Ryabov
|
|
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
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zenera/sandbox
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Where an agent's commands run, and the files those commands see. One
|
|
4
|
+
`Sandbox` per session, shared by every agent in it. Depends on Node alone, so a
|
|
5
|
+
host, a sandbox service and the agent inside a container can all build on the
|
|
6
|
+
same contract.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { podmanSandbox } from '@zenera/sandbox';
|
|
10
|
+
|
|
11
|
+
const sandbox = podmanSandbox({ root: '/path/to/workspace', key: 'session-1' });
|
|
12
|
+
await sandbox.exec('python -V');
|
|
13
|
+
const view = sandbox.files({ root: '/path/to/workspace', mount: '/workspace' });
|
|
14
|
+
await view.fs!.writeFile(`${view.root}/notes.md`, 'hello');
|
|
15
|
+
await sandbox.close();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## The contract
|
|
19
|
+
|
|
20
|
+
- **`Sandbox`** - `prepare` (pull the image, or build it from `build:`),
|
|
21
|
+
`exec`, background jobs (`startJob`, `readJob`, `stopJob`),
|
|
22
|
+
`close` at the end of a turn, and the files: `fs` (an `FsDriver`) and
|
|
23
|
+
`files(view)`, which maps the host's workspace and mounts to the paths `fs`
|
|
24
|
+
finds them at. The invariant: **a file written through `fs` is the file
|
|
25
|
+
`exec` sees under the same name, and the reverse.**
|
|
26
|
+
- **`SandboxFactory`** - `(opts: SandboxOptions) => Sandbox`, how a host picks
|
|
27
|
+
where sandboxes run.
|
|
28
|
+
- **`FsDriver`** - the filesystem seam, usable without a sandbox. `localFs` is
|
|
29
|
+
this machine's disk; `InMemoryFs` keeps a tree in memory.
|
|
30
|
+
|
|
31
|
+
## The image
|
|
32
|
+
|
|
33
|
+
A spec names an `image` to pull, or a `build` (`{ dockerfile, context, args }`)
|
|
34
|
+
to build. Getting it there is the sandbox's job, in `prepare()`: podman builds
|
|
35
|
+
on this machine, a sandbox service on its own builders. What every backend
|
|
36
|
+
shares is the build's identity, `buildIdentity(build)` - the context's files
|
|
37
|
+
and the hash of them - so a project names the same image everywhere.
|
|
38
|
+
`prepare({ onProgress, slow, force, allowBuild: false })` reports progress,
|
|
39
|
+
rebuilds, or refuses to build at all; `start` does whatever of it is still
|
|
40
|
+
needed. A failed build is an `ImageBuildError` (`isImageBuildError` checks it
|
|
41
|
+
by name, across copies of this package).
|
|
42
|
+
|
|
43
|
+
## Implementations
|
|
44
|
+
|
|
45
|
+
- **`PodmanSandbox`** / `podmanSandbox` - a podman container on this machine,
|
|
46
|
+
binding host directories, with `fs = localFs`. `ensurePodmanImage` is its
|
|
47
|
+
image step on its own.
|
|
48
|
+
- A sandbox that keeps its files with the container (a sandbox service) serves
|
|
49
|
+
`fs` itself and returns `inSandboxPaths(view, fs)` from `files()`.
|
|
50
|
+
|
|
51
|
+
## Testing an implementation
|
|
52
|
+
|
|
53
|
+
`@zenera/sandbox/testing` exports the contract as vitest suites, the same ones
|
|
54
|
+
this package runs against its own implementations:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { describeFsDriver, describeSandbox } from '@zenera/sandbox/testing';
|
|
58
|
+
|
|
59
|
+
describeFsDriver('remote fs', () => ({ fs: new RemoteFs(url), root: '/workspace' }));
|
|
60
|
+
describeSandbox('remote', () => ({ sandbox: openRemote(opts), root: opts.root }));
|
|
61
|
+
```
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { FsDriver } from './fs.ts';
|
|
2
|
+
import type { FilesView, Mount } from './mounts.ts';
|
|
3
|
+
/** The image a project gets when it does not name one. */
|
|
4
|
+
export declare const DEFAULT_SANDBOX_IMAGE = "docker.io/library/python:3.14-slim-bookworm";
|
|
5
|
+
/**
|
|
6
|
+
* Everything about a container that an author can state: the project's
|
|
7
|
+
* `sandbox:` block, resolved. The identity of a container here *is* its
|
|
8
|
+
* configuration, so a persisted one is reused only while the spec is equal.
|
|
9
|
+
*/
|
|
10
|
+
export interface SandboxSpec {
|
|
11
|
+
/** the base image commands run in */
|
|
12
|
+
image?: string;
|
|
13
|
+
/**
|
|
14
|
+
* A Dockerfile to build instead of an image to pull. The sandbox builds it
|
|
15
|
+
* where it runs — podman on this machine, a service on its own builders —
|
|
16
|
+
* under a tag derived from its content; `image` is then ignored.
|
|
17
|
+
*/
|
|
18
|
+
build?: BuildSpec;
|
|
19
|
+
/** fractional cores, as podman's `--cpus` */
|
|
20
|
+
cpus?: number;
|
|
21
|
+
/** MiB, as podman's `--memory` */
|
|
22
|
+
memory?: number;
|
|
23
|
+
network?: SandboxNetwork;
|
|
24
|
+
/** where the workspace is mounted, and the default cwd */
|
|
25
|
+
workdir?: string;
|
|
26
|
+
/** default seconds a single command may take */
|
|
27
|
+
timeout?: number;
|
|
28
|
+
/** how much of the host the container is allowed to be; `standard` by default */
|
|
29
|
+
hardening?: SandboxHardening;
|
|
30
|
+
/** uid, name, or `uid:gid`; unset means the image's own user */
|
|
31
|
+
user?: string;
|
|
32
|
+
/** keep the container between sessions rather than removing it */
|
|
33
|
+
persist?: boolean;
|
|
34
|
+
/** environment inside the container — resolved values, never a passthrough */
|
|
35
|
+
env?: Record<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Variables forwarded from this process by *name*, for credentials.
|
|
38
|
+
*
|
|
39
|
+
* Separate from `env` for two reasons, both about the value never being
|
|
40
|
+
* written down. `--env NAME` without an `=` tells podman to read the value
|
|
41
|
+
* out of its own environment, so the secret never appears on a command
|
|
42
|
+
* line, in `ps`, or in `podman inspect`. And the container's identity is
|
|
43
|
+
* computed from the names alone, so rotating a key does not abandon a
|
|
44
|
+
* persisted container's filesystem.
|
|
45
|
+
*/
|
|
46
|
+
secrets?: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
export type SandboxNetwork = 'bridge' | 'none' | 'host';
|
|
49
|
+
/** A Dockerfile and what it may see. Paths are absolute and already checked by the host. */
|
|
50
|
+
export interface BuildSpec {
|
|
51
|
+
dockerfile: string;
|
|
52
|
+
/** the build context: what `COPY` can reach */
|
|
53
|
+
context: string;
|
|
54
|
+
/** `--build-arg`s; part of the image's identity */
|
|
55
|
+
args?: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
/** How a sandbox gets its image ready. */
|
|
58
|
+
export interface PrepareOptions {
|
|
59
|
+
/** lines worth showing a person: what is being pulled or built, and how it ended */
|
|
60
|
+
onProgress?(line: string): void;
|
|
61
|
+
/** wraps a slow step — a pull, a build — so a host can show it is still going */
|
|
62
|
+
slow?<T>(label: string, work: Promise<T>): Promise<T>;
|
|
63
|
+
/** pull or build again even when the image is there; `zen sandbox pull` */
|
|
64
|
+
force?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* false: an image that would have to be built is a `SandboxError` instead —
|
|
67
|
+
* for a host whose images are built somewhere else, ahead of time.
|
|
68
|
+
*/
|
|
69
|
+
allowBuild?: boolean;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Two postures, because they cannot both be the default.
|
|
74
|
+
*
|
|
75
|
+
* `standard` is the one this tool was built around: a container with an init,
|
|
76
|
+
* a pid ceiling and `no-new-privileges`, running an image the agent can still
|
|
77
|
+
* install into. Installing a package is the ordinary use of a shell, so the
|
|
78
|
+
* capabilities stay and the root filesystem stays writable.
|
|
79
|
+
*
|
|
80
|
+
* `strict` is for running code nobody has read. Every capability is dropped,
|
|
81
|
+
* the root filesystem is read-only, the only writable temporary space is an
|
|
82
|
+
* in-memory `/tmp` with `noexec,nosuid,nodev`, the process runs as the host's
|
|
83
|
+
* own unprivileged uid rather than as container root, and the network is off
|
|
84
|
+
* unless the project turns it back on. What that costs is exactly what it
|
|
85
|
+
* removes: `apt`, `pip` and anything else that writes outside the mounts stop
|
|
86
|
+
* working, so a strict project bakes its dependencies into the image instead.
|
|
87
|
+
*
|
|
88
|
+
* What it does *not* do is make the bind mounts go away. `/workspace` is the
|
|
89
|
+
* work itself and stays writable — the profile shrinks the blast radius of a
|
|
90
|
+
* command, it does not pretend the agent has nothing to edit.
|
|
91
|
+
*/
|
|
92
|
+
export type SandboxHardening = 'standard' | 'strict';
|
|
93
|
+
export interface SandboxOptions extends SandboxSpec {
|
|
94
|
+
/** host directory mounted at `workdir` */
|
|
95
|
+
root: string;
|
|
96
|
+
/**
|
|
97
|
+
* Stable name for whatever owns this sandbox — a session id, typically.
|
|
98
|
+
* It prefixes the container name, so a crashed process leaves something a
|
|
99
|
+
* human can recognise rather than a hash.
|
|
100
|
+
*/
|
|
101
|
+
key: string;
|
|
102
|
+
/** refuse to mount the workspace writable */
|
|
103
|
+
readOnly?: boolean;
|
|
104
|
+
/** extra bind mounts; the host side must already exist */
|
|
105
|
+
mounts?: readonly Mount[];
|
|
106
|
+
/**
|
|
107
|
+
* Environment set on every command rather than on the container: values
|
|
108
|
+
* that belong to the run, not to the container — the user it is acting for.
|
|
109
|
+
*
|
|
110
|
+
* `env` is fixed when the container is created, and a persisted container
|
|
111
|
+
* is kept between sessions; a run's values written there
|
|
112
|
+
* would still be answering for whoever came before. Not part of the
|
|
113
|
+
* container's name either, for the same reason in reverse: a new user is
|
|
114
|
+
* not a new machine. Plain values on podman's command line, so nothing
|
|
115
|
+
* secret goes here — that is what `secrets` is for.
|
|
116
|
+
*/
|
|
117
|
+
execEnv?: Record<string, string>;
|
|
118
|
+
/**
|
|
119
|
+
* Directories put in front of a path-like variable for every command —
|
|
120
|
+
* `PYTHONPATH`, say — keeping whatever the image already set behind them.
|
|
121
|
+
* `--env` could only replace the image's value, and an image that relies on
|
|
122
|
+
* its own `PYTHONPATH` would break on the first `python`.
|
|
123
|
+
*/
|
|
124
|
+
execPaths?: Record<string, readonly string[]>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* One session's sandbox. `PodmanSandbox` is one on this machine; a remote
|
|
128
|
+
* sandbox service is another. The tools and the job model are the same for
|
|
129
|
+
* both.
|
|
130
|
+
*/
|
|
131
|
+
export interface Sandbox {
|
|
132
|
+
/**
|
|
133
|
+
* Derived from the spec, so a persisted sandbox is found again by an equal
|
|
134
|
+
* spec, and a host can tell a reused one from a new one.
|
|
135
|
+
*/
|
|
136
|
+
readonly name: string;
|
|
137
|
+
/** the files every command sees, under the same names */
|
|
138
|
+
readonly fs: FsDriver;
|
|
139
|
+
/**
|
|
140
|
+
* The file tools' view of this sandbox, in its own paths: given the host's
|
|
141
|
+
* workspace and mounts, where `fs` actually finds each of them.
|
|
142
|
+
*/
|
|
143
|
+
files(opts: FilesView): FilesView;
|
|
144
|
+
/** whether anything was actually started — `close` is free when not */
|
|
145
|
+
readonly started: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Makes the image available — pulled, or built from `build` — without
|
|
148
|
+
* starting anything, so a host can do the slow part before the first turn
|
|
149
|
+
* and show it. Idempotent and single-flight unless `force`; `start` does
|
|
150
|
+
* whatever of it is still needed.
|
|
151
|
+
*/
|
|
152
|
+
prepare(opts?: PrepareOptions): Promise<void>;
|
|
153
|
+
/** idempotent and single-flight; every other call starts the sandbox itself */
|
|
154
|
+
start(): Promise<void>;
|
|
155
|
+
exec(command: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
156
|
+
/** starts a command and returns its job id; the output goes to a log */
|
|
157
|
+
startJob(command: string, opts?: ExecOptions): Promise<string>;
|
|
158
|
+
/**
|
|
159
|
+
* A window of a job's log, from line `from` (1-based), plus whether it is
|
|
160
|
+
* still going. An unknown id is a `SandboxError`.
|
|
161
|
+
*/
|
|
162
|
+
readJob(id: string, from?: number): Promise<JobOutput>;
|
|
163
|
+
/** Signals the job and everything it started. An unknown id is a `SandboxError`. */
|
|
164
|
+
stopJob(id: string): Promise<JobStopped>;
|
|
165
|
+
/**
|
|
166
|
+
* The session is done for now (the end of a turn): stops a persisted
|
|
167
|
+
* sandbox, removes an ephemeral one, and keeps the files — a sandbox that
|
|
168
|
+
* holds them elsewhere persists them here.
|
|
169
|
+
*/
|
|
170
|
+
close(): Promise<void>;
|
|
171
|
+
}
|
|
172
|
+
/** Opens a session's sandbox. Cheap: nothing starts until it is used. */
|
|
173
|
+
export type SandboxFactory = (opts: SandboxOptions) => Sandbox;
|
|
174
|
+
/** A failure the caller can do something about — a missing engine, usually. */
|
|
175
|
+
export declare class SandboxError extends Error {
|
|
176
|
+
readonly hint?: string;
|
|
177
|
+
constructor(message: string, hint?: string);
|
|
178
|
+
}
|
|
179
|
+
export interface ExecOptions {
|
|
180
|
+
cwd?: string;
|
|
181
|
+
/** seconds; the spec's default otherwise */
|
|
182
|
+
timeout?: number;
|
|
183
|
+
signal?: AbortSignal;
|
|
184
|
+
}
|
|
185
|
+
export interface ExecResult {
|
|
186
|
+
exit_code: number;
|
|
187
|
+
stdout: string;
|
|
188
|
+
stderr: string;
|
|
189
|
+
truncated?: boolean;
|
|
190
|
+
timed_out?: boolean;
|
|
191
|
+
duration_ms: number;
|
|
192
|
+
out_of_memory?: OutOfMemory;
|
|
193
|
+
}
|
|
194
|
+
export interface JobOutput {
|
|
195
|
+
job_id: string;
|
|
196
|
+
command: string;
|
|
197
|
+
running: boolean;
|
|
198
|
+
/** once it has ended */
|
|
199
|
+
exit_code?: number;
|
|
200
|
+
duration_ms: number;
|
|
201
|
+
/** the window returned, 1-based and inclusive */
|
|
202
|
+
start_line: number;
|
|
203
|
+
end_line: number;
|
|
204
|
+
/** how long the log is now */
|
|
205
|
+
lines: number;
|
|
206
|
+
/** more of the log follows `end_line` */
|
|
207
|
+
truncated?: boolean;
|
|
208
|
+
output: string;
|
|
209
|
+
out_of_memory?: OutOfMemory;
|
|
210
|
+
}
|
|
211
|
+
export interface JobStopped {
|
|
212
|
+
job_id: string;
|
|
213
|
+
stopped: boolean;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Why a command was killed for memory, which is a different question from
|
|
217
|
+
* whether it was. A container that overran its own `--memory` will do it again
|
|
218
|
+
* however long anyone waits; one the kernel sacrificed for a neighbour may
|
|
219
|
+
* simply succeed later. Exit code 137 is all either of them says.
|
|
220
|
+
*/
|
|
221
|
+
export interface OutOfMemory {
|
|
222
|
+
/** what this container was allowed */
|
|
223
|
+
limit_mib: number;
|
|
224
|
+
/** high-water mark for the container's whole life, not for this command */
|
|
225
|
+
peak_mib?: number;
|
|
226
|
+
/** false when the limit itself is the problem, so an unchanged rerun ends the same way */
|
|
227
|
+
retryable: boolean;
|
|
228
|
+
hint: string;
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=contract.d.ts.map
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The sandbox
|
|
3
|
+
//
|
|
4
|
+
// The whole environment a session's agents work in: one place commands run —
|
|
5
|
+
// a container, here or on a sandbox service — and the files they see. Every
|
|
6
|
+
// agent of the session shares it. This is the contract every implementation
|
|
7
|
+
// keeps, with one invariant:
|
|
8
|
+
//
|
|
9
|
+
// A file written through `fs` is, at once, the file `exec` sees under the
|
|
10
|
+
// same name — and the reverse.
|
|
11
|
+
//
|
|
12
|
+
// On `zen run` that is free: a podman container binds the host directory the
|
|
13
|
+
// disk driver reads. A sandbox service keeps the files with the container, so
|
|
14
|
+
// it serves `fs` itself.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/** The image a project gets when it does not name one. */
|
|
17
|
+
export const DEFAULT_SANDBOX_IMAGE = 'docker.io/library/python:3.14-slim-bookworm';
|
|
18
|
+
/** A failure the caller can do something about — a missing engine, usually. */
|
|
19
|
+
export class SandboxError extends Error {
|
|
20
|
+
hint;
|
|
21
|
+
constructor(message, hint) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'SandboxError';
|
|
24
|
+
this.hint = hint;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=contract.js.map
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type PlatformPath } from 'node:path';
|
|
2
|
+
export interface FsStat {
|
|
3
|
+
size: number;
|
|
4
|
+
isFile(): boolean;
|
|
5
|
+
isDirectory(): boolean;
|
|
6
|
+
isSymbolicLink(): boolean;
|
|
7
|
+
}
|
|
8
|
+
/** One name in a listing: what `stat` says of it, less the size. */
|
|
9
|
+
export type FsEntry = Omit<FsStat, 'size'> & {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
export interface FsDriver {
|
|
13
|
+
/** how this driver's paths are spelled: the host's for `localFs`, posix otherwise */
|
|
14
|
+
readonly path: PlatformPath;
|
|
15
|
+
/**
|
|
16
|
+
* The path with every symlink resolved; throws `ENOENT` when it does not
|
|
17
|
+
* exist. Synchronous because containment is decided synchronously. A
|
|
18
|
+
* driver whose files live elsewhere may answer lexically here, provided
|
|
19
|
+
* the far side refuses an escape on its own.
|
|
20
|
+
*/
|
|
21
|
+
realpathSync(path: string): string;
|
|
22
|
+
stat(path: string): Promise<FsStat>;
|
|
23
|
+
readdir(path: string): Promise<FsEntry[]>;
|
|
24
|
+
readFile(path: string): Promise<Buffer>;
|
|
25
|
+
/** at most the first `bytes` bytes, for sniffing a file too large to read whole */
|
|
26
|
+
readHead(path: string, bytes: number): Promise<Buffer>;
|
|
27
|
+
writeFile(path: string, data: string | Uint8Array): Promise<void>;
|
|
28
|
+
/** with parents, and no complaint when it already exists */
|
|
29
|
+
mkdirp(path: string): Promise<void>;
|
|
30
|
+
rm(path: string, opts?: {
|
|
31
|
+
recursive?: boolean;
|
|
32
|
+
force?: boolean;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
rename(from: string, to: string): Promise<void>;
|
|
35
|
+
copy(from: string, to: string, opts?: {
|
|
36
|
+
recursive?: boolean;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/** This machine's disk, through `node:fs`. */
|
|
40
|
+
export declare const localFs: FsDriver;
|
|
41
|
+
/**
|
|
42
|
+
* A failure shaped like Node's, for drivers that are not `node:fs`: the file
|
|
43
|
+
* tools read `code` and `path` to say what went wrong in the model's words.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fsError(code: string, syscall: string, at: string): NodeJS.ErrnoException;
|
|
46
|
+
//# sourceMappingURL=fs.d.ts.map
|
package/dist/fs.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { cp, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path, {} from 'node:path';
|
|
4
|
+
/** This machine's disk, through `node:fs`. */
|
|
5
|
+
export const localFs = {
|
|
6
|
+
path,
|
|
7
|
+
realpathSync: (p) => realpathSync(p),
|
|
8
|
+
stat: (p) => stat(p),
|
|
9
|
+
readdir: (p) => readdir(p, { withFileTypes: true }),
|
|
10
|
+
readFile: (p) => readFile(p),
|
|
11
|
+
async readHead(p, bytes) {
|
|
12
|
+
const handle = await open(p, 'r');
|
|
13
|
+
try {
|
|
14
|
+
const buf = Buffer.alloc(bytes);
|
|
15
|
+
const { bytesRead } = await handle.read(buf, 0, bytes, 0);
|
|
16
|
+
return buf.subarray(0, bytesRead);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await handle.close();
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
writeFile: (p, data) => writeFile(p, data),
|
|
23
|
+
mkdirp: async (p) => void (await mkdir(p, { recursive: true })),
|
|
24
|
+
rm: (p, opts) => rm(p, opts),
|
|
25
|
+
rename: (from, to) => rename(from, to),
|
|
26
|
+
copy: (from, to, opts) => cp(from, to, { recursive: opts?.recursive === true }),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A failure shaped like Node's, for drivers that are not `node:fs`: the file
|
|
30
|
+
* tools read `code` and `path` to say what went wrong in the model's words.
|
|
31
|
+
*/
|
|
32
|
+
export function fsError(code, syscall, at) {
|
|
33
|
+
const err = new Error(`${code}: ${syscall} '${at}'`);
|
|
34
|
+
err.code = code;
|
|
35
|
+
err.syscall = syscall;
|
|
36
|
+
err.path = at;
|
|
37
|
+
return err;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=fs.js.map
|
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { SandboxError, type BuildSpec, type PrepareOptions } from './contract.ts';
|
|
2
|
+
import { type Runner } from './process.ts';
|
|
3
|
+
/** Where locally built images are tagged. Not scoped: a `/` or an `@` is not a legal tag. */
|
|
4
|
+
export declare const LOCAL_IMAGE_REPO = "localhost/zenera-sandbox";
|
|
5
|
+
/**
|
|
6
|
+
* A build context is meant to be small — a Dockerfile and whatever it copies.
|
|
7
|
+
* Hashing a directory someone pointed at their whole home folder would hang
|
|
8
|
+
* before it was wrong, so it stops and says which key to narrow.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_CONTEXT_FILES = 2000;
|
|
11
|
+
/** What a build is made of: the address every backend agrees on. */
|
|
12
|
+
export interface BuildIdentity {
|
|
13
|
+
/** 12 hex characters; the tag a build is known by */
|
|
14
|
+
hash: string;
|
|
15
|
+
/** every file in the context, relative and sorted — exactly what was hashed */
|
|
16
|
+
files: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The content address of a build: the Dockerfile, then every file the build can
|
|
20
|
+
* see, by path and by content, then its arguments.
|
|
21
|
+
*
|
|
22
|
+
* `.dockerignore` is not read. The engine honours it and this does not, so an
|
|
23
|
+
* ignored file that changes yields a new tag and a build that produces the same
|
|
24
|
+
* image — wasteful, never wrong, and the alternative is reimplementing a match
|
|
25
|
+
* syntax whose disagreements would be silent.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildIdentity(spec: BuildSpec): BuildIdentity;
|
|
28
|
+
/** The tag a build is run under on this machine. */
|
|
29
|
+
export declare function localImageTag(build: BuildSpec | BuildIdentity): string;
|
|
30
|
+
/**
|
|
31
|
+
* A build that ran and failed, rather than an engine that could not be asked.
|
|
32
|
+
* Both stop a run, but only one of them is the project's fault: `zen check`
|
|
33
|
+
* reports a Dockerfile that does not build as an error, and a laptop without
|
|
34
|
+
* podman as a warning.
|
|
35
|
+
*/
|
|
36
|
+
export declare class ImageBuildError extends SandboxError {
|
|
37
|
+
constructor(message: string, hint?: string);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether `err` is a failed build, by name rather than by class: a plugin
|
|
41
|
+
* that provides the sandbox may bring its own copy of this package, and its
|
|
42
|
+
* `ImageBuildError` is then a different class that means the same thing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isImageBuildError(err: unknown): err is ImageBuildError;
|
|
45
|
+
/**
|
|
46
|
+
* Puts the image on this machine's engine: nothing when it is already there,
|
|
47
|
+
* a build when the spec names a Dockerfile, a pull otherwise.
|
|
48
|
+
*
|
|
49
|
+
* Skipping a present build is safe in a way it would not be for an ordinary
|
|
50
|
+
* tag, because this one is a hash of the Dockerfile and its context: the image
|
|
51
|
+
* existing *means* the content is unchanged. What it does not cover is a moved
|
|
52
|
+
* base image — `podman build` reuses whatever `FROM node:24` resolved to last
|
|
53
|
+
* time — which is what `force` is for.
|
|
54
|
+
*/
|
|
55
|
+
export declare function ensurePodmanImage(target: {
|
|
56
|
+
image: string;
|
|
57
|
+
build?: BuildSpec;
|
|
58
|
+
}, host?: {
|
|
59
|
+
engine?: string;
|
|
60
|
+
exec?: Runner;
|
|
61
|
+
}, opts?: PrepareOptions): Promise<void>;
|
|
62
|
+
//# sourceMappingURL=image.d.ts.map
|