mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-07-14 00:43:19 +08:00
feat(hermes): add input output safety settings
This commit is contained in:
@@ -3894,6 +3894,34 @@ export function buildHermesExecutionLimitsConfigValues(config = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildHermesIoSafetyConfigValues(config = {}) {
|
||||
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
|
||||
const toolOutput = root.tool_output && typeof root.tool_output === 'object' && !Array.isArray(root.tool_output)
|
||||
? root.tool_output
|
||||
: {}
|
||||
return {
|
||||
fileReadMaxChars: parseHermesInteger(root.file_read_max_chars, 'file_read_max_chars', 100000, 1000, 1000000, false),
|
||||
toolOutputMaxBytes: parseHermesInteger(toolOutput.max_bytes, 'tool_output.max_bytes', 50000, 1000, 1000000, false),
|
||||
toolOutputMaxLines: parseHermesInteger(toolOutput.max_lines, 'tool_output.max_lines', 2000, 1, 100000, false),
|
||||
toolOutputMaxLineLength: parseHermesInteger(toolOutput.max_line_length, 'tool_output.max_line_length', 2000, 1, 100000, false),
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeHermesIoSafetyConfig(config = {}, form = {}) {
|
||||
const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {})
|
||||
const currentValues = buildHermesIoSafetyConfigValues(next)
|
||||
const toolOutput = next.tool_output && typeof next.tool_output === 'object' && !Array.isArray(next.tool_output)
|
||||
? mergeConfigsPreservingFields(next.tool_output, {})
|
||||
: {}
|
||||
|
||||
next.file_read_max_chars = parseHermesInteger(Object.hasOwn(form, 'fileReadMaxChars') ? form.fileReadMaxChars : currentValues.fileReadMaxChars, 'file_read_max_chars', 100000, 1000, 1000000, true)
|
||||
toolOutput.max_bytes = parseHermesInteger(Object.hasOwn(form, 'toolOutputMaxBytes') ? form.toolOutputMaxBytes : currentValues.toolOutputMaxBytes, 'tool_output.max_bytes', 50000, 1000, 1000000, true)
|
||||
toolOutput.max_lines = parseHermesInteger(Object.hasOwn(form, 'toolOutputMaxLines') ? form.toolOutputMaxLines : currentValues.toolOutputMaxLines, 'tool_output.max_lines', 2000, 1, 100000, true)
|
||||
toolOutput.max_line_length = parseHermesInteger(Object.hasOwn(form, 'toolOutputMaxLineLength') ? form.toolOutputMaxLineLength : currentValues.toolOutputMaxLineLength, 'tool_output.max_line_length', 2000, 1, 100000, true)
|
||||
next.tool_output = toolOutput
|
||||
return next
|
||||
}
|
||||
|
||||
export function buildHermesTerminalConfigValues(config = {}) {
|
||||
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
|
||||
const terminal = root.terminal && typeof root.terminal === 'object' && !Array.isArray(root.terminal)
|
||||
@@ -10475,6 +10503,27 @@ const handlers = {
|
||||
}
|
||||
},
|
||||
|
||||
hermes_io_safety_config_read() {
|
||||
const { configPath, exists, config } = readHermesConfigYamlObject()
|
||||
return {
|
||||
exists,
|
||||
configPath,
|
||||
values: buildHermesIoSafetyConfigValues(config),
|
||||
}
|
||||
},
|
||||
|
||||
hermes_io_safety_config_save({ form } = {}) {
|
||||
const { configPath, config } = readHermesConfigYamlObject()
|
||||
const next = mergeHermesIoSafetyConfig(config, form || {})
|
||||
const backup = writeHermesConfigYamlObject(configPath, next)
|
||||
return {
|
||||
ok: true,
|
||||
configPath,
|
||||
backup,
|
||||
values: buildHermesIoSafetyConfigValues(next),
|
||||
}
|
||||
},
|
||||
|
||||
hermes_terminal_config_read() {
|
||||
const { configPath, exists, config } = readHermesConfigYamlObject()
|
||||
return {
|
||||
|
||||
@@ -4565,6 +4565,108 @@ fn build_hermes_execution_limits_config_values(config: &serde_yaml::Value) -> Va
|
||||
})
|
||||
}
|
||||
|
||||
fn build_hermes_io_safety_config_values(config: &serde_yaml::Value) -> Value {
|
||||
let root = config.as_mapping();
|
||||
let tool_output = root.and_then(|map| yaml_get_mapping(map, "tool_output"));
|
||||
let file_read_max_chars = root
|
||||
.map(|map| {
|
||||
bounded_hermes_i64(
|
||||
yaml_i64_field(map, "file_read_max_chars"),
|
||||
100000,
|
||||
1000,
|
||||
1000000,
|
||||
)
|
||||
})
|
||||
.unwrap_or(100000);
|
||||
let tool_output_max_bytes = tool_output
|
||||
.map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_bytes"), 50000, 1000, 1000000))
|
||||
.unwrap_or(50000);
|
||||
let tool_output_max_lines = tool_output
|
||||
.map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_lines"), 2000, 1, 100000))
|
||||
.unwrap_or(2000);
|
||||
let tool_output_max_line_length = tool_output
|
||||
.map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_line_length"), 2000, 1, 100000))
|
||||
.unwrap_or(2000);
|
||||
|
||||
serde_json::json!({
|
||||
"fileReadMaxChars": file_read_max_chars,
|
||||
"toolOutputMaxBytes": tool_output_max_bytes,
|
||||
"toolOutputMaxLines": tool_output_max_lines,
|
||||
"toolOutputMaxLineLength": tool_output_max_line_length,
|
||||
})
|
||||
}
|
||||
|
||||
fn merge_hermes_io_safety_config(
|
||||
config: &mut serde_yaml::Value,
|
||||
form: &Value,
|
||||
) -> Result<(), String> {
|
||||
let current = build_hermes_io_safety_config_values(config);
|
||||
let file_read_max_chars = validate_hermes_i64(
|
||||
if form.get("fileReadMaxChars").is_some() {
|
||||
form_i64(form, "fileReadMaxChars")
|
||||
} else {
|
||||
Some(current["fileReadMaxChars"].as_i64().unwrap_or(100000))
|
||||
},
|
||||
"file_read_max_chars",
|
||||
100000,
|
||||
1000,
|
||||
1000000,
|
||||
)?;
|
||||
let tool_output_max_bytes = validate_hermes_i64(
|
||||
if form.get("toolOutputMaxBytes").is_some() {
|
||||
form_i64(form, "toolOutputMaxBytes")
|
||||
} else {
|
||||
Some(current["toolOutputMaxBytes"].as_i64().unwrap_or(50000))
|
||||
},
|
||||
"tool_output.max_bytes",
|
||||
50000,
|
||||
1000,
|
||||
1000000,
|
||||
)?;
|
||||
let tool_output_max_lines = validate_hermes_i64(
|
||||
if form.get("toolOutputMaxLines").is_some() {
|
||||
form_i64(form, "toolOutputMaxLines")
|
||||
} else {
|
||||
Some(current["toolOutputMaxLines"].as_i64().unwrap_or(2000))
|
||||
},
|
||||
"tool_output.max_lines",
|
||||
2000,
|
||||
1,
|
||||
100000,
|
||||
)?;
|
||||
let tool_output_max_line_length = validate_hermes_i64(
|
||||
if form.get("toolOutputMaxLineLength").is_some() {
|
||||
form_i64(form, "toolOutputMaxLineLength")
|
||||
} else {
|
||||
Some(current["toolOutputMaxLineLength"].as_i64().unwrap_or(2000))
|
||||
},
|
||||
"tool_output.max_line_length",
|
||||
2000,
|
||||
1,
|
||||
100000,
|
||||
)?;
|
||||
|
||||
let root = ensure_yaml_object(config)?;
|
||||
root.insert(
|
||||
yaml_key("file_read_max_chars"),
|
||||
serde_yaml::Value::Number(file_read_max_chars.into()),
|
||||
);
|
||||
let tool_output = yaml_child_object(root, "tool_output")?;
|
||||
tool_output.insert(
|
||||
yaml_key("max_bytes"),
|
||||
serde_yaml::Value::Number(tool_output_max_bytes.into()),
|
||||
);
|
||||
tool_output.insert(
|
||||
yaml_key("max_lines"),
|
||||
serde_yaml::Value::Number(tool_output_max_lines.into()),
|
||||
);
|
||||
tool_output.insert(
|
||||
yaml_key("max_line_length"),
|
||||
serde_yaml::Value::Number(tool_output_max_line_length.into()),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_hermes_execution_limits_config(
|
||||
config: &mut serde_yaml::Value,
|
||||
form: &Value,
|
||||
@@ -5988,6 +6090,30 @@ pub fn hermes_execution_limits_config_save(form: Value) -> Result<Value, String>
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_io_safety_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_io_safety_config_values(&config),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_io_safety_config_save(form: Value) -> Result<Value, String> {
|
||||
let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?;
|
||||
merge_hermes_io_safety_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_io_safety_config_values(&config),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_terminal_config_read() -> Result<Value, String> {
|
||||
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
|
||||
@@ -11504,6 +11630,101 @@ streaming:
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hermes_io_safety_config_tests {
|
||||
use super::{build_hermes_io_safety_config_values, merge_hermes_io_safety_config};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn io_safety_values_have_upstream_defaults() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap();
|
||||
let values = build_hermes_io_safety_config_values(&config);
|
||||
assert_eq!(values["fileReadMaxChars"], 100000);
|
||||
assert_eq!(values["toolOutputMaxBytes"], 50000);
|
||||
assert_eq!(values["toolOutputMaxLines"], 2000);
|
||||
assert_eq!(values["toolOutputMaxLineLength"], 2000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn io_safety_values_read_yaml_fields() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
file_read_max_chars: 200000
|
||||
tool_output:
|
||||
max_bytes: 150000
|
||||
max_lines: 5000
|
||||
max_line_length: 4000
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let values = build_hermes_io_safety_config_values(&config);
|
||||
assert_eq!(values["fileReadMaxChars"], 200000);
|
||||
assert_eq!(values["toolOutputMaxBytes"], 150000);
|
||||
assert_eq!(values["toolOutputMaxLines"], 5000);
|
||||
assert_eq!(values["toolOutputMaxLineLength"], 4000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_io_safety_config_preserves_unknown_fields() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
model:
|
||||
provider: anthropic
|
||||
file_read_max_chars: 100000
|
||||
tool_output:
|
||||
max_bytes: 50000
|
||||
custom_flag: keep-output
|
||||
streaming:
|
||||
enabled: true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_hermes_io_safety_config(
|
||||
&mut config,
|
||||
&json!({
|
||||
"fileReadMaxChars": "120000",
|
||||
"toolOutputMaxBytes": "80000",
|
||||
"toolOutputMaxLines": "3000",
|
||||
"toolOutputMaxLineLength": "2500",
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config["model"]["provider"].as_str(), Some("anthropic"));
|
||||
assert_eq!(config["streaming"]["enabled"].as_bool(), Some(true));
|
||||
assert_eq!(config["file_read_max_chars"].as_i64(), Some(120000));
|
||||
assert_eq!(config["tool_output"]["max_bytes"].as_i64(), Some(80000));
|
||||
assert_eq!(config["tool_output"]["max_lines"].as_i64(), Some(3000));
|
||||
assert_eq!(
|
||||
config["tool_output"]["max_line_length"].as_i64(),
|
||||
Some(2500)
|
||||
);
|
||||
assert_eq!(
|
||||
config["tool_output"]["custom_flag"].as_str(),
|
||||
Some("keep-output")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_io_safety_config_rejects_invalid_values() {
|
||||
let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
|
||||
let err = merge_hermes_io_safety_config(&mut config, &json!({ "fileReadMaxChars": 999 }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("file_read_max_chars"));
|
||||
let err = merge_hermes_io_safety_config(&mut config, &json!({ "toolOutputMaxBytes": 999 }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("tool_output.max_bytes"));
|
||||
let err = merge_hermes_io_safety_config(&mut config, &json!({ "toolOutputMaxLines": 0 }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("tool_output.max_lines"));
|
||||
let err =
|
||||
merge_hermes_io_safety_config(&mut config, &json!({ "toolOutputMaxLineLength": 0 }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("tool_output.max_line_length"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hermes_terminal_config_tests {
|
||||
use super::{build_hermes_terminal_config_values, merge_hermes_terminal_config};
|
||||
|
||||
@@ -281,6 +281,8 @@ pub fn run() {
|
||||
hermes::hermes_streaming_config_save,
|
||||
hermes::hermes_execution_limits_config_read,
|
||||
hermes::hermes_execution_limits_config_save,
|
||||
hermes::hermes_io_safety_config_read,
|
||||
hermes::hermes_io_safety_config_save,
|
||||
hermes::hermes_terminal_config_read,
|
||||
hermes::hermes_terminal_config_save,
|
||||
hermes::hermes_lazy_deps_features,
|
||||
|
||||
@@ -102,6 +102,13 @@ const EXECUTION_LIMITS_DEFAULTS = {
|
||||
delegationInheritMcpToolsets: true,
|
||||
}
|
||||
|
||||
const IO_SAFETY_DEFAULTS = {
|
||||
fileReadMaxChars: 100000,
|
||||
toolOutputMaxBytes: 50000,
|
||||
toolOutputMaxLines: 2000,
|
||||
toolOutputMaxLineLength: 2000,
|
||||
}
|
||||
|
||||
const TERMINAL_DEFAULTS = {
|
||||
terminalBackend: 'local',
|
||||
terminalCwd: '.',
|
||||
@@ -142,6 +149,7 @@ export function render() {
|
||||
let humanDelayValues = { ...HUMAN_DELAY_DEFAULTS }
|
||||
let streamingValues = { ...STREAMING_DEFAULTS }
|
||||
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
|
||||
let ioSafetyValues = { ...IO_SAFETY_DEFAULTS }
|
||||
let terminalValues = { ...TERMINAL_DEFAULTS }
|
||||
let loading = true
|
||||
let runtimeLoading = true
|
||||
@@ -156,6 +164,7 @@ export function render() {
|
||||
let humanDelayLoading = true
|
||||
let streamingLoading = true
|
||||
let executionLimitsLoading = true
|
||||
let ioSafetyLoading = true
|
||||
let terminalLoading = true
|
||||
let saving = false
|
||||
let runtimeSaving = false
|
||||
@@ -170,6 +179,7 @@ export function render() {
|
||||
let humanDelaySaving = false
|
||||
let streamingSaving = false
|
||||
let executionLimitsSaving = false
|
||||
let ioSafetySaving = false
|
||||
let terminalSaving = false
|
||||
let error = null
|
||||
let runtimeError = null
|
||||
@@ -184,6 +194,7 @@ export function render() {
|
||||
let humanDelayError = null
|
||||
let streamingError = null
|
||||
let executionLimitsError = null
|
||||
let ioSafetyError = null
|
||||
let terminalError = null
|
||||
|
||||
function esc(value) {
|
||||
@@ -195,7 +206,7 @@ export function render() {
|
||||
}
|
||||
|
||||
function isBusy() {
|
||||
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
|
||||
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || terminalSaving
|
||||
}
|
||||
|
||||
function option(labelKey, value, selected) {
|
||||
@@ -777,6 +788,46 @@ export function render() {
|
||||
`
|
||||
}
|
||||
|
||||
function renderIoSafetyPanel() {
|
||||
const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel hm-config-io-safety-panel">
|
||||
<div class="hm-panel-header">
|
||||
<div>
|
||||
<div class="hm-panel-title">${t('engine.hermesIoSafetyTitle')}</div>
|
||||
<div class="hm-channel-panel-desc">${t('engine.hermesIoSafetyDesc')}</div>
|
||||
</div>
|
||||
<div class="hm-panel-actions">
|
||||
<span class="hm-muted">${ioSafetySaving ? t('engine.hermesConfigStatusSaving') : ioSafetyLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesIoSafetyStatusReady')}</span>
|
||||
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-io-safety-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesIoSafetySave')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hm-panel-body">
|
||||
${renderError(ioSafetyError)}
|
||||
<div class="hm-config-runtime-grid hm-config-io-safety-grid">
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesIoSafetyFileReadMaxChars')}</span>
|
||||
<input id="hm-file-read-max-chars" class="hm-input" type="number" inputmode="numeric" min="1000" max="1000000" step="1000" value="${esc(ioSafetyValues.fileReadMaxChars)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesIoSafetyToolOutputMaxBytes')}</span>
|
||||
<input id="hm-tool-output-max-bytes" class="hm-input" type="number" inputmode="numeric" min="1000" max="1000000" step="1000" value="${esc(ioSafetyValues.toolOutputMaxBytes)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesIoSafetyToolOutputMaxLines')}</span>
|
||||
<input id="hm-tool-output-max-lines" class="hm-input" type="number" inputmode="numeric" min="1" max="100000" step="1" value="${esc(ioSafetyValues.toolOutputMaxLines)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesIoSafetyToolOutputMaxLineLength')}</span>
|
||||
<input id="hm-tool-output-max-line-length" class="hm-input" type="number" inputmode="numeric" min="1" max="100000" step="1" value="${esc(ioSafetyValues.toolOutputMaxLineLength)}" ${disabled ? 'disabled' : ''}>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-channel-footnote">${t('engine.hermesIoSafetyFootnote')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderTerminalPanel() {
|
||||
const disabled = loading || saving || terminalLoading || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving
|
||||
return `
|
||||
@@ -866,6 +917,7 @@ export function render() {
|
||||
${renderTerminalPanel()}
|
||||
${renderStreamingPanel()}
|
||||
${renderExecutionLimitsPanel()}
|
||||
${renderIoSafetyPanel()}
|
||||
${renderCompressionPanel()}
|
||||
${renderToolGuardrailsPanel()}
|
||||
${renderMemoryPanel()}
|
||||
@@ -906,6 +958,7 @@ export function render() {
|
||||
el.querySelector('#hm-human-delay-save')?.addEventListener('click', saveHumanDelayConfig)
|
||||
el.querySelector('#hm-streaming-save')?.addEventListener('click', saveStreaming)
|
||||
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
|
||||
el.querySelector('#hm-io-safety-save')?.addEventListener('click', saveIoSafety)
|
||||
el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal)
|
||||
}
|
||||
|
||||
@@ -974,6 +1027,11 @@ export function render() {
|
||||
executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadIoSafety() {
|
||||
const data = await api.hermesIoSafetyConfigRead()
|
||||
ioSafetyValues = { ...IO_SAFETY_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadTerminal() {
|
||||
const data = await api.hermesTerminalConfigRead()
|
||||
terminalValues = { ...TERMINAL_DEFAULTS, ...(data?.values || {}) }
|
||||
@@ -993,6 +1051,7 @@ export function render() {
|
||||
humanDelayLoading = true
|
||||
streamingLoading = true
|
||||
executionLimitsLoading = true
|
||||
ioSafetyLoading = true
|
||||
terminalLoading = true
|
||||
error = null
|
||||
runtimeError = null
|
||||
@@ -1007,6 +1066,7 @@ export function render() {
|
||||
humanDelayError = null
|
||||
streamingError = null
|
||||
executionLimitsError = null
|
||||
ioSafetyError = null
|
||||
terminalError = null
|
||||
draw()
|
||||
try {
|
||||
@@ -1056,6 +1116,14 @@ export function render() {
|
||||
executionLimitsLoading = false
|
||||
draw()
|
||||
}
|
||||
try {
|
||||
await loadIoSafety()
|
||||
} catch (err) {
|
||||
ioSafetyError = humanizeError(err, t('engine.hermesIoSafetyLoadFailed') || 'Load input/output safety config failed')
|
||||
} finally {
|
||||
ioSafetyLoading = false
|
||||
draw()
|
||||
}
|
||||
try {
|
||||
await loadTerminal()
|
||||
} catch (err) {
|
||||
@@ -1177,6 +1245,9 @@ export function render() {
|
||||
try {
|
||||
await loadExecutionLimits()
|
||||
} catch {}
|
||||
try {
|
||||
await loadIoSafety()
|
||||
} catch {}
|
||||
try {
|
||||
await loadTerminal()
|
||||
} catch {}
|
||||
@@ -1537,6 +1608,34 @@ export function render() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveIoSafety() {
|
||||
const form = {
|
||||
fileReadMaxChars: el.querySelector('#hm-file-read-max-chars')?.value || '100000',
|
||||
toolOutputMaxBytes: el.querySelector('#hm-tool-output-max-bytes')?.value || '50000',
|
||||
toolOutputMaxLines: el.querySelector('#hm-tool-output-max-lines')?.value || '2000',
|
||||
toolOutputMaxLineLength: el.querySelector('#hm-tool-output-max-line-length')?.value || '2000',
|
||||
}
|
||||
ioSafetySaving = true
|
||||
ioSafetyError = null
|
||||
draw()
|
||||
try {
|
||||
const result = await api.hermesIoSafetyConfigSave(form)
|
||||
ioSafetyValues = { ...IO_SAFETY_DEFAULTS, ...(result?.values || form) }
|
||||
await refreshRawAfterStructuredSave()
|
||||
const backup = result?.backup || ''
|
||||
toast({
|
||||
message: t('engine.hermesIoSafetySaveSuccess'),
|
||||
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
|
||||
}, 'success')
|
||||
} catch (err) {
|
||||
ioSafetyError = humanizeError(err, t('engine.hermesIoSafetySaveFailed') || 'Save input/output safety config failed')
|
||||
toast(ioSafetyError, 'error')
|
||||
} finally {
|
||||
ioSafetySaving = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTerminal() {
|
||||
const form = {
|
||||
terminalBackend: el.querySelector('#hm-terminal-backend')?.value || 'local',
|
||||
|
||||
@@ -533,6 +533,8 @@ export const api = {
|
||||
hermesStreamingConfigSave: (form) => invoke('hermes_streaming_config_save', { form }),
|
||||
hermesExecutionLimitsConfigRead: () => invoke('hermes_execution_limits_config_read'),
|
||||
hermesExecutionLimitsConfigSave: (form) => invoke('hermes_execution_limits_config_save', { form }),
|
||||
hermesIoSafetyConfigRead: () => invoke('hermes_io_safety_config_read'),
|
||||
hermesIoSafetyConfigSave: (form) => invoke('hermes_io_safety_config_save', { form }),
|
||||
hermesTerminalConfigRead: () => invoke('hermes_terminal_config_read'),
|
||||
hermesTerminalConfigSave: (form) => invoke('hermes_terminal_config_save', { form }),
|
||||
hermesLazyDepsFeatures: () => cachedInvoke('hermes_lazy_deps_features', {}, 600000),
|
||||
|
||||
@@ -564,6 +564,18 @@ export default {
|
||||
hermesExecutionLimitsDelegationInheritMcp: _('保留父任务 MCP 工具集', 'Inherit parent MCP toolsets', '保留父任務 MCP 工具集'),
|
||||
hermesExecutionLimitsDelegationAutoApprove: _('自动批准子任务危险命令', 'Auto-approve child dangerous commands', '自動批准子任務危險命令'),
|
||||
hermesExecutionLimitsFootnote: _('默认会拒绝子任务危险命令审批,适合交互式和长跑任务。只有在完全信任无人值守环境时才开启自动批准。', 'By default, dangerous-command approvals from child agents are auto-denied, which fits interactive and long-running tasks. Enable auto-approval only in fully trusted unattended environments.', '預設會拒絕子任務危險命令審批,適合互動式和長跑任務。只有在完全信任無人值守環境時才啟用自動批准。'),
|
||||
hermesIoSafetyTitle: _('输入输出保护', 'Input and output safety', '輸入輸出保護'),
|
||||
hermesIoSafetyDesc: _('限制单次文件读取和工具输出体量,避免大文件或长日志一次性挤爆上下文。', 'Limit single file reads and tool output size so large files or long logs do not flood the context.', '限制單次檔案讀取和工具輸出體量,避免大型檔案或長日誌一次性擠爆上下文。'),
|
||||
hermesIoSafetyStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
hermesIoSafetySave: _('保存保护配置', 'Save safety limits', '儲存保護設定'),
|
||||
hermesIoSafetySaveSuccess: _('输入输出保护已保存,建议重启 Hermes Gateway 生效', 'Input/output safety limits saved. Restart Hermes Gateway to take effect.', '輸入輸出保護已儲存,建議重啟 Hermes Gateway 生效'),
|
||||
hermesIoSafetyLoadFailed: _('加载输入输出保护失败', 'Load input/output safety failed', '載入輸入輸出保護失敗'),
|
||||
hermesIoSafetySaveFailed: _('保存输入输出保护失败', 'Save input/output safety failed', '儲存輸入輸出保護失敗'),
|
||||
hermesIoSafetyFileReadMaxChars: _('单次文件读取字符上限', 'File read character cap', '單次檔案讀取字元上限'),
|
||||
hermesIoSafetyToolOutputMaxBytes: _('终端输出字符上限', 'Terminal output character cap', '終端輸出字元上限'),
|
||||
hermesIoSafetyToolOutputMaxLines: _('文件分页最大行数', 'File page line cap', '檔案分頁最大行數'),
|
||||
hermesIoSafetyToolOutputMaxLineLength: _('单行显示字符上限', 'Per-line character cap', '單行顯示字元上限'),
|
||||
hermesIoSafetyFootnote: _('默认值适合大多数模型;小上下文模型可降低这些上限。其他 tool_output 高级字段会保留在 raw YAML 中。', 'Defaults fit most models. Lower these limits for small-context models. Other advanced tool_output fields are preserved in raw YAML.', '預設值適合多數模型;小上下文模型可降低這些上限。其他 tool_output 進階欄位會保留在 raw YAML 中。'),
|
||||
hermesCompressionTitle: _('上下文压缩', 'Context compression', '上下文壓縮'),
|
||||
hermesCompressionDesc: _('控制长对话何时触发压缩、压缩目标和保留范围,降低上下文过长导致的失败与费用浪费。', 'Control when long conversations are compressed, the target size, and protected message ranges to reduce failures and wasted cost from oversized context.', '控制長對話何時觸發壓縮、壓縮目標和保留範圍,降低上下文過長導致的失敗與費用浪費。'),
|
||||
hermesCompressionStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
|
||||
@@ -138,6 +138,18 @@ test('Hermes 配置页会暴露执行与委派限制结构化配置字段', () =
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 配置页会暴露输入输出保护结构化配置字段', () => {
|
||||
for (const id of [
|
||||
'hm-io-safety-save',
|
||||
'hm-file-read-max-chars',
|
||||
'hm-tool-output-max-bytes',
|
||||
'hm-tool-output-max-lines',
|
||||
'hm-tool-output-max-line-length',
|
||||
]) {
|
||||
assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 配置页会暴露终端执行结构化配置字段', () => {
|
||||
for (const id of [
|
||||
'hm-terminal-save',
|
||||
|
||||
78
tests/hermes-io-safety-config.test.js
Normal file
78
tests/hermes-io-safety-config.test.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
buildHermesIoSafetyConfigValues,
|
||||
mergeHermesIoSafetyConfig,
|
||||
} from '../scripts/dev-api.js'
|
||||
|
||||
test('Hermes 输入输出保护读取会提供上游默认值', () => {
|
||||
const values = buildHermesIoSafetyConfigValues({})
|
||||
|
||||
assert.deepEqual(values, {
|
||||
fileReadMaxChars: 100000,
|
||||
toolOutputMaxBytes: 50000,
|
||||
toolOutputMaxLines: 2000,
|
||||
toolOutputMaxLineLength: 2000,
|
||||
})
|
||||
})
|
||||
|
||||
test('Hermes 输入输出保护读取会回显 YAML 字段', () => {
|
||||
const values = buildHermesIoSafetyConfigValues({
|
||||
file_read_max_chars: 200000,
|
||||
tool_output: {
|
||||
max_bytes: 150000,
|
||||
max_lines: 5000,
|
||||
max_line_length: 4000,
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(values.fileReadMaxChars, 200000)
|
||||
assert.equal(values.toolOutputMaxBytes, 150000)
|
||||
assert.equal(values.toolOutputMaxLines, 5000)
|
||||
assert.equal(values.toolOutputMaxLineLength, 4000)
|
||||
})
|
||||
|
||||
test('Hermes 输入输出保护保存会保留未知字段并写入上游结构', () => {
|
||||
const next = mergeHermesIoSafetyConfig({
|
||||
model: { provider: 'anthropic' },
|
||||
file_read_max_chars: 100000,
|
||||
tool_output: {
|
||||
max_bytes: 50000,
|
||||
custom_flag: 'keep-output',
|
||||
},
|
||||
streaming: { enabled: true },
|
||||
}, {
|
||||
fileReadMaxChars: '120000',
|
||||
toolOutputMaxBytes: '80000',
|
||||
toolOutputMaxLines: '3000',
|
||||
toolOutputMaxLineLength: '2500',
|
||||
})
|
||||
|
||||
assert.deepEqual(next.model, { provider: 'anthropic' })
|
||||
assert.deepEqual(next.streaming, { enabled: true })
|
||||
assert.equal(next.file_read_max_chars, 120000)
|
||||
assert.equal(next.tool_output.max_bytes, 80000)
|
||||
assert.equal(next.tool_output.max_lines, 3000)
|
||||
assert.equal(next.tool_output.max_line_length, 2500)
|
||||
assert.equal(next.tool_output.custom_flag, 'keep-output')
|
||||
})
|
||||
|
||||
test('Hermes 输入输出保护保存会拒绝越界值', () => {
|
||||
assert.throws(
|
||||
() => mergeHermesIoSafetyConfig({}, { fileReadMaxChars: '999' }),
|
||||
/file_read_max_chars/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesIoSafetyConfig({}, { toolOutputMaxBytes: '999' }),
|
||||
/tool_output\.max_bytes/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesIoSafetyConfig({}, { toolOutputMaxLines: '0' }),
|
||||
/tool_output\.max_lines/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesIoSafetyConfig({}, { toolOutputMaxLineLength: '0' }),
|
||||
/tool_output\.max_line_length/,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user