//! 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(()) }