use std::sync::OnceLock;
use lean_ctx_protocol::hive::metrics_v2::{
FilterName, KNOWN_PROGRAMS, OTHER, ReadMode, RuleId, UnregulatedName,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignalKind {
Truncated,
Refetched,
Blocked,
WouldBlock,
UnregulatedProgram,
UnregulatedHost,
UnregulatedMcpServer,
}
#[derive(Clone, Copy)]
pub struct SignalRecorder {
pub recording: fn() -> bool,
pub record: fn(SignalKind, &[&str]),
pub secrets: fn(&[&str]),
}
static RECORDER: OnceLock<SignalRecorder> = OnceLock::new();
pub fn register_signal_recorder(recorder: SignalRecorder) {
let _ = RECORDER.set(recorder);
}
pub fn recording() -> bool {
RECORDER.get().is_some_and(|r| (r.recording)())
}
pub fn record(kind: SignalKind, names: &[&str]) {
if let Some(recorder) = RECORDER.get() {
(recorder.record)(kind, names);
}
}
pub fn note_secrets(labels: &[&str]) {
if labels.is_empty() {
return;
}
if let Some(recorder) = RECORDER.get() {
(recorder.secrets)(labels);
}
}
pub fn read_mode_filter(mode: &str) -> FilterName {
let mode = ReadMode::of(mode);
let wire = serde_json::to_value(mode)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_else(|| OTHER.to_string());
FilterName::or_other(&wire)
}
pub fn output_filter(tool: &str) -> FilterName {
FilterName::or_other(match tool {
"ctx_shell" | "ctx_execute" => "shell_output",
"ctx_search" => "search_output",
_ => OTHER,
})
}
pub fn program_name(base: &str) -> UnregulatedName {
let base = base.rsplit(['/', '\\']).next().unwrap_or(base);
let base = base.strip_suffix(".exe").unwrap_or(base);
UnregulatedName::new(base)
.filter(|_| KNOWN_PROGRAMS.contains(&base))
.unwrap_or_else(UnregulatedName::other)
}
pub fn path_jail_rule() -> RuleId {
RuleId::or_other("gateway.path_jail")
}
pub fn secrets_rule() -> RuleId {
RuleId::or_other("security.block_secrets")
}
pub fn shell_rule() -> RuleId {
RuleId::or_other("gateway.security_violation")
}
pub fn admission_rules<'a>(reason_codes: impl IntoIterator<Item = &'a str>) -> Vec<RuleId> {
let mut rules: Vec<RuleId> = Vec::new();
for code in reason_codes {
let rule = match code {
"secret.blocked" => "security.block_secrets",
"pii.blocked" => "security.pii",
"classification.blocked" => "security.classification",
"destination.constraint_satisfied" | "destination.remote_allowed_once" => continue,
c if c.starts_with("destination.") => "gateway.destination",
c if c.starts_with("egress.") => "gateway.egress",
_ => continue,
};
let rule = RuleId::or_other(rule);
if !rules.contains(&rule) {
rules.push(rule);
}
}
rules
}
pub fn truncated(filter: &FilterName) {
record(SignalKind::Truncated, &[filter.as_str()]);
}
pub fn refetched(filter: &FilterName) {
record(SignalKind::Refetched, &[filter.as_str()]);
}
pub fn blocked(rule: &RuleId) {
record(SignalKind::Blocked, &[rule.as_str()]);
}
pub fn would_block(rule: &RuleId) {
record(SignalKind::WouldBlock, &[rule.as_str()]);
}
pub fn refused(rule: &RuleId, enforcing: bool) {
if enforcing {
blocked(rule);
} else {
would_block(rule);
}
}
pub fn unregulated_programs(programs: &[UnregulatedName]) {
let names: Vec<&str> = programs.iter().map(UnregulatedName::as_str).collect();
if !names.is_empty() {
record(SignalKind::UnregulatedProgram, &names);
}
}
pub fn admission_refused<'a>(reason_codes: impl IntoIterator<Item = &'a str>) {
if !recording() {
return;
}
for rule in admission_rules(reason_codes) {
blocked(&rule);
}
}