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
//! Whether this whistle may act, and where that answer comes from.
//!
//! One source: `[whistle] allow_control` in `$SHEP_HOME/shep.toml`. Not a
//! flag, not an environment variable — see [`resolve_control`].
use DaemonConfig;
/// Whether whistle's control tools exist.
///
/// The same two-state concept lookout shipped in 12a
/// (`lookout::app::Control`), and deliberately a separate type rather than a
/// shared one: lookout reads the KV store because its gate is the operator's
/// own — a person is at the keyboard — while this one reads the shepherd's
/// config file because these tools act for a client nobody is watching. A
/// shared type would have to carry both sources and would serve neither. What
/// an operator learns once is the word `allow_control` and its two states.
///
/// **A fat-finger catch, not a security boundary.** whistle runs as the
/// operator's own uid; anyone who can launch it can run `shep stop`. What the
/// default buys is narrower and real: with the gate shut, text a sheep printed
/// — which `super::read`'s `tail_bleats` hands to a model verbatim — cannot
/// reach a tool that acts.
///
/// Not an error enum, so IR-20's `#[non_exhaustive]` rule does not apply; and
/// shep-cli is `[[bin]]`-only, so nothing here is in a library crate at all.
///
/// `Whistle::new` (`super::mod`) picks the router from this value, and
/// `Control::how_to_open` supplies the refusal text its `get_info` returns.
/// Reads the gate out of `shep.toml`'s text.
///
/// `None` means the file does not exist, which is the ordinary case and reads
/// as "no". A file that will not parse also reads as "no": a broken config is
/// exactly when something is wrong with the machine, and a gate that failed
/// open then would vanish at the worst moment. The caller
/// (`super::whistle`) prints the parse failure to stderr, so a shut gate is
/// never silent about being shut for the wrong reason.
///
/// **`&|_| None` for the environment closure is about testability, not
/// security.** `DaemonConfig::load` layers `SHEP_LOG_JSON`, `SHEP_LOG_LEVEL`,
/// `SHEP_SOCKET` and `SHEP_MAX_CRON_SLEEP` over the parsed file; **none of the
/// four touches `allow_control` in either direction**, so no env closure could
/// open this gate and passing `None` defends nothing. What it does buy is that
/// this function is a pure function of the file's text: every case is testable
/// without a tempdir, without `std::env::set_var` (`unsafe` in edition 2024,
/// and it races the rest of the suite), and without depending on how the test
/// binary happened to be launched.
///
/// There is still no `SHEP_WHISTLE_ALLOW_CONTROL`, and there must not be one —
/// but the reason is spec §14.7's, which is about a config file being
/// auditable where a per-invocation setting is not. It is **not** that argv and
/// the environment cannot reach this gate. They can, by choosing which
/// `$SHEP_HOME` is read: `shep whistle --home <dir>` and `SHEP_HOME=<dir> shep
/// whistle` both select the `shep.toml` this function is handed. The launcher
/// is the boundary, in argv, environment and file alike.
///
/// Called by `whistle::whistle` (`super::mod`), which reads `shep.toml`
/// once at startup and hands its text here.