use std::io::{Read as _, Write as _};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use serde_json::{json, Value};
use crate::core::hook_state::marker::OpenlatchMarker;
use crate::error::ERR_HOOK_NOT_ARMED;
use crate::hooks::codex_cli::{
HookLister, HookTrust, InstalledHandler, ListedHook, ManagedHooksOnly,
};
use crate::model_relay::wire_format::WireFormat;
use super::super::binding::{
AgentBinding, BindingCapabilities, DaemonChannel, EndpointConvention, FailureMode,
LivenessReport, ModelRelayWiring, TrustOccasion,
};
const EVENT_TYPES: [&str; 12] = [
"PreToolUse",
"PermissionRequest",
"PostToolUse",
"UserPromptSubmit",
"SessionStart",
"SessionEnd",
"PreCompact",
"PostCompact",
"Stop",
"SubagentStop",
"SubagentStart",
"Interrupt",
];
pub struct CodexCliBinding {
pub codex_dir: PathBuf,
pub hooks_path: PathBuf,
pub requirements_toml: Option<PathBuf>,
pub hooks_list: HookLister,
}
impl CodexCliBinding {
pub fn detect() -> Option<Self> {
let codex_dir = crate::hooks::codex_cli::detect()?;
let hooks_path = crate::hooks::codex_cli::hooks_json_path(&codex_dir);
Some(Self {
codex_dir,
hooks_path,
requirements_toml: crate::hooks::codex_cli::requirements_toml_path(),
hooks_list: crate::hooks::codex_cli::app_server_lister(),
})
}
}
impl AgentBinding for CodexCliBinding {
fn agent_type(&self) -> &'static str {
"codex-cli"
}
fn display_name(&self) -> &'static str {
"Codex CLI"
}
fn config_dir(&self) -> PathBuf {
self.codex_dir.clone()
}
fn hook_config_path(&self) -> PathBuf {
self.hooks_path.clone()
}
fn hook_event_types(&self) -> &'static [&'static str] {
&EVENT_TYPES
}
fn load_bearing_events(&self) -> &'static [&'static str] {
&["PreToolUse", "PostToolUse", "SessionStart"]
}
fn daemon_channel(&self) -> DaemonChannel {
DaemonChannel::OpenlatchDirArg
}
fn liveness(&self) -> LivenessReport {
let handler = crate::hooks::codex_cli::installed_handler(&self.codex_dir, PROBE_EVENT);
let listing = handler
.as_ref()
.and_then(|_| (self.hooks_list)(&self.codex_dir));
let mut caveats = vec![
FEATURE_DEFAULT_CAVEAT.to_string(),
PROFILE_CAVEAT.to_string(),
];
let (trusted, provenance) = self.check_trusted(handler.as_ref(), listing.as_deref());
caveats.extend(provenance);
let dimensions = [
self.check_binary_resolves(handler.as_ref()),
trusted,
self.check_not_suppressed(),
];
let mut failure: Option<(String, String)> = None;
let mut off: Option<(String, String)> = None;
let mut unknowns: Vec<String> = Vec::new();
for dimension in dimensions {
match dimension {
Dimension::Ok => {}
Dimension::Failed { detail, remedy } => {
failure.get_or_insert((detail, remedy));
}
Dimension::Off { detail, remedy } => {
off.get_or_insert((detail, remedy));
}
Dimension::Unobservable(note) => unknowns.push(note),
}
}
let is_off = failure.is_none() && off.is_some();
let failure = failure.or(off);
if let Some((detail, remedy)) = failure {
return LivenessReport {
armed: Some(false),
detail: Some(sentences(&detail, &unknowns, &caveats)),
remedy: Some(remedy),
code: Some(ERR_HOOK_NOT_ARMED),
off: is_off,
};
}
if !unknowns.is_empty() {
return LivenessReport {
armed: None,
detail: Some(sentences(CANNOT_TELL, &unknowns, &caveats)),
remedy: None,
code: None,
off: false,
};
}
LivenessReport {
armed: Some(true),
detail: Some(sentences(ARMED, &unknowns, &caveats)),
remedy: None,
code: None,
off: false,
}
}
fn build_hook_entry(
&self,
event: &str,
binary: &Path,
_port: u16,
marker: &OpenlatchMarker,
) -> Value {
let wire_event = crate::hooks::claude_code::pascal_to_snake(event);
let binary_str = binary.display().to_string();
let openlatch_dir = crate::config::openlatch_dir().display().to_string();
let command = format!(
r#""{binary_str}" --agent codex-cli --event {wire_event} --openlatch-dir "{openlatch_dir}""#
);
let timeout = match event {
"PreToolUse" => 900,
"SessionEnd" | "Interrupt" => 3,
_ => 10,
};
let hook_inner = json!({
"type": "command",
"command": command,
"timeout": timeout,
});
let marker_value =
serde_json::to_value(marker).expect("OpenlatchMarker is always serializable");
let matcher = match event {
"PreToolUse" => "Bash",
_ => "",
};
json!({
"matcher": matcher,
"_openlatch": marker_value,
"hooks": [hook_inner],
})
}
fn config_is_machine_global(&self) -> bool {
crate::hooks::codex_cli::config_is_machine_global()
}
fn capabilities(&self) -> BindingCapabilities {
BindingCapabilities {
expressible: &["allow", "deny"],
can_mutate_arguments: true,
native_failure_mode: FailureMode::FailOpen,
admin_owned_settings: true,
declares_session_in_request: false,
}
}
fn model_relay_wiring(&self) -> Option<ModelRelayWiring> {
Some(ModelRelayWiring {
wire_format: WireFormat::OpenAiResponses,
endpoint: EndpointConvention::TomlProvider {
provider_name: "openlatch",
wire_api: "responses",
},
install_id_header: "x-openlatch-install-id",
})
}
fn trust_own_hooks(&self, occasion: TrustOccasion) -> Result<usize, crate::error::OlError> {
if occasion == TrustOccasion::Drift
&& !crate::hooks::codex_cli::own_hooks_need_trust(&self.codex_dir)
{
return Ok(0);
}
let Some(listing) = (self.hooks_list)(&self.codex_dir) else {
tracing::info!(
codex_dir = %self.codex_dir.display(),
"Codex could not be asked for its hook listing — trust not granted; \
`openlatch doctor` reports what Codex has recorded"
);
return Ok(0);
};
let grants =
crate::hooks::codex_cli::grant_own_hook_trust(&self.codex_dir, &listing, occasion)?;
if let Err(e) =
crate::hooks::hook_trust_grants::record(&crate::config::openlatch_dir(), &grants)
{
tracing::warn!(error = %e, "Codex hook trust granted but not recorded");
}
Ok(grants.len())
}
fn revoke_own_hook_trust(&self) -> Result<(), crate::error::OlError> {
let grants = crate::hooks::hook_trust_grants::take_for(
&crate::config::openlatch_dir(),
&self.hooks_path,
)?;
crate::hooks::codex_cli::revoke_granted_trust(&self.codex_dir, &grants)
}
}
pub const DOCTOR_PROBE_TOOL_NAME: &str = "OpenlatchDoctorProbe";
const PROBE_EVENT: &str = "PreToolUse";
const PROBE_FALLBACK_TIMEOUT_SECS: u64 = 10;
const TRUST_REMEDY: &str = "Run `openlatch doctor --fix`, which re-installs the hook and trusts \
it in Codex, or trust it yourself with `/hooks` in Codex.";
const MODIFIED_REMEDY: &str = "Run `openlatch doctor --fix` to trust the hook's current command, \
or review the change with `/hooks` in Codex.";
const DISABLED_REMEDY: &str = "Switch the OpenLatch hook back on with `/hooks` in Codex, or run \
`openlatch doctor --fix`.";
const FEATURE_DEFAULT_CAVEAT: &str =
"An absent `features.*` key is read as no suppression: those keys are absent on a default \
host and their defaults are undocumented, so they are re-verified on each Codex upgrade.";
const PROFILE_CAVEAT: &str =
"A `codex --profile <name>` invocation may override `features.*` from `<name>.config.toml`; \
not observable from `config.toml`.";
const ARMED: &str = "The installed PreToolUse hook answers, Codex records it as trusted, and no \
administrative suppression was observed.";
const CANNOT_TELL: &str = "Codex enforcement could not be proven either way on this host.";
enum Dimension {
Ok,
Off {
detail: String,
remedy: String,
},
Failed {
detail: String,
remedy: String,
},
Unobservable(String),
}
impl CodexCliBinding {
fn check_binary_resolves(&self, handler: Option<&InstalledHandler>) -> Dimension {
let Some(handler) = handler else {
return Dimension::Failed {
detail: format!(
"No OpenLatch {PROBE_EVENT} hook is registered in {} — that row is the one \
that carries every deny.",
self.hooks_path.display()
),
remedy: "Run `openlatch init` to install the Codex hooks, or `openlatch doctor \
--fix` to repair an existing install."
.to_string(),
};
};
let timeout =
Duration::from_secs(handler.timeout_secs.unwrap_or(PROBE_FALLBACK_TIMEOUT_SECS));
match run_probe(&handler.command, timeout) {
ProbeOutcome::Answered => Dimension::Ok,
ProbeOutcome::Unspawnable(why) => Dimension::Unobservable(format!(
"The login shell Codex runs hook commands through (`sh -lc`) could not be \
started here ({why}), so the installed command was never exercised."
)),
ProbeOutcome::Bad(why) => Dimension::Failed {
detail: format!(
"The installed {PROBE_EVENT} hook command did not answer: {why}. Codex fails \
open on a hook it cannot run, and says nothing while it does."
),
remedy: format!(
"Restore the hook binary at {} — `openlatch doctor --fix` restages it — then \
re-run `openlatch doctor`.",
binary_from_command(&handler.command)
),
},
}
}
fn check_trusted(
&self,
handler: Option<&InstalledHandler>,
listing: Option<&[ListedHook]>,
) -> (Dimension, Option<String>) {
let Some(handler) = handler else {
return (Dimension::Ok, None);
};
let config_toml = crate::hooks::codex_cli::config_toml_path(&self.codex_dir);
let listed = listing.and_then(|listing| {
crate::hooks::codex_cli::listed_trust(listing, &self.codex_dir, PROBE_EVENT, handler)
});
let provenance = listed.and_then(|(trust, hook)| {
let grant =
crate::hooks::hook_trust_grants::lookup(&crate::config::openlatch_dir(), &hook.key);
attribution(trust, hook, grant.as_ref())
});
let trust = match listed {
Some((trust, _)) => Some(trust),
None => crate::hooks::codex_cli::hook_trust(&self.codex_dir, PROBE_EVENT, handler),
};
let dimension = match trust {
None => Dimension::Unobservable(format!(
"Codex could not be asked and {} could not be read, so whether Codex trusts this \
hook is unknown.",
config_toml.display()
)),
Some(HookTrust::Trusted | HookTrust::Managed) => Dimension::Ok,
Some(HookTrust::NeverTrusted) => Dimension::Failed {
detail: format!(
"Codex has not been told to trust this hook, so it never runs it. \
Recorded in {}.",
config_toml.display()
),
remedy: TRUST_REMEDY.to_string(),
},
Some(HookTrust::Modified) => Dimension::Failed {
detail: "The hook's command changed after Codex trusted it, so Codex re-armed \
its review and stopped running it."
.to_string(),
remedy: MODIFIED_REMEDY.to_string(),
},
Some(HookTrust::Disabled) => Dimension::Off {
detail: format!(
"The OpenLatch hook is switched off in Codex (`enabled = false` in {}), so \
it is never spawned.",
config_toml.display()
),
remedy: DISABLED_REMEDY.to_string(),
},
};
(dimension, provenance)
}
fn check_not_suppressed(&self) -> Dimension {
let config_toml = crate::hooks::codex_cli::config_toml_path(&self.codex_dir);
if let Some(key) = crate::hooks::codex_cli::suppressing_feature_flag(&self.codex_dir) {
return Dimension::Failed {
detail: format!(
"`features.{key} = false` in {} switches Codex hooks off wholesale.",
config_toml.display()
),
remedy: format!(
"Remove `features.{key} = false` from {} (or set it to `true`), then re-run \
`openlatch doctor`.",
config_toml.display()
),
};
}
match crate::hooks::codex_cli::managed_hooks_only(self.requirements_toml.as_deref()) {
ManagedHooksOnly::Observed(true) => Dimension::Failed {
detail: format!(
"`hooks.allow_managed_hooks_only` is set on this host and {} is a user-level \
source, so Codex drops this hook with no diagnostic output at all.",
self.hooks_path.display()
),
remedy: "Deploy the OpenLatch hook through Codex's managed channel \
(`hooks.managed_dir`, or the system `requirements.toml`), or clear \
`hooks.allow_managed_hooks_only`."
.to_string(),
},
ManagedHooksOnly::Observed(false) => Dimension::Ok,
ManagedHooksOnly::Unobservable => Dimension::Unobservable(
"Codex's system requirements layer could not be consulted here, so \
`hooks.allow_managed_hooks_only` was not observed."
.to_string(),
),
}
}
}
fn attribution(
trust: HookTrust,
hook: &ListedHook,
grant: Option<&crate::hooks::hook_trust_grants::GrantRecord>,
) -> Option<String> {
let grant = grant?;
(trust == HookTrust::Trusted && grant.trusted_hash == hook.current_hash).then(|| {
format!(
"Trust was granted by OpenLatch {} at {}, not by a person reviewing `/hooks`.",
grant.client_version, grant.granted_at
)
})
}
fn sentences(lead: &str, unknowns: &[String], caveats: &[String]) -> String {
std::iter::once(lead)
.chain(unknowns.iter().map(String::as_str))
.chain(caveats.iter().map(String::as_str))
.collect::<Vec<_>>()
.join("\n")
}
enum ProbeOutcome {
Answered,
Bad(String),
Unspawnable(String),
}
fn probe_payload() -> Value {
json!({
"hook_event_name": PROBE_EVENT,
"tool_name": DOCTOR_PROBE_TOOL_NAME,
"tool_input": {},
})
}
fn run_probe(command: &str, timeout: Duration) -> ProbeOutcome {
let mut child = match Command::new("sh")
.arg("-lc")
.arg(command)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => child,
Err(e) => return ProbeOutcome::Unspawnable(e.to_string()),
};
let drain = child.stdout.take().map(|mut out| {
std::thread::spawn(move || {
let mut buf = String::new();
let _ = out.read_to_string(&mut buf);
buf
})
});
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(probe_payload().to_string().as_bytes());
}
let deadline = Instant::now() + timeout;
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break Some(status),
Ok(None) if Instant::now() >= deadline => {
let _ = child.kill();
let _ = child.wait();
break None;
}
Ok(None) => std::thread::sleep(Duration::from_millis(10)),
Err(_) => {
let _ = child.kill();
let _ = child.wait();
break None;
}
}
};
let stdout = match status {
Some(_) => drain
.and_then(|handle| handle.join().ok())
.unwrap_or_default(),
None => String::new(),
};
match status {
None => ProbeOutcome::Bad(format!("no response within {}s", timeout.as_secs())),
Some(status) if !status.success() => ProbeOutcome::Bad(match status.code() {
Some(127) => "the command could not be resolved (exit 127)".to_string(),
Some(code) => format!("it exited {code}"),
None => "it was killed by a signal".to_string(),
}),
Some(_) if well_formed_response(&stdout) => ProbeOutcome::Answered,
Some(_) => ProbeOutcome::Bad("its response was not a JSON object".to_string()),
}
}
fn well_formed_response(stdout: &str) -> bool {
let is_object = |s: &str| {
serde_json::from_str::<Value>(s)
.map(|v| v.is_object())
.unwrap_or(false)
};
let trimmed = stdout.trim();
if is_object(trimmed) {
return true;
}
trimmed
.lines()
.rev()
.map(str::trim)
.find(|line| !line.is_empty())
.is_some_and(is_object)
}
fn binary_from_command(command: &str) -> &str {
let trimmed = command.trim_start();
if let Some(rest) = trimmed.strip_prefix('"') {
if let Some(end) = rest.find('"') {
return &rest[..end];
}
}
trimmed.split_whitespace().next().unwrap_or(trimmed)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
fn binding_at(root: &Path) -> CodexCliBinding {
CodexCliBinding {
codex_dir: root.to_path_buf(),
hooks_path: root.join("hooks.json"),
requirements_toml: Some(root.join("requirements.toml")),
hooks_list: std::sync::Arc::new(|_| None),
}
}
#[test]
fn codex_home_relocates_the_hooks_path() {
let _lock = crate::hooks::codex_cli::CONFIG_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let previous = std::env::var_os(crate::hooks::codex_cli::CONFIG_DIR_ENV);
let dir = tempfile::tempdir().expect("temp dir");
std::env::set_var(crate::hooks::codex_cli::CONFIG_DIR_ENV, dir.path());
let b = CodexCliBinding::detect().expect("relocated dir exists, so detect must succeed");
assert_eq!(b.codex_dir, dir.path());
assert_eq!(b.hook_config_path(), dir.path().join("hooks.json"));
std::env::set_var(crate::hooks::codex_cli::CONFIG_DIR_ENV, "");
if let Some(b) = CodexCliBinding::detect() {
assert_ne!(b.codex_dir, Path::new(""));
}
match previous {
Some(v) => std::env::set_var(crate::hooks::codex_cli::CONFIG_DIR_ENV, v),
None => std::env::remove_var(crate::hooks::codex_cli::CONFIG_DIR_ENV),
}
}
#[test]
fn codex_binding_event_list_is_the_twelve_with_pre_tool_use() {
let b = binding_at(Path::new("/home/test/.codex"));
let events = b.hook_event_types();
assert_eq!(events.len(), 12, "all twelve of Codex's native events");
assert!(
events.contains(&"PreToolUse"),
"PreToolUse is the registration — without it Codex is captured, never enforced: \
{events:?}"
);
assert!(events.contains(&"Interrupt"));
assert!(events.contains(&"PostCompact"));
assert!(
b.load_bearing_events().contains(&"PreToolUse"),
"a stripped PreToolUse row must read as unhealthy: {:?}",
b.load_bearing_events()
);
}
#[test]
fn codex_capabilities_cannot_express_ask() {
let b = binding_at(Path::new("/home/test/.codex"));
assert_eq!(b.capabilities().expressible, &["allow", "deny"]);
}
static PROBE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn probe_test_guard() -> std::sync::MutexGuard<'static, ()> {
PROBE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn install_probe_hook(root: &Path) -> InstalledHandler {
std::fs::write(
root.join("hooks.json"),
json!({
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"_openlatch": { "v": 1, "id": "x" },
"hooks": [{
"type": "command",
"command": "printf '{}' # openlatch-hook",
"timeout": PROBE_FALLBACK_TIMEOUT_SECS,
}],
}],
}
})
.to_string(),
)
.expect("write hooks.json");
crate::hooks::codex_cli::installed_handler(root, PROBE_EVENT)
.expect("the group we just wrote is ours")
}
fn trust_it(root: &Path, handler: &InstalledHandler, extra: &str) {
let key = crate::hooks::codex_cli::trust_key(root, PROBE_EVENT, handler);
std::fs::write(
crate::hooks::codex_cli::config_toml_path(root),
format!("{extra}[hooks.state.'{key}']\ntrusted_hash = \"codex-computed\"\n"),
)
.expect("write config.toml");
}
#[test]
fn codex_untrusted_is_armed_false_with_a_hooks_remedy() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
install_probe_hook(root);
let report = binding_at(root).liveness();
assert_eq!(
report.armed,
Some(false),
"an untrusted hook enforces nothing: {:?}",
report.detail
);
assert_eq!(report.code, Some(ERR_HOOK_NOT_ARMED));
let remedy = report.remedy.expect("a Some(false) MUST carry a remedy");
assert!(
remedy.contains("doctor --fix") && remedy.contains("/hooks"),
"the remedy is the repair that grants trust, or the manual trust step: {remedy}"
);
let detail = report.detail.expect("a Some(false) explains itself");
assert!(
detail
.lines()
.next()
.is_some_and(|lead| lead.contains("trust this hook")),
"the FIRST failing dimension is trust, not the binary: {detail}"
);
}
#[test]
fn codex_trusted_is_armed_true() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "");
let report = binding_at(root).liveness();
assert_eq!(
report.armed,
Some(true),
"all three dimensions hold: {:?}",
report.detail
);
assert_eq!(report.code, None, "a proven-armed report carries no code");
assert_eq!(report.remedy, None, "and nothing to remedy");
}
#[test]
fn managed_only_suppression_is_armed_false() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "");
let requirements = root.join("requirements.toml");
std::fs::write(&requirements, "[hooks]\nallow_managed_hooks_only = true\n")
.expect("write requirements.toml");
let report = CodexCliBinding {
requirements_toml: Some(requirements),
..binding_at(root)
}
.liveness();
assert_eq!(
report.armed,
Some(false),
"a managed-only host drops a user-level hook silently: {:?}",
report.detail
);
assert_eq!(report.code, Some(ERR_HOOK_NOT_ARMED));
let remedy = report.remedy.expect("a Some(false) MUST carry a remedy");
assert!(
remedy.contains("managed_dir") && remedy.contains("requirements.toml"),
"the remedy is managed delivery, not `/hooks`: {remedy}"
);
}
#[test]
fn unobservable_suppression_is_not_reported_as_armed() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "");
let report = CodexCliBinding {
requirements_toml: None,
..binding_at(root)
}
.liveness();
assert_ne!(
report.armed,
Some(true),
"a dimension that was never read cannot report armed: {:?}",
report.detail
);
let detail = report.detail.expect("an unread dimension must say so");
assert!(
detail.contains("allow_managed_hooks_only"),
"the detail must name what could not be observed: {detail}"
);
}
#[test]
fn absent_feature_flag_does_not_block_armed() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "");
let raw = std::fs::read_to_string(crate::hooks::codex_cli::config_toml_path(root))
.expect("read config.toml");
assert!(
!raw.contains("features"),
"the fixture is pointless unless the key really is absent: {raw}"
);
let report = binding_at(root).liveness();
assert_eq!(
report.armed,
Some(true),
"an absent feature flag is `no suppression observed`, never `unproven`: {:?}",
report.detail
);
let detail = report
.detail
.expect("the caveats ride on the check we push");
assert!(
detail.contains("undocumented"),
"the undocumented default is recorded in the text, not in the state: {detail}"
);
assert!(
detail.contains("--profile"),
"and so is what a per-invocation profile could override: {detail}"
);
}
#[test]
fn observed_feature_flag_false_is_armed_false() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "[features]\nhooks = false\n\n");
let report = binding_at(root).liveness();
assert_eq!(
report.armed,
Some(false),
"`features.hooks = false` switches Codex hooks off: {:?}",
report.detail
);
assert_eq!(report.code, Some(ERR_HOOK_NOT_ARMED));
let remedy = report.remedy.expect("a Some(false) MUST carry a remedy");
assert!(
remedy.contains("features.hooks"),
"the remedy names the key that switched them off: {remedy}"
);
}
#[test]
fn synthetic_probe_uses_a_non_evaluated_tool_name() {
assert_eq!(DOCTOR_PROBE_TOOL_NAME, "OpenlatchDoctorProbe");
assert!(
!crate::core::policy::SHELL_TOOL_NAMES.contains(&DOCTOR_PROBE_TOOL_NAME),
"a probe using an evaluated tool name could match a live policy rule: {:?}",
crate::core::policy::SHELL_TOOL_NAMES
);
let payload = probe_payload();
assert_eq!(payload["tool_name"], DOCTOR_PROBE_TOOL_NAME);
assert_eq!(
payload["tool_input"],
json!({}),
"an inert payload carries no command for anything to act on"
);
assert!(
crate::daemon::handlers::is_doctor_probe(Some(&payload)),
"the daemon must skip this envelope before the audit log"
);
}
fn listed(
root: &Path,
handler: &InstalledHandler,
trust_status: &str,
enabled: bool,
) -> ListedHook {
serde_json::from_value(json!({
"key": crate::hooks::codex_cli::trust_key(root, PROBE_EVENT, handler),
"eventName": "preToolUse",
"command": handler.command,
"sourcePath": root.join("hooks.json"),
"enabled": enabled,
"isManaged": false,
"currentHash": "sha256:current",
"trustStatus": trust_status,
}))
.expect("a listed hook")
}
fn binding_listing(root: &Path, listing: Vec<ListedHook>) -> CodexCliBinding {
CodexCliBinding {
hooks_list: std::sync::Arc::new(move |_| Some(listing.clone())),
..binding_at(root)
}
}
#[test]
fn codex_modified_is_armed_false_with_its_own_remedy() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "");
let report =
binding_listing(root, vec![listed(root, &handler, "modified", true)]).liveness();
assert_eq!(report.armed, Some(false), "{:?}", report.detail);
assert!(!report.off, "a changed command is not an off switch");
assert_eq!(report.code, Some(ERR_HOOK_NOT_ARMED));
assert_eq!(report.remedy.as_deref(), Some(MODIFIED_REMEDY));
let detail = report.detail.expect("explains itself");
assert!(
detail.contains("command changed"),
"`modified` must not read as never trusted: {detail}"
);
}
#[test]
fn codex_listed_untrusted_is_armed_false() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "");
let report =
binding_listing(root, vec![listed(root, &handler, "untrusted", true)]).liveness();
assert_eq!(report.armed, Some(false), "{:?}", report.detail);
assert_eq!(report.remedy.as_deref(), Some(TRUST_REMEDY));
}
#[test]
fn codex_trusted_but_disabled_is_off() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
let report =
binding_listing(root, vec![listed(root, &handler, "trusted", false)]).liveness();
assert_eq!(report.armed, Some(false), "{:?}", report.detail);
assert!(report.off, "an off switch renders State::Off");
assert_eq!(report.code, Some(ERR_HOOK_NOT_ARMED));
assert_eq!(report.remedy.as_deref(), Some(DISABLED_REMEDY));
}
#[test]
fn a_failure_outranks_an_off_switch() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "[features]\nhooks = false\n\n");
let report =
binding_listing(root, vec![listed(root, &handler, "trusted", false)]).liveness();
assert_eq!(report.armed, Some(false));
assert!(
!report.off,
"the suppression is a failure, not an off switch"
);
assert!(report
.remedy
.is_some_and(|remedy| remedy.contains("features.hooks")));
}
#[test]
fn codex_managed_is_armed_true() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
let mut hook = listed(root, &handler, "managed", true);
hook.is_managed = true;
let report = binding_listing(root, vec![hook]).liveness();
assert_eq!(report.armed, Some(true), "{:?}", report.detail);
}
#[test]
fn a_listing_without_our_hook_falls_back_to_the_file() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
let handler = install_probe_hook(root);
trust_it(root, &handler, "");
let report = binding_listing(root, Vec::new()).liveness();
assert_eq!(report.armed, Some(true), "{:?}", report.detail);
}
#[test]
fn nothing_readable_is_unobservable() {
let _probe_guard = probe_test_guard();
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path();
install_probe_hook(root);
std::fs::write(
crate::hooks::codex_cli::config_toml_path(root),
"[hooks.state\n",
)
.expect("write config.toml");
let report = binding_at(root).liveness();
assert_eq!(report.armed, None, "{:?}", report.detail);
let detail = report.detail.expect("says what it could not see");
assert!(detail.contains("could not be asked"), "{detail}");
}
#[test]
fn trust_openlatch_granted_is_attributed() {
let root = Path::new("/home/test/.codex");
let handler = InstalledHandler {
command: "\"/ol/openlatch-hook\"".into(),
timeout_secs: Some(900),
group_index: 1,
handler_index: 0,
};
let hook = listed(root, &handler, "trusted", true);
let grant = crate::hooks::hook_trust_grants::GrantRecord {
trusted_hash: hook.current_hash.clone(),
client_version: "0.9.9".into(),
granted_at: "2026-09-22T18:40:54Z".into(),
};
let line = attribution(HookTrust::Trusted, &hook, Some(&grant)).expect("ours");
assert!(
line.contains("granted by OpenLatch 0.9.9 at 2026-09-22T18:40:54Z"),
"{line}"
);
let regranted = crate::hooks::hook_trust_grants::GrantRecord {
trusted_hash: "sha256:reviewed-by-hand".into(),
..grant.clone()
};
assert_eq!(
attribution(HookTrust::Trusted, &hook, Some(®ranted)),
None
);
assert_eq!(attribution(HookTrust::Modified, &hook, Some(&grant)), None);
assert_eq!(attribution(HookTrust::Trusted, &hook, None), None);
}
#[test]
fn drift_asks_codex_only_when_the_files_show_a_gap() {
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 = tempfile::tempdir().expect("temp dir");
let root = dir.path().to_path_buf();
let handler = install_probe_hook(&root);
let hook = listed(&root, &handler, "untrusted", true);
let asked = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let counter = asked.clone();
let binding = CodexCliBinding {
hooks_list: Arc::new(move |_| {
counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Some(vec![hook.clone()])
}),
..binding_at(&root)
};
let first = binding
.trust_own_hooks(TrustOccasion::Drift)
.expect("drift");
let second = binding
.trust_own_hooks(TrustOccasion::Drift)
.expect("drift");
match prev {
Some(v) => std::env::set_var("OPENLATCH_DIR", v),
None => std::env::remove_var("OPENLATCH_DIR"),
}
assert_eq!(first, 1, "the gap is granted");
assert_eq!(second, 0);
assert_eq!(
asked.load(std::sync::atomic::Ordering::SeqCst),
1,
"a pass with nothing to grant must not start Codex"
);
let config = std::fs::read_to_string(crate::hooks::codex_cli::config_toml_path(&root))
.expect("config.toml");
assert!(
config.contains("trusted_hash = \"sha256:current\""),
"{config}"
);
assert!(
!config.contains("enabled"),
"the drift path never switches a hook on: {config}"
);
}
#[test]
fn codex_cli_binding_is_arc_dyn_compatible() {
let b = binding_at(Path::new("/tmp/.codex"));
let _arc: Arc<dyn AgentBinding> = Arc::new(b);
}
}