@zenera/engine 1.1.51 → 1.1.53
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 +5 -4
- package/dist/env.d.ts +2 -1
- package/dist/env.js +2 -1
- package/dist/plugins.d.ts +12 -0
- package/dist/plugins.js +70 -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
|
/**
|
|
@@ -26,9 +26,10 @@ export async function openEngine(opts) {
|
|
|
26
26
|
// in it. The real environment still beats both.
|
|
27
27
|
const env = loadProjectEnv(opts.project.dir);
|
|
28
28
|
if (env?.refused.length && !opts.quiet) {
|
|
29
|
-
logger.warn(`${env.path} sets ${env.refused.join(', ')} — ignored: who a run acts for ` +
|
|
30
|
-
'
|
|
29
|
+
logger.warn(`${env.path} sets ${env.refused.join(', ')} — ignored: who a run acts for, and ` +
|
|
30
|
+
'which plugins load, are the host’s to say (zen run --as <user> to try one)');
|
|
31
31
|
}
|
|
32
|
+
const hostPlugins = mergePlugins(opts.plugins ?? [], opts.envPlugins === false ? [] : await envPlugins(logger));
|
|
32
33
|
const keys = await KeyStore.open();
|
|
33
34
|
keys.materialize();
|
|
34
35
|
assertUsable(keys);
|
|
@@ -100,7 +101,7 @@ export async function openEngine(opts) {
|
|
|
100
101
|
// Every agent can ask who it is acting for without the project
|
|
101
102
|
// having to know the tool exists; `-current_user` still takes it away.
|
|
102
103
|
const always = identityTools(user);
|
|
103
|
-
plugins = await loadPlugins(
|
|
104
|
+
plugins = await loadPlugins(hostPlugins, { user, project: opts.project, session: opts.session, logger }, [...builtins, ...always]);
|
|
104
105
|
project = await loadProject(opts.project.dir, {
|
|
105
106
|
tools: [...builtins, ...plugins.tools],
|
|
106
107
|
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
|
@@ -33,4 +33,16 @@ export interface LoadedPlugins {
|
|
|
33
33
|
* closed before the error is thrown.
|
|
34
34
|
*/
|
|
35
35
|
export declare function loadPlugins(plugins: readonly ZenPlugin[], ctx: PluginContext, builtins: readonly AnyTool[]): Promise<LoadedPlugins>;
|
|
36
|
+
export declare const PLUGINS_ENV = "ZEN_PLUGINS";
|
|
37
|
+
/** The plugins `ZEN_PLUGINS` named when this process started, imported once. */
|
|
38
|
+
export declare function envPlugins(logger: Logger): Promise<ZenPlugin[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Imports the plugins a `ZEN_PLUGINS`-style list names. One that cannot be
|
|
41
|
+
* loaded is a warning, not a failure: a project that never names its tools
|
|
42
|
+
* should still run, and one that does fails on the unknown tool, which says
|
|
43
|
+
* more than a missing file does.
|
|
44
|
+
*/
|
|
45
|
+
export declare function importPlugins(list: string | undefined, logger: Logger): Promise<ZenPlugin[]>;
|
|
46
|
+
/** The host's plugins and the machine's, the host's winning where both name one. */
|
|
47
|
+
export declare function mergePlugins(given: readonly ZenPlugin[], machine: readonly ZenPlugin[]): ZenPlugin[];
|
|
36
48
|
//# 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,64 @@ 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
|
+
}
|
|
70
140
|
//# 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.53",
|
|
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.53"
|
|
49
49
|
}
|
|
50
50
|
}
|