@zenera/engine 1.1.58 → 1.2.0

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/engine.js CHANGED
@@ -97,12 +97,15 @@ export async function openEngine(opts) {
97
97
  provider: opts.newSandbox ? 'the host' : chosen?.provider.describe,
98
98
  ...(opts.disposable ? { persist: false } : {}),
99
99
  });
100
- const workspaceOptions = setup.sandbox.files({
101
- root: workspace,
102
- readOnly: opts.readOnly,
100
+ // The sandbox's own files, by the names its commands use: what
101
+ // `run_command` prints a path as, `read_file` accepts.
102
+ const workspaceOptions = {
103
+ fs: setup.sandbox.fs,
104
+ root: setup.workdir,
103
105
  mount: setup.workdir,
104
- mounts,
105
- });
106
+ mounts: mounts.map(({ at, readOnly, source }) => ({ at, readOnly, source })),
107
+ readOnly: opts.readOnly,
108
+ };
106
109
  // The same view the file tools have, so a path the agent just wrote
107
110
  // resolves the same way when it asks memory to keep a copy.
108
111
  files = new Workspace(workspaceOptions);
@@ -362,10 +365,11 @@ async function record(engine, input, result, startedAt, durationMs) {
362
365
  title: `${engine.name} · ${run.id}`,
363
366
  architecture: await engine.runner.describe(),
364
367
  memory: engine.runner.services.memory?.store,
365
- mounts: [
366
- { at: engine.workspaceMount, href: pathToFileURL(engine.files.root).href },
367
- ...engine.files.mounts.map((m) => ({ at: m.at, href: pathToFileURL(m.host).href })),
368
- ],
368
+ // Links to the trees on this machine — the ones the tools reach.
369
+ mounts: mounts(engine)
370
+ .filter((m) => m.at === engine.workspaceMount ||
371
+ engine.files.mounts.some((f) => f.at === m.at))
372
+ .map((m) => ({ at: m.at, href: pathToFileURL(m.host).href })),
369
373
  });
370
374
  await writeFile(run.report, renderReportHtml(built), 'utf8');
371
375
  report = run.report;
package/dist/keys.d.ts CHANGED
@@ -27,6 +27,8 @@ export declare const MODES: readonly KeyMode[];
27
27
  /** Anything the keyring can hold a credential for. */
28
28
  export declare const OWNERS: readonly ["openai", "anthropic", "google", "vertex", "openrouter", "exa", "bifrost"];
29
29
  export type KeyOwner = Provider | Service | GatewayOwner;
30
+ /** Non-secret provider settings that accompany credentials into a runtime or sandbox. */
31
+ export declare const MODEL_CONTEXT_ENV: readonly ["GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_LOCATION", "GOOGLE_CLOUD_QUOTA_PROJECT", "BIFROST_BASE_URL"];
30
32
  /**
31
33
  * One way a credential can arrive.
32
34
  *
package/dist/keys.js CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Host keyring and ambient credential discovery used by the engine and CLI.
3
+ * KeyStore materializes active entries, credentials lists supplied secrets/files,
4
+ * and gcloudAdc locates SDK-default credentials. MODEL_CONTEXT_ENV defines the
5
+ * non-secret provider settings shared by command sandboxes and agent containers.
6
+ */
1
7
  import { EXA_API_KEY_ENV, GATEWAY_KEY_ENV, GATEWAY_URL_ENV } from '@zenera/neo';
2
8
  import { chmodSync, copyFileSync, existsSync, readFileSync, statSync } from 'node:fs';
3
9
  import { homedir } from 'node:os';
@@ -40,6 +46,13 @@ export const GATEWAYS = ['bifrost'];
40
46
  export const MODES = ['direct', ...GATEWAYS];
41
47
  /** Anything the keyring can hold a credential for. */
42
48
  export const OWNERS = [...PROVIDERS, ...SERVICES, ...GATEWAYS];
