pub mod action;
pub mod audit;
pub mod audit_replay;
pub mod completions;
pub mod ctl;
pub mod drift;
pub mod envs;
pub mod explain;
pub mod lint;
pub mod mcp;
pub mod versions;
pub const SUBCOMMANDS: &[&str] = &[
"envs",
"action",
"ctl",
"lint",
"drift",
"audit",
"mcp",
"explain",
"versions",
"completions",
];
pub(crate) use crate::deploy_poll::{decide_poll, PollDecision};
pub(crate) use crate::util::{json_escape as cli_esc, json_string};
pub(crate) fn refuse_if_frozen(prog: &str) {
if let Some(m) = crate::freeze::read_active() {
eprintln!("{prog}: refusing — {}", crate::freeze::refusal_message(&m));
std::process::exit(3);
}
}
pub(crate) fn write_refusal(
safety_cfg: &crate::config::Config,
env: &str,
profile: &Option<String>,
active_freeze: Option<crate::freeze::FreezeMarker>,
) -> Option<String> {
if let Some(m) = active_freeze {
return Some(crate::freeze::refusal_message(&m));
}
let pin_profile = profile
.clone()
.or_else(|| std::env::var("AWS_PROFILE").ok());
if let Some(pin) = safety_cfg.pin_reason(env, pin_profile.as_deref()) {
return Some(format!("refusing {env} — pinned by {pin}"));
}
None
}
pub(crate) fn refuse_write(prog: &str, subject: &str, env: &str, profile: Option<&str>) {
let profile = profile.map(str::to_string);
if let Some(reason) = write_refusal(
&crate::config::load(),
env,
&profile,
crate::freeze::read_active(),
) {
let reason = reason
.strip_prefix(&format!("refusing {env} — "))
.map(|r| format!("refusing {subject} — {r}"))
.unwrap_or(reason);
eprintln!("{prog}: {reason}");
std::process::exit(3);
}
}
pub(crate) fn take_value<'a, I: Iterator<Item = &'a String>>(
iter: &mut I,
prog: &str,
flag: &str,
what: &str,
) -> Result<String, String> {
let Some(v) = iter.next() else {
return Err(format!("{prog}: {flag} expects {what}"));
};
if v.starts_with("--") {
return Err(format!("{prog}: {flag} expects {what}, got flag '{v}'"));
}
Ok(v.clone())
}
pub(crate) async fn exit_after_drain(code: i32) -> ! {
crate::audit::drain_webhooks(std::time::Duration::from_secs(12)).await;
std::process::exit(code);
}
pub(crate) async fn drain_before_return() {
crate::audit::drain_webhooks(std::time::Duration::from_secs(12)).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_esc_escapes_quotes_and_backslashes() {
assert_eq!(cli_esc("hello"), "hello");
assert_eq!(cli_esc("a\"b"), "a\\\"b");
assert_eq!(cli_esc("a\\b"), "a\\\\b");
assert_eq!(cli_esc("a\nb"), "a\\nb");
assert_eq!(cli_esc("a\tb"), "a\\tb");
}
#[test]
fn json_string_wraps_in_quotes_and_escapes() {
assert_eq!(json_string(""), "\"\"");
assert_eq!(json_string("hello"), "\"hello\"");
assert_eq!(json_string("a\"b"), "\"a\\\"b\"");
let s = "line1\nline2 \"with quotes\"";
let escaped = json_string(s);
let parsed: String =
serde_json::from_str(&escaped).expect("hand-rolled JSON must be valid JSON");
assert_eq!(parsed, s);
}
}
#[cfg(test)]
mod write_gate_guard {
#[test]
fn cli_write_paths_do_not_reach_past_the_shared_gate() {
let mut offenders: Vec<String> = Vec::new();
let mut stack = vec![std::path::PathBuf::from("src/cli")];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("src/cli") {
let path = entry.expect("entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
if path.file_name().and_then(|f| f.to_str()) == Some("mod.rs") {
continue;
}
let text = std::fs::read_to_string(&path).expect("read");
let prod = text.split("#[cfg(test)]").next().unwrap_or("");
for (n, line) in prod.lines().enumerate() {
let code = crate::app::tests::scan::strip_line_comment(line);
if code.contains(".pin_reason(") {
offenders.push(format!("{}:{}", path.display(), n + 1));
}
}
}
}
assert!(
offenders.is_empty(),
"these CLI paths call `pin_reason` directly instead of going \
through `cli::write_refusal`, which also checks the freeze — \
the exact half-composition 0.14.1 shipped: {offenders:?}"
);
}
}
#[cfg(test)]
mod write_refusal_tests {
use super::write_refusal;
use crate::config::Config;
#[test]
fn an_account_pin_is_resolved_against_the_profile_passed_in() {
let mut cfg = Config::default();
cfg.safety_accounts.insert("prod-admin".into(), true);
let refused = write_refusal(&cfg, "api-prod", &Some("prod-admin".into()), None);
assert!(
refused.is_some_and(|r| r.contains("prod-admin")),
"a pinned account must refuse when it is the profile the write runs under"
);
assert_eq!(
write_refusal(&cfg, "api-prod", &Some("dev".into()), None),
None
);
}
}
#[cfg(test)]
mod write_gate_input_guard {
#[test]
fn a_subcommand_with_its_own_profile_flag_passes_it_to_the_gate() {
let src = std::fs::read_to_string("src/cli/action.rs").expect("read action.rs");
let mut offenders: Vec<String> = Vec::new();
let mut current_fn = String::new();
let mut body = String::new();
let check = |name: &str, body: &str, offenders: &mut Vec<String>| {
if name.is_empty() || !body.contains("\"--profile\"") {
return;
}
for line in body.lines() {
let t = line.trim_start();
if t.starts_with("refuse_write(") && t.contains(", None)") {
offenders.push(format!("{name}: {}", t.trim()));
}
}
};
fn is_top_level_fn(line: &str) -> bool {
if line.starts_with(char::is_whitespace) {
return false;
}
let rest = match line.find(") ") {
Some(i) if line.starts_with("pub(") => &line[i + 2..],
_ => line.strip_prefix("pub ").unwrap_or(line),
};
rest.starts_with("fn ") || rest.starts_with("async fn ")
}
for line in src.lines() {
if is_top_level_fn(line) {
check(¤t_fn, &body, &mut offenders);
current_fn = line
.split("fn ")
.nth(1)
.unwrap_or("")
.split('(')
.next()
.unwrap_or("")
.to_string();
body.clear();
}
body.push('\n');
body.push_str(line);
}
check(¤t_fn, &body, &mut offenders);
assert!(
offenders.is_empty(),
"these subcommands parse `--profile` but hand the write gate \
`None`, so the account pin is resolved against the ambient \
AWS_PROFILE instead of the account the write runs under: \
{offenders:?}"
);
}
}