@zenera/engine 1.1.59 → 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/keys.d.ts +2 -0
- package/dist/keys.js +13 -0
- package/dist/sandbox.d.ts +19 -11
- package/dist/sandbox.js +73 -34
- package/package.json +2 -2
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
|
|
88
|
+
* for an image build that puts them in the image.
|
|
89
89
|
*/
|
|
90
90
|
export declare const RUNTIME_SOURCE: string;
|
|
91
|
-
/** Where
|
|
92
|
-
export declare const
|
|
93
|
-
export declare const
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
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
|
|
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
|
|
210
|
-
export const
|
|
211
|
-
export const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.
|
|
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.
|
|
49
|
+
"@zenera/neo": "^1.2.0"
|
|
50
50
|
}
|
|
51
51
|
}
|