From e74df5f288d20c3e2c266eec7b420a846f35abf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Mon, 25 May 2026 02:26:50 +0800 Subject: [PATCH] feat(hermes): add approval safety settings --- scripts/dev-api.js | 67 +++++++ src-tauri/src/commands/hermes.rs | 264 ++++++++++++++++++++++++++ src-tauri/src/lib.rs | 2 + src/engines/hermes/pages/config.js | 153 +++++++++++++-- src/lib/tauri-api.js | 2 + src/locales/modules/engine.js | 18 ++ tests/hermes-approvals-config.test.js | 82 ++++++++ tests/hermes-config-page-ui.test.js | 16 +- 8 files changed, 583 insertions(+), 21 deletions(-) create mode 100644 tests/hermes-approvals-config.test.js diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 4cd10c7..0ede237 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3325,6 +3325,8 @@ const HERMES_STREAMING_TRANSPORTS = new Set(['auto', 'draft', 'edit', 'off']) const HERMES_CODE_EXECUTION_MODES = new Set(['project', 'strict']) const HERMES_TERMINAL_BACKENDS = new Set(['local', 'ssh', 'docker', 'singularity', 'modal', 'daytona', 'vercel_sandbox']) const HERMES_BROWSER_ENGINES = new Set(['auto', 'lightpanda', 'chrome']) +const HERMES_APPROVAL_MODES = new Set(['manual', 'smart', 'off']) +const HERMES_APPROVAL_CRON_MODES = new Set(['deny', 'approve']) const HERMES_AGENT_IMAGE_INPUT_MODES = new Set(['auto', 'native', 'text']) const HERMES_DISPLAY_TOOL_PROGRESS_VALUES = new Set(['off', 'new', 'all', 'verbose']) const HERMES_DISPLAY_STREAMING_VALUES = new Set(['inherit', 'true', 'false']) @@ -3410,6 +3412,20 @@ function normalizeHermesBrowserEngine(value, strict = false) { return 'auto' } +function normalizeHermesApprovalMode(value, strict = false) { + const mode = String(value ?? '').trim().toLowerCase() || 'manual' + if (HERMES_APPROVAL_MODES.has(mode)) return mode + if (strict) throw new Error('approvals.mode 必须是 manual、smart 或 off') + return 'manual' +} + +function normalizeHermesApprovalCronMode(value, strict = false) { + const mode = String(value ?? '').trim().toLowerCase() || 'deny' + if (HERMES_APPROVAL_CRON_MODES.has(mode)) return mode + if (strict) throw new Error('approvals.cron_mode 必须是 deny 或 approve') + return 'deny' +} + function normalizeHermesImageInputMode(value, strict = false) { const mode = String(value ?? '').trim().toLowerCase() || 'auto' if (HERMES_AGENT_IMAGE_INPUT_MODES.has(mode)) return mode @@ -4050,6 +4066,36 @@ export function mergeHermesCheckpointsConfig(config = {}, form = {}) { return next } +export function buildHermesApprovalsConfigValues(config = {}) { + const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} + const approvals = root.approvals && typeof root.approvals === 'object' && !Array.isArray(root.approvals) + ? root.approvals + : {} + return { + approvalMode: normalizeHermesApprovalMode(approvals.mode, false), + approvalTimeout: parseHermesInteger(approvals.timeout, 'approvals.timeout', 60, 1, 86400, false), + approvalCronMode: normalizeHermesApprovalCronMode(approvals.cron_mode, false), + approvalMcpReloadConfirm: readHermesBool(approvals.mcp_reload_confirm, true), + approvalDestructiveSlashConfirm: readHermesBool(approvals.destructive_slash_confirm, true), + } +} + +export function mergeHermesApprovalsConfig(config = {}, form = {}) { + const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {}) + const currentValues = buildHermesApprovalsConfigValues(next) + const approvals = next.approvals && typeof next.approvals === 'object' && !Array.isArray(next.approvals) + ? mergeConfigsPreservingFields(next.approvals, {}) + : {} + + approvals.mode = normalizeHermesApprovalMode(Object.hasOwn(form, 'approvalMode') ? form.approvalMode : currentValues.approvalMode, true) + approvals.timeout = parseHermesInteger(Object.hasOwn(form, 'approvalTimeout') ? form.approvalTimeout : currentValues.approvalTimeout, 'approvals.timeout', 60, 1, 86400, true) + approvals.cron_mode = normalizeHermesApprovalCronMode(Object.hasOwn(form, 'approvalCronMode') ? form.approvalCronMode : currentValues.approvalCronMode, true) + approvals.mcp_reload_confirm = formHermesBool(form, 'approvalMcpReloadConfirm', currentValues.approvalMcpReloadConfirm) + approvals.destructive_slash_confirm = formHermesBool(form, 'approvalDestructiveSlashConfirm', currentValues.approvalDestructiveSlashConfirm) + next.approvals = approvals + return next +} + export function buildHermesPrivacyConfigValues(config = {}) { const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} const privacy = root.privacy && typeof root.privacy === 'object' && !Array.isArray(root.privacy) @@ -10763,6 +10809,27 @@ const handlers = { } }, + hermes_approvals_config_read() { + const { configPath, exists, config } = readHermesConfigYamlObject() + return { + exists, + configPath, + values: buildHermesApprovalsConfigValues(config), + } + }, + + hermes_approvals_config_save({ form } = {}) { + const { configPath, config } = readHermesConfigYamlObject() + const next = mergeHermesApprovalsConfig(config, form || {}) + const backup = writeHermesConfigYamlObject(configPath, next) + return { + ok: true, + configPath, + backup, + values: buildHermesApprovalsConfigValues(next), + } + }, + hermes_privacy_config_read() { const { configPath, exists, config } = readHermesConfigYamlObject() return { diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 9474fcc..35fdba7 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -4631,6 +4631,43 @@ fn normalize_hermes_browser_engine(value: Option, strict: bool) -> Resul } } +fn normalize_hermes_approval_mode(value: Option, strict: bool) -> Result { + let mode = value.unwrap_or_default().trim().to_ascii_lowercase(); + let mode = if mode.is_empty() { + "manual".to_string() + } else { + mode + }; + if matches!(mode.as_str(), "manual" | "smart" | "off") { + return Ok(mode); + } + if strict { + Err("approvals.mode 必须是 manual、smart 或 off".to_string()) + } else { + Ok("manual".to_string()) + } +} + +fn normalize_hermes_approval_cron_mode( + value: Option, + strict: bool, +) -> Result { + let mode = value.unwrap_or_default().trim().to_ascii_lowercase(); + let mode = if mode.is_empty() { + "deny".to_string() + } else { + mode + }; + if matches!(mode.as_str(), "deny" | "approve") { + return Ok(mode); + } + if strict { + Err("approvals.cron_mode 必须是 deny 或 approve".to_string()) + } else { + Ok("deny".to_string()) + } +} + fn hermes_streaming_config_source(config: &serde_yaml::Value) -> Option<&serde_yaml::Mapping> { let root = config.as_mapping()?; if let Some(streaming) = yaml_get_mapping(root, "streaming") { @@ -5051,6 +5088,107 @@ fn merge_hermes_checkpoints_config( Ok(()) } +fn build_hermes_approvals_config_values(config: &serde_yaml::Value) -> Value { + let root = config.as_mapping(); + let approvals = root.and_then(|map| yaml_get_mapping(map, "approvals")); + let approval_mode = normalize_hermes_approval_mode( + approvals.and_then(|map| yaml_string_field(map, "mode")), + false, + ) + .unwrap_or_else(|_| "manual".to_string()); + let approval_timeout = approvals + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "timeout"), 60, 1, 86400)) + .unwrap_or(60); + let approval_cron_mode = normalize_hermes_approval_cron_mode( + approvals.and_then(|map| yaml_string_field(map, "cron_mode")), + false, + ) + .unwrap_or_else(|_| "deny".to_string()); + let approval_mcp_reload_confirm = approvals + .and_then(|map| yaml_bool_field(map, "mcp_reload_confirm")) + .unwrap_or(true); + let approval_destructive_slash_confirm = approvals + .and_then(|map| yaml_bool_field(map, "destructive_slash_confirm")) + .unwrap_or(true); + + serde_json::json!({ + "approvalMode": approval_mode, + "approvalTimeout": approval_timeout, + "approvalCronMode": approval_cron_mode, + "approvalMcpReloadConfirm": approval_mcp_reload_confirm, + "approvalDestructiveSlashConfirm": approval_destructive_slash_confirm, + }) +} + +fn merge_hermes_approvals_config( + config: &mut serde_yaml::Value, + form: &Value, +) -> Result<(), String> { + let current = build_hermes_approvals_config_values(config); + let approval_mode = normalize_hermes_approval_mode( + if form.get("approvalMode").is_some() { + form_string(form, "approvalMode") + } else { + current["approvalMode"].as_str().map(ToString::to_string) + }, + true, + )?; + let approval_timeout = validate_hermes_i64( + if form.get("approvalTimeout").is_some() { + form_i64(form, "approvalTimeout") + } else { + Some(current["approvalTimeout"].as_i64().unwrap_or(60)) + }, + "approvals.timeout", + 60, + 1, + 86400, + )?; + let approval_cron_mode = normalize_hermes_approval_cron_mode( + if form.get("approvalCronMode").is_some() { + form_string(form, "approvalCronMode") + } else { + current["approvalCronMode"] + .as_str() + .map(ToString::to_string) + }, + true, + )?; + let approval_mcp_reload_confirm = + form_bool(form, "approvalMcpReloadConfirm").unwrap_or_else(|| { + current["approvalMcpReloadConfirm"] + .as_bool() + .unwrap_or(true) + }); + let approval_destructive_slash_confirm = form_bool(form, "approvalDestructiveSlashConfirm") + .unwrap_or_else(|| { + current["approvalDestructiveSlashConfirm"] + .as_bool() + .unwrap_or(true) + }); + + let root = ensure_yaml_object(config)?; + let approvals = yaml_child_object(root, "approvals")?; + approvals.insert(yaml_key("mode"), serde_yaml::Value::String(approval_mode)); + approvals.insert( + yaml_key("timeout"), + serde_yaml::Value::Number(approval_timeout.into()), + ); + approvals.insert( + yaml_key("cron_mode"), + serde_yaml::Value::String(approval_cron_mode), + ); + approvals.insert( + yaml_key("mcp_reload_confirm"), + serde_yaml::Value::Bool(approval_mcp_reload_confirm), + ); + approvals.insert( + yaml_key("destructive_slash_confirm"), + serde_yaml::Value::Bool(approval_destructive_slash_confirm), + ); + Ok(()) +} + fn build_hermes_privacy_config_values(config: &serde_yaml::Value) -> Value { let root = config.as_mapping(); let privacy = root.and_then(|map| yaml_get_mapping(map, "privacy")); @@ -6675,6 +6813,30 @@ pub fn hermes_checkpoints_config_save(form: Value) -> Result { })) } +#[tauri::command] +pub fn hermes_approvals_config_read() -> Result { + let (config_path, exists, config) = read_hermes_channel_yaml_config()?; + ensure_yaml_object(&mut config.clone())?; + Ok(serde_json::json!({ + "exists": exists, + "configPath": config_path.to_string_lossy(), + "values": build_hermes_approvals_config_values(&config), + })) +} + +#[tauri::command] +pub fn hermes_approvals_config_save(form: Value) -> Result { + let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?; + merge_hermes_approvals_config(&mut config, &form)?; + let backup = write_hermes_yaml_config(&config_path, &config)?; + Ok(serde_json::json!({ + "ok": true, + "configPath": config_path.to_string_lossy(), + "backup": backup, + "values": build_hermes_approvals_config_values(&config), + })) +} + #[tauri::command] pub fn hermes_privacy_config_read() -> Result { let (config_path, exists, config) = read_hermes_channel_yaml_config()?; @@ -12622,6 +12784,108 @@ streaming: } } +#[cfg(test)] +mod hermes_approvals_config_tests { + use super::{build_hermes_approvals_config_values, merge_hermes_approvals_config}; + use serde_json::json; + + #[test] + fn approvals_values_have_upstream_defaults() { + let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap(); + let values = build_hermes_approvals_config_values(&config); + assert_eq!(values["approvalMode"], "manual"); + assert_eq!(values["approvalTimeout"], 60); + assert_eq!(values["approvalCronMode"], "deny"); + assert_eq!(values["approvalMcpReloadConfirm"], true); + assert_eq!(values["approvalDestructiveSlashConfirm"], true); + } + + #[test] + fn approvals_values_read_yaml_fields() { + let config: serde_yaml::Value = serde_yaml::from_str( + r#" +approvals: + mode: smart + timeout: 120 + cron_mode: approve + mcp_reload_confirm: false + destructive_slash_confirm: false +"#, + ) + .unwrap(); + let values = build_hermes_approvals_config_values(&config); + assert_eq!(values["approvalMode"], "smart"); + assert_eq!(values["approvalTimeout"], 120); + assert_eq!(values["approvalCronMode"], "approve"); + assert_eq!(values["approvalMcpReloadConfirm"], false); + assert_eq!(values["approvalDestructiveSlashConfirm"], false); + } + + #[test] + fn merge_approvals_config_preserves_unknown_fields() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +model: + provider: anthropic +approvals: + mode: manual + custom_flag: keep-approvals +streaming: + enabled: true +"#, + ) + .unwrap(); + + merge_hermes_approvals_config( + &mut config, + &json!({ + "approvalMode": "off", + "approvalTimeout": "15", + "approvalCronMode": "approve", + "approvalMcpReloadConfirm": false, + "approvalDestructiveSlashConfirm": false, + }), + ) + .unwrap(); + + assert_eq!(config["model"]["provider"].as_str(), Some("anthropic")); + assert_eq!(config["streaming"]["enabled"].as_bool(), Some(true)); + assert_eq!(config["approvals"]["mode"].as_str(), Some("off")); + assert_eq!(config["approvals"]["timeout"].as_i64(), Some(15)); + assert_eq!(config["approvals"]["cron_mode"].as_str(), Some("approve")); + assert_eq!( + config["approvals"]["mcp_reload_confirm"].as_bool(), + Some(false) + ); + assert_eq!( + config["approvals"]["destructive_slash_confirm"].as_bool(), + Some(false) + ); + assert_eq!( + config["approvals"]["custom_flag"].as_str(), + Some("keep-approvals") + ); + } + + #[test] + fn merge_approvals_config_rejects_invalid_values() { + let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new()); + let err = merge_hermes_approvals_config(&mut config, &json!({ "approvalMode": "always" })) + .unwrap_err(); + assert!(err.contains("approvals.mode")); + let err = + merge_hermes_approvals_config(&mut config, &json!({ "approvalCronMode": "prompt" })) + .unwrap_err(); + assert!(err.contains("approvals.cron_mode")); + let err = merge_hermes_approvals_config(&mut config, &json!({ "approvalTimeout": 0 })) + .unwrap_err(); + assert!(err.contains("approvals.timeout")); + let err = merge_hermes_approvals_config(&mut config, &json!({ "approvalTimeout": 86401 })) + .unwrap_err(); + assert!(err.contains("approvals.timeout")); + } +} + #[cfg(test)] mod hermes_terminal_config_tests { use super::{build_hermes_terminal_config_values, merge_hermes_terminal_config}; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5288faf..79aa518 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -289,6 +289,8 @@ pub fn run() { hermes::hermes_io_safety_config_save, hermes::hermes_checkpoints_config_read, hermes::hermes_checkpoints_config_save, + hermes::hermes_approvals_config_read, + hermes::hermes_approvals_config_save, hermes::hermes_privacy_config_read, hermes::hermes_privacy_config_save, hermes::hermes_browser_config_read, diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index 5a6f1f3..a6d9026 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -136,6 +136,14 @@ const CHECKPOINTS_DEFAULTS = { checkpointMinIntervalHours: 24, } +const APPROVALS_DEFAULTS = { + approvalMode: 'manual', + approvalTimeout: 60, + approvalCronMode: 'deny', + approvalMcpReloadConfirm: true, + approvalDestructiveSlashConfirm: true, +} + const PRIVACY_DEFAULTS = { redactPii: false, } @@ -171,6 +179,8 @@ const DISPLAY_TOOL_PROGRESS_VALUES = ['off', 'new', 'all', 'verbose'] const DISPLAY_LANGUAGE_VALUES = ['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu'] const DISPLAY_RESUME_VALUES = ['full', 'minimal'] const HUMAN_DELAY_MODES = ['off', 'natural', 'custom'] +const APPROVAL_MODES = ['manual', 'smart', 'off'] +const APPROVAL_CRON_MODES = ['deny', 'approve'] export function render() { const el = document.createElement('div') @@ -193,6 +203,7 @@ export function render() { let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS } let ioSafetyValues = { ...IO_SAFETY_DEFAULTS } let checkpointsValues = { ...CHECKPOINTS_DEFAULTS } + let approvalsValues = { ...APPROVALS_DEFAULTS } let privacyValues = { ...PRIVACY_DEFAULTS } let browserValues = { ...BROWSER_DEFAULTS } let terminalValues = { ...TERMINAL_DEFAULTS } @@ -213,6 +224,7 @@ export function render() { let executionLimitsLoading = true let ioSafetyLoading = true let checkpointsLoading = true + let approvalsLoading = true let privacyLoading = true let browserLoading = true let terminalLoading = true @@ -233,6 +245,7 @@ export function render() { let executionLimitsSaving = false let ioSafetySaving = false let checkpointsSaving = false + let approvalsSaving = false let privacySaving = false let browserSaving = false let terminalSaving = false @@ -253,6 +266,7 @@ export function render() { let executionLimitsError = null let ioSafetyError = null let checkpointsError = null + let approvalsError = null let privacyError = null let browserError = null let terminalError = null @@ -266,7 +280,7 @@ export function render() { } function isBusy() { - return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || privacySaving || browserSaving || terminalSaving + return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || approvalsLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving } function option(labelKey, value, selected) { @@ -283,7 +297,7 @@ export function render() { } function renderRuntimePanel() { - const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -331,7 +345,7 @@ export function render() { } function renderCompressionPanel() { - const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -381,7 +395,7 @@ export function render() { } function renderToolGuardrailsPanel() { - const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -443,7 +457,7 @@ export function render() { } function renderMemoryPanel() { - const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -493,7 +507,7 @@ export function render() { } function renderSkillsConfigPanel() { - const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -525,7 +539,7 @@ export function render() { } function renderQuickCommandsConfigPanel() { - const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -551,7 +565,7 @@ export function render() { } function renderAgentToolsetsConfigPanel() { - const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -577,7 +591,7 @@ export function render() { } function renderAgentRuntimeConfigPanel() { - const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || privacySaving || browserSaving || terminalSaving + const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || approvalsSaving || privacySaving || browserSaving || terminalSaving return `
@@ -639,7 +653,7 @@ export function render() { } function renderUnauthorizedDmConfigPanel() { - const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -669,7 +683,7 @@ export function render() { } function renderSecurityConfigPanel() { - const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -711,7 +725,7 @@ export function render() { } function renderDisplayConfigPanel() { - const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -775,7 +789,7 @@ export function render() { } function renderHumanDelayConfigPanel() { - const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -813,7 +827,7 @@ export function render() { } function renderStreamingPanel() { - const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || terminalSaving + const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || approvalsSaving || terminalSaving return `
@@ -865,7 +879,7 @@ export function render() { } function renderExecutionLimitsPanel() { - const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving + const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving || approvalsSaving return `
@@ -937,7 +951,7 @@ export function render() { } function renderIoSafetyPanel() { - const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || approvalsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -977,7 +991,7 @@ export function render() { } function renderCheckpointsPanel() { - const disabled = loading || saving || checkpointsLoading || checkpointsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || checkpointsLoading || checkpointsSaving || ioSafetySaving || approvalsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -1034,8 +1048,58 @@ export function render() { ` } + function renderApprovalsPanel() { + const disabled = loading || saving || approvalsLoading || approvalsSaving || checkpointsSaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving + return ` +
+
+
+
${t('engine.hermesApprovalsConfigTitle')}
+
${t('engine.hermesApprovalsConfigDesc')}
+
+
+ ${approvalsSaving ? t('engine.hermesConfigStatusSaving') : approvalsLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesApprovalsConfigStatusReady')} + +
+
+
+ ${renderError(approvalsError)} +
+ + + +
+
+ + +
+
${t('engine.hermesApprovalsConfigFootnote')}
+
+
+ ` + } + function renderPrivacyPanel() { - const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving + const disabled = loading || saving || privacyLoading || privacySaving || approvalsSaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -1063,7 +1127,7 @@ export function render() { } function renderBrowserPanel() { - const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving + const disabled = loading || saving || browserLoading || browserSaving || approvalsSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -1107,7 +1171,7 @@ export function render() { } function renderTerminalPanel() { - const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving + const disabled = loading || saving || terminalLoading || terminalSaving || approvalsSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving return `
@@ -1197,6 +1261,7 @@ export function render() { ${renderExecutionLimitsPanel()} ${renderIoSafetyPanel()} ${renderCheckpointsPanel()} + ${renderApprovalsPanel()} ${renderPrivacyPanel()} ${renderBrowserPanel()} ${renderCompressionPanel()} @@ -1245,6 +1310,7 @@ export function render() { el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits) el.querySelector('#hm-io-safety-save')?.addEventListener('click', saveIoSafety) el.querySelector('#hm-checkpoints-save')?.addEventListener('click', saveCheckpoints) + el.querySelector('#hm-approvals-save')?.addEventListener('click', saveApprovalsConfig) el.querySelector('#hm-privacy-save')?.addEventListener('click', savePrivacyConfig) el.querySelector('#hm-browser-save')?.addEventListener('click', saveBrowserConfig) el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal) @@ -1335,6 +1401,11 @@ export function render() { checkpointsValues = { ...CHECKPOINTS_DEFAULTS, ...(data?.values || {}) } } + async function loadApprovalsConfig() { + const data = await api.hermesApprovalsConfigRead() + approvalsValues = { ...APPROVALS_DEFAULTS, ...(data?.values || {}) } + } + async function loadPrivacyConfig() { const data = await api.hermesPrivacyConfigRead() privacyValues = { ...PRIVACY_DEFAULTS, ...(data?.values || {}) } @@ -1368,6 +1439,7 @@ export function render() { executionLimitsLoading = true ioSafetyLoading = true checkpointsLoading = true + approvalsLoading = true privacyLoading = true browserLoading = true terminalLoading = true @@ -1388,6 +1460,7 @@ export function render() { executionLimitsError = null ioSafetyError = null checkpointsError = null + approvalsError = null privacyError = null browserError = null terminalError = null @@ -1455,6 +1528,14 @@ export function render() { checkpointsLoading = false draw() } + try { + await loadApprovalsConfig() + } catch (err) { + approvalsError = humanizeError(err, t('engine.hermesApprovalsConfigLoadFailed') || 'Load approvals config failed') + } finally { + approvalsLoading = false + draw() + } try { await loadPrivacyConfig() } catch (err) { @@ -1620,6 +1701,9 @@ export function render() { try { await loadCheckpoints() } catch {} + try { + await loadApprovalsConfig() + } catch {} try { await loadPrivacyConfig() } catch {} @@ -2104,6 +2188,35 @@ export function render() { } } + async function saveApprovalsConfig() { + const form = { + approvalMode: el.querySelector('#hm-approval-mode')?.value || 'manual', + approvalTimeout: el.querySelector('#hm-approval-timeout')?.value || '60', + approvalCronMode: el.querySelector('#hm-approval-cron-mode')?.value || 'deny', + approvalMcpReloadConfirm: !!el.querySelector('#hm-approval-mcp-reload-confirm')?.checked, + approvalDestructiveSlashConfirm: !!el.querySelector('#hm-approval-destructive-slash-confirm')?.checked, + } + approvalsSaving = true + approvalsError = null + draw() + try { + const result = await api.hermesApprovalsConfigSave(form) + approvalsValues = { ...APPROVALS_DEFAULTS, ...(result?.values || form) } + await refreshRawAfterStructuredSave() + const backup = result?.backup || '' + toast({ + message: t('engine.hermesApprovalsConfigSaveSuccess'), + hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '', + }, 'success') + } catch (err) { + approvalsError = humanizeError(err, t('engine.hermesApprovalsConfigSaveFailed') || 'Save approvals config failed') + toast(approvalsError, 'error') + } finally { + approvalsSaving = false + draw() + } + } + async function savePrivacyConfig() { const form = { redactPii: !!el.querySelector('#hm-privacy-redact-pii')?.checked, diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index ae063ad..0ec7026 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -541,6 +541,8 @@ export const api = { hermesIoSafetyConfigSave: (form) => invoke('hermes_io_safety_config_save', { form }), hermesCheckpointsConfigRead: () => invoke('hermes_checkpoints_config_read'), hermesCheckpointsConfigSave: (form) => invoke('hermes_checkpoints_config_save', { form }), + hermesApprovalsConfigRead: () => invoke('hermes_approvals_config_read'), + hermesApprovalsConfigSave: (form) => invoke('hermes_approvals_config_save', { form }), hermesPrivacyConfigRead: () => invoke('hermes_privacy_config_read'), hermesPrivacyConfigSave: (form) => invoke('hermes_privacy_config_save', { form }), hermesBrowserConfigRead: () => invoke('hermes_browser_config_read'), diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 57b1fe4..5fcab39 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -592,6 +592,24 @@ export default { hermesCheckpointsConfigRetentionDays: _('保留天数', 'Retention days', '保留天數'), hermesCheckpointsConfigMinIntervalHours: _('自动清理最小间隔小时', 'Minimum auto-prune interval hours', '自動清理最小間隔小時'), hermesCheckpointsConfigFootnote: _('这里写入 checkpoints.*。容量上限或单文件上限设为 0 表示关闭对应限制;其他 checkpoints 高级字段会保留在 raw YAML 中。', 'This writes checkpoints.*. Set total size cap or single-file cap to 0 to disable that limit. Other advanced checkpoint fields stay in raw YAML.', '這裡寫入 checkpoints.*。容量上限或單檔上限設為 0 表示關閉對應限制;其他 checkpoints 進階欄位會保留在 raw YAML 中。'), + hermesApprovalsConfigTitle: _('审批安全', 'Approval safety', '審批安全'), + hermesApprovalsConfigDesc: _('控制危险命令、Cron 任务和破坏性 slash 命令的审批策略,避免无人值守长跑任务误放行高风险操作。', 'Control approval policy for dangerous commands, cron jobs, and destructive slash commands so unattended long runs do not approve risky operations by mistake.', '控制危險命令、Cron 任務和破壞性 slash 命令的審批策略,避免無人值守長跑任務誤放行高風險操作。'), + hermesApprovalsConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), + hermesApprovalsConfigSave: _('保存审批配置', 'Save approval settings', '儲存審批設定'), + hermesApprovalsConfigSaveSuccess: _('审批安全配置已保存,建议重启 Hermes Gateway 生效', 'Approval safety settings saved. Restart Hermes Gateway to take effect.', '審批安全設定已儲存,建議重啟 Hermes Gateway 生效'), + hermesApprovalsConfigLoadFailed: _('加载审批安全配置失败', 'Load approval safety settings failed', '載入審批安全設定失敗'), + hermesApprovalsConfigSaveFailed: _('保存审批安全配置失败', 'Save approval safety settings failed', '儲存審批安全設定失敗'), + hermesApprovalsConfigMode: _('交互审批模式', 'Interactive approval mode', '互動審批模式'), + hermesApprovalsConfigMode_manual: _('手动确认', 'Manual confirmation', '手動確認'), + hermesApprovalsConfigMode_smart: _('智能判断', 'Smart decision', '智慧判斷'), + hermesApprovalsConfigMode_off: _('关闭审批', 'Disable approvals', '關閉審批'), + hermesApprovalsConfigTimeout: _('等待审批超时秒数', 'Approval timeout seconds', '等待審批逾時秒數'), + hermesApprovalsConfigCronMode: _('Cron 任务默认策略', 'Default cron-job policy', 'Cron 任務預設策略'), + hermesApprovalsConfigCronMode_deny: _('默认拒绝', 'Deny by default', '預設拒絕'), + hermesApprovalsConfigCronMode_approve: _('默认批准', 'Approve by default', '預設批准'), + hermesApprovalsConfigMcpReloadConfirm: _('重载 MCP 工具前要求确认', 'Confirm before reloading MCP tools', '重載 MCP 工具前要求確認'), + hermesApprovalsConfigDestructiveSlashConfirm: _('破坏性 slash 命令要求确认', 'Confirm destructive slash commands', '破壞性 slash 命令要求確認'), + hermesApprovalsConfigFootnote: _('推荐保持交互审批为手动或智能,并让 Cron 默认拒绝危险命令;只有在完全可信的无人值守环境中才使用默认批准。其他 approvals 高级字段会保留在 raw YAML 中。', 'Manual or smart interactive approval is recommended, with cron jobs denying dangerous commands by default. Use default approval only in fully trusted unattended environments. Other advanced approvals fields stay in raw YAML.', '建議保持互動審批為手動或智慧,並讓 Cron 預設拒絕危險命令;只有在完全可信的無人值守環境中才使用預設批准。其他 approvals 進階欄位會保留在 raw YAML 中。'), hermesPrivacyConfigTitle: _('隐私脱敏', 'Privacy redaction', '隱私脫敏'), hermesPrivacyConfigDesc: _('对支持的 Gateway 渠道在送入模型前脱敏用户 ID、手机号和会话标识,降低公网渠道泄露风险。', 'Redact user IDs, phone numbers, and chat identifiers before supported Gateway channels send context to the model, reducing public-channel exposure risk.', '對支援的 Gateway 渠道在送入模型前脫敏使用者 ID、電話號碼和會話識別,降低公開渠道外洩風險。'), hermesPrivacyConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), diff --git a/tests/hermes-approvals-config.test.js b/tests/hermes-approvals-config.test.js new file mode 100644 index 0000000..781a199 --- /dev/null +++ b/tests/hermes-approvals-config.test.js @@ -0,0 +1,82 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + buildHermesApprovalsConfigValues, + mergeHermesApprovalsConfig, +} from '../scripts/dev-api.js' + +test('Hermes 审批安全配置读取会提供上游默认值', () => { + const values = buildHermesApprovalsConfigValues({}) + + assert.deepEqual(values, { + approvalMode: 'manual', + approvalTimeout: 60, + approvalCronMode: 'deny', + approvalMcpReloadConfirm: true, + approvalDestructiveSlashConfirm: true, + }) +}) + +test('Hermes 审批安全配置读取会回显 YAML 字段', () => { + const values = buildHermesApprovalsConfigValues({ + approvals: { + mode: 'smart', + timeout: 120, + cron_mode: 'approve', + mcp_reload_confirm: false, + destructive_slash_confirm: false, + }, + }) + + assert.equal(values.approvalMode, 'smart') + assert.equal(values.approvalTimeout, 120) + assert.equal(values.approvalCronMode, 'approve') + assert.equal(values.approvalMcpReloadConfirm, false) + assert.equal(values.approvalDestructiveSlashConfirm, false) +}) + +test('Hermes 审批安全配置保存会保留未知字段并写入 approvals', () => { + const next = mergeHermesApprovalsConfig({ + model: { provider: 'anthropic' }, + approvals: { + mode: 'manual', + custom_flag: 'keep-approvals', + }, + streaming: { enabled: true }, + }, { + approvalMode: 'off', + approvalTimeout: '15', + approvalCronMode: 'approve', + approvalMcpReloadConfirm: false, + approvalDestructiveSlashConfirm: false, + }) + + assert.deepEqual(next.model, { provider: 'anthropic' }) + assert.deepEqual(next.streaming, { enabled: true }) + assert.equal(next.approvals.mode, 'off') + assert.equal(next.approvals.timeout, 15) + assert.equal(next.approvals.cron_mode, 'approve') + assert.equal(next.approvals.mcp_reload_confirm, false) + assert.equal(next.approvals.destructive_slash_confirm, false) + assert.equal(next.approvals.custom_flag, 'keep-approvals') +}) + +test('Hermes 审批安全配置保存会拒绝非法枚举和越界值', () => { + assert.throws( + () => mergeHermesApprovalsConfig({}, { approvalMode: 'always' }), + /approvals\.mode/, + ) + assert.throws( + () => mergeHermesApprovalsConfig({}, { approvalCronMode: 'prompt' }), + /approvals\.cron_mode/, + ) + assert.throws( + () => mergeHermesApprovalsConfig({}, { approvalTimeout: '0' }), + /approvals\.timeout/, + ) + assert.throws( + () => mergeHermesApprovalsConfig({}, { approvalTimeout: '86401' }), + /approvals\.timeout/, + ) +}) diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index 1d7c29f..f1b246c 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -192,6 +192,19 @@ test('Hermes 配置页会暴露文件快照回滚结构化配置字段', () => { } }) +test('Hermes 配置页会暴露审批安全结构化配置字段', () => { + for (const id of [ + 'hm-approvals-save', + 'hm-approval-mode', + 'hm-approval-timeout', + 'hm-approval-cron-mode', + 'hm-approval-mcp-reload-confirm', + 'hm-approval-destructive-slash-confirm', + ]) { + assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) + } +}) + test('Hermes 配置页会暴露隐私脱敏结构化配置字段', () => { for (const id of [ 'hm-privacy-save', @@ -252,7 +265,8 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => { key.includes('PrivacyConfig') || key.includes('BrowserConfig') || key.includes('TerminalConfig') || - key.includes('CheckpointsConfig') + key.includes('CheckpointsConfig') || + key.includes('ApprovalsConfig') ))) assert.ok(keys.size > 0, '应能提取新增结构化配置用到的 engine 翻译 key')