Skip to main content

safe_chains/registry/
types.rs

1use serde::Deserialize;
2
3use crate::verdict::SafetyLevel;
4
5#[derive(Debug, Deserialize)]
6pub(super) struct TomlFile {
7    pub command: Vec<TomlCommand>,
8}
9
10#[derive(Debug, Deserialize)]
11pub(super) struct TomlCommand {
12    pub name: String,
13    #[serde(default)]
14    pub description: Option<String>,
15    #[serde(default)]
16    pub candidate: Option<bool>,
17    #[serde(default)]
18    pub aliases: Vec<String>,
19    #[serde(default)]
20    pub url: String,
21    #[serde(default)]
22    pub level: Option<TomlLevel>,
23    #[serde(default)]
24    pub bare: Option<bool>,
25    #[serde(default)]
26    pub max_positional: Option<usize>,
27    /// Removed in favor of `tolerate_unknown_short` / `tolerate_unknown_long`.
28    /// Build panics if any TOML still sets this — see SAMPLE.toml for the
29    /// migration guidance. Kept on the deserializer struct so the panic
30    /// message can name the offending command instead of a serde error.
31    #[serde(default)]
32    pub positional_style: Option<bool>,
33    #[serde(default)]
34    pub tolerate_unknown_short: Option<bool>,
35    #[serde(default)]
36    pub tolerate_unknown_long: Option<bool>,
37    #[serde(default)]
38    pub numeric_dash: Option<bool>,
39    #[serde(default)]
40    pub standalone: Vec<String>,
41    #[serde(default)]
42    pub valued: Vec<String>,
43    #[serde(default)]
44    pub bare_flags: Vec<String>,
45    #[serde(default)]
46    pub sub: Vec<TomlSub>,
47    #[serde(default)]
48    pub handler: Option<String>,
49    #[serde(default)]
50    pub doc_body: Option<String>,
51    #[serde(default)]
52    pub require_any: Vec<String>,
53    #[serde(default)]
54    pub first_arg: Vec<String>,
55    #[serde(default)]
56    pub wrapper: Option<TomlWrapper>,
57    #[serde(default)]
58    pub write_flags: Vec<String>,
59    #[serde(default)]
60    pub researched_version: Option<String>,
61    /// Sample invocations that double as test fixtures.
62    /// `examples_safe` must validate as Allowed; `examples_denied` must validate as Denied.
63    /// Use these to exercise aliases and canonical forms (e.g. `mise use` and `mise u`)
64    /// so drift between the TOML and runtime dispatch fails the test suite.
65    #[serde(default)]
66    pub examples_safe: Vec<String>,
67    #[serde(default)]
68    pub examples_denied: Vec<String>,
69    /// Shortcut: every invocation of this command is denied. Used in custom
70    /// TOMLs to lock down a built-in (e.g. `name = "gh", deny = true` in
71    /// `.safe-chains.toml` denies every gh form for that project).
72    #[serde(default)]
73    pub deny: Option<bool>,
74}
75
76#[derive(Debug, Deserialize)]
77pub(super) struct TomlWrapper {
78    #[serde(default)]
79    pub standalone: Vec<String>,
80    #[serde(default)]
81    pub valued: Vec<String>,
82    #[serde(default)]
83    pub positional_skip: Option<usize>,
84    #[serde(default)]
85    pub separator: Option<String>,
86    #[serde(default)]
87    pub bare_ok: Option<bool>,
88}
89
90#[derive(Debug, Deserialize)]
91pub(super) struct TomlSub {
92    pub name: String,
93    #[serde(default)]
94    pub candidate: Option<bool>,
95    #[serde(default)]
96    pub aliases: Vec<String>,
97    #[serde(default)]
98    pub level: Option<TomlLevel>,
99    #[serde(default)]
100    pub bare: Option<bool>,
101    #[serde(default)]
102    pub max_positional: Option<usize>,
103    /// Removed; see TomlCommand::positional_style.
104    #[serde(default)]
105    pub positional_style: Option<bool>,
106    #[serde(default)]
107    pub tolerate_unknown_short: Option<bool>,
108    #[serde(default)]
109    pub tolerate_unknown_long: Option<bool>,
110    #[serde(default)]
111    pub numeric_dash: Option<bool>,
112    #[serde(default)]
113    pub standalone: Vec<String>,
114    #[serde(default)]
115    pub valued: Vec<String>,
116    #[serde(default)]
117    pub guard: Option<String>,
118    #[serde(default)]
119    pub guard_short: Option<String>,
120    #[serde(default)]
121    pub allow_all: Option<bool>,
122    #[serde(default)]
123    pub sub: Vec<TomlSub>,
124    #[serde(default)]
125    pub nested_bare: Option<bool>,
126    #[serde(default)]
127    pub require_any: Vec<String>,
128    #[serde(default)]
129    pub first_arg: Vec<String>,
130    #[serde(default)]
131    pub write_flags: Vec<String>,
132    #[serde(default)]
133    pub delegate_after: Option<String>,
134    #[serde(default)]
135    pub delegate_skip: Option<usize>,
136    #[serde(default)]
137    pub handler: Option<String>,
138    #[serde(default)]
139    pub doc_body: Option<String>,
140}
141
142#[derive(Debug, Clone, Copy, Deserialize)]
143pub(super) enum TomlLevel {
144    Inert,
145    SafeRead,
146    SafeWrite,
147}
148
149impl From<TomlLevel> for SafetyLevel {
150    fn from(l: TomlLevel) -> Self {
151        match l {
152            TomlLevel::Inert => SafetyLevel::Inert,
153            TomlLevel::SafeRead => SafetyLevel::SafeRead,
154            TomlLevel::SafeWrite => SafetyLevel::SafeWrite,
155        }
156    }
157}
158
159#[derive(Debug)]
160pub struct CommandSpec {
161    pub name: String,
162    pub description: String,
163    pub aliases: Vec<String>,
164    pub url: String,
165    pub category: String,
166    /// Upstream version of the underlying tool that was researched
167    /// when this spec was last updated. Free-form string — e.g.
168    /// `"1.9.0"`, `"v5.10.3"`, `"2026-05-08 master"`,
169    /// `"@northflank/cli 0.10.15"`. Internal-only: not rendered in
170    /// docs or used at runtime. Surfaces in tests and as a tripwire
171    /// when researching newer versions of the same tool.
172    pub researched_version: Option<String>,
173    /// Sample invocations that the registry test runs through `is_safe_command`.
174    /// Each `examples_safe` entry must produce `Verdict::Allowed`.
175    pub examples_safe: Vec<String>,
176    /// Sample invocations that must be denied. Use these to lock in security
177    /// boundaries (e.g. `srb tc --metrics-file=/etc/passwd` should always
178    /// be denied; recording it here catches regressions).
179    pub examples_denied: Vec<String>,
180    pub(super) kind: DispatchKind,
181}
182
183#[derive(Debug, Clone)]
184pub(super) struct SubSpec {
185    pub name: String,
186    pub kind: DispatchKind,
187}
188
189#[derive(Debug, Clone)]
190pub(super) enum DispatchKind {
191    Policy {
192        policy: OwnedPolicy,
193        level: SafetyLevel,
194    },
195    FirstArg {
196        patterns: Vec<String>,
197        level: SafetyLevel,
198    },
199    RequireAny {
200        require_any: Vec<String>,
201        policy: OwnedPolicy,
202        level: SafetyLevel,
203        accept_bare_help: bool,
204    },
205    Branching {
206        subs: Vec<SubSpec>,
207        bare_flags: Vec<String>,
208        bare_ok: bool,
209        pre_standalone: Vec<String>,
210        pre_valued: Vec<String>,
211        first_arg: Vec<String>,
212        first_arg_level: SafetyLevel,
213    },
214    WriteFlagged {
215        policy: OwnedPolicy,
216        base_level: SafetyLevel,
217        write_flags: Vec<String>,
218    },
219    DelegateAfterSeparator {
220        separator: String,
221    },
222    DelegateSkip {
223        skip: usize,
224    },
225    Wrapper {
226        standalone: Vec<String>,
227        valued: Vec<String>,
228        positional_skip: usize,
229        separator: Option<String>,
230        bare_ok: bool,
231    },
232    Custom {
233        #[allow(dead_code)]
234        handler_name: String,
235        doc_body: Option<String>,
236    },
237}
238
239#[derive(Debug, Clone)]
240pub struct OwnedPolicy {
241    pub standalone: Vec<String>,
242    pub valued: Vec<String>,
243    pub bare: bool,
244    pub max_positional: Option<usize>,
245    pub tolerance: crate::policy::FlagTolerance,
246}