use std::path::{Path, PathBuf};
use crate::error::{OlError, ERR_BOUNDARY_FOREIGN_PROVIDER};
pub(crate) const CONFIG_DIR_ENV: &str = "CODEX_HOME";
#[cfg(test)]
pub(crate) static CONFIG_DIR_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn relocated_dir() -> Option<PathBuf> {
std::env::var_os(CONFIG_DIR_ENV)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
pub fn config_dir() -> Option<PathBuf> {
match relocated_dir() {
Some(relocated) => Some(relocated),
None => Some(dirs::home_dir()?.join(".codex")),
}
}
pub fn detect() -> Option<PathBuf> {
let codex_dir = config_dir()?;
codex_dir.is_dir().then_some(codex_dir)
}
pub fn config_is_machine_global() -> bool {
let Some(resolved) = config_dir() else {
return true;
};
let Some(default) = dirs::home_dir().map(|home| home.join(".codex")) else {
return true;
};
let canonical = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
canonical(&resolved) == canonical(&default)
}
pub fn hooks_json_path(codex_dir: &Path) -> PathBuf {
codex_dir.join("hooks.json")
}
pub fn config_toml_path(codex_dir: &Path) -> PathBuf {
codex_dir.join("config.toml")
}
pub fn requirements_toml_path() -> Option<PathBuf> {
if cfg!(windows) {
None
} else {
Some(PathBuf::from("/etc/codex/requirements.toml")) }
}
#[derive(Debug, Default, serde::Deserialize)]
struct CodexConfig {
#[serde(default)]
hooks: HooksSection,
#[serde(default)]
features: FeaturesSection,
}
#[derive(Debug, Default, serde::Deserialize)]
struct HooksSection {
#[serde(default)]
state: std::collections::BTreeMap<String, HookStateToml>,
#[serde(default)]
allow_managed_hooks_only: Option<bool>,
}
#[derive(Debug, Default, serde::Deserialize)]
struct HookStateToml {
enabled: Option<bool>,
trusted_hash: Option<String>,
}
#[derive(Debug, Default, serde::Deserialize)]
struct FeaturesSection {
hooks: Option<bool>,
codex_hooks: Option<bool>,
plugin_hooks: Option<bool>,
}
enum ConfigLayer {
Absent,
Read(CodexConfig),
Unreadable,
}
fn read_config_layer(path: &Path) -> ConfigLayer {
let raw = match std::fs::read_to_string(path) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return ConfigLayer::Absent,
Err(_) => return ConfigLayer::Unreadable,
};
match toml::from_str::<CodexConfig>(&raw) {
Ok(config) => ConfigLayer::Read(config),
Err(_) => ConfigLayer::Unreadable,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledHandler {
pub command: String,
pub timeout_secs: Option<u64>,
pub group_index: usize,
pub handler_index: usize,
}
pub fn installed_handler(codex_dir: &Path, event: &str) -> Option<InstalledHandler> {
let raw = std::fs::read_to_string(hooks_json_path(codex_dir)).ok()?;
let parsed = super::jsonc::parse_settings_value(&raw).ok()?;
let groups = parsed
.get("hooks")
.and_then(|hooks| hooks.get(event))
.and_then(serde_json::Value::as_array)?;
for (group_index, group) in groups.iter().enumerate() {
let marked = matches!(
group.get("_openlatch"),
Some(serde_json::Value::Bool(true)) | Some(serde_json::Value::Object(_))
);
let Some(handlers) = group.get("hooks").and_then(serde_json::Value::as_array) else {
continue;
};
for (handler_index, handler) in handlers.iter().enumerate() {
let Some(command) = handler.get("command").and_then(serde_json::Value::as_str) else {
continue;
};
if !marked && !command.contains("openlatch-hook") {
continue;
}
return Some(InstalledHandler {
command: command.to_string(),
timeout_secs: handler.get("timeout").and_then(serde_json::Value::as_u64),
group_index,
handler_index,
});
}
}
None
}
pub fn trust_key(codex_dir: &Path, event: &str, handler: &InstalledHandler) -> String {
format!(
"{}:{}:{}:{}",
hooks_json_path(codex_dir).display(),
super::claude_code::pascal_to_snake(event),
handler.group_index,
handler.handler_index
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookTrust {
Trusted,
NeverTrusted,
Disabled,
}
pub fn hook_trust(codex_dir: &Path, event: &str, handler: &InstalledHandler) -> Option<HookTrust> {
let config = match read_config_layer(&config_toml_path(codex_dir)) {
ConfigLayer::Unreadable => return None,
ConfigLayer::Absent => return Some(HookTrust::NeverTrusted),
ConfigLayer::Read(config) => config,
};
let wire_event = super::claude_code::pascal_to_snake(event);
let ours = hooks_json_path(codex_dir);
let found = config.hooks.state.iter().find(|(key, _)| {
let Some(parsed) = split_state_key(key) else {
return false;
};
parsed.event == wire_event
&& parsed.group_index == handler.group_index
&& parsed.handler_index == handler.handler_index
&& is_same_file(Path::new(parsed.source_path), &ours)
});
Some(match found {
None => HookTrust::NeverTrusted,
Some((_, state)) if state.enabled == Some(false) => HookTrust::Disabled,
Some((_, state)) if state.trusted_hash.is_some() => HookTrust::Trusted,
Some(_) => HookTrust::NeverTrusted,
})
}
struct StateKey<'a> {
source_path: &'a str,
event: &'a str,
group_index: usize,
handler_index: usize,
}
fn split_state_key(key: &str) -> Option<StateKey<'_>> {
let mut parts = key.rsplitn(4, ':');
let handler_index = parts.next()?.parse().ok()?;
let group_index = parts.next()?.parse().ok()?;
let event = parts.next()?;
let source_path = parts.next()?;
Some(StateKey {
source_path,
event,
group_index,
handler_index,
})
}
fn is_same_file(a: &Path, b: &Path) -> bool {
let resolved = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
crate::core::path_compat::dedup_key(&resolved(a))
== crate::core::path_compat::dedup_key(&resolved(b))
}
pub fn suppressing_feature_flag(codex_dir: &Path) -> Option<&'static str> {
let ConfigLayer::Read(config) = read_config_layer(&config_toml_path(codex_dir)) else {
return None;
};
[
("hooks", config.features.hooks),
("codex_hooks", config.features.codex_hooks),
("plugin_hooks", config.features.plugin_hooks),
]
.into_iter()
.find_map(|(name, value)| (value == Some(false)).then_some(name))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ManagedHooksOnly {
Observed(bool),
Unobservable,
}
pub fn managed_hooks_only(requirements_toml: Option<&Path>) -> ManagedHooksOnly {
let Some(path) = requirements_toml else {
return ManagedHooksOnly::Unobservable;
};
match read_config_layer(path) {
ConfigLayer::Absent => ManagedHooksOnly::Observed(false),
ConfigLayer::Read(config) => {
ManagedHooksOnly::Observed(config.hooks.allow_managed_hooks_only.unwrap_or(false))
}
ConfigLayer::Unreadable => ManagedHooksOnly::Unobservable,
}
}
const PROVIDER_LABEL: &str = "OpenLatch boundary";
const MODEL_PROVIDER_KEY: &str = "model_provider";
const MODEL_PROVIDERS_TABLE: &str = "model_providers";
fn provider_base_url<'a>(doc: &'a toml_edit::DocumentMut, provider_name: &str) -> Option<&'a str> {
doc.get(MODEL_PROVIDERS_TABLE)
.and_then(toml_edit::Item::as_table_like)
.and_then(|providers| providers.get(provider_name))
.and_then(toml_edit::Item::as_table_like)
.and_then(|provider| provider.get("base_url"))
.and_then(toml_edit::Item::as_str)
}
pub(crate) fn provider_table_is_ours(
doc: &toml_edit::DocumentMut,
provider_name: &str,
) -> Option<bool> {
provider_base_url(doc, provider_name).map(crate::hooks::is_openlatch_loopback_base_url)
}
pub fn read_provider_base_url(config_toml: &Path, provider_name: &str) -> Option<String> {
let raw = std::fs::read_to_string(config_toml).ok()?;
let doc = raw.parse::<toml_edit::DocumentMut>().ok()?;
let url = provider_base_url(&doc, provider_name)?;
crate::hooks::is_openlatch_loopback_base_url(url).then(|| url.to_string())
}
fn set_root_value(doc: &mut toml_edit::DocumentMut, key: &str, v: &str) {
let decor = doc
.get(key)
.and_then(toml_edit::Item::as_value)
.map(|existing| existing.decor().clone());
let mut item = toml_edit::value(v);
if let (Some(decor), Some(value)) = (decor, item.as_value_mut()) {
*value.decor_mut() = decor;
}
doc[key] = item;
}
pub fn write_provider_table(
config_toml: &Path,
provider_name: &str,
wire_api: &str,
install_id_header: &str,
port: u16,
install_id: &str,
) -> Result<Prior, OlError> {
let mut prior = Prior::Ours;
crate::hooks::atomic::atomic_rewrite_toml(config_toml, |doc| {
if provider_table_is_ours(doc, provider_name) == Some(false) {
return Err(OlError::new(
ERR_BOUNDARY_FOREIGN_PROVIDER,
format!(
"'{}' already declares a [model_providers.{provider_name}] table that does \
not point at the OpenLatch boundary",
crate::core::path_compat::display_path(config_toml)
),
)
.with_suggestion(
"Rename or remove that provider table, or disable the model boundary for \
Codex CLI — OpenLatch will not overwrite a provider entry it did not write.",
));
}
prior = prior_from_document(doc, provider_name);
let mut provider = toml_edit::Table::new();
provider.insert("name", toml_edit::value(PROVIDER_LABEL));
provider.insert(
"base_url",
toml_edit::value(format!("http://127.0.0.1:{port}/v1")),
);
provider.insert("wire_api", toml_edit::value(wire_api));
provider.insert("requires_openai_auth", toml_edit::value(true));
let mut headers = toml_edit::Table::new();
headers.insert(install_id_header, toml_edit::value(install_id));
provider.insert("http_headers", toml_edit::Item::Table(headers));
let parent = doc
.entry(MODEL_PROVIDERS_TABLE)
.or_insert(toml_edit::Item::Table({
let mut t = toml_edit::Table::new();
t.set_implicit(true);
t
}));
let Some(parent) = parent.as_table_mut() else {
return Err(OlError::new(
ERR_BOUNDARY_FOREIGN_PROVIDER,
format!(
"'{}' declares `{MODEL_PROVIDERS_TABLE}` as something other than a table",
crate::core::path_compat::display_path(config_toml)
),
)
.with_suggestion(
"Fix or remove that key — OpenLatch will not rewrite a value it did not write.",
));
};
parent.insert(provider_name, toml_edit::Item::Table(provider));
set_root_value(doc, MODEL_PROVIDER_KEY, provider_name);
Ok(())
})?;
Ok(prior)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Prior {
Ours,
Theirs(Option<String>),
}
fn prior_from_document(doc: &toml_edit::DocumentMut, provider_name: &str) -> Prior {
match doc
.get(MODEL_PROVIDER_KEY)
.and_then(toml_edit::Item::as_str)
{
Some(current) if current == provider_name => Prior::Ours,
Some(current) => Prior::Theirs(Some(current.to_string())),
None => Prior::Theirs(None),
}
}
pub fn read_prior_provider(config_toml: &Path, provider_name: &str) -> Result<Prior, OlError> {
if !config_toml.exists() {
return Ok(Prior::Theirs(None));
}
let raw = std::fs::read_to_string(config_toml).map_err(|e| {
OlError::new(
ERR_BOUNDARY_FOREIGN_PROVIDER,
format!("cannot read {}: {e}", config_toml.display()),
)
})?;
let doc = raw.parse::<toml_edit::DocumentMut>().map_err(|e| {
OlError::new(
ERR_BOUNDARY_FOREIGN_PROVIDER,
format!("{} is not valid TOML: {e}", config_toml.display()),
)
})?;
Ok(prior_from_document(&doc, provider_name))
}
pub fn remove_provider_table(
config_toml: &Path,
provider_name: &str,
prior: Option<Option<String>>,
) -> Result<(), OlError> {
if !config_toml.exists() {
return Ok(());
}
crate::hooks::atomic::atomic_rewrite_toml(config_toml, |doc| {
if provider_table_is_ours(doc, provider_name) != Some(true) {
return Ok(());
}
let mut parent_is_empty = false;
if let Some(providers) = doc
.get_mut(MODEL_PROVIDERS_TABLE)
.and_then(toml_edit::Item::as_table_mut)
{
providers.remove(provider_name);
parent_is_empty = providers.is_empty();
}
if parent_is_empty {
doc.remove(MODEL_PROVIDERS_TABLE);
}
match prior {
Some(Some(ref v))
if doc
.get(MODEL_PROVIDER_KEY)
.and_then(toml_edit::Item::as_str)
== Some(provider_name) =>
{
set_root_value(doc, MODEL_PROVIDER_KEY, v)
}
Some(Some(_)) => {}
Some(None) | None => {
if doc
.get(MODEL_PROVIDER_KEY)
.and_then(toml_edit::Item::as_str)
== Some(provider_name)
{
doc.remove(MODEL_PROVIDER_KEY);
}
}
}
Ok(())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codex_paths_are_derived_from_the_config_dir() {
let root = Path::new("/home/test/.codex");
assert_eq!(hooks_json_path(root), root.join("hooks.json"));
assert_eq!(config_toml_path(root), root.join("config.toml"));
}
#[test]
fn codex_trust_key_uses_the_found_indices() {
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
std::fs::write(
hooks_json_path(root),
r#"{
"hooks": {
"PreToolUse": [
{"matcher": "", "hooks": [{"type": "command", "command": "echo mine", "timeout": 5}]},
{"matcher": "Bash", "_openlatch": {"v": 1, "id": "x"},
"hooks": [{"type": "command", "command": "\"/ol/bin/openlatch-hook\" --agent codex-cli --event pre_tool_use", "timeout": 10}]}
]
}
}"#,
)
.expect("write hooks.json");
let ours = installed_handler(root, "PreToolUse").expect("our appended group must be found");
assert_eq!(ours.group_index, 1, "ours is the SECOND group: {ours:?}");
assert_eq!(ours.handler_index, 0);
assert_eq!(ours.timeout_secs, Some(10));
assert!(ours.command.contains("openlatch-hook"));
let key = trust_key(root, "PreToolUse", &ours);
assert!(
key.ends_with(":1:0"),
"the key must carry the indices we FOUND, not :0:0 — {key}"
);
assert!(
!key.ends_with(":0:0"),
"a :0:0 key reads the customer's trust state as ours — {key}"
);
assert!(key.contains(":pre_tool_use:"), "{key}");
let at_zero = InstalledHandler {
group_index: 0,
handler_index: 0,
..ours.clone()
};
let documented_root = Path::new("/home/u/.codex");
let documented_key = format!(
"{}:pre_tool_use:0:0",
documented_root.join("hooks.json").display()
);
assert_eq!(
trust_key(documented_root, "PreToolUse", &at_zero),
documented_key
);
assert_eq!(
hook_trust(root, "PreToolUse", &ours),
Some(HookTrust::NeverTrusted),
"an absent config.toml is an answer, not an unknown"
);
let real_key = trust_key(root, "PreToolUse", &ours);
let customer_key = trust_key(root, "PreToolUse", &at_zero);
std::fs::write(
config_toml_path(root),
format!(
"[hooks.state.'{customer_key}']\n\
trusted_hash = \"customer-hash\"\n\
\n\
[hooks.state.'{real_key}']\n\
trusted_hash = \"ours\"\n"
),
)
.expect("write config.toml");
assert_eq!(
hook_trust(root, "PreToolUse", &ours),
Some(HookTrust::Trusted)
);
std::fs::write(
config_toml_path(root),
format!(
"[hooks.state.'{customer_key}']\n\
trusted_hash = \"customer-hash\"\n\
\n\
[hooks.state.'{real_key}']\n"
),
)
.expect("write config.toml");
assert_eq!(
hook_trust(root, "PreToolUse", &ours),
Some(HookTrust::NeverTrusted),
"a present key with no trusted_hash is a re-armed review"
);
std::fs::write(
config_toml_path(root),
format!(
"[hooks.state.'{real_key}']\n\
enabled = false\n\
trusted_hash = \"ours\"\n"
),
)
.expect("write config.toml");
assert_eq!(
hook_trust(root, "PreToolUse", &ours),
Some(HookTrust::Disabled)
);
std::fs::write(config_toml_path(root), "[hooks.state\n").expect("write config.toml");
assert_eq!(hook_trust(root, "PreToolUse", &ours), None);
std::fs::write(
config_toml_path(root),
format!("[hooks.state.'{real_key}']\ntrusted_hash = \"ours\"\n"),
)
.expect("write config.toml");
let link_parent = tempfile::tempdir().expect("temp dir");
let link = link_parent.path().join("codex-link");
symlink_dir(root, &link).expect("symlink");
assert_ne!(
trust_key(&link, "PreToolUse", &ours),
real_key,
"the fixture is pointless unless the two spellings differ"
);
assert_eq!(
hook_trust(&link, "PreToolUse", &ours),
Some(HookTrust::Trusted),
"a symlinked $CODEX_HOME must still find the entry Codex wrote"
);
}
#[cfg(unix)]
fn symlink_dir(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
fn symlink_dir(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::windows::fs::symlink_dir(target, link)
}
const SEED: &str = "\
# my config, hand written
model = \"gpt-5-codex\" # trailing comment
# a customer provider they actually use
model_provider = \"corporate-gateway\"
[model_providers.corporate-gateway]
name = \"ACME\"
base_url = \"https://llm.acme.internal/v1\"
wire_api = \"responses\"
[tui]
theme = \"dark\"
";
fn seeded_config(seed: &str) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let path = config_toml_path(dir.path());
std::fs::write(&path, seed).expect("seed config.toml");
(dir, path)
}
fn install(path: &Path, port: u16) -> Result<Prior, OlError> {
write_provider_table(
path,
"openlatch",
"responses",
"x-openlatch-install-id",
port,
"agt_demo",
)
}
#[test]
fn toml_writer_preserves_comments_and_key_order() {
let (_dir, path) = seeded_config(SEED);
install(&path, 7600).expect("install");
let after = std::fs::read_to_string(&path).expect("read back");
for fragment in [
"# my config, hand written",
"model = \"gpt-5-codex\" # trailing comment",
"# a customer provider they actually use",
"[model_providers.corporate-gateway]",
"name = \"ACME\"",
"base_url = \"https://llm.acme.internal/v1\"",
"[tui]",
"theme = \"dark\"",
] {
assert!(
after.contains(fragment),
"install ate `{fragment}`:\n{after}"
);
}
assert!(
after.contains("[model_providers.openlatch]"),
"our table must be an explicit header, never an inline table:\n{after}"
);
assert!(
after.contains("base_url = \"http://127.0.0.1:7600/v1\""),
"the RESOLVED port, and Codex's `/v1` suffix:\n{after}"
);
assert!(
after.contains("name = \"OpenLatch boundary\""),
"`name` is MANDATORY — Codex rejects the whole config without it:\n{after}"
);
assert!(
after.contains("wire_api = \"responses\""),
"the wire_api comes from the binding:\n{after}"
);
assert!(
after.contains("[model_providers.openlatch.http_headers]"),
"the install-id header rides on the provider table:\n{after}"
);
assert_eq!(
after.matches("model_provider = ").count(),
1,
"the pointer is replaced in place, never duplicated:\n{after}"
);
assert!(
after.contains("model_provider = \"openlatch\""),
"and it names us after an install:\n{after}"
);
assert_eq!(
read_provider_base_url(&path, "openlatch").as_deref(),
Some("http://127.0.0.1:7600/v1")
);
assert_eq!(
read_provider_base_url(&path, "corporate-gateway"),
None,
"a customer's own provider table is not evidence that we wired anything"
);
}
#[test]
fn toml_uninstall_is_byte_identical_to_the_seed() {
let (_dir, path) = seeded_config(SEED);
let prior = install(&path, 7600).expect("install");
assert_eq!(
prior,
Prior::Theirs(Some("corporate-gateway".into())),
"the writer must hand back what the file named before it"
);
remove_provider_table(&path, "openlatch", Some(Some("corporate-gateway".into())))
.expect("uninstall");
let after = std::fs::read_to_string(&path).expect("read back");
assert_eq!(after, SEED, "uninstall must be byte-identical to the seed");
assert!(
!after.contains("[model_providers]"),
"a bare parent header must not survive uninstall:\n{after}"
);
}
#[test]
fn a_provider_the_customer_chose_after_install_is_not_overwritten() {
let (_dir, path) = seeded_config(SEED);
install(&path, 7600).expect("install");
let installed = std::fs::read_to_string(&path).expect("read");
std::fs::write(
&path,
installed.replace(
r#"model_provider = "openlatch""#,
r#"model_provider = "new-gateway""#,
),
)
.expect("customer edit");
remove_provider_table(&path, "openlatch", Some(Some("corporate-gateway".into())))
.expect("uninstall");
let after = std::fs::read_to_string(&path).expect("read back");
assert!(
after.contains(r#"model_provider = "new-gateway""#),
"the customer's later choice must survive uninstall:\n{after}"
);
assert!(
!after.contains(r#"model_provider = "corporate-gateway""#),
"uninstall must not resurrect the pointer we displaced once the customer \
has moved on:\n{after}"
);
assert!(
after.contains("[model_providers.corporate-gateway]"),
"the customer's own provider table is theirs and must survive:\n{after}"
);
assert!(
!after.contains("[model_providers.openlatch]"),
"our own provider table must still be removed:\n{after}"
);
}
#[test]
fn toml_restores_a_customer_model_provider() {
let (_dir, path) = seeded_config(SEED);
install(&path, 7600).expect("install");
remove_provider_table(&path, "openlatch", Some(Some("corporate-gateway".into())))
.expect("uninstall");
assert!(
std::fs::read_to_string(&path)
.expect("read back")
.contains("model_provider = \"corporate-gateway\""),
"the customer's pointer must come back"
);
let fresh = "model = \"gpt-5-codex\"\n";
let (_dir2, path2) = seeded_config(fresh);
assert_eq!(
install(&path2, 7600).expect("install"),
Prior::Theirs(None),
"a file that named no provider records an absence, not a value"
);
remove_provider_table(&path2, "openlatch", Some(None)).expect("uninstall");
let after = std::fs::read_to_string(&path2).expect("read back");
assert_eq!(after, fresh, "and the file comes back byte-identical");
assert!(!after.contains("model_provider"));
}
#[test]
fn reinstall_does_not_destroy_the_recorded_prior() {
let _guard = crate::config::OPENLATCH_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let state = tempfile::tempdir().expect("state dir");
let prev = std::env::var_os("OPENLATCH_DIR");
std::env::set_var("OPENLATCH_DIR", state.path());
let (_dir, path) = seeded_config(SEED);
let binding = crate::hooks::binding::test_support::FakeBinding {
agent_type: "codex-cli",
config_dir: path.parent().expect("parent").to_path_buf(),
boundary_wiring: Some(crate::hooks::binding::BoundaryWiring {
wire_format: crate::boundary::wire_format::WireFormat::OpenAiResponses,
endpoint: crate::hooks::binding::EndpointConvention::TomlProvider {
provider_name: "openlatch",
wire_api: "responses",
},
install_id_header: "x-openlatch-install-id",
}),
..Default::default()
};
crate::hooks::write_boundary_config(&binding, 7600, "agt_demo").expect("install");
crate::hooks::write_boundary_config(&binding, 7600, "agt_demo").expect("re-install");
crate::hooks::remove_boundary_config(&binding).expect("uninstall");
crate::hooks::remove_boundary_config(&binding).expect("second uninstall pass");
let after = std::fs::read_to_string(&path).expect("read back");
match prev {
Some(v) => std::env::set_var("OPENLATCH_DIR", v),
None => std::env::remove_var("OPENLATCH_DIR"),
}
assert_eq!(
after, SEED,
"after TWO installs the uninstall must still restore the CUSTOMER's pointer"
);
}
#[test]
fn foreign_openlatch_table_refuses() {
let foreign = "\
# somebody else got here first
model_provider = \"openlatch\"
[model_providers.openlatch]
name = \"Someone else's openlatch\"
base_url = \"https://openlatch.internal.example/v1\"
wire_api = \"responses\"
";
let (_dir, path) = seeded_config(foreign);
let err = install(&path, 7600).expect_err("a foreign table must refuse");
assert_eq!(err.code, ERR_BOUNDARY_FOREIGN_PROVIDER);
assert!(
err.suggestion.is_some(),
"a refusal the operator cannot act on is not a remedy"
);
assert_eq!(
std::fs::read_to_string(&path).expect("read back"),
foreign,
"the file must be untouched after the refusal"
);
remove_provider_table(&path, "openlatch", Some(Some("whatever".into())))
.expect("removal is a no-op on a foreign table");
assert_eq!(
std::fs::read_to_string(&path).expect("read back"),
foreign,
"uninstall must not eat a provider table it did not write"
);
}
}