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>
209 lines
7.4 KiB
Rust
209 lines
7.4 KiB
Rust
//! `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 (0–6) |
|
||
//! | `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());
|
||
}
|
||
}
|