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
//! The machine-level site policy layer above `drep.toml`.
//!
//! `drep.toml` is a repository file, and `drep init` adds it to `.gitignore` by
//! default - so it is per-developer scratch, and a control written there is
//! opt-in. Opt-in means off for the person who most needs it. This layer sits
//! above it: a repository checkout can tighten what the site allows, never
//! loosen it.
//!
//! Three rules carry the whole module.
//!
//! - **A missing file is no policy, and is not an error.** Most machines have
//! none. [`load`] returns `Option<SiteConfig>` so that is structural rather
//! than a convention a caller could get backwards.
//! - **A file that exists and cannot be loaded is fatal.** A policy that
//! silently fails to load is worse than no policy at all, because the
//! unconstrained run that follows reports as compliance. Every
//! [`SiteConfigError`] says so in its own message.
//! - **The file is not per-user, and the process it constrains cannot move it.**
//! The location is a system path rather than the `ProjectDirs` directory holding
//! `auth.toml` and the response cache, because a policy file the policed
//! developer can edit without privilege is not a policy file. [`PATH_VAR`] names
//! the file only on a machine where none is installed, for the same reason: an
//! override that could displace an installed policy would be one `export` away
//! from switching it off. There is no `${VAR}` expansion in the file either - a
//! policy that takes its values from the environment of the process it
//! constrains constrains nothing.
//!
//! The layering is applied by the caller, after [`super::load`] returns, which
//! is what keeps [`super::ConfigError`] a statement about `drep.toml` alone.
use BTreeSet;
use ;
use StreamExt;
use Deserialize;
use Error;
use Config;
/// The environment variable that relocates the policy file.
pub const PATH_VAR: &str = "DREP_SITE_CONFIG";
/// How many repository roots resolve at once.
///
/// Four, matching `check::deterministic`'s `TOOL_PROCESS_CONCURRENCY`, because it
/// bounds the same resource: short-lived child processes on a developer machine
/// that is probably also compiling. Its own constant rather than a shared one,
/// since the two fan-outs spawn different programs and would be tuned apart.
const ROOT_RESOLUTION_CONCURRENCY: usize = 4;
/// The machine-wide policy path, per platform.
///
/// Deliberately not under `directories::ProjectDirs`, where `auth.toml` and the
/// cache live: those are the user's own state and belong in the user's own
/// directory, while this file is the thing the user is not supposed to be able
/// to edit. The system path uses the plain `drep` name rather than drep's
/// `dev.slb350.drep` identity triple because an administrator installs it by
/// hand, and a reverse-DNS directory under `/etc` is a path nobody can type.
///
/// A `const` with two `cfg` arms rather than a `cfg`'d pair of functions: a
/// function body that is not compiled on this platform is a mutation the test
/// suite can never detect, which `auth::restrict` records. A const is not a
/// mutation target at all.
const MACHINE_PATH: &str = "/Library/Application Support/drep/site.toml";
const MACHINE_PATH: &str = "/etc/drep/site.toml";
/// The policy path: the machine-wide file, or [`PATH_VAR`] when there is none.
/// The machine-wide path this platform installs policy at.
///
/// A function so `default_path` and its test read one thing rather than each
/// restating the literal, and so the two `cfg` arms above have a single reader.
/// [`default_path`] with the override and the machine path supplied rather than
/// read.
///
/// Split out for the reason `auth::path_from` is: `std::env::set_var` is
/// `unsafe` in edition 2024 because another thread reading the environment is a
/// data race, and `cargo test` is multi-threaded, so the override has to be
/// suppliable to be testable at all. The machine path joins it because a test
/// cannot write to `/etc` either.
///
/// **The override cannot displace an installed policy.** If a file exists at
/// `machine`, that file is the policy and the variable is ignored. Otherwise the
/// whole layer is one `export` away from off: the developer `refuse_markers`
/// constrains points the variable at an empty file, the marker list is empty, the
/// probe short-circuits before git is even spawned, and the run sends the
/// repository's source and exits 0. `ConfigError::SiteOnlyField` refuses that
/// field in `drep.toml` because a refusal a developer can delete is not one, and a
/// per-process override is a way to delete it. The precedence is not silent
/// either: `drep doctor` names the file in effect, so an administrator who moved
/// the policy and left the old one behind can see which one answered.
///
/// So the variable names the policy on a machine that installed none - an
/// installation that keeps the file elsewhere, and every test in this suite, which
/// must not read whatever this machine happens to hold. An **empty** value falls
/// back to the machine path rather than being honoured as a file that does not
/// exist, because `DREP_SITE_CONFIG=` quietly switching enforcement off is the
/// same defect as a policy file that fails to load.
///
/// Presence is [`std::fs::symlink_metadata`], matching the marker probe: a name
/// someone deliberately placed is a name claiming to be the policy, and following
/// the link would let a dangling symlink hand the decision back to the
/// environment.
///
/// Infallible, unlike `auth::path_from`, because no `ProjectDirs` lookup is
/// involved and a system path exists on every platform drep ships to.
/// What the site allows, for every repository on this machine.
///
/// `deny_unknown_fields` is the whole of the "no providers, no credentials"
/// rule: an `[[llm]]`, an `endpoint` or an `api_key` in this file is an unknown
/// key and is rejected, so there is no separate rejection list to drift from the
/// field list. It is also what makes a misspelled policy key loud rather than a
/// silent no-op.
///
/// This is the one config type that may derive `Debug`. `LlmConfig`, `AuthStore`
/// and `LlmClient` hand-write theirs because they can hold a credential; this
/// file is defined to carry none, and the attribute above is what enforces that
/// definition rather than a promise in a comment.
/// The fields of this file that `drep.toml` must not be able to state.
///
/// Here rather than in `config.rs` because it is a statement about the fields
/// declared directly above it, and the decision about a new one belongs where the
/// field is added. `config::site_only_field` used to be a hard-coded
/// `tree.get("refuse_markers")` in the other module: a third field added here
/// would have compiled, said nothing, and been silently dropped from a
/// `drep.toml` that named it - the one outcome
/// [`super::ConfigError::SiteOnlyField`] exists to prevent, reintroduced by the
/// ordinary act of adding a policy field. `Config` has since gained
/// `deny_unknown_fields`, so the same omission now costs the message rather than
/// the refusal: the key is rejected as a misspelling, against a list of the two
/// keys `drep.toml` does take, and a developer reads that as a line to delete
/// rather than as policy that lives in this file.
///
/// Both fields are rejected in `drep.toml`. A repository can already lower its
/// own `max_concurrent`, but silently dropping the ceiling spelling makes a
/// developer believe a cross-provider policy is active when it is not.
pub const SITE_ONLY_FIELDS: & = &;
/// Fails to compile when a field is added to [`SiteConfig`] without a decision
/// about whether `drep.toml` may state it.
///
/// The exhaustive destructure is the whole point, and is the idiom `KeySource::ALL`
/// and `Severity::ALL` use for the same purpose: a list that has to be kept in
/// step with a type by hand is a list that drifts silently, and the drift here
/// ships source. Adding a field breaks this function, and the fix is one line in
/// [`SITE_ONLY_FIELDS`] or one name added below.
/// What went wrong loading the site policy file.
///
/// A separate enum from [`super::ConfigError`], so the error's *type* names
/// which of the two files is at fault. Folding these into `ConfigError::Io` and
/// `ConfigError::Parse` would make those variants reachable from two files with
/// two different grammars, and a reader could no longer tell which grammar was
/// violated from the variant alone.
/// A repository the site policy refuses to have reviewed by a model.
///
/// Carries both paths because the message has to answer two questions at once: a
/// developer who has never seen this needs to know which file caused it, and
/// that it came from machine policy rather than a broken install.
/// Read the policy at `path`.
///
/// `Ok(None)` means there is no policy on this machine, which is the ordinary
/// state and the reason the return type is an `Option` rather than a
/// `SiteConfig` with empty fields: a caller cannot then confuse "no policy" with
/// "a policy that permits everything", and the two states print differently in
/// `doctor`.
/// Reject what serde cannot enforce from the type alone.
///
/// Rejects rather than repairs, following the house rule `ConfigError` states:
/// a ceiling silently bumped to 1, or a marker silently dropped, is a policy
/// doing something other than what the administrator wrote.
/// Whether `candidate` is a single filename and nothing else.
///
/// Compared back against the original string so a spelling the platform
/// normalises away - `"marker/"`, which parses to one component named `marker` -
/// is rejected too. The alternative is accepting a string that names a file
/// other than the one written down.