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