From 68320496b33f4d3c1f590cdeeb31b8c61fad5c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Wed, 27 May 2026 03:39:06 +0800 Subject: [PATCH] feat(hermes): add terminal env passthrough controls --- scripts/dev-api.js | 4 ++ src-tauri/src/commands/hermes.rs | 84 ++++++++++++++++++++++++++++ src/engines/hermes/pages/config.js | 6 ++ src/locales/modules/engine.js | 3 +- tests/hermes-config-page-ui.test.js | 1 + tests/hermes-terminal-config.test.js | 24 ++++++++ 6 files changed, 121 insertions(+), 1 deletion(-) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 087e879..36ad220 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -5520,6 +5520,7 @@ export function buildHermesTerminalConfigValues(config = {}) { terminalShellInitFiles: normalizeHermesShellInitFileList(terminal.shell_init_files || [], 'terminal.shell_init_files').join('\n'), terminalAutoSourceBashrc: readHermesBool(terminal.auto_source_bashrc, true), terminalPersistentShell: readHermesBool(terminal.persistent_shell, true), + terminalEnvPassthrough: normalizeHermesEnvNameList(terminal.env_passthrough || [], 'terminal.env_passthrough').join('\n'), terminalDockerMountCwdToWorkspace: readHermesBool(terminal.docker_mount_cwd_to_workspace, false), terminalDockerRunAsHostUser: readHermesBool(terminal.docker_run_as_host_user, false), terminalDockerImage: typeof terminal.docker_image === 'string' ? terminal.docker_image.trim() : '', @@ -5553,6 +5554,9 @@ export function mergeHermesTerminalConfig(config = {}, form = {}) { else delete terminal.shell_init_files terminal.auto_source_bashrc = formHermesBool(form, 'terminalAutoSourceBashrc', currentValues.terminalAutoSourceBashrc) terminal.persistent_shell = formHermesBool(form, 'terminalPersistentShell', currentValues.terminalPersistentShell) + const envPassthrough = normalizeHermesEnvNameList(Object.hasOwn(form, 'terminalEnvPassthrough') ? form.terminalEnvPassthrough : currentValues.terminalEnvPassthrough, 'terminal.env_passthrough') + if (envPassthrough.length) terminal.env_passthrough = envPassthrough + else delete terminal.env_passthrough terminal.docker_mount_cwd_to_workspace = formHermesBool(form, 'terminalDockerMountCwdToWorkspace', currentValues.terminalDockerMountCwdToWorkspace) terminal.docker_run_as_host_user = formHermesBool(form, 'terminalDockerRunAsHostUser', currentValues.terminalDockerRunAsHostUser) for (const [formKey, yamlKey] of [ diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 3798d6a..03d05ae 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -8646,6 +8646,9 @@ fn build_hermes_terminal_config_values(config: &serde_yaml::Value) -> Value { let terminal_persistent_shell = terminal .and_then(|map| yaml_bool_field(map, "persistent_shell")) .unwrap_or(true); + let terminal_env_passthrough = terminal + .map(|map| yaml_string_sequence_field(map, "env_passthrough").join("\n")) + .unwrap_or_default(); let terminal_docker_mount_cwd_to_workspace = terminal .and_then(|map| yaml_bool_field(map, "docker_mount_cwd_to_workspace")) .unwrap_or(false); @@ -8686,6 +8689,7 @@ fn build_hermes_terminal_config_values(config: &serde_yaml::Value) -> Value { "terminalShellInitFiles": terminal_shell_init_files, "terminalAutoSourceBashrc": terminal_auto_source_bashrc, "terminalPersistentShell": terminal_persistent_shell, + "terminalEnvPassthrough": terminal_env_passthrough, "terminalDockerMountCwdToWorkspace": terminal_docker_mount_cwd_to_workspace, "terminalDockerRunAsHostUser": terminal_docker_run_as_host_user, "terminalDockerImage": terminal_docker_image, @@ -8768,6 +8772,14 @@ fn merge_hermes_terminal_config( }); let terminal_persistent_shell = form_bool(form, "terminalPersistentShell") .unwrap_or_else(|| current["terminalPersistentShell"].as_bool().unwrap_or(true)); + let terminal_env_passthrough = normalize_hermes_env_name_list( + form_string(form, "terminalEnvPassthrough").or_else(|| { + current["terminalEnvPassthrough"] + .as_str() + .map(ToString::to_string) + }), + "terminal.env_passthrough", + )?; let terminal_docker_mount_cwd_to_workspace = form_bool(form, "terminalDockerMountCwdToWorkspace").unwrap_or_else(|| { current["terminalDockerMountCwdToWorkspace"] @@ -8926,6 +8938,19 @@ fn merge_hermes_terminal_config( yaml_key("persistent_shell"), serde_yaml::Value::Bool(terminal_persistent_shell), ); + if terminal_env_passthrough.is_empty() { + terminal.remove(yaml_key("env_passthrough")); + } else { + terminal.insert( + yaml_key("env_passthrough"), + serde_yaml::Value::Sequence( + terminal_env_passthrough + .into_iter() + .map(serde_yaml::Value::String) + .collect(), + ), + ); + } terminal.insert( yaml_key("docker_mount_cwd_to_workspace"), serde_yaml::Value::Bool(terminal_docker_mount_cwd_to_workspace), @@ -17849,6 +17874,7 @@ mod hermes_terminal_config_tests { assert_eq!(values["terminalShellInitFiles"], ""); assert_eq!(values["terminalAutoSourceBashrc"], true); assert_eq!(values["terminalPersistentShell"], true); + assert_eq!(values["terminalEnvPassthrough"], ""); assert_eq!(values["terminalDockerMountCwdToWorkspace"], false); assert_eq!(values["terminalDockerRunAsHostUser"], false); assert_eq!(values["terminalContainerCpu"], 1); @@ -17880,6 +17906,9 @@ terminal: - ${HOME}/.config/hermes/env.sh auto_source_bashrc: false persistent_shell: false + env_passthrough: + - OPENROUTER_API_KEY + - GITHUB_TOKEN docker_mount_cwd_to_workspace: true docker_run_as_host_user: true docker_image: nikolaik/python-nodejs:python3.11-nodejs20 @@ -17911,6 +17940,10 @@ terminal: ); assert_eq!(values["terminalAutoSourceBashrc"], false); assert_eq!(values["terminalPersistentShell"], false); + assert_eq!( + values["terminalEnvPassthrough"], + "OPENROUTER_API_KEY\nGITHUB_TOKEN" + ); assert_eq!(values["terminalDockerMountCwdToWorkspace"], true); assert_eq!(values["terminalDockerRunAsHostUser"], true); assert_eq!( @@ -17947,6 +17980,8 @@ terminal: backend: local shell_init_files: - ~/.profile + env_passthrough: + - OLD_TOKEN docker_image: custom/python-node docker_forward_env: - OLD_TOKEN @@ -17967,6 +18002,7 @@ streaming: "terminalShellInitFiles": "~/.zshrc\n${HOME}/.config/hermes/env.sh\n~/.zshrc", "terminalAutoSourceBashrc": false, "terminalPersistentShell": false, + "terminalEnvPassthrough": "OPENROUTER_API_KEY\nGITHUB_TOKEN\nOPENROUTER_API_KEY", "terminalDockerMountCwdToWorkspace": true, "terminalDockerRunAsHostUser": true, "terminalDockerImage": "nikolaik/python-nodejs:python3.12-nodejs22", @@ -18015,6 +18051,21 @@ streaming: config["terminal"]["persistent_shell"].as_bool(), Some(false) ); + assert_eq!( + config["terminal"]["env_passthrough"][0].as_str(), + Some("OPENROUTER_API_KEY") + ); + assert_eq!( + config["terminal"]["env_passthrough"][1].as_str(), + Some("GITHUB_TOKEN") + ); + assert_eq!( + config["terminal"]["env_passthrough"] + .as_sequence() + .unwrap() + .len(), + 2 + ); assert_eq!( config["terminal"]["docker_mount_cwd_to_workspace"].as_bool(), Some(true) @@ -18131,6 +18182,33 @@ terminal: ); } + #[test] + fn merge_terminal_config_removes_empty_env_passthrough() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +terminal: + env_passthrough: + - OPENROUTER_API_KEY + custom_flag: keep-terminal +"#, + ) + .unwrap(); + + merge_hermes_terminal_config( + &mut config, + &json!({ + "terminalEnvPassthrough": " \n", + }), + ) + .unwrap(); + + assert!(config["terminal"]["env_passthrough"].is_null()); + assert_eq!( + config["terminal"]["custom_flag"].as_str(), + Some("keep-terminal") + ); + } + #[test] fn merge_terminal_config_removes_empty_images() { let mut config: serde_yaml::Value = serde_yaml::from_str( @@ -18240,6 +18318,12 @@ terminal: ) .unwrap_err(); assert!(err.contains("terminal.shell_init_files")); + let err = merge_hermes_terminal_config( + &mut config, + &json!({ "terminalEnvPassthrough": "GOOD_TOKEN\nBAD TOKEN" }), + ) + .unwrap_err(); + assert!(err.contains("terminal.env_passthrough")); } } diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index 354ab4d..3d92a75 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -311,6 +311,7 @@ const TERMINAL_DEFAULTS = { terminalShellInitFiles: '', terminalAutoSourceBashrc: true, terminalPersistentShell: true, + terminalEnvPassthrough: '', terminalDockerMountCwdToWorkspace: false, terminalDockerRunAsHostUser: false, terminalDockerImage: '', @@ -2309,6 +2310,10 @@ export function render() { ${t('engine.hermesTerminalConfigShellInitFiles')} +