@zenera/sandbox 1.1.56 → 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/README.md CHANGED
@@ -17,7 +17,8 @@ await sandbox.close();
17
17
 
18
18
  ## The contract
19
19
 
20
- - **`Sandbox`** - `exec`, background jobs (`startJob`, `readJob`, `stopJob`),
20
+ - **`Sandbox`** - `prepare` (pull the image, or build it from `build:`),
21
+ `exec`, background jobs (`startJob`, `readJob`, `stopJob`),
21
22
  `close` at the end of a turn, and the files: `fs` (an `FsDriver`) and
22
23
  `files(view)`, which maps the host's workspace and mounts to the paths `fs`
23
24
  finds them at. The invariant: **a file written through `fs` is the file
@@ -27,10 +28,23 @@ await sandbox.close();
27
28
  - **`FsDriver`** - the filesystem seam, usable without a sandbox. `localFs` is
28
29
  this machine's disk; `InMemoryFs` keeps a tree in memory.
29
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
+
30
43
  ## Implementations
31
44
 
32
45
  - **`PodmanSandbox`** / `podmanSandbox` - a podman container on this machine,
33
- binding host directories, with `fs = localFs`.
46
+ binding host directories, with `fs = localFs`. `ensurePodmanImage` is its
47
+ image step on its own.
34
48
  - A sandbox that keeps its files with the container (a sandbox service) serves
35
49
  `fs` itself and returns `inSandboxPaths(view, fs)` from `files()`.
36
50
 
@@ -10,6 +10,12 @@ export declare const DEFAULT_SANDBOX_IMAGE = "docker.io/library/python:3.14-slim
10
10
  export interface SandboxSpec {
11
11
  /** the base image commands run in */
12
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;
13
19
  /** fractional cores, as podman's `--cpus` */
14
20
  cpus?: number;
15
21
  /** MiB, as podman's `--memory` */
@@ -40,6 +46,29 @@ export interface SandboxSpec {
40
46
  secrets?: readonly string[];
41
47
  }
42
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
+ }
43
72
  /**
44
73
  * Two postures, because they cannot both be the default.
45
74
  *
@@ -114,6 +143,13 @@ export interface Sandbox {
114
143
  files(opts: FilesView): FilesView;
115
144
  /** whether anything was actually started — `close` is free when not */
116
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>;
117
153
  /** idempotent and single-flight; every other call starts the sandbox itself */
118
154
  start(): Promise<void>;
119
155
  exec(command: string, opts?: ExecOptions): Promise<ExecResult>;
