gsh/libgsh/src/governance_env.rs
Tyler J King a97e9569d6 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>
2026-05-30 11:46:41 -04:00

209 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `GSH_*` env-var contract for child processes spawned inside gsh.
//!
//! org-ops-core (substrate-level operations library) reads these
//! discrete env vars to construct a `GshContext` without re-parsing
//! the `GSAP_SESSION_AC` JSON blob:
//!
//! | Variable | Source |
//! |----------------------|---------------------------------------|
//! | `GSH_DID` | `principal.did` (canonical string) |
//! | `GSH_ACCORD_HASH` | `accord_hash` |
//! | `GSH_SHELL_CLASS` | `shell_class` |
//! | `GSH_SHELL_TIER` | `shell_tier` numeric (06) |
//! | `GSH_POSTURE_LEVEL` | `posture_level` (decimal) |
//! | `GSH_CAPABILITY_SET` | `capability_set` formatted `0x{:08x}` |
//!
//! The legacy `GSAP_SESSION_*` exports are kept by the gsh binary for
//! existing consumers; this module is purely additive.
use std::process::Command;
use crate::ac::AuthorizationContext;
use crate::shell_tier::ShellTier;
/// Apply the `GSH_*` env-var contract to a child `Command`.
///
/// Each parameter is `Option`-typed; absent values leave the
/// corresponding env var unset (the child sees no `GSH_FOO` rather
/// than an empty `GSH_FOO`). Caller passes whatever subset is known —
/// ungoverned mode might supply only `did`.
pub fn apply(
cmd: &mut Command,
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>,
) {
if let Some(d) = did {
cmd.env("GSH_DID", d);
}
if let Some(h) = accord_hash {
cmd.env("GSH_ACCORD_HASH", h);
}
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());
}
if let Some(c) = capability_set {
cmd.env("GSH_CAPABILITY_SET", format!("0x{:08x}", c));
}
if let Some(b) = cap_bounding {
cmd.env("GSH_CAP_BOUNDING", format!("0x{b:016x}"));
}
}
/// Apply the `GSH_*` env-var contract from a parsed AC.
pub fn apply_from_ac(cmd: &mut Command, ac: &AuthorizationContext) {
let did = ac
.principal
.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,
);
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn cmd_env(cmd: &Command, key: &str) -> Option<String> {
cmd.get_envs().find_map(|(k, v)| {
if k == std::ffi::OsStr::new(key) {
v.map(|s| s.to_string_lossy().into_owned())
} else {
None
}
})
}
#[test]
fn apply_all_fields() {
let mut cmd = Command::new("true");
apply(
&mut cmd,
Some("did:web:guildhouse.dev:user:tking"),
Some("sha256:abcd"),
Some("Application"),
Some(ShellTier::T2Operator),
Some(3),
Some(0xCAFEBABE),
Some(0x0000000000200404),
);
assert_eq!(
cmd_env(&cmd, "GSH_DID").as_deref(),
Some("did:web:guildhouse.dev:user:tking")
);
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(),
Some("0xcafebabe")
);
assert_eq!(
cmd_env(&cmd, "GSH_CAP_BOUNDING").as_deref(),
Some("0x0000000000200404")
);
}
#[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, 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#"{
"context_id":"x",
"principal":{"did":"did:web:guildhouse.dev:user:tking"},
"accord_hash":"sha256:zz",
"shell_class":"System",
"capability_set":1,
"posture_level":5
}"#;
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_DID").as_deref(),
Some("did:web:guildhouse.dev:user:tking")
);
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(),
Some("0x00000001")
);
}
#[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"}}"#;
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_DID").as_deref(), Some("did:web:foo:bar"));
// No governance metadata — none of the other GSH_* vars set.
assert!(cmd_env(&cmd, "GSH_ACCORD_HASH").is_none());
assert!(cmd_env(&cmd, "GSH_SHELL_CLASS").is_none());
assert!(cmd_env(&cmd, "GSH_POSTURE_LEVEL").is_none());
assert!(cmd_env(&cmd, "GSH_CAPABILITY_SET").is_none());
}
}