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    // Defaulted so a user config that contains only `[[trusted]]` (the repo-pin
8    // list, parsed separately) is valid. Unknown tables like `[[trusted]]` are
9    // ignored here.
10    #[serde(default)]
11    pub command: Vec<TomlCommand>,
12}
13
14#[derive(Debug, Deserialize)]
15pub(super) struct TomlCommand {
16    pub name: String,
17    #[serde(default)]
18    pub description: Option<String>,
19    #[serde(default)]
20    pub candidate: Option<bool>,
21    #[serde(default)]
22    pub aliases: Vec<String>,
23    #[serde(default)]
24    pub url: String,
25    #[serde(default)]
26    pub level: Option<TomlLevel>,
27    #[serde(default)]
28    pub bare: Option<bool>,
29    #[serde(default)]
30    pub max_positional: Option<usize>,
31    /// This command PUTS ITS `NAME=VALUE` POSITIONALS INTO THE ENVIRONMENT (`export`, `declare -x`).
32    /// Each one is classified through `envvars.toml`, exactly as the `VAR=value cmd` prefix form is,
33    /// so the two spellings of one capability cannot disagree.
34    #[serde(default)]
35    pub env_assignment_positionals: Option<bool>,
36    /// Removed in favor of `tolerate_unknown_short` / `tolerate_unknown_long`.
37    /// Build panics if any TOML still sets this — see SAMPLE.toml for the
38    /// migration guidance. Kept on the deserializer struct so the panic
39    /// message can name the offending command instead of a serde error.
40    #[serde(default)]
41    pub positional_style: Option<bool>,
42    #[serde(default)]
43    pub tolerate_unknown_short: Option<bool>,
44    #[serde(default)]
45    pub tolerate_unknown_long: Option<bool>,
46    #[serde(default)]
47    pub numeric_dash: Option<bool>,
48    #[serde(default)]
49    pub standalone: Vec<String>,
50    #[serde(default)]
51    pub valued: Vec<String>,
52    #[serde(default)]
53    pub bare_flags: Vec<String>,
54    #[serde(default)]
55    pub sub: Vec<TomlSub>,
56    #[serde(default)]
57    pub handler: Option<String>,
58    #[serde(default)]
59    pub doc_body: Option<String>,
60    #[serde(default)]
61    pub require_any: Vec<String>,
62    #[serde(default)]
63    pub first_arg: Vec<String>,
64    /// Flags a `first_arg` GLOB family accepts. The glob admits an invocation on its first
65    /// positional alone, so without these it never examines the flags at all and
66    /// `--endpoint-url http://evil.com` rides along on a read. Empty = family not yet researched
67    /// (permissive, grandfathered — see `no_new_unresearched_first_arg_family`).
68    #[serde(default)]
69    pub first_arg_standalone: Vec<String>,
70    #[serde(default)]
71    pub first_arg_valued: Vec<String>,
72    /// Flags admitted only when their VALUE names this machine (`--endpoint-url
73    /// http://localhost:8000`). Same arity as `first_arg_valued`; the value is classified by
74    /// `netloc::is_loopback`, and anything not positively recognized as loopback denies.
75    #[serde(default)]
76    pub first_arg_loopback_valued: Vec<String>,
77    #[serde(default)]
78    pub credential_first_arg: Vec<String>,
79    /// Top-level classifying flags (`[[command.flag]]`): a flag whose PRESENCE classifies the WHOLE
80    /// invocation as an archetype — the flat-command analog of `[[command.sub.flag]]`. For a bimodal
81    /// tool where a mode flag flips the operation: `age -d` / `sops --decrypt` reveal plaintext to the
82    /// model (`decrypt-read`), while the bare/encrypt form is an ordinary local write. Resolved by
83    /// `engine::resolve` via `registry::command_flag_archetypes`; each flag's `classifies` must name a
84    /// known archetype and carry `fact`/`source` provenance (the `assert_command_flag_provenance` guard).
85    #[serde(default)]
86    pub flag: Vec<TomlSubFlag>,
87    #[serde(default)]
88    pub wrapper: Option<TomlWrapper>,
89    #[serde(default)]
90    pub write_flags: Vec<String>,
91    /// Path-argument gate co-located with the command (`[command.path_gate]`): the read/write
92    /// role of each path-bearing flag value and of bare positionals. Consulted by
93    /// `pathgate::should_deny` so a `--output`/`-i` path can't ship ungated. Same shape as
94    /// `pathgates.toml`'s `[roles.X]`.
95    #[serde(default)]
96    pub path_gate: Option<crate::pathgate::RoleSpec>,
97    #[serde(default)]
98    pub researched_version: Option<String>,
99    /// Sample invocations that double as test fixtures.
100    /// `examples_safe` must validate as Allowed; `examples_denied` must validate as Denied.
101    /// Use these to exercise aliases and canonical forms (e.g. `mise use` and `mise u`)
102    /// so drift between the TOML and runtime dispatch fails the test suite.
103    #[serde(default)]
104    pub examples_safe: Vec<String>,
105    #[serde(default)]
106    pub examples_denied: Vec<String>,
107    /// Marks this command's leaf invocation as safe inside
108    /// `eval "$(CMD ...)"`. Set on flat commands whose stdout is documented
109    /// shell-init code (e.g. `ssh-agent`). The leaf is the deepest matched
110    /// dispatch node — tagging here does NOT propagate to subs; each sub
111    /// must be tagged independently. Unset = not eval-safe (the default).
112    #[serde(default)]
113    pub eval_safe: Option<bool>,
114    /// Flag allowlist that extends `eval_safe = true` — these `-`-prefixed
115    /// tokens are also permitted inside the substitution. Default empty,
116    /// meaning only the bare form plus positionals are eval-safe.
117    /// Build panics if this is set without `eval_safe = true`.
118    #[serde(default)]
119    pub eval_safe_flags: Vec<String>,
120    /// Per-valued-flag value allowlist. Maps each valued flag (which
121    /// MUST also appear in `eval_safe_flags`) to the set of values
122    /// permitted in eval substitutions. Use for tools where the flag's
123    /// value determines stdout shape (`aws --format env` vs
124    /// `--format json`). Default empty = no value restriction beyond
125    /// the bare-literal alphabet.
126    #[serde(default)]
127    pub eval_safe_flag_values: std::collections::HashMap<String, Vec<String>>,
128    /// Flags where AT LEAST ONE must appear in the eval substitution.
129    /// Use for tools whose bare invocation isn't shell-init code:
130    /// `fzf` is interactive without `--bash|--zsh|--fish|--nushell`.
131    /// Every entry must also appear in `eval_safe_flags`. Default
132    /// empty = no required flags (bare invocation is fine).
133    #[serde(default)]
134    pub eval_safe_required_flags: Vec<String>,
135    /// Shortcut: every invocation of this command is denied. Used in custom
136    /// TOMLs to lock down a built-in (e.g. `name = "gh", deny = true` in
137    /// `.safe-chains.toml` denies every gh form for that project).
138    #[serde(default)]
139    pub deny: Option<bool>,
140    /// Alternate grammar engaged when standard sub-dispatch finds no match.
141    /// Only meaningful for handler-using commands (e.g. tilt's Ruby template
142    /// engine fallback when no Kubernetes tilt sub matches). The handler is
143    /// responsible for invoking it via `registry::try_fallback_grammar()`.
144    #[serde(default)]
145    pub fallback: Option<TomlFallback>,
146    /// Named flag policies the handler references by string key. Used when
147    /// a handler's dispatch logic genuinely can't move to TOML (e.g. gh's
148    /// sub × action matrix) but the per-policy WordSets are still data that
149    /// should live in TOML. The handler reads them via
150    /// `registry::check_handler_policy(cmd, key, tokens)`.
151    #[serde(default)]
152    pub handler_policy: std::collections::HashMap<String, TomlHandlerPolicy>,
153    /// Parent × action → policy matrices. One block declares: "for
154    /// these parent subcommand names, each of these action verbs maps
155    /// to a named `handler_policy` and validates at this safety level."
156    /// Lets handlers express their dispatch tables as data instead of
157    /// `match` arms. Walked by `registry::try_matrix_dispatch()`.
158    #[serde(default)]
159    pub matrix: Vec<TomlMatrix>,
160    /// A `verb-chain` grammar (`mlr`): a strict main-flag region followed by a
161    /// `then`-chain of allowlisted verbs. Fully declarative — no handler needed.
162    #[serde(default)]
163    pub verb_chain: Option<TomlVerbChain>,
164    /// Declarative facet behavior (`[command.behavior]`) — the non-legacy classification
165    /// path. When present, the engine resolves this command by building a `Profile` from the
166    /// declared operation + operand-role + flags (see `engine::resolve::resolve_behavior`),
167    /// retiring a hardcoded `RESOLVERS` entry. The legacy `level` remains only as the
168    /// fallback the engine already overrides.
169    #[serde(default)]
170    pub behavior: Option<TomlBehavior>,
171    /// What this command's STDOUT can name (`[command.output]`) — the axis that decides whether a
172    /// `$(…)` around it yields a bounded path or an unknowable one. Absent (the default) means
173    /// unpinnable: the substitution worst-cases exactly as it always has. See
174    /// docs/design/behavioral-taxonomy-substitution-locus.md.
175    #[serde(default)]
176    pub output: Option<TomlOutput>,
177}
178
179/// `[command.output]` — a researched claim about where this command's stdout can POINT. It is a
180/// separate axis from every facet: how safe a command is to RUN says nothing about what its output
181/// names (`echo` is inert and `$(echo /etc/shadow)` names a credential file), so this is declared
182/// per command or not at all.
183#[derive(Debug, Deserialize)]
184#[serde(deny_unknown_fields)]
185pub(super) struct TomlOutput {
186    /// `operands` — output names paths beneath the command's own path operands (`fd`, `git
187    /// ls-files`); `cwd` — output names the working directory (`pwd`).
188    pub locus_from: String,
189    /// Flags under which the claim does NOT hold, because they change what stdout CONTAINS.
190    /// `fd -x cat {}` prints file contents rather than paths, and `fd -l` prints `ls -l` rows —
191    /// neither is a path any more, so the locus rule cannot describe them. Any of these present
192    /// makes the substitution unpinnable again.
193    #[serde(default)]
194    pub invalidated_by: Vec<String>,
195    /// Value-taking flags, so their VALUE is not mistaken for an operand (`head -n 5` takes no
196    /// file). Carried here rather than read off the legacy top-level `valued` for the same reason
197    /// `[command.behavior]` carries its own grammar: this claim is researched as a unit, and a
198    /// silently-shared list would let an unrelated edit change what counts as a root.
199    #[serde(default)]
200    pub valued: Vec<String>,
201}
202
203/// A command's declarative facet behavior (`[command.behavior]`). Field values that name a
204/// facet term are the kebab strings from `engine::facet` (`operation = "observe"`); the build
205/// maps them via `FacetTerm::from_term` and PANICS (naming the command) on an unknown term, so
206/// a typo can't silently mis-classify. The behavior carries its OWN flag grammar
207/// (`standalone`/`valued`), independent of the legacy top-level `standalone` — `rm`'s legacy
208/// flag set is restricted to `--help`/`--version` (so the legacy fallback can't fail-open on
209/// `rm -rf`), while its behavior grammar is the full destructive set.
210#[derive(Debug, Deserialize)]
211#[serde(deny_unknown_fields)]
212pub(super) struct TomlBehavior {
213    /// The act each operand capability performs — an `Operation` term
214    /// (`observe`/`create`/`mutate`/`destroy`/…).
215    pub operation: String,
216    /// How bare positionals are touched: `none` | `read` | `write` | `pattern-then-read` |
217    /// `transfer` (the closed set the `Operands` enum encodes).
218    pub positionals: String,
219    /// Scale model: `single` (every read is one item — cat/head) or `breadth`
220    /// (count/glob/recursion widen it — rm/mkdir). Defaults to `single`.
221    #[serde(default)]
222    pub scale: Option<String>,
223    /// Boolean flags this command accepts (behavior's own grammar). Single-dash single-char
224    /// tokens (`-r`) cluster; `--long` tokens are matched whole.
225    #[serde(default)]
226    pub standalone: Vec<String>,
227    /// Value-taking flags (consume the next token or a glued `=value`).
228    #[serde(default)]
229    pub valued: Vec<String>,
230    /// Accept the obsolete `-NUM` count shorthand (`head -20`).
231    #[serde(default)]
232    pub numeric_shorthand: Option<bool>,
233    /// Per-flag facet deltas — a flag whose presence widens scale (`"-r" = { scale =
234    /// "unbounded" }`), consumes a path value, or supplies a pattern.
235    #[serde(default)]
236    pub flags: std::collections::HashMap<String, TomlBehaviorFlag>,
237    /// Thin custom hook for the irreducible token logic a declaration can't express
238    /// (`grep`'s pattern-vs-file disambiguation). Composes: it returns the classified operand
239    /// set; the facets + level projection stay declarative. Absent = pure declarative.
240    #[serde(default)]
241    pub hook: Option<String>,
242    /// Transfer semantics (`[command.behavior.transfer]`), REQUIRED when `positionals =
243    /// "transfer"` — the source-operand operation and the clobber/recursion flag sets that a
244    /// `cp`/`mv`/`ln`-shaped command differs on.
245    #[serde(default)]
246    pub transfer: Option<TomlTransfer>,
247}
248
249/// The differing knobs of a transfer command (`cp`/`mv`/`ln`): every source operand is read at
250/// its own locus and the destination is a create/overwrite at its locus, but the source
251/// *operation* and the clobber/recursion flags differ per command.
252#[derive(Debug, Deserialize)]
253#[serde(deny_unknown_fields)]
254pub(super) struct TomlTransfer {
255    /// The source-operand operation: `observe` (cp/ln read the source into the dest/link) or
256    /// `relocate` (mv removes the source from its old location).
257    pub source: String,
258    /// Flags whose PRESENCE means the destination will not be overwritten (`cp`/`mv`: `-n`,
259    /// `--no-clobber`). Mutually exclusive with `clobber_flags`.
260    #[serde(default)]
261    pub no_clobber_flags: Vec<String>,
262    /// Flags whose PRESENCE means the destination WILL be overwritten, the default being
263    /// no-clobber (`ln`: `-f`, `--force`). Mutually exclusive with `no_clobber_flags`.
264    #[serde(default)]
265    pub clobber_flags: Vec<String>,
266    /// Flags whose presence widens the scale to unbounded (`cp`: `-r`/`-R`/`-a`).
267    #[serde(default)]
268    pub recursive_flags: Vec<String>,
269}
270
271/// One flag's contribution to a `[command.behavior]` profile: a scale bump when present, and/or
272/// a path role on the flag's VALUE (a valued flag whose value is a path safe-chains must gate,
273/// e.g. `touch -r REF` reads REF's timestamp — folds the `[command.path_gate]` idea into behavior).
274#[derive(Debug, Deserialize)]
275#[serde(deny_unknown_fields)]
276pub(super) struct TomlBehaviorFlag {
277    /// Scale bump when present (`"-r" = { scale = "unbounded" }`).
278    #[serde(default)]
279    pub scale: Option<String>,
280    /// The flag's VALUE is a path with this role: `read` (gated by its read locus) or `write`
281    /// (gated by its write locus). The flag must be a valued flag (in `valued`).
282    #[serde(default)]
283    pub kind: Option<String>,
284}
285
286/// A `verb-chain` command grammar: `CMD [main-flags…] verb [args…] then verb [args…] …`
287/// (`mlr`). The main-flag region is a STRICT allowlist (an unlisted flag denies — so a
288/// mutating flag like mlr's `-I`/`--in-place`, omitted, is caught by omission); the verb
289/// region is a `then`-chain where every verb NAME must be on the `verbs` allowlist (verb
290/// ARGS are open-ended and not inspected — a pure verb has no shell/file escape).
291#[derive(Debug, Deserialize)]
292#[serde(deny_unknown_fields)]
293pub(super) struct TomlVerbChain {
294    #[serde(default)]
295    pub level: Option<TomlLevel>,
296    /// The chain separator keyword (mlr: `then`). Defaults to `then`.
297    #[serde(default)]
298    pub separator: Option<String>,
299    /// Boolean main flags (no value). SAFETY: every value-TAKING main flag must go in
300    /// `main_valued` instead, or the walk mistakes its value for the verb boundary and a
301    /// later mutating flag slips past in verb-land.
302    #[serde(default)]
303    pub main_standalone: Vec<String>,
304    /// Value-taking main flags (`--from FILE`, `--ifs ,`), each consuming the next token.
305    #[serde(default)]
306    pub main_valued: Vec<String>,
307    /// Variadic main flags (mlr `--mfrom A B …`) that consume tokens until a `--` terminator.
308    #[serde(default)]
309    pub main_variadic: Vec<String>,
310    /// The allowlist of verb names permitted in every `then`-segment.
311    #[serde(default)]
312    pub verbs: Vec<String>,
313}
314
315#[derive(Debug, Deserialize)]
316#[serde(deny_unknown_fields)]
317pub(super) struct TomlMatrix {
318    pub parents: Vec<String>,
319    pub level: TomlLevel,
320    pub actions: std::collections::HashMap<String, TomlMatrixAction>,
321}
322
323#[derive(Debug, Deserialize)]
324#[serde(untagged)]
325pub(super) enum TomlMatrixAction {
326    /// Shorthand: `list = "policy_name"` — references handler_policy by
327    /// name; no guard required.
328    Policy(String),
329    /// Detailed form: `download = { policy = "release_download", guard
330    /// = "--output", guard_short = "-O" }`. The guard flag must be
331    /// present in the action's args for the dispatch to succeed.
332    Detailed(TomlMatrixActionDetailed),
333}
334
335#[derive(Debug, Deserialize)]
336#[serde(deny_unknown_fields)]
337pub(super) struct TomlMatrixActionDetailed {
338    pub policy: String,
339    #[serde(default)]
340    pub guard: Option<String>,
341    #[serde(default)]
342    pub guard_short: Option<String>,
343}
344
345#[derive(Debug, Deserialize)]
346pub(super) struct TomlHandlerPolicy {
347    #[serde(default)]
348    pub standalone: Vec<String>,
349    #[serde(default)]
350    pub valued: Vec<String>,
351    #[serde(default)]
352    pub bare: Option<bool>,
353    #[serde(default)]
354    pub max_positional: Option<usize>,
355    #[serde(default)]
356    pub tolerate_unknown_short: Option<bool>,
357    #[serde(default)]
358    pub tolerate_unknown_long: Option<bool>,
359    #[serde(default)]
360    pub numeric_dash: Option<bool>,
361}
362
363#[derive(Debug, Deserialize)]
364pub(super) struct TomlFallback {
365    #[serde(default)]
366    pub level: Option<TomlLevel>,
367    #[serde(default)]
368    pub bare: Option<bool>,
369    #[serde(default)]
370    pub max_positional: Option<usize>,
371    #[serde(default)]
372    pub standalone: Vec<String>,
373    #[serde(default)]
374    pub valued: Vec<String>,
375    #[serde(default)]
376    pub tolerate_unknown_short: Option<bool>,
377    #[serde(default)]
378    pub tolerate_unknown_long: Option<bool>,
379    #[serde(default)]
380    pub numeric_dash: Option<bool>,
381    /// Named predicate the handler applies to the first positional arg.
382    /// Currently the only value is `"path"` — accepts a token shaped like
383    /// a file path (contains `/`, `.`, or is `-` for stdin). Adding new
384    /// shapes is a one-line `PositionalShape` enum addition plus a match
385    /// arm in `policy::positional_matches_shape()`.
386    #[serde(default)]
387    pub positional_shape: Option<String>,
388    /// `"file"` gates the first positional as an EXECUTOR through the execution-origin
389    /// engine (worktree-local code allows, foreign denies) rather than the flat `level`.
390    /// For interpreters run as `python3 ./s.py` / `ruby s.rb`. (`"project"` exists for subs
391    /// but is not used on fallbacks.)
392    #[serde(default)]
393    pub executor: Option<String>,
394    #[serde(default)]
395    pub executor_redirect_flag: Option<String>,
396}
397
398#[derive(Debug, Deserialize)]
399pub(super) struct TomlWrapper {
400    #[serde(default)]
401    pub standalone: Vec<String>,
402    #[serde(default)]
403    pub valued: Vec<String>,
404    #[serde(default)]
405    pub positional_skip: Option<usize>,
406    #[serde(default)]
407    pub separator: Option<String>,
408    #[serde(default)]
409    pub bare_ok: Option<bool>,
410}
411
412/// One `[[command.sub.flag]]`: a flag that escalates its sub's classification when present.
413#[derive(Debug, Deserialize)]
414pub(super) struct TomlSubFlag {
415    pub name: String,
416    /// The archetype (`archetypes.toml`) this flag's presence ADDS to the profile — or
417    /// `"unclassified"` to worst-case (fail-closed) a flag whose effect we can't yet name.
418    pub classifies: String,
419    /// Optional value-match: escalate only when the flag's VALUE starts with this prefix (space
420    /// form `-c core.sshCommand=…` or glued `--flag=core.sshCommand=…`). Absent = escalate on the
421    /// flag's mere PRESENCE (a bare flag like `--force`). This is what lets ONE valued flag be
422    /// benign for most values and dangerous for a specific key (`git -c core.sshCommand=` = exec).
423    #[serde(default)]
424    pub value_prefix: Option<String>,
425    /// `true` INVERTS the trigger: escalate when the flag is ABSENT, not present. For a SAFETY flag
426    /// whose absence is the risk — `npm ci` runs lifecycle scripts UNLESS `--ignore-scripts` is
427    /// given, so its base profile (local-install-pinned) escalates to supply-chain-build when
428    /// `--ignore-scripts` is missing. Mutually exclusive with `value_prefix`.
429    #[serde(default)]
430    pub when_absent: Option<bool>,
431    #[serde(default)]
432    pub fact: Option<String>,
433    #[serde(default)]
434    pub source: Option<String>,
435    #[serde(default)]
436    pub judgment: Option<String>,
437}
438
439#[derive(Debug, Deserialize)]
440pub(super) struct TomlSub {
441    pub name: String,
442    #[serde(default)]
443    pub candidate: Option<bool>,
444    /// A facet archetype name (`archetypes.toml`) — the Phase-1 successor to `candidate = true`:
445    /// instead of hand-marking the sub above the line, it declares which recurring capability
446    /// profile it is, and the engine DERIVES the verdict by projecting that profile through the
447    /// levels. See `docs/design/behavioral-taxonomy-archetypes.md`.
448    #[serde(default)]
449    pub profile: Option<String>,
450    /// Per-item research provenance for the classification (required when `profile` is set — the
451    /// `every_profiled_sub_has_provenance` guard). Three layers so a future researcher can act on
452    /// each precisely: `fact` = what the upstream tool DOCUMENTS (re-check `source` if it moves),
453    /// the `profile` itself = our inference (which archetype it maps to), `judgment` = our stance
454    /// where the source doesn't decide it (a policy call they may revisit). `source` cites the
455    /// upstream doc/section. See `docs/design/behavioral-taxonomy-archetypes.md` §3.
456    #[serde(default)]
457    pub fact: Option<String>,
458    #[serde(default)]
459    pub source: Option<String>,
460    #[serde(default)]
461    pub judgment: Option<String>,
462    #[serde(default)]
463    pub aliases: Vec<String>,
464    #[serde(default)]
465    pub level: Option<TomlLevel>,
466    #[serde(default)]
467    pub bare: Option<bool>,
468    #[serde(default)]
469    pub max_positional: Option<usize>,
470    /// Removed; see TomlCommand::positional_style.
471    #[serde(default)]
472    pub positional_style: Option<bool>,
473    #[serde(default)]
474    pub tolerate_unknown_short: Option<bool>,
475    #[serde(default)]
476    pub tolerate_unknown_long: Option<bool>,
477    #[serde(default)]
478    pub numeric_dash: Option<bool>,
479    #[serde(default)]
480    pub standalone: Vec<String>,
481    #[serde(default)]
482    pub valued: Vec<String>,
483    #[serde(default)]
484    pub guard: Option<String>,
485    #[serde(default)]
486    pub guard_short: Option<String>,
487    #[serde(default)]
488    pub allow_all: Option<bool>,
489    /// Reference a `[command.handler_policy.KEY]` block by name, copying
490    /// its standalone/valued/bare/etc. into this sub's effective policy.
491    /// Lets a single-sub form (search, browse, gh status) re-use the
492    /// same flag list a matrix entry would, without duplicating the
493    /// WordSets. Mutually exclusive with inline standalone/valued.
494    #[serde(default)]
495    pub policy: Option<String>,
496    #[serde(default)]
497    pub sub: Vec<TomlSub>,
498    /// Per-FLAG escalation + provenance (`[[command.sub.flag]]`): a flag that, when present, ADDS a
499    /// capability to this sub's resolved profile — `git push --force` (→ destroy), `-c
500    /// core.sshCommand=` (→ execution). The level algebra takes the max over the added capabilities,
501    /// so a benign base + a dangerous flag lands at the flag's tier. See
502    /// `docs/design/behavioral-taxonomy-archetypes.md` §3 (per-flag layer).
503    #[serde(default)]
504    pub flag: Vec<TomlSubFlag>,
505    /// `true` marks the sub's first positional as a NETWORK DESTINATION whose *provenance* the
506    /// engine classifies onto `locus.provenance` (established remote-name / literal URL / opaque
507    /// `$VAR`), and whose command-transport form (`ext::<cmd>`) worst-cases as RCE. For
508    /// `git push` and its kin (`scp`/`rsync`/`curl -d`). See `behavioral-taxonomy-exposure.md` §4.
509    #[serde(default)]
510    pub network_destination: Option<bool>,
511    /// A flag that ALSO carries the destination and OVERRIDES the positional (`git push
512    /// --repo=<dest>`). Classified with the same provenance rules — so `--repo=ext::sh` is caught as
513    /// RCE. Requires `network_destination`.
514    #[serde(default)]
515    pub destination_flag: Option<String>,
516    /// Flags whose VALUE is a local output-file path, for a `data-export` sub (`supabase db dump
517    /// -f`, `pg_dump --file`). When one is present the engine adds a path-gated write capability at
518    /// that file's locus — a dump to `./out.sql` is a worktree write, one to `/etc/cron.d/job` a
519    /// system write. Absent (the export goes to stdout) → no write, just the bulk remote read.
520    /// Requires `profile` (only a `data-export` sub has an output file). See
521    /// `behavioral-taxonomy-exposure.md`.
522    #[serde(default)]
523    pub output_path_flags: Vec<String>,
524    /// Flags naming a network endpoint that must be on THIS machine (`--endpoint-url
525    /// http://localhost:8000`). Two effects, both keyed on `netloc::is_loopback`: the flag is
526    /// admitted only with a loopback value, and a loopback value re-classifies the sub's
527    /// non-destroy capabilities as local (`resolve`'s loopback modifier). Declare only where a
528    /// local emulator is a researched workflow for that service.
529    #[serde(default)]
530    pub loopback_valued: Vec<String>,
531    /// Whether a `loopback_valued` flag naming this machine re-classifies the sub: `resolve` clears
532    /// the destination-determined facets (remote reach, net direction, payload, metered cost) and
533    /// leaves everything describing the operation alone. Absent = the sub keeps its remote
534    /// classification whatever the destination; mandatory for destroy archetypes, build-enforced.
535    #[serde(default)]
536    pub loopback_localizes: Option<bool>,
537    #[serde(default)]
538    pub nested_bare: Option<bool>,
539    #[serde(default)]
540    pub require_any: Vec<String>,
541    #[serde(default)]
542    pub first_arg: Vec<String>,
543    /// Flags a `first_arg` GLOB family accepts. The glob admits an invocation on its first
544    /// positional alone, so without these it never examines the flags at all and
545    /// `--endpoint-url http://evil.com` rides along on a read. Empty = family not yet researched
546    /// (permissive, grandfathered — see `no_new_unresearched_first_arg_family`).
547    #[serde(default)]
548    pub first_arg_standalone: Vec<String>,
549    #[serde(default)]
550    pub first_arg_valued: Vec<String>,
551    /// Flags admitted only when their VALUE names this machine (`--endpoint-url
552    /// http://localhost:8000`). Same arity as `first_arg_valued`; the value is classified by
553    /// `netloc::is_loopback`, and anything not positively recognized as loopback denies.
554    #[serde(default)]
555    pub first_arg_loopback_valued: Vec<String>,
556    /// First-positional globs (`secret`, `secret/*`) that make this sub a CREDENTIAL-READ: matching
557    /// denies, before the allow-glob. The value-dependent complement to `profile=credential-read`
558    /// (whole sub) — `kubectl get secret/x`, `aws configure get aws_secret_access_key`.
559    #[serde(default)]
560    pub credential_first_arg: Vec<String>,
561    #[serde(default)]
562    pub write_flags: Vec<String>,
563    #[serde(default)]
564    pub delegate_after: Option<String>,
565    #[serde(default)]
566    pub delegate_skip: Option<usize>,
567    /// `"file"` (first positional is the executor path — `go run ./cmd`) or `"project"`
568    /// (the current project is the executor — `cargo run`). Gates via the execution-origin
569    /// engine instead of a flat level. See `DispatchKind::Executor`.
570    #[serde(default)]
571    pub executor: Option<String>,
572    /// A valued flag whose value redirects the executor out of the project
573    /// (`cargo run --manifest-path DIR/Cargo.toml`); its value is locus-gated. `Project` only.
574    #[serde(default)]
575    pub executor_redirect_flag: Option<String>,
576    /// Predicate the executor path must satisfy (`"go-package"`), else deny. `File` only.
577    #[serde(default)]
578    pub positional_shape: Option<String>,
579    #[serde(default)]
580    pub handler: Option<String>,
581    #[serde(default)]
582    pub doc_body: Option<String>,
583    /// Marks this sub's leaf invocation as safe inside
584    /// `eval "$(CMD SUB ...)"`. The leaf is the deepest matched dispatch
585    /// node — if this sub has nested sub-subs and the invocation matches
586    /// deeper, the tag does NOT apply; the sub-sub must be tagged itself.
587    /// Unset = not eval-safe (the default).
588    #[serde(default)]
589    pub eval_safe: Option<bool>,
590    /// Flag allowlist that extends `eval_safe = true` — these `-`-prefixed
591    /// tokens are also permitted inside the substitution. Default empty,
592    /// meaning only the bare form plus positionals are eval-safe.
593    /// Build panics if this is set without `eval_safe = true`.
594    #[serde(default)]
595    pub eval_safe_flags: Vec<String>,
596    /// Per-valued-flag value allowlist (same semantics as the
597    /// command-level field). Maps each valued flag (which MUST also
598    /// appear in `eval_safe_flags`) to its permitted values.
599    #[serde(default)]
600    pub eval_safe_flag_values: std::collections::HashMap<String, Vec<String>>,
601    /// Flags where AT LEAST ONE must appear (same semantics as the
602    /// command-level field).
603    #[serde(default)]
604    pub eval_safe_required_flags: Vec<String>,
605}
606
607#[derive(Debug, Clone, Copy, Deserialize)]
608pub(super) enum TomlLevel {
609    Inert,
610    SafeRead,
611    SafeWrite,
612}
613
614impl From<TomlLevel> for SafetyLevel {
615    fn from(l: TomlLevel) -> Self {
616        match l {
617            TomlLevel::Inert => SafetyLevel::Inert,
618            TomlLevel::SafeRead => SafetyLevel::SafeRead,
619            TomlLevel::SafeWrite => SafetyLevel::SafeWrite,
620        }
621    }
622}
623
624#[derive(Debug)]
625pub struct CommandSpec {
626    pub name: String,
627    pub description: String,
628    pub aliases: Vec<String>,
629    pub url: String,
630    pub category: String,
631    /// Upstream version of the underlying tool that was researched
632    /// when this spec was last updated. Free-form string — e.g.
633    /// `"1.9.0"`, `"v5.10.3"`, `"2026-05-08 master"`,
634    /// `"@northflank/cli 0.10.15"`. Internal-only: not rendered in
635    /// docs or used at runtime. Surfaces in tests and as a tripwire
636    /// when researching newer versions of the same tool.
637    pub researched_version: Option<String>,
638    /// Sample invocations that the registry test runs through `is_safe_command`.
639    /// Each `examples_safe` entry must produce `Verdict::Allowed`.
640    pub examples_safe: Vec<String>,
641    /// Sample invocations that must be denied. Use these to lock in security
642    /// boundaries (e.g. `srb tc --metrics-file=/etc/passwd` should always
643    /// be denied; recording it here catches regressions).
644    pub examples_denied: Vec<String>,
645    /// True when this command's bare invocation (no sub) is tagged as
646    /// safe-to-eval. Walked by `registry::is_eval_safe_invocation()`.
647    pub eval_safe: bool,
648    /// Flag allowlist extending `eval_safe` — flags permitted in the
649    /// substituted invocation when the walker stops at this node.
650    pub eval_safe_flags: Vec<String>,
651    /// Per-valued-flag value allowlist. When the walker hits a flag
652    /// listed here, the value following the flag (separated by `=` or
653    /// space) must be in this list.
654    pub eval_safe_flag_values: std::collections::HashMap<String, Vec<String>>,
655    /// Flags where at least one must appear in the substituted
656    /// invocation. Empty = no required-flag constraint.
657    pub eval_safe_required_flags: Vec<String>,
658    /// The command's own path-argument gate (`[command.path_gate]`), if declared. Read by
659    /// `registry::command_path_gate` → `pathgate::should_deny`.
660    pub(super) path_gate: Option<crate::pathgate::RoleSpec>,
661    /// Top-level classifying flags (`[[command.flag]]`), lowered from `TomlSubFlag`. A present flag
662    /// classifies the whole invocation as its archetype — read by `registry::command_flag_archetypes`
663    /// → `engine::resolve::resolve` (the flat-command analog of a profiled sub's escalating flags).
664    pub(super) archetype_flags: Vec<FlagProvenance>,
665    /// Declarative facet behavior (`[command.behavior]`), lowered to typed facet enums. Read
666    /// by `registry::command_behavior` → `engine::resolve::resolve_behavior`. When present,
667    /// the engine classifies this command from its declared facets instead of a Rust resolver.
668    pub(super) behavior: Option<BehaviorSpec>,
669    /// Lowered `[command.output]` — read by `registry::command_output_locus` →
670    /// `engine::resolve::substitution_locus`, which decides whether a `$(…)` around this command
671    /// yields a bounded path instead of the unpinnable sentinel.
672    pub(super) output: Option<OutputSpec>,
673    /// True when the command's `NAME=VALUE` positionals become environment variables (`export`,
674    /// `declare -x`). `dispatch_spec` then classifies each through `envvars::assignment_verdict`
675    /// and combines the result, so `export LD_PRELOAD=/tmp/evil.so` denies as
676    /// `LD_PRELOAD=/tmp/evil.so ls` does.
677    pub(super) env_assignment_positionals: bool,
678    pub(super) kind: DispatchKind,
679}
680
681/// A command's declarative facet behavior, lowered from `[command.behavior]` (`TomlBehavior`)
682/// with every facet string resolved to its enum at build time. The generic resolver reads this
683/// plus the tokens and builds a `Profile`. Clone so it can be attached uniformly across the
684/// `build_command` construction sites.
685#[derive(Debug, Clone)]
686pub(crate) struct BehaviorSpec {
687    pub operation: crate::engine::facet::Operation,
688    pub positionals: PositionalRole,
689    pub scale: ScaleModel,
690    /// Behavior's own flag grammar, pre-split for the shared `walk_positionals`.
691    pub short: Vec<u8>,
692    pub valued_short: Vec<u8>,
693    pub long: Vec<String>,
694    pub valued_long: Vec<String>,
695    pub numeric_shorthand: bool,
696    /// Flags whose presence widens the scale to unbounded (`rm -r`, `grep -r`).
697    pub unbounded_flags: Vec<String>,
698    /// Valued flags whose VALUE is a path to gate (`touch -r REF` reads REF), with its role.
699    pub path_flags: Vec<PathFlag>,
700    pub hook: Option<BehaviorHook>,
701    /// Transfer semantics, present iff `positionals == Transfer`.
702    pub transfer: Option<TransferSpec>,
703}
704
705/// A valued flag whose value is a path safe-chains gates by locus. One spelling per entry (a
706/// flag with both short and long forms is two entries); the resolver scans for each.
707#[derive(Debug, Clone)]
708pub(crate) struct PathFlag {
709    pub short: Option<u8>,
710    pub long: Option<String>,
711    pub role: PathRole,
712}
713
714/// The role a path-flag's value plays — read (gated by read locus) or write (by write locus).
715#[derive(Debug, Clone, Copy, PartialEq, Eq)]
716pub(crate) enum PathRole {
717    Read,
718    Write,
719}
720
721/// Lowered `[command.behavior.transfer]` — the per-command transfer knobs, terms resolved.
722#[derive(Debug, Clone)]
723pub(crate) struct TransferSpec {
724    pub source: TransferSource,
725    pub no_clobber_flags: Vec<String>,
726    pub clobber_flags: Vec<String>,
727    pub recursive_flags: Vec<String>,
728}
729
730/// The source-operand operation of a transfer command.
731#[derive(Debug, Clone, Copy, PartialEq, Eq)]
732pub(crate) enum TransferSource {
733    /// cp/ln: read the source into the destination/link (no disclosure to the model).
734    Observe,
735    /// mv: remove the source from its old location (trivially reversible).
736    Relocate,
737}
738
739/// The closed set of operand-role shapes (§ design doc: the `Operands` enum, as data).
740#[derive(Debug, Clone, Copy, PartialEq, Eq)]
741pub(crate) enum PositionalRole {
742    None,
743    Read,
744    Write,
745    PatternThenRead,
746    Transfer,
747}
748
749/// How a command's `Scale` is computed from its operands.
750#[derive(Debug, Clone, Copy, PartialEq, Eq)]
751pub(crate) enum ScaleModel {
752    /// Every operation is a single item regardless of operand count (cat/head).
753    Single,
754    /// Count, glob, or a recursion flag widen it (`breadth_scale`) — rm/mkdir.
755    Breadth,
756}
757
758/// Runtime form of `[command.output]`: how to derive the locus of a command's stdout.
759#[derive(Debug, Clone)]
760pub(crate) struct OutputSpec {
761    pub locus_from: OutputLocus,
762    /// Flags that void the claim (see `TomlOutput::invalidated_by`).
763    pub invalidated_by: Vec<String>,
764    /// Value-taking flags (see `TomlOutput::valued`).
765    pub valued: Vec<String>,
766}
767
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769pub(crate) enum OutputLocus {
770    /// Beneath the command's own path operands — the worst `read_locus` over them, or the cwd
771    /// when it has none (`fd pattern` with no root searches `.`).
772    Operands,
773    /// The working directory itself (`pwd`).
774    Cwd,
775    /// A subset of what it was piped (`head -1`, `sort`, `uniq`) — so the locus is the PREVIOUS
776    /// pipeline stage's. Only when the command has no path operand: `head f.txt` prints the
777    /// contents of a file rather than filtering a stream, and contents are not paths.
778    Stdin,
779    /// The output words are ATOMS: they carry no path separator, so splicing one into a path
780    /// cannot move which directory the path names. `seq` prints integers; `basename` prints a
781    /// single component by definition.
782    ///
783    /// This is a different KIND of claim from the others, which all answer "which locus does this
784    /// output name". An atom names no locus at all — the point is that it cannot CHANGE one. That
785    /// is what makes `for i in $(seq 1 4); do … > "$SP/dx_$i.txt"; done` confinable: the prefix is
786    /// literal, and an atom spliced into the leaf cannot escape it.
787    ///
788    /// Separator-freedom alone is not sufficient — an atom that IS a whole component could be
789    /// `..`. Confinement additionally requires the interpolation to be flanked by literal text
790    /// within its component, which is a property of the PATH, not of this declaration. See
791    /// `docs/design/behavioral-taxonomy-*` and the plan recorded in TODO.md.
792    Atom,
793}
794
795/// A named thin resolver hook for irreducible token logic a declaration can't express — a
796/// command whose operand syntax is not getopt positional (grep's pattern disambiguation, dd's
797/// `key=value`, tar's dashless mode bundles, sed's mini-language script). The hook parses the
798/// tokens; the facets still come from the declaration + the builders.
799#[derive(Debug, Clone, Copy, PartialEq, Eq)]
800pub(crate) enum BehaviorHook {
801    Grep,
802    Dd,
803    Tar,
804    Sed,
805    Perl,
806}
807
808/// Runtime form of a `[[command.sub.flag]]` — the engine-relevant part of an escalating flag: its
809/// `name` (matched against the tokens) and the archetype it `classifies` as when present. Its
810/// research provenance (`fact`/`source`/`judgment`) lives on the TOML side and is validated at build
811/// time, not carried here.
812#[derive(Debug, Clone)]
813pub(super) struct FlagProvenance {
814    pub name: String,
815    pub classifies: String,
816    /// See `TomlSubFlag::value_prefix` — `None` = escalate on presence; `Some` = only when the
817    /// flag's value starts with this.
818    pub value_prefix: Option<String>,
819    /// See `TomlSubFlag::when_absent` — escalate when the flag is ABSENT (a safety flag whose
820    /// absence is the risk).
821    pub when_absent: bool,
822}
823
824#[derive(Debug, Clone)]
825pub(super) struct SubSpec {
826    pub name: String,
827    pub kind: DispatchKind,
828    /// The facet archetype this sub is classified as (`archetypes.toml`), if declared via
829    /// `profile = …`. The engine resolves the sub to this archetype's static capability profile
830    /// (`registry::sub_archetype`), deriving the verdict rather than taking a hand-marked level.
831    /// (Its research provenance — `fact`/`source`/`judgment` — lives on the TOML side only and is
832    /// validated at build time; it is not carried on the runtime spec.)
833    pub profile: Option<String>,
834    /// Escalating flags: each, when present, adds `classifies`'s capability to the resolved profile.
835    pub flags: Vec<FlagProvenance>,
836    /// The sub's DECLARED flag allowlist, preserved for a PROFILED sub. Its legacy `kind` is forced
837    /// to deny-all at build time, but the engine classifies a profiled sub straight from its
838    /// archetype and never reaches that kind — so without keeping the lists here the declaration
839    /// would be discarded and ANY flag would ride along on the profile. That was a fail-OPEN:
840    /// `git rebase --exec 'rm -rf /'` and `supabase db dump --frobnicate` both classified as their
841    /// benign base profile. `sub_archetypes` validates presented flags against these.
842    pub allowed_standalone: Vec<String>,
843    pub allowed_valued: Vec<String>,
844    /// The sub's declared tolerance for flags it does not enumerate — the existing, explicit way to
845    /// say "this tool's flag surface is genuinely unbounded" (a cloud API's per-service options).
846    /// Preserved alongside the allowlist so the profiled path honors it: a sub that declares it stays
847    /// open BY DECLARATION (reviewable in the TOML) rather than by a silent engine default, and a sub
848    /// that does not — `git rebase`, where a flag changes the operation — enforces.
849    pub allowed_unknown: crate::policy::UnknownTolerance,
850    /// If this sub was declared with `policy = "key"`, the referenced
851    /// handler_policy name is preserved for docs rendering so a sub
852    /// that points at a policy also shown in **Shared flag sets** can
853    /// render as a reference rather than duplicating the flag list.
854    pub policy_ref: Option<String>,
855    /// True when this sub's leaf invocation is tagged as safe-to-eval.
856    /// Walked by `registry::is_eval_safe_invocation()`.
857    pub eval_safe: bool,
858    /// Flag allowlist extending `eval_safe` — flags permitted in the
859    /// substituted invocation when the walker stops at this sub.
860    pub eval_safe_flags: Vec<String>,
861    /// Per-valued-flag value allowlist (same semantics as on
862    /// `CommandSpec`).
863    pub eval_safe_flag_values: std::collections::HashMap<String, Vec<String>>,
864    /// Flags where at least one must appear in the substituted
865    /// invocation (same semantics as on `CommandSpec`).
866    pub eval_safe_required_flags: Vec<String>,
867    /// `true` = classify this sub's first positional as a network destination onto
868    /// `locus.provenance` (see `TomlSub::network_destination`).
869    pub network_destination: bool,
870    /// A flag that overrides the positional destination (`git push --repo=…`); see
871    /// `TomlSub::destination_flag`.
872    pub destination_flag: Option<String>,
873    /// Output-file flags for a `data-export` sub; a present one adds a path-gated write capability
874    /// at the file's locus (see `TomlSub::output_path_flags`).
875    pub output_path_flags: Vec<String>,
876    /// Endpoint flags gated on naming this machine; see `TomlSub::loopback_valued`.
877    pub loopback_valued: Vec<String>,
878    /// What a loopback endpoint buys this sub; see `TomlSub::loopback_localizes`.
879    pub loopback_effect: LoopbackEffect,
880}
881
882/// What a recognized loopback destination buys a sub. Gating the FLAG and re-classifying the
883/// OPERATION are separate powers: a read only needs the former (it already passes), while a write
884/// needs the latter to stop looking like a call to a cloud service.
885#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
886pub(super) enum LoopbackEffect {
887    /// The endpoint flag is admissible when local; the classification is untouched.
888    #[default]
889    AdmitOnly,
890    /// Also clears the facets the destination determines (remote reach, net direction, payload,
891    /// metered cost), leaving the ones describing the operation alone.
892    Localizes,
893}
894
895#[derive(Debug, Clone)]
896pub(super) enum DispatchKind {
897    Policy {
898        policy: OwnedPolicy,
899        level: SafetyLevel,
900    },
901    FirstArg {
902        patterns: Vec<String>,
903        level: SafetyLevel,
904        /// Flags the glob family accepts. Empty = not yet researched (grandfathered); see
905        /// `glob_presents_unlisted_flag`.
906        standalone: Vec<String>,
907        valued: Vec<String>,
908        /// Flags admitted only when their value is a loopback endpoint.
909        loopback_valued: Vec<String>,
910    },
911    RequireAny {
912        require_any: Vec<String>,
913        policy: OwnedPolicy,
914        level: SafetyLevel,
915        accept_bare_help: bool,
916    },
917    Branching {
918        subs: Vec<SubSpec>,
919        bare_flags: Vec<String>,
920        bare_ok: bool,
921        pre_standalone: Vec<String>,
922        pre_valued: Vec<String>,
923        first_arg: Vec<String>,
924        first_arg_level: SafetyLevel,
925        /// Flags the `first_arg` glob family accepts, as in `DispatchKind::FirstArg`.
926        first_arg_standalone: Vec<String>,
927        first_arg_valued: Vec<String>,
928        first_arg_loopback_valued: Vec<String>,
929        /// First-positional globs that classify the invocation as a credential-read (deny), checked
930        /// after explicit subs and before the allow-glob. Empty for almost every command.
931        credential_first_arg: Vec<String>,
932    },
933    WriteFlagged {
934        policy: OwnedPolicy,
935        base_level: SafetyLevel,
936        write_flags: Vec<String>,
937    },
938    DelegateAfterSeparator {
939        separator: String,
940    },
941    DelegateSkip {
942        skip: usize,
943    },
944    Wrapper {
945        standalone: Vec<String>,
946        valued: Vec<String>,
947        positional_skip: usize,
948        separator: Option<String>,
949        bare_ok: bool,
950    },
951    /// A `verb-chain` grammar (`mlr`): a strict main-flag region + a `then`-chain of
952    /// allowlisted verbs. See `dispatch::dispatch_verb_chain`.
953    VerbChain(VerbChainSpec),
954    /// A code-execution command whose verdict is the execution-origin gate (worktree code
955    /// allows, foreign denies), not a flat level. See `dispatch::dispatch_executor` and
956    /// docs/design/behavioral-taxonomy-execution-origin.md.
957    Executor {
958        policy: OwnedPolicy,
959        /// Verdict for a flag-only invocation with no executor (`python3 --version`).
960        level: SafetyLevel,
961        kind: ExecutorKind,
962        /// A valued flag whose value REDIRECTS the executor out of the project
963        /// (`cargo run --manifest-path DIR/Cargo.toml`) — its value is locus-gated like a
964        /// file executor. Only meaningful for `ExecutorKind::Project`.
965        redirect_flag: Option<String>,
966        /// A predicate the executor path must satisfy, else deny (`ExecutorKind::File`).
967        /// `go run` uses `go-package` so a remote import path (`rsc.io/x@latest`) is not
968        /// treated as a worktree executor.
969        shape: Option<crate::policy::PositionalShape>,
970    },
971    Custom {
972        #[allow(dead_code)]
973        handler_name: String,
974        doc_body: Option<String>,
975        /// TOML-declared subs the handler may consult via
976        /// `registry::try_sub_dispatch()`. Empty unless the handler
977        /// uses the helper.
978        subs: Vec<SubSpec>,
979        /// TOML-declared alternate grammar the handler may consult
980        /// via `registry::try_fallback_grammar()`. `None` unless the
981        /// handler uses the helper.
982        fallback: Option<FallbackSpec>,
983        /// Named flag policies the handler consults via
984        /// `registry::check_handler_policy()`. Empty unless the handler
985        /// has dispatch logic that picks a policy by name at runtime.
986        handler_policies: std::collections::HashMap<String, OwnedPolicy>,
987        /// Sub × action matrices the handler walks via
988        /// `registry::try_matrix_dispatch()`.
989        matrices: Vec<MatrixSpec>,
990    },
991}
992
993/// How a code-execution command locates its executor. `File`: the first positional is the
994/// executor path (`bash x.sh`, `python3 x.py`, `go run ./cmd`). `Project`: the current
995/// project is the executor and there is no path operand (`cargo run`, `dotnet run`).
996#[derive(Debug, Clone, Copy, PartialEq, Eq)]
997pub(super) enum ExecutorKind {
998    File,
999    Project,
1000}
1001
1002impl ExecutorKind {
1003    pub(super) fn from_name(name: &str) -> Option<Self> {
1004        match name {
1005            "file" => Some(Self::File),
1006            "project" => Some(Self::Project),
1007            _ => None,
1008        }
1009    }
1010}
1011
1012#[derive(Debug, Clone)]
1013pub struct OwnedPolicy {
1014    pub standalone: Vec<String>,
1015    pub valued: Vec<String>,
1016    pub bare: bool,
1017    pub max_positional: Option<usize>,
1018    pub tolerance: crate::policy::FlagTolerance,
1019}
1020
1021#[derive(Debug, Clone)]
1022pub(super) struct MatrixSpec {
1023    pub parents: Vec<String>,
1024    pub level: SafetyLevel,
1025    pub actions: std::collections::HashMap<String, MatrixAction>,
1026}
1027
1028#[derive(Debug, Clone)]
1029pub(super) struct MatrixAction {
1030    pub policy_key: String,
1031    pub guard: Option<String>,
1032    pub guard_short: Option<String>,
1033}
1034
1035#[derive(Debug, Clone)]
1036pub(super) struct VerbChainSpec {
1037    pub level: SafetyLevel,
1038    pub separator: String,
1039    pub main_standalone: Vec<String>,
1040    pub main_valued: Vec<String>,
1041    pub main_variadic: Vec<String>,
1042    pub verbs: std::collections::HashSet<String>,
1043}
1044
1045#[derive(Debug, Clone)]
1046pub(super) struct FallbackSpec {
1047    pub policy: OwnedPolicy,
1048    pub level: SafetyLevel,
1049    pub positional_shape: Option<crate::policy::PositionalShape>,
1050    /// When set, the first positional is an EXECUTOR (a script/package the command runs),
1051    /// gated by the execution-origin engine instead of the flat `level`. `ExecutorKind::File`
1052    /// is the only form used by fallbacks (interpreters). See `dispatch::dispatch_executor`.
1053    pub executor: Option<ExecutorKind>,
1054    /// See `DispatchKind::Executor::redirect_flag`. Unused for `File` fallbacks.
1055    pub executor_redirect_flag: Option<String>,
1056}