@zenera/cli 1.1.36 → 1.1.37
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/archive.js +1 -1
- package/dist/batch.js +2 -3
- package/dist/cache.js +1 -1
- package/dist/candidates.js +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/cache.js +1 -1
- package/dist/commands/check.js +1 -3
- package/dist/commands/export.js +1 -3
- package/dist/commands/import.js +1 -2
- package/dist/commands/init.js +1 -4
- package/dist/commands/inspect.d.ts +1 -1
- package/dist/commands/inspect.js +1 -4
- package/dist/commands/key.js +1 -2
- package/dist/commands/list.js +4 -5
- package/dist/commands/memory.js +1 -2
- package/dist/commands/meta.js +2 -4
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +1 -2
- package/dist/commands/run.js +2 -2
- package/dist/commands/sandbox.js +1 -2
- package/dist/engine.d.ts +3 -90
- package/dist/engine.js +16 -302
- package/dist/history.js +1 -1
- package/dist/lib.d.ts +1 -2
- package/dist/lib.js +1 -2
- package/dist/liveness.d.ts +1 -1
- package/dist/liveness.js +1 -1
- package/dist/meta/host.d.ts +1 -2
- package/dist/meta/host.js +1 -2
- package/dist/modelref.d.ts +1 -1
- package/dist/podman.d.ts +1 -1
- package/dist/podman.js +1 -1
- package/dist/resolve.d.ts +2 -2
- package/dist/resolve.js +2 -3
- package/dist/resources.js +1 -1
- package/dist/sandbox.d.ts +2 -60
- package/dist/sandbox.js +1 -200
- package/dist/term.d.ts +1 -22
- package/dist/term.js +3 -26
- package/dist/tui/app.js +1 -1
- package/dist/tui/models.d.ts +1 -1
- package/dist/validate.d.ts +1 -2
- package/dist/validate.js +1 -3
- package/package.json +4 -3
- package/dist/audit.d.ts +0 -58
- package/dist/audit.js +0 -148
- package/dist/env.d.ts +0 -20
- package/dist/env.js +0 -65
- package/dist/home.d.ts +0 -35
- package/dist/home.js +0 -112
- package/dist/ids.d.ts +0 -12
- package/dist/ids.js +0 -44
- package/dist/image.d.ts +0 -16
- package/dist/image.js +0 -85
- package/dist/keys.d.ts +0 -269
- package/dist/keys.js +0 -624
- package/dist/projects.d.ts +0 -91
- package/dist/projects.js +0 -303
- package/dist/session.d.ts +0 -112
- package/dist/session.js +0 -188
package/dist/sandbox.js
CHANGED
|
@@ -1,202 +1,12 @@
|
|
|
1
|
-
import { DEFAULT_SANDBOX_IMAGE, GATEWAY_URL_ENV, SANDBOX_GROUP, SandboxPool, } from '@zenera/neo';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
3
|
-
import { basename, join } from 'node:path';
|
|
4
|
-
import { resolveBuild } from "./image.js";
|
|
5
|
-
import { credentials } from "./keys.js";
|
|
6
1
|
import { assertLocks, ensurePodmanReady } from "./podman.js";
|
|
7
|
-
import { warn } from "./term.js";
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// The sandbox, as this CLI wires it
|
|
10
|
-
//
|
|
11
|
-
// The library takes resolved values; the config states names and numbers. This
|
|
12
|
-
// is the layer between the two, and it owns the two decisions the library has
|
|
13
|
-
// no business making: which host variables are worth forwarding, and where the
|
|
14
|
-
// container's home directory lives.
|
|
15
|
-
//
|
|
16
|
-
// It lives in the session directory, which is the whole point. A session is
|
|
17
|
-
// already a self-contained thing — copy the directory and its conversation,
|
|
18
|
-
// memory and blobs travel with it — and a `pip install --user` that vanished
|
|
19
|
-
// on close would be the only part of a session that did not. So `$HOME` is a
|
|
20
|
-
// bind mount into `.data/sandbox/home`, and what the agent installs for itself
|
|
21
|
-
// is still there when the session is opened again. Everything *outside* the
|
|
22
|
-
// mounts is throwaway, which is what keeps a stale container from becoming a
|
|
23
|
-
// second, invisible configuration.
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
/** Where the persistent home lives, inside the container. */
|
|
26
|
-
const HOME = '/home/agent';
|
|
27
|
-
/** Where a credential *file* is mounted, inside the container. */
|
|
28
|
-
const KEY_MOUNT = '/run/zenera/keys';
|
|
29
|
-
/**
|
|
30
|
-
* The credentials the run is using, in the form a container takes.
|
|
31
|
-
*
|
|
32
|
-
* Two shapes, two mechanisms. A secret is forwarded by *name* — podman reads
|
|
33
|
-
* the value from its own environment, so nothing is written into an argv that
|
|
34
|
-
* `ps` will show. A service-account file has to be present as a file, so the
|
|
35
|
-
* one file is bind-mounted read-only and the variable is rewritten to where it
|
|
36
|
-
* landed; mounting the directory it came from would drag in whatever else the
|
|
37
|
-
* user keeps beside it.
|
|
38
|
-
*
|
|
39
|
-
* This is the part of the design that gives something away, and it should be
|
|
40
|
-
* said plainly: the sandbox runs code the model wrote, and a key it can read
|
|
41
|
-
* is a key it can send. `keys: false`, or `network: none`, is the answer for a
|
|
42
|
-
* project where that is not an acceptable trade.
|
|
43
|
-
*/
|
|
44
|
-
function forwarded() {
|
|
45
|
-
const secrets = [];
|
|
46
|
-
const env = {};
|
|
47
|
-
const mounts = [];
|
|
48
|
-
for (const cred of credentials()) {
|
|
49
|
-
if (cred.holds === 'file') {
|
|
50
|
-
const at = `${KEY_MOUNT}/${basename(cred.value)}`;
|
|
51
|
-
mounts.push({ host: cred.value, at, readOnly: true });
|
|
52
|
-
env[cred.env] = at;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
secrets.push(cred.env);
|
|
56
|
-
}
|
|
57
|
-
// Not secrets, and useless without one: a service account says which
|
|
58
|
-
// project it belongs to but never which region to call, and a gateway key
|
|
59
|
-
// says nothing of where the gateway is.
|
|
60
|
-
for (const name of ['GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION', GATEWAY_URL_ENV]) {
|
|
61
|
-
const value = process.env[name];
|
|
62
|
-
if (value) {
|
|
63
|
-
env[name] = value;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return { secrets, env, mounts };
|
|
67
|
-
}
|
|
68
|
-
export function buildSandbox(opts) {
|
|
69
|
-
// An explicit --image is an answer, so there is nothing left to build.
|
|
70
|
-
const build = opts.image ? undefined : resolveBuild(opts.root, opts.config.sandbox);
|
|
71
|
-
const base = {
|
|
72
|
-
...(opts.config.sandbox ?? {}),
|
|
73
|
-
...((opts.image ?? build?.tag) ? { image: opts.image ?? build?.tag } : {}),
|
|
74
|
-
...(opts.keys === false ? { keys: false } : {}),
|
|
75
|
-
...(opts.persist === false ? { persist: false } : {}),
|
|
76
|
-
};
|
|
77
|
-
const home = join(opts.session.data, 'sandbox', 'home');
|
|
78
|
-
const mounts = [{ host: home, at: HOME }, ...(opts.mounts ?? [])];
|
|
79
|
-
const keys = base.keys === false ? undefined : forwarded();
|
|
80
|
-
if (keys) {
|
|
81
|
-
mounts.push(...keys.mounts);
|
|
82
|
-
}
|
|
83
|
-
// Skills and assets are mounted read-only, and a python script run from a
|
|
84
|
-
// read-only directory fails on writing its own `__pycache__` — a confusing
|
|
85
|
-
// error about a file nobody asked for.
|
|
86
|
-
// `ZENERA_AGENT` tells a `zen` run by `run_command` that no person is there.
|
|
87
|
-
const extra = { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(keys?.env ?? {}) };
|
|
88
|
-
const spec = toSpec(base, extra, secretsOf(keys?.secrets, opts.env));
|
|
89
|
-
const agents = {};
|
|
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({
|
|
105
|
-
...spec,
|
|
106
|
-
agents,
|
|
107
|
-
root: opts.workspace,
|
|
108
|
-
key: opts.session.id,
|
|
109
|
-
readOnly: opts.readOnly,
|
|
110
|
-
mounts,
|
|
111
|
-
});
|
|
112
|
-
return { pool, spec, image: spec.image ?? DEFAULT_SANDBOX_IMAGE, build, home, mounts };
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* An agent's overrides on the project's block. `image` and `build` answer the
|
|
116
|
-
* same question, so naming either one drops the other: a plain spread would
|
|
117
|
-
* leave an agent's `image` sitting next to the project's `build`, and the
|
|
118
|
-
* schema forbids exactly that combination when it is written down.
|
|
119
|
-
*/
|
|
120
|
-
function merge(base, agent) {
|
|
121
|
-
const merged = { ...base, ...agent };
|
|
122
|
-
if (agent.image !== undefined) {
|
|
123
|
-
delete merged.build;
|
|
124
|
-
}
|
|
125
|
-
if (agent.build !== undefined) {
|
|
126
|
-
delete merged.image;
|
|
127
|
-
}
|
|
128
|
-
return merged;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* The two sources of forwarded names, as one list: the credentials the keyring
|
|
132
|
-
* selected, and whatever the project's `.env` declares. Deduped, because the
|
|
133
|
-
* same variable named twice is the same `--env` twice.
|
|
134
|
-
*
|
|
135
|
-
* `undefined` in means keys are withheld, and the file goes with them.
|
|
136
|
-
*/
|
|
137
|
-
function secretsOf(secrets, env) {
|
|
138
|
-
if (!secrets) {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
return [...new Set([...secrets, ...(env ?? [])])];
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Config names a variable; this reads it. A name that is not set on this host
|
|
145
|
-
* is simply not forwarded — an empty string in the container is a different
|
|
146
|
-
* thing from an absent one, and tools test for absence.
|
|
147
|
-
*
|
|
148
|
-
* A name that has already been answered with a value drops off the forwarded
|
|
149
|
-
* list: `--env NAME=value` and `--env NAME` are the same variable twice, and
|
|
150
|
-
* the one that carries a value is the one that was resolved on purpose. That
|
|
151
|
-
* matters for `GOOGLE_APPLICATION_CREDENTIALS`, which is rewritten here to
|
|
152
|
-
* where the key file was mounted — a `.env` naming it must not put the host's
|
|
153
|
-
* path back, since that path does not exist inside the container.
|
|
154
|
-
*/
|
|
155
|
-
function toSpec(config, extra, secrets) {
|
|
156
|
-
const env = { ...extra };
|
|
157
|
-
for (const name of config.env ?? []) {
|
|
158
|
-
const value = process.env[name];
|
|
159
|
-
if (value !== undefined && value !== '') {
|
|
160
|
-
env[name] = value;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
image: config.image,
|
|
165
|
-
cpus: config.cpus,
|
|
166
|
-
memory: config.memory,
|
|
167
|
-
network: config.network,
|
|
168
|
-
workdir: config.workdir,
|
|
169
|
-
timeout: config.timeout,
|
|
170
|
-
hardening: config.hardening,
|
|
171
|
-
user: config.user,
|
|
172
|
-
persist: config.persist,
|
|
173
|
-
env,
|
|
174
|
-
secrets: secrets?.filter((name) => !(name in env)),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Whether anything in this project can reach a shell.
|
|
179
|
-
*
|
|
180
|
-
* Read off the *resolved* tool lists rather than off the config's selectors,
|
|
181
|
-
* because `sandbox:*`, `*` and a bare name all mean the same thing by the time
|
|
182
|
-
* the loader is done, and only one of those three is greppable.
|
|
183
|
-
*/
|
|
184
|
-
export function usesSandbox(project) {
|
|
185
|
-
return project.agents.some((a) => a.tools.some((t) => t.group === SANDBOX_GROUP));
|
|
186
|
-
}
|
|
187
2
|
/**
|
|
188
3
|
* Images this process has already settled. A batch opens the same project
|
|
189
4
|
* dozens of times at once, and without this each one shells out to podman and
|
|
190
5
|
* the concurrent copies race each other to pull the very same image.
|
|
191
6
|
*/
|
|
192
7
|
const READY = new Set();
|
|
193
|
-
/**
|
|
194
|
-
* Asked before the first turn rather than at the first tool call, so a missing
|
|
195
|
-
* container engine costs nothing instead of costing a round trip and half a
|
|
196
|
-
* plan. A project that never shells out never gets here at all.
|
|
197
|
-
*/
|
|
8
|
+
/** The engine's preflight on a terminal: podman is offered for install and started if needed. */
|
|
198
9
|
export async function preflight(setup, yes) {
|
|
199
|
-
mkdirSync(setup.home, { recursive: true });
|
|
200
10
|
if (READY.has(setup.image)) {
|
|
201
11
|
return;
|
|
202
12
|
}
|
|
@@ -211,13 +21,4 @@ export async function preflight(setup, yes) {
|
|
|
211
21
|
await assertLocks(1, { reuses: setup.pool.for().name });
|
|
212
22
|
READY.add(setup.image);
|
|
213
23
|
}
|
|
214
|
-
/** Best-effort teardown: losing a container must never lose a run. */
|
|
215
|
-
export async function teardown(pool) {
|
|
216
|
-
try {
|
|
217
|
-
await pool.dispose();
|
|
218
|
-
}
|
|
219
|
-
catch (err) {
|
|
220
|
-
warn(`could not clean up the sandbox: ${err instanceof Error ? err.message : String(err)}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
24
|
//# sourceMappingURL=sandbox.js.map
|
package/dist/term.d.ts
CHANGED
|
@@ -1,25 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
readonly ok: 0;
|
|
3
|
-
readonly failed: 1;
|
|
4
|
-
readonly usage: 2;
|
|
5
|
-
readonly invalid: 3;
|
|
6
|
-
readonly credentials: 4;
|
|
7
|
-
readonly sandbox: 5;
|
|
8
|
-
};
|
|
9
|
-
export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
|
|
10
|
-
/**
|
|
11
|
-
* An error that already knows what it means. Anything else reaching the top is
|
|
12
|
-
* a bug and exits `failed`, which is the distinction a script needs: a wrong
|
|
13
|
-
* invocation and a wrong answer are not the same failure.
|
|
14
|
-
*/
|
|
15
|
-
export declare class CliError extends Error {
|
|
16
|
-
readonly code: ExitCode;
|
|
17
|
-
readonly hint?: string;
|
|
18
|
-
constructor(message: string, code?: ExitCode, hint?: string);
|
|
19
|
-
}
|
|
20
|
-
export declare const usageError: (m: string, hint?: string) => CliError;
|
|
21
|
-
export declare const invalidError: (m: string, hint?: string) => CliError;
|
|
22
|
-
export declare const credentialError: (m: string, hint?: string) => CliError;
|
|
1
|
+
export { EngineError as CliError, credentialError, EXIT, invalidError, usageError, type ExitCode, } from '@zenera/engine';
|
|
23
2
|
export type Appearance = 'dark' | 'light';
|
|
24
3
|
/** One grey per background. The only 256-colour indices in the CLI. */
|
|
25
4
|
export declare const CHROME: Record<Appearance, number>;
|
package/dist/term.js
CHANGED
|
@@ -11,32 +11,9 @@ import { styleText } from 'node:util';
|
|
|
11
11
|
// `styleText` no-ops when the stream is not a tty and honours NO_COLOR itself,
|
|
12
12
|
// so there is no flag to thread through and no piped output to garble.
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
usage: 2,
|
|
18
|
-
invalid: 3,
|
|
19
|
-
credentials: 4,
|
|
20
|
-
sandbox: 5,
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* An error that already knows what it means. Anything else reaching the top is
|
|
24
|
-
* a bug and exits `failed`, which is the distinction a script needs: a wrong
|
|
25
|
-
* invocation and a wrong answer are not the same failure.
|
|
26
|
-
*/
|
|
27
|
-
export class CliError extends Error {
|
|
28
|
-
code;
|
|
29
|
-
hint;
|
|
30
|
-
constructor(message, code = EXIT.failed, hint) {
|
|
31
|
-
super(message);
|
|
32
|
-
this.name = 'CliError';
|
|
33
|
-
this.code = code;
|
|
34
|
-
this.hint = hint;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
export const usageError = (m, hint) => new CliError(m, EXIT.usage, hint);
|
|
38
|
-
export const invalidError = (m, hint) => new CliError(m, EXIT.invalid, hint);
|
|
39
|
-
export const credentialError = (m, hint) => new CliError(m, EXIT.credentials, hint);
|
|
14
|
+
// The engine raises these too, so `instanceof CliError` holds for its errors.
|
|
15
|
+
import { usageError } from '@zenera/engine';
|
|
16
|
+
export { EngineError as CliError, credentialError, EXIT, invalidError, usageError, } from '@zenera/engine';
|
|
40
17
|
/** One grey per background. The only 256-colour indices in the CLI. */
|
|
41
18
|
export const CHROME = { dark: 245, light: 242 };
|
|
42
19
|
let sniffed;
|
package/dist/tui/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { TextInput } from '@inkjs/ui';
|
|
3
|
+
import { display } from '@zenera/engine';
|
|
3
4
|
import { addUsage, isCheckpoint, turns, zeroUsage, } from '@zenera/neo';
|
|
4
5
|
import { Box, Static, Text, useApp, useInput, useStdout } from 'ink';
|
|
5
6
|
import { pathToFileURL } from 'node:url';
|
|
@@ -7,7 +8,6 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'rea
|
|
|
7
8
|
import * as Engine from "../engine.js";
|
|
8
9
|
import { History } from "../history.js";
|
|
9
10
|
import { format } from "../narrate.js";
|
|
10
|
-
import { display } from "../session.js";
|
|
11
11
|
import { CliError } from "../term.js";
|
|
12
12
|
import { hostUrls, hyperlink, hyperlinks, linkBlock, mountsOf } from "./links.js";
|
|
13
13
|
import { resolveTheme, THEMES } from "./theme.js";
|
package/dist/tui/models.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { Provider } from '@zenera/engine';
|
|
1
2
|
import { type Balance, type Catalog, type CatalogEntry, type Role } from '../catalog.ts';
|
|
2
|
-
import type { Provider } from '../keys.ts';
|
|
3
3
|
export interface ProviderRow {
|
|
4
4
|
provider: Provider;
|
|
5
5
|
/** where its credential came from — `environment`, `keyring` — or '' for none */
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import { type DeclaredRole, type KeyStore, type Liveness } from '@zenera/engine';
|
|
1
2
|
import { type AnyTool, type ProjectConfig, type Runner } from '@zenera/neo';
|
|
2
|
-
import { type DeclaredRole } from './audit.ts';
|
|
3
|
-
import { type KeyStore, type Liveness } from './keys.ts';
|
|
4
3
|
/**
|
|
5
4
|
* `error` — the project will not load, or will not run.
|
|
6
5
|
* `warning` — it loads, and something about it is probably not what was meant.
|
package/dist/validate.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { SHAPES, auditModels, credentialFor, envNames, form, resolveBuild, } from '@zenera/engine';
|
|
1
2
|
import { EXA_GROUP, FileSkillProvider, SANDBOX_MOUNT, Sandbox, exaTools, fileTools, frontmatter, projectRegistry, readProjectConfig, sandboxTools, selectTools, toList, } from '@zenera/neo';
|
|
2
3
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, } from 'node:fs';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { auditModels, credentialFor } from "./audit.js";
|
|
6
|
-
import { resolveBuild } from "./image.js";
|
|
7
|
-
import { SHAPES, envNames, form } from "./keys.js";
|
|
8
6
|
import { probeModels } from "./liveness.js";
|
|
9
7
|
import { BuildError, ensurePodmanReady } from "./podman.js";
|
|
10
8
|
import { ownVersion, staleShared } from "./scaffold.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.37",
|
|
4
4
|
"description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -54,8 +54,9 @@
|
|
|
54
54
|
"@google/genai": "^2.30.0",
|
|
55
55
|
"@inkjs/ui": "^2.0.0",
|
|
56
56
|
"@openrouter/sdk": "^1.4.25",
|
|
57
|
-
"@zenera/
|
|
58
|
-
"@zenera/
|
|
57
|
+
"@zenera/engine": "^1.1.37",
|
|
58
|
+
"@zenera/neo": "^1.1.37",
|
|
59
|
+
"@zenera/view": "^1.1.37",
|
|
59
60
|
"google-auth-library": "^10.0.0",
|
|
60
61
|
"ink": "^7.1.1",
|
|
61
62
|
"openai": "^6.0.0",
|
package/dist/audit.d.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { type ModelRequirement } from '@zenera/neo';
|
|
2
|
-
import { type KeyOwner, type KeyStore } from './keys.ts';
|
|
3
|
-
/** Which key of the config named a reference: `model:`/`models:` or `embedding:`/`embeddings:`. */
|
|
4
|
-
export type DeclaredRole = 'model' | 'embedding';
|
|
5
|
-
export interface ModelIssue {
|
|
6
|
-
/** the model as the config names it: an alias, or the ref itself */
|
|
7
|
-
name: string;
|
|
8
|
-
/** what the config declared it for */
|
|
9
|
-
role: DeclaredRole;
|
|
10
|
-
/** the provider it resolves to */
|
|
11
|
-
provider: string;
|
|
12
|
-
/** the variable that would carry the credential */
|
|
13
|
-
env: string;
|
|
14
|
-
/** `missing` — nothing to authenticate with. `dead` — rejected when checked.
|
|
15
|
-
* `blocked` — accepted, and then refused by the account behind it. */
|
|
16
|
-
reason: 'missing' | 'dead' | 'blocked';
|
|
17
|
-
/** the provider's own words, when it was the one to say no */
|
|
18
|
-
detail?: string;
|
|
19
|
-
/** what to do about a refusal that a new key would not fix */
|
|
20
|
-
fix?: string;
|
|
21
|
-
/** the keyring provider the fix names, when the kind is one of them */
|
|
22
|
-
add?: KeyOwner;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Whether a requirement is actually met, and under which variable.
|
|
26
|
-
*
|
|
27
|
-
* `satisfied` is the library's answer and it is about api keys, which is the
|
|
28
|
-
* wrong question for Vertex: that kind is `keyOptional` because it can
|
|
29
|
-
* authenticate from a service-account file instead. So both of its variables
|
|
30
|
-
* are looked for, along with the file `gcloud` leaves behind.
|
|
31
|
-
*
|
|
32
|
-
* The variable named back is the one that is actually set, because a report
|
|
33
|
-
* that named `VERTEX_API_KEY` while the audit silently checked
|
|
34
|
-
* `GOOGLE_APPLICATION_CREDENTIALS` would be two answers to one question, and
|
|
35
|
-
* the wrong one is the one people would act on. With nothing set there is no
|
|
36
|
-
* such answer, so it names the form the provider is usually reached by.
|
|
37
|
-
*/
|
|
38
|
-
export declare function credentialFor(need: ModelRequirement): {
|
|
39
|
-
env: string;
|
|
40
|
-
present: boolean;
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* Reports the models a run could not reach. Best effort by design: a config
|
|
44
|
-
* that will not parse, or that names a provider that does not exist, is the
|
|
45
|
-
* loader's to report — precisely, and with the offending key named. Guessing
|
|
46
|
-
* at it here would only produce a worse version of the same message.
|
|
47
|
-
*
|
|
48
|
-
* Call *after* `KeyStore.materialize()`, so the keyring's keys are as visible
|
|
49
|
-
* here as they will be to the library.
|
|
50
|
-
*/
|
|
51
|
-
export declare function auditModels(projectDir: string, store: KeyStore): ModelIssue[];
|
|
52
|
-
/**
|
|
53
|
-
* One line, and the command that fixes it. A warning nobody can act on is
|
|
54
|
-
* noise, so the fix is part of the sentence rather than something to go and
|
|
55
|
-
* look up.
|
|
56
|
-
*/
|
|
57
|
-
export declare function describeIssue(issue: ModelIssue): string;
|
|
58
|
-
//# sourceMappingURL=audit.d.ts.map
|
package/dist/audit.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { projectRegistry, readProjectConfig, } from '@zenera/neo';
|
|
2
|
-
import { envNames, form, gcloudAdc, isOwner, isProvider, SHAPES, } from "./keys.js";
|
|
3
|
-
import { bold, dim } from "./term.js";
|
|
4
|
-
/**
|
|
5
|
-
* Every model the project names, keyed by how one would refer to it.
|
|
6
|
-
*
|
|
7
|
-
* The `models:` table comes first so a declared alias keeps its own name in the
|
|
8
|
-
* report; a `model:` that names one collapses onto it rather than appearing
|
|
9
|
-
* twice, and a `model:` that names no alias is a shorthand standing for itself.
|
|
10
|
-
*
|
|
11
|
-
* Embeddings are walked the same way and kept in their own list: an alias may
|
|
12
|
-
* legitimately appear under both keys meaning two different things, and they
|
|
13
|
-
* are two credentials to check either way.
|
|
14
|
-
*/
|
|
15
|
-
function declared(config) {
|
|
16
|
-
// The schema widens `reasoningEffort` to `string` on purpose, which is the
|
|
17
|
-
// one thing keeping a config's model spec from being a `ModelSpec`. The
|
|
18
|
-
// loader hands the same cast to `models.model()` for the same reason.
|
|
19
|
-
const models = new Map(Object.entries(config.models ?? {}));
|
|
20
|
-
const add = (into, ref) => {
|
|
21
|
-
if (ref && !into.has(ref)) {
|
|
22
|
-
into.set(ref, ref);
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
add(models, config.model);
|
|
26
|
-
for (const agent of config.agents) {
|
|
27
|
-
add(models, agent.model);
|
|
28
|
-
}
|
|
29
|
-
const embeddings = new Map(Object.entries(config.embeddings ?? {}));
|
|
30
|
-
add(embeddings, config.embedding);
|
|
31
|
-
return new Map([
|
|
32
|
-
['model', models],
|
|
33
|
-
['embedding', embeddings],
|
|
34
|
-
]);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Whether a requirement is actually met, and under which variable.
|
|
38
|
-
*
|
|
39
|
-
* `satisfied` is the library's answer and it is about api keys, which is the
|
|
40
|
-
* wrong question for Vertex: that kind is `keyOptional` because it can
|
|
41
|
-
* authenticate from a service-account file instead. So both of its variables
|
|
42
|
-
* are looked for, along with the file `gcloud` leaves behind.
|
|
43
|
-
*
|
|
44
|
-
* The variable named back is the one that is actually set, because a report
|
|
45
|
-
* that named `VERTEX_API_KEY` while the audit silently checked
|
|
46
|
-
* `GOOGLE_APPLICATION_CREDENTIALS` would be two answers to one question, and
|
|
47
|
-
* the wrong one is the one people would act on. With nothing set there is no
|
|
48
|
-
* such answer, so it names the form the provider is usually reached by.
|
|
49
|
-
*/
|
|
50
|
-
export function credentialFor(need) {
|
|
51
|
-
if (need.gateway) {
|
|
52
|
-
return { env: need.apiKeyEnv, present: need.satisfied };
|
|
53
|
-
}
|
|
54
|
-
const provider = isProvider(need.kind) ? need.kind : undefined;
|
|
55
|
-
// A provider with a file form can authenticate without an api key, which is
|
|
56
|
-
// the only thing `satisfied` knows how to look for.
|
|
57
|
-
if (provider && SHAPES[provider].forms.some((f) => f.holds === 'file')) {
|
|
58
|
-
const set = envNames(provider).find((name) => process.env[name]);
|
|
59
|
-
return { env: set ?? form(provider).env, present: Boolean(set) || Boolean(gcloudAdc()) };
|
|
60
|
-
}
|
|
61
|
-
return { env: need.apiKeyEnv, present: need.satisfied };
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Reports the models a run could not reach. Best effort by design: a config
|
|
65
|
-
* that will not parse, or that names a provider that does not exist, is the
|
|
66
|
-
* loader's to report — precisely, and with the offending key named. Guessing
|
|
67
|
-
* at it here would only produce a worse version of the same message.
|
|
68
|
-
*
|
|
69
|
-
* Call *after* `KeyStore.materialize()`, so the keyring's keys are as visible
|
|
70
|
-
* here as they will be to the library.
|
|
71
|
-
*/
|
|
72
|
-
export function auditModels(projectDir, store) {
|
|
73
|
-
let config;
|
|
74
|
-
try {
|
|
75
|
-
config = readProjectConfig(projectDir).config;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
const registry = projectRegistry(config);
|
|
81
|
-
const issues = [];
|
|
82
|
-
for (const [role, refs] of declared(config)) {
|
|
83
|
-
for (const [name, ref] of refs) {
|
|
84
|
-
let need;
|
|
85
|
-
try {
|
|
86
|
-
need = registry.requirement(ref);
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
const { env, present } = credentialFor(need);
|
|
92
|
-
const provider = need.gateway && isOwner(need.gateway)
|
|
93
|
-
? need.gateway
|
|
94
|
-
: isProvider(need.kind)
|
|
95
|
-
? need.kind
|
|
96
|
-
: undefined;
|
|
97
|
-
if (!present) {
|
|
98
|
-
issues.push({
|
|
99
|
-
name,
|
|
100
|
-
role,
|
|
101
|
-
provider: need.provider,
|
|
102
|
-
env,
|
|
103
|
-
reason: 'missing',
|
|
104
|
-
add: provider,
|
|
105
|
-
});
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
// A key that the provider itself rejected last time it was asked.
|
|
109
|
-
// Said as a warning rather than an error because a key can be
|
|
110
|
-
// reinstated between the check and the run, and a stale verdict must
|
|
111
|
-
// not be the thing that stops a run from being attempted.
|
|
112
|
-
const check = provider ? store.active(provider)?.check : undefined;
|
|
113
|
-
if (check?.state === 'dead' || check?.state === 'blocked') {
|
|
114
|
-
issues.push({
|
|
115
|
-
name,
|
|
116
|
-
role,
|
|
117
|
-
provider: need.provider,
|
|
118
|
-
env,
|
|
119
|
-
reason: check.state,
|
|
120
|
-
detail: check.detail,
|
|
121
|
-
fix: check.fix,
|
|
122
|
-
add: provider,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return issues;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* One line, and the command that fixes it. A warning nobody can act on is
|
|
131
|
-
* noise, so the fix is part of the sentence rather than something to go and
|
|
132
|
-
* look up.
|
|
133
|
-
*/
|
|
134
|
-
export function describeIssue(issue) {
|
|
135
|
-
const what = `${bold(issue.name)} (${issue.provider}${issue.role === 'embedding' ? ', embedding' : ''})`;
|
|
136
|
-
if (issue.reason === 'missing') {
|
|
137
|
-
const fix = issue.add ? `zen key add ${issue.add}` : `set ${issue.env}`;
|
|
138
|
-
return `${what} has no credential — ${issue.env} is not set; ${dim(fix)}`;
|
|
139
|
-
}
|
|
140
|
-
const why = issue.detail ? `: ${issue.detail}` : '';
|
|
141
|
-
if (issue.reason === 'blocked') {
|
|
142
|
-
const list = `zen models ls --${issue.role === 'embedding' ? 'embeddings' : 'chat'}`;
|
|
143
|
-
return `${what} authenticated and was then refused${why} — ${dim(issue.fix ?? list)}`;
|
|
144
|
-
}
|
|
145
|
-
const fix = `zen key check ${issue.add ?? ''}`.trim();
|
|
146
|
-
return `${what} was rejected when last checked${why} — ${dim(fix)}`;
|
|
147
|
-
}
|
|
148
|
-
//# sourceMappingURL=audit.js.map
|
package/dist/env.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/** The file, relative to the project directory. */
|
|
2
|
-
export declare const ENV_FILE = ".env";
|
|
3
|
-
export interface ProjectEnv {
|
|
4
|
-
/** the file it was read from */
|
|
5
|
-
path: string;
|
|
6
|
-
/** every name the file declares, whether or not its value won */
|
|
7
|
-
names: string[];
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Reads `<dir>/.env` into `process.env`, without overwriting anything already
|
|
11
|
-
* set, and returns the names it found. A project with no such file is not a
|
|
12
|
-
* problem to report — most have none — so the answer is simply `undefined`.
|
|
13
|
-
*
|
|
14
|
-
* The names are returned in full, including the ones the environment already
|
|
15
|
-
* answered: the variable is in play either way, and the sandbox forwards by
|
|
16
|
-
* name, so what it needs to know is which names matter and not which of the
|
|
17
|
-
* two sources supplied them.
|
|
18
|
-
*/
|
|
19
|
-
export declare function loadProjectEnv(dir: string): ProjectEnv | undefined;
|
|
20
|
-
//# sourceMappingURL=env.d.ts.map
|
package/dist/env.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
-
import { parseEnv } from 'node:util';
|
|
4
|
-
import { FILE_ENVS } from "./keys.js";
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// The project's `.env`
|
|
7
|
-
//
|
|
8
|
-
// A project is a directory, and some of what it needs to run is particular to
|
|
9
|
-
// it: the token for the service it talks to, the base url of the environment
|
|
10
|
-
// it talks to it in. The keyring answers "what can this machine pay for"; this
|
|
11
|
-
// answers "what does this project need", and the two are different questions
|
|
12
|
-
// often enough to be worth two places.
|
|
13
|
-
//
|
|
14
|
-
// Precedence is the same one everything else here uses — the real environment
|
|
15
|
-
// always wins. What is already set in the shell is left alone, this file fills
|
|
16
|
-
// the gaps, and the keyring fills whatever is still missing after that. So a
|
|
17
|
-
// variable exported for one command still overrides the file, which is the
|
|
18
|
-
// behaviour anybody typing `FOO=bar zen run` is expecting.
|
|
19
|
-
//
|
|
20
|
-
// The names, not the values, are what leaves this module: they are what the
|
|
21
|
-
// sandbox forwards, and forwarding by name is what keeps a secret out of an
|
|
22
|
-
// argv. See `forwarded()` in sandbox.ts for the rest of that story.
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
/** The file, relative to the project directory. */
|
|
25
|
-
export const ENV_FILE = '.env';
|
|
26
|
-
/**
|
|
27
|
-
* Reads `<dir>/.env` into `process.env`, without overwriting anything already
|
|
28
|
-
* set, and returns the names it found. A project with no such file is not a
|
|
29
|
-
* problem to report — most have none — so the answer is simply `undefined`.
|
|
30
|
-
*
|
|
31
|
-
* The names are returned in full, including the ones the environment already
|
|
32
|
-
* answered: the variable is in play either way, and the sandbox forwards by
|
|
33
|
-
* name, so what it needs to know is which names matter and not which of the
|
|
34
|
-
* two sources supplied them.
|
|
35
|
-
*/
|
|
36
|
-
export function loadProjectEnv(dir) {
|
|
37
|
-
const path = join(dir, ENV_FILE);
|
|
38
|
-
if (!existsSync(path)) {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
const names = [];
|
|
42
|
-
for (const [name, value] of Object.entries(parseEnv(readFileSync(path, 'utf8')))) {
|
|
43
|
-
if (value === undefined) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
names.push(name);
|
|
47
|
-
// An empty string is not an answer: tools test for absence, and a
|
|
48
|
-
// commented-out line someone half-filled in should not shadow a key
|
|
49
|
-
// the keyring has.
|
|
50
|
-
if (!process.env[name]) {
|
|
51
|
-
process.env[name] = located(dir, name, value);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return { path, names };
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* A service-account file is normally kept beside the `.env` that names it, so
|
|
58
|
-
* a relative path there means relative to the *project* — not to wherever the
|
|
59
|
-
* command happened to be run, which is what both the Google SDK and podman's
|
|
60
|
-
* bind mount would otherwise make of it.
|
|
61
|
-
*/
|
|
62
|
-
function located(dir, name, value) {
|
|
63
|
-
return FILE_ENVS.includes(name) && !isAbsolute(value) ? resolve(dir, value) : value;
|
|
64
|
-
}
|
|
65
|
-
//# sourceMappingURL=env.js.map
|