@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 +20 -0
- package/dist/base.js +151 -0
- package/dist/image.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +3 -2
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
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.
|
|
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"
|