use std::io::{self, Write as _};
use std::path::PathBuf;
use anyhow::{Result, bail};
use openmoat_hosts::Host;
use serde_json::{Value, json};
use toml_edit::DocumentMut;
use crate::cli::{Format, SandboxShowArgs};
use crate::exit::Code;
use crate::home::Home;
use crate::install::HostConfig;
use crate::integrity::{self, HookPins};
use crate::render::{self, Deferred};
use crate::sandbox::{
Grants, Plan, Report, claude, codex, codex_config_path, install, landlock_rules,
seatbelt_profile,
};
struct Shown<'a> {
host: Host,
path: PathBuf,
text: String,
report: &'a Report,
}
pub fn show(args: &SandboxShowArgs) -> Result<Code> {
let policy = Home::locate()?.load_policy()?;
let plan = Plan::new(&policy)?;
let ctx = crate::context::eval_context(None, None)?;
let seatbelt = seatbelt_profile(&policy, &ctx, Grants::default())?;
let landlock = landlock_rules(&policy, &ctx, Grants::default())?;
let landlock_text = serde_json::to_string_pretty(&landlock.rules)?;
let lightweight = [
("Seatbelt (macOS)", &seatbelt.profile, &seatbelt.report),
("Landlock (Linux)", &landlock_text, &landlock.report),
];
let mut codex_doc = DocumentMut::new();
codex::apply(&mut codex_doc, &plan.codex)?;
let mut claude_doc = json!({});
claude::apply(&mut claude_doc, &plan.claude)?;
let hosts = [
Shown {
host: Host::ClaudeCode,
path: HostConfig::for_host(Host::ClaudeCode)?.settings_path,
text: serde_json::to_string_pretty(&claude_doc)?,
report: &plan.claude.report,
},
Shown {
host: Host::Codex,
path: codex_config_path()?,
text: codex_doc.to_string(),
report: &plan.codex.report,
},
Shown {
host: Host::Cursor,
path: install::settings_path(Host::Cursor)?,
text: serde_json::to_string_pretty(&plan.cursor.settings)?,
report: &plan.cursor.report,
},
];
if args.format == Format::Json {
let doc: serde_json::Map<String, Value> = hosts
.iter()
.map(|s| {
let entry = match install::unavailable(s.host) {
Some(note) => json!({ "path": s.path, "unavailable": note }),
None => json!({ "path": s.path, "settings": s.text, "report": s.report }),
};
(s.host.id().to_owned(), entry)
})
.collect();
render::json(&json!({
"default_read_roots": plan.default_read_roots,
"hosts": doc,
"lightweight": {
"seatbelt": { "profile": &seatbelt.profile, "report": &seatbelt.report },
"landlock": { "rules": &landlock.rules, "report": &landlock.report },
},
}))?;
return Ok(Code::Ok);
}
let mut out = io::stdout().lock();
if plan.default_read_roots {
writeln!(
out,
"note: the policy has no `sandbox.read_roots`; the default policy's list applies"
)?;
}
for shown in hosts {
if let Some(note) = install::unavailable(shown.host) {
writeln!(out, "{} {note}", shown.host.display_name())?;
continue;
}
writeln!(
out,
"{} {}",
shown.host.display_name(),
shown.path.display()
)?;
writeln!(out, "{}", shown.text.trim_end())?;
write_report(&mut out, shown.report)?;
}
for (title, text, report) in lightweight {
writeln!(
out,
"{title} `moat run` in this directory (Lightweight tier)"
)?;
writeln!(out, "{}", text.trim_end())?;
write_report(&mut out, report)?;
}
writeln!(out, "dry run: nothing was written")?;
Ok(Code::Ok)
}
pub fn sync() -> Result<Code> {
if !crate::terminal::interactive() {
bail!(
"`moat sandbox sync` must be run by a person in a terminal, not from a hook or script"
);
}
let home = Home::locate()?;
integrity::refuse_drift(&home, "sync")?;
let plan = Plan::new(&home.load_policy()?)?;
let binary = crate::install::hook_binary()?;
let mut out = Deferred::default();
let (mut files, mut codex_configs) = (Vec::new(), Vec::new());
for host in install::HOSTS {
let path = install::settings_path(host)?;
if !path.parent().is_some_and(std::path::Path::is_dir) {
writeln!(out, "· {:<16} host not found", host.display_name())?;
continue;
}
let changed = install::write(host, &plan, false)?;
if let Some(note) = install::unavailable(host) {
if changed {
let removed = install::OLD_SANDBOX_REMOVED;
writeln!(out, "✔ {:<16} {removed}", host.display_name())?;
files.push(path);
}
writeln!(out, "· {:<16} {note}", host.display_name())?;
continue;
}
let verb = if changed { "updated" } else { "unchanged" };
writeln!(
out,
"✔ {:<16} {} (sandbox {verb})",
host.display_name(),
path.display()
)?;
write_report(&mut out, install::report(host, &plan))?;
match host {
Host::Codex => codex_configs.push(path),
_ => files.push(path),
}
}
let lock = integrity::repin_with(&home, &binary, HookPins::Keep, &files, &codex_configs)?;
writeln!(
out,
"✔ lock {} files and {} Codex profile(s) pinned",
lock.entries.len(),
lock.codex_profiles.len()
)?;
match super::proxy::restart_if_installed(&home) {
Ok(Some(message)) => writeln!(out, "✔ service {message}")?,
Ok(None) => {}
Err(e) => writeln!(
out,
"! service restart failed: {e:#}; `moat proxy install` restarts it by hand"
)?,
}
out.finish()?;
Ok(Code::Ok)
}
pub(super) fn write_report(out: &mut impl io::Write, report: &Report) -> io::Result<()> {
for loss in &report.losses {
writeln!(out, " stricter: {loss}")?;
}
for allowance in &report.allowances {
writeln!(out, " wider: {allowance}")?;
}
Ok(())
}