feat(hermes): add input output safety settings

This commit is contained in:
晴天
2026-05-24 23:33:03 +08:00
parent d245d2e320
commit 459811b188
8 changed files with 476 additions and 1 deletions

View File

@@ -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',

View File

@@ -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),

View File

@@ -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', '結構化設定'),