Skip to main content

safe_chains/registry/
types.rs

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