1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
//! The credential store: API keys drep holds on the user's behalf.
//!
//! `drep.toml` is a repository file. It names an endpoint, a model and a
//! protocol, all of which are shareable, and it is meant to survive being
//! committed - which is why `api_key = "${VAR}"` names an environment variable
//! rather than holding a secret. That indirection is the right answer for CI,
//! where the key arrives as a secret and nobody is at a keyboard, and the wrong
//! answer for a person setting drep up on their laptop: it makes the first-run
//! experience "now go and export something into the right shell profile".
//!
//! So keys live here instead, once per machine, outside any repository:
//!
//! ```text
//! ~/.config/drep/auth.toml (macOS: ~/Library/Application Support/dev.slb350.drep)
//! ```
//!
//! ## Keyed by endpoint, not by provider name
//!
//! A key authenticates a *host*, so the endpoint is what it belongs to. Keying
//! by a preset name instead would mean a config that named no preset - a custom
//! endpoint, or one edited by hand after `drep init` - could not find its own
//! credential, and two presets pointed at the same host would each need their
//! own copy of one key.
//!
//! The endpoint is normalised before use (see [`normalise`]) so a trailing
//! slash or a difference in case does not hide a key from the config that
//! stored it.
//!
//! There is deliberately no `load_default`/`save_default` pair. Both were thin
//! wrappers over `default_path()`, which reads the environment - so nothing
//! could test them without `std::env::set_var`, and the mutation gate found
//! them undetectable. Every caller resolves the path once, at its own entry
//! point, and passes it down; that is also what keeps tests off the real store.
//!
//! ## Resolution order
//!
//! [`resolve`] fills in what `drep.toml` left unset, in this order:
//!
//! 1. an explicit `api_key` - a literal, or a `${VAR}` the loader has already
//! substituted;
//! 2. `api_key_command`, an argv drep runs to mint one;
//! 3. a key held here for the same endpoint;
//! 4. nothing, which `LlmClient::new` turns into `not-needed`.
//!
//! An explicit value in the file always wins. A user who writes
//! `api_key = "${OPENROUTER_API_KEY}"` has said where the key comes from, and
//! silently preferring a stored one would make the file lie about what the run
//! used. The command sits above the store for the same reason: a file naming a
//! command has not left the question unanswered.
use BTreeMap;
use ;
use ;
use Error;
use crateConfig;
pub use KeyCommandError;
/// Where a provider's key came from, for [`doctor`](crate::cli::doctor) to report.
///
/// The point of naming the source is that "it works on my machine" and "it works
/// in CI" are different configurations, and the difference is invisible in
/// `drep.toml` once a stored key exists.
///
/// The variant order is the resolution order.
/// Keys held for this machine, keyed by normalised endpoint.
///
/// `Serialize`/`Deserialize` drive the on-disk TOML directly; there is no
/// separate wire type because the file is drep's own and has one shape.
/// Hand-written so a key cannot reach a log.
///
/// The same reasoning as `LlmConfig` and `LlmClient`: a derived `Debug` prints
/// every value, so one `{:?}` anywhere would emit every credential the user has.
/// The endpoints are printed because they are not secret and they are the useful
/// half when debugging.
/// What can go wrong reading or writing the store.
/// The environment variable that relocates the store.
pub const PATH_VAR: &str = "DREP_AUTH_PATH";
/// The store location: [`PATH_VAR`] if set, else the platform's config dir.
///
/// The platform path comes from `directories::ProjectDirs` with the same triple
/// `Cache::default_root` uses, so drep's two user-level directories are siblings
/// under one application identity rather than two unrelated paths.
///
/// The override exists because there is otherwise **no way to run drep against a
/// scratch store**. `directories` follows each platform's own convention rather
/// than the XDG variables, so on macOS `XDG_CONFIG_HOME` is ignored entirely and
/// a command run to try something out writes into the real store - which is how
/// a test key ended up in one. It also serves the ordinary case of keeping
/// credentials somewhere deliberate, such as a mounted volume.
/// [`default_path`] with the override supplied rather than read.
///
/// Split out so the override can be tested without writing to the process
/// environment. `std::env::set_var` is `unsafe` in edition 2024 because another
/// thread reading the environment concurrently is a data race, and `cargo test`
/// runs tests on several threads - a "single-threaded test process" safety
/// comment would simply have been untrue.
/// Why a credential cannot be used, whatever produced it.
///
/// Two variants because they send the reader to different fixes: nothing arrived
/// at all, or something arrived that cannot be sent.
pub
/// Trim a candidate credential and refuse one that cannot become a header value.
///
/// One definition, because this is a safety property, and `crate::http`'s module
/// doc already records what happens to a safety property written twice: "written
/// once and forgotten once". It was written twice - here for a key pasted at the
/// prompt, and in `auth::command::run` for one a helper printed - and the two
/// copies had already drifted cosmetically. The minted path is the one that never
/// passes through [`AuthStore::set`], so a defect class added at the prompt would
/// silently not have applied to the credential a gateway helper produces, which is
/// the only path whose value nobody ever sees.
///
/// The wording stays with each caller: this returns [`CredentialDefect`] rather
/// than an `AuthError`, so the store can name the endpoint and the helper can name
/// the program. That is the mechanism-shared, classification-private split
/// `crate::http` documents for the same pair of callers.
///
/// **Both ends** are trimmed. Every helper that prints a token prints a newline
/// after it, and a leading space is one `printf ' %s'` or one `cut` field away;
/// trimming only the tail accepted `" sk-live-..."`, which passes the
/// control-character guard and is then stripped again by the transport, so the
/// value sent was not the value checked and the endpoint answered 401.
///
/// A control character is refused because every use of a credential is an HTTP
/// header value, which cannot carry one. Rejecting it here turns a paste or a
/// helper that picked up a stray escape sequence into a message the user can act
/// on, rather than a transport failure on the first file of the first push.
pub
/// Canonical form of an endpoint for use as a store key.
///
/// Lowercased and stripped of trailing slashes. `https://API.Z.AI/v1/` and
/// `https://api.z.ai/v1` are the same host and the same credential, and a store
/// that disagreed would report "no key" for a config that had just stored one -
/// a failure with no visible cause, since both spellings look right.
///
/// Deliberately no more than that: the path is significant (`/v1` and
/// `/anthropic/v1` are different APIs on the same host, and can carry different
/// keys), so nothing beyond the trailing slash is trimmed.
/// Lowercase everything before the first `/` and leave the rest alone.
/// Fill in keys the config left unset, and report where each one came from.
///
/// Returns one [`KeySource`] per entry in `config.llm`, positionally - including
/// the disabled ones, so the position in the returned slice is the position in the
/// file. No production caller reads it: `check` discards it, and `doctor` calls
/// [`source_of`] against the raw TOML tree instead, because `config::load` fails
/// on an unset `${VAR}` and that is exactly the config `doctor` is most useful on.
/// It is retained for the tests, which assert the precedence rule per entry
/// against the one function that decides it, and the positional shape is what lets
/// them name an entry by the line the user wrote.
///
/// **Disabled entries are skipped**, matching every other pass over the provider
/// list: `${VAR}` expansion and field validation already leave a parked entry
/// alone, and looking a key up for one would report a missing credential for a
/// provider that is never contacted. For an `api_key_command` that also means no
/// subprocess: some helpers are rate-limited and some prompt for a fingerprint,
/// so spending a real credential call on a provider drep will not contact is
/// worse than the missing-key report it avoids.
///
/// This is the one pass where a credential command runs, so each entry's command
/// runs exactly once per process however many files the run reviews. A
/// short-lived credential re-minted per file would fail per file, which the
/// chain's demotion logic reads as an endpoint problem. There is deliberately no
/// disk cache and no TTL behind that: drep is a short-lived process, so a
/// credential written to disk buys nothing and adds a file worth stealing.
///
/// Once per process for every *enabled* entry, whether or not the chain reaches
/// it. Deferring to first use would put credential resolution inside the request
/// path, and "a broken credential is fatal rather than a provider drep quietly
/// asks instead" holds precisely because resolution happens before the chain
/// exists. An unset `${VAR}` in the same position is equally fatal, one layer up,
/// in `ConfigError::EnvVarUnset`. `enabled = false` is the control for a fallback
/// whose helper should not be spent.
pub async
/// Run one entry's `api_key_command` and discard the credential.
///
/// For `doctor`, whose contract is "what will actually run here": it has to
/// really invoke the helper, because a helper that no longer authenticates is
/// precisely what `api_key_command` exists to make visible. Returning `()`
/// rather than the key is what makes printing it impossible at the call site,
/// instead of a rule the reporting code has to remember.
pub async
/// What one `[[llm]]` entry declares about its own credential.
///
/// Named fields rather than positional arguments, following `ExplicitFields`:
/// `check` fills these from a loaded `Config` and `doctor` from the raw TOML
/// tree, and two `Option<&str>` plus two `bool` in a call is a swap waiting to
/// happen that the compiler would not catch.
/// Where a provider's key will come from, given what its config names.
///
/// The precedence rule itself, in one place. [`resolve`] applies it to a loaded
/// `Config`; `doctor` applies it to the *raw* TOML tree, which it has to read
/// separately so a `${VAR}` prints as itself rather than being swallowed by the
/// variable-not-set error. Two readers of one rule is the shape
/// `config::env_var_refs_in` already exists to prevent: doctor once carried a
/// narrower copy of that scanner and reported a config as fine that `check`
/// refused to load.
/// Create `dir` if it is missing, narrowing it to 0700 only when drep made it.
///
/// Only a directory drep creates is narrowed. `DREP_AUTH_PATH` can name any
/// path, so chmodding whatever happens to be its parent would let
/// `/etc/drep.toml` turn `/etc` into 0700 - breaking the system to protect one
/// file. An existing directory is the user's, and the store file's own 0600 is
/// what actually guards the key.
///
/// Shared with the model-quirks cache, which sits in the same directory: a
/// second copy of this rule that only called `create_dir_all` would leave the
/// credential store's directory world-readable whenever the cache happened to
/// be written first.
pub
/// Write `body` to `path`, creating it readable only by its owner.
///
/// `File::create` plus a later `chmod` leaves the key in a 0644 file for the
/// duration of the write. `NamedTempFile` exclusively creates a random 0600
/// sibling, so there is no readable window and no predictable name for a
/// planted symlink. The mode is re-applied before publication as an explicit
/// invariant rather than depending on the temporary-file crate's default.
///
/// One function with the `cfg` around the mode call, rather than two whole
/// implementations: a `#[cfg(not(unix))]` twin is not compiled here, so
/// mutating it changes nothing and the mutation gate reports an undetectable
/// survivor on every run. Windows has no mode bits, and `directories` puts the
/// file under the user's own roaming profile there.
/// Directory in which an atomic replacement must be created.
/// Narrow `path` to `mode` on Unix. A no-op elsewhere.
///
/// Windows has no mode bits and `directories` puts the file under the user's
/// roaming profile, which is already user-scoped; failing the save there would
/// refuse to store a key for no gain.