refactor: Agent 配置硬编码化、压缩系统不可变重构、前端组件化与安全硬化

- AgentConfig: 移除 10+ 个环境变量读取,仅保留 TOKEN_SOFT/HARD_LIMIT 两个
    可调参数,context_char_limit 替换为统一的 token_soft_limit 阈值
  - compact: find_safe_cut_point 重写为 HashSet O(n) 算法,
    micro_compact 改为不可变风格,compress_context 签名升级为
    token_soft_limit + max_messages 双参数,新增 COMPACTION_OUTPUT_RESERVE
  - modes: ModeConfig.max_steps/tool_timeout_secs 去 Optional 化,
    Deep Research 步数 16→100,Literature Reader 步数 6→25
  - dashboard: 提取 AgentMarkdown/ThoughtCard/ToolCallCard/AnswerCard/
    SubAgentContainer 等共享组件,ResearchAgentPanel 大幅瘦身,
    交互卡片重构为 console-panel 紧凑风格
  - security: 移除 HERMES_YOLO_MODE、AGENT_BLOCK_NETWORK 开关、
    AGENT_CHECKPOINT_ENABLED 开关,关键安全机制强制启用
This commit is contained in:
fmq
2026-06-25 00:49:45 +08:00
parent 85b6429c30
commit b11b8ad015
37 changed files with 1981 additions and 1475 deletions
+2 -9
View File
@@ -151,15 +151,8 @@ impl CheckpointManager {
CheckpointManager {
enabled: enabled && inner.repo.is_some(),
repo_path: store_path,
max_snapshots: std::env::var("AGENT_CHECKPOINT_MAX_SNAPSHOTS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(10),
max_file_size: std::env::var("AGENT_CHECKPOINT_MAX_FILE_SIZE_MB")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.map(|mb| mb * 1024 * 1024)
.unwrap_or(10 * 1024 * 1024), // 10 MB
max_snapshots: 10,
max_file_size: 10 * 1024 * 1024, // 10 MB
inner: Mutex::new(inner),
}
}
-15
View File
@@ -285,24 +285,9 @@ impl HardlineResult {
///
/// 执行反规避标准化后再匹配,返回第一个命中的模式。
///
/// 注意:此函数在模块导入时冻结 `HERMES_YOLO_MODE`
/// 确保运行时无法通过设置环境变量绕过 hardline 检查。
pub fn check_command(raw_command: &str) -> HardlineResult {
let normalized = normalize_command(raw_command);
// ── YOLO 模式冻结 ──
// YOLO 模式在首次调用时从环境变量读取并缓存,
// 后续设置环境变量不会生效(防止注入攻击)。
static YOLO_MODE_FROZEN: LazyLock<bool> = LazyLock::new(|| {
let val = std::env::var("HERMES_YOLO_MODE")
.unwrap_or_default()
.to_lowercase();
val == "1" || val == "true" || val == "yes" || val == "on"
});
// Hardline 即使在 YOLO 模式下也不可绕过
let _yolo = *YOLO_MODE_FROZEN;
let matches: Vec<usize> = HARDLINE_REGEX_SET
.matches(&normalized)
.into_iter()
+21 -79
View File
@@ -66,9 +66,7 @@ pub struct AgentConfig {
pub tool_timeout_secs: u64,
/// 工具输出最大字符数
pub max_tool_output_chars: usize,
/// 上下文 Token 估算上限(触发自动摘要压缩)
pub context_char_limit: usize,
/// Token 预算软限制(触发 nudging 提醒)
/// Token 预算软限制 — 各压缩层统一触发阈值
pub token_soft_limit: usize,
/// Token 预算硬限制(触发强制动作)
pub token_hard_limit: usize,
@@ -99,80 +97,32 @@ pub struct AgentConfig {
impl AgentConfig {
/// 从环境变量加载配置,缺失时使用默认值。
pub fn from_env_optional() -> Self {
let mut config = AgentConfig {
max_steps: std::env::var("AGENT_MAX_STEPS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(8),
AgentConfig {
max_steps: 8,
duplicate_call_threshold: 3,
tool_timeout_secs: std::env::var("AGENT_TOOL_TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(120),
max_tool_output_chars: std::env::var("AGENT_MAX_TOOL_OUTPUT_CHARS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(4000),
context_char_limit: std::env::var("AGENT_CONTEXT_CHAR_LIMIT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(16000),
tool_timeout_secs: 120,
max_tool_output_chars: 4000,
token_soft_limit: std::env::var("AGENT_TOKEN_SOFT_LIMIT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(32000),
.unwrap_or(80000),
token_hard_limit: std::env::var("AGENT_TOKEN_HARD_LIMIT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(40000),
max_messages: std::env::var("AGENT_MAX_MESSAGES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(50),
.unwrap_or(100000),
max_messages: 50,
enable_thinking: false,
permission_deny_rules: parse_comma_list("AGENT_PERMISSIONS_DENY"),
permission_allow_rules: parse_comma_list("AGENT_PERMISSIONS_ALLOW"),
permission_ask_rules: parse_comma_list("AGENT_PERMISSIONS_ASK"),
permission_mode: std::env::var("AGENT_PERMISSION_MODE")
.unwrap_or_else(|_| "default".to_string()),
denial_max_consecutive: std::env::var("AGENT_DENIAL_MAX_CONSECUTIVE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(3),
denial_max_total: std::env::var("AGENT_DENIAL_MAX_TOTAL")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(20),
denial_max_consecutive: 3,
denial_max_total: 20,
additional_allowed_dirs: parse_comma_list("AGENT_ADDITIONAL_DIRS"),
subagent_allowed_tools: parse_comma_list("AGENT_SUBAGENT_ALLOWED_TOOLS"),
mode: std::env::var("AGENT_MODE").unwrap_or_else(|_| "default".to_string()),
};
// 加载权限档案(AGENT_PERMISSION_PROFILE),追加到现有规则
let profile_name = std::env::var("AGENT_PERMISSION_PROFILE").unwrap_or_default();
if !profile_name.is_empty() {
if let Some(profile) = permission_profile::load_profile(&profile_name) {
info!(
"[AgentConfig] 加载权限档案: {} — {}",
profile.name, profile.description
);
permission_profile::apply_profile_to_config(
&profile,
&mut config.permission_deny_rules,
&mut config.permission_allow_rules,
&mut config.permission_ask_rules,
&mut config.permission_mode,
);
} else {
warn!(
"[AgentConfig] 未知的权限档案: {}(可用: {:?})",
profile_name,
permission_profile::list_available_profiles()
);
}
mode: "default".to_string(),
}
config
}
}
@@ -380,10 +330,7 @@ impl AgentRuntime {
apply_mode_tool_filter(&mut tool_registry, mode);
// 初始化 checkpoint 管理器
let checkpoint_enabled = std::env::var("AGENT_CHECKPOINT_ENABLED")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
!= "false";
let checkpoint_enabled = true;
let checkpoint_store = app_state.config.library_dir.join("..").join(".checkpoints");
let checkpoint_manager = Arc::new(checkpoint::CheckpointManager::new(
std::fs::canonicalize(&checkpoint_store).unwrap_or(checkpoint_store),
@@ -455,10 +402,7 @@ impl AgentRuntime {
apply_mode_tool_filter(&mut tool_registry, mode);
// 初始化 checkpoint 管理器
let checkpoint_enabled = std::env::var("AGENT_CHECKPOINT_ENABLED")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
!= "false";
let checkpoint_enabled = true;
let checkpoint_store = app_state.config.library_dir.join("..").join(".checkpoints");
let checkpoint_manager = Arc::new(checkpoint::CheckpointManager::new(
std::fs::canonicalize(&checkpoint_store).unwrap_or(checkpoint_store),
@@ -561,7 +505,8 @@ impl AgentRuntime {
compact::compress_context_with_hooks_and_log(
messages,
llm,
self.config.context_char_limit,
self.config.token_soft_limit,
self.config.max_messages,
session_id,
Some(hook_registry),
Some(&self.collapse_log),
@@ -799,7 +744,7 @@ impl AgentRuntime {
None => compact::rough_estimate_tokens(messages),
};
// 使用 token 预算的软限制作为压缩触发点(而非粗糙的 context_char_limit * 1.5
// 使用 TokenBudget 软限制作为压缩触发点
let token_limit = token_budget.soft_limit;
let mut did_compress = false;
@@ -1418,14 +1363,15 @@ impl AgentRuntime {
error_recovery::RecoveryStep::AggressiveCompact => {
info!("[AgentRuntime] 错误恢复: 激进压缩 (snip + micro with keep_recent=2)");
compact::snip_compact(messages, self.config.max_messages);
compact::micro_compact(messages, 2);
*messages = compact::micro_compact(messages, 2);
}
error_recovery::RecoveryStep::ReactiveCompact => {
info!("[AgentRuntime] 错误恢复: LLM 摘要压缩");
compact::compress_context(
messages,
llm,
self.config.context_char_limit,
self.config.token_soft_limit,
self.config.max_messages,
session_id,
)
.await;
@@ -1846,15 +1792,11 @@ impl AgentRuntime {
///
/// 仅覆盖 mode_config 中 Some 的字段,None 保持原值不变。
fn apply_mode_config(config: &mut AgentConfig, mode: &AgentMode) {
if let Some(max_steps) = mode.mode_config.max_steps {
config.max_steps = max_steps;
}
config.max_steps = mode.mode_config.max_steps;
if let Some(enable_thinking) = mode.mode_config.enable_thinking {
config.enable_thinking = enable_thinking;
}
if let Some(tool_timeout_secs) = mode.mode_config.tool_timeout_secs {
config.tool_timeout_secs = tool_timeout_secs;
}
config.tool_timeout_secs = mode.mode_config.tool_timeout_secs;
// 加载权限档案(模式绑定的 permission_profile
if let Some(profile_name) = mode.mode_config.permission_profile {