@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.
@@ -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
@@ -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