use anyhow::Result;
use clap::Args;
use serde::Deserialize;
use std::{path::PathBuf, sync::Arc};
use typesec_agent::SecureAgent;
use typesec_core::{Credentials, RequestContext, ResourceId, SubjectId, policy::PolicyResult};
#[derive(Args)]
pub struct RunArgs {
#[arg(long)]
pub scenario: Option<PathBuf>,
#[arg(long)]
pub policy: Option<PathBuf>,
#[arg(long)]
pub agent: Option<String>,
#[arg(long)]
pub task: Option<String>,
#[arg(long)]
pub input: Option<PathBuf>,
#[arg(long)]
pub purpose: Option<String>,
#[arg(long)]
pub format: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ScenarioDocument {
scenario: Scenario,
}
#[derive(Debug, Deserialize)]
struct Scenario {
name: Option<String>,
policy: PathBuf,
format: Option<String>,
#[serde(default)]
purpose: Option<String>,
steps: Vec<ScenarioStep>,
}
#[derive(Debug, Deserialize)]
struct ScenarioStep {
agent: String,
action: String,
resource: String,
#[serde(default)]
purpose: Option<String>,
#[serde(default)]
expect: Option<String>,
}
pub async fn run(args: RunArgs) -> Result<()> {
if let Some(scenario) = &args.scenario {
return run_scenario(scenario).await;
}
let policy = args
.policy
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--policy is required unless --scenario is used"))?;
let agent_id = args
.agent
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--agent is required unless --scenario is used"))?;
let task = args
.task
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--task is required unless --scenario is used"))?;
let yaml = std::fs::read_to_string(policy)?;
let context = args
.purpose
.as_ref()
.map_or_else(RequestContext::default, |purpose| {
RequestContext::default().with_purpose(purpose.clone())
});
let engine = load_engine(&yaml, &args.format)?;
let agent = SecureAgent::new(engine);
let credentials = Credentials::new(agent_id.clone(), "cli-token");
let agent = agent
.authenticate_unverified(credentials)
.map_err(|e| anyhow::anyhow!("auth failed: {e}"))?;
println!("Agent '{}' authenticated ✓", agent.subject());
println!("Running task: {task}");
println!();
let resource_id = args
.input
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| format!("simulation:{agent_id}"));
match task.as_str() {
"summarize" | "read" => {
let _ = simulate_task(
&agent,
"read",
&resource_id,
&context,
"Task: summarize/read completed",
)
.await;
}
"write" => {
let _ = simulate_task(
&agent,
"write",
&resource_id,
&context,
"Task: write completed",
)
.await;
}
"infer" => {
let _ = simulate_task(
&agent,
"ai:infer",
&resource_id,
&context,
"Task: AI inference completed",
)
.await;
}
"exfiltrate" => {
let _ = simulate_task(
&agent,
"ai:exfiltrate",
&resource_id,
&context,
"Task: exfiltrate (DANGEROUS!)",
)
.await;
}
other => {
anyhow::bail!("Unknown task: '{other}'. Try: summarize, write, infer, exfiltrate");
}
}
Ok(())
}
async fn run_scenario(path: &PathBuf) -> Result<()> {
let yaml = std::fs::read_to_string(path)?;
let doc: ScenarioDocument = serde_yaml::from_str(&yaml)?;
let policy_path = if doc.scenario.policy.is_relative() {
path.parent()
.map(|parent| parent.join(&doc.scenario.policy))
.unwrap_or_else(|| doc.scenario.policy.clone())
} else {
doc.scenario.policy.clone()
};
let policy_yaml = std::fs::read_to_string(&policy_path)?;
let engine = load_engine(&policy_yaml, &doc.scenario.format)?;
println!(
"Scenario: {}",
doc.scenario.name.as_deref().unwrap_or("unnamed")
);
println!("Policy: {}", policy_path.display());
println!("Steps: {}", doc.scenario.steps.len());
println!();
let mut mismatches = 0usize;
for (idx, step) in doc.scenario.steps.iter().enumerate() {
let context = step
.purpose
.as_ref()
.or(doc.scenario.purpose.as_ref())
.map_or_else(RequestContext::default, |purpose| {
RequestContext::default().with_purpose(purpose.clone())
});
let agent = SecureAgent::new(engine.clone())
.authenticate_unverified(Credentials::new(step.agent.clone(), "scenario-token"))
.map_err(|e| anyhow::anyhow!("scenario step {} auth failed: {e}", idx + 1))?;
println!(
"[{}] agent='{}' action='{}' resource='{}'",
idx + 1,
agent.subject(),
step.action,
step.resource
);
let result = simulate_task(
&agent,
&step.action,
&step.resource,
&context,
"Step completed",
)
.await;
if let Some(expected) = &step.expect {
let expected = expected.to_ascii_lowercase();
let actual = result_label(&result);
if actual == expected {
println!(" ✓ EXPECTED {expected}");
} else {
println!(" ✗ EXPECTED {expected}, got {actual}");
mismatches += 1;
}
}
println!();
}
if mismatches > 0 {
anyhow::bail!("{mismatches} scenario expectation(s) failed");
}
Ok(())
}
fn load_engine(
yaml: &str,
format: &Option<String>,
) -> Result<Arc<dyn typesec_core::policy::PolicyEngine>> {
match detect_format(format, yaml).as_deref() {
Some("rbac") => {
let e = typesec_rbac::RbacEngine::from_yaml(yaml).map_err(|e| anyhow::anyhow!(e))?;
Ok(Arc::new(e))
}
Some("odrl") => {
let engine =
typesec_odrl::OdrlEngine::from_yaml(yaml).map_err(|e| anyhow::anyhow!(e))?;
Ok(Arc::new(engine))
}
_ => anyhow::bail!("Could not detect policy format"),
}
}
async fn simulate_task(
agent: &SecureAgent<typesec_core::typestate::Authenticated>,
action: &str,
resource_id: &str,
context: &RequestContext,
success_message: &str,
) -> PolicyResult {
println!("Requesting: action='{action}' on '{resource_id}'");
let engine = agent.engine();
let subject = SubjectId::from(agent.subject());
let resource = ResourceId::from(resource_id);
let result = engine.check_with_context(&subject, action, &resource, context);
match &result {
PolicyResult::Allow => {
println!(" ✓ ALLOWED — capability granted");
println!(" → {success_message}");
}
PolicyResult::Deny(reason) => {
println!(" ✗ DENIED — {reason}");
}
PolicyResult::Delegate(reason) => {
println!(
" → DELEGATED to {}: {} (no definitive answer)",
reason.engine, reason.reason
);
}
_ => {
println!(" ✗ UNKNOWN POLICY RESULT — treating as denied");
}
}
result
}
fn result_label(result: &PolicyResult) -> &'static str {
match result {
PolicyResult::Allow => "allow",
PolicyResult::Deny(_) => "deny",
PolicyResult::Delegate(_) => "delegate",
_ => "unknown",
}
}
fn detect_format(explicit: &Option<String>, yaml: &str) -> Option<String> {
if let Some(f) = explicit {
return Some(f.clone());
}
if yaml.contains("roles:") {
Some("rbac".into())
} else if yaml.contains("policies:") {
Some("odrl".into())
} else {
None
}
}