@@ -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
package/dist/image.js ADDED
@@ -0,0 +1,210 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { join, relative, sep } from 'node:path';
4
+ import { SandboxError } from "./contract.js";
5
+ import { runProcess } from "./process.js";
6
+ // ---------------------------------------------------------------------------
7
+ // The sandbox image
8
+ //
9
+ // `image:` names something to pull; `build:` names a Dockerfile to build. Both
10
+ // end as an image a sandbox runs, and getting it there is the sandbox's job:
11
+ // podman builds on this machine, a sandbox service builds wherever it builds.
12
+ // What they share is the *identity* of a build — which files go in and what
13
+ // they hash to — so a project names the same image on every backend.
14
+ //
15
+ // The tag is a function of what goes into the image, and it has to be: a
16
+ // container's name hashes its image, so a tag that stayed put while the
17
+ // Dockerfile changed would leave a `persist: true` container running last
18
+ // week's filesystem, with nothing anywhere saying so. Hashing the context as
19
+ // well as the Dockerfile means an edit to either yields a new tag, a new
20
+ // container name, and a build — which is the only honest answer.
21
+ // ---------------------------------------------------------------------------
22
+ /** Where locally built images are tagged. Not scoped: a `/` or an `@` is not a legal tag. */
23
+ export const LOCAL_IMAGE_REPO = 'localhost/zenera-sandbox';
24
+ /**
25
+ * A build context is meant to be small — a Dockerfile and whatever it copies.
26
+ * Hashing a directory someone pointed at their whole home folder would hang
27
+ * before it was wrong, so it stops and says which key to narrow.
28
+ */
29
+ export const MAX_CONTEXT_FILES = 2_000;
30
+ /**
31
+ * The content address of a build: the Dockerfile, then every file the build can
32
+ * see, by path and by content, then its arguments.
33
+ *
34
+ * `.dockerignore` is not read. The engine honours it and this does not, so an
35
+ * ignored file that changes yields a new tag and a build that produces the same
36
+ * image — wasteful, never wrong, and the alternative is reimplementing a match
37
+ * syntax whose disagreements would be silent.
38
+ */
39
+ export function buildIdentity(spec) {
40
+ const hash = createHash('sha256');
41
+ hash.update(readFileSync(spec.dockerfile));
42
+ const files = walk(spec.context);
43
+ for (const rel of files) {
44
+ // The separator is hashed as posix so the same tree tags the same on
45
+ // any host.
46
+ hash.update(`\0${rel.split(sep).join('/')}\0`);
47
+ hash.update(readFileSync(join(spec.context, rel)));
48
+ }
49
+ // Only when there are any, so a build without arguments keeps the tag it
50
+ // had before arguments existed.
51
+ const args = Object.entries(spec.args ?? {}).sort(([a], [b]) => a.localeCompare(b));
52
+ if (args.length > 0) {
53
+ hash.update(`\0args\0${JSON.stringify(args)}`);
54
+ }
55
+ return { hash: hash.digest('hex').slice(0, 12), files };
56
+ }
57
+ /** The tag a build is run under on this machine. */
58
+ export function localImageTag(build) {
59
+ const id = 'hash' in build ? build : buildIdentity(build);
60
+ return `${LOCAL_IMAGE_REPO}:${id.hash}`;
61
+ }
62
+ /** Every file under `dir`, relative and sorted, so the digest is stable. */
63
+ function walk(dir) {
64
+ const found = [];
65
+ const pending = [dir];
66
+ while (pending.length > 0) {
67
+ const at = pending.pop();
68
+ for (const entry of readdirSync(at, { withFileTypes: true })) {
69
+ const path = join(at, entry.name);
70
+ if (entry.isDirectory()) {
71
+ pending.push(path);
72
+ }
73
+ else if (entry.isFile()) {
74
+ found.push(relative(dir, path));
75
+ }
76
+ }
77
+ if (found.length > MAX_CONTEXT_FILES) {
78
+ throw new SandboxError(`sandbox.build.context: ${dir} holds more than ${MAX_CONTEXT_FILES} files`, 'point `context:` at the directory the build actually copies from');
79
+ }
80
+ }
81
+ return found.sort();
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // On this machine
85
+ // ---------------------------------------------------------------------------
86
+ /**
87
+ * A build that ran and failed, rather than an engine that could not be asked.
88
+ * Both stop a run, but only one of them is the project's fault: `zen check`
89
+ * reports a Dockerfile that does not build as an error, and a laptop without
90
+ * podman as a warning.
91
+ */
92
+ export class ImageBuildError extends SandboxError {
93
+ constructor(message, hint) {
94
+ super(message, hint);
95
+ this.name = 'ImageBuildError';
96
+ }
97
+ }
98
+ /**
99
+ * Whether `err` is a failed build, by name rather than by class: a plugin
100
+ * that provides the sandbox may bring its own copy of this package, and its
101
+ * `ImageBuildError` is then a different class that means the same thing.
102
+ */
103
+ export function isImageBuildError(err) {
104
+ return err instanceof Error && err.name === 'ImageBuildError';
105
+ }
106
+ /** Pulling is slow on purpose... */
107
+ const PULL_MS = 600_000;
108
+ /** ...and a build from a cold cache is slower still. */
109
+ const BUILD_MS = 900_000;
110
+ /**
111
+ * Images this process has already settled, per runner and per engine. A batch
112
+ * opens the same project dozens of times at once, and without this each one
113
+ * asks podman and the concurrent copies race each other to pull or build the
114
+ * very same image. Keyed by runner too, so a test's fake engine never inherits
115
+ * an answer the real one gave.
116
+ */
117
+ const settled = new WeakMap();
118
+ /**
119
+ * Puts the image on this machine's engine: nothing when it is already there,
120
+ * a build when the spec names a Dockerfile, a pull otherwise.
121
+ *
122
+ * Skipping a present build is safe in a way it would not be for an ordinary
123
+ * tag, because this one is a hash of the Dockerfile and its context: the image
124
+ * existing *means* the content is unchanged. What it does not cover is a moved
125
+ * base image — `podman build` reuses whatever `FROM node:24` resolved to last
126
+ * time — which is what `force` is for.
127
+ */
128
+ export function ensurePodmanImage(target, host = {}, opts = {}) {
129
+ const engine = host.engine ?? 'podman';
130
+ const run = host.exec ?? runProcess;
131
+ let byKey = settled.get(run);
132
+ if (!byKey) {
133
+ byKey = new Map();
134
+ settled.set(run, byKey);
135
+ }
136
+ const key = `${engine}::${target.image}`;
137
+ const pending = byKey.get(key);
138
+ if (pending && !opts.force) {
139
+ return pending;
140
+ }
141
+ const attempt = obtain(engine, run, target, opts).catch((err) => {
142
+ // A failure is not a settled answer: the user may fix the Dockerfile
143
+ // and try again in the same process.
144
+ byKey.delete(key);
145
+ throw err;
146
+ });
147
+ byKey.set(key, attempt);
148
+ return attempt;
149
+ }
150
+ async function obtain(engine, run, target, opts) {
151
+ const say = (line) => opts.onProgress?.(line);
152
+ const slow = (label, work) => opts.slow ? opts.slow(label, work) : work;
153
+ const tail = (res) => {
154
+ for (const line of res.stderr.split('\n').slice(-3)) {
155
+ if (line.trim()) {
156
+ say(` ${line.trim()}`);
157
+ }
158
+ }
159
+ };
160
+ if (!opts.force) {
161
+ const present = await run(engine, ['image', 'exists', target.image], {
162
+ timeoutMs: 30_000,
163
+ });
164
+ if (present.code === 0) {
165
+ return;
166
+ }
167
+ }
168
+ const build = target.build;
169
+ if (build) {
170
+ if (opts.allowBuild === false) {
171
+ throw new SandboxError(`the sandbox image ${target.image} is not built`, 'build it before running: `zen sandbox up` builds it on this machine');
172
+ }
173
+ say(`${opts.force ? 'rebuilding' : 'building'} the sandbox image from ${build.dockerfile}`);
174
+ say(' the agent runs its commands inside a container, so the image is needed first');
175
+ say(' this takes a few minutes; later runs reuse it');
176
+ const args = Object.entries(build.args ?? {}).flatMap(([k, v]) => [
177
+ '--build-arg',
178
+ `${k}=${v}`,
179
+ ]);
180
+ const built = await slow('building the sandbox image', run(engine, [
181
+ 'build',
182
+ '--tag',
183
+ target.image,
184
+ '--file',
185
+ build.dockerfile,
186
+ ...args,
187
+ build.context,
188
+ ], { timeoutMs: BUILD_MS, signal: opts.signal }));
189
+ tail(built);
190
+ if (built.code !== 0) {
191
+ throw new ImageBuildError(`could not build ${build.dockerfile}`, last(built) || 'run the build by hand to see what the engine says');
192
+ }
193
+ return;
194
+ }
195
+ say(`pulling ${target.image} — this happens once`);
196
+ const pulled = await slow('pulling', run(engine, ['pull', target.image], { timeoutMs: PULL_MS, signal: opts.signal }));
197
+ tail(pulled);
198
+ if (pulled.code !== 0) {
199
+ throw new SandboxError(`could not pull ${target.image}`, first(pulled) || 'check the image name and your network');
200
+ }
201
+ }
202
+ function first(res) {
203
+ return (res.stderr.trim() || res.stdout.trim()).split('\n')[0] ?? '';
204
+ }
205
+ /** A build says what went wrong on its last line, not its first. */
206
+ function last(res) {
207
+ const lines = (res.stderr.trim() || res.stdout.trim()).split('\n').filter((l) => l.trim());
208
+ return lines.slice(-2).join(' — ');
209
+ }
210
+ //# sourceMappingURL=image.js.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './contract.ts';
2
2
  export * from './fs.ts';
3
+ export * from './image.ts';
3
4
  export * from './in-memory-fs.ts';
4
5
  export * from './limits.ts';
5
6
  export * from './mounts.ts';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // can all stand on the same contract. Contract tests: `@zenera/sandbox/testing`.
4
4
  export * from "./contract.js";
5
5
  export * from "./fs.js";
6
+ export * from "./image.js";
6
7
  export * from "./in-memory-fs.js";
7
8
  export * from "./limits.js";
8
9
  export * from "./mounts.js";
package/dist/podman.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type FsDriver } from './fs.ts';
2
2
  import { type FilesView, type Mount } from './mounts.ts';
