@zenera/sandbox 0.0.0-stage → 1.1.56
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 +46 -2
- package/dist/contract.d.ts +194 -0
- package/dist/contract.js +27 -0
- package/dist/fs.d.ts +46 -0
- package/dist/fs.js +39 -0
- package/dist/in-memory-fs.d.ts +25 -0
- package/dist/in-memory-fs.js +165 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +11 -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 +76 -0
- package/dist/podman.js +555 -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 +112 -0
- package/package.json +58 -5
package/dist/mounts.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { FsDriver } from './fs.ts';
|
|
2
|
+
/** Where the workspace appears in the agent's namespace, and the default cwd. */
|
|
3
|
+
export declare const WORKSPACE_MOUNT = "/workspace";
|
|
4
|
+
/** Where a project's `assets/` directory appears. */
|
|
5
|
+
export declare const ASSETS_MOUNT = "/assets";
|
|
6
|
+
/** ...and where its skill catalog does, so a skill's scripts can be run. */
|
|
7
|
+
export declare const SKILLS_MOUNT = "/skills";
|
|
8
|
+
export type MountSource =
|
|
9
|
+
/** the project's reference material, at `/assets` */
|
|
10
|
+
{
|
|
11
|
+
kind: 'assets';
|
|
12
|
+
}
|
|
13
|
+
/** the skill catalog — one merged tree at `/skills`, however many folders it came from */
|
|
14
|
+
| {
|
|
15
|
+
kind: 'skills';
|
|
16
|
+
}
|
|
17
|
+
/** the container's private, persistent `$HOME` */
|
|
18
|
+
| {
|
|
19
|
+
kind: 'home';
|
|
20
|
+
}
|
|
21
|
+
/** one credential file, by the variable that names it */
|
|
22
|
+
| {
|
|
23
|
+
kind: 'credential';
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
export interface Mount {
|
|
27
|
+
/** absolute host path; must already exist */
|
|
28
|
+
host: string;
|
|
29
|
+
/** absolute path the agent names it by, e.g. `/assets` */
|
|
30
|
+
at: string;
|
|
31
|
+
/**
|
|
32
|
+
* Say it. Unset, the file tools treat an extra tree as reference material
|
|
33
|
+
* and refuse to write it, while the sandbox binds it writable — the container's
|
|
34
|
+
* `$HOME` is the one mount that relies on that.
|
|
35
|
+
*/
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
/** what the tree is, for a backend that has to supply it rather than bind `host` */
|
|
38
|
+
source?: MountSource;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The trees a file tool reaches, and the names it reaches them by: the
|
|
42
|
+
* workspace, the name it is also mounted at, and the extra trees beside it.
|
|
43
|
+
* What `Sandbox.files` translates into the sandbox's own paths, and what the
|
|
44
|
+
* file tools in `@zenera/neo` take.
|
|
45
|
+
*/
|
|
46
|
+
export interface FilesView {
|
|
47
|
+
root: string;
|
|
48
|
+
/** refuse every mutating tool; `zen run --read-only` */
|
|
49
|
+
readOnly?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* An absolute path the root is *also* known by, because something else has
|
|
52
|
+
* mounted it there — the sandbox mounts the workspace at `/workspace`, so
|
|
53
|
+
* `run_command` returns compiler errors, stack traces and `pwd` output
|
|
54
|
+
* naming files under a prefix the file tools would otherwise refuse. Set
|
|
55
|
+
* it and both spellings reach the same file.
|
|
56
|
+
*/
|
|
57
|
+
mount?: string;
|
|
58
|
+
/** further trees, each under its own name; read-only unless one says otherwise */
|
|
59
|
+
mounts?: readonly Mount[];
|
|
60
|
+
/**
|
|
61
|
+
* Where the bytes are; this machine's disk when unset. `root` and every
|
|
62
|
+
* mount's `host` are paths in this driver's namespace.
|
|
63
|
+
*/
|
|
64
|
+
fs?: FsDriver;
|
|
65
|
+
}
|
|
66
|
+
/** A mount stated by what it is: no host path, nothing to bind. */
|
|
67
|
+
export type LogicalMount = Required<Omit<Mount, 'host'>>;
|
|
68
|
+
/**
|
|
69
|
+
* The mounts a supplying backend is asked for. The skill catalog arrives here
|
|
70
|
+
* as one bind per skill folder — podman cannot merge directories — and leaves
|
|
71
|
+
* as the single `/skills` tree it is to the agent. A mount nobody labelled is a
|
|
72
|
+
* host directory with no meaning elsewhere, so it is refused rather than
|
|
73
|
+
* silently dropped.
|
|
74
|
+
*/
|
|
75
|
+
export declare function logicalMounts(mounts: readonly Mount[]): LogicalMount[];
|
|
76
|
+
/**
|
|
77
|
+
* The file tools' view for a sandbox that keeps its files with the container
|
|
78
|
+
* rather than reading host directories: the workspace is its mount point, and
|
|
79
|
+
* each tree is found where the container has it — so a path the model writes
|
|
80
|
+
* means one file to the tools and the shell. What a remote `Sandbox.files`
|
|
81
|
+
* returns; a podman one reads the host paths as they are.
|
|
82
|
+
*/
|
|
83
|
+
export declare function inSandboxPaths(opts: FilesView, fs: FsDriver): FilesView;
|
|
84
|
+
//# sourceMappingURL=mounts.d.ts.map
|
package/dist/mounts.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The trees an agent can reach, and what each one is
|
|
3
|
+
//
|
|
4
|
+
// One directory is one mount, and the host hands the same array to the file
|
|
5
|
+
// tools and to the sandbox so that both call it by the same name: a path a command
|
|
6
|
+
// prints is a path `read_file` accepts.
|
|
7
|
+
//
|
|
8
|
+
// On `zen run` every tree is a host directory bound into the container, and
|
|
9
|
+
// `host` is the whole story. A sandbox on another machine has no host path to
|
|
10
|
+
// bind: it has to be told what the tree is — the project's assets, its skills —
|
|
11
|
+
// and fetch that itself. So a mount carries both: `host` for a backend that
|
|
12
|
+
// binds, `source` for one that supplies. Memory is not a mount: its files stay
|
|
13
|
+
// with the graph and reach a sandbox only through `memory_copy_file`.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/** Where the workspace appears in the agent's namespace, and the default cwd. */
|
|
16
|
+
export const WORKSPACE_MOUNT = '/workspace';
|
|
17
|
+
/** Where a project's `assets/` directory appears. */
|
|
18
|
+
export const ASSETS_MOUNT = '/assets';
|
|
19
|
+
/** ...and where its skill catalog does, so a skill's scripts can be run. */
|
|
20
|
+
export const SKILLS_MOUNT = '/skills';
|
|
21
|
+
/**
|
|
22
|
+
* The mounts a supplying backend is asked for. The skill catalog arrives here
|
|
23
|
+
* as one bind per skill folder — podman cannot merge directories — and leaves
|
|
24
|
+
* as the single `/skills` tree it is to the agent. A mount nobody labelled is a
|
|
25
|
+
* host directory with no meaning elsewhere, so it is refused rather than
|
|
26
|
+
* silently dropped.
|
|
27
|
+
*/
|
|
28
|
+
export function logicalMounts(mounts) {
|
|
29
|
+
const out = [];
|
|
30
|
+
let skills = false;
|
|
31
|
+
for (const m of mounts) {
|
|
32
|
+
if (!m.source) {
|
|
33
|
+
throw new Error(`mount ${m.at} has no source, so only a local sandbox can provide it`);
|
|
34
|
+
}
|
|
35
|
+
if (m.source.kind === 'skills') {
|
|
36
|
+
if (!skills) {
|
|
37
|
+
out.push({ at: SKILLS_MOUNT, readOnly: true, source: m.source });
|
|
38
|
+
skills = true;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
out.push({ at: m.at, readOnly: m.readOnly ?? false, source: m.source });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The file tools' view for a sandbox that keeps its files with the container
|
|
48
|
+
* rather than reading host directories: the workspace is its mount point, and
|
|
49
|
+
* each tree is found where the container has it — so a path the model writes
|
|
50
|
+
* means one file to the tools and the shell. What a remote `Sandbox.files`
|
|
51
|
+
* returns; a podman one reads the host paths as they are.
|
|
52
|
+
*/
|
|
53
|
+
export function inSandboxPaths(opts, fs) {
|
|
54
|
+
const mount = opts.mount ?? WORKSPACE_MOUNT;
|
|
55
|
+
return {
|
|
56
|
+
...opts,
|
|
57
|
+
root: mount,
|
|
58
|
+
mount,
|
|
59
|
+
mounts: logicalMounts(opts.mounts ?? []).map((m) => ({ ...m, host: m.at })),
|
|
60
|
+
fs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=mounts.js.map
|
package/dist/podman.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type FsDriver } from './fs.ts';
|
|
2
|
+
import { type FilesView, type Mount } from './mounts.ts';
|
|
3
|
+
import { type Runner } from './process.ts';
|
|
4
|
+
import { type Sandbox, type SandboxFactory, type ExecOptions, type ExecResult, type JobOutput, type JobStopped, type SandboxOptions, type SandboxSpec } from './contract.ts';
|
|
5
|
+
/** What only a podman sandbox needs: which binary to drive, and how to spawn it. */
|
|
6
|
+
export interface PodmanHost {
|
|
7
|
+
/** the container engine binary; `podman` when unset */
|
|
8
|
+
engine?: string;
|
|
9
|
+
/** how processes are spawned — the seam tests replace */
|
|
10
|
+
exec?: Runner;
|
|
11
|
+
}
|
|
12
|
+
interface Resolved extends Required<Omit<SandboxSpec, 'user'>> {
|
|
13
|
+
user?: string;
|
|
14
|
+
root: string;
|
|
15
|
+
key: string;
|
|
16
|
+
readOnly: boolean;
|
|
17
|
+
mounts: readonly Mount[];
|
|
18
|
+
execEnv: Record<string, string>;
|
|
19
|
+
execPaths: Record<string, readonly string[]>;
|
|
20
|
+
explicitCpus?: boolean;
|
|
21
|
+
explicitMemory?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The container, started when it is first needed and not before. A project
|
|
25
|
+
* whose agents never shell out never pays for an image pull, and a session
|
|
26
|
+
* that only ever asks a question never leaves anything behind.
|
|
27
|
+
*/
|
|
28
|
+
export declare class PodmanSandbox implements Sandbox {
|
|
29
|
+
#private;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
/** the container binds the host directories, so the disk is where the files are */
|
|
32
|
+
readonly fs: FsDriver;
|
|
33
|
+
readonly spec: Resolved;
|
|
34
|
+
readonly engine: string;
|
|
35
|
+
constructor(opts: SandboxOptions, host?: PodmanHost);
|
|
36
|
+
/** Whether anything was actually started — `close` is free when not. */
|
|
37
|
+
get started(): boolean;
|
|
38
|
+
/** The host paths, as they are: the container binds exactly these. */
|
|
39
|
+
files(opts: FilesView): FilesView;
|
|
40
|
+
/**
|
|
41
|
+
* Idempotent, and single-flight: several tool calls in one batch all await
|
|
42
|
+
* the same start rather than racing to create the same container.
|
|
43
|
+
*/
|
|
44
|
+
start(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Runs one command and waits for it. The script goes down stdin, so no part
|
|
47
|
+
* of it is ever parsed by a shell on this side of the container.
|
|
48
|
+
*/
|
|
49
|
+
exec(command: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Starts a command and returns. The wrapper writes the log and the exit
|
|
52
|
+
* code to files the container owns, which is what makes the job readable
|
|
53
|
+
* after the `exec` that started it has long returned.
|
|
54
|
+
*/
|
|
55
|
+
startJob(command: string, opts?: ExecOptions): Promise<string>;
|
|
56
|
+
/** A window of a job's log, plus whether it is still going. */
|
|
57
|
+
readJob(id: string, from?: number): Promise<JobOutput>;
|
|
58
|
+
/** Signals a job's whole process group, so its children go too. */
|
|
59
|
+
stopJob(id: string): Promise<JobStopped>;
|
|
60
|
+
/**
|
|
61
|
+
* Stops a persisted container and removes an ephemeral one. Stopping
|
|
62
|
+
* rather than removing is what makes `persist` mean anything: a stopped
|
|
63
|
+
* container costs disk, a running one costs the machine's whole budget.
|
|
64
|
+
*/
|
|
65
|
+
close(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* A path inside the container, contained to the working directory. Purely
|
|
68
|
+
* lexical, and correctly so: this is the container's path space, not this
|
|
69
|
+
* machine's, and there is nothing here to resolve a symlink against.
|
|
70
|
+
*/
|
|
71
|
+
inside(path?: string): string;
|
|
72
|
+
}
|
|
73
|
+
/** Podman on this machine: what `zen run` uses, and the engine's default. */
|
|
74
|
+
export declare const podmanSandbox: SandboxFactory;
|
|
75
|
+
export {};
|
|
76
|
+
//# sourceMappingURL=podman.d.ts.map
|