feat(m3): bascule-shell exports DEFCON env vars from posture-current.json
bascule-shell loads /opt/substrate/posture/current.json (BASCULE_POSTURE_FILE override) at session start and exports: BASCULE_DEFCON_LEVEL numeric global level (1..5) BASCULE_POSTURE_LEVEL alias (already shipped in M1) BASCULE_CAPABILITY_CEILING CAP_NONE..CAP_GOVERN BASCULE_CEREMONY_REQUIRED "0" / "1" BASCULE_MAX_SESSION_TTL minutes, omitted when 0 Fail-soft: missing/malformed file degrades to peacetime defaults so the shell exec path stays alive on misconfigured hosts. The new posture.rs module is a tiny inline snapshot loader (60 LOC, serde_json on top of an already-present dep) — bascule-oss does not pull libgsh as a dep, so the JSON wire format produced by substrate-operator is the contract. gsh and bascule-shell share that contract, not Rust types. Stacked on feat/m2-roles-export. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Claude Code <claude@guildhouse.dev>
This commit is contained in:
parent
56529626f6
commit
df5a2a6f88
2 changed files with 74 additions and 0 deletions
|
|
@ -12,6 +12,7 @@ mod attestation;
|
|||
mod banner;
|
||||
mod config;
|
||||
mod identity;
|
||||
mod posture;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "bascule-shell", about = "Identity-aware shell with TPM attestation")]
|
||||
|
|
@ -140,6 +141,28 @@ fn set_env(
|
|||
};
|
||||
std::env::set_var("BASCULE_ROLES", default_role);
|
||||
}
|
||||
|
||||
// M3: load the operator's posture-current.json once at session
|
||||
// start and surface the consumer-facing fields as BASCULE_*
|
||||
// env vars so gsh's classifier (and any external tooling) gets
|
||||
// the same view without re-reading the file. gsh ALSO reads the
|
||||
// file directly via libgsh::PostureState — these env vars are
|
||||
// the secondary surface for non-gsh consumers and for the
|
||||
// posture-watcher fallback when the file is unreachable.
|
||||
let posture = posture::PostureSnapshot::load();
|
||||
std::env::set_var("BASCULE_DEFCON_LEVEL", posture.global_level.to_string());
|
||||
std::env::set_var("BASCULE_POSTURE_LEVEL", posture.global_level.to_string());
|
||||
std::env::set_var("BASCULE_CAPABILITY_CEILING", &posture.capability_ceiling);
|
||||
std::env::set_var(
|
||||
"BASCULE_CEREMONY_REQUIRED",
|
||||
if posture.ceremony_required_for_writes { "1" } else { "0" },
|
||||
);
|
||||
if posture.max_session_ttl_minutes > 0 {
|
||||
std::env::set_var(
|
||||
"BASCULE_MAX_SESSION_TTL",
|
||||
posture.max_session_ttl_minutes.to_string(),
|
||||
);
|
||||
}
|
||||
// BASCULE_ATTESTATION_HASH was an opaque "evidence string" SHA. M1
|
||||
// replaces it with the proto SAT composite hash. Kept under the same
|
||||
// env var name for backward compatibility with existing gsh consumers
|
||||
|
|
|
|||
51
crates/bascule-shell/src/posture.rs
Normal file
51
crates/bascule-shell/src/posture.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
//! M3: minimal DEFCON posture loader for bascule-shell.
|
||||
//!
|
||||
//! Reads `/opt/substrate/posture/current.json` (override via
|
||||
//! `BASCULE_POSTURE_FILE`), exposes a few fields the env var exporter
|
||||
//! cares about, and falls back gracefully if the file is missing or
|
||||
//! malformed. The struct is intentionally NOT shared with libgsh's
|
||||
//! richer `PostureState` — bascule-shell ships independently from gsh
|
||||
//! and the cross-workspace dep would force cargo to compile libgsh just
|
||||
//! to pull a serde struct. Keep them in sync via the JSON wire format
|
||||
//! (`current.json`), which substrate-operator owns.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
pub const DEFAULT_POSTURE_FILE: &str = "/opt/substrate/posture/current.json";
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct PostureSnapshot {
|
||||
pub global_level: u8,
|
||||
pub max_session_ttl_minutes: i64,
|
||||
pub ceremony_required_for_writes: bool,
|
||||
pub capability_ceiling: String,
|
||||
}
|
||||
|
||||
impl Default for PostureSnapshot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
global_level: 5,
|
||||
max_session_ttl_minutes: 0,
|
||||
ceremony_required_for_writes: false,
|
||||
capability_ceiling: "CAP_GOVERN".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PostureSnapshot {
|
||||
/// Load the operator's posture-current.json. Never errors:
|
||||
/// missing/malformed files degrade to peacetime defaults so the
|
||||
/// shell exec path stays alive on misconfigured hosts.
|
||||
pub fn load() -> Self {
|
||||
let path = std::env::var("BASCULE_POSTURE_FILE")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from(DEFAULT_POSTURE_FILE));
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<Self>(&s).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue