use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub const WINDOW: Duration = Duration::from_secs(24 * 60 * 60);
pub const GRACE: Duration = Duration::from_secs(10);
pub const MAX_IDS: usize = 4096;
pub const USER_AGENT_TOKEN: &str = "opencode/";
pub fn is_opencode_user_agent(user_agent: &str) -> bool {
user_agent
.trim_start()
.get(..USER_AGENT_TOKEN.len())
.is_some_and(|p| p.eq_ignore_ascii_case(USER_AGENT_TOKEN))
}
#[derive(Clone, Copy)]
struct RelaySeen {
first: Instant,
last: Instant,
}
#[derive(Default)]
struct Inner {
relay: HashMap<String, RelaySeen>,
plugin: HashMap<String, Instant>,
reported: HashSet<String>,
last_traffic: Option<Instant>,
last_traffic_ms: Option<i64>,
}
pub struct OpencodeSessionWindow {
inner: Mutex<Inner>,
started: Instant,
}
impl Default for OpencodeSessionWindow {
fn default() -> Self {
Self::new(Instant::now())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WindowSnapshot {
pub traffic: bool,
pub last_traffic_ms: Option<i64>,
pub suspected: usize,
pub since_daemon_start: bool,
}
impl WindowSnapshot {
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"traffic": self.traffic,
"last_traffic_ms": self.last_traffic_ms,
"bypass_suspected": self.suspected > 0,
"suspected_sessions": self.suspected,
"since_daemon_start": self.since_daemon_start,
})
}
}
fn lock(m: &Mutex<Inner>) -> std::sync::MutexGuard<'_, Inner> {
m.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn bounded_upsert<V: Copy>(
map: &mut HashMap<String, V>,
id: &str,
new: V,
update: impl FnOnce(&mut V),
age: impl Fn(&V) -> Instant,
) {
if let Some(seen) = map.get_mut(id) {
update(seen);
return;
}
if map.len() >= MAX_IDS {
if let Some(oldest) = map
.iter()
.min_by_key(|(_, v)| age(v))
.map(|(k, _)| k.clone())
{
map.remove(&oldest);
}
}
map.insert(id.to_string(), new);
}
impl OpencodeSessionWindow {
pub fn new(started: Instant) -> Self {
Self {
inner: Mutex::new(Inner::default()),
started,
}
}
pub fn note_relay(&self, session_id: &str, now: Instant) {
let mut inner = lock(&self.inner);
inner.last_traffic = Some(now);
inner.last_traffic_ms = Some(chrono::Utc::now().timestamp_millis());
let id = session_id.trim();
if !id.is_empty() {
bounded_upsert(
&mut inner.relay,
id,
RelaySeen {
first: now,
last: now,
},
|seen| seen.last = now,
|seen| seen.last,
);
}
}
pub fn note_plugin(&self, session_id: &str, now: Instant) {
let id = session_id.trim();
if id.is_empty() {
return;
}
bounded_upsert(
&mut lock(&self.inner).plugin,
id,
now,
|seen| *seen = now,
|seen| *seen,
);
}
fn prune(inner: &mut Inner, now: Instant) {
let live = |t: &Instant| now.saturating_duration_since(*t) <= WINDOW;
inner.relay.retain(|_, seen| live(&seen.last));
inner.plugin.retain(|_, t| live(t));
let relay = &inner.relay;
inner.reported.retain(|id| relay.contains_key(id));
if inner.last_traffic.is_some_and(|t| !live(&t)) {
inner.last_traffic = None;
inner.last_traffic_ms = None;
}
}
fn suspected_in(inner: &Inner, now: Instant) -> Vec<String> {
let mut out: Vec<String> = inner
.relay
.iter()
.filter(|(id, seen)| {
now.saturating_duration_since(seen.first) >= GRACE
&& !inner.plugin.contains_key(*id)
})
.map(|(id, _)| id.clone())
.collect();
out.sort();
out
}
pub fn take_new_suspects(&self, now: Instant) -> Vec<String> {
let mut inner = lock(&self.inner);
Self::prune(&mut inner, now);
let fresh: Vec<String> = Self::suspected_in(&inner, now)
.into_iter()
.filter(|id| !inner.reported.contains(id))
.collect();
inner.reported.extend(fresh.iter().cloned());
fresh
}
pub fn requeue(&self, sessions: &[String]) {
let mut inner = lock(&self.inner);
for id in sessions {
inner.reported.remove(id);
}
}
pub fn snapshot(&self, now: Instant) -> WindowSnapshot {
let mut inner = lock(&self.inner);
Self::prune(&mut inner, now);
WindowSnapshot {
traffic: inner.last_traffic.is_some(),
last_traffic_ms: inner.last_traffic_ms,
suspected: Self::suspected_in(&inner, now).len(),
since_daemon_start: now.saturating_duration_since(self.started) < WINDOW,
}
}
}
pub fn bypass_event(session_id: &str) -> crate::cloud::tamper::TamperEvent {
use crate::cloud::tamper::{TamperEvent, DETECTION_PLUGIN_BYPASS_SUSPECTED};
TamperEvent::new(
format!("opencode:session:{session_id}"),
crate::hooks::bindings::opencode::OpencodeBinding::AGENT_TYPE.to_string(),
String::new(),
"plugin_loaded".to_string(),
DETECTION_PLUGIN_BYPASS_SUSPECTED.to_string(),
)
.with_heal("not_healable", 0, "closed")
}
#[cfg(test)]
mod tests {
use super::*;
fn at(base: Instant, secs: u64) -> Instant {
base + Duration::from_secs(secs)
}
#[test]
fn the_user_agent_token_is_the_first_product_only() {
assert!(is_opencode_user_agent(
"opencode/1.18.35 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14"
));
assert!(is_opencode_user_agent("OpenCode/1.0"));
assert!(!is_opencode_user_agent("ai-sdk/4 opencode/1.18.35"));
assert!(!is_opencode_user_agent("cline/3.0"));
assert!(!is_opencode_user_agent("opencode"));
assert!(!is_opencode_user_agent(""));
}
#[test]
fn a_session_the_plugin_never_reported_is_suspected_once() {
let t0 = Instant::now();
let w = OpencodeSessionWindow::new(t0);
w.note_relay("ses_pure", t0);
assert!(
w.take_new_suspects(at(t0, 1)).is_empty(),
"inside the grace"
);
assert_eq!(w.snapshot(at(t0, 1)).suspected, 0);
assert_eq!(
w.take_new_suspects(at(t0, 11)),
vec!["ses_pure".to_string()]
);
assert!(w.take_new_suspects(at(t0, 12)).is_empty(), "reported once");
let snap = w.snapshot(at(t0, 12));
assert!(snap.traffic);
assert_eq!(snap.suspected, 1, "still suspected, only not re-reported");
assert!(snap.since_daemon_start);
}
#[test]
fn a_session_the_plugin_reported_is_never_suspected() {
let t0 = Instant::now();
let w = OpencodeSessionWindow::new(t0);
w.note_plugin("ses_a", t0);
w.note_relay("ses_a", at(t0, 1));
w.note_relay("ses_b", at(t0, 1));
w.note_plugin("ses_b", at(t0, 5));
assert!(w.take_new_suspects(at(t0, 60)).is_empty());
assert_eq!(w.snapshot(at(t0, 60)).suspected, 0);
}
#[test]
fn an_unrelated_plugin_session_does_not_hide_a_pure_run() {
let t0 = Instant::now();
let w = OpencodeSessionWindow::new(t0);
w.note_plugin("ses_live", t0);
w.note_relay("ses_pure", t0);
assert_eq!(
w.take_new_suspects(at(t0, 30)),
vec!["ses_pure".to_string()]
);
}
#[test]
fn traffic_with_a_blank_session_is_traffic_but_no_session() {
let t0 = Instant::now();
let w = OpencodeSessionWindow::new(t0);
assert!(!w.snapshot(t0).traffic);
w.note_relay(" ", t0);
let snap = w.snapshot(at(t0, 60));
assert!(snap.traffic);
assert_eq!(snap.suspected, 0);
}
#[test]
fn entries_age_out_of_the_window() {
let t0 = Instant::now();
let w = OpencodeSessionWindow::new(t0);
w.note_relay("ses_old", t0);
let later = t0 + WINDOW + Duration::from_secs(1);
let snap = w.snapshot(later);
assert!(!snap.traffic);
assert_eq!(snap.suspected, 0);
assert!(!snap.since_daemon_start);
}
#[test]
fn an_active_session_survives_the_window_and_is_reported_once() {
let t0 = Instant::now();
let w = OpencodeSessionWindow::new(t0);
w.note_relay("ses_long", t0);
assert_eq!(
w.take_new_suspects(at(t0, 11)),
vec!["ses_long".to_string()]
);
let near_end = t0 + WINDOW - Duration::from_secs(60);
w.note_relay("ses_long", near_end);
let past = t0 + WINDOW + Duration::from_secs(60);
assert!(w.take_new_suspects(past).is_empty(), "never twice");
let snap = w.snapshot(past);
assert!(snap.traffic);
assert_eq!(snap.suspected, 1, "still suspected past the 24h mark");
}
#[test]
fn a_requeued_suspect_is_handed_out_again() {
let t0 = Instant::now();
let w = OpencodeSessionWindow::new(t0);
w.note_relay("ses_pure", t0);
let got = w.take_new_suspects(at(t0, 11));
assert_eq!(got, vec!["ses_pure".to_string()]);
w.requeue(&got);
assert_eq!(w.take_new_suspects(at(t0, 16)), got);
assert!(w.take_new_suspects(at(t0, 21)).is_empty());
}
#[test]
fn each_side_is_bounded() {
let t0 = Instant::now();
let w = OpencodeSessionWindow::new(t0);
for i in 0..(MAX_IDS + 10) {
w.note_relay(&format!("ses_{i}"), at(t0, i as u64 % 3));
}
assert!(lock(&w.inner).relay.len() <= MAX_IDS);
}
#[test]
fn the_bypass_event_is_a_high_unhealable_hook_entry_fact() {
let e = bypass_event("ses_x");
assert_eq!(e.tamper.agent_type, "opencode");
assert_eq!(
e.tamper.component,
crate::cloud::tamper::COMPONENT_HOOK_ENTRY
);
assert_eq!(e.tamper.detection_method, "plugin_bypass_suspected");
assert_eq!(e.ocsf.severity_id, 4);
assert_eq!(e.tamper.heal.outcome, "not_healable");
assert_eq!(e.tamper.entry_id, "opencode:session:ses_x");
}
#[test]
fn the_snapshot_json_names_its_fields() {
let snap = WindowSnapshot {
traffic: true,
last_traffic_ms: Some(7),
suspected: 2,
since_daemon_start: true,
};
assert_eq!(
snap.to_json(),
serde_json::json!({
"traffic": true,
"last_traffic_ms": 7,
"bypass_suspected": true,
"suspected_sessions": 2,
"since_daemon_start": true,
})
);
}
}