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",
|
"async-trait",
|
||||||
"ceremony-engine",
|
"ceremony-engine",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"governance-types",
|
||||||
"hex",
|
"hex",
|
||||||
"registry-protocol",
|
"registry-protocol",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -461,6 +462,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
|
"governance-types",
|
||||||
"hex",
|
"hex",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"k8s-openapi",
|
"k8s-openapi",
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,6 @@ ceremony-engine = { workspace = true }
|
||||||
# Cross-workspace path deps — Guildhouse governance primitives.
|
# Cross-workspace path deps — Guildhouse governance primitives.
|
||||||
accord-core = { path = "../../guildhouse/services/accord-core" }
|
accord-core = { path = "../../guildhouse/services/accord-core" }
|
||||||
registry-protocol = { path = "../../guildhouse/services/registry-protocol" }
|
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 command;
|
||||||
pub mod scope;
|
pub mod scope;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
pub mod shell_class;
|
||||||
|
|
||||||
|
pub use shell_class::{derive_shell_class, ShellClass};
|
||||||
|
|
||||||
// Governance ceremony engine — extracted to ceremony-engine crate.
|
// Governance ceremony engine — extracted to ceremony-engine crate.
|
||||||
// Re-exported here for backward compatibility while consumers migrate.
|
// Re-exported here for backward compatibility while consumers migrate.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::shell_class::ShellClass;
|
||||||
|
|
||||||
/// Defines what an operator can do within a session.
|
/// Defines what an operator can do within a session.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SessionScope {
|
pub struct SessionScope {
|
||||||
|
|
@ -9,6 +11,17 @@ pub struct SessionScope {
|
||||||
/// Maximum mutations before session requires a new ceremony. None = unlimited.
|
/// Maximum mutations before session requires a new ceremony. None = unlimited.
|
||||||
pub mutation_budget: Option<u32>,
|
pub mutation_budget: Option<u32>,
|
||||||
pub can_delegate: bool,
|
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.
|
/// Per-namespace access rules.
|
||||||
|
|
@ -134,6 +147,8 @@ mod tests {
|
||||||
pathways: vec![ChangePathway::DryRunOnly],
|
pathways: vec![ChangePathway::DryRunOnly],
|
||||||
mutation_budget: None,
|
mutation_budget: None,
|
||||||
can_delegate: false,
|
can_delegate: false,
|
||||||
|
shell_class: ShellClass::default(),
|
||||||
|
posture_level_at_establishment: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(scope.permits("default", "", "pods", Verb::Get));
|
assert!(scope.permits("default", "", "pods", Verb::Get));
|
||||||
|
|
@ -160,6 +175,8 @@ mod tests {
|
||||||
pathways: vec![],
|
pathways: vec![],
|
||||||
mutation_budget: None,
|
mutation_budget: None,
|
||||||
can_delegate: false,
|
can_delegate: false,
|
||||||
|
shell_class: ShellClass::default(),
|
||||||
|
posture_level_at_establishment: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(scope.permits("default", "", "pods", Verb::Get));
|
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