use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use clap::Args;
use aa_gateway::simulation::{HistoricalReplay, SimulationEngine, SimulationReport};
use aa_gateway::PolicyEngine;
#[derive(Args)]
pub struct SimulateArgs {
#[arg(long)]
pub policy: PathBuf,
#[arg(long)]
pub against: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub live: bool,
#[arg(long)]
pub duration: Option<String>,
#[arg(long)]
pub output_file: Option<PathBuf>,
}
pub fn run(args: SimulateArgs) -> ExitCode {
let (budget_tx, _budget_rx) = tokio::sync::broadcast::channel(16);
let engine = match PolicyEngine::load_from_file(&args.policy, budget_tx) {
Ok(e) => Arc::new(e),
Err(e) => {
eprintln!("error: failed to load policy: {e:?}");
return ExitCode::FAILURE;
}
};
let sim_engine = SimulationEngine::new(engine);
if args.live {
eprintln!("error: live simulation is not yet supported (requires AAASM-73)");
return ExitCode::FAILURE;
}
let log_path = match &args.against {
Some(p) => p,
None => {
eprintln!("error: --against <log-file> is required for file-based simulation");
return ExitCode::FAILURE;
}
};
let replay = match HistoricalReplay::from_file(log_path) {
Ok(r) => r,
Err(e) => {
eprintln!("error: failed to read audit log: {e}");
return ExitCode::FAILURE;
}
};
let report = sim_engine.run(replay.events());
if let Some(ref output_path) = args.output_file {
match serde_json::to_string_pretty(&report) {
Ok(json) => {
if let Err(e) = std::fs::write(output_path, &json) {
eprintln!("error: failed to write report to {}: {e}", output_path.display());
return ExitCode::FAILURE;
}
}
Err(e) => {
eprintln!("error: failed to serialize report: {e}");
return ExitCode::FAILURE;
}
}
}
print_report(&report);
if report.denied > 0 {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
fn print_report(report: &SimulationReport) {
println!("Simulation Report");
println!("{}", "-".repeat(50));
println!("Total events: {}", report.total_events);
println!("Allowed: {}", report.allowed);
println!("Denied: {}", report.denied);
println!("Approval required: {}", report.approval_required);
if let Some(budget) = report.budget_impact_usd {
println!("Budget impact: ${budget:.2}");
}
if !report.flagged_outcomes.is_empty() {
println!();
println!("{:<8} {:<20} {:<12} REASON", "EVENT#", "ACTION", "DECISION");
println!("{}", "-".repeat(70));
for outcome in &report.flagged_outcomes {
println!(
"{:<8} {:<20} {:<12} {}",
outcome.event_index, outcome.action, outcome.decision, outcome.reason
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_policy_file_exits_failure() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut tmp, b"not: valid: policy: [[[").unwrap();
let args = SimulateArgs {
policy: tmp.path().to_path_buf(),
against: None,
live: false,
duration: None,
output_file: None,
};
assert_eq!(run(args), ExitCode::FAILURE);
}
#[test]
fn missing_policy_file_exits_failure() {
let args = SimulateArgs {
policy: PathBuf::from("/tmp/nonexistent-policy-simulate.yaml"),
against: None,
live: false,
duration: None,
output_file: None,
};
assert_eq!(run(args), ExitCode::FAILURE);
}
#[test]
fn missing_against_flag_exits_failure() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(
&mut tmp,
br#"apiVersion: agent-assembly/v1
kind: Policy
metadata:
name: sim-test
spec:
tier: low
rules:
- id: allow-all
description: Allow all
match:
actions: ["*"]
effect: allow
audit: true
"#,
)
.unwrap();
let args = SimulateArgs {
policy: tmp.path().to_path_buf(),
against: None,
live: false,
duration: None,
output_file: None,
};
assert_eq!(run(args), ExitCode::FAILURE);
}
#[test]
fn live_mode_exits_failure_not_implemented() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(
&mut tmp,
br#"apiVersion: agent-assembly/v1
kind: Policy
metadata:
name: sim-test
spec:
tier: low
rules:
- id: allow-all
description: Allow all
match:
actions: ["*"]
effect: allow
audit: true
"#,
)
.unwrap();
let args = SimulateArgs {
policy: tmp.path().to_path_buf(),
against: None,
live: true,
duration: Some("30s".to_string()),
output_file: None,
};
assert_eq!(run(args), ExitCode::FAILURE);
}
}