@zenera/cli 1.1.2 → 1.1.3

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 (50) hide show
  1. package/README.md +88 -11
  2. package/dist/audit.d.ts +8 -6
  3. package/dist/audit.js +14 -22
  4. package/dist/commands/check.js +34 -7
  5. package/dist/commands/init.js +71 -11
  6. package/dist/commands/key.js +126 -36
  7. package/dist/commands/models.js +3 -3
  8. package/dist/commands/open.js +2 -2
  9. package/dist/commands/run.js +3 -0
  10. package/dist/engine.d.ts +2 -0
  11. package/dist/engine.js +1 -0
  12. package/dist/keys.d.ts +95 -12
  13. package/dist/keys.js +175 -34
  14. package/dist/lib.d.ts +1 -1
  15. package/dist/lib.js +1 -1
  16. package/dist/liveness.d.ts +16 -6
  17. package/dist/liveness.js +74 -23
  18. package/dist/sandbox.d.ts +2 -0
  19. package/dist/sandbox.js +58 -7
  20. package/dist/scaffold.d.ts +21 -21
  21. package/dist/scaffold.js +132 -204
  22. package/dist/validate.js +3 -3
  23. package/package.json +2 -18
  24. package/templates/{.github → editor/.github}/copilot-instructions.md +7 -6
  25. package/templates/editor/.github/skills/api-schema-index/SKILL.md +292 -0
  26. package/templates/editor/.github/skills/zen-cli/SKILL.md +74 -0
  27. package/templates/editor/.github/skills/zen-cli/references/check.md +92 -0
  28. package/templates/editor/.github/skills/zen-cli/references/faker.md +111 -0
  29. package/templates/editor/.github/skills/zen-cli/references/frame.md +119 -0
  30. package/templates/editor/.github/skills/zen-cli/references/inspect.md +61 -0
  31. package/templates/editor/.github/skills/zen-cli/references/keys.md +114 -0
  32. package/templates/editor/.github/skills/zen-cli/references/projects.md +99 -0
  33. package/templates/editor/.github/skills/zen-cli/references/rag.md +159 -0
  34. package/templates/editor/.github/skills/zen-cli/references/run.md +104 -0
  35. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +91 -0
  36. package/templates/editor/.vscode/settings.json +6 -0
  37. package/templates/parts/exa.yaml.tmpl +5 -0
  38. package/templates/parts/model.yaml.tmpl +4 -0
  39. package/templates/parts/models.yaml.tmpl +10 -0
  40. package/templates/project/INSTRUCTIONS.md +7 -0
  41. package/templates/project/SPECIFICATION.md +6 -0
  42. package/templates/project/agents/prompts/default.md +15 -0
  43. package/templates/project/agents.yaml.tmpl +44 -0
  44. package/templates/project/assets/README.md +12 -0
  45. package/templates/project/gitignore +9 -0
  46. package/templates/{sandbox → project/sandbox}/Dockerfile +2 -0
  47. package/templates/.github/skills/zen-cli/SKILL.md +0 -110
  48. /package/templates/{.github → editor/.github}/prompts/new-agent.prompt.md +0 -0
  49. /package/templates/{.github → editor/.github}/prompts/new-skill.prompt.md +0 -0
  50. /package/templates/{.github → editor/.github}/prompts/review-project.prompt.md +0 -0
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { parse } from "../args.js";
3
3
  import { ensureHome } from "../home.js";
