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