use std::path::{Path, PathBuf};
use clap::Subcommand;
use newt_core::flight_recorder::{read_capture_jsonl, CAPTURE_PATH_ENV};
use newt_core::ocap_propose::{in_policy_pairs, propose_from_capture, Proposal};
use newt_core::ocap_store::{build_store, CapabilityClass, PolicyFile, Verdict, VERDICTS};
#[derive(Subcommand, Debug)]
pub enum OcapCmd {
Denials {
#[arg(long, value_name = "FILE")]
journal: Option<PathBuf>,
},
Propose {
#[arg(long, default_value_t = false)]
save: bool,
#[arg(long, value_name = "FILE")]
capture: Option<PathBuf>,
},
RevokeCredential {
#[arg(value_name = "CRED-SHORT")]
handle: String,
#[arg(long, default_value = "operator")]
subject: String,
#[arg(long, env = "NEWT_OPERATOR_KEY", value_name = "FILE")]
operator_key_path: Option<PathBuf>,
},
}
pub fn run(cmd: OcapCmd, config: Option<&Path>) -> anyhow::Result<i32> {
match cmd {
OcapCmd::Denials { journal } => run_denials(journal, config),
OcapCmd::Propose { save, capture } => run_propose(save, capture, config),
OcapCmd::RevokeCredential {
handle,
subject,
operator_key_path,
} => run_revoke_credential(&handle, &subject, operator_key_path, config),
}
}
fn run_revoke_credential(
handle: &str,
subject: &str,
operator_key_path: Option<PathBuf>,
config: Option<&Path>,
) -> anyhow::Result<i32> {
let config_path = config
.map(Path::to_path_buf)
.or_else(newt_core::Config::user_config_path)
.ok_or_else(|| anyhow::anyhow!("cannot locate the newt config directory"))?;
let key_path = match operator_key_path {
Some(path) => path,
None => newt_identity::default_key_path()?,
};
let root_key = newt_identity::load_user_key(&key_path)?;
let window = newt_core::tty::Terminal::suspend_for_prompt();
window.ask(&format!(
"revoke credential `{handle}` for `{subject}`? [y/N] "
))?;
let mut answer = String::new();
if window.read_line_into(&mut answer)? == 0
|| !matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes")
{
window.notice("revoke declined; nothing changed")?;
return Ok(1);
}
match newt_core::credential_registry::revoke_credential(
&config_path,
subject,
handle,
&root_key,
) {
Ok(full) => {
window.notice(&format!("revoked {full}"))?;
Ok(0)
}
Err(error) => {
window.notice(&format!("revoke failed: {error}"))?;
Ok(1)
}
}
}
fn run_denials(journal: Option<PathBuf>, config: Option<&Path>) -> anyhow::Result<i32> {
let config_path = config
.map(Path::to_path_buf)
.or_else(newt_core::Config::user_config_path)
.ok_or_else(|| anyhow::anyhow!("cannot resolve the newt config path"))?;
let path = journal.unwrap_or_else(|| config_path.with_file_name("denial-journal.jsonl"));
let body = match std::fs::read_to_string(&path) {
Ok(body) => body,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
println!(
"No denial journal at {}.\nConfined run_command refusals will be recorded there.",
path.display()
);
return Ok(0);
}
Err(e) => return Err(anyhow::anyhow!("read {}: {e}", path.display())),
};
print!("{}", render_denials(&body, &path));
Ok(0)
}
fn resolve_capture_path(capture: Option<PathBuf>, config_path: &Path) -> PathBuf {
capture_path_from(capture, std::env::var_os(CAPTURE_PATH_ENV), config_path)
}
fn capture_path_from(
capture: Option<PathBuf>,
env_val: Option<std::ffi::OsString>,
config_path: &Path,
) -> PathBuf {
if let Some(p) = capture {
return p;
}
if let Some(v) = env_val {
let s = v.to_string_lossy();
if !(s.eq_ignore_ascii_case("off") || s == "0") {
return PathBuf::from(v);
}
}
config_path
.with_file_name("flight-recorder")
.join("unconfined.jsonl")
}
fn run_propose(save: bool, capture: Option<PathBuf>, config: Option<&Path>) -> anyhow::Result<i32> {
let config_path = config
.map(Path::to_path_buf)
.or_else(newt_core::Config::user_config_path)
.ok_or_else(|| anyhow::anyhow!("cannot resolve the newt config path"))?;
let capture_path = resolve_capture_path(capture, &config_path);
let capture_text = match std::fs::read_to_string(&capture_path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
println!(
"No flight-recorder capture at {}.\n\
Run a task under `--full-access` first — every unconfined command records the \n\
authority a leash would have gated on, and `newt ocap propose` turns that into a \n\
reviewable policy.",
capture_path.display()
);
return Ok(0);
}
Err(e) => return Err(anyhow::anyhow!("read {}: {e}", capture_path.display())),
};
let store_dir = config_path.with_file_name("ocap");
let store_files: Vec<(Verdict, Option<String>)> = VERDICTS
.iter()
.map(|&v| {
(
v,
std::fs::read_to_string(store_dir.join(v.filename())).ok(),
)
})
.collect();
let approve_path = store_dir.join(Verdict::Approve.filename());
let existing_approve = std::fs::read_to_string(&approve_path).ok();
let now = newt_tui::probe::today_local_date();
let (proposal, merged_toml) = plan_proposal(
&capture_text,
&store_files,
existing_approve.as_deref(),
newt_tui::ocap_high_danger_predicate(),
&now,
)
.map_err(|e| anyhow::anyhow!(e))?;
if proposal.is_empty() {
println!(
"The capture at {} is fully accounted for by the current policy — nothing to propose.",
capture_path.display()
);
return Ok(0);
}
let wrote = save && proposal.additions_len() > 0;
if wrote {
if let Err(e) = std::fs::create_dir_all(&store_dir) {
return Err(anyhow::anyhow!("create {}: {e}", store_dir.display()));
}
std::fs::write(&approve_path, &merged_toml)
.map_err(|e| anyhow::anyhow!("write {}: {e}", approve_path.display()))?;
}
print!("{}", render(&proposal, wrote, &approve_path));
Ok(0)
}
pub(crate) fn plan_proposal(
capture_text: &str,
store_files: &[(Verdict, Option<String>)],
existing_approve: Option<&str>,
is_high_danger: impl Fn(CapabilityClass, &str) -> bool,
now: &str,
) -> Result<(Proposal, String), String> {
let cap = read_capture_jsonl(capture_text);
let (set, _warnings) = build_store(store_files);
let in_policy = in_policy_pairs(&set);
let proposal = propose_from_capture(&cap, &in_policy, is_high_danger, now);
let mut merged = match existing_approve {
Some(t) => PolicyFile::parse(t).map_err(|e| format!("approve.toml: {e}"))?,
None => PolicyFile::default(),
};
merged.exec.extend(proposal.additions.exec.iter().cloned());
merged.fs.extend(proposal.additions.fs.iter().cloned());
merged.net.extend(proposal.additions.net.iter().cloned());
let toml = merged
.to_toml()
.map_err(|e| format!("serialize approve.toml: {e}"))?;
Ok((proposal, toml))
}
pub(crate) fn render(proposal: &Proposal, wrote: bool, approve_path: &Path) -> String {
let mut out = String::new();
let a = &proposal.additions;
if proposal.additions_len() > 0 {
out.push_str(&format!(
"Proposed {} durable candidate(s) from the flight recorder:\n",
proposal.additions_len()
));
for e in &a.exec {
out.push_str(&format!(" exec {} [{}]\n", e.target, note_of(&e.note)));
}
for e in &a.fs {
let mode = if e.write { "rw" } else { "ro" };
out.push_str(&format!(
" fs {} ({mode}) [{}]\n",
e.path,
note_of(&e.note)
));
}
for e in &a.net {
out.push_str(&format!(" net {} [{}]\n", e.host, note_of(&e.note)));
}
out.push('\n');
}
if !proposal.deferred.is_empty() {
out.push_str(&format!(
"Deferred {} high-danger observation(s) — NOT proposed as durable grants:\n",
proposal.deferred.len()
));
for d in &proposal.deferred {
out.push_str(&format!(
" {:<5} {} ({}x)\n {}\n",
d.class, d.target, d.count, d.reason
));
}
out.push('\n');
}
if wrote {
out.push_str(&format!(
"Wrote {} UNSIGNED candidate(s) to {}.\n\
They do nothing until you bless them — review the file, then run:\n\
\n newt doctor --sign-ocap\n",
proposal.additions_len(),
approve_path.display()
));
} else if proposal.additions_len() > 0 {
out.push_str(
"Dry run — nothing written. Re-run with `--save` to record these as \n\
unsigned candidates, then bless them with `newt doctor --sign-ocap`.\n",
);
}
out
}
fn note_of(note: &Option<String>) -> &str {
note.as_deref().unwrap_or("observed")
}
pub(crate) fn render_denials(body: &str, path: &Path) -> String {
let records = newt_core::denial_journal::read_jsonl(body);
let summaries = newt_core::denial_journal::summarize(&records);
if summaries.is_empty() {
return format!("No structured denials recorded in {}.\n", path.display());
}
let mut out = format!(
"Denial repair journal: {} attempt(s), {} target(s)\nsource: {}\n\n",
records.len(),
summaries.len(),
path.display()
);
for summary in summaries {
out.push_str(&format!(
"[{}] {}:{} ({}x)\n reason: {}\n replay: {}\n",
summary.classification.as_str(),
summary.kind,
summary.target,
summary.count,
summary.reason,
summary.example_command
));
match summary.classification {
newt_core::denial_journal::RepairClass::PolicyGap => out.push_str(
" next: review whether this target is necessary; if so, use the normal \
permission/policy approval path.\n",
),
newt_core::denial_journal::RepairClass::Structural => out.push_str(
" next: implement or route around this shell construct; a grant cannot \
make the confined engine interpret it.\n",
),
newt_core::denial_journal::RepairClass::GrantRetryFailure => out.push_str(
" next:\n 1. Reproduce from the replay command.\n 2. Do not add it to policy; \
the grant already failed.\n 3. Repair denial attribution, grant matching, \
or the upstream command implementation.\n",
),
}
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn danger(class: CapabilityClass, target: &str) -> bool {
match class {
CapabilityClass::Exec => matches!(target, "bash" | "python3" | "env"),
CapabilityClass::Fs => target.starts_with("/home") || target == "/",
CapabilityClass::Net => false,
}
}
const CARGO: &str = r#"{"axis":"exec","target":"cargo","command":"cargo build","count":2}"#;
const BASH: &str = r#"{"axis":"exec","target":"bash","command":"bash -c x","count":1}"#;
#[test]
fn plan_merges_low_danger_candidates_into_existing_approve() {
let existing = "[[exec]]\ntarget = \"git\"\nsig = \"ab\"\n";
let capture = format!("{CARGO}\n{BASH}\n");
let (proposal, merged) = plan_proposal(
&capture,
&[(Verdict::Approve, Some(existing.to_string()))],
Some(existing),
danger,
"2026-07-17",
)
.unwrap();
assert_eq!(proposal.additions.exec.len(), 1);
assert_eq!(proposal.deferred.len(), 1);
let parsed = PolicyFile::parse(&merged).unwrap();
assert_eq!(parsed.exec.len(), 2);
let git = parsed.exec.iter().find(|e| e.target == "git").unwrap();
assert_eq!(git.sig.as_deref(), Some("ab"), "existing grant untouched");
let cargo = parsed.exec.iter().find(|e| e.target == "cargo").unwrap();
assert!(cargo.sig.is_none(), "candidate is unsigned");
assert_eq!(cargo.by.as_deref(), Some("flight-recorder"));
}
#[test]
fn plan_is_idempotent_against_an_already_written_unsigned_candidate() {
let approve = "[[exec]]\ntarget = \"cargo\"\nby = \"flight-recorder\"\n";
let (proposal, _merged) = plan_proposal(
&format!("{CARGO}\n"),
&[(Verdict::Approve, Some(approve.to_string()))],
Some(approve),
danger,
"2026-07-17",
)
.unwrap();
assert!(proposal.is_empty(), "no re-proposal: {proposal:?}");
}
#[test]
fn render_dry_run_points_at_write_then_bless() {
let (proposal, _) = plan_proposal(
&format!("{CARGO}\n{BASH}\n"),
&[],
None,
danger,
"2026-07-17",
)
.unwrap();
let path = PathBuf::from("/home/x/.newt/ocap/approve.toml");
let dry = render(&proposal, false, &path);
assert!(dry.contains("exec cargo"));
assert!(dry.contains("Deferred 1 high-danger"));
assert!(dry.contains("bash"));
assert!(dry.contains("--save"), "dry run tells you how to persist");
assert!(!dry.contains("Wrote"));
let wrote = render(&proposal, true, &path);
assert!(wrote.contains("Wrote 1 UNSIGNED candidate"));
assert!(wrote.contains("newt doctor --sign-ocap"));
}
#[test]
fn capture_path_prefers_override_then_env_then_default() {
let cfg = PathBuf::from("/home/x/.newt/config.toml");
assert_eq!(
capture_path_from(
Some(PathBuf::from("/tmp/c.jsonl")),
Some("/env/c".into()),
&cfg
),
PathBuf::from("/tmp/c.jsonl")
);
assert_eq!(
capture_path_from(None, Some("/env/c.jsonl".into()), &cfg),
PathBuf::from("/env/c.jsonl")
);
let off = capture_path_from(None, Some("off".into()), &cfg);
assert!(off.ends_with("flight-recorder/unconfined.jsonl"), "{off:?}");
let def = capture_path_from(None, None, &cfg);
assert!(def.ends_with("flight-recorder/unconfined.jsonl"), "{def:?}");
}
#[test]
fn denial_report_presents_repair_class_and_replay_fixture() {
let record = newt_core::denial_journal::DenialRecord {
ts_claim: "2026-07-23T22:00:00Z".into(),
command: "wc -l src/lib.rs".into(),
cwd: "/workspace".into(),
stage: newt_core::denial_journal::DenialStage::AfterGrant,
denials: vec![newt_core::denial_journal::JournalDenial {
kind: "exec".into(),
target: "confinement".into(),
reason: "exec of \"confinement\" is not within the granted authority".into(),
}],
};
let body = serde_json::to_string(&record).unwrap();
let report = render_denials(&body, Path::new("/home/x/.newt/denial-journal.jsonl"));
assert!(report.contains("grant-retry"));
assert!(report.contains("exec:confinement"));
assert!(report.contains("2. Do not add it to policy"));
assert!(report.contains("wc -l src/lib.rs"));
assert!(report.contains("/home/x/.newt/denial-journal.jsonl"));
}
}