use std::io::{self, Write as _};
use std::path::PathBuf;
use anyhow::{Result, bail};
use openmoat_hosts::{Host, SandboxFormat};
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::{HookState, HostConfig};
use crate::integrity::{self, HookPins};
use crate::render::{self, Deferred};
use crate::sandbox::{
Grants, Plan, Report, claude, codex, 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 home = Home::locate()?;
let policy = home.load_policy()?;
let plan = Plan::new(&home, &policy)?;
let ctx = crate::context::eval_context(&home, 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, &mut Vec::new())?;
let mut hosts = Vec::new();
for host in install::hosts() {
let text = match install::format(host) {
Some(SandboxFormat::CodexProfile) => codex_doc.to_string(),
Some(SandboxFormat::CursorSandbox) => {
serde_json::to_string_pretty(&plan.cursor.settings)?
}
Some(SandboxFormat::ClaudeSettings) | None => {
serde_json::to_string_pretty(&claude_doc)?
}
};
hosts.push(Shown {
host,
path: install::settings_path(&home, host)?,
text,
report: install::report(host, &plan),
});
}
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"
)?;
}
write_secrets(&mut out, &policy.secrets, &claude_doc, &plan.claude.report)?;
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> {
crate::terminal::refuse_agent("moat sandbox sync")?;
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, &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(&home, host)?;
if !path.parent().is_some_and(std::path::Path::is_dir) {
writeln!(out, "· {:<16} host not found", host.display_name())?;
continue;
}
let config = HostConfig::for_host(&home, host)?;
if !config.set_up(&binary) {
let why = match config.state(&binary) {
HookState::Missing => "not set up",
_ => "hook out of date or unreadable; `moat init` repairs it",
};
let (name, id) = (host.display_name(), host.id());
writeln!(out, "· {name:<16} {why} (`moat init --hosts {id}` adds it)")?;
continue;
}
let changed = install::write(&home, 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 install::format(host) {
Some(SandboxFormat::CodexProfile) => 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)
}
fn write_secrets(
out: &mut impl io::Write,
secrets: &[openmoat_core::Secret],
claude_doc: &Value,
claude: &Report,
) -> io::Result<()> {
if secrets.is_empty() {
return Ok(());
}
writeln!(out, "Secrets values are never shown")?;
let states = claude::mask::states(claude_doc, secrets, claude, install::MASK_FILES_DENIED);
for (secret, state) in states {
let source = match &secret.source {
openmoat_core::SecretSource::Env(name) => format!("env {name}"),
openmoat_core::SecretSource::File(path) => format!("file {path}"),
openmoat_core::SecretSource::Keychain { service, .. } => format!("keychain {service}"),
};
let claude = if install::unavailable(Host::ClaudeCode).is_some() {
"Claude Code's sandbox is not available here, so it masks nothing".to_owned()
} else {
state.describe()
};
writeln!(
out,
" {} {} {} from {source}; {claude}",
secret.id, secret.host, secret.header
)?;
}
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(())
}