feat(bascule-core): add ShellClass enum with posture-based derivation
Introduce ShellClass (Application | System) as a session-scoped classification derived from PostureLevel at ceremony grant time. - ShellClass::Application: default, software operations only - ShellClass::System: host operations, requires Normal (5) posture - derive_shell_class(): pure function, configurable threshold - satisfies(): hierarchical check (System satisfies Application) - No mid-session upgrade by design (immutable in SessionScope) Added shell_class and posture_level_at_establishment to SessionScope with #[serde(default)] for backward-compatible deserialization. Signed-off-by: Tyler King <tking@guildhouse.dev> Signed-off-by: Tyler J King <tking727@gmail.com>
This commit is contained in:
parent
47a5484614
commit
e28be3335d
5 changed files with 192 additions and 0 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -424,6 +424,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"ceremony-engine",
|
||||
"chrono",
|
||||
"governance-types",
|
||||
"hex",
|
||||
"registry-protocol",
|
||||
"serde",
|
||||
|
|
@ -461,6 +462,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"config",
|
||||
"dashmap",
|
||||
"governance-types",
|
||||
"hex",
|
||||
"jsonwebtoken",
|
||||
"k8s-openapi",
|
||||
|
|
|
|||
|
|
@ -21,3 +21,6 @@ ceremony-engine = { workspace = true }
|
|||
# Cross-workspace path deps — Guildhouse governance primitives.
|
||||
accord-core = { path = "../../guildhouse/services/accord-core" }
|
||||
registry-protocol = { path = "../../guildhouse/services/registry-protocol" }
|
||||
|
||||
# Cross-workspace path dep — substrate governance types (for PostureLevel).
|
||||
governance-types = { path = "../../substrate/crates/governance-types" }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ pub mod ceremony;
|
|||
pub mod command;
|
||||
pub mod scope;
|
||||
pub mod session;
|
||||
pub mod shell_class;
|
||||
|
||||
pub use shell_class::{derive_shell_class, ShellClass};
|
||||
|
||||
// Governance ceremony engine — extracted to ceremony-engine crate.
|
||||
// Re-exported here for backward compatibility while consumers migrate.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::shell_class::ShellClass;
|
||||
|
||||
/// Defines what an operator can do within a session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionScope {
|
||||
|
|
@ -9,6 +11,17 @@ pub struct SessionScope {
|
|||
/// Maximum mutations before session requires a new ceremony. None = unlimited.
|
||||
pub mutation_budget: Option<u32>,
|
||||
pub can_delegate: bool,
|
||||
|
||||
/// Shell class for this session, derived from posture at establishment.
|
||||
/// Immutable after creation — downgrade creates a new scope, upgrade
|
||||
/// requires a new ceremony.
|
||||
#[serde(default)]
|
||||
pub shell_class: ShellClass,
|
||||
|
||||
/// Posture level (wire value 1-5) at time of session establishment.
|
||||
/// Stored for audit (Chronicle) and for posture breach comparison.
|
||||
#[serde(default)]
|
||||
pub posture_level_at_establishment: Option<u8>,
|
||||
}
|
||||
|
||||
/// Per-namespace access rules.
|
||||
|
|
@ -134,6 +147,8 @@ mod tests {
|
|||
pathways: vec![ChangePathway::DryRunOnly],
|
||||
mutation_budget: None,
|
||||
can_delegate: false,
|
||||
shell_class: ShellClass::default(),
|
||||
posture_level_at_establishment: None,
|
||||
};
|
||||
|
||||
assert!(scope.permits("default", "", "pods", Verb::Get));
|
||||
|
|
@ -160,6 +175,8 @@ mod tests {
|
|||
pathways: vec![],
|
||||
mutation_budget: None,
|
||||
can_delegate: false,
|
||||
shell_class: ShellClass::default(),
|
||||
posture_level_at_establishment: None,
|
||||
};
|
||||
|
||||
assert!(scope.permits("default", "", "pods", Verb::Get));
|
||||
|
|
|
|||
167
bascule-core/src/shell_class.rs
Normal file
167
bascule-core/src/shell_class.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// Copyright 2026 Guildhouse Dev
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Shell class — session-scoped classification derived from posture.
|
||||
//!
|
||||
//! Determines what class of operations a governed shell session permits.
|
||||
//! Derived from [`PostureLevel`] at ceremony grant time and immutable for
|
||||
//! the session lifetime. Upgrade requires a new ceremony; downgrade can
|
||||
//! occur on posture breach.
|
||||
|
||||
use governance_types::PostureLevel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
/// Classification of a governed shell session.
|
||||
///
|
||||
/// - `Application` — software operations (deploy, query, playbooks).
|
||||
/// - `System` — host operations (kernel modules, firmware, storage).
|
||||
///
|
||||
/// This is a Bascule concept (session-scoped), distinct from the
|
||||
/// substrate-wide `PostureLevel` (host-scoped, continuous).
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ShellClass {
|
||||
/// Software operations only. The host's kernel and firmware
|
||||
/// integrity is not verified. Default for all sessions unless
|
||||
/// attestation elevates to System.
|
||||
#[default]
|
||||
Application,
|
||||
|
||||
/// Host operations. Kernel modules, firmware updates, network
|
||||
/// config, storage management, security policy changes. Requires
|
||||
/// the host to be in an attested posture (TPM + IMA verified).
|
||||
System,
|
||||
}
|
||||
|
||||
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 this class permits system-level operations.
|
||||
pub fn is_system(&self) -> bool {
|
||||
matches!(self, ShellClass::System)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive `ShellClass` from a `PostureLevel` and an optional threshold.
|
||||
///
|
||||
/// Uses the **operational** PostureLevel semantic (Lockdown=1 → Normal=5).
|
||||
/// Default threshold: `PostureLevel::Normal` — the host must be in normal
|
||||
/// operational posture for System access. Any DEFCON escalation restricts
|
||||
/// to Application shells.
|
||||
///
|
||||
/// The Accord's WitnessConfig can override the threshold via a
|
||||
/// `PostureCondition` with kind `"system_shell_minimum"`.
|
||||
///
|
||||
/// # Dual PostureLevel semantic note
|
||||
///
|
||||
/// governance-types defines PostureLevel on the operational scale
|
||||
/// (Lockdown < Critical < Restricted < Elevated < Normal). The
|
||||
/// session/attestation scale (None < Local < Verified < Governed <
|
||||
/// Attested) lives in the proto layer. This function uses the
|
||||
/// operational scale because that's what the posture-current ConfigMap
|
||||
/// provides. When the two semantics are disambiguated into separate
|
||||
/// types, this function should migrate to the attestation type.
|
||||
pub fn derive_shell_class(
|
||||
posture_level: PostureLevel,
|
||||
system_threshold: Option<PostureLevel>,
|
||||
) -> ShellClass {
|
||||
let threshold = system_threshold.unwrap_or(PostureLevel::Normal);
|
||||
|
||||
// PostureLevel derives Ord: Lockdown(1) < Normal(5).
|
||||
// posture_level >= threshold means "at least as permissive as the threshold."
|
||||
if posture_level >= threshold {
|
||||
ShellClass::System
|
||||
} else {
|
||||
ShellClass::Application
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_application() {
|
||||
assert_eq!(ShellClass::default(), ShellClass::Application);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_normal_posture_is_system() {
|
||||
let class = derive_shell_class(PostureLevel::Normal, None);
|
||||
assert_eq!(class, ShellClass::System);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_elevated_posture_is_application() {
|
||||
let class = derive_shell_class(PostureLevel::Elevated, None);
|
||||
assert_eq!(class, ShellClass::Application);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_lockdown_is_application() {
|
||||
let class = derive_shell_class(PostureLevel::Lockdown, None);
|
||||
assert_eq!(class, ShellClass::Application);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_with_custom_threshold() {
|
||||
// Lower threshold: Elevated (4) or above gets System
|
||||
let class = derive_shell_class(PostureLevel::Elevated, Some(PostureLevel::Elevated));
|
||||
assert_eq!(class, ShellClass::System);
|
||||
|
||||
let class = derive_shell_class(PostureLevel::Restricted, Some(PostureLevel::Elevated));
|
||||
assert_eq!(class, ShellClass::Application);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn satisfies_system_satisfies_both() {
|
||||
assert!(ShellClass::System.satisfies(ShellClass::System));
|
||||
assert!(ShellClass::System.satisfies(ShellClass::Application));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn satisfies_application_only_satisfies_application() {
|
||||
assert!(ShellClass::Application.satisfies(ShellClass::Application));
|
||||
assert!(!ShellClass::Application.satisfies(ShellClass::System));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_formatting() {
|
||||
assert_eq!(format!("{}", ShellClass::Application), "application");
|
||||
assert_eq!(format!("{}", ShellClass::System), "system");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip() {
|
||||
let json = serde_json::to_string(&ShellClass::System).unwrap();
|
||||
assert_eq!(json, "\"system\"");
|
||||
let parsed: ShellClass = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed, ShellClass::System);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_system() {
|
||||
assert!(ShellClass::System.is_system());
|
||||
assert!(!ShellClass::Application.is_system());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue