Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/agentsight/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ fn main() {

// Generate tcpsniff skeleton (no header — reuses sslsniff.h event format)
generate_skeleton(&mut out, "tcpsniff");

// Generate schedmon skeleton and bindings
generate_skeleton(&mut out, "schedmon");
generate_header(&mut out, "schedmon");

// generate_header(&mut out, "frametypes");
// generate_header(&mut out, "errors");
Expand Down
1 change: 1 addition & 0 deletions src/agentsight/src/bpf/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ typedef enum {
EVENT_SOURCE_FILEWATCH = 4, // File watch events (filewatch)
EVENT_SOURCE_FILEWRITE = 5, // File write events (filewrite)
EVENT_SOURCE_UDPDNS = 6, // UDP DNS query events (udpdns)
EVENT_SOURCE_SCHED = 7, // Scheduler monitor events (schedmon)
} event_source_t;

// Common event header - every ringbuffer event MUST start with this
Expand Down
88 changes: 88 additions & 0 deletions src/agentsight/src/bpf/schedmon.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
// Copyright (c) 2025 AgentSight Project
//
// Scheduler monitor BPF program — detects idle/active state transitions for
// traced Agent processes via BTF-typed sched_switch / sched_wakeup tracepoints.
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "schedmon.h"
#include "common.h"

struct pid_sched_state {
u8 last_state;
};

struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, MAX_TRACED_PROCESSES);
__type(key, u32);
__type(value, struct pid_sched_state);
} pid_sched_state SEC(".maps");

static __always_inline int
emit_sched_event(u32 tgid, u32 tid, u64 now, u8 event_type)
{
struct pid_sched_state *st = bpf_map_lookup_elem(&pid_sched_state, &tid);
if (st) {
if (st->last_state == event_type)
return 0;
st->last_state = event_type;
} else {
struct pid_sched_state new_st = {
.last_state = event_type,
};
bpf_map_update_elem(&pid_sched_state, &tid, &new_st, BPF_ANY);
}

struct sched_event *ev = bpf_ringbuf_reserve(&rb, sizeof(*ev), 0);
if (!ev)
return 0;

ev->source = EVENT_SOURCE_SCHED;
ev->tgid = tgid;
ev->tid = tid;
ev->timestamp_ns = now;
ev->event_type = event_type;
ev->pad[0] = 0;
ev->pad[1] = 0;
ev->pad[2] = 0;

bpf_ringbuf_submit(ev, 0);
return 0;
}

SEC("tp_btf/sched_switch")
int BPF_PROG(handle_sched_switch, bool preempt, struct task_struct *prev,
struct task_struct *next)
{
if (preempt)
return 0;

unsigned int state = BPF_CORE_READ(prev, __state);
if (state == 0)
return 0;

u32 tgid = BPF_CORE_READ(prev, tgid);
u32 *traced = bpf_map_lookup_elem(&traced_processes, &tgid);
if (!traced)
return 0;

u32 tid = BPF_CORE_READ(prev, pid);
return emit_sched_event(tgid, tid, bpf_ktime_get_ns(), SCHED_EVENT_SLEEP);
}

SEC("tp_btf/sched_wakeup")
int BPF_PROG(handle_sched_wakeup, struct task_struct *p)
{
u32 tgid = BPF_CORE_READ(p, tgid);
u32 *traced = bpf_map_lookup_elem(&traced_processes, &tgid);
if (!traced)
return 0;

u32 tid = BPF_CORE_READ(p, pid);
return emit_sched_event(tgid, tid, bpf_ktime_get_ns(), SCHED_EVENT_WAKEUP);
}

char LICENSE[] SEC("license") = "GPL";
31 changes: 31 additions & 0 deletions src/agentsight/src/bpf/schedmon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
// Copyright (c) 2025 AgentSight Project
#ifndef __SCHEDMON_H
#define __SCHEDMON_H

#define TASK_COMM_LEN 16

typedef signed char s8;
typedef unsigned char u8;
typedef signed short s16;
typedef unsigned short u16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed long long s64;
typedef unsigned long long u64;

enum sched_event_type {
SCHED_EVENT_SLEEP = 1,
SCHED_EVENT_WAKEUP = 2,
};

struct sched_event {
u32 source; // EVENT_SOURCE_SCHED
u32 tgid;
u32 tid;
u64 timestamp_ns;
u8 event_type; // enum sched_event_type
u8 pad[3];
};

#endif /* __SCHEDMON_H */
109 changes: 83 additions & 26 deletions src/agentsight/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use anyhow::Context;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::Context;

// ==================== Default Constants ====================

Expand Down Expand Up @@ -42,7 +42,7 @@ pub const DEFAULT_PURGE_INTERVAL: u64 = 1000;
pub const HF_ENDPOINT: &str = "https://hf-mirror.com";

