use std::path::Path;
use anyhow::{Context as _, Result};
use openmoat_hosts::{Host, SandboxFormat};
use serde_json::{Value, json};
use toml_edit::DocumentMut;
use super::install::{Rendered, parse_json, read_toml, unavailable};
use super::{Plan, Report, claude, codex, cursor};
use crate::home::Home;
use crate::install::{Recorded, read_or_empty};
use crate::integrity::Lock;
#[derive(Clone, Copy)]
pub enum Undo {
WithHooks(fn(&mut Value, &[String]) -> bool),
Toml(fn(&mut DocumentMut) -> bool),
Json(fn(&mut Value) -> bool),
}
pub trait SandboxBackend: Sync {
fn render(
&self,
home: &Home,
host: Host,
plan: &Plan,
current: &str,
) -> Result<Option<Rendered>>;
fn report<'a>(&self, plan: &'a Plan) -> &'a Report;
fn shown(&self, plan: &Plan) -> Result<String>;
fn problems(
&self,
home: &Home,
path: &Path,
plan: &Plan,
lock: Option<&Lock>,
) -> Result<Vec<String>>;
fn undo(&self) -> Undo;
fn pinned_by_part(&self) -> bool {
false
}
fn masks_secrets(&self) -> bool {
false
}
}
pub fn backend(format: SandboxFormat) -> &'static dyn SandboxBackend {
match format {
SandboxFormat::ClaudeSettings => &ClaudeSettings,
SandboxFormat::CodexProfile => &CodexProfile,
SandboxFormat::CursorSandbox => &CursorSandbox,
}
}
const OUT_OF_DATE: &str = "differs from the policy; run `openmoat sandbox sync`";
const NOT_PINNED: &str = "not pinned by the lock; run `openmoat sandbox sync`";
struct ClaudeSettings;
impl SandboxBackend for ClaudeSettings {
fn render(
&self,
home: &Home,
host: Host,
plan: &Plan,
current: &str,
) -> Result<Option<Rendered>> {
let mut owned = Recorded::load(home)?.claude_rules;
let mut root = parse_json(current)?;
let changed = if unavailable(host).is_some() {
claude::apply_rules(&mut root, &plan.claude, &mut owned)
} else {
claude::apply(&mut root, &plan.claude, &mut owned)
}?;
Ok(changed.then_some(Rendered {
text: serde_json::to_string_pretty(&root)? + "\n",
claude_rules: Some(owned),
}))
}
fn report<'a>(&self, plan: &'a Plan) -> &'a Report {
&plan.claude.report
}
fn shown(&self, plan: &Plan) -> Result<String> {
let mut doc = json!({});
claude::apply(&mut doc, &plan.claude, &mut Vec::new())?;
Ok(serde_json::to_string_pretty(&doc)?)
}
fn problems(
&self,
home: &Home,
path: &Path,
plan: &Plan,
_lock: Option<&Lock>,
) -> Result<Vec<String>> {
let root = read_or_empty(path)?;
let mut found = claude::weaknesses(&root, plan.claude.block_reads, plan.proxy_port);
found.extend(claude::mask::weaknesses(&root, &plan.claude));
let owned = Recorded::load(home)?.claude_rules;
if !claude::in_sync(&root, &plan.claude, &owned) {
found.push(OUT_OF_DATE.to_owned());
}
Ok(found)
}
fn undo(&self) -> Undo {
Undo::WithHooks(claude::remove)
}
fn masks_secrets(&self) -> bool {
true
}
}
struct CodexProfile;
impl SandboxBackend for CodexProfile {
fn render(
&self,
_home: &Home,
_host: Host,
plan: &Plan,
current: &str,
) -> Result<Option<Rendered>> {
let mut doc: DocumentMut = current.parse().context("parsing the settings")?;
let changed = codex::apply(&mut doc, &plan.codex)?;
Ok(changed.then_some(Rendered {
text: doc.to_string(),
claude_rules: None,
}))
}
fn report<'a>(&self, plan: &'a Plan) -> &'a Report {
&plan.codex.report
}
fn shown(&self, plan: &Plan) -> Result<String> {
let mut doc = DocumentMut::new();
codex::apply(&mut doc, &plan.codex)?;
Ok(doc.to_string())
}
fn problems(
&self,
_home: &Home,
path: &Path,
plan: &Plan,
lock: Option<&Lock>,
) -> Result<Vec<String>> {
let doc = read_toml(path)?;
let mut found = codex::weaknesses(&doc, plan.proxy_port.is_some());
if !codex::in_sync(&doc, &plan.codex) {
found.push(OUT_OF_DATE.to_owned());
}
if lock.is_some_and(|l| !l.pins_codex_profile(path)) {
found.push(NOT_PINNED.to_owned());
}
Ok(found)
}
fn undo(&self) -> Undo {
Undo::Toml(codex::remove)
}
fn pinned_by_part(&self) -> bool {
true
}
}
struct CursorSandbox;
impl SandboxBackend for CursorSandbox {
fn render(
&self,
_home: &Home,
host: Host,
plan: &Plan,
current: &str,
) -> Result<Option<Rendered>> {
if unavailable(host).is_some() {
return Ok(None);
}
let mut root = parse_json(current)?;
let changed = cursor::apply(&mut root, &plan.cursor)?;
Ok(changed.then_some(Rendered {
text: serde_json::to_string_pretty(&root)? + "\n",
claude_rules: None,
}))
}
fn report<'a>(&self, plan: &'a Plan) -> &'a Report {
&plan.cursor.report
}
fn shown(&self, plan: &Plan) -> Result<String> {
Ok(serde_json::to_string_pretty(&plan.cursor.settings)?)
}
fn problems(
&self,
_home: &Home,
path: &Path,
plan: &Plan,
lock: Option<&Lock>,
) -> Result<Vec<String>> {
let root = read_or_empty(path)?;
let mut found = cursor::weaknesses(&root);
if !cursor::in_sync(&root, &plan.cursor) {
found.push(OUT_OF_DATE.to_owned());
}
if lock.is_some_and(|l| path.is_file() && !l.pins(path)) {
found.push(NOT_PINNED.to_owned());
}
Ok(found)
}
fn undo(&self) -> Undo {
Undo::Json(cursor::remove)
}
}