feat(hermes): add tirith security settings

This commit is contained in:
晴天
2026-05-24 22:08:12 +08:00
parent 7be0ec66cc
commit b2146b54cb
8 changed files with 456 additions and 3 deletions

View File

@@ -56,6 +56,13 @@ const UNAUTHORIZED_DM_DEFAULTS = {
unauthorizedDmBehavior: 'pair',
}
const SECURITY_DEFAULTS = {
tirithEnabled: true,
tirithPath: 'tirith',
tirithTimeout: 5,
tirithFailOpen: true,
}
const STREAMING_DEFAULTS = {
enabled: false,
transport: 'edit',
@@ -109,6 +116,7 @@ export function render() {
let skillsValues = { ...SKILLS_DEFAULTS }
let quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS }
let unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS }
let securityValues = { ...SECURITY_DEFAULTS }
let streamingValues = { ...STREAMING_DEFAULTS }
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
let terminalValues = { ...TERMINAL_DEFAULTS }
@@ -120,6 +128,7 @@ export function render() {
let skillsLoading = true
let quickCommandsLoading = true
let unauthorizedDmLoading = true
let securityLoading = true
let streamingLoading = true
let executionLimitsLoading = true
let terminalLoading = true
@@ -131,6 +140,7 @@ export function render() {
let skillsSaving = false
let quickCommandsSaving = false
let unauthorizedDmSaving = false
let securitySaving = false
let streamingSaving = false
let executionLimitsSaving = false
let terminalSaving = false
@@ -142,6 +152,7 @@ export function render() {
let skillsError = null
let quickCommandsError = null
let unauthorizedDmError = null
let securityError = null
let streamingError = null
let executionLimitsError = null
let terminalError = null
@@ -155,7 +166,7 @@ export function render() {
}
function isBusy() {
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
}
function option(labelKey, value, selected) {
@@ -440,7 +451,7 @@ export function render() {
}
function renderUnauthorizedDmConfigPanel() {
const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-unauthorized-dm-panel">
<div class="hm-panel-header">
@@ -469,8 +480,50 @@ export function render() {
`
}
function renderSecurityConfigPanel() {
const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-security-panel">
<div class="hm-panel-header">
<div>
<div class="hm-panel-title">${t('engine.hermesSecurityConfigTitle')}</div>
<div class="hm-channel-panel-desc">${t('engine.hermesSecurityConfigDesc')}</div>
</div>
<div class="hm-panel-actions">
<span class="hm-muted">${securitySaving ? t('engine.hermesConfigStatusSaving') : securityLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesSecurityConfigStatusReady')}</span>
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-security-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesSecurityConfigSave')}</button>
</div>
</div>
<div class="hm-panel-body">
${renderError(securityError)}
<div class="hm-config-check-grid">
<label class="hm-channel-check">
<input id="hm-security-tirith-enabled" type="checkbox" ${securityValues.tirithEnabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesSecurityConfigTirithEnabled')}</span>
</label>
<label class="hm-channel-check hm-channel-check--danger">
<input id="hm-security-tirith-fail-open" type="checkbox" ${securityValues.tirithFailOpen ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${t('engine.hermesSecurityConfigTirithFailOpen')}</span>
</label>
</div>
<div class="hm-config-runtime-grid hm-config-security-grid">
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesSecurityConfigTirithPath')}</span>
<input id="hm-security-tirith-path" class="hm-input" value="${esc(securityValues.tirithPath)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesSecurityConfigTirithTimeout')}</span>
<input id="hm-security-tirith-timeout" class="hm-input" type="number" inputmode="numeric" min="1" max="300" step="1" value="${esc(securityValues.tirithTimeout)}" ${disabled ? 'disabled' : ''}>
</label>
</div>
<div class="hm-channel-footnote">${t('engine.hermesSecurityConfigFootnote')}</div>
</div>
</div>
`
}
function renderStreamingPanel() {
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || executionLimitsSaving || terminalSaving
const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || terminalSaving
return `
<div class="hm-panel hm-config-runtime-panel hm-config-streaming-panel">
<div class="hm-panel-header">
@@ -688,6 +741,7 @@ export function render() {
${renderSkillsConfigPanel()}
${renderQuickCommandsConfigPanel()}
${renderUnauthorizedDmConfigPanel()}
${renderSecurityConfigPanel()}
<div class="hm-panel">
<div class="hm-panel-header">
@@ -714,6 +768,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-unauthorized-dm-save')?.addEventListener('click', saveUnauthorizedDmConfig)
el.querySelector('#hm-security-save')?.addEventListener('click', saveSecurityConfig)
el.querySelector('#hm-streaming-save')?.addEventListener('click', saveStreaming)
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal)
@@ -759,6 +814,11 @@ export function render() {
unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS, ...(data?.values || {}) }
}
async function loadSecurityConfig() {
const data = await api.hermesSecurityConfigRead()
securityValues = { ...SECURITY_DEFAULTS, ...(data?.values || {}) }
}
async function loadStreaming() {
const data = await api.hermesStreamingConfigRead()
streamingValues = { ...STREAMING_DEFAULTS, ...(data?.values || {}) }
@@ -783,6 +843,7 @@ export function render() {
skillsLoading = true
quickCommandsLoading = true
unauthorizedDmLoading = true
securityLoading = true
streamingLoading = true
executionLimitsLoading = true
terminalLoading = true
@@ -794,6 +855,7 @@ export function render() {
skillsError = null
quickCommandsError = null
unauthorizedDmError = null
securityError = null
streamingError = null
executionLimitsError = null
terminalError = null
@@ -885,6 +947,14 @@ export function render() {
unauthorizedDmLoading = false
draw()
}
try {
await loadSecurityConfig()
} catch (err) {
securityError = humanizeError(err, t('engine.hermesSecurityConfigLoadFailed') || 'Load security config failed')
} finally {
securityLoading = false
draw()
}
}
async function refreshRawAfterStructuredSave() {
@@ -927,6 +997,9 @@ export function render() {
try {
await loadUnauthorizedDmConfig()
} catch {}
try {
await loadSecurityConfig()
} catch {}
try {
await loadStreaming()
} catch {}
@@ -1142,6 +1215,34 @@ export function render() {
}
}
async function saveSecurityConfig() {
const form = {
tirithEnabled: !!el.querySelector('#hm-security-tirith-enabled')?.checked,
tirithPath: el.querySelector('#hm-security-tirith-path')?.value || 'tirith',
tirithTimeout: el.querySelector('#hm-security-tirith-timeout')?.value || '5',
tirithFailOpen: !!el.querySelector('#hm-security-tirith-fail-open')?.checked,
}
securitySaving = true
securityError = null
draw()
try {
const result = await api.hermesSecurityConfigSave(form)
securityValues = { ...SECURITY_DEFAULTS, ...(result?.values || form) }
await refreshRawAfterStructuredSave()
const backup = result?.backup || ''
toast({
message: t('engine.hermesSecurityConfigSaveSuccess'),
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
}, 'success')
} catch (err) {
securityError = humanizeError(err, t('engine.hermesSecurityConfigSaveFailed') || 'Save security config failed')
toast(securityError, 'error')
} finally {
securitySaving = false
draw()
}
}
async function saveStreaming() {
const form = {
enabled: !!el.querySelector('#hm-streaming-enabled')?.checked,

View File

@@ -523,6 +523,8 @@ export const api = {
hermesQuickCommandsConfigSave: (form) => invoke('hermes_quick_commands_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'),
hermesSecurityConfigSave: (form) => invoke('hermes_security_config_save', { form }),
hermesStreamingConfigRead: () => invoke('hermes_streaming_config_read'),
hermesStreamingConfigSave: (form) => invoke('hermes_streaming_config_save', { form }),
hermesExecutionLimitsConfigRead: () => invoke('hermes_execution_limits_config_read'),

View File

@@ -640,6 +640,18 @@ export default {
hermesUnauthorizedDmConfigBehavior_pair: _('回复配对码', 'Reply with pairing code', '回覆配對碼'),
hermesUnauthorizedDmConfigBehavior_ignore: _('静默忽略', 'Silently ignore', '靜默忽略'),
hermesUnauthorizedDmConfigFootnote: _('pair 是默认值会拒绝访问但在私信中回复一次性配对码ignore 会静默丢弃陌生私信。平台级覆盖仍可在渠道配置或 raw YAML 中单独设置。', 'pair is the default: Hermes denies access but replies with a one-time pairing code in DMs. ignore silently drops unknown DMs. Platform-level overrides can still be set in channel settings or raw YAML.', 'pair 是預設值會拒絕存取但在私訊中回覆一次性配對碼ignore 會靜默丟棄陌生私訊。平台級覆蓋仍可在頻道設定或 raw YAML 中單獨設定。'),
hermesSecurityConfigTitle: _('Tirith 安全扫描', 'Tirith security scanning', 'Tirith 安全掃描'),
hermesSecurityConfigDesc: _('控制终端命令执行前的 Tirith 内容扫描,拦截明显的 URL 伪装、管道执行和注入风险。', 'Control Tirith content scanning before terminal commands run to catch obvious URL spoofing, pipe-to-shell, and injection risks.', '控制終端命令執行前的 Tirith 內容掃描,攔截明顯的 URL 偽裝、管道執行和注入風險。'),
hermesSecurityConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesSecurityConfigSave: _('保存安全设置', 'Save security settings', '儲存安全設定'),
hermesSecurityConfigSaveSuccess: _('Tirith 安全扫描配置已保存,建议重启 Hermes Gateway 生效', 'Tirith security settings saved. Restart Hermes Gateway to take effect.', 'Tirith 安全掃描設定已儲存,建議重啟 Hermes Gateway 生效'),
hermesSecurityConfigLoadFailed: _('加载 Tirith 安全扫描配置失败', 'Load Tirith security settings failed', '載入 Tirith 安全掃描設定失敗'),
hermesSecurityConfigSaveFailed: _('保存 Tirith 安全扫描配置失败', 'Save Tirith security settings failed', '儲存 Tirith 安全掃描設定失敗'),
hermesSecurityConfigTirithEnabled: _('启用 Tirith 扫描', 'Enable Tirith scanning', '啟用 Tirith 掃描'),
hermesSecurityConfigTirithPath: _('Tirith 可执行文件路径', 'Tirith executable path', 'Tirith 可執行檔路徑'),
hermesSecurityConfigTirithTimeout: _('扫描超时(秒)', 'Scan timeout (s)', '掃描逾時(秒)'),
hermesSecurityConfigTirithFailOpen: _('Tirith 不可用时放行', 'Allow when Tirith is unavailable', 'Tirith 不可用時放行'),
hermesSecurityConfigFootnote: _('启用后Hermes 会在终端命令执行前调用 Tirith 进行内容级安全扫描Tirith 不可用时是否放行取决于 fail-open。Windows 平台通常会静默跳过 Tirith真实执行能力仍受宿主环境影响。', 'When enabled, Hermes runs Tirith before terminal commands for content-level scanning. Whether commands pass when Tirith is unavailable depends on fail-open. On Windows, Tirith is usually skipped silently, and actual execution still depends on the host environment.', '啟用後Hermes 會在終端命令執行前呼叫 Tirith 進行內容級安全掃描Tirith 不可用時是否放行取決於 fail-open。Windows 平台通常會靜默跳過 Tirith真實執行能力仍受主機環境影響。'),
// Batch 1 §E: 会话导出
sessionsExport: _('导出', 'Export', '匯出'),
sessionsExportSuccess: _('已导出', 'Exported', '已匯出'),