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    /// Named flag policies the handler references by string key. Used when
81    /// a handler's dispatch logic genuinely can't move to TOML (e.g. gh's
82    /// sub × action matrix) but the per-policy WordSets are still data that
83    /// should live in TOML. The handler reads them via
84    /// `registry::check_handler_policy(cmd, key, tokens)`.
85    #[serde(default)]
86    pub handler_policy: std::collections::HashMap<String, TomlHandlerPolicy>,
87    /// Parent × action → policy matrices. One block declares: "for
88    /// these parent subcommand names, each of these action verbs maps
89    /// to a named `handler_policy` and validates at this safety level."
90    /// Lets handlers express their dispatch tables as data instead of
91    /// `match` arms. Walked by `registry::try_matrix_dispatch()`.
92    #[serde(default)]
93    pub matrix: Vec<TomlMatrix>,
94}
95
96#[derive(Debug, Deserialize)]
97pub(super) struct TomlMatrix {
98    pub parents: Vec<String>,
99    pub level: TomlLevel,
100    pub actions: std::collections::HashMap<String, TomlMatrixAction>,
101}
102
103#[derive(Debug, Deserialize)]
104#[serde(untagged)]
105pub(super) enum TomlMatrixAction {
106    /// Shorthand: `list = "policy_name"` — references handler_policy by
107    /// name; no guard required.
108    Policy(String),
109    /// Detailed form: `download = { policy = "release_download", guard
110    /// = "--output", guard_short = "-O" }`. The guard flag must be
111    /// present in the action's args for the dispatch to succeed.
112    Detailed {
113        policy: String,
114        #[serde(default)]
115        guard: Option<String>,
116        #[serde(default)]
117        guard_short: Option<String>,
118    },
119}
120
121#[derive(Debug, Deserialize)]
122pub(super) struct TomlHandlerPolicy {
123    #[serde(default)]
124    pub standalone: Vec<String>,
125    #[serde(default)]
126    pub valued: Vec<String>,
127    #[serde(default)]
128    pub bare: Option<bool>,
129    #[serde(default)]
130    pub max_positional: Option<usize>,
131    #[serde(default)]
132    pub tolerate_unknown_short: Option<bool>,
133    #[serde(default)]
134    pub tolerate_unknown_long: Option<bool>,
135    #[serde(default)]
136    pub numeric_dash: Option<bool>,
137}
138
139#[derive(Debug, Deserialize)]
140pub(super) struct TomlFallback {
141    #[serde(default)]
142    pub level: Option<TomlLevel>,
143    #[serde(default)]
144    pub bare: Option<bool>,
145    #[serde(default)]
146    pub max_positional: Option<usize>,
147    #[serde(default)]
148    pub standalone: Vec<String>,
149    #[serde(default)]
150    pub valued: Vec<String>,
151    #[serde(default)]
152    pub tolerate_unknown_short: Option<bool>,
153    #[serde(default)]
154    pub tolerate_unknown_long: Option<bool>,
155    #[serde(default)]
156    pub numeric_dash: Option<bool>,
157    /// Named predicate the handler applies to the first positional arg.
158    /// Currently the only value is `"path"` — accepts a token shaped like
159    /// a file path (contains `/`, `.`, or is `-` for stdin). Adding new
160    /// shapes is a one-line `PositionalShape` enum addition plus a match
161    /// arm in `policy::positional_matches_shape()`.
162    #[serde(default)]
163    pub positional_shape: Option<String>,
164}
165
166#[derive(Debug, Deserialize)]
167pub(super) struct TomlWrapper {
168    #[serde(default)]
169    pub standalone: Vec<String>,
170    #[serde(default)]
171    pub valued: Vec<String>,
172    #[serde(default)]
173    pub positional_skip: Option<usize>,
174    #[serde(default)]
175    pub separator: Option<String>,
176    #[serde(default)]
177    pub bare_ok: Option<bool>,
178}
179
180#[derive(Debug, Deserialize)]
181pub(super) struct TomlSub {
182    pub name: String,
183    #[serde(default)]
184    pub candidate: Option<bool>,
185    #[serde(default)]
186    pub aliases: Vec<String>,
187    #[serde(default)]
188    pub level: Option<TomlLevel>,
189    #[serde(default)]
190    pub bare: Option<bool>,
191    #[serde(default)]
192    pub max_positional: Option<usize>,
193    /// Removed; see TomlCommand::positional_style.
194    #[serde(default)]
195    pub positional_style: Option<bool>,
196    #[serde(default)]
197    pub tolerate_unknown_short: Option<bool>,
198    #[serde(default)]
199    pub tolerate_unknown_long: Option<bool>,
200    #[serde(default)]
201    pub numeric_dash: Option<bool>,
202    #[serde(default)]
203    pub standalone: Vec<String>,
204    #[serde(default)]
205    pub valued: Vec<String>,
206    #[serde(default)]
207    pub guard: Option<String>,
208    #[serde(default)]
209    pub guard_short: Option<String>,
210    #[serde(default)]
211    pub allow_all: Option<bool>,
212    /// Reference a `[command.handler_policy.KEY]` block by name, copying
213    /// its standalone/valued/bare/etc. into this sub's effective policy.
214    /// Lets a single-sub form (search, browse, gh status) re-use the
215    /// same flag list a matrix entry would, without duplicating the
216    /// WordSets. Mutually exclusive with inline standalone/valued.
217    #[serde(default)]
218    pub policy: Option<String>,
219    #[serde(default)]
220    pub sub: Vec<TomlSub>,
221    #[serde(default)]
222    pub nested_bare: Option<bool>,
223    #[serde(default)]
224    pub require_any: Vec<String>,
225    #[serde(default)]
226    pub first_arg: Vec<String>,
227    #[serde(default)]
228    pub write_flags: Vec<String>,
229    #[serde(default)]
230    pub delegate_after: Option<String>,
231    #[serde(default)]
232    pub delegate_skip: Option<usize>,
233    #[serde(default)]
234    pub handler: Option<String>,
235    #[serde(default)]
236    pub doc_body: Option<String>,
237}
238
239#[derive(Debug, Clone, Copy, Deserialize)]
240pub(super) enum TomlLevel {
241    Inert,
242    SafeRead,
243    SafeWrite,
244}
245
246impl From<TomlLevel> for SafetyLevel {
247    fn from(l: TomlLevel) -> Self {
248        match l {
249            TomlLevel::Inert => SafetyLevel::Inert,
250            TomlLevel::SafeRead => SafetyLevel::SafeRead,
251            TomlLevel::SafeWrite => SafetyLevel::SafeWrite,
252        }
253    }
254}
255
256#[derive(Debug)]
257pub struct CommandSpec {
258    pub name: String,
259    pub description: String,
260    pub aliases: Vec<String>,
261    pub url: String,
262    pub category: String,
263    /// Upstream version of the underlying tool that was researched
264    /// when this spec was last updated. Free-form string — e.g.
265    /// `"1.9.0"`, `"v5.10.3"`, `"2026-05-08 master"`,
266    /// `"@northflank/cli 0.10.15"`. Internal-only: not rendered in
267    /// docs or used at runtime. Surfaces in tests and as a tripwire
268    /// when researching newer versions of the same tool.
269    pub researched_version: Option<String>,
270    /// Sample invocations that the registry test runs through `is_safe_command`.
271    /// Each `examples_safe` entry must produce `Verdict::Allowed`.
272    pub examples_safe: Vec<String>,
273    /// Sample invocations that must be denied. Use these to lock in security
274    /// boundaries (e.g. `srb tc --metrics-file=/etc/passwd` should always
275    /// be denied; recording it here catches regressions).
276    pub examples_denied: Vec<String>,
277    pub(super) kind: DispatchKind,
278}
279
280#[derive(Debug, Clone)]
281pub(super) struct SubSpec {
282    pub name: String,
283    pub kind: DispatchKind,
284    /// If this sub was declared with `policy = "key"`, the referenced
285    /// handler_policy name is preserved for docs rendering so a sub
286    /// that points at a policy also shown in **Shared flag sets** can
287    /// render as a reference rather than duplicating the flag list.
288    pub policy_ref: Option<String>,
289}
290
291#[derive(Debug, Clone)]
292pub(super) enum DispatchKind {
293    Policy {
294        policy: OwnedPolicy,
295        level: SafetyLevel,
296    },
297    FirstArg {
298        patterns: Vec<String>,
299        level: SafetyLevel,
300    },
301    RequireAny {
302        require_any: Vec<String>,
303        policy: OwnedPolicy,
304        level: SafetyLevel,
305        accept_bare_help: bool,
306    },
307    Branching {
308        subs: Vec<SubSpec>,
309        bare_flags: Vec<String>,
310        bare_ok: bool,
311        pre_standalone: Vec<String>,
312        pre_valued: Vec<String>,
313        first_arg: Vec<String>,
314        first_arg_level: SafetyLevel,
315    },
316    WriteFlagged {
317        policy: OwnedPolicy,
318        base_level: SafetyLevel,
319        write_flags: Vec<String>,
320    },
321    DelegateAfterSeparator {
322        separator: String,
323    },
324    DelegateSkip {
325        skip: usize,
326    },
327    Wrapper {
328        standalone: Vec<String>,
329        valued: Vec<String>,
330        positional_skip: usize,
331        separator: Option<String>,
332        bare_ok: bool,
333    },
334    Custom {
335        #[allow(dead_code)]
336        handler_name: String,
337        doc_body: Option<String>,
338        /// TOML-declared subs the handler may consult via
339        /// `registry::try_sub_dispatch()`. Empty unless the handler
340        /// uses the helper.
341        subs: Vec<SubSpec>,
342        /// TOML-declared alternate grammar the handler may consult
343        /// via `registry::try_fallback_grammar()`. `None` unless the
344        /// handler uses the helper.
345        fallback: Option<FallbackSpec>,
346        /// Named flag policies the handler consults via
347        /// `registry::check_handler_policy()`. Empty unless the handler
348        /// has dispatch logic that picks a policy by name at runtime.
349        handler_policies: std::collections::HashMap<String, OwnedPolicy>,
350        /// Sub × action matrices the handler walks via
351        /// `registry::try_matrix_dispatch()`.
352        matrices: Vec<MatrixSpec>,
353    },
354}
355
356#[derive(Debug, Clone)]
357pub struct OwnedPolicy {
358    pub standalone: Vec<String>,
359    pub valued: Vec<String>,
360    pub bare: bool,
361    pub max_positional: Option<usize>,
362    pub tolerance: crate::policy::FlagTolerance,
363}
364
365#[derive(Debug, Clone)]
366pub(super) struct MatrixSpec {
367    pub parents: Vec<String>,
368    pub level: SafetyLevel,
369    pub actions: std::collections::HashMap<String, MatrixAction>,
370}
371
372#[derive(Debug, Clone)]
373pub(super) struct MatrixAction {
374    pub policy_key: String,
375    pub guard: Option<String>,
376    pub guard_short: Option<String>,
377}
378
379#[derive(Debug, Clone)]
380pub(super) struct FallbackSpec {
381    pub policy: OwnedPolicy,
382    pub level: SafetyLevel,
383    pub positional_shape: Option<crate::policy::PositionalShape>,
384}