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    /// Alternate grammar engaged when standard sub-dispatch finds no match.
75    /// Only meaningful for handler-using commands (e.g. tilt's Ruby template
76    /// engine fallback when no Kubernetes tilt sub matches). The handler is
77    /// responsible for invoking it via `registry::try_fallback_grammar()`.
78    #[serde(default)]
79    pub fallback: Option<TomlFallback>,
80}
81
82#[derive(Debug, Deserialize)]
83pub(super) struct TomlFallback {
84    #[serde(default)]
85    pub level: Option<TomlLevel>,
86    #[serde(default)]
87    pub bare: Option<bool>,
88    #[serde(default)]
89    pub max_positional: Option<usize>,
90    #[serde(default)]
91    pub standalone: Vec<String>,
92    #[serde(default)]
93    pub valued: Vec<String>,
94    #[serde(default)]
95    pub tolerate_unknown_short: Option<bool>,
96    #[serde(default)]
97    pub tolerate_unknown_long: Option<bool>,
98    #[serde(default)]
99    pub numeric_dash: Option<bool>,
100    /// Named predicate the handler applies to the first positional arg.
101    /// Currently the only value is `"path"` — accepts a token shaped like
102    /// a file path (contains `/`, `.`, or is `-` for stdin). Adding new
103    /// shapes is a one-line `PositionalShape` enum addition plus a match
104    /// arm in `policy::positional_matches_shape()`.
105    #[serde(default)]
106    pub positional_shape: Option<String>,
107}
108
109#[derive(Debug, Deserialize)]
110pub(super) struct TomlWrapper {
111    #[serde(default)]
112    pub standalone: Vec<String>,
113    #[serde(default)]
114    pub valued: Vec<String>,
115    #[serde(default)]
116    pub positional_skip: Option<usize>,
117    #[serde(default)]
118    pub separator: Option<String>,
119    #[serde(default)]
120    pub bare_ok: Option<bool>,
121}
122
123#[derive(Debug, Deserialize)]
124pub(super) struct TomlSub {
125    pub name: String,
126    #[serde(default)]
127    pub candidate: Option<bool>,
128    #[serde(default)]
129    pub aliases: Vec<String>,
130    #[serde(default)]
131    pub level: Option<TomlLevel>,
132    #[serde(default)]
133    pub bare: Option<bool>,
134    #[serde(default)]
135    pub max_positional: Option<usize>,
136    /// Removed; see TomlCommand::positional_style.
137    #[serde(default)]
138    pub positional_style: Option<bool>,
139    #[serde(default)]
140    pub tolerate_unknown_short: Option<bool>,
141    #[serde(default)]
142    pub tolerate_unknown_long: Option<bool>,
143    #[serde(default)]
144    pub numeric_dash: Option<bool>,
145    #[serde(default)]
146    pub standalone: Vec<String>,
147    #[serde(default)]
148    pub valued: Vec<String>,
149    #[serde(default)]
150    pub guard: Option<String>,
151    #[serde(default)]
152    pub guard_short: Option<String>,
153    #[serde(default)]
154    pub allow_all: Option<bool>,
155    #[serde(default)]
156    pub sub: Vec<TomlSub>,
157    #[serde(default)]
158    pub nested_bare: Option<bool>,
159    #[serde(default)]
160    pub require_any: Vec<String>,
161    #[serde(default)]
162    pub first_arg: Vec<String>,
163    #[serde(default)]
164    pub write_flags: Vec<String>,
165    #[serde(default)]
166    pub delegate_after: Option<String>,
167    #[serde(default)]
168    pub delegate_skip: Option<usize>,
169    #[serde(default)]
170    pub handler: Option<String>,
171    #[serde(default)]
172    pub doc_body: Option<String>,
173}
174
175#[derive(Debug, Clone, Copy, Deserialize)]
176pub(super) enum TomlLevel {
177    Inert,
178    SafeRead,
179    SafeWrite,
180}
181
182impl From<TomlLevel> for SafetyLevel {
183    fn from(l: TomlLevel) -> Self {
184        match l {
185            TomlLevel::Inert => SafetyLevel::Inert,
186            TomlLevel::SafeRead => SafetyLevel::SafeRead,
187            TomlLevel::SafeWrite => SafetyLevel::SafeWrite,
188        }
189    }
190}
191
192#[derive(Debug)]
193pub struct CommandSpec {
194    pub name: String,
195    pub description: String,
196    pub aliases: Vec<String>,
197    pub url: String,
198    pub category: String,
199    /// Upstream version of the underlying tool that was researched
200    /// when this spec was last updated. Free-form string — e.g.
201    /// `"1.9.0"`, `"v5.10.3"`, `"2026-05-08 master"`,
202    /// `"@northflank/cli 0.10.15"`. Internal-only: not rendered in
203    /// docs or used at runtime. Surfaces in tests and as a tripwire
204    /// when researching newer versions of the same tool.
205    pub researched_version: Option<String>,
206    /// Sample invocations that the registry test runs through `is_safe_command`.
207    /// Each `examples_safe` entry must produce `Verdict::Allowed`.
208    pub examples_safe: Vec<String>,
209    /// Sample invocations that must be denied. Use these to lock in security
210    /// boundaries (e.g. `srb tc --metrics-file=/etc/passwd` should always
211    /// be denied; recording it here catches regressions).
212    pub examples_denied: Vec<String>,
213    pub(super) kind: DispatchKind,
214}
215
216#[derive(Debug, Clone)]
217pub(super) struct SubSpec {
218    pub name: String,
219    pub kind: DispatchKind,
220}
221
222#[derive(Debug, Clone)]
223pub(super) enum DispatchKind {
224    Policy {
225        policy: OwnedPolicy,
226        level: SafetyLevel,
227    },
228    FirstArg {
229        patterns: Vec<String>,
230        level: SafetyLevel,
231    },
232    RequireAny {
233        require_any: Vec<String>,
234        policy: OwnedPolicy,
235        level: SafetyLevel,
236        accept_bare_help: bool,
237    },
238    Branching {
239        subs: Vec<SubSpec>,
240        bare_flags: Vec<String>,
241        bare_ok: bool,
242        pre_standalone: Vec<String>,
243        pre_valued: Vec<String>,
244        first_arg: Vec<String>,
245        first_arg_level: SafetyLevel,
246    },
247    WriteFlagged {
248        policy: OwnedPolicy,
249        base_level: SafetyLevel,
250        write_flags: Vec<String>,
251    },
252    DelegateAfterSeparator {
253        separator: String,
254    },
255    DelegateSkip {
256        skip: usize,
257    },
258    Wrapper {
259        standalone: Vec<String>,
260        valued: Vec<String>,
261        positional_skip: usize,
262        separator: Option<String>,
263        bare_ok: bool,
264    },
265    Custom {
266        #[allow(dead_code)]
267        handler_name: String,
268        doc_body: Option<String>,
269        /// TOML-declared subs the handler may consult via
270        /// `registry::try_sub_dispatch()`. Empty unless the handler
271        /// uses the helper.
272        subs: Vec<SubSpec>,
273        /// TOML-declared alternate grammar the handler may consult
274        /// via `registry::try_fallback_grammar()`. `None` unless the
275        /// handler uses the helper.
276        fallback: Option<FallbackSpec>,
277    },
278}
279
280#[derive(Debug, Clone)]
281pub struct OwnedPolicy {
282    pub standalone: Vec<String>,
283    pub valued: Vec<String>,
284    pub bare: bool,
285    pub max_positional: Option<usize>,
286    pub tolerance: crate::policy::FlagTolerance,
287}
288
289#[derive(Debug, Clone)]
290pub(super) struct FallbackSpec {
291    pub policy: OwnedPolicy,
292    pub level: SafetyLevel,
293    pub positional_shape: Option<crate::policy::PositionalShape>,
294}