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
//! `[security]`: what an agent's own code and commands are allowed to reach.
//!
//! The section that decides whether a blueprint may loosen a policy, which
//! environment variables a shell or script can see, and where `[read_paths]`
//! grants apply. Its defaults are the shipped posture, so a change here is a
//! change to what a fresh install permits.
use serde::{Deserialize, Serialize};
use super::default_true;
/// One agent's entry in `[agent_read_paths.<name>]`.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ReadPathGrants {
/// Granted entries, same forms as a blueprint's `[read_paths] allow`.
#[serde(default)]
pub allow: Vec<String>,
}
/// `[security]` in `~/.leviath/config.toml`.
///
/// Distinct from a *blueprint's* `[security]` block, which configures taint
/// tracking for one agent - this one holds machine-wide switches.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SecurityConfig {
/// Directories a run's workdir may sit under without being confirmed.
///
/// **Empty by default**, which means "ask once about anything alarming"
/// rather than "ask about everything": a workdir is only questioned when it
/// is a home directory or a filesystem root, which is where an agent's file
/// writes do the most damage and the least good. Listing a path here says
/// "yes, I work there" and silences the prompt for it and everything under
/// it.
///
/// This exists because the workdir defaults to wherever `lev run` was
/// invoked, and running from `~` is an easy thing to do by accident - issue
/// #252 is a machine that lost 115 GB to an agent writing under a profile
/// root. Confirming is cheap; noticing afterwards is not.
#[serde(default)]
pub allowed_workdirs: Vec<String>,
/// Whether a blueprint's `seed = { command = "..." }` regions may run.
///
/// **On by default.** A command seed executes at spawn - before the first
/// inference, and therefore before any tool-approval prompt - so it is the
/// one place a manifest can run something without the user being asked.
/// It is still confined to the run's workdir, routed through the entry
/// stage's sandbox when the agent declares one, and capped by
/// `[limits] script_shell_timeout_secs`. Set this to `false` to refuse them
/// machine-wide, or pass `--no-seed-commands` for a single run. Inspect an
/// agent's command seeds before installing it with `lev validate <path>`.
#[serde(default = "default_true")]
pub allow_seed_commands: bool,
/// Whether agent-driven fetches may reach loopback, private, and link-local
/// addresses.
///
/// **Off by default.** An agent's `web_fetch` URL is chosen by the model out
/// of context an attacker can influence - a search result, a page fetched a
/// moment ago, an issue body - so an unrestricted fetch makes the agent a
/// confused deputy *inside* the user's network. The concrete targets are
/// `http://169.254.169.254/…` (cloud metadata, which returns instance
/// credentials), `http://127.0.0.1:3000/api/…` (the user's own `lev serve`),
/// and anything on the LAN.
///
/// Turn this on when the agent is genuinely meant to talk to something local -
/// a self-hosted model, a dev server under test. It applies to the script
/// host's `http_get`/`http_post` and to redirect following; see
/// [`leviath_net`].
#[serde(default)]
pub allow_local_network: bool,
/// Credential-shaped environment variables that agent scripts may read.
///
/// A Rhai script tool or script provider calling `env_var("NAME")` gets any
/// ordinary variable - `PATH`, `TZ`, an app's own config. A name that *looks
/// like a credential* (see [`leviath_core::secrets::is_sensitive_env_name`])
/// is refused unless it appears here, because a two-line script tool reading
/// `ANTHROPIC_API_KEY` and POSTing it elsewhere was otherwise a working
/// exfiltration path with no prompt anywhere in it.
///
/// List the exact names a script legitimately needs - typically the key for
/// a custom provider script:
///
/// ```toml
/// [security]
/// allow_env_vars = ["MY_PROVIDER_KEY"]
/// ```
///
/// Matching is case-insensitive and exact. There is no wildcard: `"*"` is
/// read as a variable literally named `*`, not as "allow everything".
#[serde(default)]
pub allow_env_vars: Vec<String>,
/// How much of the daemon's environment a `shell` tool call, a Rhai
/// `shell()` host call, and a region's command seed inherit.
///
/// The daemon holds provider keys, `LEVIATH_API_TOKEN`, and whatever
/// credentials the person who started it had exported. Handing all of that
/// to every shell command means a single `env` in tool output leaks the lot.
///
/// `filtered` (the default) withholds credential-shaped names but keeps
/// `SSH_AUTH_SOCK`, so `git push` over agent keys still works. `strict`
/// drops the carve-out and also takes `AWS_PROFILE`, `KUBECONFIG` and
/// friends. `custom` ignores the shape heuristic and withholds exactly what
/// [`Self::shell_env_withhold`] names. `inherit` is the old behaviour.
///
/// Toolchain variables - `PATH`, `HOME`, `CARGO_HOME`, `JAVA_HOME`,
/// `VIRTUAL_ENV`, `NVM_DIR`, `GOPATH`, `DOCKER_HOST` - pass through under
/// every mode. [`Self::allow_env_vars`] hands a specific name over under
/// every mode too.
#[serde(default)]
pub shell_env: leviath_core::ShellEnvMode,
/// The names `shell_env = "custom"` withholds. Ignored under every other
/// mode, where the name-shape heuristic decides instead.
#[serde(default)]
pub shell_env_withhold: Vec<String>,
/// Whether a blueprint's `[read_paths]` declarations are honored as-is.
///
/// **Off by default.** A `[read_paths]` block travels inside the
/// `agent.leviath` you installed, and a manifest may only *tighten* what
/// your config allows, never widen it - otherwise any agent package could
/// read `~/.ssh`, this very config file (your API keys), or a password
/// store by shipping one TOML line. With this off, an agent's declared
/// read paths are inert until you grant them via [`Self::read_paths`] or
/// `[agent_read_paths.<name>]`. Turning it on says "any blueprint I run
/// may read every path it declares" - reads only, each access still
/// resolves symlinks and must land inside a declared entry, but prefer
/// the per-agent grant for anything you did not author yourself.
#[serde(default)]
pub allow_blueprint_read_paths: bool,
/// Honour every blueprint's own `[safe_commands]` block.
///
/// **Off by default**, and for the same reason as
/// [`Self::allow_blueprint_read_paths`]: a `[safe_commands]` block travels
/// inside an `agent.leviath` you installed, so letting it count by itself
/// would let any agent package pre-approve its own shell with one TOML line.
/// With this off, a blueprint's list is inert until you opt in, either here
/// for every agent or per agent via
/// `[agent_safe_commands.<name>] allow_blueprint = true`. Prefer the
/// per-agent grant for anything you did not author yourself.
#[serde(default)]
pub allow_blueprint_safe_commands: bool,
/// Honour a blueprint's `[tool_permissions]` even where it is *more*
/// permissive than the built-in default for a tool you have not configured.
///
/// Off by default, for the same reason as the two switches around it:
/// declaring is not granting. Saying nothing about `shell` is the normal
/// state, so without this a downloaded manifest could give itself
/// `shell = "allow"` on a stock machine. With it off, a blueprint may still
/// pre-approve the read-only web tools that are some agents' whole point,
/// and anything beyond that is clamped to the built-in default.
///
/// The per-agent grant needs no switch of its own: naming the tool under
/// `[agent_tool_permissions.<name>]` makes it a ceiling for that agent, and
/// a blueprint may go up to a ceiling. Prefer that for anything you did not
/// author yourself - it says which agent and which tool, where this says
/// "every agent, every tool".
#[serde(default)]
pub allow_blueprint_permissions: bool,
/// Machine-wide read grants for agents that declare `[read_paths]`.
///
/// Entries use the same three forms as a blueprint's `[read_paths] allow`:
/// an exact path (grants its subtree), `glob:` and `regex:` patterns
/// (matched against the symlink-resolved real path, written with `/` on
/// every OS, regex auto-anchored). `~/` expands to your home; a relative
/// entry resolves against the run's workdir.
///
/// ```toml
/// [security]
/// read_paths = ["~/.leviath/runs", "glob:~/design-docs/**"]
/// ```
///
/// A grant only takes effect for a path the running blueprint *also*
/// declares - by itself it grants nothing, so listing a directory here
/// does not open it to agents that never asked.
#[serde(default)]
pub read_paths: Vec<String>,
/// Where provider API keys and MCP OAuth tokens are kept.
///
/// **`file` by default** - `~/.leviath/config.toml` and
/// `~/.leviath/mcp-auth.json`, both created `0600` so they are never even
/// briefly world-readable. This is what Claude Code and Codex do, and it is
/// the only backend that works headless, in a container, over SSH, and on a
/// CI runner.
///
/// Set it to `keychain` to move secrets into the OS credential store (macOS
/// Keychain, Windows Credential Manager, Secret Service elsewhere), so a
/// stolen `~/.leviath` directory yields nothing:
///
/// ```toml
/// [security]
/// credential_store = "keychain"
/// ```
///
/// Then run `lev auth migrate` to move the secrets you already have. It is
/// opt-in rather than the default because an unavailable keychain is not a
/// degraded experience but a broken one - every inference fails at once -
/// and the environments Leviath is most useful in are the least likely to
/// have a working credential store. `lev auth status` reports whether this
/// machine actually has one.
#[serde(default)]
pub credential_store: leviath_core::CredentialStoreKind,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
allowed_workdirs: Vec::new(),
allow_seed_commands: true,
allow_local_network: false,
allow_env_vars: Vec::new(),
shell_env: leviath_core::ShellEnvMode::default(),
shell_env_withhold: Vec::new(),
allow_blueprint_read_paths: false,
allow_blueprint_safe_commands: false,
allow_blueprint_permissions: false,
read_paths: Vec::new(),
credential_store: leviath_core::CredentialStoreKind::File,
}
}
}