3
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';
4
+ import { type BuildSpec, type PrepareOptions, type Sandbox, type SandboxFactory, type ExecOptions, type ExecResult, type JobOutput, type JobStopped, type SandboxOptions, type SandboxSpec } from './contract.ts';
5
5
  /** What only a podman sandbox needs: which binary to drive, and how to spawn it. */
6
6
  export interface PodmanHost {
7
7
  /** the container engine binary; `podman` when unset */
@@ -9,8 +9,10 @@ export interface PodmanHost {
9
9
  /** how processes are spawned — the seam tests replace */
10
10
  exec?: Runner;
11
11
  }
12
- interface Resolved extends Required<Omit<SandboxSpec, 'user'>> {
12
+ interface Resolved extends Required<Omit<SandboxSpec, 'user' | 'build'>> {
13
13
  user?: string;
14
+ /** the Dockerfile `image` is built from, when it is */
15
+ build?: BuildSpec;
14
16
  root: string;
15
17
  key: string;
16
18
  readOnly: boolean;
@@ -37,6 +39,8 @@ export declare class PodmanSandbox implements Sandbox {
37
39
  get started(): boolean;
38
40
  /** The host paths, as they are: the container binds exactly these. */
39
41
  files(opts: FilesView): FilesView;
42
+ /** Pulls the image, or builds it from the Dockerfile, unless the engine already has it. */
43
+ prepare(opts?: PrepareOptions): Promise<void>;
40
44
  /**
41
45
  * Idempotent, and single-flight: several tool calls in one batch all await
42
46
  * the same start rather than racing to create the same container.
package/dist/podman.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { posix } from 'node:path';
3
+ import { ensurePodmanImage, localImageTag } from "./image.js";
3
4
  import { localFs } from "./fs.js";
4
5
  import { sandboxLimits } from "./limits.js";
5
6
  import { WORKSPACE_MOUNT } from "./mounts.js";
@@ -51,15 +52,18 @@ export class PodmanSandbox {
51
52
  spec;
52
53
  engine;
53
54
  #run;
55
+ #spawn;
54
56
  #jobs = new Map();
55
57
  #ready;
58
+ #prepared;
56
59
  #created = false;
57
60
  /** kills counted in this container's cgroup as of the last probe */
58
61
  #kills;
59
62
  constructor(opts, host = {}) {
60
63
  this.spec = resolveSpec(opts);
61
64
  this.engine = host.engine ?? 'podman';
62
- this.#run = resilient(host.exec ?? runProcess);
65
+ this.#spawn = host.exec ?? runProcess;
66
+ this.#run = resilient(this.#spawn);
63
67
  this.name = containerName(this.spec);
64
68
  }
65
69
  /** Whether anything was actually started — `close` is free when not. */
@@ -70,6 +74,17 @@ export class PodmanSandbox {
70
74
  files(opts) {
71
75
  return { ...opts, fs: this.fs };
72
76
  }
77
+ /** Pulls the image, or builds it from the Dockerfile, unless the engine already has it. */
78
+ prepare(opts = {}) {
79
+ if (this.#prepared && !opts.force) {
80
+ return this.#prepared;
81
+ }
82
+ this.#prepared = ensurePodmanImage({ image: this.spec.image, build: this.spec.build }, { engine: this.engine, exec: this.#spawn }, opts).catch((err) => {
83
+ this.#prepared = undefined;
84
+ throw err;
85
+ });
86
+ return this.#prepared;
87
+ }
73
88
  /**
74
89
  * Idempotent, and single-flight: several tool calls in one batch all await
75
90
  * the same start rather than racing to create the same container.
@@ -99,6 +114,10 @@ export class PodmanSandbox {
99
114
  this.#created = true;
100
115
  return;
101
116
  }
117
+ // `run` pulls a registry image by itself; a built one has to exist first.
118
+ if (this.spec.build) {
119
+ await this.prepare();
120
+ }
102
121
  const made = await this.#podman(this.#createArgs());
103
122
  if (made.code !== 0) {
104
123
  throw new SandboxError(`could not create container ${this.name}: ${message(made)}`, LOCKS.test(made.stderr) ? locksHint(this.engine) : undefined);
@@ -489,7 +508,8 @@ function resolveSpec(opts) {
489
508
  return {
490
509
  root: opts.root,
491
510
  key: opts.key,
492
- image: opts.image ?? DEFAULT_SANDBOX_IMAGE,
511
+ image: opts.build ? localImageTag(opts.build) : (opts.image ?? DEFAULT_SANDBOX_IMAGE),
512
+ build: opts.build,
493
513
  cpus: limits.cpus,
494
514
  memory: limits.memory,
495
515
  explicitCpus: opts.cpus !== undefined,
package/dist/testing.js CHANGED
@@ -102,6 +102,12 @@ export function describeSandbox(name, make, opts = {}) {
102
102
  expect(await sandbox.stopJob(id)).toMatchObject({ job_id: id, stopped: true });
103
103
  await expect(sandbox.readJob('job-unknown')).rejects.toBeInstanceOf(SandboxError);
104
104
  }), timeout);
105
+ it('prepares its image without starting, once however often it is asked', withSandbox(async ({ sandbox }) => {
106
+ await Promise.all([sandbox.prepare(), sandbox.prepare()]);
107
+ await sandbox.prepare();
108
+ expect(sandbox.started).toBe(false);
109
+ expect((await sandbox.exec('echo ready')).stdout).toBe('ready\n');
110
+ }), timeout);
105
111
  it('is free to close, and closes once', withSandbox(async ({ sandbox }) => {
106
112
  await sandbox.exec('true');
107
113
  await sandbox.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/sandbox",
3
- "version": "1.1.56",
3
+ "version": "1.1.57",
4
4
  "description": "Where an agent's commands run and the files they see: the Sandbox contract, a podman implementation, filesystem drivers and a contract test suite for other implementations.",
5
5
  "keywords": [
6
6
  "sandbox",