@zenera/cli 1.1.35 → 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.
Files changed (62) hide show
  1. package/dist/archive.js +1 -1
  2. package/dist/batch.js +2 -3
  3. package/dist/cache.js +1 -1
  4. package/dist/candidates.js +1 -1
  5. package/dist/catalog.d.ts +1 -1
  6. package/dist/catalog.js +1 -1
  7. package/dist/commands/cache.js +1 -1
  8. package/dist/commands/check.js +1 -3
  9. package/dist/commands/export.js +1 -3
  10. package/dist/commands/import.js +1 -2
  11. package/dist/commands/init.js +1 -4
  12. package/dist/commands/inspect.d.ts +1 -1
  13. package/dist/commands/inspect.js +1 -4
  14. package/dist/commands/key.js +1 -2
  15. package/dist/commands/list.js +4 -5
  16. package/dist/commands/memory.js +1 -2
  17. package/dist/commands/meta.js +2 -4
  18. package/dist/commands/models.d.ts +1 -1
  19. package/dist/commands/models.js +1 -2
  20. package/dist/commands/run.js +2 -2
  21. package/dist/commands/sandbox.js +1 -2
  22. package/dist/engine.d.ts +3 -90
  23. package/dist/engine.js +16 -302
  24. package/dist/history.js +1 -1
  25. package/dist/lib.d.ts +1 -2
  26. package/dist/lib.js +1 -2
  27. package/dist/liveness.d.ts +1 -1
  28. package/dist/liveness.js +1 -1
  29. package/dist/meta/host.d.ts +1 -2
  30. package/dist/meta/host.js +1 -2
  31. package/dist/meta/launch.js +3 -1
  32. package/dist/modelref.d.ts +1 -1
  33. package/dist/podman.d.ts +1 -1
  34. package/dist/podman.js +1 -1
  35. package/dist/resolve.d.ts +2 -2
  36. package/dist/resolve.js +2 -3
  37. package/dist/resources.js +1 -1
  38. package/dist/sandbox.d.ts +2 -60
  39. package/dist/sandbox.js +1 -200
  40. package/dist/term.d.ts +1 -22
  41. package/dist/term.js +3 -26
  42. package/dist/tui/app.js +1 -1
  43. package/dist/tui/models.d.ts +1 -1
  44. package/dist/validate.d.ts +1 -2
  45. package/dist/validate.js +1 -3
  46. package/package.json +4 -3
  47. package/dist/audit.d.ts +0 -58
  48. package/dist/audit.js +0 -148
  49. package/dist/env.d.ts +0 -20
  50. package/dist/env.js +0 -65
  51. package/dist/home.d.ts +0 -35
  52. package/dist/home.js +0 -112
  53. package/dist/ids.d.ts +0 -12
  54. package/dist/ids.js +0 -44
  55. package/dist/image.d.ts +0 -16
  56. package/dist/image.js +0 -85
  57. package/dist/keys.d.ts +0 -269
  58. package/dist/keys.js +0 -624
  59. package/dist/projects.d.ts +0 -91
  60. package/dist/projects.js +0 -303
  61. package/dist/session.d.ts +0 -112
  62. 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 declare const EXIT: {
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
- export const EXIT = {
15
- ok: 0,
16
- failed: 1,
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";
@@ -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 */
@@ -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.35",
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/neo": "^1.1.35",
58
- "@zenera/view": "^1.1.35",
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