@zenera/sandbox 1.2.5 → 1.2.6

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/dist/base.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { PrepareOptions } from './contract.ts';
2
+ import type { Runner } from './process.ts';
3
+ export declare const SANDBOX_BASE_REPO = "ghcr.io/zenera-ai/zenera-sandbox";
4
+ /**
5
+ * The base image's Dockerfile, for `--build-arg ZEN_VERSION=<a published version>`.
6
+ * packages/sandbox/sandbox-base.Dockerfile is this, kept in step by a test.
7
+ */
8
+ export declare function sandboxBaseDockerfile(): string;
9
+ /** The base images a Dockerfile starts FROM, as `{ image, version }`. */
10
+ export declare function sandboxBasesOf(dockerfile: string): {
11
+ image: string;
12
+ version: string;
13
+ }[];
14
+ /**
15
+ * Puts every zenera base image the Dockerfile names on the engine: present, pulled, or — when
16
+ * the pull fails — built here and tagged as that name, so the project's own build finds it.
17
+ * A base that cannot be built either is left to the project's build to report.
18
+ */
19
+ export declare function ensureSandboxBases(engine: string, run: Runner, dockerfilePath: string, opts?: PrepareOptions): Promise<void>;
20
+ //# sourceMappingURL=base.d.ts.map
package/dist/base.js ADDED
@@ -0,0 +1,151 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ // ---------------------------------------------------------------------------
5
+ // The sandbox base image
6
+ //
7
+ // What every project's sandbox/Dockerfile used to install itself — Python,
8
+ // Node, the third-party packages zen's tools need, and @zenera/cli and
9
+ // @zenera/rag at the version that wrote the file — built once per release and
10
+ // published as ghcr.io/zenera-ai/zenera-sandbox:<version> (CI, RELEASE.md).
11
+ // A project's Dockerfile starts `FROM` it and adds only its own needs, so a
12
+ // new project pulls a few layers instead of running npm for minutes.
13
+ //
14
+ // When the image cannot be pulled (offline, air-gapped, not public yet, a
15
+ // version not released), the build makes it here from the same Dockerfile and
16
+ // tags it as the name the project asked for — slower, never broken.
17
+ // ---------------------------------------------------------------------------
18
+ export const SANDBOX_BASE_REPO = 'ghcr.io/zenera-ai/zenera-sandbox';
19
+ /** `FROM [--platform=…] ghcr.io/zenera-ai/zenera-sandbox:<version>`: the version is the tag. */
20
+ const BASE_FROM = /^\s*FROM\s+(?:--platform=\S+\s+)?ghcr\.io\/zenera-ai\/zenera-sandbox:([^\s@]+)/gim;
21
+ /**
22
+ * The base image's Dockerfile, for `--build-arg ZEN_VERSION=<a published version>`.
23
+ * packages/sandbox/sandbox-base.Dockerfile is this, kept in step by a test.
24
+ */
25
+ export function sandboxBaseDockerfile() {
26
+ return `# Generated from sandboxBaseDockerfile() in src/base.ts - edit that, then run
27
+ # \`npm run sandbox-base:dockerfile -w packages/sandbox\`. The base every zen project's
28
+ # sandbox/Dockerfile starts FROM: ${SANDBOX_BASE_REPO}:<version>.
29
+ #
30
+ # Python and Node both, because an agent asked to look at a repository does not
31
+ # get to choose which one it finds.
32
+
33
+ FROM docker.io/library/node:24-bookworm-slim AS node
34
+ FROM docker.io/library/python:3.14-slim-bookworm
35
+
36
+ # Copy Node.js runtime and global modules from official Node image
37
+ COPY --from=node /usr/local/bin/node /usr/local/bin/node
38
+ COPY --from=node /usr/local/include/node /usr/local/include/node
39
+ COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
40
+
41
+ # Symlink npm and npx executables
42
+ RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \\
43
+ && ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
44
+
45
+ # Keep the third-party dependencies of @zenera/{cli,engine,neo,rag,view} cached
46
+ # when the pinned CLI release changes. Keep in sync with their package.json files.
47
+ RUN npm install -g \\
48
+ "@anthropic-ai/sdk@^0.120.0" \\
49
+ "@apidevtools/swagger-parser@^12.0.0" \\
50
+ "@google/genai@^2.30.0" \\
51
+ "@inkjs/ui@^2.0.0" \\
52
+ "@lancedb/lancedb@^0.38.0" \\
53
+ "@openrouter/sdk@^1.4.25" \\
54
+ "@types/hast@^3.0.5" \\
55
+ "google-auth-library@^10.0.0" \\
56
+ "graphology@^0.26.0" \\
57
+ "ink@^7.1.1" \\
58
+ "lowlight@^3.3.0" \\
59
+ "mermaid@^12.2.0" \\
60
+ "openai@^6.0.0" \\
61
+ "react@^19.2.8" \\
62
+ "rehype-sanitize@^6.0.0" \\
63
+ "rehype-slug@^6.0.0" \\
64
+ "remark-frontmatter@^5.0.0" \\
65
+ "remark-gfm@^4.0.1" \\
66
+ "remark-parse@^11.0.0" \\
67
+ "remark-rehype@^11.1.2" \\
68
+ "unified@^11.0.5" \\
69
+ "yaml@^2.9.1" \\
70
+ "yauzl@^3.4.0" \\
71
+ "yazl@^3.3.1" \\
72
+ "zod@^4.6.7"
73
+
74
+ # HTTP clients agents' scripts commonly reach for. Global npm packages resolve for require()
75
+ # through NODE_PATH; ESM import does not read it (built-in fetch needs nothing).
76
+ RUN pip install --no-cache-dir "requests>=2.32,<3" \\
77
+ && npm install -g "axios@^1"
78
+ ENV NODE_PATH=/usr/local/lib/node_modules
79
+
80
+ # The zen of this release, so the tools in the container are the ones the session
81
+ # outside it was built by. Declared here: an ARG invalidates the cache of every RUN after it.
82
+ ARG ZEN_VERSION
83
+ RUN test -n "$ZEN_VERSION" \\
84
+ && npm install -g "@zenera/cli@$ZEN_VERSION" "@zenera/rag@$ZEN_VERSION"
85
+
86
+ LABEL zenera.sandbox-base=1 zenera.version=$ZEN_VERSION \\
87
+ org.opencontainers.image.source=https://github.com/zenera-ai/ZeneraNeo
88
+ `;
89
+ }
90
+ /** The base images a Dockerfile starts FROM, as `{ image, version }`. */
91
+ export function sandboxBasesOf(dockerfile) {
92
+ const found = new Map();
93
+ for (const match of dockerfile.matchAll(BASE_FROM)) {
94
+ const version = match[1];
95
+ found.set(`${SANDBOX_BASE_REPO}:${version}`, version);
96
+ }
97
+ return [...found].map(([image, version]) => ({ image, version }));
98
+ }
99
+ const PULL_MS = 600_000;
100
+ const BUILD_MS = 1_800_000;
101
+ /**
102
+ * Puts every zenera base image the Dockerfile names on the engine: present, pulled, or — when
103
+ * the pull fails — built here and tagged as that name, so the project's own build finds it.
104
+ * A base that cannot be built either is left to the project's build to report.
105
+ */
106
+ export async function ensureSandboxBases(engine, run, dockerfilePath, opts = {}) {
107
+ let text;
108
+ try {
109
+ text = readFileSync(dockerfilePath, 'utf8');
110
+ }
111
+ catch {
112
+ return;
113
+ }
114
+ const say = (line) => opts.onProgress?.(line);
115
+ for (const { image, version } of sandboxBasesOf(text)) {
116
+ const present = await run(engine, ['image', 'exists', image], { timeoutMs: 30_000 });
117
+ if (present.code === 0) {
118
+ continue;
119
+ }
120
+ say(`pulling the sandbox base ${image}`);
121
+ const pulled = await run(engine, ['pull', image], {
122
+ timeoutMs: PULL_MS,
123
+ signal: opts.signal,
124
+ });
125
+ if (pulled.code === 0) {
126
+ continue;
127
+ }
128
+ say(` could not pull it; building it here instead (several minutes, once)`);
129
+ const ctx = mkdtempSync(join(tmpdir(), 'zen-sandbox-base-'));
130
+ try {
131
+ writeFileSync(join(ctx, 'Dockerfile'), sandboxBaseDockerfile());
132
+ const built = await run(engine, [
133
+ 'build',
134
+ '--tag',
135
+ image,
136
+ '--build-arg',
137
+ `ZEN_VERSION=${version}`,
138
+ '--file',
139
+ join(ctx, 'Dockerfile'),
140
+ ctx,
141
+ ], { timeoutMs: BUILD_MS, signal: opts.signal });
142
+ if (built.code !== 0) {
143
+ say(` building the base failed too; the project's build will say why`);
144
+ }
145
+ }
146
+ finally {
147
+ rmSync(ctx, { recursive: true, force: true });
148
+ }
149
+ }
150
+ }
151
+ //# sourceMappingURL=base.js.map
package/dist/image.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFileSync, readdirSync } from 'node:fs';
3
3
  import { join, relative, sep } from 'node:path';