49
+ /** Non-secret provider settings that accompany credentials into a runtime or sandbox. */
50
+ export const MODEL_CONTEXT_ENV = [
51
+ 'GOOGLE_CLOUD_PROJECT',
52
+ 'GOOGLE_CLOUD_LOCATION',
53
+ 'GOOGLE_CLOUD_QUOTA_PROJECT',
54
+ GATEWAY_URL_ENV,
55
+ ];
43
56
  /**
44
57
  * Vertex is the odd one, and it is odd twice.
45
58
  *
package/dist/sandbox.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Engine sandbox setup and credential forwarding used by zen run and server turns.
3
+ * forwarded collects credential mounts, secret names and MODEL_CONTEXT_ENV settings;
4
+ * the setup helpers build the sandbox specification and map per-run environment.
5
+ * File credentials reach commands only when the project's keys policy allows them.
6
+ */
1
7
  import { type AgentProject, type ProjectConfig, type RunIdentity, type Sandbox, type SandboxFactory, type Mount, type SandboxSpec } from '@zenera/neo';
2
8
  import { type Logger } from './errors.ts';
3
9
  import { type ResolvedBuild } from './image.ts';
@@ -75,22 +81,24 @@ export declare function usesSandbox(project: AgentProject): boolean;
75
81
  * attached to it, so the host supplies this; the engine only calls it.
76
82
  */
77
83
  export type Preflight = (setup: SandboxSetup, yes?: boolean) => Promise<void>;
78
- /**
79
- * Where the container's `$HOME` lives must exist before podman mounts it — and
80
- * the `zenera` libraries go into it, written fresh on every open so they match
81
- * the engine that set the variables they read. Done for every sandbox: it is a
82
- * small copy into the session folder, and a sandbox that does not bind it
83
- * supplies its own `$HOME` (`source: home`) with the same libraries.
84
- */
84
+ /** Where the container's `$HOME` lives must exist before podman mounts it. */
85
85
  export declare function prepareHome(setup: SandboxSetup): void;
86
86
  /**
87
87
  * The packaged sources, beside `src/` and `dist/` alike — also `@zenera/engine/runtime/*`,
88
- * for a sandbox that puts them in its image rather than in a bound `$HOME`.
88
+ * for an image build that puts them in the image.
89
89
  */
90
90
  export declare const RUNTIME_SOURCE: string;
91
- /** Where each one lands, inside the container. */
92
- export declare const RUNTIME_PYTHON = "/home/agent/.zenera/python";
93
- export declare const RUNTIME_NODE = "/home/agent/.node_modules/zenera";
91
+ /** Where the libraries are, inside every sandbox. */
92
+ export declare const RUNTIME_DIR = "/opt/zenera/runtime";
93
+ export declare const RUNTIME_PYTHON = "/opt/zenera/runtime/python";
94
+ export declare const RUNTIME_NODE = "/opt/zenera/runtime/node";
95
+ /** Lays the libraries out as a sandbox sees them: `python/zenera/`, `node/zenera/`. */
96
+ export declare function writeRuntime(dir: string): void;
97
+ /**
98
+ * This machine's copy of the libraries: the agent image's own when it is one, else one in the
99
+ * zen home per content — written once, by whichever process gets there first.
100
+ */
101
+ export declare function runtimeDir(): string;
94
102
  /**
95
103
  * Best-effort teardown: losing a container must never lose a run. Closing is
96
104
  * also where a sandbox that keeps its files elsewhere persists them.
package/dist/sandbox.js CHANGED
@@ -1,10 +1,18 @@
1
- import { DEFAULT_SANDBOX_IMAGE, GATEWAY_URL_ENV, SANDBOX_GROUP, WORKSPACE_MOUNT, identityEnv, podmanSandbox, isHostOnlyEnv, } from '@zenera/neo';
2
- import { cpSync, mkdirSync, rmSync } from 'node:fs';
3
- import { basename, join, relative } from 'node:path';
1
+ /**
2
+ * Engine sandbox setup and credential forwarding used by zen run and server turns.
3
+ * forwarded collects credential mounts, secret names and MODEL_CONTEXT_ENV settings;
4
+ * the setup helpers build the sandbox specification and map per-run environment.
5
+ * File credentials reach commands only when the project's keys policy allows them.
6
+ */
7
+ import { DEFAULT_SANDBOX_IMAGE, SANDBOX_GROUP, WORKSPACE_MOUNT, identityEnv, podmanSandbox, isHostOnlyEnv, } from '@zenera/neo';
8
+ import { createHash } from 'node:crypto';
9
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, } from 'node:fs';
10
+ import { basename, join } from 'node:path';
4
11
  import { fileURLToPath } from 'node:url';
