@zenera/engine 1.1.52 → 1.1.54
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/README.md +6 -0
- package/dist/engine.d.ts +4 -1
- package/dist/engine.js +8 -4
- package/dist/env.d.ts +2 -1
- package/dist/env.js +2 -1
- package/dist/plugins.d.ts +23 -1
- package/dist/plugins.js +92 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,6 +41,12 @@ const lookup: ZenPlugin = {
|
|
|
41
41
|
const engine = await openEngine({ project, session, user, plugins: [lookup] });
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
A `zen` nobody embeds (the CLI, `zen check`, a job) loads the plugins the machine names instead:
|
|
45
|
+
`ZEN_PLUGINS` is a comma-separated list of absolute paths to ES modules whose default export is a
|
|
46
|
+
`ZenPlugin`. It is read from the real environment when the engine is first imported; a project's
|
|
47
|
+
`.env` cannot set it. An embedding host gets them too, unless it passes a plugin of the same name or
|
|
48
|
+
`envPlugins: false`.
|
|
49
|
+
|
|
44
50
|
## Use
|
|
45
51
|
|
|
46
52
|
```ts
|
package/dist/engine.d.ts
CHANGED
|
@@ -72,9 +72,12 @@ export interface EngineOptions {
|
|
|
72
72
|
* Tools the host brings — see `ZenPlugin`. Each plugin is asked once, here,
|
|
73
73
|
* so its tools can hold what this run needs (who it is for, a credential
|
|
74
74
|
* the host minted for it). Config selects them like any other tool, and a
|
|
75
|
-
* name or group that clashes with a built-in is refused.
|
|
75
|
+
* name or group that clashes with a built-in is refused. The plugins
|
|
76
|
+
* `ZEN_PLUGINS` names are added too, unless one of these has the same name.
|
|
76
77
|
*/
|
|
77
78
|
plugins?: ZenPlugin[];
|
|
79
|
+
/** false: ignore `ZEN_PLUGINS` and use only `plugins` */
|
|
80
|
+
envPlugins?: boolean;
|
|
78
81
|
}
|
|
79
82
|
export interface Engine {
|
|
80
83
|
project: AgentProject;
|
package/dist/engine.js
CHANGED
|
@@ -9,7 +9,7 @@ import { EXIT, EngineError, invalidError, stderrLogger, usageError, } from "./er
|
|
|
9
9
|
import { readJson, writeJson } from "./home.js";
|
|
10
10
|
import { KeyStore, assertUsable } from "./keys.js";
|
|
11
11
|
import { projectMounts } from "./projects.js";
|
|
12
|
-
import { loadPlugins } from "./plugins.js";
|
|
12
|
+
import { envPlugins, loadPlugins, mergePlugins, } from "./plugins.js";
|
|
13
13
|
import { buildSandbox, prepareHome, teardown, usesSandbox, } from "./sandbox.js";
|
|
14
14
|
import { acquire, createRun, readSessionMeta, writeRunMeta, writeSessionMeta, } from "./session.js";
|
|
15
15
|
/**
|
|
@@ -21,14 +21,18 @@ export async function openEngine(opts) {
|
|
|
21
21
|
const logger = opts.logger ?? stderrLogger;
|
|
22
22
|
const onIssue = opts.onIssue ?? ((issue) => logger.warn(describeIssue(issue)));
|
|
23
23
|
const user = resolveUser(opts.user);
|
|
24
|
+
// The machine's plugins are imported before the project's env file is read: a plugin may
|
|
25
|
+
// configure itself from the environment when imported, and that must be the host's.
|
|
26
|
+
const machinePlugins = opts.envPlugins === false ? [] : await envPlugins(logger);
|
|
24
27
|
// Before the keyring, so the project's own file wins over the machine's
|
|
25
28
|
// keys, and before the load, so `${VAR}` in agents.yaml can name anything
|
|
26
29
|
// in it. The real environment still beats both.
|
|
27
30
|
const env = loadProjectEnv(opts.project.dir);
|
|
28
31
|
if (env?.refused.length && !opts.quiet) {
|
|
29
|
-
logger.warn(`${env.path} sets ${env.refused.join(', ')} — ignored: who a run acts for ` +
|
|
30
|
-
'
|
|
32
|
+
logger.warn(`${env.path} sets ${env.refused.join(', ')} — ignored: who a run acts for, and ` +
|
|
33
|
+
'which plugins load, are the host’s to say (zen run --as <user> to try one)');
|
|
31
34
|
}
|
|
35
|
+
const hostPlugins = mergePlugins(opts.plugins ?? [], machinePlugins);
|
|
32
36
|
const keys = await KeyStore.open();
|
|
33
37
|
keys.materialize();
|
|
34
38
|
assertUsable(keys);
|
|
@@ -100,7 +104,7 @@ export async function openEngine(opts) {
|
|
|
100
104
|
// Every agent can ask who it is acting for without the project
|
|
101
105
|
// having to know the tool exists; `-current_user` still takes it away.
|
|
102
106
|
const always = identityTools(user);
|
|
103
|
-
plugins = await loadPlugins(
|
|
107
|
+
plugins = await loadPlugins(hostPlugins, { user, project: opts.project, session: opts.session, logger }, [...builtins, ...always]);
|
|
104
108
|
project = await loadProject(opts.project.dir, {
|
|
105
109
|
tools: [...builtins, ...plugins.tools],
|
|
106
110
|
alwaysTools: always,
|
package/dist/env.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ export interface ProjectEnv {
|
|
|
16
16
|
/**
|
|
17
17
|
* Names the file declared and was not allowed to: `ZEN_USER_*` says who a
|
|
18
18
|
* run is acting for, and only the host gets to say that. A project that
|
|
19
|
-
* could set it in a committed file could be anyone it liked.
|
|
19
|
+
* could set it in a committed file could be anyone it liked. `ZEN_PLUGINS`
|
|
20
|
+
* names code to run inside the engine's process, with its credentials.
|
|
20
21
|
*/
|
|
21
22
|
refused: string[];
|
|
22
23
|
}
|
package/dist/env.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { isAbsolute, resolve } from 'node:path';
|
|
4
4
|
import { parseEnv } from 'node:util';
|
|
5
5
|
import { FILE_ENVS } from "./keys.js";
|
|
6
|
+
import { PLUGINS_ENV } from "./plugins.js";
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// The project's `.env`
|
|
8
9
|
//
|
|
@@ -46,7 +47,7 @@ export function readProjectEnv(dir) {
|
|
|
46
47
|
const values = {};
|
|
47
48
|
const refused = [];
|
|
48
49
|
for (const [name, value] of Object.entries(parseEnv(readFileSync(path, 'utf8')))) {
|
|
49
|
-
if (isIdentityEnv(name)) {
|
|
50
|
+
if (isIdentityEnv(name) || name === PLUGINS_ENV) {
|
|
50
51
|
refused.push(name);
|
|
51
52
|
}
|
|
52
53
|
else if (value !== undefined) {
|
package/dist/plugins.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export interface PluginContext {
|
|
|
11
11
|
dir: string;
|
|
12
12
|
name: string;
|
|
13
13
|
};
|
|
14
|
-
session
|
|
14
|
+
/** the session the run belongs to; absent when tools are only listed (`zen check`) */
|
|
15
|
+
session?: SessionPaths;
|
|
15
16
|
logger: Logger;
|
|
16
17
|
}
|
|
17
18
|
export interface ZenPlugin {
|
|
@@ -33,4 +34,25 @@ export interface LoadedPlugins {
|
|
|
33
34
|
* closed before the error is thrown.
|
|
34
35
|
*/
|
|
35
36
|
export declare function loadPlugins(plugins: readonly ZenPlugin[], ctx: PluginContext, builtins: readonly AnyTool[]): Promise<LoadedPlugins>;
|
|
37
|
+
export declare const PLUGINS_ENV = "ZEN_PLUGINS";
|
|
38
|
+
/** The plugins `ZEN_PLUGINS` named when this process started, imported once. */
|
|
39
|
+
export declare function envPlugins(logger: Logger): Promise<ZenPlugin[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Imports the plugins a `ZEN_PLUGINS`-style list names. One that cannot be
|
|
42
|
+
* loaded is a warning, not a failure: a project that never names its tools
|
|
43
|
+
* should still run, and one that does fails on the unknown tool, which says
|
|
44
|
+
* more than a missing file does.
|
|
45
|
+
*/
|
|
46
|
+
export declare function importPlugins(list: string | undefined, logger: Logger): Promise<ZenPlugin[]>;
|
|
47
|
+
/** The host's plugins and the machine's, the host's winning where both name one. */
|
|
48
|
+
export declare function mergePlugins(given: readonly ZenPlugin[], machine: readonly ZenPlugin[]): ZenPlugin[];
|
|
49
|
+
/**
|
|
50
|
+
* The tools the machine's plugins add, for checking a project rather than
|
|
51
|
+
* running it: the plugins are asked with no user and no session, and closed
|
|
52
|
+
* again. A plugin that fails or clashes is a warning, and adds nothing.
|
|
53
|
+
*/
|
|
54
|
+
export declare function machinePluginTools(project: {
|
|
55
|
+
dir: string;
|
|
56
|
+
name: string;
|
|
57
|
+
}, builtins: readonly AnyTool[], logger: Logger): Promise<AnyTool[]>;
|
|
36
58
|
//# sourceMappingURL=plugins.d.ts.map
|
package/dist/plugins.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { isAbsolute } from 'node:path';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
1
11
|
/** What a plugin needs to write a tool — re-exported so a host needs only this package. */
|
|
2
12
|
export { tool } from '@zenera/neo';
|
|
3
13
|
/**
|
|
@@ -67,4 +77,86 @@ export async function loadPlugins(plugins, ctx, builtins) {
|
|
|
67
77
|
function message(err) {
|
|
68
78
|
return err instanceof Error ? err.message : String(err);
|
|
69
79
|
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Plugins the machine names
|
|
82
|
+
//
|
|
83
|
+
// A host that embeds the engine passes its plugins in. A `zen` it does not
|
|
84
|
+
// embed — the CLI in a terminal, a job, `zen check` — has no such call, so the
|
|
85
|
+
// machine names them instead: `ZEN_PLUGINS` is a comma-separated list of
|
|
86
|
+
// absolute paths to ES modules whose default export is a `ZenPlugin`. A project
|
|
87
|
+
// naming one of their groups then loads the same way everywhere.
|
|
88
|
+
//
|
|
89
|
+
// It is read once, when this module is first imported, and only from the real
|
|
90
|
+
// environment: a plugin runs inside this process, with every credential the
|
|
91
|
+
// process holds, so a project must not be able to name one. `readProjectEnv`
|
|
92
|
+
// refuses the name for the same reason, should a host apply a project's env
|
|
93
|
+
// file before importing the engine.
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
export const PLUGINS_ENV = 'ZEN_PLUGINS';
|
|
96
|
+
const STARTUP_PLUGINS = process.env[PLUGINS_ENV];
|
|
97
|
+
let fromEnv;
|
|
98
|
+
/** The plugins `ZEN_PLUGINS` named when this process started, imported once. */
|
|
99
|
+
export function envPlugins(logger) {
|
|
100
|
+
fromEnv ??= importPlugins(STARTUP_PLUGINS, logger);
|
|
101
|
+
return fromEnv;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Imports the plugins a `ZEN_PLUGINS`-style list names. One that cannot be
|
|
105
|
+
* loaded is a warning, not a failure: a project that never names its tools
|
|
106
|
+
* should still run, and one that does fails on the unknown tool, which says
|
|
107
|
+
* more than a missing file does.
|
|
108
|
+
*/
|
|
109
|
+
export async function importPlugins(list, logger) {
|
|
110
|
+
const plugins = [];
|
|
111
|
+
const paths = (list ?? '')
|
|
112
|
+
.split(',')
|
|
113
|
+
.map((p) => p.trim())
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
for (const path of paths) {
|
|
116
|
+
if (!isAbsolute(path)) {
|
|
117
|
+
logger.warn(`${PLUGINS_ENV}: ${path} is not an absolute path — ignored`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)));
|
|
122
|
+
const plugin = mod.default;
|
|
123
|
+
if (!plugin || typeof plugin.name !== 'string' || typeof plugin.tools !== 'function') {
|
|
124
|
+
logger.warn(`${PLUGINS_ENV}: ${path} has no default export with a name and tools() — ignored`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
plugins.push(plugin);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
logger.warn(`${PLUGINS_ENV}: cannot load ${path}: ${message(err)} — ignored`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return plugins;
|
|
134
|
+
}
|
|
135
|
+
/** The host's plugins and the machine's, the host's winning where both name one. */
|
|
136
|
+
export function mergePlugins(given, machine) {
|
|
137
|
+
const names = new Set(given.map((p) => p.name));
|
|
138
|
+
return [...given, ...machine.filter((p) => !names.has(p.name))];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The tools the machine's plugins add, for checking a project rather than
|
|
142
|
+
* running it: the plugins are asked with no user and no session, and closed
|
|
143
|
+
* again. A plugin that fails or clashes is a warning, and adds nothing.
|
|
144
|
+
*/
|
|
145
|
+
export async function machinePluginTools(project, builtins, logger) {
|
|
146
|
+
const tools = [];
|
|
147
|
+
for (const plugin of await envPlugins(logger)) {
|
|
148
|
+
try {
|
|
149
|
+
const loaded = await loadPlugins([plugin], { project, logger }, [
|
|
150
|
+
...builtins,
|
|
151
|
+
...tools,
|
|
152
|
+
]);
|
|
153
|
+
await loaded.close();
|
|
154
|
+
tools.push(...loaded.tools);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
logger.warn(`${PLUGINS_ENV}: ${message(err)} — its tools are left out`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return tools;
|
|
161
|
+
}
|
|
70
162
|
//# sourceMappingURL=plugins.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.54",
|
|
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.54"
|
|
49
49
|
}
|
|
50
50
|
}
|