feat(gsh): ShellTier T0-T6 + LMDB session enrichment + GSH_SHELL_TIER
ShellTier enum (T0-T6) with tree hierarchy, satisfies(), from_shell_class() backward compat mapping. Exported as GSH_SHELL_TIER alongside GSH_SHELL_CLASS. SessionState carries shell_tier derived from AC shell_tier field, GSH_SHELL_TIER env, or shell_class mapping. Prompt shows tier: [governed] T2:tking@gsh. Optional LMDB enrichment (behind `lmdb` feature flag): reads earned credentials and identity class from substrate-identity-store, displays in banner. 16 shell_tier tests, 3 LMDB enrichment tests, 3 governance_env tests. 66 tests without lmdb, 69 with --features lmdb. Signed-off-by: Tyler J King <tking@guildhouse.dev> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tyler J King <tking@guildhouse.dev>
This commit is contained in:
parent
872a53a3c7
commit
a97e9569d6
11 changed files with 662 additions and 4 deletions
176
Cargo.lock
generated
176
Cargo.lock
generated
|
|
@ -117,6 +117,15 @@ version = "1.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
|
|
@ -141,6 +150,12 @@ version = "3.20.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
|
|
@ -293,6 +308,21 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
|
|
@ -440,6 +470,15 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doxygen-rs"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9"
|
||||
dependencies = [
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
|
|
@ -633,6 +672,44 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "heed"
|
||||
version = "0.20.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"heed-traits",
|
||||
"heed-types",
|
||||
"libc",
|
||||
"lmdb-master-sys",
|
||||
"once_cell",
|
||||
"page_size",
|
||||
"serde",
|
||||
"synchronoise",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heed-traits"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff"
|
||||
|
||||
[[package]]
|
||||
name = "heed-types"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d3f528b053a6d700b2734eabcd0fd49cb8230647aa72958467527b0b7917114"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"byteorder",
|
||||
"heed-traits",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
|
|
@ -854,6 +931,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"substrate-identity-store",
|
||||
"substrate-ipc",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
|
|
@ -888,6 +966,17 @@ version = "0.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "lmdb-master-sys"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaeb9bd22e73bd1babffff614994b341e9b2008de7bb73bf1f7e9154f1978f8b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"doxygen-rs",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
|
|
@ -1002,6 +1091,16 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
|
|
@ -1031,6 +1130,48 @@ version = "2.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
|
|
@ -1090,6 +1231,15 @@ version = "6.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
|
|
@ -1292,6 +1442,12 @@ dependencies = [
|
|||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
|
|
@ -1358,6 +1514,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "substrate-identity-store"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"heed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "substrate-ipc"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1387,6 +1554,15 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synchronoise"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2"
|
||||
dependencies = [
|
||||
"crossbeam-queue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@ colored = "2"
|
|||
atty = "0.2"
|
||||
tracing = "0.1"
|
||||
substrate-ipc = { path = "../substrate/crates/substrate-ipc" }
|
||||
substrate-identity-store = { path = "../substrate/crates/substrate-identity-store" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
libc = "0.2"
|
||||
|
|
|
|||
|
|
@ -21,3 +21,7 @@ colored = { workspace = true }
|
|||
atty = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
substrate-ipc = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
lmdb = ["libgsh/lmdb"]
|
||||
|
|
|
|||
|
|
@ -143,6 +143,15 @@ fn print_banner(session: &SessionState) {
|
|||
},
|
||||
"║".bright_blue());
|
||||
|
||||
if let Some(ref class) = session.identity_class {
|
||||
println!("{} Class: {:<44}{}", "║".bright_blue(), class, "║".bright_blue());
|
||||
}
|
||||
if !session.earned_credentials.is_empty() {
|
||||
for cred in &session.earned_credentials {
|
||||
println!("{} Credential: {:<44}{}", "║".bright_blue(), cred, "║".bright_blue());
|
||||
}
|
||||
}
|
||||
|
||||
if session.defcon_level < 5 {
|
||||
let defcon_label = match session.defcon_level {
|
||||
1 => "LOCKDOWN".red().to_string(),
|
||||
|
|
@ -205,9 +214,10 @@ fn build_prompt(session: &SessionState) -> DefaultPrompt {
|
|||
};
|
||||
|
||||
let short_name = session.display_name.split('@').next().unwrap_or(&session.display_name);
|
||||
let tier_tag = format!("T{}", session.shell_tier.numeric_level());
|
||||
|
||||
DefaultPrompt::new(
|
||||
DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, short_name)),
|
||||
DefaultPromptSegment::Basic(format!("{} {}:{}@gsh", risk_indicator, tier_tag, short_name)),
|
||||
DefaultPromptSegment::Empty,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,14 @@ chrono = { workspace = true }
|
|||
dirs = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
substrate-ipc = { workspace = true }
|
||||
substrate-identity-store = { workspace = true, optional = true }
|
||||
tokio = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
lmdb = ["substrate-identity-store"]
|
||||
|
||||
[dev-dependencies]
|
||||
substrate-identity-store = { workspace = true }
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ pub struct AuthorizationContext {
|
|||
/// `"Application"` | `"System"` (or future variants); threaded into `GSH_SHELL_CLASS`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub shell_class: Option<String>,
|
||||
/// Shell tier (0–6); threaded into `GSH_SHELL_TIER`. When absent,
|
||||
/// derived from `shell_class` (Application→T2, System→T1).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub shell_tier: Option<u8>,
|
||||
/// Capability bitmask; threaded into `GSH_CAPABILITY_SET` as `0x{:08x}`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub capability_set: Option<u32>,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
//! | `GSH_DID` | `principal.did` (canonical string) |
|
||||
//! | `GSH_ACCORD_HASH` | `accord_hash` |
|
||||
//! | `GSH_SHELL_CLASS` | `shell_class` |
|
||||
//! | `GSH_SHELL_TIER` | `shell_tier` numeric (0–6) |
|
||||
//! | `GSH_POSTURE_LEVEL` | `posture_level` (decimal) |
|
||||
//! | `GSH_CAPABILITY_SET` | `capability_set` formatted `0x{:08x}` |
|
||||
//!
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
use std::process::Command;
|
||||
|
||||
use crate::ac::AuthorizationContext;
|
||||
use crate::shell_tier::ShellTier;
|
||||
|
||||
/// Apply the `GSH_*` env-var contract to a child `Command`.
|
||||
///
|
||||
|
|
@ -30,6 +32,7 @@ pub fn apply(
|
|||
did: Option<&str>,
|
||||
accord_hash: Option<&str>,
|
||||
shell_class: Option<&str>,
|
||||
shell_tier: Option<ShellTier>,
|
||||
posture_level: Option<u8>,
|
||||
capability_set: Option<u32>,
|
||||
cap_bounding: Option<u64>,
|
||||
|
|
@ -43,6 +46,9 @@ pub fn apply(
|
|||
if let Some(c) = shell_class {
|
||||
cmd.env("GSH_SHELL_CLASS", c);
|
||||
}
|
||||
if let Some(tier) = shell_tier {
|
||||
cmd.env("GSH_SHELL_TIER", tier.numeric_level().to_string());
|
||||
}
|
||||
if let Some(p) = posture_level {
|
||||
cmd.env("GSH_POSTURE_LEVEL", p.to_string());
|
||||
}
|
||||
|
|
@ -61,11 +67,18 @@ pub fn apply_from_ac(cmd: &mut Command, ac: &AuthorizationContext) {
|
|||
.as_ref()
|
||||
.and_then(|p| p.did.as_ref())
|
||||
.map(|d| d.as_str().to_owned());
|
||||
|
||||
let tier = ac
|
||||
.shell_tier
|
||||
.and_then(ShellTier::from_numeric)
|
||||
.or_else(|| ac.shell_class.as_deref().map(ShellTier::from_shell_class));
|
||||
|
||||
apply(
|
||||
cmd,
|
||||
did.as_deref(),
|
||||
ac.accord_hash.as_deref(),
|
||||
ac.shell_class.as_deref(),
|
||||
tier,
|
||||
ac.posture_level,
|
||||
ac.capability_set,
|
||||
None,
|
||||
|
|
@ -95,6 +108,7 @@ mod tests {
|
|||
Some("did:web:guildhouse.dev:user:tking"),
|
||||
Some("sha256:abcd"),
|
||||
Some("Application"),
|
||||
Some(ShellTier::T2Operator),
|
||||
Some(3),
|
||||
Some(0xCAFEBABE),
|
||||
Some(0x0000000000200404),
|
||||
|
|
@ -105,6 +119,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(cmd_env(&cmd, "GSH_ACCORD_HASH").as_deref(), Some("sha256:abcd"));
|
||||
assert_eq!(cmd_env(&cmd, "GSH_SHELL_CLASS").as_deref(), Some("Application"));
|
||||
assert_eq!(cmd_env(&cmd, "GSH_SHELL_TIER").as_deref(), Some("2"));
|
||||
assert_eq!(cmd_env(&cmd, "GSH_POSTURE_LEVEL").as_deref(), Some("3"));
|
||||
assert_eq!(
|
||||
cmd_env(&cmd, "GSH_CAPABILITY_SET").as_deref(),
|
||||
|
|
@ -119,14 +134,23 @@ mod tests {
|
|||
#[test]
|
||||
fn apply_partial_only_did() {
|
||||
let mut cmd = Command::new("true");
|
||||
apply(&mut cmd, Some("did:web:foo:bar"), None, None, None, None, None);
|
||||
apply(&mut cmd, Some("did:web:foo:bar"), None, None, None, None, None, None);
|
||||
assert_eq!(cmd_env(&cmd, "GSH_DID").as_deref(), Some("did:web:foo:bar"));
|
||||
assert!(cmd_env(&cmd, "GSH_ACCORD_HASH").is_none());
|
||||
assert!(cmd_env(&cmd, "GSH_SHELL_CLASS").is_none());
|
||||
assert!(cmd_env(&cmd, "GSH_SHELL_TIER").is_none());
|
||||
assert!(cmd_env(&cmd, "GSH_POSTURE_LEVEL").is_none());
|
||||
assert!(cmd_env(&cmd, "GSH_CAPABILITY_SET").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_shell_tier_without_class() {
|
||||
let mut cmd = Command::new("true");
|
||||
apply(&mut cmd, None, None, None, Some(ShellTier::T3Agent), None, None, None);
|
||||
assert!(cmd_env(&cmd, "GSH_SHELL_CLASS").is_none());
|
||||
assert_eq!(cmd_env(&cmd, "GSH_SHELL_TIER").as_deref(), Some("3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_from_ac_full() {
|
||||
let ac_json = r#"{
|
||||
|
|
@ -146,6 +170,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(cmd_env(&cmd, "GSH_ACCORD_HASH").as_deref(), Some("sha256:zz"));
|
||||
assert_eq!(cmd_env(&cmd, "GSH_SHELL_CLASS").as_deref(), Some("System"));
|
||||
assert_eq!(cmd_env(&cmd, "GSH_SHELL_TIER").as_deref(), Some("1"));
|
||||
assert_eq!(cmd_env(&cmd, "GSH_POSTURE_LEVEL").as_deref(), Some("5"));
|
||||
assert_eq!(
|
||||
cmd_env(&cmd, "GSH_CAPABILITY_SET").as_deref(),
|
||||
|
|
@ -153,6 +178,21 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_from_ac_with_explicit_tier() {
|
||||
let ac_json = r#"{
|
||||
"context_id":"x",
|
||||
"principal":{"did":"did:web:example.com:user:bob"},
|
||||
"shell_class":"Application",
|
||||
"shell_tier":3
|
||||
}"#;
|
||||
let ac: AuthorizationContext = serde_json::from_str(ac_json).unwrap();
|
||||
let mut cmd = Command::new("true");
|
||||
apply_from_ac(&mut cmd, &ac);
|
||||
assert_eq!(cmd_env(&cmd, "GSH_SHELL_CLASS").as_deref(), Some("Application"));
|
||||
assert_eq!(cmd_env(&cmd, "GSH_SHELL_TIER").as_deref(), Some("3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_from_legacy_ac_no_governance_fields() {
|
||||
let ac_json = r#"{"context_id":"legacy","principal":{"did":"did:web:foo:bar"}}"#;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@ pub mod config;
|
|||
pub mod corpus;
|
||||
pub mod cr;
|
||||
pub mod governance_env;
|
||||
#[cfg(feature = "lmdb")]
|
||||
pub mod lmdb_enrichment;
|
||||
pub mod register;
|
||||
pub mod registry;
|
||||
pub mod session;
|
||||
pub mod shell_tier;
|
||||
|
||||
pub use ac::{AcValidationError, AuthorizationContext};
|
||||
pub use classifier::{classify_command, CommandClass, FREE_COMMANDS};
|
||||
|
|
@ -18,6 +21,7 @@ pub use corpus::{corpus_check, corpus_check_with_base, CorpusCheckResult, DEFAUL
|
|||
pub use cr::{post_cr, CrResult};
|
||||
pub use registry::ConsumedRegistry;
|
||||
pub use session::SessionState;
|
||||
pub use shell_tier::ShellTier;
|
||||
|
||||
/// Compute SHA-256 hash with "sha256:" prefix.
|
||||
pub fn sha256_hash(data: &[u8]) -> String {
|
||||
|
|
|
|||
95
libgsh/src/lmdb_enrichment.rs
Normal file
95
libgsh/src/lmdb_enrichment.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
//! Optional LMDB session enrichment (behind `lmdb` feature flag).
|
||||
//!
|
||||
//! Reads earned credentials, identity class, shell tier, and capability
|
||||
//! bounding set from the substrate identity store for the session's DID.
|
||||
|
||||
use substrate_identity_store::{CredentialRef, IdentityStore};
|
||||
use std::path::Path;
|
||||
|
||||
/// Enrichment data retrieved from the LMDB identity store.
|
||||
pub struct LmdbEnrichment {
|
||||
pub credentials: Vec<CredentialRef>,
|
||||
pub identity_class: String,
|
||||
pub shell_tier: u8,
|
||||
pub cap_bounding: u64,
|
||||
}
|
||||
|
||||
/// Read identity data from LMDB for the given DID.
|
||||
/// Returns `None` if LMDB is unavailable or the DID is not found.
|
||||
pub fn enrich_from_lmdb(did: &str) -> Option<LmdbEnrichment> {
|
||||
let path = std::env::var("SUBSTRATE_IDENTITY_PATH")
|
||||
.unwrap_or_else(|_| substrate_identity_store::DEFAULT_IDENTITY_PATH.to_string());
|
||||
let store = IdentityStore::open(Path::new(&path)).ok()?;
|
||||
let entry = store.get_by_did(did).ok()??;
|
||||
Some(LmdbEnrichment {
|
||||
credentials: entry.credentials,
|
||||
identity_class: entry.identity_class,
|
||||
shell_tier: entry.shell_tier,
|
||||
cap_bounding: entry.cap_bounding,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn enrich_returns_none_when_lmdb_unavailable() {
|
||||
std::env::set_var("SUBSTRATE_IDENTITY_PATH", "/nonexistent/lmdb");
|
||||
let result = enrich_from_lmdb("did:web:example.com:user:test");
|
||||
assert!(result.is_none());
|
||||
std::env::remove_var("SUBSTRATE_IDENTITY_PATH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrich_returns_none_for_unknown_did() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let _store = IdentityStore::open(dir.path()).unwrap();
|
||||
std::env::set_var("SUBSTRATE_IDENTITY_PATH", dir.path().to_str().unwrap());
|
||||
let result = enrich_from_lmdb("did:web:example.com:user:nobody");
|
||||
assert!(result.is_none());
|
||||
std::env::remove_var("SUBSTRATE_IDENTITY_PATH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrich_returns_data_for_known_did() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = IdentityStore::open(dir.path()).unwrap();
|
||||
let entry = substrate_identity_store::IdentityEntry {
|
||||
did: "did:web:example.com:user:tking".into(),
|
||||
uid: 60001,
|
||||
gid: 60001,
|
||||
login_name: "tking".into(),
|
||||
home_directory: "/home/tking".into(),
|
||||
login_shell: "/usr/bin/gsh".into(),
|
||||
gecos: String::new(),
|
||||
supplementary_groups: vec![],
|
||||
shell_tier: 2,
|
||||
cap_bounding: 0x0404,
|
||||
cap_ambient: 0,
|
||||
namespace_flags: 0,
|
||||
identity_class: "Authority".into(),
|
||||
credentials: vec![substrate_identity_store::CredentialRef {
|
||||
vc_hash: "sha256:abc".into(),
|
||||
capability: "CAP_DEPLOY".into(),
|
||||
issuer_did: "did:web:example.com:authority:root".into(),
|
||||
earned_at: "2026-05-28".into(),
|
||||
ceremony_id: "cer-001".into(),
|
||||
corpus_evidence_root: "sha256:def".into(),
|
||||
}],
|
||||
forge_sources: vec![],
|
||||
generation: 1,
|
||||
last_updated_ns: 0,
|
||||
source_resource_version: "rv-1".into(),
|
||||
};
|
||||
store.put_identity(&entry).unwrap();
|
||||
|
||||
std::env::set_var("SUBSTRATE_IDENTITY_PATH", dir.path().to_str().unwrap());
|
||||
let result = enrich_from_lmdb("did:web:example.com:user:tking").unwrap();
|
||||
assert_eq!(result.identity_class, "Authority");
|
||||
assert_eq!(result.credentials.len(), 1);
|
||||
assert_eq!(result.credentials[0].capability, "CAP_DEPLOY");
|
||||
assert_eq!(result.shell_tier, 2);
|
||||
std::env::remove_var("SUBSTRATE_IDENTITY_PATH");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! Session state tracking for human mode.
|
||||
|
||||
use crate::ac::AuthorizationContext;
|
||||
use crate::shell_tier::ShellTier;
|
||||
|
||||
/// Tracks session state across the REPL loop.
|
||||
pub struct SessionState {
|
||||
|
|
@ -22,8 +23,15 @@ pub struct SessionState {
|
|||
/// `GSH_*` env vars via [`SessionState::apply_governance_env`].
|
||||
pub accord_hash: Option<String>,
|
||||
pub shell_class: Option<String>,
|
||||
pub shell_tier: ShellTier,
|
||||
pub capability_set: Option<u32>,
|
||||
pub posture_level: Option<u8>,
|
||||
/// Earned credentials from LMDB identity store, formatted as
|
||||
/// "capability (issuer_did)". Populated only when the `lmdb`
|
||||
/// feature is enabled and the DID is found in the store.
|
||||
pub earned_credentials: Vec<String>,
|
||||
/// Identity class from LMDB (e.g. "Authority", "Operator").
|
||||
pub identity_class: Option<String>,
|
||||
}
|
||||
|
||||
impl SessionState {
|
||||
|
|
@ -57,7 +65,7 @@ impl SessionState {
|
|||
.ok().and_then(|v| v.parse().ok()).unwrap_or(5);
|
||||
let defcon_reason = std::env::var("BASCULE_DEFCON_REASON").ok();
|
||||
|
||||
Self {
|
||||
let mut session = Self {
|
||||
ac_id: ac.context_id.clone(),
|
||||
corpus_cid: corpus_cid.to_string(),
|
||||
principal,
|
||||
|
|
@ -73,9 +81,24 @@ impl SessionState {
|
|||
denied_count: 0,
|
||||
accord_hash: ac.accord_hash.clone(),
|
||||
shell_class: ac.shell_class.clone(),
|
||||
shell_tier: resolve_shell_tier(ac.shell_tier, ac.shell_class.as_deref()),
|
||||
capability_set: ac.capability_set,
|
||||
posture_level: ac.posture_level,
|
||||
earned_credentials: Vec::new(),
|
||||
identity_class: None,
|
||||
};
|
||||
|
||||
#[cfg(feature = "lmdb")]
|
||||
{
|
||||
if let Some(enrichment) = crate::lmdb_enrichment::enrich_from_lmdb(&session.principal) {
|
||||
session.earned_credentials = enrichment.credentials.iter()
|
||||
.map(|c| format!("{} ({})", c.capability, c.issuer_did))
|
||||
.collect();
|
||||
session.identity_class = Some(enrichment.identity_class);
|
||||
}
|
||||
}
|
||||
|
||||
session
|
||||
}
|
||||
|
||||
/// Create a minimal session for ungoverned mode.
|
||||
|
|
@ -93,7 +116,7 @@ impl SessionState {
|
|||
.ok().and_then(|v| v.parse().ok()).unwrap_or(5);
|
||||
let defcon_reason = std::env::var("BASCULE_DEFCON_REASON").ok();
|
||||
|
||||
Self {
|
||||
let mut session = Self {
|
||||
ac_id: "ungoverned".to_string(),
|
||||
corpus_cid: corpus_cid.to_string(),
|
||||
principal,
|
||||
|
|
@ -109,9 +132,24 @@ impl SessionState {
|
|||
denied_count: 0,
|
||||
accord_hash: None,
|
||||
shell_class: None,
|
||||
shell_tier: resolve_shell_tier(None, None),
|
||||
capability_set: None,
|
||||
posture_level: None,
|
||||
earned_credentials: Vec::new(),
|
||||
identity_class: None,
|
||||
};
|
||||
|
||||
#[cfg(feature = "lmdb")]
|
||||
{
|
||||
if let Some(enrichment) = crate::lmdb_enrichment::enrich_from_lmdb(&session.principal) {
|
||||
session.earned_credentials = enrichment.credentials.iter()
|
||||
.map(|c| format!("{} ({})", c.capability, c.issuer_did))
|
||||
.collect();
|
||||
session.identity_class = Some(enrichment.identity_class);
|
||||
}
|
||||
}
|
||||
|
||||
session
|
||||
}
|
||||
|
||||
/// Apply the `GSH_*` env-var contract to a child `Command`.
|
||||
|
|
@ -127,6 +165,7 @@ impl SessionState {
|
|||
Some(&self.principal),
|
||||
self.accord_hash.as_deref(),
|
||||
self.shell_class.as_deref(),
|
||||
Some(self.shell_tier),
|
||||
self.posture_level,
|
||||
self.capability_set,
|
||||
None,
|
||||
|
|
@ -145,6 +184,23 @@ impl SessionState {
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_shell_tier(explicit: Option<u8>, shell_class: Option<&str>) -> ShellTier {
|
||||
if let Some(n) = explicit {
|
||||
if let Some(tier) = ShellTier::from_numeric(n) {
|
||||
return tier;
|
||||
}
|
||||
}
|
||||
if let Some(env_tier) = std::env::var("GSH_SHELL_TIER").ok() {
|
||||
if let Ok(tier) = env_tier.parse::<ShellTier>() {
|
||||
return tier;
|
||||
}
|
||||
}
|
||||
if let Some(class) = shell_class {
|
||||
return ShellTier::from_shell_class(class);
|
||||
}
|
||||
ShellTier::default()
|
||||
}
|
||||
|
||||
fn whoami() -> String {
|
||||
std::env::var("USER")
|
||||
.or_else(|_| std::env::var("USERNAME"))
|
||||
|
|
|
|||
263
libgsh/src/shell_tier.rs
Normal file
263
libgsh/src/shell_tier.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
// Copyright 2026 Guildhouse Dev
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Typed shell hierarchy (T0–T6).
|
||||
//!
|
||||
//! Local definition matching org-ops-core's `ShellTier`. Kept here to
|
||||
//! avoid pulling the full org-ops-core dependency tree into gsh. If a
|
||||
//! shared governance-types crate is created, consolidate there.
|
||||
//!
|
||||
//! ```text
|
||||
//! T0 Genesis → T1 Infrastructure → ┬─ T2 Operator → ┬─ T3 Agent
|
||||
//! │ └─ T4 Task
|
||||
//! ├─ T5 Forensic
|
||||
//! └─ T6 Recovery
|
||||
//! ```
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ShellTier {
|
||||
T0Genesis,
|
||||
T1Infrastructure,
|
||||
T2Operator,
|
||||
T3Agent,
|
||||
T4Task,
|
||||
T5Forensic,
|
||||
T6Recovery,
|
||||
}
|
||||
|
||||
impl ShellTier {
|
||||
pub fn numeric_level(&self) -> u8 {
|
||||
match self {
|
||||
Self::T0Genesis => 0,
|
||||
Self::T1Infrastructure => 1,
|
||||
Self::T2Operator => 2,
|
||||
Self::T3Agent => 3,
|
||||
Self::T4Task => 4,
|
||||
Self::T5Forensic => 5,
|
||||
Self::T6Recovery => 6,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_numeric(n: u8) -> Option<Self> {
|
||||
match n {
|
||||
0 => Some(Self::T0Genesis),
|
||||
1 => Some(Self::T1Infrastructure),
|
||||
2 => Some(Self::T2Operator),
|
||||
3 => Some(Self::T3Agent),
|
||||
4 => Some(Self::T4Task),
|
||||
5 => Some(Self::T5Forensic),
|
||||
6 => Some(Self::T6Recovery),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::T0Genesis => "Genesis",
|
||||
Self::T1Infrastructure => "Infrastructure",
|
||||
Self::T2Operator => "Operator",
|
||||
Self::T3Agent => "Agent",
|
||||
Self::T4Task => "Task",
|
||||
Self::T5Forensic => "Forensic",
|
||||
Self::T6Recovery => "Recovery",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<ShellTier> {
|
||||
match self {
|
||||
Self::T0Genesis => None,
|
||||
Self::T1Infrastructure => Some(Self::T0Genesis),
|
||||
Self::T2Operator => Some(Self::T1Infrastructure),
|
||||
Self::T3Agent => Some(Self::T2Operator),
|
||||
Self::T4Task => Some(Self::T2Operator),
|
||||
Self::T5Forensic => Some(Self::T1Infrastructure),
|
||||
Self::T6Recovery => Some(Self::T1Infrastructure),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn satisfies(&self, required: ShellTier) -> bool {
|
||||
if *self == required {
|
||||
return true;
|
||||
}
|
||||
let mut current = required;
|
||||
while let Some(p) = current.parent() {
|
||||
if p == *self {
|
||||
return true;
|
||||
}
|
||||
current = p;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Map a legacy `GSH_SHELL_CLASS` string to a tier.
|
||||
pub fn from_shell_class(class: &str) -> Self {
|
||||
match class {
|
||||
"System" => Self::T1Infrastructure,
|
||||
"Application" => Self::T2Operator,
|
||||
_ => Self::T2Operator,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the legacy shell class string for backward compatibility.
|
||||
pub fn to_shell_class(&self) -> &'static str {
|
||||
match self {
|
||||
Self::T0Genesis | Self::T1Infrastructure => "System",
|
||||
_ => "Application",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShellTier {
|
||||
fn default() -> Self {
|
||||
ShellTier::T2Operator
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ShellTier {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "T{} ({})", self.numeric_level(), self.label())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ShellTier {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(n) = s.parse::<u8>() {
|
||||
return Self::from_numeric(n).ok_or_else(|| format!("unknown shell tier: {s}"));
|
||||
}
|
||||
match s.to_lowercase().trim() {
|
||||
"t0" | "genesis" | "t0genesis" => Ok(Self::T0Genesis),
|
||||
"t1" | "infrastructure" | "t1infrastructure" => Ok(Self::T1Infrastructure),
|
||||
"t2" | "operator" | "t2operator" => Ok(Self::T2Operator),
|
||||
"t3" | "agent" | "t3agent" => Ok(Self::T3Agent),
|
||||
"t4" | "task" | "t4task" => Ok(Self::T4Task),
|
||||
"t5" | "forensic" | "t5forensic" => Ok(Self::T5Forensic),
|
||||
"t6" | "recovery" | "t6recovery" => Ok(Self::T6Recovery),
|
||||
other => Err(format!("unknown shell tier: {other}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_t2() {
|
||||
assert_eq!(ShellTier::default(), ShellTier::T2Operator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_format() {
|
||||
assert_eq!(format!("{}", ShellTier::T0Genesis), "T0 (Genesis)");
|
||||
assert_eq!(format!("{}", ShellTier::T2Operator), "T2 (Operator)");
|
||||
assert_eq!(format!("{}", ShellTier::T5Forensic), "T5 (Forensic)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_canonical_short() {
|
||||
assert_eq!("t0".parse::<ShellTier>().unwrap(), ShellTier::T0Genesis);
|
||||
assert_eq!("T2".parse::<ShellTier>().unwrap(), ShellTier::T2Operator);
|
||||
assert_eq!("T6".parse::<ShellTier>().unwrap(), ShellTier::T6Recovery);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_numeric_string() {
|
||||
assert_eq!("0".parse::<ShellTier>().unwrap(), ShellTier::T0Genesis);
|
||||
assert_eq!("3".parse::<ShellTier>().unwrap(), ShellTier::T3Agent);
|
||||
assert_eq!("6".parse::<ShellTier>().unwrap(), ShellTier::T6Recovery);
|
||||
assert!("7".parse::<ShellTier>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_label() {
|
||||
assert_eq!("genesis".parse::<ShellTier>().unwrap(), ShellTier::T0Genesis);
|
||||
assert_eq!("operator".parse::<ShellTier>().unwrap(), ShellTier::T2Operator);
|
||||
assert_eq!("forensic".parse::<ShellTier>().unwrap(), ShellTier::T5Forensic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid() {
|
||||
assert!("t7".parse::<ShellTier>().is_err());
|
||||
assert!("bogus".parse::<ShellTier>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_tree() {
|
||||
assert_eq!(ShellTier::T0Genesis.parent(), None);
|
||||
assert_eq!(ShellTier::T1Infrastructure.parent(), Some(ShellTier::T0Genesis));
|
||||
assert_eq!(ShellTier::T2Operator.parent(), Some(ShellTier::T1Infrastructure));
|
||||
assert_eq!(ShellTier::T3Agent.parent(), Some(ShellTier::T2Operator));
|
||||
assert_eq!(ShellTier::T4Task.parent(), Some(ShellTier::T2Operator));
|
||||
assert_eq!(ShellTier::T5Forensic.parent(), Some(ShellTier::T1Infrastructure));
|
||||
assert_eq!(ShellTier::T6Recovery.parent(), Some(ShellTier::T1Infrastructure));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn satisfies_self() {
|
||||
for tier in [
|
||||
ShellTier::T0Genesis, ShellTier::T1Infrastructure, ShellTier::T2Operator,
|
||||
ShellTier::T3Agent, ShellTier::T4Task, ShellTier::T5Forensic, ShellTier::T6Recovery,
|
||||
] {
|
||||
assert!(tier.satisfies(tier));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t0_satisfies_everything() {
|
||||
assert!(ShellTier::T0Genesis.satisfies(ShellTier::T4Task));
|
||||
assert!(ShellTier::T0Genesis.satisfies(ShellTier::T6Recovery));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2_satisfies_t3_t4_not_t5_t6() {
|
||||
assert!(ShellTier::T2Operator.satisfies(ShellTier::T3Agent));
|
||||
assert!(ShellTier::T2Operator.satisfies(ShellTier::T4Task));
|
||||
assert!(!ShellTier::T2Operator.satisfies(ShellTier::T5Forensic));
|
||||
assert!(!ShellTier::T2Operator.satisfies(ShellTier::T6Recovery));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaf_does_not_satisfy_sibling() {
|
||||
assert!(!ShellTier::T3Agent.satisfies(ShellTier::T4Task));
|
||||
assert!(!ShellTier::T5Forensic.satisfies(ShellTier::T6Recovery));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_shell_class_mapping() {
|
||||
assert_eq!(ShellTier::from_shell_class("Application"), ShellTier::T2Operator);
|
||||
assert_eq!(ShellTier::from_shell_class("System"), ShellTier::T1Infrastructure);
|
||||
assert_eq!(ShellTier::from_shell_class("Unknown"), ShellTier::T2Operator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_shell_class_backward_compat() {
|
||||
assert_eq!(ShellTier::T0Genesis.to_shell_class(), "System");
|
||||
assert_eq!(ShellTier::T1Infrastructure.to_shell_class(), "System");
|
||||
assert_eq!(ShellTier::T2Operator.to_shell_class(), "Application");
|
||||
assert_eq!(ShellTier::T3Agent.to_shell_class(), "Application");
|
||||
assert_eq!(ShellTier::T5Forensic.to_shell_class(), "Application");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_levels() {
|
||||
assert_eq!(ShellTier::T0Genesis.numeric_level(), 0);
|
||||
assert_eq!(ShellTier::T3Agent.numeric_level(), 3);
|
||||
assert_eq!(ShellTier::T6Recovery.numeric_level(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_numeric_round_trip() {
|
||||
for n in 0..=6u8 {
|
||||
let tier = ShellTier::from_numeric(n).unwrap();
|
||||
assert_eq!(tier.numeric_level(), n);
|
||||
}
|
||||
assert!(ShellTier::from_numeric(7).is_none());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue