feat(hermes): add agent runtime guard settings

This commit is contained in:
晴天
2026-05-25 01:29:28 +08:00
parent 4766a99d87
commit 77cadf9e0a
8 changed files with 724 additions and 18 deletions

View File

@@ -3325,6 +3325,7 @@ 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_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'])
const HERMES_DISPLAY_RESUME_VALUES = new Set(['full', 'minimal'])
@@ -3409,6 +3410,13 @@ function normalizeHermesBrowserEngine(value, strict = false) {
return 'auto'
}
function normalizeHermesImageInputMode(value, strict = false) {
const mode = String(value ?? '').trim().toLowerCase() || 'auto'
if (HERMES_AGENT_IMAGE_INPUT_MODES.has(mode)) return mode
if (strict) throw new Error('agent.image_input_mode 必须是 auto、native 或 text')
return 'auto'
}
function normalizeHermesDisplayToolProgress(value, strict = false, key = 'display.tool_progress') {
const progress = String(value ?? '').trim().toLowerCase() || 'all'
if (HERMES_DISPLAY_TOOL_PROGRESS_VALUES.has(progress)) return progress
@@ -3733,6 +3741,43 @@ export function mergeHermesAgentToolsetsConfig(config = {}, form = {}) {
return next
}
export function buildHermesAgentRuntimeConfigValues(config = {}) {
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
const agent = root.agent && typeof root.agent === 'object' && !Array.isArray(root.agent)
? root.agent
: {}
return {
agentMaxTurns: parseHermesInteger(agent.max_turns, 'agent.max_turns', 90, 1, 10000, false),
gatewayTimeout: parseHermesInteger(agent.gateway_timeout, 'agent.gateway_timeout', 1800, 0, 604800, false),
restartDrainTimeout: parseHermesInteger(agent.restart_drain_timeout, 'agent.restart_drain_timeout', 180, 0, 86400, false),
apiMaxRetries: parseHermesInteger(agent.api_max_retries, 'agent.api_max_retries', 3, 1, 20, false),
gatewayTimeoutWarning: parseHermesInteger(agent.gateway_timeout_warning, 'agent.gateway_timeout_warning', 900, 0, 604800, false),
clarifyTimeout: parseHermesInteger(agent.clarify_timeout, 'agent.clarify_timeout', 600, 0, 86400, false),
gatewayNotifyInterval: parseHermesInteger(agent.gateway_notify_interval, 'agent.gateway_notify_interval', 180, 0, 86400, false),
gatewayAutoContinueFreshness: parseHermesInteger(agent.gateway_auto_continue_freshness, 'agent.gateway_auto_continue_freshness', 3600, 0, 604800, false),
imageInputMode: normalizeHermesImageInputMode(agent.image_input_mode, false),
}
}
export function mergeHermesAgentRuntimeConfig(config = {}, form = {}) {
const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {})
const currentValues = buildHermesAgentRuntimeConfigValues(next)
const agent = next.agent && typeof next.agent === 'object' && !Array.isArray(next.agent)
? mergeConfigsPreservingFields(next.agent, {})
: {}
agent.max_turns = parseHermesInteger(Object.hasOwn(form, 'agentMaxTurns') ? form.agentMaxTurns : currentValues.agentMaxTurns, 'agent.max_turns', 90, 1, 10000, true)
agent.gateway_timeout = parseHermesInteger(Object.hasOwn(form, 'gatewayTimeout') ? form.gatewayTimeout : currentValues.gatewayTimeout, 'agent.gateway_timeout', 1800, 0, 604800, true)
agent.restart_drain_timeout = parseHermesInteger(Object.hasOwn(form, 'restartDrainTimeout') ? form.restartDrainTimeout : currentValues.restartDrainTimeout, 'agent.restart_drain_timeout', 180, 0, 86400, true)
agent.api_max_retries = parseHermesInteger(Object.hasOwn(form, 'apiMaxRetries') ? form.apiMaxRetries : currentValues.apiMaxRetries, 'agent.api_max_retries', 3, 1, 20, true)
agent.gateway_timeout_warning = parseHermesInteger(Object.hasOwn(form, 'gatewayTimeoutWarning') ? form.gatewayTimeoutWarning : currentValues.gatewayTimeoutWarning, 'agent.gateway_timeout_warning', 900, 0, 604800, true)
agent.clarify_timeout = parseHermesInteger(Object.hasOwn(form, 'clarifyTimeout') ? form.clarifyTimeout : currentValues.clarifyTimeout, 'agent.clarify_timeout', 600, 0, 86400, true)
agent.gateway_notify_interval = parseHermesInteger(Object.hasOwn(form, 'gatewayNotifyInterval') ? form.gatewayNotifyInterval : currentValues.gatewayNotifyInterval, 'agent.gateway_notify_interval', 180, 0, 86400, true)
agent.gateway_auto_continue_freshness = parseHermesInteger(Object.hasOwn(form, 'gatewayAutoContinueFreshness') ? form.gatewayAutoContinueFreshness : currentValues.gatewayAutoContinueFreshness, 'agent.gateway_auto_continue_freshness', 3600, 0, 604800, true)
agent.image_input_mode = normalizeHermesImageInputMode(Object.hasOwn(form, 'imageInputMode') ? form.imageInputMode : currentValues.imageInputMode, true)
next.agent = agent
return next
}
function validateHermesQuickCommands(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('quick_commands 必须是 JSON 对象')
@@ -10493,6 +10538,27 @@ const handlers = {
}
},
hermes_agent_runtime_config_read() {
const { configPath, exists, config } = readHermesConfigYamlObject()
return {
exists,
configPath,
values: buildHermesAgentRuntimeConfigValues(config),
}
},
hermes_agent_runtime_config_save({ form } = {}) {
const { configPath, config } = readHermesConfigYamlObject()
const next = mergeHermesAgentRuntimeConfig(config, form || {})
const backup = writeHermesConfigYamlObject(configPath, next)
return {
ok: true,
configPath,
backup,
values: buildHermesAgentRuntimeConfigValues(next),
}
},
hermes_unauthorized_dm_config_read() {
const { configPath, exists, config } = readHermesConfigYamlObject()
return {

View File

@@ -3912,6 +3912,173 @@ fn merge_hermes_agent_toolsets_config(
Ok(())
}
fn normalize_hermes_image_input_mode(
value: Option<String>,
strict: bool,
) -> Result<String, String> {
let mode = value.unwrap_or_default().trim().to_ascii_lowercase();
let mode = if mode.is_empty() {
"auto".to_string()
} else {
mode
};
if matches!(mode.as_str(), "auto" | "native" | "text") {
return Ok(mode);
}
if strict {
Err("agent.image_input_mode 必须是 auto、native 或 text".to_string())
} else {
Ok("auto".to_string())
}
}
fn build_hermes_agent_runtime_config_values(config: &serde_yaml::Value) -> Value {
let root = config.as_mapping();
let agent = root.and_then(|map| yaml_get_mapping(map, "agent"));
let image_input_mode = normalize_hermes_image_input_mode(
agent.and_then(|map| yaml_string_field(map, "image_input_mode")),
false,
)
.unwrap_or_else(|_| "auto".to_string());
serde_json::json!({
"agentMaxTurns": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_turns"), 90, 1, 10000)).unwrap_or(90),
"gatewayTimeout": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_timeout"), 1800, 0, 604800)).unwrap_or(1800),
"restartDrainTimeout": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "restart_drain_timeout"), 180, 0, 86400)).unwrap_or(180),
"apiMaxRetries": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "api_max_retries"), 3, 1, 20)).unwrap_or(3),
"gatewayTimeoutWarning": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_timeout_warning"), 900, 0, 604800)).unwrap_or(900),
"clarifyTimeout": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "clarify_timeout"), 600, 0, 86400)).unwrap_or(600),
"gatewayNotifyInterval": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_notify_interval"), 180, 0, 86400)).unwrap_or(180),
"gatewayAutoContinueFreshness": agent.map(|map| bounded_hermes_i64(yaml_i64_field(map, "gateway_auto_continue_freshness"), 3600, 0, 604800)).unwrap_or(3600),
"imageInputMode": image_input_mode,
})
}
fn agent_runtime_i64_value(
form: &Value,
current: &Value,
form_key: &str,
default_value: i64,
) -> Option<i64> {
if form.get(form_key).is_some() {
form_i64(form, form_key)
} else {
Some(current[form_key].as_i64().unwrap_or(default_value))
}
}
fn merge_hermes_agent_runtime_config(
config: &mut serde_yaml::Value,
form: &Value,
) -> Result<(), String> {
let current = build_hermes_agent_runtime_config_values(config);
let agent_max_turns = validate_hermes_i64(
agent_runtime_i64_value(form, &current, "agentMaxTurns", 90),
"agent.max_turns",
90,
1,
10000,
)?;
let gateway_timeout = validate_hermes_i64(
agent_runtime_i64_value(form, &current, "gatewayTimeout", 1800),
"agent.gateway_timeout",
1800,
0,
604800,
)?;
let restart_drain_timeout = validate_hermes_i64(
agent_runtime_i64_value(form, &current, "restartDrainTimeout", 180),
"agent.restart_drain_timeout",
180,
0,
86400,
)?;
let api_max_retries = validate_hermes_i64(
agent_runtime_i64_value(form, &current, "apiMaxRetries", 3),
"agent.api_max_retries",
3,
1,
20,
)?;
let gateway_timeout_warning = validate_hermes_i64(
agent_runtime_i64_value(form, &current, "gatewayTimeoutWarning", 900),
"agent.gateway_timeout_warning",
900,
0,
604800,
)?;
let clarify_timeout = validate_hermes_i64(
agent_runtime_i64_value(form, &current, "clarifyTimeout", 600),
"agent.clarify_timeout",
600,
0,
86400,
)?;
let gateway_notify_interval = validate_hermes_i64(
agent_runtime_i64_value(form, &current, "gatewayNotifyInterval", 180),
"agent.gateway_notify_interval",
180,
0,
86400,
)?;
let gateway_auto_continue_freshness = validate_hermes_i64(
agent_runtime_i64_value(form, &current, "gatewayAutoContinueFreshness", 3600),
"agent.gateway_auto_continue_freshness",
3600,
0,
604800,
)?;
let image_input_mode = normalize_hermes_image_input_mode(
if form.get("imageInputMode").is_some() {
form_string(form, "imageInputMode")
} else {
current["imageInputMode"].as_str().map(ToString::to_string)
},
true,
)?;
let root = ensure_yaml_object(config)?;
let agent = yaml_child_object(root, "agent")?;
agent.insert(
yaml_key("max_turns"),
serde_yaml::Value::Number(agent_max_turns.into()),
);
agent.insert(
yaml_key("gateway_timeout"),
serde_yaml::Value::Number(gateway_timeout.into()),
);
agent.insert(
yaml_key("restart_drain_timeout"),
serde_yaml::Value::Number(restart_drain_timeout.into()),
);
agent.insert(
yaml_key("api_max_retries"),
serde_yaml::Value::Number(api_max_retries.into()),
);
agent.insert(
yaml_key("gateway_timeout_warning"),
serde_yaml::Value::Number(gateway_timeout_warning.into()),
);
agent.insert(
yaml_key("clarify_timeout"),
serde_yaml::Value::Number(clarify_timeout.into()),
);
agent.insert(
yaml_key("gateway_notify_interval"),
serde_yaml::Value::Number(gateway_notify_interval.into()),
);
agent.insert(
yaml_key("gateway_auto_continue_freshness"),
serde_yaml::Value::Number(gateway_auto_continue_freshness.into()),
);
agent.insert(
yaml_key("image_input_mode"),
serde_yaml::Value::String(image_input_mode),
);
Ok(())
}
fn normalize_hermes_unauthorized_dm_behavior(
value: Option<String>,
strict: bool,
@@ -6149,6 +6316,30 @@ pub fn hermes_agent_toolsets_config_save(form: Value) -> Result<Value, String> {
}))
}
#[tauri::command]
pub fn hermes_agent_runtime_config_read() -> Result<Value, String> {
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_agent_runtime_config_values(&config),
}))
}
#[tauri::command]
pub fn hermes_agent_runtime_config_save(form: Value) -> Result<Value, String> {
let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?;
merge_hermes_agent_runtime_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_agent_runtime_config_values(&config),
}))
}
#[tauri::command]
pub fn hermes_unauthorized_dm_config_read() -> Result<Value, String> {
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
@@ -12707,6 +12898,161 @@ agent:
}
}
#[cfg(test)]
mod hermes_agent_runtime_config_tests {
use super::{build_hermes_agent_runtime_config_values, merge_hermes_agent_runtime_config};
use serde_json::json;
#[test]
fn agent_runtime_values_have_upstream_defaults() {
let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap();
let values = build_hermes_agent_runtime_config_values(&config);
assert_eq!(values["agentMaxTurns"], 90);
assert_eq!(values["gatewayTimeout"], 1800);
assert_eq!(values["restartDrainTimeout"], 180);
assert_eq!(values["apiMaxRetries"], 3);
assert_eq!(values["gatewayTimeoutWarning"], 900);
assert_eq!(values["clarifyTimeout"], 600);
assert_eq!(values["gatewayNotifyInterval"], 180);
assert_eq!(values["gatewayAutoContinueFreshness"], 3600);
assert_eq!(values["imageInputMode"], "auto");
}
#[test]
fn agent_runtime_values_read_yaml_fields() {
let config: serde_yaml::Value = serde_yaml::from_str(
r#"
agent:
max_turns: 240
gateway_timeout: 7200
restart_drain_timeout: 600
api_max_retries: 5
gateway_timeout_warning: 1200
clarify_timeout: 900
gateway_notify_interval: 240
gateway_auto_continue_freshness: 5400
image_input_mode: native
"#,
)
.unwrap();
let values = build_hermes_agent_runtime_config_values(&config);
assert_eq!(values["agentMaxTurns"], 240);
assert_eq!(values["gatewayTimeout"], 7200);
assert_eq!(values["restartDrainTimeout"], 600);
assert_eq!(values["apiMaxRetries"], 5);
assert_eq!(values["gatewayTimeoutWarning"], 1200);
assert_eq!(values["clarifyTimeout"], 900);
assert_eq!(values["gatewayNotifyInterval"], 240);
assert_eq!(values["gatewayAutoContinueFreshness"], 5400);
assert_eq!(values["imageInputMode"], "native");
}
#[test]
fn merge_agent_runtime_config_preserves_unrelated_yaml() {
let mut config: serde_yaml::Value = serde_yaml::from_str(
r#"
model:
provider: anthropic
agent:
max_turns: 90
disabled_toolsets:
- terminal
custom_flag: keep-agent
streaming:
enabled: true
"#,
)
.unwrap();
merge_hermes_agent_runtime_config(
&mut config,
&json!({
"agentMaxTurns": "180",
"gatewayTimeout": "3600",
"restartDrainTimeout": "300",
"apiMaxRetries": "2",
"gatewayTimeoutWarning": "600",
"clarifyTimeout": "300",
"gatewayNotifyInterval": "120",
"gatewayAutoContinueFreshness": "1800",
"imageInputMode": "text",
}),
)
.unwrap();
assert_eq!(config["model"]["provider"].as_str(), Some("anthropic"));
assert_eq!(config["streaming"]["enabled"].as_bool(), Some(true));
assert_eq!(config["agent"]["max_turns"].as_i64(), Some(180));
assert_eq!(config["agent"]["gateway_timeout"].as_i64(), Some(3600));
assert_eq!(config["agent"]["restart_drain_timeout"].as_i64(), Some(300));
assert_eq!(config["agent"]["api_max_retries"].as_i64(), Some(2));
assert_eq!(
config["agent"]["gateway_timeout_warning"].as_i64(),
Some(600)
);
assert_eq!(config["agent"]["clarify_timeout"].as_i64(), Some(300));
assert_eq!(
config["agent"]["gateway_notify_interval"].as_i64(),
Some(120)
);
assert_eq!(
config["agent"]["gateway_auto_continue_freshness"].as_i64(),
Some(1800)
);
assert_eq!(config["agent"]["image_input_mode"].as_str(), Some("text"));
assert_eq!(
config["agent"]["disabled_toolsets"][0].as_str(),
Some("terminal")
);
assert_eq!(config["agent"]["custom_flag"].as_str(), Some("keep-agent"));
}
#[test]
fn merge_agent_runtime_config_allows_zero_disable_values() {
let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
merge_hermes_agent_runtime_config(
&mut config,
&json!({
"gatewayTimeout": "0",
"restartDrainTimeout": "0",
"gatewayTimeoutWarning": "0",
"gatewayNotifyInterval": "0",
"gatewayAutoContinueFreshness": "0",
}),
)
.unwrap();
assert_eq!(config["agent"]["gateway_timeout"].as_i64(), Some(0));
assert_eq!(config["agent"]["restart_drain_timeout"].as_i64(), Some(0));
assert_eq!(config["agent"]["gateway_timeout_warning"].as_i64(), Some(0));
assert_eq!(config["agent"]["gateway_notify_interval"].as_i64(), Some(0));
assert_eq!(
config["agent"]["gateway_auto_continue_freshness"].as_i64(),
Some(0)
);
}
#[test]
fn merge_agent_runtime_config_rejects_invalid_values() {
let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
let err =
merge_hermes_agent_runtime_config(&mut config, &json!({ "imageInputMode": "pixel" }))
.unwrap_err();
assert!(err.contains("agent.image_input_mode"));
let err = merge_hermes_agent_runtime_config(&mut config, &json!({ "agentMaxTurns": "0" }))
.unwrap_err();
assert!(err.contains("agent.max_turns"));
let err = merge_hermes_agent_runtime_config(&mut config, &json!({ "apiMaxRetries": "0" }))
.unwrap_err();
assert!(err.contains("agent.api_max_retries"));
let err =
merge_hermes_agent_runtime_config(&mut config, &json!({ "clarifyTimeout": "-1" }))
.unwrap_err();
assert!(err.contains("agent.clarify_timeout"));
}
}
#[cfg(test)]
mod hermes_unauthorized_dm_config_tests {
use super::{build_hermes_unauthorized_dm_config_values, merge_hermes_unauthorized_dm_config};

View File

@@ -271,6 +271,8 @@ pub fn run() {
hermes::hermes_quick_commands_config_save,
hermes::hermes_agent_toolsets_config_read,
hermes::hermes_agent_toolsets_config_save,
hermes::hermes_agent_runtime_config_read,
hermes::hermes_agent_runtime_config_save,
hermes::hermes_unauthorized_dm_config_read,
hermes::hermes_unauthorized_dm_config_save,
hermes::hermes_security_config_read,

View File

@@ -56,6 +56,18 @@ const AGENT_TOOLSETS_DEFAULTS = {
disabledToolsets: '',
}
const AGENT_RUNTIME_DEFAULTS = {
agentMaxTurns: 90,
gatewayTimeout: 1800,
restartDrainTimeout: 180,
apiMaxRetries: 3,
gatewayTimeoutWarning: 900,
clarifyTimeout: 600,
gatewayNotifyInterval: 180,
gatewayAutoContinueFreshness: 3600,
imageInputMode: 'auto',
}
const UNAUTHORIZED_DM_DEFAULTS = {
unauthorizedDmBehavior: 'pair',
}
@@ -143,6 +155,7 @@ const CODE_EXECUTION_MODES = ['project', 'strict']
const TERMINAL_BACKENDS = ['local', 'ssh', 'docker', 'singularity', 'modal', 'daytona', 'vercel_sandbox']
const BROWSER_ENGINES = ['auto', 'lightpanda', 'chrome']
const UNAUTHORIZED_DM_BEHAVIORS = ['pair', 'ignore']
const IMAGE_INPUT_MODES = ['auto', 'native', 'text']
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']
@@ -160,6 +173,7 @@ export function render() {
let skillsValues = { ...SKILLS_DEFAULTS }
let quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS }
let agentToolsetsValues = { ...AGENT_TOOLSETS_DEFAULTS }
let agentRuntimeValues = { ...AGENT_RUNTIME_DEFAULTS }
let unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS }
let securityValues = { ...SECURITY_DEFAULTS }
let displayValues = { ...DISPLAY_DEFAULTS }
@@ -178,6 +192,7 @@ export function render() {
let skillsLoading = true
let quickCommandsLoading = true
let agentToolsetsLoading = true
let agentRuntimeLoading = true
let unauthorizedDmLoading = true
let securityLoading = true
let displayLoading = true
@@ -196,6 +211,7 @@ export function render() {
let skillsSaving = false
let quickCommandsSaving = false
let agentToolsetsSaving = false
let agentRuntimeSaving = false
let unauthorizedDmSaving = false
let securitySaving = false
let displaySaving = false
@@ -214,6 +230,7 @@ export function render() {
let skillsError = null
let quickCommandsError = null
let agentToolsetsError = null
let agentRuntimeError = null
let unauthorizedDmError = null
let securityError = null
let displayError = null
@@ -234,7 +251,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving
}
function option(labelKey, value, selected) {
@@ -251,7 +268,7 @@ export function render() {
}
function renderRuntimePanel() {
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel">
<div class="hm-panel-header">
@@ -299,7 +316,7 @@ export function render() {
}
function renderCompressionPanel() {
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-compression-panel">
<div class="hm-panel-header">
@@ -349,7 +366,7 @@ export function render() {
}
function renderToolGuardrailsPanel() {
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-guardrails-panel">
<div class="hm-panel-header">
@@ -411,7 +428,7 @@ export function render() {
}
function renderMemoryPanel() {
const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-memory-panel">
<div class="hm-panel-header">
@@ -461,7 +478,7 @@ export function render() {
}
function renderSkillsConfigPanel() {
const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-skills-panel">
<div class="hm-panel-header">
@@ -493,7 +510,7 @@ export function render() {
}
function renderQuickCommandsConfigPanel() {
const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-quick-commands-panel">
<div class="hm-panel-header">
@@ -519,7 +536,7 @@ export function render() {
}
function renderAgentToolsetsConfigPanel() {
const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-agent-toolsets-panel">
<div class="hm-panel-header">
@@ -544,8 +561,70 @@ 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 || privacySaving || browserSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-agent-runtime-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesAgentRuntimeConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesAgentRuntimeConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${agentRuntimeSaving ? t('engine.hermesConfigStatusSaving') : agentRuntimeLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesAgentRuntimeConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-agent-runtime-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesAgentRuntimeConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(agentRuntimeError)}
<div class="hm-config-runtime-grid hm-config-agent-runtime-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigMaxTurns')}</span>
<input id="hm-agent-max-turns" class="hm-input" type="number" inputmode="numeric" min="1" max="10000" step="1" value="${esc(agentRuntimeValues.agentMaxTurns)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigGatewayTimeout')}</span>
<input id="hm-agent-gateway-timeout" class="hm-input" type="number" inputmode="numeric" min="0" max="604800" step="1" value="${esc(agentRuntimeValues.gatewayTimeout)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigRestartDrainTimeout')}</span>
<input id="hm-agent-restart-drain-timeout" class="hm-input" type="number" inputmode="numeric" min="0" max="86400" step="1" value="${esc(agentRuntimeValues.restartDrainTimeout)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigApiMaxRetries')}</span>
<input id="hm-agent-api-max-retries" class="hm-input" type="number" inputmode="numeric" min="1" max="20" step="1" value="${esc(agentRuntimeValues.apiMaxRetries)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigGatewayTimeoutWarning')}</span>
<input id="hm-agent-gateway-timeout-warning" class="hm-input" type="number" inputmode="numeric" min="0" max="604800" step="1" value="${esc(agentRuntimeValues.gatewayTimeoutWarning)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigClarifyTimeout')}</span>
<input id="hm-agent-clarify-timeout" class="hm-input" type="number" inputmode="numeric" min="0" max="86400" step="1" value="${esc(agentRuntimeValues.clarifyTimeout)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigGatewayNotifyInterval')}</span>
<input id="hm-agent-gateway-notify-interval" class="hm-input" type="number" inputmode="numeric" min="0" max="86400" step="1" value="${esc(agentRuntimeValues.gatewayNotifyInterval)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigGatewayAutoContinueFreshness')}</span>
<input id="hm-agent-gateway-auto-continue-freshness" class="hm-input" type="number" inputmode="numeric" min="0" max="604800" step="1" value="${esc(agentRuntimeValues.gatewayAutoContinueFreshness)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesAgentRuntimeConfigImageInputMode')}</span>
<select id="hm-agent-image-input-mode" class="hm-input" ${disabled ? 'disabled' : ''}>
${IMAGE_INPUT_MODES.map(mode => option(`engine.hermesAgentRuntimeConfigImageInputMode_${mode}`, mode, agentRuntimeValues.imageInputMode)).join('')}
</select>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesAgentRuntimeConfigFootnote')}</div>
</div>
</div>
`
}
function renderUnauthorizedDmConfigPanel() {
const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-unauthorized-dm-panel">
<div class="hm-panel-header">
@@ -575,7 +654,7 @@ export function render() {
}
function renderSecurityConfigPanel() {
const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-security-panel">
<div class="hm-panel-header">
@@ -617,7 +696,7 @@ export function render() {
}
function renderDisplayConfigPanel() {
const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-display-panel">
<div class="hm-panel-header">
@@ -681,7 +760,7 @@ export function render() {
}
function renderHumanDelayConfigPanel() {
const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-human-delay-panel">
<div class="hm-panel-header">
@@ -719,7 +798,7 @@ export function render() {
}
function renderStreamingPanel() {
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-streaming-panel">
<div class="hm-panel-header">
@@ -771,7 +850,7 @@ export function render() {
}
function renderExecutionLimitsPanel() {
const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving
const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-execution-limits-panel">
<div class="hm-panel-header">
@@ -843,7 +922,7 @@ export function render() {
}
function renderIoSafetyPanel() {
const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-io-safety-panel">
<div class="hm-panel-header">
@@ -883,7 +962,7 @@ export function render() {
}
function renderPrivacyPanel() {
const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-privacy-panel">
<div class="hm-panel-header">
@@ -911,7 +990,7 @@ export function render() {
}
function renderBrowserPanel() {
const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-browser-panel">
<div class="hm-panel-header">
@@ -955,7 +1034,7 @@ export function render() {
}
function renderTerminalPanel() {
const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-terminal-panel">
<div class="hm-panel-header">
@@ -1052,6 +1131,7 @@ export function render() {
${renderSkillsConfigPanel()}
${renderQuickCommandsConfigPanel()}
${renderAgentToolsetsConfigPanel()}
${renderAgentRuntimeConfigPanel()}
${renderUnauthorizedDmConfigPanel()}
${renderSecurityConfigPanel()}
${renderDisplayConfigPanel()}
@@ -1082,6 +1162,7 @@ export function render() {
el.querySelector('#hm-skills-config-save')?.addEventListener('click', saveSkillsConfig)
el.querySelector('#hm-quick-commands-save')?.addEventListener('click', saveQuickCommandsConfig)
el.querySelector('#hm-agent-toolsets-save')?.addEventListener('click', saveAgentToolsetsConfig)
el.querySelector('#hm-agent-runtime-save')?.addEventListener('click', saveAgentRuntimeConfig)
el.querySelector('#hm-unauthorized-dm-save')?.addEventListener('click', saveUnauthorizedDmConfig)
el.querySelector('#hm-security-save')?.addEventListener('click', saveSecurityConfig)
el.querySelector('#hm-display-save')?.addEventListener('click', saveDisplayConfig)
@@ -1134,6 +1215,11 @@ export function render() {
agentToolsetsValues = { ...AGENT_TOOLSETS_DEFAULTS, ...(data?.values || {}) }
}
async function loadAgentRuntimeConfig() {
const data = await api.hermesAgentRuntimeConfigRead()
agentRuntimeValues = { ...AGENT_RUNTIME_DEFAULTS, ...(data?.values || {}) }
}
async function loadUnauthorizedDmConfig() {
const data = await api.hermesUnauthorizedDmConfigRead()
unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS, ...(data?.values || {}) }
@@ -1193,6 +1279,7 @@ export function render() {
skillsLoading = true
quickCommandsLoading = true
agentToolsetsLoading = true
agentRuntimeLoading = true
unauthorizedDmLoading = true
securityLoading = true
displayLoading = true
@@ -1211,6 +1298,7 @@ export function render() {
skillsError = null
quickCommandsError = null
agentToolsetsError = null
agentRuntimeError = null
unauthorizedDmError = null
securityError = null
displayError = null
@@ -1333,6 +1421,14 @@ export function render() {
agentToolsetsLoading = false
draw()
}
try {
await loadAgentRuntimeConfig()
} catch (err) {
agentRuntimeError = humanizeError(err, t('engine.hermesAgentRuntimeConfigLoadFailed') || 'Load agent runtime config failed')
} finally {
agentRuntimeLoading = false
draw()
}
try {
await loadUnauthorizedDmConfig()
} catch (err) {
@@ -1407,6 +1503,9 @@ export function render() {
try {
await loadAgentToolsetsConfig()
} catch {}
try {
await loadAgentRuntimeConfig()
} catch {}
try {
await loadUnauthorizedDmConfig()
} catch {}
@@ -1643,6 +1742,39 @@ export function render() {
}
}
async function saveAgentRuntimeConfig() {
const form = {
agentMaxTurns: el.querySelector('#hm-agent-max-turns')?.value || '90',
gatewayTimeout: el.querySelector('#hm-agent-gateway-timeout')?.value || '1800',
restartDrainTimeout: el.querySelector('#hm-agent-restart-drain-timeout')?.value || '180',
apiMaxRetries: el.querySelector('#hm-agent-api-max-retries')?.value || '3',
gatewayTimeoutWarning: el.querySelector('#hm-agent-gateway-timeout-warning')?.value || '900',
clarifyTimeout: el.querySelector('#hm-agent-clarify-timeout')?.value || '600',
gatewayNotifyInterval: el.querySelector('#hm-agent-gateway-notify-interval')?.value || '180',
gatewayAutoContinueFreshness: el.querySelector('#hm-agent-gateway-auto-continue-freshness')?.value || '3600',
imageInputMode: el.querySelector('#hm-agent-image-input-mode')?.value || 'auto',
}
agentRuntimeSaving = true
agentRuntimeError = null
draw()
try {
const result = await api.hermesAgentRuntimeConfigSave(form)
agentRuntimeValues = { ...AGENT_RUNTIME_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesAgentRuntimeConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
agentRuntimeError = humanizeError(err, t('engine.hermesAgentRuntimeConfigSaveFailed') || 'Save agent runtime config failed')
toast(agentRuntimeError, 'error')
} finally {
agentRuntimeSaving = false
draw()
}
}
async function saveUnauthorizedDmConfig() {
const form = {
unauthorizedDmBehavior: el.querySelector('#hm-unauthorized-dm-behavior')?.value || 'pair',

View File

@@ -523,6 +523,8 @@ export const api = {
hermesQuickCommandsConfigSave: (form) => invoke('hermes_quick_commands_config_save', { form }),
hermesAgentToolsetsConfigRead: () => invoke('hermes_agent_toolsets_config_read'),
hermesAgentToolsetsConfigSave: (form) => invoke('hermes_agent_toolsets_config_save', { form }),
hermesAgentRuntimeConfigRead: () => invoke('hermes_agent_runtime_config_read'),
hermesAgentRuntimeConfigSave: (form) => invoke('hermes_agent_runtime_config_save', { form }),
hermesUnauthorizedDmConfigRead: () => invoke('hermes_unauthorized_dm_config_read'),
hermesUnauthorizedDmConfigSave: (form) => invoke('hermes_unauthorized_dm_config_save', { form }),
hermesSecurityConfigRead: () => invoke('hermes_security_config_read'),

View File

@@ -674,6 +674,26 @@ export default {
hermesAgentToolsetsConfigSaveFailed: _('保存全局工具集配置失败', 'Save global toolset settings failed', '儲存全域工具集設定失敗'),
hermesAgentToolsetsConfigDisabledToolsets: _('禁用工具集(每行一个)', 'Disabled toolsets, one per line', '停用工具集(每行一個)'),
hermesAgentToolsetsConfigFootnote: _('常见值包括 terminal、browser、memory、web。该设置会覆盖平台级工具配置留空表示不做全局禁用。高级 agent 字段会保留在 raw YAML 中。', 'Common values include terminal, browser, memory, and web. This setting overrides platform-level tool configuration; leave it empty for no global disables. Advanced agent fields stay in raw YAML.', '常見值包括 terminal、browser、memory、web。此設定會覆蓋平台級工具設定留空表示不做全域停用。進階 agent 欄位會保留在 raw YAML 中。'),
hermesAgentRuntimeConfigTitle: _('Agent 长跑保护', 'Agent runtime guards', 'Agent 長跑保護'),
hermesAgentRuntimeConfigDesc: _('控制 Agent 轮次上限、Gateway 等待、重启排水、重试、超时预警、澄清等待和自动续跑新鲜度,减少长时间任务无人值守失控。', 'Control turn limits, Gateway waits, restart drain, retries, timeout warnings, clarification waits, and auto-continue freshness to keep unattended long runs bounded.', '控制 Agent 輪次上限、Gateway 等待、重啟排水、重試、逾時預警、澄清等待和自動續跑新鮮度,減少長時間任務無人值守失控。'),
hermesAgentRuntimeConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesAgentRuntimeConfigSave: _('保存长跑保护', 'Save runtime guards', '儲存長跑保護'),
hermesAgentRuntimeConfigSaveSuccess: _('Agent 长跑保护配置已保存,建议重启 Hermes Gateway 生效', 'Agent runtime guard settings saved. Restart Hermes Gateway to take effect.', 'Agent 長跑保護設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesAgentRuntimeConfigLoadFailed: _('加载 Agent 长跑保护配置失败', 'Load agent runtime guard settings failed', '載入 Agent 長跑保護設定失敗'),
hermesAgentRuntimeConfigSaveFailed: _('保存 Agent 长跑保护配置失败', 'Save agent runtime guard settings failed', '儲存 Agent 長跑保護設定失敗'),
hermesAgentRuntimeConfigMaxTurns: _('单次运行最大轮数', 'Max turns per run', '單次執行最大輪數'),
hermesAgentRuntimeConfigGatewayTimeout: _('Gateway 等待超时(秒)', 'Gateway timeout (seconds)', 'Gateway 等待逾時(秒)'),
hermesAgentRuntimeConfigRestartDrainTimeout: _('重启排水等待(秒)', 'Restart drain timeout (seconds)', '重啟排水等待(秒)'),
hermesAgentRuntimeConfigApiMaxRetries: _('API 最大重试次数', 'API max retries', 'API 最大重試次數'),
hermesAgentRuntimeConfigGatewayTimeoutWarning: _('超时预警阈值(秒)', 'Timeout warning threshold (seconds)', '逾時預警閾值(秒)'),
hermesAgentRuntimeConfigClarifyTimeout: _('澄清等待超时(秒)', 'Clarification timeout (seconds)', '澄清等待逾時(秒)'),
hermesAgentRuntimeConfigGatewayNotifyInterval: _('Gateway 心跳通知间隔(秒)', 'Gateway notify interval (seconds)', 'Gateway 心跳通知間隔(秒)'),
hermesAgentRuntimeConfigGatewayAutoContinueFreshness: _('自动续跑新鲜度(秒)', 'Auto-continue freshness (seconds)', '自動續跑新鮮度(秒)'),
hermesAgentRuntimeConfigImageInputMode: _('图片输入模式', 'Image input mode', '圖片輸入模式'),
hermesAgentRuntimeConfigImageInputMode_auto: _('自动选择', 'Auto', '自動選擇'),
hermesAgentRuntimeConfigImageInputMode_native: _('原生图片输入', 'Native image input', '原生圖片輸入'),
hermesAgentRuntimeConfigImageInputMode_text: _('转文本描述', 'Convert to text description', '轉文字描述'),
hermesAgentRuntimeConfigFootnote: _('这些字段会写入 agent.*,影响 CLI 与 Gateway 长跑行为。将可选超时设为 0 表示关闭对应限制或通知disabled_toolsets 和其他高级 agent 字段会保留在 raw YAML 中。', 'These fields are written under agent.* and affect CLI and Gateway long-running behavior. Set optional timeouts to 0 to disable the corresponding limit or notification. disabled_toolsets and other advanced agent fields stay in raw YAML.', '這些欄位會寫入 agent.*,影響 CLI 與 Gateway 長跑行為。將可選逾時設為 0 表示關閉對應限制或通知disabled_toolsets 和其他進階 agent 欄位會保留在 raw YAML 中。'),
hermesUnauthorizedDmConfigTitle: _('未授权私信', 'Unauthorized DMs', '未授權私訊'),
hermesUnauthorizedDmConfigDesc: _('控制陌生用户直接私信 Hermes 时的全局响应策略,适合公网部署时减少无效打扰或保留配对入口。', 'Control the global response when unknown users send Hermes a direct message. Useful for public deployments that need fewer unsolicited replies or a pairing entry point.', '控制陌生使用者直接私訊 Hermes 時的全域回應策略,適合公開部署時減少無效打擾或保留配對入口。'),
hermesUnauthorizedDmConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),

View File

@@ -0,0 +1,120 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildHermesAgentRuntimeConfigValues,
mergeHermesAgentRuntimeConfig,
} from '../scripts/dev-api.js'
test('Hermes Agent 长跑保护配置读取会提供上游默认值', () => {
const values = buildHermesAgentRuntimeConfigValues({})
assert.deepEqual(values, {
agentMaxTurns: 90,
gatewayTimeout: 1800,
restartDrainTimeout: 180,
apiMaxRetries: 3,
gatewayTimeoutWarning: 900,
clarifyTimeout: 600,
gatewayNotifyInterval: 180,
gatewayAutoContinueFreshness: 3600,
imageInputMode: 'auto',
})
})
test('Hermes Agent 长跑保护配置读取会回显 YAML 字段', () => {
const values = buildHermesAgentRuntimeConfigValues({
agent: {
max_turns: 240,
gateway_timeout: 7200,
restart_drain_timeout: 600,
api_max_retries: 5,
gateway_timeout_warning: 1200,
clarify_timeout: 900,
gateway_notify_interval: 240,
gateway_auto_continue_freshness: 5400,
image_input_mode: 'native',
},
})
assert.equal(values.agentMaxTurns, 240)
assert.equal(values.gatewayTimeout, 7200)
assert.equal(values.restartDrainTimeout, 600)
assert.equal(values.apiMaxRetries, 5)
assert.equal(values.gatewayTimeoutWarning, 1200)
assert.equal(values.clarifyTimeout, 900)
assert.equal(values.gatewayNotifyInterval, 240)
assert.equal(values.gatewayAutoContinueFreshness, 5400)
assert.equal(values.imageInputMode, 'native')
})
test('Hermes Agent 长跑保护配置保存会保留未知字段并写入 agent', () => {
const next = mergeHermesAgentRuntimeConfig({
model: { provider: 'anthropic' },
agent: {
max_turns: 90,
disabled_toolsets: ['terminal'],
custom_flag: 'keep-agent',
},
streaming: { enabled: true },
}, {
agentMaxTurns: '180',
gatewayTimeout: '3600',
restartDrainTimeout: '300',
apiMaxRetries: '2',
gatewayTimeoutWarning: '600',
clarifyTimeout: '300',
gatewayNotifyInterval: '120',
gatewayAutoContinueFreshness: '1800',
imageInputMode: 'text',
})
assert.deepEqual(next.model, { provider: 'anthropic' })
assert.deepEqual(next.streaming, { enabled: true })
assert.equal(next.agent.max_turns, 180)
assert.equal(next.agent.gateway_timeout, 3600)
assert.equal(next.agent.restart_drain_timeout, 300)
assert.equal(next.agent.api_max_retries, 2)
assert.equal(next.agent.gateway_timeout_warning, 600)
assert.equal(next.agent.clarify_timeout, 300)
assert.equal(next.agent.gateway_notify_interval, 120)
assert.equal(next.agent.gateway_auto_continue_freshness, 1800)
assert.equal(next.agent.image_input_mode, 'text')
assert.deepEqual(next.agent.disabled_toolsets, ['terminal'])
assert.equal(next.agent.custom_flag, 'keep-agent')
})
test('Hermes Agent 长跑保护配置保存允许 0 表示关闭或无限制', () => {
const next = mergeHermesAgentRuntimeConfig({}, {
gatewayTimeout: '0',
restartDrainTimeout: '0',
gatewayTimeoutWarning: '0',
gatewayNotifyInterval: '0',
gatewayAutoContinueFreshness: '0',
})
assert.equal(next.agent.gateway_timeout, 0)
assert.equal(next.agent.restart_drain_timeout, 0)
assert.equal(next.agent.gateway_timeout_warning, 0)
assert.equal(next.agent.gateway_notify_interval, 0)
assert.equal(next.agent.gateway_auto_continue_freshness, 0)
})
test('Hermes Agent 长跑保护配置保存会拒绝非法枚举和越界值', () => {
assert.throws(
() => mergeHermesAgentRuntimeConfig({}, { imageInputMode: 'pixel' }),
/agent\.image_input_mode/,
)
assert.throws(
() => mergeHermesAgentRuntimeConfig({}, { agentMaxTurns: '0' }),
/agent\.max_turns/,
)
assert.throws(
() => mergeHermesAgentRuntimeConfig({}, { apiMaxRetries: '0' }),
/agent\.api_max_retries/,
)
assert.throws(
() => mergeHermesAgentRuntimeConfig({}, { clarifyTimeout: '-1' }),
/agent\.clarify_timeout/,
)
})

View File

@@ -67,6 +67,23 @@ test('Hermes 配置页会暴露全局禁用工具集结构化配置字段', () =
}
})
test('Hermes 配置页会暴露 Agent 长跑保护结构化配置字段', () => {
for (const id of [
'hm-agent-runtime-save',
'hm-agent-max-turns',
'hm-agent-gateway-timeout',
'hm-agent-restart-drain-timeout',
'hm-agent-api-max-retries',
'hm-agent-gateway-timeout-warning',
'hm-agent-clarify-timeout',
'hm-agent-gateway-notify-interval',
'hm-agent-gateway-auto-continue-freshness',
'hm-agent-image-input-mode',
]) {
assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
}
})
test('Hermes 配置页会暴露未授权 DM 全局策略字段', () => {
for (const id of [
'hm-unauthorized-dm-save',
@@ -209,6 +226,7 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => {
key.includes('SkillsConfig') ||
key.includes('QuickCommandsConfig') ||
key.includes('AgentToolsetsConfig') ||
key.includes('AgentRuntimeConfig') ||
key.includes('UnauthorizedDmConfig') ||
key.includes('SecurityConfig') ||
key.includes('HumanDelayConfig') ||