4
- import { assertNotEmpty, describe, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
4
+ import { ambient, ambientId, assertNotEmpty, describe, envNames, envOf, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
5
5
  import { probe, probeAll } from "../liveness.js";
6
6
  import { ago, ask, askSecret, bold, confirm, credentialError, cyan, dim, green, isInteractive, json, note, progress, readStdin, red, table, usageError, write, writeAll, yellow, } from "../term.js";
7
7
  const USAGE = 'zen key <ls|add|use|check|rm|show|env> [ref] [options]';
@@ -16,35 +16,59 @@ const MARK = {
16
16
  function state(entry) {
17
17
  return entry.check ? MARK[entry.check.state] : dim('unchecked');
18
18
  }
19
- function rows(store) {
19
+ /**
20
+ * An ambient credential borrowed into the shape the rest of this file works
21
+ * in. It is not in the store and never will be: `store.find` misses it, so
22
+ * `store.record` drops its check and nothing is written to disk. That is the
23
+ * intent — it can be listed and probed, not chosen or forgotten.
24
+ */
25
+ function asEntry(cred) {
26
+ return {
27
+ provider: cred.provider,
28
+ name: cred.env ? `$${cred.env}` : 'adc',
29
+ holds: cred.holds,
30
+ value: cred.value,
31
+ env: cred.env,
32
+ addedAt: new Date().toISOString(),
33
+ };
34
+ }
35
+ function rows(store, borrowed) {
20
36
  const out = [
21
37
  [bold(''), bold('KEY'), bold('VALUE'), bold('STATE'), bold('CHECKED')],
22
38
  ];
23
39
  for (const provider of OWNERS) {
24
40
  for (const entry of store.for(provider)) {
25
- const shadowed = Boolean(process.env[SHAPES[provider].env]);
41
+ const shadow = envNames(provider).find((name) => process.env[name]);
26
42
  out.push([
27
43
  store.isActive(entry) ? green('*') : ' ',
28
44
  keyId(entry),
29
45
  dim(describe(store, entry)),
30
46
  state(entry),
31
47
  dim(entry.check ? ago(entry.check.at) : '—') +
32
- (shadowed && store.isActive(entry)
33
- ? yellow(` shadowed by $${SHAPES[provider].env}`)
34
- : ''),
48
+ (shadow && store.isActive(entry) ? yellow(` shadowed by $${shadow}`) : ''),
49
+ ]);
50
+ }
51
+ for (const [cred, entry] of borrowed.filter(([c]) => c.provider === provider)) {
52
+ out.push([
53
+ dim('~'),
54
+ dim(ambientId(cred)),
55
+ dim(describe(store, entry)),
56
+ state(entry),
57
+ dim(cred.env ? 'from the environment' : 'from gcloud'),
35
58
  ]);
36
59
  }
37
60
  }
38
61
  return table(out);
39
62
  }
40
63
  /**
41
- * Checking is a network round trip per key, so it says which one it is waiting
42
- * on. Silence for ten seconds is indistinguishable from a hang.
64
+ * Checking is a network round trip per key, so it says which ones have come
65
+ * back. Silence for ten seconds is indistinguishable from a hang.
43
66
  */
44
67
  async function checkAll(ctx, store, targets) {
45
68
  const bar = ctx.json ? undefined : progress();
69
+ bar?.update(dim(`checking ${targets.length} key${targets.length === 1 ? '' : 's'} …`));
46
70
  try {
47
- return await probeAll(store, targets, (entry, index, total) => bar?.update(dim(`checking ${keyId(entry)} … ${index + 1}/${total}`)));
71
+ return await probeAll(store, targets, (entry, done, total) => bar?.update(dim(`checked ${keyId(entry)} … ${done}/${total}`)));
48
72
  }
49
73
  finally {
50
74
  bar?.done();
@@ -53,29 +77,47 @@ async function checkAll(ctx, store, targets) {
53
77
  const ls = async (ctx, args) => {
54
78
  const { values } = parse(args, { check: { type: 'boolean' } }, USAGE);
55
79
  const store = await KeyStore.open();
80
+ const borrowed = ambient(store).map((cred) => [cred, asEntry(cred)]);
56
81
  if (values.check) {
57
- const checks = await checkAll(ctx, store, store.entries);
82
+ // Ambient credentials are checked alongside the stored ones and, unlike
83
+ // them, forgotten again: there is nowhere to write the result, and a
84
+ // variable that changes between runs would make a cached one a lie.
85
+ const checks = await checkAll(ctx, store, [
86
+ ...store.entries,
87
+ ...borrowed.map(([, entry]) => entry),
88
+ ]);
58
89
  for (const [entry, check] of checks) {
59
90
  store.record(entry, check);
91
+ entry.check = check;
60
92
  }
61
93
  store.save();
62
94
  }
63
95
  if (ctx.json) {
64
- json(store.entries.map((e) => ({
65
- ...e,
66
- value: e.holds === 'file' ? store.fileOf(e) : mask(e.value),
67
- active: store.isActive(e),
68
- env: SHAPES[e.provider].env,
69
- })));
96
+ json({
97
+ keys: store.entries.map((e) => ({
98
+ ...e,
99
+ value: e.holds === 'file' ? store.fileOf(e) : mask(e.value),
100
+ active: store.isActive(e),
101
+ env: envOf(e),
102
+ })),
103
+ ambient: borrowed.map(([cred, entry]) => ({
104
+ provider: cred.provider,
105
+ id: ambientId(cred),
106
+ env: cred.env ?? null,
107
+ holds: cred.holds,
108
+ value: cred.holds === 'file' ? cred.value : mask(cred.value),
109
+ check: entry.check,
110
+ })),
111
+ });
70
112
  return;
71
113
  }
72
- if (store.entries.length === 0) {
114
+ if (store.entries.length === 0 && borrowed.length === 0) {
73
115
  note('the keyring is empty');
74
116
  note(dim('add one: zen key add openai'));
75
117
  return;
76
118
  }
77
- writeAll(rows(store));
78
- const missing = OWNERS.filter((p) => !store.active(p) && !process.env[SHAPES[p].env]);
119
+ writeAll(rows(store, borrowed));
120
+ const missing = OWNERS.filter((p) => !store.active(p) && !borrowed.some(([c]) => c.provider === p));
79
121
  if (missing.length) {
80
122
  note('');
81
123
  note(dim(`no key for: ${missing.join(', ')}`));
@@ -87,7 +129,12 @@ const ls = async (ctx, args) => {
87
129
  * Piped stdin or an echo-off prompt are the only two ways in.
88
130
  */
89
131
  const add = async (ctx, args) => {
90
- const { values, positionals } = parse(args, { name: { type: 'string' }, 'no-check': { type: 'boolean' } }, 'zen key add <provider>[/name] [--name <name>] [--no-check]');
132
+ const { values, positionals } = parse(args, {
133
+ name: { type: 'string' },
134
+ 'no-check': { type: 'boolean' },
135
+ project: { type: 'string' },
136
+ location: { type: 'string' },
137
+ }, 'zen key add <provider>[/name] [--name <name>] [--project <id>] [--location <region>] [--no-check]');
91
138
  const ref = positionals[0];
92
139
  if (!ref) {
93
140
  throw usageError('which provider?', `one of: ${OWNERS.join(', ')}`);
@@ -96,6 +143,9 @@ const add = async (ctx, args) => {
96
143
  const provider = parsed.provider;
97
144
  const name = values.name ?? parsed.name ?? 'default';
98
145
  const shape = SHAPES[provider];
146
+ if ((values.project || values.location) && provider !== 'vertex') {
147
+ throw usageError(`--project and --location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
148
+ }
99
149
  ensureHome();
100
150
  const store = await KeyStore.open();
101
151
  if (store.find(provider, name) && isInteractive()) {
@@ -103,14 +153,20 @@ const add = async (ctx, args) => {
103
153
  throw usageError('cancelled');
104
154
  }
105
155
  }
106
- const raw = (await readStdin()) ?? (await promptFor(shape.holds, shape.label, shape.where));
156
+ const raw = (await readStdin()) ?? (await promptFor(shape));
107
157
  if (!raw) {
108
158
  throw usageError('no value given');
109
159
  }
110
- if (shape.holds === 'file' && !existsSync(raw)) {
160
+ // A provider with one form can say up front that a path is wrong. One with
161
+ // two cannot: a value that is not a path is not a mistake there, it is the
162
+ // other kind of credential, and `add` decides which by looking.
163
+ if (shape.forms.length === 1 && shape.forms[0].holds === 'file' && !existsSync(raw)) {
111
164
  throw usageError(`no such file: ${raw}`);
112
165
  }
113
- const entry = store.add(provider, name, raw);
166
+ const entry = store.add(provider, name, raw, {
167
+ project: values.project,
168
+ location: values.location,
169
+ });
114
170
  // Verified before it is trusted, but stored either way: a key that cannot
115
171
  // be checked right now — offline, behind a proxy — is not a key that is
116
172
  // wrong, and refusing to save it would make `zen key add` fail on a plane.
@@ -128,20 +184,37 @@ const add = async (ctx, args) => {
128
184
  if (ctx.json) {
129
185
  json({
130
186
  key: keyId(entry),
131
- env: shape.env,
187
+ env: envOf(entry),
132
188
  active: store.isActive(entry),
133
189
  check: entry.check,
134
190
  });
135
191
  return;
136
192
  }
137
- note(`${green('added')} ${bold(keyId(entry))} ${dim(describe(store, entry))} ${state(entry)}`);
138
- if (process.env[shape.env]) {
139
- note(yellow(`$${shape.env} is set and will win over this`));
193
+ note(`${green('added')} ${bold(keyId(entry))} ${dim(describe(store, entry))} ` +
194
+ `${dim(`→ $${envOf(entry)}`)} ${state(entry)}`);
195
+ if (process.env[envOf(entry)]) {
196
+ note(yellow(`$${envOf(entry)} is set and will win over this`));
197
+ }
198
+ if (provider === 'vertex' && entry.holds === 'file' && !entry.project) {
199
+ note(dim('no --project given; the project_id inside the file will be used'));
140
200
  }
141
201
  };
142
- async function promptFor(holds, label, where) {
143
- note(dim(`${label} ${where}`));
144
- return holds === 'file'
202
+ /**
203
+ * A provider with two forms cannot ask for one of them. It asks with the echo
204
+ * off, because one of the two answers is a secret and there is no way to know
205
+ * which is coming until it arrives.
206
+ */
207
+ async function promptFor(shape) {
208
+ const [first, ...rest] = shape.forms;
209
+ if (rest.length) {
210
+ note(dim(`${shape.label} — ${first.where},`));
211
+ for (const form of rest) {
212
+ note(dim(` or ${form.where}`));
213
+ }
214
+ return askSecret('Paste the key, or a path to the file:');
215
+ }
216
+ note(dim(`${shape.label} — ${first.where}`));
217
+ return first.holds === 'file'
145
218
  ? ask('Path to the credentials file:')
146
219
  : askSecret('Paste the key (it will not be shown):');
147
220
  }
@@ -159,7 +232,7 @@ const use = async (ctx, args) => {
159
232
  const entry = store.use(provider, name);
160
233
  store.save();
161
234
  if (ctx.json) {
162
- json({ key: keyId(entry), env: SHAPES[provider].env });
235
+ json({ key: keyId(entry), env: envOf(entry) });
163
236
  return;
164
237
  }
165
238
  note(`${green('using')} ${bold(keyId(entry))} for ${SHAPES[provider].label}`);
@@ -242,7 +315,7 @@ const show = async (ctx, args) => {
242
315
  if (ctx.json) {
243
316
  json({
244
317
  key: keyId(entry),
245
- env: SHAPES[entry.provider].env,
318
+ env: envOf(entry),
246
319
  value,
247
320
  revealed: Boolean(values.reveal),
248
321
  });
@@ -256,8 +329,10 @@ const show = async (ctx, args) => {
256
329
  }
257
330
  writeAll(table([
258
331
  [dim('key'), keyId(entry)],
259
- [dim('env'), SHAPES[entry.provider].env],
332
+ [dim('env'), envOf(entry)],
260
333
  [dim('value'), value],
334
+ ...(entry.project ? [[dim('project'), entry.project]] : []),
335
+ ...(entry.location ? [[dim('location'), entry.location]] : []),
261
336
  [dim('state'), state(entry)],
262
337
  [dim('added'), ago(entry.addedAt)],
263
338
  ]));
@@ -278,8 +353,18 @@ const env = async (ctx, args) => {
278
353
  const vars = {};
279
354
  for (const provider of only ?? OWNERS) {
280
355
  const entry = store.active(provider);
281
- if (entry) {
282
- vars[SHAPES[provider].env] = store.reveal(entry);
356
+ if (!entry) {
357
+ continue;
358
+ }
359
+ vars[envOf(entry)] = store.reveal(entry);
360
+ // A service account says which project it belongs to but not which
361
+ // region to call, and a shell that has the file and not the region is
362
+ // still a shell that cannot reach a model.
363
+ if (entry.project) {
364
+ vars.GOOGLE_CLOUD_PROJECT = entry.project;
365
+ }
366
+ if (entry.location) {
367
+ vars.GOOGLE_CLOUD_LOCATION = entry.location;
283
368
  }
284
369
  }
285
370
  if (ctx.json) {
@@ -306,11 +391,16 @@ export const key = {
306
391
  usage: USAGE,
307
392
  details: [
308
393
  'Keys live in ~/.zenera/neo/keys.json (0600) and are materialised into',
309
- 'the environment before a run. A real environment variable always wins.',
394
+ 'the environment before a run. A real environment variable always wins,',
395
+ 'and is listed as ~ so it is clear where a working provider comes from.',
310
396
  '',
311
397
  'Model providers: openai, anthropic, google, vertex, openrouter.',
312
398
  'Services the tools call: exa.',
313
399
  '',
400
+ 'Vertex takes either shape: a service-account JSON file, which wants',
401
+ '--project and --location too, or an express-mode API key, which wants',
402
+ 'neither. Which one you gave is read off the value.',
403
+ '',
314
404
  ' zen key ls [--check] Everything stored, and its state.',
315
405
  ' zen key add <provider>[/name] Read a key from stdin, or ask for it.',
316
406
  ' zen key use <provider>/<name> Choose which one a run uses.',
@@ -1,6 +1,6 @@
1
1
  import { loadProject, readProjectConfig } from '@zenera/neo';
2
2
  import { parse } from "../args.js";
3
- import { KeyStore, PROVIDERS, SHAPES } from "../keys.js";
3
+ import { envNames, form, KeyStore, PROVIDERS } from "../keys.js";
4
4
  import { project as resolveProject } from "../resolve.js";
5
5
  import { bold, credentialError, cyan, dim, green, invalidError, json, note, red, table, writeAll, yellow, } from "../term.js";
6
6
  import { availableTools } from "../validate.js";
@@ -22,7 +22,7 @@ export const models = {
22
22
  // Asked before materialising, because materialising is exactly what
23
23
  // erases the difference between "the environment had it" and "the
24
24
  // keyring supplied it".
25
- const fromEnv = new Set(PROVIDERS.filter((p) => process.env[SHAPES[p].env]));
25
+ const fromEnv = new Set(PROVIDERS.filter((p) => envNames(p).some((name) => process.env[name])));
26
26
  store.materialize();
27
27
  let project;
28
28
  try {
@@ -64,7 +64,7 @@ export const models = {
64
64
  }));
65
65
  const credentials = PROVIDERS.map((p) => ({
66
66
  provider: p,
67
- env: SHAPES[p].env,
67
+ env: envNames(p).find((name) => process.env[name]) ?? form(p).env,
68
68
  source: fromEnv.has(p)
69
69
  ? 'environment'
70
70
  : store.active(p)
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
4
4
  import { delimiter, join, resolve, sep } from 'node:path';
5
5
  import { one, parse } from "../args.js";
6
6
  import { project as resolveProject } from "../resolve.js";
7
- import { copilotInstructions, editorSettings } from "../scaffold.js";
7
+ import { editorFiles } from "../scaffold.js";
8
8
  import { CliError, cyan, dim, EXIT, json, note, usageError } from "../term.js";
9
9
  const USAGE = 'zen open [project] [--editor <cmd>] [--wait]';
10
10
  /**
@@ -42,7 +42,7 @@ export const open = {
42
42
  // opened on, and a project may predate either of them — or the version
43
43
  // this `zen` writes. They are ours, so they are written fresh before
44
44
  // the window is there to read them.
45
- const written = [editorSettings(dir), ...copilotInstructions(dir)];
45
+ const written = editorFiles(dir);
46
46
  if (ctx.json) {
47
47
  json({
48
48
  name: found.name,
@@ -19,6 +19,7 @@ export const run = {
19
19
  ' --model <ref> Override the default model.',
20
20
  ' --image <ref> Override the sandbox image commands run in.',
21
21
  ' --read-only Give the agent no way to write.',
22
+ ' --no-keys Keep the API keys out of the sandbox.',
22
23
  ' --quiet Answer only; no narration.',
23
24
  ' --plain One shot, even on a terminal.',
24
25
  ' --theme <dark|light> Force the palette. Detected otherwise; $ZENERA_THEME.',
@@ -43,6 +44,7 @@ export const run = {
43
44
  workspace: { type: 'string' },
44
45
  model: { type: 'string' },
45
46
  image: { type: 'string' },
47
+ 'no-keys': { type: 'boolean' },
46
48
  'read-only': { type: 'boolean' },
47
49
  yes: { type: 'boolean' },
48
50
  quiet: { type: 'boolean' },
@@ -84,6 +86,7 @@ export const run = {
84
86
  readOnly: values['read-only'],
85
87
  model: values.model,
86
88
  image: values.image,
89
+ keys: values['no-keys'] ? false : undefined,
87
90
  yes: values.yes || ctx.json,
88
91
  });
89
92
  try {
package/dist/engine.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface EngineOptions {
10
10
  model?: string;
11
11
  /** sandbox image override — `--image` */
12
12
  image?: string;
13
+ /** whether credentials reach the sandbox — `--no-keys` sets this false */
14
+ keys?: boolean;
13
15
  /** answer the sandbox's install question without asking — `--yes` */
14
16
  yes?: boolean;
15
17
  }
package/dist/engine.js CHANGED
@@ -46,6 +46,7 @@ export async function open(opts) {
46
46
  workspace,
47
47
  readOnly: opts.readOnly,
48
48
  image: opts.image,
49
+ keys: opts.keys,
49
50
  mounts,
50
51
  });
51
52
  project = await loadProject(opts.project.dir, {
package/dist/keys.d.ts CHANGED
@@ -17,24 +17,49 @@ export type Service = (typeof SERVICES)[number];
17
17
  /** Anything the keyring can hold a credential for. */
18
18
  export declare const OWNERS: readonly ["openai", "anthropic", "google", "vertex", "openrouter", "exa"];
19
19
  export type KeyOwner = Provider | Service;
20
- interface ProviderShape {
21
- /** whether this is somewhere a model lives, or something a tool calls */
22
- kind: 'model' | 'service';
23
- /** environment variable the library reads */
24
- env: string;
20
+ /**
21
+ * One way a credential can arrive.
22
+ *
23
+ * A provider that accepts two accepts two of these, and they agree about
24
+ * nothing: not the variable, not whether the value is the secret or a path to
25
+ * it, not where you go to get one. So the alternative is a whole form rather
26
+ * than a wider `holds`.
27
+ */
28
+ export interface CredentialForm {
25
29
  /** what the value is: a secret string, or a path to a credentials file */
26
30
  holds: 'secret' | 'file';
27
- label: string;
31
+ /** environment variable the library reads */
32
+ env: string;
28
33
  /** where to get one, printed when there is none */
29
34
  where: string;
30
35
  }
36
+ interface ProviderShape {
37
+ /** whether this is somewhere a model lives, or something a tool calls */
38
+ kind: 'model' | 'service';
39
+ label: string;
40
+ /** the ways in, first being the one this provider is usually reached by */
41
+ forms: [CredentialForm, ...CredentialForm[]];
42
+ }
31
43
  /**
32
- * Vertex is the odd one. The GenAI SDK resolves Application Default
33
- * Credentials itself, so what is stored is a service-account *file* and what is
34
- * exported is a path not a key. Pretending otherwise would mean inventing a
35
- * credential shape Google does not have.
44
+ * Vertex is the odd one, and it is odd twice.
45
+ *
46
+ * Its usual credential is not a key at all: the GenAI SDK resolves Application
47
+ * Default Credentials itself, so what is stored is a service-account *file* and
48
+ * what is exported is a path. But it also accepts an express-mode api key,
49
+ * which is an ordinary secret under an entirely different variable. The two are
50
+ * alternatives — express mode addresses no project — so which form a credential
51
+ * is gets decided per entry rather than per provider.
36
52
  */
37
53
  export declare const SHAPES: Record<KeyOwner, ProviderShape>;
54
+ /**
55
+ * The form a provider is usually reached by — for the questions that have to
56
+ * have one answer, like which variable to name when nothing is set yet.
57
+ */
58
+ export declare function form(provider: KeyOwner): CredentialForm;
59
+ /** Every variable a provider's credential could arrive in, usual one first. */
60
+ export declare function envNames(provider: KeyOwner): string[];
61
+ /** The variable this particular credential occupies. */
62
+ export declare function envOf(entry: Pick<KeyEntry, 'provider' | 'holds' | 'env'>): string;
38
63
  export declare function isProvider(name: string): name is Provider;
39
64
  export declare function isOwner(name: string): name is KeyOwner;
40
65
  export declare function assertOwner(name: string): KeyOwner;
@@ -52,6 +77,11 @@ export interface KeyEntry {
52
77
  holds: 'secret' | 'file';
53
78
  /** the secret itself, or a path relative to the key directory */
54
79
  value: string;
80
+ /** the variable it is exported as; absent on entries written before providers had two */
81
+ env?: string;
82
+ /** vertex, file-shaped only: what the library would otherwise have to be told twice */
83
+ project?: string;
84
+ location?: string;
55
85
  addedAt: string;
56
86
  check?: KeyCheck;
57
87
  }
@@ -65,6 +95,13 @@ export declare function parseRef(ref: string): {
65
95
  provider: KeyOwner;
66
96
  name?: string;
67
97
  };
98
+ /** What a credential cannot say about itself. */
99
+ export interface KeyMeta {
100
+ /** GCP project id, for a Vertex service account */
101
+ project?: string;
102
+ /** GCP region, or `global` */
103
+ location?: string;
104
+ }
68
105
  export declare class KeyStore {
69
106
  #private;
70
107
  private constructor();
@@ -81,13 +118,21 @@ export declare class KeyStore {
81
118
  * directory: the point of a store is that the credential survives the
82
119
  * original being moved, renamed or cleaned up, and a stored path that
83
120
  * silently stops resolving is worse than no store at all.
121
+ *
122
+ * Which form a value is, when the provider accepts two, is read off the
123
+ * value: a path that is there is a credentials file, and anything else is a
124
+ * secret. Asking would be a flag to get wrong, and a service-account key
125
+ * and an api key are not mistakable for one another.
84
126
  */
85
- add(provider: KeyOwner, name: string, raw: string): KeyEntry;
127
+ add(provider: KeyOwner, name: string, raw: string, meta?: KeyMeta): KeyEntry;
86
128
  remove(provider: KeyOwner, name: string): boolean;
87
129
  use(provider: KeyOwner, name: string): KeyEntry;
88
130
  record(entry: KeyEntry, check: KeyCheck): void;
89
131
  save(): void;
90
- /** Absolute path behind a file-shaped entry. */
132
+ /**
133
+ * Absolute path behind a file-shaped entry. Stored entries name a file in
134
+ * the key directory; an ambient one already knows where it is.
135
+ */
91
136
  fileOf(entry: KeyEntry): string;
92
137
  /** The plaintext an entry stands for — the only way out of the store. */
93
138
  reveal(entry: KeyEntry): string;
@@ -110,6 +155,44 @@ export declare class KeyStore {
110
155
  */
111
156
  export declare function mask(secret: string): string;
112
157
  export declare function describe(store: KeyStore, entry: KeyEntry): string;
158
+ /**
159
+ * A credential the keyring does not hold but the libraries will nonetheless
160
+ * find: a variable already in the environment, or the file `gcloud auth
161
+ * application-default login` writes.
162
+ *
163
+ * These have to be listed, because they are the reason a provider works when
164
+ * `zen key ls` says there is nothing for it — and the reason one keeps working
165
+ * after its entry is removed.
166
+ */
167
+ export interface Ambient {
168
+ provider: KeyOwner;
169
+ /** the variable it arrived in; absent when it was found where the SDK looks */
170
+ env?: string;
171
+ holds: 'secret' | 'file';
172
+ value: string;
173
+ }
174
+ /** How an ambient credential is named on the command line: it has no key name. */
175
+ export declare function ambientId(cred: Ambient): string;
176
+ /**
177
+ * Where `gcloud auth application-default login` leaves its credentials. The
178
+ * GenAI SDK reads this without being told to, so it counts even though nothing
179
+ * in the environment mentions it.
180
+ */
181
+ export declare function gcloudAdc(): string | undefined;
182
+ export declare function ambient(store: KeyStore, only?: KeyOwner[]): Ambient[];
183
+ /**
184
+ * Every credential variable this process is carrying, whatever put it there.
185
+ *
186
+ * Read off the environment rather than off the store, and deliberately: by the
187
+ * time anyone asks, `materialize()` has already run, so the environment is the
188
+ * union of the keyring and whatever the shell brought — which is exactly the
189
+ * set of credentials the run is actually using.
190
+ */
191
+ export declare function credentials(): {
192
+ env: string;
193
+ holds: 'secret' | 'file';
194
+ value: string;
195
+ }[];
113
196
  /**
114
197
  * Called before a run: says plainly that there is no way to reach a model,
115
198
  * rather than letting the SDK raise it three frames deeper as a 401.