Merge pull request #84903 from NousResearch/salv/41236

feat(desktop): auto-detect Linux keychain backend for secure token storage (salvage #41236)
This commit is contained in:
brooklyn! 2026-08-12 19:57:09 -05:00 committed by GitHub
commit 0c3f60fe4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 408 additions and 12 deletions

View File

@ -6,7 +6,8 @@ import {
bundledRuntimeImportCheck,
detectRemoteDisplay,
isWindowsBinaryPathInWsl,
isWslEnvironment
isWslEnvironment,
resolveLinuxPasswordStore
} from './bootstrap-platform'
test('isWslEnvironment detects WSL2 env vars on linux', () => {
@ -84,3 +85,41 @@ test('detectRemoteDisplay honors the HERMES_DESKTOP_DISABLE_GPU override both wa
null
)
})
test('resolveLinuxPasswordStore applies known backends on linux', () => {
for (const store of ['gnome-libsecret', 'kwallet', 'kwallet5', 'kwallet6', 'basic']) {
assert.deepEqual(
resolveLinuxPasswordStore({ env: { HERMES_DESKTOP_PASSWORD_STORE: store }, platform: 'linux' }),
{ store, warning: null }
)
}
})
test('resolveLinuxPasswordStore is a no-op when the env var is unset', () => {
assert.deepEqual(resolveLinuxPasswordStore({ env: {}, platform: 'linux' }), { store: null, warning: null })
assert.deepEqual(
resolveLinuxPasswordStore({ env: { HERMES_DESKTOP_PASSWORD_STORE: ' ' }, platform: 'linux' }),
{ store: null, warning: null }
)
})
test('resolveLinuxPasswordStore ignores the env var off linux', () => {
assert.deepEqual(
resolveLinuxPasswordStore({ env: { HERMES_DESKTOP_PASSWORD_STORE: 'gnome-libsecret' }, platform: 'darwin' }),
{ store: null, warning: null }
)
assert.deepEqual(
resolveLinuxPasswordStore({ env: { HERMES_DESKTOP_PASSWORD_STORE: 'kwallet6' }, platform: 'win32' }),
{ store: null, warning: null }
)
})
test('resolveLinuxPasswordStore warns on unknown values instead of applying them', () => {
const result = resolveLinuxPasswordStore({
env: { HERMES_DESKTOP_PASSWORD_STORE: 'keychain-of-wonders' },
platform: 'linux'
})
assert.equal(result.store, null)
assert.match(String(result.warning), /keychain-of-wonders/)
})

View File

@ -107,4 +107,44 @@ function detectRemoteDisplay(options: { env?: NodeJS.ProcessEnv; platform?: Node
return null
}
export { bundledRuntimeImportCheck, detectRemoteDisplay, isWindowsBinaryPathInWsl, isWslEnvironment }
const LINUX_PASSWORD_STORES = new Set(['gnome-libsecret', 'kwallet', 'kwallet5', 'kwallet6', 'basic'])
/**
* Resolve the Chromium `--password-store` switch for Linux safeStorage.
*
* Without the switch Chromium often fails to pick a keychain backend when the
* app is launched outside a full desktop session, safeStorage reports
* encryption as unavailable, and hardening.ts refuses to persist remote
* gateway tokens. The `hermes desktop` launcher detects the session keychain
* (or reads `desktop.password_store` from config.yaml) and bridges the value
* in via HERMES_DESKTOP_PASSWORD_STORE.
*
* Returns `{ store, warning }`: `store` is the validated backend to apply (or
* null to leave Chromium's default), `warning` is a message to log for
* unrecognized values. Pure + dependency-free so it can be unit-tested and
* called before app ready.
*/
function resolveLinuxPasswordStore(options: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {}) {
const env = options.env ?? process.env
const platform = options.platform ?? process.platform
const requested = String(env.HERMES_DESKTOP_PASSWORD_STORE || '').trim()
if (platform !== 'linux' || !requested) {
return { store: null, warning: null }
}
if (!LINUX_PASSWORD_STORES.has(requested)) {
return { store: null, warning: `ignoring unknown HERMES_DESKTOP_PASSWORD_STORE value: ${requested}` }
}
return { store: requested, warning: null }
}
export {
bundledRuntimeImportCheck,
detectRemoteDisplay,
isWindowsBinaryPathInWsl,
isWslEnvironment,
resolveLinuxPasswordStore
}

View File

@ -46,7 +46,7 @@ import {
} from './backend-probes'
import { waitForDashboardPortAnnouncement } from './backend-ready'
import { shouldLatchBackendStartFailure, shouldLatchRemoteReauthFailure } from './backend-start-failure'
import { detectRemoteDisplay, isWindowsBinaryPathInWsl, isWslEnvironment } from './bootstrap-platform'
import { detectRemoteDisplay, isWindowsBinaryPathInWsl, isWslEnvironment, resolveLinuxPasswordStore } from './bootstrap-platform'
import { decideBootstrapRepair } from './bootstrap-repair-guard'
import { runBootstrap } from './bootstrap-runner'
import { applyConnectionChange, resolveTerminalConnection } from './connection-apply'
@ -334,6 +334,22 @@ if (IS_WSL && !REMOTE_DISPLAY_REASON && fs.existsSync('/dev/dxg')) {
console.log('[hermes] WSL GPU passthrough (/dev/dxg) detected; enabling GPU acceleration')
}
// Linux: point Chromium at the session's keychain backend so safeStorage can
// encrypt remote gateway tokens (hardening.ts refuses to persist them without
// it). The value arrives via HERMES_DESKTOP_PASSWORD_STORE, bridged by the
// `hermes desktop` launcher from detection or `desktop.password_store` in
// config.yaml. Must run before app `ready` — the switch only applies pre-launch.
const PASSWORD_STORE = resolveLinuxPasswordStore()
if (PASSWORD_STORE.warning) {
console.warn(`[hermes] ${PASSWORD_STORE.warning}`)
}
if (PASSWORD_STORE.store) {
app.commandLine.appendSwitch('password-store', PASSWORD_STORE.store)
console.log(`[hermes] using password-store backend: ${PASSWORD_STORE.store}`)
}
// Windows sandbox / GPU breakpoint crash recovery (#38216).
//
// Some hosts (AMD RX 6000 drivers, orphan AppContainer SIDs under %LOCALAPPDATA%,

View File

@ -0,0 +1 @@
hsearcy

View File

@ -3221,6 +3221,17 @@ DEFAULT_CONFIG = {
# false - always keep GPU acceleration on, even over a remote display.
# Bridged to the HERMES_DESKTOP_DISABLE_GPU env var the Electron app reads.
"disable_gpu": "auto",
# Linux keychain backend for secure token storage (Chromium's
# --password-store switch, which safeStorage needs before it can
# encrypt remote gateway tokens):
# "auto" - detect the session keychain: KWallet via KDE session env
# vars, GNOME Keyring / any org.freedesktop.secrets
# provider (e.g. KeePassXC) via D-Bus (default).
# "gnome-libsecret" / "kwallet" / "kwallet5" / "kwallet6" / "basic"
# - force a specific backend ("basic" = unencrypted store).
# Ignored on macOS/Windows. Bridged to the HERMES_DESKTOP_PASSWORD_STORE
# env var the Electron app reads, so an explicit env var still wins.
"password_store": "auto",
# macOS only: optional persistent code-signing identity (a cert in the
# login keychain — a self-signed "Code Signing" cert from Keychain
# Access works; no Apple Developer account needed) used to re-sign

View File

@ -7002,23 +7002,69 @@ def _desktop_linux_sandbox_fixup(packaged_executable: Path) -> bool:
return True
def _desktop_launch_options() -> tuple[list[str], str]:
_LINUX_PASSWORD_STORES = frozenset({"gnome-libsecret", "kwallet", "kwallet5", "kwallet6", "basic"})
def _detect_linux_password_store() -> str | None:
"""Detect the Chromium password-store backend for the current Linux session.
Electron's safeStorage only reports encryption as available when Chromium
selects the right keychain backend, and Chromium's own detection routinely
fails under `hermes desktop` because the launcher environment doesn't look
like a full desktop session. Probe order: KDE session env vars, GNOME
Keyring's control socket, then a D-Bus ping of org.freedesktop.secrets
(covers any Secret Service implementation, e.g. KeePassXC). Returns None
when no keychain daemon is reachable.
"""
kde_version = os.environ.get("KDE_SESSION_VERSION", "").strip()
if kde_version == "6":
return "kwallet6"
if kde_version == "5":
return "kwallet5"
if kde_version:
return "kwallet"
if os.environ.get("KDE_FULL_SESSION"):
return "kwallet"
if os.environ.get("GNOME_KEYRING_CONTROL"):
return "gnome-libsecret"
try:
result = subprocess.run(
[
"dbus-send", "--session", "--print-reply", "--reply-timeout=2000",
"--dest=org.freedesktop.secrets",
"/org/freedesktop/secrets",
"org.freedesktop.DBus.Peer.Ping",
],
capture_output=True,
timeout=5,
)
if result.returncode == 0:
return "gnome-libsecret"
except Exception:
pass
return None
def _desktop_launch_options() -> tuple[list[str], str, str]:
"""Read `desktop.*` launch options from config.yaml.
Returns ``(electron_flags, disable_gpu)`` where ``electron_flags`` is a list
of extra Electron CLI flags and ``disable_gpu`` is one of "auto"/"1"/"0"
(normalized for the HERMES_DESKTOP_DISABLE_GPU env var the Electron app
reads). Best-effort: any config error yields the safe defaults
``([], "auto")`` so a malformed config never blocks the launch.
Returns ``(electron_flags, disable_gpu, password_store)`` where
``electron_flags`` is a list of extra Electron CLI flags, ``disable_gpu``
is one of "auto"/"1"/"0" (normalized for the HERMES_DESKTOP_DISABLE_GPU
env var the Electron app reads), and ``password_store`` is "auto" or one
of the Chromium password-store backends (unknown values normalize to
"auto"). Best-effort: any config error yields the safe defaults
``([], "auto", "auto")`` so a malformed config never blocks the launch.
"""
flags: list[str] = []
disable_gpu = "auto"
password_store = "auto"
try:
from hermes_cli.config import load_config
desktop_cfg = (load_config() or {}).get("desktop") or {}
except Exception:
return flags, disable_gpu
return flags, disable_gpu, password_store
raw_flags = desktop_cfg.get("electron_flags")
if isinstance(raw_flags, str):
@ -7037,7 +7083,13 @@ def _desktop_launch_options() -> tuple[list[str], str]:
disable_gpu = "0"
else:
disable_gpu = "auto"
return flags, disable_gpu
raw_store = desktop_cfg.get("password_store", "auto")
if isinstance(raw_store, str):
low_store = raw_store.strip().lower()
if low_store in _LINUX_PASSWORD_STORES:
password_store = low_store
return flags, disable_gpu, password_store
def _register_linux_desktop_entry() -> None:
@ -7091,10 +7143,25 @@ def cmd_gui(args: argparse.Namespace):
# `desktop.disable_gpu`). The GPU policy is bridged to the env var the
# Electron app already reads; an explicit env var still wins over config so
# `HERMES_DESKTOP_DISABLE_GPU=... hermes desktop` keeps working.
config_electron_flags, config_disable_gpu = _desktop_launch_options()
config_electron_flags, config_disable_gpu, config_password_store = _desktop_launch_options()
if config_disable_gpu != "auto" and "HERMES_DESKTOP_DISABLE_GPU" not in os.environ:
env["HERMES_DESKTOP_DISABLE_GPU"] = config_disable_gpu
# Linux keychain backend for safeStorage (`desktop.password_store`).
# Chromium needs the --password-store switch to pick the right keychain;
# without it safeStorage.isEncryptionAvailable() is often false and the
# desktop app refuses to persist remote gateway tokens. Config wins over
# detection; an explicit env var wins over both so
# `HERMES_DESKTOP_PASSWORD_STORE=... hermes desktop` keeps working.
if sys.platform == "linux" and "HERMES_DESKTOP_PASSWORD_STORE" not in os.environ:
password_store = (
config_password_store
if config_password_store != "auto"
else _detect_linux_password_store()
)
if password_store:
env["HERMES_DESKTOP_PASSWORD_STORE"] = password_store
source_mode = getattr(args, "source", False)
skip_build = getattr(args, "skip_build", False)
force_build = getattr(args, "force_build", False)

View File

@ -31,6 +31,22 @@ def _isolate_xdg_data_home(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg-data"))
@pytest.fixture(autouse=True)
def _stable_keychain_detection(monkeypatch):
"""Pin Linux keychain detection to the fast GNOME env path.
On Linux, ``cmd_gui`` falls back to a D-Bus ping via ``subprocess.run``
when no keychain env var is present. Tests here mock ``subprocess.run``
with strict ``side_effect`` lists, so an unpinned probe would silently
consume an item meant for the build/launch calls. Detection-specific
tests clear these vars again via ``_clear_keychain_env``.
"""
monkeypatch.delenv("KDE_SESSION_VERSION", raising=False)
monkeypatch.delenv("KDE_FULL_SESSION", raising=False)
monkeypatch.delenv("HERMES_DESKTOP_PASSWORD_STORE", raising=False)
monkeypatch.setenv("GNOME_KEYRING_CONTROL", "/run/user/1000/keyring")
def _ns(**kw):
defaults = dict(
skip_build=False,
@ -557,3 +573,209 @@ def test_gui_skips_desktop_entry_off_linux(tmp_path, monkeypatch):
cli_main.cmd_gui(_ns())
assert exc.value.code == 0
@pytest.mark.parametrize(
"raw,expected",
[
("gnome-libsecret", "gnome-libsecret"),
("KWallet6", "kwallet6"),
("basic", "basic"),
("auto", "auto"),
("keychain-of-wonders", "auto"),
(True, "auto"),
],
)
def test_desktop_launch_options_normalizes_password_store(raw, expected):
cfg = {"desktop": {"password_store": raw}}
with patch("hermes_cli.config.load_config", return_value=cfg):
_, _, store = cli_main._desktop_launch_options()
assert store == expected
# --- desktop.password_store detection & bridging (linux) ------------------
def _clear_keychain_env(monkeypatch):
for var in (
"KDE_SESSION_VERSION",
"KDE_FULL_SESSION",
"GNOME_KEYRING_CONTROL",
"HERMES_DESKTOP_PASSWORD_STORE",
):
monkeypatch.delenv(var, raising=False)
@pytest.mark.parametrize(
"kde_version,expected",
[
("6", "kwallet6"),
("5", "kwallet5"),
("4", "kwallet"),
],
)
def test_detect_linux_password_store_prefers_kde_session(monkeypatch, kde_version, expected):
_clear_keychain_env(monkeypatch)
monkeypatch.setenv("KDE_SESSION_VERSION", kde_version)
assert cli_main._detect_linux_password_store() == expected
def test_detect_linux_password_store_kde_full_session(monkeypatch):
_clear_keychain_env(monkeypatch)
monkeypatch.setenv("KDE_FULL_SESSION", "true")
assert cli_main._detect_linux_password_store() == "kwallet"
def test_detect_linux_password_store_gnome_keyring(monkeypatch):
_clear_keychain_env(monkeypatch)
monkeypatch.setenv("GNOME_KEYRING_CONTROL", "/run/user/1000/keyring")
assert cli_main._detect_linux_password_store() == "gnome-libsecret"
def test_detect_linux_password_store_via_dbus_secret_service(monkeypatch):
_clear_keychain_env(monkeypatch)
ping_ok = subprocess.CompletedProcess(["dbus-send"], 0)
with patch("hermes_cli.main.subprocess.run", return_value=ping_ok) as mock_run:
assert cli_main._detect_linux_password_store() == "gnome-libsecret"
assert "--dest=org.freedesktop.secrets" in mock_run.call_args.args[0]
def test_detect_linux_password_store_none_when_no_keychain(monkeypatch):
_clear_keychain_env(monkeypatch)
ping_fail = subprocess.CompletedProcess(["dbus-send"], 1)
with patch("hermes_cli.main.subprocess.run", return_value=ping_fail):
assert cli_main._detect_linux_password_store() is None
with patch("hermes_cli.main.subprocess.run", side_effect=FileNotFoundError):
assert cli_main._detect_linux_password_store() is None
@pytest.mark.linux_only
def test_gui_linux_packaged_launch_bridges_detected_password_store(tmp_path, monkeypatch):
_clear_keychain_env(monkeypatch)
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
_make_packaged_executable(root, monkeypatch)
ok = subprocess.CompletedProcess([], 0)
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
patch("hermes_cli.main._run_npm_install_deterministic", return_value=ok), \
patch("hermes_cli.main._desktop_build_needed", return_value=True), \
patch("hermes_cli.main._write_desktop_build_stamp"), \
patch("hermes_cli.main._desktop_macos_relaunchable_fixup"), \
patch("hermes_cli.main._desktop_linux_sandbox_fixup", return_value=True), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_cli.linux_desktop_entry.install_desktop_entry", return_value=None), \
patch("hermes_cli.main._detect_linux_password_store", return_value="gnome-libsecret"), \
patch("hermes_cli.main.subprocess.run", side_effect=[ok, ok]) as mock_run, \
pytest.raises(SystemExit):
cli_main.cmd_gui(_ns())
launch_env = mock_run.call_args_list[1].kwargs["env"]
assert launch_env["HERMES_DESKTOP_PASSWORD_STORE"] == "gnome-libsecret"
@pytest.mark.linux_only
def test_gui_linux_source_launch_bridges_detected_password_store(tmp_path, monkeypatch):
_clear_keychain_env(monkeypatch)
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
ok = subprocess.CompletedProcess([], 0)
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
patch("hermes_cli.main._run_npm_install_deterministic", return_value=ok), \
patch("hermes_cli.main._desktop_build_needed", return_value=True), \
patch("hermes_cli.main._write_desktop_build_stamp"), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_cli.linux_desktop_entry.install_desktop_entry", return_value=None), \
patch("hermes_cli.main._detect_linux_password_store", return_value="kwallet6"), \
patch("hermes_cli.main.subprocess.run", side_effect=[ok, ok]) as mock_run, \
pytest.raises(SystemExit):
cli_main.cmd_gui(_ns(source=True))
assert mock_run.call_args_list[1].args[0] == ["/usr/bin/npm", "exec", "--", "electron", "."]
launch_env = mock_run.call_args_list[1].kwargs["env"]
assert launch_env["HERMES_DESKTOP_PASSWORD_STORE"] == "kwallet6"
@pytest.mark.linux_only
def test_gui_config_password_store_skips_detection(tmp_path, monkeypatch):
_clear_keychain_env(monkeypatch)
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
_make_packaged_executable(root, monkeypatch)
ok = subprocess.CompletedProcess([], 0)
cfg = {"desktop": {"password_store": "kwallet6"}}
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
patch("hermes_cli.main._run_npm_install_deterministic", return_value=ok), \
patch("hermes_cli.main._desktop_build_needed", return_value=True), \
patch("hermes_cli.main._write_desktop_build_stamp"), \
patch("hermes_cli.main._desktop_macos_relaunchable_fixup"), \
patch("hermes_cli.main._desktop_linux_sandbox_fixup", return_value=True), \
patch("hermes_cli.config.load_config", return_value=cfg), \
patch("hermes_cli.linux_desktop_entry.install_desktop_entry", return_value=None), \
patch("hermes_cli.main._detect_linux_password_store") as mock_detect, \
patch("hermes_cli.main.subprocess.run", side_effect=[ok, ok]) as mock_run, \
pytest.raises(SystemExit):
cli_main.cmd_gui(_ns())
mock_detect.assert_not_called()
launch_env = mock_run.call_args_list[1].kwargs["env"]
assert launch_env["HERMES_DESKTOP_PASSWORD_STORE"] == "kwallet6"
@pytest.mark.linux_only
def test_gui_explicit_password_store_env_wins_over_config_and_detection(tmp_path, monkeypatch):
_clear_keychain_env(monkeypatch)
monkeypatch.setenv("HERMES_DESKTOP_PASSWORD_STORE", "basic")
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
_make_packaged_executable(root, monkeypatch)
ok = subprocess.CompletedProcess([], 0)
cfg = {"desktop": {"password_store": "kwallet6"}}
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
patch("hermes_cli.main._run_npm_install_deterministic", return_value=ok), \
patch("hermes_cli.main._desktop_build_needed", return_value=True), \
patch("hermes_cli.main._write_desktop_build_stamp"), \
patch("hermes_cli.main._desktop_macos_relaunchable_fixup"), \
patch("hermes_cli.main._desktop_linux_sandbox_fixup", return_value=True), \
patch("hermes_cli.config.load_config", return_value=cfg), \
patch("hermes_cli.linux_desktop_entry.install_desktop_entry", return_value=None), \
patch("hermes_cli.main._detect_linux_password_store") as mock_detect, \
patch("hermes_cli.main.subprocess.run", side_effect=[ok, ok]) as mock_run, \
pytest.raises(SystemExit):
cli_main.cmd_gui(_ns())
mock_detect.assert_not_called()
launch_env = mock_run.call_args_list[1].kwargs["env"]
assert launch_env["HERMES_DESKTOP_PASSWORD_STORE"] == "basic"
@pytest.mark.macos_only
def test_gui_password_store_bridge_is_linux_only(tmp_path, monkeypatch):
_clear_keychain_env(monkeypatch)
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
_make_packaged_executable(root, monkeypatch)
ok = subprocess.CompletedProcess([], 0)
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
patch("hermes_cli.main._run_npm_install_deterministic", return_value=ok), \
patch("hermes_cli.main._desktop_build_needed", return_value=True), \
patch("hermes_cli.main._write_desktop_build_stamp"), \
patch("hermes_cli.main._desktop_macos_relaunchable_fixup"), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_cli.linux_desktop_entry.install_desktop_entry", return_value=None), \
patch("hermes_cli.main._detect_linux_password_store") as mock_detect, \
patch("hermes_cli.main.subprocess.run", side_effect=[ok, ok]) as mock_run, \
pytest.raises(SystemExit):
cli_main.cmd_gui(_ns())
mock_detect.assert_not_called()
launch_env = mock_run.call_args_list[1].kwargs["env"]
assert "HERMES_DESKTOP_PASSWORD_STORE" not in launch_env