//! The typed step vocabulary. //! //! Steps are *typed actions*, not raw command strings, so a capability check //! means something: an executor granted only `deploy`+`restart` rejects a //! `sign` step before it ever dispatches. The actual command to run still //! travels in `argv` (and `env`/`cwd`); the `action` is the capability label. use serde::{Deserialize, Serialize}; use std::path::PathBuf; /// What a step *does*, for capability gating. The label is independent of the /// concrete command in `Step::argv` — two different `sign` recipes are both /// `Action::Sign` and both gated by the `sign` grant. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Action { // Bento (app release) Build, Sign, Notarize, Staple, Package, // Sando (server promotion) Deploy, Restart, Rollback, /// Read-only host inspection. Never mutates; gated by the `observe` plane. Observe(ObserveKind), /// Escape hatch for one-off steps. Still grant-gated: a `Custom` action is /// only permitted if the grant explicitly lists that same custom name. Custom(String), } impl Action { /// Is this an actuating (mutating) action, as opposed to observe? pub fn is_actuate(&self) -> bool { !matches!(self, Action::Observe(_)) } /// The lower-case token used in topology config (`actuate = ["deploy", ...]`). /// `None` for `Observe` (those live in the `observe` list under their kind). pub fn token(&self) -> Option { Some(match self { Action::Build => "build".into(), Action::Sign => "sign".into(), Action::Notarize => "notarize".into(), Action::Staple => "staple".into(), Action::Package => "package".into(), Action::Deploy => "deploy".into(), Action::Restart => "restart".into(), Action::Rollback => "rollback".into(), Action::Custom(s) => s.clone(), Action::Observe(_) => return None, }) } /// Parse an actuate token from topology config. Unknown tokens become /// `Custom` so a config typo is a denied capability, never a silent build /// action. pub fn actuate_from_token(token: &str) -> Action { match token { "build" => Action::Build, "sign" => Action::Sign, "notarize" => Action::Notarize, "staple" => Action::Staple, "package" => Action::Package, "deploy" => Action::Deploy, "restart" => Action::Restart, "rollback" => Action::Rollback, other => Action::Custom(other.to_string()), } } } /// The kinds of read-only host inspection an `observe` grant can cover. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ObserveKind { Journal, Metrics, Health, BuildLog, Custom(String), } impl ObserveKind { pub fn token(&self) -> String { match self { ObserveKind::Journal => "journal".into(), ObserveKind::Metrics => "metrics".into(), ObserveKind::Health => "health".into(), ObserveKind::BuildLog => "build-log".into(), ObserveKind::Custom(s) => s.clone(), } } pub fn from_token(token: &str) -> ObserveKind { match token { "journal" => ObserveKind::Journal, "metrics" => ObserveKind::Metrics, "health" => ObserveKind::Health, "build-log" | "build_log" => ObserveKind::BuildLog, other => ObserveKind::Custom(other.to_string()), } } } /// One executable step: a typed action plus the command to run for it. /// /// `argv` is the command and its arguments (argv[0] is the program). A shell /// script is just `["/bin/sh", "-c", "