@zenera/engine 1.1.55 → 1.1.56
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.d.ts +8 -2
- package/dist/engine.js +15 -14
- package/dist/projects.d.ts +2 -2
- package/dist/projects.js +15 -13
- package/dist/sandbox.d.ts +18 -8
- package/dist/sandbox.js +30 -41
- package/package.json +2 -2
package/dist/engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AgentRunner, Workspace, type AgentEvent, type AgentProject, type AgentState, type Input, type ResumeAnswer, type MemoryIndex, type RunIdentity, type RunResult } from '@zenera/neo';
|
|
1
|
+
import { AgentRunner, Workspace, type AgentEvent, type AgentProject, type AgentState, type Input, type ResumeAnswer, type MemoryIndex, type RunIdentity, type RunResult, type SandboxFactory } from '@zenera/neo';
|
|
2
2
|
import { type ModelIssue } from './audit.ts';
|
|
3
3
|
import { type Logger } from './errors.ts';
|
|
4
4
|
import { type ZenPlugin } from './plugins.ts';
|
|
@@ -30,7 +30,6 @@ export interface EngineOptions {
|
|
|
30
30
|
* An already-open graph to remember into instead of opening `memoryDir`.
|
|
31
31
|
* Sessions may each open the same directory; handing them one index saves
|
|
32
32
|
* every session loading the graph again.
|
|
33
|
-
* Name its directory in `memoryDir` too: that is what is mounted at `/memory`.
|
|
34
33
|
*/
|
|
35
34
|
memory?: MemoryIndex;
|
|
36
35
|
/** whether credentials reach the sandbox — `--no-keys` sets this false */
|
|
@@ -54,6 +53,13 @@ export interface EngineOptions {
|
|
|
54
53
|
onIssue?: (issue: ModelIssue) => void;
|
|
55
54
|
/** readies the container engine when the project can shell out; nothing when unset */
|
|
56
55
|
preflight?: Preflight;
|
|
56
|
+
/**
|
|
57
|
+
* Opens the session's sandbox — where its commands run and its files
|
|
58
|
+
* live. Podman on this machine when unset, which is what `zen run` is; a
|
|
59
|
+
* host with a sandbox service passes its own, and the file tools then read
|
|
60
|
+
* the files where that sandbox keeps them.
|
|
61
|
+
*/
|
|
62
|
+
newSandbox?: SandboxFactory;
|
|
57
63
|
/** draws each finished run as `graph.mmd`; no graph when unset */
|
|
58
64
|
graph?: GraphRenderer;
|
|
59
65
|
/**
|
package/dist/engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AgentRunner, FilePayloadStore, MemoryError,
|
|
1
|
+
import { AgentRunner, FilePayloadStore, MemoryError, SKILLS_MOUNT, Workspace, assertState, buildRunReport, compactIdentity, exaTools, fileTools, identityFromEnv, identityTools, lastText, loadProject, memoryDir, readProjectConfig, renderReportHtml, sandboxTools, turns, } from '@zenera/neo';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { writeFile } from 'node:fs/promises';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
@@ -52,7 +52,7 @@ export async function openEngine(opts) {
|
|
|
52
52
|
// resolved against them. They are always registered, whether or not anyone
|
|
53
53
|
// selects them: a project that names `sandbox:*` against an empty tool list
|
|
54
54
|
// fails to load with "unknown tool", which would be a lie.
|
|
55
|
-
let
|
|
55
|
+
let setup;
|
|
56
56
|
let project;
|
|
57
57
|
let files;
|
|
58
58
|
let memory;
|
|
@@ -66,8 +66,8 @@ export async function openEngine(opts) {
|
|
|
66
66
|
}
|
|
67
67
|
memory = memoryDir(root, config, opts.memoryDir);
|
|
68
68
|
// Assets and the skill catalog are mounted for both, under one name.
|
|
69
|
-
const mounts = projectMounts(root, config
|
|
70
|
-
|
|
69
|
+
const mounts = projectMounts(root, config);
|
|
70
|
+
setup = buildSandbox({
|
|
71
71
|
config,
|
|
72
72
|
root,
|
|
73
73
|
session: opts.session,
|
|
@@ -78,14 +78,15 @@ export async function openEngine(opts) {
|
|
|
78
78
|
env: env?.names,
|
|
79
79
|
mounts,
|
|
80
80
|
identity: user,
|
|
81
|
+
newSandbox: opts.newSandbox,
|
|
81
82
|
...(opts.disposable ? { persist: false } : {}),
|
|
82
83
|
});
|
|
83
|
-
const workspaceOptions = {
|
|
84
|
+
const workspaceOptions = setup.sandbox.files({
|
|
84
85
|
root: workspace,
|
|
85
86
|
readOnly: opts.readOnly,
|
|
86
|
-
mount:
|
|
87
|
+
mount: setup.workdir,
|
|
87
88
|
mounts,
|
|
88
|
-
};
|
|
89
|
+
});
|
|
89
90
|
// The same view the file tools have, so a path the agent just wrote
|
|
90
91
|
// resolves the same way when it asks memory to keep a copy.
|
|
91
92
|
files = new Workspace(workspaceOptions);
|
|
@@ -94,7 +95,7 @@ export async function openEngine(opts) {
|
|
|
94
95
|
// one name for it: whatever `run_command` prints a path as,
|
|
95
96
|
// `read_file` accepts.
|
|
96
97
|
...fileTools(workspaceOptions),
|
|
97
|
-
...sandboxTools(sandbox
|
|
98
|
+
...sandboxTools(setup.sandbox),
|
|
98
99
|
// Registered whether or not a key exists: the credential is
|
|
99
100
|
// read when a tool is called, so a project that names `exa:*`
|
|
100
101
|
// loads on a machine that cannot yet search, and says so on the
|
|
@@ -113,7 +114,7 @@ export async function openEngine(opts) {
|
|
|
113
114
|
memoryDir: opts.memoryDir,
|
|
114
115
|
memoryReadOnly: opts.memoryReadOnly,
|
|
115
116
|
payloads,
|
|
116
|
-
|
|
117
|
+
files,
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
catch (err) {
|
|
@@ -129,8 +130,8 @@ export async function openEngine(opts) {
|
|
|
129
130
|
// container engine costs an error instead of a round trip. Nothing is
|
|
130
131
|
// started: the container itself waits until something actually runs.
|
|
131
132
|
if (usesSandbox(project)) {
|
|
132
|
-
prepareHome(
|
|
133
|
-
await opts.preflight?.(
|
|
133
|
+
prepareHome(setup);
|
|
134
|
+
await opts.preflight?.(setup, opts.yes);
|
|
134
135
|
}
|
|
135
136
|
// `recordRequests` is what makes the report show the exact bytes sent
|
|
136
137
|
// rather than a reconstruction. It costs state size, and a CLI run is
|
|
@@ -155,18 +156,18 @@ export async function openEngine(opts) {
|
|
|
155
156
|
runner,
|
|
156
157
|
name: opts.project.name,
|
|
157
158
|
workspace,
|
|
158
|
-
workspaceMount:
|
|
159
|
+
workspaceMount: setup.workdir,
|
|
159
160
|
files,
|
|
160
161
|
readOnly: Boolean(opts.readOnly),
|
|
161
162
|
session: opts.session,
|
|
162
163
|
memory,
|
|
163
164
|
state,
|
|
164
|
-
sandbox,
|
|
165
|
+
sandbox: setup,
|
|
165
166
|
lock,
|
|
166
167
|
graph: opts.graph,
|
|
167
168
|
user,
|
|
168
169
|
close: async () => {
|
|
169
|
-
await teardown(
|
|
170
|
+
await teardown(setup, logger);
|
|
170
171
|
// A host-supplied graph is the host's to release.
|
|
171
172
|
if (!opts.memory) {
|
|
172
173
|
project.close();
|
package/dist/projects.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ProjectConfig, type
|
|
1
|
+
import { type ProjectConfig, type Mount } from '@zenera/neo';
|
|
2
2
|
/** Whether a directory is a project: whether the loader has something to read. */
|
|
3
3
|
export declare function isProjectDir(dir: string): boolean;
|
|
4
4
|
/** A project resolved on disk. */
|
|
@@ -16,7 +16,7 @@ export interface Project {
|
|
|
16
16
|
* and does not edit — and the paths are resolved, because the podman machine on
|
|
17
17
|
* macOS shares the real path or nothing.
|
|
18
18
|
*/
|
|
19
|
-
export declare function projectMounts(root: string, config: ProjectConfig
|
|
19
|
+
export declare function projectMounts(root: string, config: ProjectConfig): Mount[];
|
|
20
20
|
/** Walks up from `start` looking for a project configuration. */
|
|
21
21
|
export declare function findUp(start: string): Promise<Project | undefined>;
|
|
22
22
|
export declare function openDir(dir: string): Promise<Project>;
|
package/dist/projects.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ASSETS_MOUNT, assetsDir,
|
|
2
|
-
import { existsSync, lstatSync,
|
|
1
|
+
import { ASSETS_MOUNT, assetsDir, pidAlive, skillDirs, skillMounts, SKILLS_MOUNT, } from '@zenera/neo';
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { invalidError, usageError } from "./errors.js";
|
|
5
5
|
import { paths, readJson, writeJson } from "./home.js";
|
|
@@ -35,25 +35,27 @@ export function isProjectDir(dir) {
|
|
|
35
35
|
* and does not edit — and the paths are resolved, because the podman machine on
|
|
36
36
|
* macOS shares the real path or nothing.
|
|
37
37
|
*/
|
|
38
|
-
export function projectMounts(root, config
|
|
38
|
+
export function projectMounts(root, config) {
|
|
39
39
|
const mounts = [];
|
|
40
40
|
const assets = assetsDir(root, config);
|
|
41
41
|
if (assets) {
|
|
42
|
-
mounts.push({
|
|
42
|
+
mounts.push({
|
|
43
|
+
host: realpathSync(assets),
|
|
44
|
+
at: ASSETS_MOUNT,
|
|
45
|
+
readOnly: true,
|
|
46
|
+
source: { kind: 'assets' },
|
|
47
|
+
});
|
|
43
48
|
}
|
|
44
49
|
// The catalog goes in whole, at fixed names, before anything is loaded: a
|
|
45
50
|
// container's mounts are decided when it is created, so a skill folder
|
|
46
51
|
// cannot be added at the moment `skill_load` asks for it.
|
|
47
52
|
for (const dir of skillMounts(skillDirs(root, config), SKILLS_MOUNT)) {
|
|
48
|
-
mounts.push({
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const files = join(memory, FILES_DIR);
|
|
55
|
-
mkdirSync(files, { recursive: true });
|
|
56
|
-
mounts.push({ host: realpathSync(files), at: MEMORY_MOUNT, readOnly: true });
|
|
53
|
+
mounts.push({
|
|
54
|
+
host: realpathSync(dir.path),
|
|
55
|
+
at: dir.at,
|
|
56
|
+
readOnly: true,
|
|
57
|
+
source: { kind: 'skills' },
|
|
58
|
+
});
|
|
57
59
|
}
|
|
58
60
|
return mounts;
|
|
59
61
|
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type AgentProject, type ProjectConfig, type RunIdentity, type Sandbox, type SandboxFactory, type Mount, type SandboxSpec } from '@zenera/neo';
|
|
2
2
|
import { type Logger } from './errors.ts';
|
|
3
3
|
import { type ResolvedBuild } from './image.ts';
|
|
4
4
|
import type { SessionPaths } from './session.ts';
|
|
5
5
|
export interface SandboxSetup {
|
|
6
|
-
|
|
6
|
+
/** where the session's commands run, and the files they see; podman for `zen run` */
|
|
7
|
+
sandbox: Sandbox;
|
|
7
8
|
/** the resolved base spec, for `zn sandbox status` and the image pre-warm */
|
|
8
9
|
spec: SandboxSpec;
|
|
10
|
+
/** where the workspace is in the sandbox, and so the name the file tools answer with */
|
|
11
|
+
workdir: string;
|
|
9
12
|
image: string;
|
|
10
13
|
/** the Dockerfile behind `image`, when it came from one */
|
|
11
14
|
build?: ResolvedBuild;
|
|
12
15
|
/** host side of the container's `$HOME`, created only if it is ever needed */
|
|
13
16
|
home: string;
|
|
14
17
|
/** every tree bound into the container besides the workspace itself */
|
|
15
|
-
mounts: readonly
|
|
18
|
+
mounts: readonly Mount[];
|
|
16
19
|
}
|
|
17
20
|
export interface SandboxInputs {
|
|
18
21
|
config: ProjectConfig;
|
|
@@ -26,7 +29,7 @@ export interface SandboxInputs {
|
|
|
26
29
|
* and its skill catalog. The same array goes to the file tools, so a path
|
|
27
30
|
* a command prints is a path `read_file` accepts.
|
|
28
31
|
*/
|
|
29
|
-
mounts?: readonly
|
|
32
|
+
mounts?: readonly Mount[];
|
|
30
33
|
/** `--image` */
|
|
31
34
|
image?: string;
|
|
32
35
|
/** `--no-keys`: refuse to forward credentials, whatever the config says */
|
|
@@ -40,13 +43,15 @@ export interface SandboxInputs {
|
|
|
40
43
|
* promise it does not keep.
|
|
41
44
|
*/
|
|
42
45
|
env?: readonly string[];
|
|
43
|
-
/** `false` overrides
|
|
46
|
+
/** `false` overrides the config's `persist:` */
|
|
44
47
|
persist?: boolean;
|
|
45
48
|
/**
|
|
46
49
|
* Who the run is acting for. Set on every command as `ZEN_USER_*`, never
|
|
47
50
|
* on the container — see `execEnv` in the library for why.
|
|
48
51
|
*/
|
|
49
52
|
identity?: RunIdentity;
|
|
53
|
+
/** opens the sandbox; podman on this machine when unset */
|
|
54
|
+
newSandbox?: SandboxFactory;
|
|
50
55
|
}
|
|
51
56
|
export declare function buildSandbox(opts: SandboxInputs): SandboxSetup;
|
|
52
57
|
/**
|
|
@@ -67,12 +72,17 @@ export type Preflight = (setup: SandboxSetup, yes?: boolean) => Promise<void>;
|
|
|
67
72
|
/**
|
|
68
73
|
* Where the container's `$HOME` lives must exist before podman mounts it — and
|
|
69
74
|
* the `zenera` libraries go into it, written fresh on every open so they match
|
|
70
|
-
* the engine that set the variables they read.
|
|
75
|
+
* the engine that set the variables they read. Done for every sandbox: it is a
|
|
76
|
+
* small copy into the session folder, and a sandbox that does not bind it
|
|
77
|
+
* supplies its own `$HOME` (`source: home`) with the same libraries.
|
|
71
78
|
*/
|
|
72
79
|
export declare function prepareHome(setup: SandboxSetup): void;
|
|
73
80
|
/** Where each one lands, inside the container. */
|
|
74
81
|
export declare const RUNTIME_PYTHON = "/home/agent/.zenera/python";
|
|
75
82
|
export declare const RUNTIME_NODE = "/home/agent/.node_modules/zenera";
|
|
76
|
-
/**
|
|
77
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Best-effort teardown: losing a container must never lose a run. Closing is
|
|
85
|
+
* also where a sandbox that keeps its files elsewhere persists them.
|
|
86
|
+
*/
|
|
87
|
+
export declare function teardown(setup: SandboxSetup, logger?: Logger): Promise<void>;
|
|
78
88
|
//# sourceMappingURL=sandbox.d.ts.map
|
package/dist/sandbox.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_SANDBOX_IMAGE, GATEWAY_URL_ENV, SANDBOX_GROUP,
|
|
1
|
+
import { DEFAULT_SANDBOX_IMAGE, GATEWAY_URL_ENV, SANDBOX_GROUP, WORKSPACE_MOUNT, identityEnv, podmanSandbox, isIdentityEnv, } from '@zenera/neo';
|
|
2
2
|
import { cpSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
3
|
import { basename, join, relative } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -48,7 +48,12 @@ function forwarded() {
|
|
|
48
48
|
for (const cred of credentials()) {
|
|
49
49
|
if (cred.holds === 'file') {
|
|
50
50
|
const at = `${KEY_MOUNT}/${basename(cred.value)}`;
|
|
51
|
-
mounts.push({
|
|
51
|
+
mounts.push({
|
|
52
|
+
host: cred.value,
|
|
53
|
+
at,
|
|
54
|
+
readOnly: true,
|
|
55
|
+
source: { kind: 'credential', name: cred.env },
|
|
56
|
+
});
|
|
52
57
|
env[cred.env] = at;
|
|
53
58
|
continue;
|
|
54
59
|
}
|
|
@@ -75,7 +80,10 @@ export function buildSandbox(opts) {
|
|
|
75
80
|
...(opts.persist === false ? { persist: false } : {}),
|
|
76
81
|
};
|
|
77
82
|
const home = join(opts.session.data, 'sandbox', 'home');
|
|
78
|
-
const mounts = [
|
|
83
|
+
const mounts = [
|
|
84
|
+
{ host: home, at: HOME, source: { kind: 'home' } },
|
|
85
|
+
...(opts.mounts ?? []),
|
|
86
|
+
];
|
|
79
87
|
const keys = base.keys === false ? undefined : forwarded();
|
|
80
88
|
if (keys) {
|
|
81
89
|
mounts.push(...keys.mounts);
|
|
@@ -86,24 +94,8 @@ export function buildSandbox(opts) {
|
|
|
86
94
|
// `ZENERA_AGENT` tells a `zen` run by `run_command` that no person is there.
|
|
87
95
|
const extra = { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(keys?.env ?? {}) };
|
|
88
96
|
const spec = toSpec(base, extra, secretsOf(keys?.secrets, opts.env));
|
|
89
|
-
const
|
|
90
|
-
for (const agent of opts.config.agents) {
|
|
91
|
-
if (agent.sandbox) {
|
|
92
|
-
const merged = merge(base, agent.sandbox);
|
|
93
|
-
if (opts.persist === false) {
|
|
94
|
-
merged.persist = false;
|
|
95
|
-
}
|
|
96
|
-
// An agent that opts out gets neither the variables nor the mount,
|
|
97
|
-
// but the mount is on the pool and cannot be taken back per agent —
|
|
98
|
-
// so the variable is what actually decides, and without it the file
|
|
99
|
-
// sitting there is not a credential anything will look for.
|
|
100
|
-
const own = merged.keys === false ? undefined : keys;
|
|
101
|
-
agents[agent.name] = toSpec(merged, { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(own?.env ?? {}) }, own && secretsOf(own.secrets, opts.env));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const pool = new SandboxPool({
|
|
97
|
+
const sandbox = (opts.newSandbox ?? podmanSandbox)({
|
|
105
98
|
...spec,
|
|
106
|
-
agents,
|
|
107
99
|
root: opts.workspace,
|
|
108
100
|
key: opts.session.id,
|
|
109
101
|
readOnly: opts.readOnly,
|
|
@@ -111,23 +103,15 @@ export function buildSandbox(opts) {
|
|
|
111
103
|
execEnv: identityEnv(opts.identity),
|
|
112
104
|
execPaths: RUNTIME_PATHS,
|
|
113
105
|
});
|
|
114
|
-
return {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const merged = { ...base, ...agent };
|
|
124
|
-
if (agent.image !== undefined) {
|
|
125
|
-
delete merged.build;
|
|
126
|
-
}
|
|
127
|
-
if (agent.build !== undefined) {
|
|
128
|
-
delete merged.image;
|
|
129
|
-
}
|
|
130
|
-
return merged;
|
|
106
|
+
return {
|
|
107
|
+
sandbox,
|
|
108
|
+
spec,
|
|
109
|
+
workdir: spec.workdir ?? WORKSPACE_MOUNT,
|
|
110
|
+
image: spec.image ?? DEFAULT_SANDBOX_IMAGE,
|
|
111
|
+
build,
|
|
112
|
+
home,
|
|
113
|
+
mounts,
|
|
114
|
+
};
|
|
131
115
|
}
|
|
132
116
|
/**
|
|
133
117
|
* The two sources of forwarded names, as one list: the credentials the keyring
|
|
@@ -189,7 +173,9 @@ export function usesSandbox(project) {
|
|
|
189
173
|
/**
|
|
190
174
|
* Where the container's `$HOME` lives must exist before podman mounts it — and
|
|
191
175
|
* the `zenera` libraries go into it, written fresh on every open so they match
|
|
192
|
-
* the engine that set the variables they read.
|
|
176
|
+
* the engine that set the variables they read. Done for every sandbox: it is a
|
|
177
|
+
* small copy into the session folder, and a sandbox that does not bind it
|
|
178
|
+
* supplies its own `$HOME` (`source: home`) with the same libraries.
|
|
193
179
|
*/
|
|
194
180
|
export function prepareHome(setup) {
|
|
195
181
|
mkdirSync(setup.home, { recursive: true });
|
|
@@ -226,10 +212,13 @@ function installRuntime(home) {
|
|
|
226
212
|
cpSync(join(RUNTIME_SOURCE, from), target, { recursive: true });
|
|
227
213
|
}
|
|
228
214
|
}
|
|
229
|
-
/**
|
|
230
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Best-effort teardown: losing a container must never lose a run. Closing is
|
|
217
|
+
* also where a sandbox that keeps its files elsewhere persists them.
|
|
218
|
+
*/
|
|
219
|
+
export async function teardown(setup, logger = stderrLogger) {
|
|
231
220
|
try {
|
|
232
|
-
await
|
|
221
|
+
await setup.sandbox.close();
|
|
233
222
|
}
|
|
234
223
|
catch (err) {
|
|
235
224
|
logger.warn(`could not clean up the sandbox: ${err instanceof Error ? err.message : String(err)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.56",
|
|
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",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"prepack": "tsc -b"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@zenera/neo": "^1.1.
|
|
48
|
+
"@zenera/neo": "^1.1.56"
|
|
49
49
|
}
|
|
50
50
|
}
|