5
12
  import { stderrLogger } from "./errors.js";
13
+ import { home as zenHome } from "./home.js";
6
14
  import { resolveBuild } from "./image.js";
7
- import { credentials } from "./keys.js";
15
+ import { credentials, MODEL_CONTEXT_ENV } from "./keys.js";
8
16
  // ---------------------------------------------------------------------------
9
17
  // The sandbox, as zen wires it
10
18
  //
@@ -62,7 +70,7 @@ function forwarded() {
62
70
  // Not secrets, and useless without one: a service account says which
63
71
  // project it belongs to but never which region to call, and a gateway key
64
72
  // says nothing of where the gateway is.
65
- for (const name of ['GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION', GATEWAY_URL_ENV]) {
73
+ for (const name of MODEL_CONTEXT_ENV) {
66
74
  const value = process.env[name];
67
75
  if (value) {
68
76
  env[name] = value;
@@ -82,6 +90,7 @@ export function buildSandbox(opts) {
82
90
  const home = join(opts.session.data, 'sandbox', 'home');
83
91
  const mounts = [
84
92
  { host: home, at: HOME, source: { kind: 'home' } },
93
+ { host: runtimeDir(), at: RUNTIME_DIR, readOnly: true, source: { kind: 'runtime' } },
85
94
  ...(opts.mounts ?? []),
86
95
  ];
87
96
  const keys = base.keys === false ? undefined : forwarded();
@@ -176,16 +185,9 @@ function toSpec(config, extra, secrets) {
176
185
  export function usesSandbox(project) {
177
186
  return project.agents.some((a) => a.tools.some((t) => t.group === SANDBOX_GROUP));
178
187
  }
179
- /**
180
- * Where the container's `$HOME` lives must exist before podman mounts it — and
181
- * the `zenera` libraries go into it, written fresh on every open so they match
182
- * the engine that set the variables they read. Done for every sandbox: it is a
183
- * small copy into the session folder, and a sandbox that does not bind it
184
- * supplies its own `$HOME` (`source: home`) with the same libraries.
185
- */
188
+ /** Where the container's `$HOME` lives must exist before podman mounts it. */
186
189
  export function prepareHome(setup) {
187
190
  mkdirSync(setup.home, { recursive: true });
188
- installRuntime(setup.home);
189
191
  }
190
192
  // ---------------------------------------------------------------------------
191
193
  // The `zenera` libraries
@@ -193,33 +195,70 @@ export function prepareHome(setup) {
193
195
  // What generated code calls to ask who it is running for. Python and Node,
194
196
  // because those are what agents write; anything else reads `ZEN_USER_*`.
195
197
  //
196
- // They live in the home the container already mounts, not in a mount of their
197
- // own: a new mount would rename every container that already exists, and a
198
- // persisted one would lose what it had installed. Each language is put where
199
- // its interpreter looks without being told — `require` searches
200
- // `$HOME/.node_modules` on its own — except Python, which has no such place
201
- // that does not depend on the version, so its directory is put in front of
202
- // `PYTHONPATH` on every command, ahead of whatever the image set.
198
+ // One read-only copy for every session, at the same path in every sandbox:
199
+ // /opt/zenera/runtime/{python,node}/zenera. podman binds this machine's copy
200
+ // there (written once per content, in the zen home); the agent image has it
201
+ // there already. Every command gets both directories in front of PYTHONPATH and
202
+ // NODE_PATH, ahead of whatever the image set, so `import zenera` and
203
+ // `require('zenera')` work as they are (an ES `import` ignores NODE_PATH).
203
204
  // ---------------------------------------------------------------------------
204
205
  /**
205
206
  * The packaged sources, beside `src/` and `dist/` alike — also `@zenera/engine/runtime/*`,
206
- * for a sandbox that puts them in its image rather than in a bound `$HOME`.
207
+ * for an image build that puts them in the image.
207
208
  */
208
209
  export const RUNTIME_SOURCE = fileURLToPath(new URL('../runtime/', import.meta.url));
209
- /** Where each one lands, inside the container. */
210
- export const RUNTIME_PYTHON = `${HOME}/.zenera/python`;
211
- export const RUNTIME_NODE = `${HOME}/.node_modules/zenera`;
212
- const RUNTIME_PATHS = { PYTHONPATH: [RUNTIME_PYTHON] };
213
- function installRuntime(home) {
214
- const inside = (path) => join(home, relative(HOME, path));
215
- for (const [from, to] of [
216
- ['python', RUNTIME_PYTHON],
217
- ['node', RUNTIME_NODE],
218
- ]) {
219
- const target = inside(to);
220
- rmSync(target, { recursive: true, force: true });
221
- cpSync(join(RUNTIME_SOURCE, from), target, { recursive: true });
210
+ /** Where the libraries are, inside every sandbox. */
211
+ export const RUNTIME_DIR = '/opt/zenera/runtime';
212
+ export const RUNTIME_PYTHON = `${RUNTIME_DIR}/python`;
213
+ export const RUNTIME_NODE = `${RUNTIME_DIR}/node`;
214
+ const RUNTIME_PATHS = { PYTHONPATH: [RUNTIME_PYTHON], NODE_PATH: [RUNTIME_NODE] };
215
+ /** Lays the libraries out as a sandbox sees them: `python/zenera/`, `node/zenera/`. */
216
+ export function writeRuntime(dir) {
217
+ mkdirSync(join(dir, 'node'), { recursive: true });
218
+ cpSync(join(RUNTIME_SOURCE, 'python'), join(dir, 'python'), { recursive: true });
219
+ cpSync(join(RUNTIME_SOURCE, 'node'), join(dir, 'node', 'zenera'), { recursive: true });
220
+ }
221
+ /**
222
+ * This machine's copy of the libraries: the agent image's own when it is one, else one in the
223
+ * zen home per content — written once, by whichever process gets there first.
224
+ */
225
+ export function runtimeDir() {
226
+ if (existsSync(join(RUNTIME_PYTHON, 'zenera', '__init__.py'))) {
227
+ return RUNTIME_DIR;
222
228
  }
229
+ const dir = join(zenHome(), 'runtime', runtimeHash());
230
+ if (!existsSync(dir)) {
231
+ const tmp = `${dir}.tmp-${process.pid}-${Date.now()}`;
232
+ writeRuntime(tmp);
233
+ try {
234
+ renameSync(tmp, dir);
235
+ }
236
+ catch {
237
+ // Another process wrote the same content first.
238
+ rmSync(tmp, { recursive: true, force: true });
239
+ }
240
+ }
241
+ return dir;
242
+ }
243
+ let hashed;
244
+ function runtimeHash() {
245
+ if (hashed)
246
+ return hashed;
247
+ const hash = createHash('sha256');
248
+ const walk = (rel) => {
249
+ for (const entry of readdirSync(join(RUNTIME_SOURCE, rel), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
250
+ const path = join(rel, entry.name);
251
+ if (entry.isDirectory()) {
252
+ walk(path);
253
+ }
254
+ else {
255
+ hash.update(`\0${path}\0`).update(readFileSync(join(RUNTIME_SOURCE, path)));
256
+ }
257
+ }
258
+ };
259
+ walk('');
260
+ hashed = hash.digest('hex').slice(0, 12);
261
+ return hashed;
223
262
  }
224
263
  /**
225
264
  * Best-effort teardown: losing a container must never lose a run. Closing is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/engine",
3
- "version": "1.1.58",
3
+ "version": "1.2.0",
4
4
  "description": "Runs a @zenera/neo project on this machine: keyring, sandbox, sessions and the record each turn leaves behind.",
5
5
  "keywords": [
6
6
  "agents",
@@ -46,6 +46,6 @@
46
46
  "prepack": "tsc -b"
47
47
  },
48
48
  "dependencies": {
49
- "@zenera/neo": "^1.1.58"
49
+ "@zenera/neo": "^1.2.0"
50
50
  }
51
51
  }