use anyhow::{Context, Result, bail};
use dialoguer::Password;
use crate::cli::{EstopLevelArg, EstopSubcommands};
use crate::config::Config;
use vw_agent::security;
pub(crate) fn handle_estop_command(
config: &Config,
estop_command: Option<EstopSubcommands>,
level: Option<EstopLevelArg>,
domains: Vec<String>,
tools: Vec<String>,
) -> Result<()> {
if !config.security.estop.enabled {
bail!(
"Emergency stop is disabled. Enable [security.estop].enabled = true in vibewindow.json"
);
}
let config_dir =
config.config_path.parent().context("Config path must have a parent directory")?;
let mut manager = security::EstopManager::load(&config.security.estop, config_dir)?;
match estop_command {
Some(EstopSubcommands::Status) => {
print_estop_status(&manager.status());
Ok(())
}
Some(EstopSubcommands::Resume { network, domains, tools, otp }) => {
let selector = build_resume_selector(network, domains, tools)?;
let mut otp_code = otp;
let otp_validator = if config.security.estop.require_otp_to_resume {
if !config.security.otp.enabled {
bail!(
"security.estop.require_otp_to_resume=true but security.otp.enabled=false"
);
}
if otp_code.is_none() {
let entered = Password::new()
.with_prompt("Enter OTP code")
.allow_empty_password(false)
.interact()?;
otp_code = Some(entered);
}
let store = security::SecretStore::new(config_dir, config.secrets.encrypt);
let (validator, enrollment_uri) =
security::OtpValidator::from_config(&config.security.otp, config_dir, &store)?;
if let Some(uri) = enrollment_uri {
println!("Initialized OTP secret for VibeWindow.");
println!("Enrollment URI: {uri}");
}
Some(validator)
} else {
None
};
manager.resume(selector, otp_code.as_deref(), otp_validator.as_ref())?;
println!("Estop resume completed.");
print_estop_status(&manager.status());
Ok(())
}
None => {
let engage_level = build_engage_level(level, domains, tools)?;
manager.engage(engage_level)?;
println!("Estop engaged.");
print_estop_status(&manager.status());
Ok(())
}
}
}
fn build_engage_level(
level: Option<EstopLevelArg>,
domains: Vec<String>,
tools: Vec<String>,
) -> Result<security::EstopLevel> {
let requested = level.unwrap_or(EstopLevelArg::KillAll);
match requested {
EstopLevelArg::KillAll => {
if !domains.is_empty() || !tools.is_empty() {
bail!("--domain/--tool are only valid with --level domain-block/tool-freeze");
}
Ok(security::EstopLevel::KillAll)
}
EstopLevelArg::NetworkKill => {
if !domains.is_empty() || !tools.is_empty() {
bail!("--domain/--tool are not valid with --level network-kill");
}
Ok(security::EstopLevel::NetworkKill)
}
EstopLevelArg::DomainBlock => {
if domains.is_empty() {
bail!("--level domain-block requires at least one --domain");
}
if !tools.is_empty() {
bail!("--tool is not valid with --level domain-block");
}
Ok(security::EstopLevel::DomainBlock(domains))
}
EstopLevelArg::ToolFreeze => {
if tools.is_empty() {
bail!("--level tool-freeze requires at least one --tool");
}
if !domains.is_empty() {
bail!("--domain is not valid with --level tool-freeze");
}
Ok(security::EstopLevel::ToolFreeze(tools))
}
}
}
fn build_resume_selector(
network: bool,
domains: Vec<String>,
tools: Vec<String>,
) -> Result<security::ResumeSelector> {
let selected =
usize::from(network) + usize::from(!domains.is_empty()) + usize::from(!tools.is_empty());
if selected > 1 {
bail!("Use only one of --network, --domain, or --tool for estop resume");
}
if network {
return Ok(security::ResumeSelector::Network);
}
if !domains.is_empty() {
return Ok(security::ResumeSelector::Domains(domains));
}
if !tools.is_empty() {
return Ok(security::ResumeSelector::Tools(tools));
}
Ok(security::ResumeSelector::KillAll)
}
fn print_estop_status(state: &security::EstopState) {
println!("Estop status:");
println!(" engaged: {}", if state.is_engaged() { "yes" } else { "no" });
println!(" kill_all: {}", if state.kill_all { "active" } else { "inactive" });
println!(" network_kill: {}", if state.network_kill { "active" } else { "inactive" });
if state.blocked_domains.is_empty() {
println!(" domain_blocks: (none)");
} else {
println!(" domain_blocks: {}", state.blocked_domains.join(", "));
}
if state.frozen_tools.is_empty() {
println!(" tool_freeze: (none)");
} else {
println!(" tool_freeze: {}", state.frozen_tools.join(", "));
}
if let Some(updated_at) = &state.updated_at {
println!(" updated_at: {updated_at}");
}
}
#[cfg(test)]
#[path = "estop_tests.rs"]
mod estop_tests;