mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 04:40:18 +08:00
feat(hermes): add agent runtime guard settings
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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, ¤t, "agentMaxTurns", 90),
|
||||
"agent.max_turns",
|
||||
90,
|
||||
1,
|
||||
10000,
|
||||
)?;
|
||||
let gateway_timeout = validate_hermes_i64(
|
||||
agent_runtime_i64_value(form, ¤t, "gatewayTimeout", 1800),
|
||||
"agent.gateway_timeout",
|
||||
1800,
|
||||
0,
|
||||
604800,
|
||||
)?;
|
||||
let restart_drain_timeout = validate_hermes_i64(
|
||||
agent_runtime_i64_value(form, ¤t, "restartDrainTimeout", 180),
|
||||
"agent.restart_drain_timeout",
|
||||
180,
|
||||
0,
|
||||
86400,
|
||||
)?;
|
||||
let api_max_retries = validate_hermes_i64(
|
||||
agent_runtime_i64_value(form, ¤t, "apiMaxRetries", 3),
|
||||
"agent.api_max_retries",
|
||||
3,
|
||||
1,
|
||||
20,
|
||||
)?;
|
||||
let gateway_timeout_warning = validate_hermes_i64(
|
||||
agent_runtime_i64_value(form, ¤t, "gatewayTimeoutWarning", 900),
|
||||
"agent.gateway_timeout_warning",
|
||||
900,
|
||||
0,
|
||||
604800,
|
||||
)?;
|
||||
let clarify_timeout = validate_hermes_i64(
|
||||
agent_runtime_i64_value(form, ¤t, "clarifyTimeout", 600),
|
||||
"agent.clarify_timeout",
|
||||
600,
|
||||
0,
|
||||
86400,
|
||||
)?;
|
||||
let gateway_notify_interval = validate_hermes_i64(
|
||||
agent_runtime_i64_value(form, ¤t, "gatewayNotifyInterval", 180),
|
||||
"agent.gateway_notify_interval",
|
||||
180,
|
||||
0,
|
||||
86400,
|
||||
)?;
|
||||
let gateway_auto_continue_freshness = validate_hermes_i64(
|
||||
agent_runtime_i64_value(form, ¤t, "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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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', '結構化設定'),
|
||||
|
||||
120
tests/hermes-agent-runtime-config.test.js
Normal file
120
tests/hermes-agent-runtime-config.test.js
Normal 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/,
|
||||
)
|
||||
})
|
||||
@@ -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') ||
|
||||
|
||||
Reference in New Issue
Block a user