/// Get the HF_HOME path, expanding `~` to the user's home directory.
///
///
/// Uses `$HOME` on Unix and `$USERPROFILE` on Windows as fallback.
/// Returns `./.agentsight/tokenizers` if home directory cannot be determined.
pub fn hf_home() -> PathBuf {
Expand Down Expand Up @@ -171,7 +171,10 @@ impl FromStr for TcpTarget {

// Full wildcard shortcuts: "*", "*:*", ":*"
if s == "*" || s == "*:*" || s == ":*" {
return Ok(TcpTarget { ip: None, port: None });
return Ok(TcpTarget {
ip: None,
port: None,
});
}

// Helper: parse `"*"` as wildcard, otherwise as IPv4.
Expand Down Expand Up @@ -215,7 +218,6 @@ impl FromStr for TcpTarget {
}
}


/// Internal JSON structures for parsing the config file (same format as FFI).
#[derive(serde::Deserialize)]
struct JsonFullConfig {
Expand All @@ -241,6 +243,16 @@ struct JsonFullConfig {
cgroup_filter_enabled: Option<bool>,
#[serde(default)]
cgroup_ids: Option<Vec<u64>>,
#[serde(default)]
activity_monitor: Option<JsonActivityMonitor>,
}

#[derive(serde::Deserialize, Clone, Debug)]
struct JsonActivityMonitor {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
idle_threshold_ms: Option<u64>,
}

/// DeadLoop 检测配置区段
Expand Down Expand Up @@ -331,7 +343,9 @@ fn extract_rules(parsed: &JsonFullConfig) -> (Vec<CmdlineRule>, Vec<HttpsRule>,
for group in https_groups {
for pat in &group.rule {
if !pat.is_empty() {
https_rules.push(HttpsRule { pattern: pat.clone() });
https_rules.push(HttpsRule {
pattern: pat.clone(),
});
}
}
}
Expand All @@ -357,13 +371,14 @@ fn extract_rules(parsed: &JsonFullConfig) -> (Vec<CmdlineRule>, Vec<HttpsRule>,
/// Parse a JSON config string into cmdline rules, https rules, and http targets.
///
/// This is the shared parser for both the config file and FFI's `load_config()`.
pub fn parse_json_rules(json: &str) -> Result<(Vec<CmdlineRule>, Vec<HttpsRule>, Vec<HttpTarget>), String> {
let parsed: JsonFullConfig = serde_json::from_str(json)
.map_err(|e| format!("JSON parse error: {}", e))?;
pub fn parse_json_rules(
json: &str,
) -> Result<(Vec<CmdlineRule>, Vec<HttpsRule>, Vec<HttpTarget>), String> {
let parsed: JsonFullConfig =
serde_json::from_str(json).map_err(|e| format!("JSON parse error: {}", e))?;
Ok(extract_rules(&parsed))
}


/// Ensure the agents configuration file exists at the given path.
///
/// If the file does not exist, creates it with the embedded default configuration.
Expand All @@ -384,8 +399,8 @@ pub fn ensure_default_agents_config(path: &Path) -> anyhow::Result<()> {

/// Load default cmdline rules (embedded), without touching the filesystem.
pub fn default_cmdline_rules() -> Vec<CmdlineRule> {
let (rules, _, _) = parse_json_rules(DEFAULT_AGENTS_JSON)
.expect("embedded DEFAULT_AGENTS_JSON is valid");
let (rules, _, _) =
parse_json_rules(DEFAULT_AGENTS_JSON).expect("embedded DEFAULT_AGENTS_JSON is valid");
rules
}

Expand Down Expand Up @@ -502,6 +517,12 @@ pub struct AgentsightConfig {
pub deadloop_kill_enabled: bool,
/// 触发 kill 的循环次数阈值(检测到 N 次后 kill)
pub deadloop_kill_after_count: usize,

// --- Activity Monitor Configuration ---
/// Enable scheduler activity monitor (BPF schedmon probe)
pub enable_activity_monitor: bool,
/// Idle threshold in ms before a family transitions to Idle state
pub activity_idle_threshold_ms: u64,
}

impl Default for AgentsightConfig {
Expand Down Expand Up @@ -542,8 +563,13 @@ impl Default for AgentsightConfig {
log_path: None,

// Tokenizer defaults (read from env vars)
tokenizer_path: std::env::var("AGENTSIGHT_TOKENIZER_PATH").ok().map(PathBuf::from),
tokenizer_url: Some("https://www.modelscope.cn/models/Qwen/Qwen3.5-27B/resolve/master/tokenizer.json".to_owned()),
tokenizer_path: std::env::var("AGENTSIGHT_TOKENIZER_PATH")
.ok()
.map(PathBuf::from),
tokenizer_url: Some(
"https://www.modelscope.cn/models/Qwen/Qwen3.5-27B/resolve/master/tokenizer.json"
.to_owned(),
),

// FFI Rule defaults
cmdline_rules: Vec::new(),
Expand All @@ -562,6 +588,8 @@ impl Default for AgentsightConfig {
// DeadLoop auto-kill defaults (disabled by default)
deadloop_kill_enabled: false,
deadloop_kill_after_count: 3,
enable_activity_monitor: false,
activity_idle_threshold_ms: 50,
}
}
}
Expand Down Expand Up @@ -645,8 +673,8 @@ impl AgentsightConfig {
///
/// Parses `verbose`, `log_path`, `cmdline`, `https` and `http` fields.
pub fn load_from_json(&mut self, json: &str) -> Result<(), String> {
let mut parsed: JsonFullConfig = serde_json::from_str(json)
.map_err(|e| format!("JSON parse error: {}", e))?;
let mut parsed: JsonFullConfig =
serde_json::from_str(json).map_err(|e| format!("JSON parse error: {}", e))?;

if let Some(t) = parsed.trace_enabled {
self.trace_enabled = t;
Expand Down Expand Up @@ -711,6 +739,15 @@ impl AgentsightConfig {
self.cgroup_ids = ids;
}

if let Some(ref am) = parsed.activity_monitor {
if let Some(enabled) = am.enabled {
self.enable_activity_monitor = enabled;
}
if let Some(ms) = am.idle_threshold_ms {
self.activity_idle_threshold_ms = ms;
}
}

let (cmdline_rules, https_rules, http_targets) = extract_rules(&parsed);
self.cmdline_rules.extend(cmdline_rules);
self.https_rules.extend(https_rules);
Expand Down Expand Up @@ -770,7 +807,10 @@ impl AgentsightConfig {
/// # Panics
/// Panics if `config_path` was not set via `set_config_path` (CLI `--config`).
pub fn resolve_config_path(&self) -> PathBuf {
assert!(self.config_path.is_some(), "config_path must be set via --config");
assert!(
self.config_path.is_some(),
"config_path must be set via --config"
);
self.config_path.clone().unwrap()
}
}
Expand Down Expand Up @@ -1015,14 +1055,20 @@ mod tests {
fn test_set_tokenizer_path() {
let config = AgentsightConfig::new()
.set_tokenizer_path(Some(PathBuf::from("/path/to/tokenizer.json")));
assert_eq!(config.tokenizer_path, Some(PathBuf::from("/path/to/tokenizer.json")));
assert_eq!(
config.tokenizer_path,
Some(PathBuf::from("/path/to/tokenizer.json"))
);
}

#[test]
fn test_set_tokenizer_url() {
let config = AgentsightConfig::new()
.set_tokenizer_url(Some("https://example.com/tok.json".into()));
assert_eq!(config.tokenizer_url, Some("https://example.com/tok.json".to_string()));
let config =
AgentsightConfig::new().set_tokenizer_url(Some("https://example.com/tok.json".into()));
assert_eq!(
config.tokenizer_url,
Some("https://example.com/tok.json".to_string())
);
}

#[test]
Expand All @@ -1042,7 +1088,10 @@ mod tests {
let config = AgentsightConfig::new().add_cmdline_rule(rule);
assert_eq!(config.cmdline_rules.len(), 1);
assert_eq!(config.cmdline_rules[0].patterns, vec!["node", "*claude*"]);
assert_eq!(config.cmdline_rules[0].agent_name, Some("Claude Code".to_string()));
assert_eq!(
config.cmdline_rules[0].agent_name,
Some("Claude Code".to_string())
);
assert!(config.cmdline_rules[0].allow);
}

Expand All @@ -1061,7 +1110,9 @@ mod tests {

#[test]
fn test_add_https_rule() {
let rule = HttpsRule { pattern: "*.openai.com".to_string() };
let rule = HttpsRule {
pattern: "*.openai.com".to_string(),
};
let config = AgentsightConfig::new().add_https_rule(rule);
assert_eq!(config.https_rules.len(), 1);
assert_eq!(config.https_rules[0].pattern, "*.openai.com");
Expand All @@ -1080,8 +1131,12 @@ mod tests {
agent_name: Some("Agent2".to_string()),
allow: true,
})
.add_https_rule(HttpsRule { pattern: "*.openai.com".to_string() })
.add_https_rule(HttpsRule { pattern: "*.anthropic.com".to_string() });
.add_https_rule(HttpsRule {
pattern: "*.openai.com".to_string(),
})
.add_https_rule(HttpsRule {
pattern: "*.anthropic.com".to_string(),
});
assert_eq!(config.cmdline_rules.len(), 2);
assert_eq!(config.https_rules.len(), 2);
}
Expand All @@ -1093,7 +1148,8 @@ mod tests {
// All should be allow rules
assert!(rules.iter().all(|r| r.allow));
// Should contain Hermes, Cosh, OpenClaw agent names
let names: Vec<&str> = rules.iter()
let names: Vec<&str> = rules
.iter()
.filter_map(|r| r.agent_name.as_deref())
.collect();
assert!(names.contains(&"Hermes"));
Expand All @@ -1103,7 +1159,8 @@ mod tests {

#[test]
fn test_default_agents_json_valid() {
let (cmdline_rules, https_rules, http_targets) = parse_json_rules(DEFAULT_AGENTS_JSON).unwrap();
let (cmdline_rules, https_rules, http_targets) =
parse_json_rules(DEFAULT_AGENTS_JSON).unwrap();
assert!(!cmdline_rules.is_empty());
// https rules: dashscope.aliyuncs.com configured by default
assert_eq!(https_rules.len(), 1);
Expand Down
Loading
Loading