From d7be3f649dfcc34167d6ad506db21f0886878c45 Mon Sep 17 00:00:00 2001 From: rahlquist Date: Fri, 28 Aug 2026 09:25:02 -0400 Subject: [PATCH] feat: expose terminal.temp_dir to redirect session temp root off tmpfs Some Linux distros (notably RAM-based tmpfs /tmp on several Arch-based setups) cap the temp directory at a small size, so Hermes runs out of space for session temp files (background logs/pid/exit files, code- execution sandboxes). Add a terminal.temp_dir config key that points these at real storage. - Add terminal.temp_dir default (empty) in config_defaults.py - Bridge it to TERMINAL_TEMP_DIR via TERMINAL_CONFIG_ENV_MAP - Honor TERMINAL_TEMP_DIR first in LocalEnvironment.get_temp_dir(), falling through to TMPDIR//tmp//gettempdir when unset/invalid - Add tests covering override, process-env, missing-dir, and empty --- hermes_cli/config.py | 1 + hermes_cli/config_defaults.py | 7 ++++ tests/tools/test_local_temp_dir.py | 60 ++++++++++++++++++++++++++++++ tools/environments/local.py | 7 ++++ 4 files changed, 75 insertions(+) create mode 100644 tests/tools/test_local_temp_dir.py diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 1e482d16f4..640179b0b4 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3640,6 +3640,7 @@ TERMINAL_CONFIG_ENV_MAP = { "modal_mode": "TERMINAL_MODAL_MODE", "degraded_mode": "TERMINAL_DEGRADED_MODE", "cwd": "TERMINAL_CWD", + "temp_dir": "TERMINAL_TEMP_DIR", "timeout": "TERMINAL_TIMEOUT", "lifetime_seconds": "TERMINAL_LIFETIME_SECONDS", "docker_image": "TERMINAL_DOCKER_IMAGE", diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 6841c3e553..c378d3030d 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -382,6 +382,13 @@ DEFAULT_CONFIG = { # preserves the historical error + traceback behavior. "degraded_mode": "warn", "cwd": ".", # Use current directory + # Root directory for Hermes' terminal session temp files (background + # logs/pid/exit files, code-execution sandboxes, etc.). Defaults to the + # OS temp root. Set this to a path on real storage when /tmp is a small + # tmpfs and Hermes runs out of space — e.g. on RAM-based tmpfs setups + # common to some Arch-based distros. Must be an absolute POSIX path; + # relative or empty values fall back to the default temp resolution. + "temp_dir": "", # Terminal font family for the desktop app's embedded xterm.js terminal. # When set (e.g. "'CaskaydiaCoveNerdFont', 'JetBrains Mono', monospace"), # the desktop terminal uses this as the CSS font-family value, with the diff --git a/tests/tools/test_local_temp_dir.py b/tests/tools/test_local_temp_dir.py new file mode 100644 index 0000000000..30a4573023 --- /dev/null +++ b/tests/tools/test_local_temp_dir.py @@ -0,0 +1,60 @@ +"""Tests for ``LocalEnvironment.get_temp_dir`` temp-dir redirect. + +Hermes exposes ``terminal.temp_dir`` (mirrored to ``TERMINAL_TEMP_DIR``) so +users on RAM-based tmpfs ``/tmp`` can point session temp files (background +logs/pid/exit files, code-execution sandboxes) at real storage. +""" + +import os +import sys + +import pytest + +from tools.environments.local import LocalEnvironment + + +def _make_local_env(env: dict) -> LocalEnvironment: + """Construct a LocalEnvironment without running init_session (no bash).""" + obj = LocalEnvironment.__new__(LocalEnvironment) + obj.env = dict(env) + return obj + + +def test_temp_dir_override_honored(tmp_path): + target = str(tmp_path) + env = _make_local_env({"TERMINAL_TEMP_DIR": target}) + assert env.get_temp_dir() == target + + +def test_temp_dir_from_process_env(tmp_path): + target = str(tmp_path) + env = _make_local_env({}) + prev = os.environ.get("TERMINAL_TEMP_DIR") + os.environ["TERMINAL_TEMP_DIR"] = target + try: + assert env.get_temp_dir() == target + finally: + if prev is None: + os.environ.pop("TERMINAL_TEMP_DIR", None) + else: + os.environ["TERMINAL_TEMP_DIR"] = prev + + +def test_temp_dir_non_existent_falls_through(tmp_path): + """A configured path that does not exist must not be used.""" + missing = str(tmp_path / "does-not-exist") + env = _make_local_env({"TERMINAL_TEMP_DIR": missing}) + # Should fall through to the standard TMPDIR//tmp//gettempdir chain, not + # return the missing path. + assert env.get_temp_dir() != missing + + +def test_temp_dir_empty_falls_through(tmp_path, monkeypatch): + """An empty/relative terminal.temp_dir must not redirect.""" + monkeypatch.setenv("TMPDIR", str(tmp_path)) + env = _make_local_env({"TERMINAL_TEMP_DIR": ""}) + assert env.get_temp_dir() == str(tmp_path) + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-q"])) diff --git a/tools/environments/local.py b/tools/environments/local.py index 04fee8f6f0..0ccd5a32b6 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -1789,6 +1789,13 @@ class LocalEnvironment(BaseEnvironment): # Force forward slashes so the same string serves both contexts. return str(cache_dir).replace("\\", "/") + # Explicit temp-dir override from terminal.temp_dir (TERMINAL_TEMP_DIR). + # Honored ahead of the generic TMPDIR so users can redirect Hermes' temp + # root to real storage when /tmp is a small tmpfs. + configured = self.env.get("TERMINAL_TEMP_DIR") or os.environ.get("TERMINAL_TEMP_DIR") + if configured and configured.startswith("/") and os.path.isdir(configured): + return configured.rstrip("/") or "/" + for env_var in ("TMPDIR", "TMP", "TEMP"): candidate = self.env.get(env_var) or os.environ.get(env_var) if candidate and candidate.startswith("/"):