@zenera/engine 1.1.52 → 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 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
- 'is the host’s to say (zen run --as <user> to try one)');
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(opts.plugins ?? [], { user, project: opts.project, session: opts.session, logger }, [...builtins, ...always]);
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.52",
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.52"
48
+ "@zenera/neo": "^1.1.53"
49
49
  }
50
50
  }