use crate::error::{KernelError, Result};
use crate::trace::Trace;
use std::collections::BTreeMap;
pub const AGENT_LOCATOR_PREFIX: &str = "agent-session:";
struct Host {
name: &'static str,
exact: &'static [&'static str],
prefixes: &'static [&'static str],
session_vars: &'static [&'static str],
}
const HOSTS: &[Host] = &[
Host {
name: "claude-code",
exact: &["CLAUDECODE"],
prefixes: &["CLAUDE_CODE_"],
session_vars: &["CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_REMOTE_SESSION_ID"],
},
Host {
name: "codex",
exact: &[],
prefixes: &["CODEX_"],
session_vars: &["CODEX_SESSION_ID", "CODEX_THREAD_ID"],
},
Host {
name: "cursor",
exact: &[],
prefixes: &["CURSOR_"],
session_vars: &["CURSOR_SESSION_ID", "CURSOR_TRACE_ID"],
},
Host {
name: "gemini-cli",
exact: &["GEMINI_CLI"],
prefixes: &[],
session_vars: &[],
},
Host {
name: "ai-agent",
exact: &["AI_AGENT"],
prefixes: &[],
session_vars: &[],
},
];
pub const USER_CONFIGURATION: &[&str] = &[
"CODEX_HOME",
"CLAUDE_CODE_USE_BEDROCK",
"CLAUDE_CODE_USE_VERTEX",
"CLAUDE_CODE_USE_FOUNDRY",
"CLAUDE_CODE_SKIP_BEDROCK_AUTH",
"CLAUDE_CODE_SKIP_VERTEX_AUTH",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
"CLAUDE_CODE_ENABLE_TELEMETRY",
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
"CLAUDE_CODE_OAUTH_TOKEN",
"CLAUDE_CODE_CLIENT_CERT",
"CLAUDE_CODE_CLIENT_KEY",
"CLAUDE_CODE_CLIENT_KEY_PASSPHRASE",
"CLAUDE_CODE_GIT_BASH_PATH",
];
const FALSE_POSITIVE_HELP: &str = "\"Agent sessions\" in docs/MEMORY.md";
const LISTED_MARKERS: usize = 5;
const MAX_SESSION_ID: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentSession {
pub host: &'static str,
pub marker: String,
pub markers: Vec<String>,
pub session_id: Option<String>,
}
impl AgentSession {
pub fn locator(&self) -> String {
format!(
"{AGENT_LOCATOR_PREFIX}{}:{}",
self.host,
self.session_id.as_deref().unwrap_or("unknown")
)
}
pub fn stamp(&self, trace: &mut Trace) -> Result<()> {
if trace.provenance.verified {
return Err(KernelError::Invalid(format!(
"--verified is refused inside an AI agent session ({}). `verified` means a person checked the claim, and an agent cannot attest its own memory; `--source` does not change that. Record it without --verified (it is stored with locator {}). To verify it, a person runs `hikmah remember --verified --supersedes <trace id> ...` from their own terminal, outside the agent session. {}",
self.detected(),
self.locator(),
self.false_positive_note()
)));
}
trace.provenance.locator = Some(match trace.provenance.locator.take() {
Some(given) if !given.trim().is_empty() => format!("{}; {given}", self.locator()),
_ => self.locator(),
});
Ok(())
}
pub fn refuse_person_only(&self, command: &str, why: &str) -> KernelError {
KernelError::Invalid(format!(
"`{command}` is refused inside an AI agent session ({}): {why}. An agent must stop here and ask a person. The person inspects what `hikmah verify-ledger` lists and runs `{command}` from their own terminal, outside the agent session. {}",
self.detected(),
self.false_positive_note()
))
}
fn detected(&self) -> String {
format!("{} detected through {}", self.host, self.marker)
}
fn false_positive_note(&self) -> String {
let mut listed: Vec<&str> = self
.markers
.iter()
.take(LISTED_MARKERS)
.map(String::as_str)
.collect();
let more;
if self.markers.len() > LISTED_MARKERS {
more = format!("and {} more", self.markers.len() - LISTED_MARKERS);
listed.push(&more);
}
format!(
"Agent variables set: {}. If you are a person and your own shell profile or IDE terminal sets them, see {FALSE_POSITIVE_HELP}",
listed.join(", ")
)
}
}
pub fn is_agent_marker(name: &str) -> bool {
host_of(name).is_some()
}
fn host_of(name: &str) -> Option<&'static Host> {
if USER_CONFIGURATION.contains(&name) {
return None;
}
HOSTS.iter().find(|host| {
host.exact.contains(&name) || host.prefixes.iter().any(|p| name.starts_with(p))
})
}
pub fn detect<I, K, V>(vars: I) -> Option<AgentSession>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let vars: BTreeMap<String, String> = vars
.into_iter()
.filter(|(_, value)| !value.as_ref().trim().is_empty())
.map(|(name, value)| (name.as_ref().to_string(), value.as_ref().to_string()))
.collect();
let markers: Vec<String> = vars
.keys()
.filter(|name| is_agent_marker(name))
.cloned()
.collect();
HOSTS.iter().find_map(|host| {
let marker = vars
.keys()
.find(|name| host_of(name).is_some_and(|h| h.name == host.name))?;
let session_id = host
.session_vars
.iter()
.filter_map(|var| vars.get(*var))
.find_map(|value| sanitize_id(value));
Some(AgentSession {
host: host.name,
marker: marker.clone(),
markers: markers.clone(),
session_id,
})
})
}
pub fn detect_from_env() -> Option<AgentSession> {
detect(std::env::vars_os().filter_map(|(name, value)| {
Some((
name.into_string().ok()?,
value.to_string_lossy().into_owned(),
))
}))
}
fn sanitize_id(value: &str) -> Option<String> {
let id: String = value
.chars()
.filter(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
.take(MAX_SESSION_ID)
.collect();
(!id.is_empty()).then_some(id)
}