bascule-workspace/bascule-agent/src/main.rs
Tyler King b1865a0627 initial: bascule v0.1.0
Bascule shell runtime workspace — governed shell access layer
for Substrate/Guildhouse FFC deployments.

Crates:
- bascule-agent: node agent with SSH server + command filtering
- bascule-core: audit, grant engine, ceremony types, session
- bascule-filter-core: log line filtering (stdio protocol)
- bascule-gateway: OIDC auth, session management, SAT validation
- bascule-node-agent: k8s DaemonSet agent (pod watcher, BPF manager)
- bascule-proto: protobuf definitions
- bascule-shell: governed SSH shell (commands, elevation, REPL)
- bascule-tail: chronicle log tail + fanout
- ceremony-engine: ceremony lifecycle (6 types + request/resolution)

172 tests passing.
Implements SBS-SPEC-0001 shell model.
Reference impl for SPEC-SHELLOPS-0001 Layer 1 (root shell).
2026-03-18 16:40:48 -04:00

147 lines
4.3 KiB
Rust

//! bascule-agent — governed application sidecar.
//!
//! A single Rust process serving two interfaces over one governance engine:
//!
//! 1. **Governance IPC** — Unix socket at `/var/run/substrate/agent.sock`
//! Django middleware, Celery workers, and co-located apps talk Shellstream here.
//!
//! 2. **Interactive Shell** — SSH server on port 2222 (optional)
//! Bascule Gateway, `sb shell`, and Dashboard WebSocket connect here.
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Context;
use clap::Parser;
use tracing::{error, info};
mod command_filter;
mod config;
mod governance_server;
mod namespace;
mod session_store;
mod shellstream;
mod ssh_server;
use governance_server::GovernanceServer;
use namespace::NamespaceRouter;
use session_store::SessionStore;
use shellstream::Namespace;
use ssh_server::AgentSSHServer;
#[derive(Parser)]
#[command(name = "bascule-agent", about = "Governed application sidecar")]
struct Cli {
/// Path to shell.toml configuration file.
#[arg(long, default_value = "/etc/substrate/shell.toml")]
config: PathBuf,
/// Run with default config (dev mode, no config file needed).
#[arg(long)]
dev: bool,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.json()
.init();
let cli = Cli::parse();
let config = if cli.dev {
info!("Starting in dev mode with default configuration");
config::AgentConfig::default_config()
} else {
config::AgentConfig::load(&cli.config)
.with_context(|| format!("Failed to load config from {:?}", cli.config))?
};
info!(
socket_path = %config.agent.socket_path,
ssh_enabled = config.agent.ssh.enabled,
namespace_backend = %config.agent.namespaces.backend,
"bascule-agent starting"
);
let dev_mode = config.agent.namespaces.backend == "soft";
let config = Arc::new(config);
// Build namespace router with all soft-mode handlers
let mut router = NamespaceRouter::new();
router.register(
Namespace::Crypto,
Arc::new(namespace::crypto::CryptoHandler::new()),
);
router.register(
Namespace::Identity,
Arc::new(namespace::identity::IdentityHandler::new(dev_mode)),
);
router.register(
Namespace::Secrets,
Arc::new(namespace::secrets::SecretsHandler::new()),
);
router.register(
Namespace::Governance,
Arc::new(namespace::governance::GovernanceHandler::new(dev_mode)),
);
router.register(
Namespace::Attestation,
Arc::new(namespace::attestation::AttestationHandler::new(
config.agent.namespaces.attestation.default_posture.clone(),
)),
);
router.register(
Namespace::Audit,
Arc::new(namespace::audit::AuditHandler::new()),
);
router.register(
Namespace::Network,
Arc::new(namespace::network::NetworkHandler::new()),
);
router.register(
Namespace::Intelligence,
Arc::new(namespace::intelligence::IntelligenceHandler::new()),
);
let router = Arc::new(router);
let sessions = Arc::new(SessionStore::new());
// Start governance IPC server
let server = GovernanceServer::new(config.clone(), router.clone(), sessions);
let ipc_handle = tokio::spawn(async move {
if let Err(e) = server.serve().await {
error!(error = %e, "Governance IPC server failed");
}
});
// Start SSH shell server (if enabled)
let ssh_handle = if config.agent.ssh.enabled {
let ssh_server = AgentSSHServer::new(config.clone(), router.clone())?;
Some(tokio::spawn(async move {
if let Err(e) = ssh_server.serve().await {
error!(error = %e, "SSH shell server failed");
}
}))
} else {
info!("SSH shell server disabled");
None
};
info!("bascule-agent ready");
// Wait for shutdown signal
tokio::signal::ctrl_c().await?;
info!("bascule-agent shutting down");
ipc_handle.abort();
if let Some(h) = ssh_handle {
h.abort();
}
Ok(())
}