diff --git a/Cargo.lock b/Cargo.lock index 40155fd..2750f94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,14 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bxnet-ops" +version = "0.1.0" +dependencies = [ + "anyhow", + "org-ops-core", +] + [[package]] name = "bytes" version = "1.11.1" @@ -382,11 +390,14 @@ dependencies = [ ] [[package]] -name = "guildhouse-ops" +name = "governance-types" version = "0.1.0" dependencies = [ - "anyhow", - "org-ops-core", + "hex", + "serde", + "serde_json", + "sha1", + "sha2", ] [[package]] @@ -429,6 +440,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.0" @@ -838,11 +855,14 @@ dependencies = [ "anyhow", "base64", "clap", + "governance-types", + "hex", "rand", "reqwest", "serde", "serde_json", "sha2", + "tempfile", "urlencoding", "uuid", ] @@ -1160,6 +1180,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" diff --git a/org-ops-core/src/lib.rs b/org-ops-core/src/lib.rs index 0ff3fcd..b42644d 100644 --- a/org-ops-core/src/lib.rs +++ b/org-ops-core/src/lib.rs @@ -19,6 +19,7 @@ pub mod pkce; pub mod playbook_commands; pub mod score_fetcher; pub mod session; +pub mod shell_class; pub mod traits; pub mod gsap_client; @@ -27,6 +28,7 @@ pub use config::OrgOpsConfig; pub use playbook_commands::PlaybookCommands; pub use display::SessionBanner; pub use git_commands::{GitConfig, GovernedGitCommands}; +pub use shell_class::ShellClass; pub use traits::{OrgCommands, RiskScorer}; /// The main entry point. Build with your config and commands, then call run(). @@ -88,9 +90,22 @@ impl OrgOps { org_name: self.config.org_name.clone(), trust_domain: self.config.trust_domain.clone(), bascule_endpoint: self.config.bascule_endpoint.clone(), + shell_class: shell_class::ShellClass::default(), + posture_level: 5, // Normal — default for local/unconnected }; for cmd in &self.commands { if cmd.handles(name) { + let required = cmd.required_shell_class(); + if !ctx.shell_class.satisfies(required) { + anyhow::bail!( + "Command '{}' requires {} shell, but session is {}. \ + Reconnect to a host with attested posture \ + (TPM + IMA verified) to get a System session.", + name, + required, + ctx.shell_class + ); + } return cmd.handle(name, sub, &ctx); } } diff --git a/org-ops-core/src/session.rs b/org-ops-core/src/session.rs index e9e5b1e..b5e9719 100644 --- a/org-ops-core/src/session.rs +++ b/org-ops-core/src/session.rs @@ -1,6 +1,18 @@ +// Copyright 2026 Guildhouse Dev +// SPDX-License-Identifier: Apache-2.0 + +use crate::shell_class::ShellClass; + #[derive(Debug, Clone)] pub struct SessionContext { pub org_name: String, pub trust_domain: String, pub bascule_endpoint: String, + /// Shell class for this session (Application or System). + /// Populated from Bascule SessionScope when connected via gsh. + /// Defaults to Application for local/unconnected usage. + pub shell_class: ShellClass, + /// Posture level wire value (1-5) at session establishment. + /// Defaults to 5 (Normal) for local/unconnected usage. + pub posture_level: u8, } diff --git a/org-ops-core/src/shell_class.rs b/org-ops-core/src/shell_class.rs new file mode 100644 index 0000000..520b1b9 --- /dev/null +++ b/org-ops-core/src/shell_class.rs @@ -0,0 +1,77 @@ +// Copyright 2026 Guildhouse Dev +// SPDX-License-Identifier: Apache-2.0 + +//! Shell class for org-ops dispatch enforcement. +//! +//! Mirrors `bascule_core::ShellClass` without pulling in the full +//! bascule-core dependency tree. The canonical definition lives in +//! bascule-core; this is a lightweight duplicate for the CLI layer. + +use std::fmt; + +/// Classification of a governed shell session. +/// +/// - `Application` — software operations (deploy, query, playbooks). +/// - `System` — host operations (kernel, firmware, network config). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShellClass { + Application, + System, +} + +impl Default for ShellClass { + fn default() -> Self { + ShellClass::Application + } +} + +impl fmt::Display for ShellClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ShellClass::Application => write!(f, "application"), + ShellClass::System => write!(f, "system"), + } + } +} + +impl ShellClass { + /// Returns true if `required` is satisfied by `self`. + /// + /// System satisfies both System and Application requirements. + /// Application satisfies only Application requirements. + pub fn satisfies(&self, required: ShellClass) -> bool { + match (self, required) { + (ShellClass::System, _) => true, + (ShellClass::Application, ShellClass::Application) => true, + (ShellClass::Application, ShellClass::System) => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_is_application() { + assert_eq!(ShellClass::default(), ShellClass::Application); + } + + #[test] + fn system_satisfies_both() { + assert!(ShellClass::System.satisfies(ShellClass::System)); + assert!(ShellClass::System.satisfies(ShellClass::Application)); + } + + #[test] + fn application_only_satisfies_application() { + assert!(ShellClass::Application.satisfies(ShellClass::Application)); + assert!(!ShellClass::Application.satisfies(ShellClass::System)); + } + + #[test] + fn display() { + assert_eq!(format!("{}", ShellClass::Application), "application"); + assert_eq!(format!("{}", ShellClass::System), "system"); + } +} diff --git a/org-ops-core/src/traits.rs b/org-ops-core/src/traits.rs index f95ce5b..bf9f5b0 100644 --- a/org-ops-core/src/traits.rs +++ b/org-ops-core/src/traits.rs @@ -1,4 +1,5 @@ use crate::session::SessionContext; +use crate::shell_class::ShellClass; /// Implement this to add org-specific subcommands. pub trait OrgCommands: Send + Sync { @@ -10,6 +11,15 @@ pub trait OrgCommands: Send + Sync { matches: &clap::ArgMatches, ctx: &SessionContext, ) -> anyhow::Result<()>; + + /// Minimum shell class required to execute commands from this module. + /// + /// Default: `Application` — backward compatible, all existing modules + /// work without changes. Override to `System` for modules that perform + /// host-level operations (kernel, firmware, network config). + fn required_shell_class(&self) -> ShellClass { + ShellClass::Application + } } /// Implement this for custom risk scoring.