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 banner;
|
||||||
mod config;
|
mod config;
|
||||||
mod identity;
|
mod identity;
|
||||||
|
mod posture;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "bascule-shell", about = "Identity-aware shell with TPM attestation")]
|
#[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);
|
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
|
// BASCULE_ATTESTATION_HASH was an opaque "evidence string" SHA. M1
|
||||||
// replaces it with the proto SAT composite hash. Kept under the same
|
// replaces it with the proto SAT composite hash. Kept under the same
|
||||||
// env var name for backward compatibility with existing gsh consumers
|
// 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