use std::io::Write;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
const ENV_KEY: &str = "ANTHROPIC_API_KEY";
const MAX_LOG_BYTES: u64 = 256 * 1024;
const MAX_READ_BYTES: u64 = 128 * 1024;
pub fn observe(callsite: &'static str) -> Result<String, std::env::VarError> {
let result = std::env::var(ENV_KEY);
if let Ok(v) = &result
&& !v.is_empty()
{
record_hit(callsite);
}
result
}
pub fn scrub_key(cmd: &mut std::process::Command) {
cmd.env_remove(ENV_KEY);
}
pub fn scrub_key_pty(cmd: &mut portable_pty::CommandBuilder) {
cmd.env_remove(ENV_KEY);
}
pub fn log_path() -> PathBuf {
crate::data_root::data_root().join("api-canary.jsonl")
}
fn record_hit(callsite: &'static str) {
let path = log_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
rotate_if_needed(&path);
let line = format!(
"{{\"ts\":\"{}\",\"callsite\":\"{}\",\"pid\":{},\"thread\":\"{}\",\"seq\":{}}}\n",
rfc3339_now(),
callsite,
std::process::id(),
thread_name(),
next_seq(),
);
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
let _ = f.write_all(line.as_bytes());
}
}
fn rotate_if_needed(path: &std::path::Path) {
let Ok(meta) = std::fs::metadata(path) else {
return;
};
if meta.len() < MAX_LOG_BYTES {
return;
}
let mut old = path.to_path_buf();
old.set_extension("jsonl.old");
let _ = std::fs::rename(path, &old);
}
pub fn tail_log() -> String {
use std::io::{Read, Seek, SeekFrom};
let path = log_path();
let Ok(mut f) = std::fs::File::open(&path) else {
return format!(
"# api-canary — no hits recorded yet.\n\
# Every read of $ANTHROPIC_API_KEY appends one line here.\n\
# Empty file means no code in mnml is fetching the metered API.\n\
# Log path: {}\n",
path.display()
);
};
let len = f.metadata().map(|m| m.len()).unwrap_or(0);
let (mut buf, truncated) = if len <= MAX_READ_BYTES {
(Vec::with_capacity(len as usize), false)
} else {
let _ = f.seek(SeekFrom::End(-(MAX_READ_BYTES as i64)));
(Vec::with_capacity(MAX_READ_BYTES as usize), true)
};
if f.read_to_end(&mut buf).is_err() {
return format!("# api-canary — read failed: {}\n", path.display());
}
let mut s = String::from_utf8_lossy(&buf).into_owned();
if truncated {
if let Some(first_nl) = s.find('\n') {
s = s[first_nl + 1..].to_string();
}
s = format!(
"# api-canary — file exceeded {} KB, showing tail.\n{}",
MAX_READ_BYTES / 1024,
s
);
}
if s.trim().is_empty() {
format!(
"# api-canary — empty log at {}\n\
# (This is the healthy state.)\n",
path.display()
)
} else {
s
}
}
fn rfc3339_now() -> String {
let now = std::time::SystemTime::now();
let secs = now
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let (year, month, day, hh, mm, ss) = civil_from_unix(secs);
format!("{year:04}-{month:02}-{day:02}T{hh:02}:{mm:02}:{ss:02}Z")
}
fn thread_name() -> String {
std::thread::current()
.name()
.unwrap_or("<unnamed>")
.to_string()
}
static SEQ: AtomicU64 = AtomicU64::new(0);
fn next_seq() -> u64 {
SEQ.fetch_add(1, Ordering::Relaxed)
}
fn civil_from_unix(secs: u64) -> (u32, u32, u32, u32, u32, u32) {
let days = (secs / 86_400) as i64;
let rem = (secs % 86_400) as u32;
let hh = rem / 3600;
let mm = (rem % 3600) / 60;
let ss = rem % 60;
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let year = (y + if m <= 2 { 1 } else { 0 }) as u32;
(year, m, d, hh, mm, ss)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scrub_key_removes_rather_than_blanks_the_var() {
let mut cmd = std::process::Command::new("true");
scrub_key(&mut cmd);
let entry = cmd
.get_envs()
.find(|(k, _)| *k == std::ffi::OsStr::new(ENV_KEY));
assert_eq!(
entry,
Some((std::ffi::OsStr::new(ENV_KEY), None)),
"scrub_key must env_remove the key, not set it empty"
);
}
#[test]
fn scrub_key_pty_drops_a_present_key() {
let mut cmd = portable_pty::CommandBuilder::new("true");
cmd.env(ENV_KEY, "sk-ant-sentinel");
assert!(cmd.get_env(ENV_KEY).is_some(), "precondition: key present");
scrub_key_pty(&mut cmd);
assert!(
cmd.get_env(ENV_KEY).is_none(),
"pty child would still inherit the key"
);
}
#[test]
fn scrubbed_child_process_cannot_see_an_inherited_key() {
let Ok(inherited) = std::env::var(ENV_KEY) else {
return;
};
if inherited.is_empty() {
return;
}
let mut cmd = std::process::Command::new("sh");
cmd.args(["-c", "printf '%s' \"$ANTHROPIC_API_KEY\""]);
scrub_key(&mut cmd);
let out = cmd.output().expect("spawn sh");
assert!(
out.stdout.is_empty(),
"child inherited the key despite scrub_key"
);
}
#[test]
fn civil_from_unix_matches_known_dates() {
let (y, mo, d, hh, mm, ss) = civil_from_unix(1_787_505_123);
assert_eq!((y, mo, d, hh, mm, ss), (2026, 8, 23, 17, 12, 3));
}
#[test]
fn tail_log_returns_empty_message_when_file_missing() {
let tmp = std::env::temp_dir().join(format!(
"mnml-canary-test-{}-{}",
std::process::id(),
next_seq(),
));
std::fs::create_dir_all(&tmp).unwrap();
let body = tail_log();
assert!(
body.starts_with("# api-canary"),
"tail_log must always return a display-safe message; got: {body:?}"
);
let _ = std::fs::remove_dir_all(&tmp);
}
}