use std::path::Path;
use anyhow::{Result, bail};
use toml_edit::{DocumentMut, Item, Table, Value, value};
use super::{Generated, PROFILE};
pub fn apply(doc: &mut DocumentMut, generated: &Generated) -> Result<bool> {
let before = doc.to_string();
doc.insert("default_permissions", value(PROFILE));
let permissions = table_at(doc.as_table_mut(), "permissions")?;
permissions.insert(PROFILE, Item::Table(generated.profile.clone()));
let features = table_at(doc.as_table_mut(), "features")?;
match features.get_mut("network_proxy") {
Some(Item::Table(proxy)) => {
proxy.insert("enabled", value(true));
}
_ => {
features.insert("network_proxy", value(true));
}
}
Ok(doc.to_string() != before)
}
pub fn protect(generated: &mut Generated, codex_home: &Path) {
let filesystem = generated
.profile
.get_mut("filesystem")
.and_then(Item::as_table_mut);
if let Some(filesystem) = filesystem {
filesystem.insert(&crate::context::path_string(codex_home), value("deny"));
}
}
pub fn owned_part(doc: &DocumentMut) -> String {
let permissions = doc.get("permissions").and_then(|p| p.get(PROFILE));
let proxy = doc.get("features").and_then(|f| f.get("network_proxy"));
format!(
"default_permissions={}\npermissions.{PROFILE}={}\nfeatures.network_proxy={}\n",
canon(doc.get("default_permissions")),
canon(permissions),
canon(proxy)
)
}
pub fn in_sync(doc: &DocumentMut, generated: &Generated) -> bool {
let mut expected = doc.clone();
apply(&mut expected, generated).is_ok() && owned_part(&expected) == owned_part(doc)
}
pub fn weaknesses(doc: &DocumentMut, via_moat_proxy: bool) -> Vec<String> {
let mut out = Vec::new();
if doc.get("default_permissions").and_then(Item::as_str) != Some(PROFILE) {
out.push(format!(
"default_permissions is not \"{PROFILE}\": commands run under another profile"
));
}
if let Some(mode) = doc.get("sandbox_mode") {
out.push(format!(
"sandbox_mode is set ({}): remove it so the moat profile is the only one",
mode.to_string().trim()
));
}
if doc
.get("permissions")
.and_then(|p| p.get(PROFILE))
.is_none()
{
out.push(format!("[permissions.{PROFILE}] is missing"));
}
let proxy = doc.get("features").and_then(|f| f.get("network_proxy"));
let enabled = proxy.and_then(Item::as_bool).unwrap_or(false)
|| proxy
.and_then(|p| p.get("enabled"))
.and_then(Item::as_bool)
.unwrap_or(false);
if !enabled {
out.push("features.network_proxy is off: the domain rules are not enforced".to_owned());
}
let network = doc
.get("permissions")
.and_then(|p| p.get(PROFILE))
.and_then(|p| p.get("network"));
let set = |key: &str| network.and_then(|n| n.get(key)).and_then(Item::as_bool);
if via_moat_proxy && set("allow_upstream_proxy") == Some(false) {
out.push(
"allow_upstream_proxy is false: Codex's proxy never hands traffic to `moat proxy`"
.to_owned(),
);
}
if set("allow_local_binding") == Some(true) {
out.push(
"allow_local_binding is true: commands connect to local services directly".to_owned(),
);
}
out
}
fn canon(item: Option<&Item>) -> String {
match item {
None | Some(Item::None) => "none".to_owned(),
Some(Item::Value(v)) => canon_value(v),
Some(Item::Table(t)) => canon_entries(t.iter().map(|(k, v)| (k, canon(Some(v))))),
Some(Item::ArrayOfTables(a)) => {
let tables: Vec<String> = a
.iter()
.map(|t| canon_entries(t.iter().map(|(k, v)| (k, canon(Some(v))))))
.collect();
format!("[{}]", tables.join(","))
}
}
}
fn canon_value(v: &Value) -> String {
match v {
Value::String(s) => format!("{:?}", s.value()),
Value::Array(a) => {
let items: Vec<String> = a.iter().map(canon_value).collect();
format!("[{}]", items.join(","))
}
Value::InlineTable(t) => canon_entries(t.iter().map(|(k, v)| (k, canon_value(v)))),
other => other.clone().decorated("", "").to_string(),
}
}
fn canon_entries<'a>(entries: impl Iterator<Item = (&'a str, String)>) -> String {
let mut pairs: Vec<String> = entries.map(|(k, v)| format!("{k:?}={v}")).collect();
pairs.sort();
format!("{{{}}}", pairs.join(","))
}
fn table_at<'a>(parent: &'a mut Table, key: &str) -> Result<&'a mut Table> {
let item = parent.entry(key).or_insert_with(|| {
let mut table = Table::new();
table.set_implicit(true);
Item::Table(table)
});
match item {
Item::Table(table) => Ok(table),
_ => bail!("`{key}` in the Codex config is not a table"),
}
}