4
+ import { ensureSandboxBases } from "./base.js";
4
5
  import { SandboxError } from "./contract.js";
5
6
  import { runProcess } from "./process.js";
6
7
  // ---------------------------------------------------------------------------
@@ -173,6 +174,8 @@ async function obtain(engine, run, target, opts) {
173
174
  say(`${opts.force ? 'rebuilding' : 'building'} the sandbox image from ${build.dockerfile}`);
174
175
  say(' the agent runs its commands inside a container, so the image is needed first');
175
176
  say(' this takes a few minutes; later runs reuse it');
177
+ // The zenera base it starts FROM: pulled, or built here when it cannot be.
178
+ await ensureSandboxBases(engine, run, build.dockerfile, opts);
176
179
  const args = Object.entries(build.args ?? {}).flatMap(([k, v]) => [
177
180
  '--build-arg',
178
181
  `${k}=${v}`,
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './base.ts';
1
2
  export * from './bwrap.ts';
2
3
  export * from './contract.ts';
3
4
  export * from './fs.ts';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // The sandbox: where a session's commands run and the files they see. Depends
2
2
  // on Node alone, so a host, a sandbox service and the agent inside a container
3
3
  // can all stand on the same contract. Contract tests: `@zenera/sandbox/testing`.
4
+ export * from "./base.js";
4
5
  export * from "./bwrap.js";
5
6
  export * from "./contract.js";
6
7
  export * from "./fs.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/sandbox",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "Where an agent's commands run and the files they see: the Sandbox contract, podman and bubblewrap implementations, filesystem drivers and a contract test suite for other implementations.",
5
5
  "keywords": [
6
6
  "sandbox",
@@ -47,7 +47,8 @@
47
47
  "scripts": {
48
48
  "build": "tsc -b",
49
49
  "clean": "tsc -b --clean && rm -rf dist",
50
- "prepack": "tsc -b"
50
+ "prepack": "tsc -b",
51
+ "sandbox-base:dockerfile": "tsc -b && node -e \"import(\\\"./dist/base.js\\\").then((m) => process.stdout.write(m.sandboxBaseDockerfile()))\" > sandbox-base.Dockerfile"
51
52
  },
52
53
  "peerDependencies": {
53
54
  "vitest": ">=3"