From e16fee4db11cf0d8fddd3351a7676ef6eb3311de Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 19 Sep 2026 01:38:12 -0700 Subject: [PATCH] refactor: resolve runtime temp paths via tempfile/TMPDIR instead of literal /tmp Hermes now routes scratch space through HERMES_HOME/cache/scratch (exported as TMPDIR), so every production path that still spelled out /tmp bypassed that and kept teaching the agent the habit. Fallbacks in tool_result_storage, code_execution_tool, process_registry, the ACP child HOME, mini_swe_runner's local cwd, and the CI/profiling scripts now use tempfile.gettempdir(); shell installers fall back to $TMPDIR (then HERMES_HOME) when mktemp is missing, and repro/eval shells use `mktemp -d -t`. User-facing help text and sample payloads (hermes send, approvals test, hooks test, voice-mode WSL hints, meet_bot debug line) no longer suggest /tmp. Container-side paths (mini_swe_runner docker cwd, sandbox base env, remote sync tarballs) keep the literal because they name the sandbox filesystem, not the host. --- acp_adapter/server.py | 2 +- agent/copilot_acp_client.py | 7 ++++--- hermes_cli/hooks.py | 2 +- hermes_cli/send_cmd.py | 4 ++-- hermes_cli/subcommands/approvals.py | 2 +- mini_swe_runner.py | 19 ++++++++++++------- plugins/google_meet/meet_bot.py | 2 +- scripts/ci/live_comment.py | 3 ++- scripts/desktop-update/repro.sh | 10 +++++----- scripts/install.sh | 4 ++-- scripts/launch_capture_probe.sh | 16 +++++++++------- scripts/profile-tui.py | 11 ++++++----- setup-hermes.sh | 4 ++-- tools/code_execution_tool.py | 2 +- tools/process_registry.py | 3 ++- tools/tool_result_storage.py | 3 ++- tools/voice_mode.py | 4 ++-- 17 files changed, 55 insertions(+), 43 deletions(-) diff --git a/acp_adapter/server.py b/acp_adapter/server.py index f2a0ed4731..482e6abe33 100644 --- a/acp_adapter/server.py +++ b/acp_adapter/server.py @@ -238,7 +238,7 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent): "accept_edits": ( "workspace_session", "Accept Edits", - "Auto-allow workspace and /tmp edits; still asks for sensitive paths.", + "Auto-allow workspace and temp-dir edits; still asks for sensitive paths.", ), "dont_ask": ( "session", "Don't Ask", "Auto-allow file edits for this session except sensitive paths." diff --git a/agent/copilot_acp_client.py b/agent/copilot_acp_client.py index 39e4e197e6..fcb98f0824 100644 --- a/agent/copilot_acp_client.py +++ b/agent/copilot_acp_client.py @@ -14,6 +14,7 @@ import queue import re import shlex import subprocess +import tempfile import threading import time from collections import deque @@ -103,7 +104,7 @@ def _acp_supported(command: str, args: list[str]) -> bool | None: def _resolve_home_dir() -> str: - """Stable HOME for child ACP processes; /tmp as a last resort so the child never starts HOME-less.""" + """Stable HOME for child ACP processes; the temp dir as a last resort so the child never starts HOME-less.""" if home := os.environ.get("HOME", "").strip(): return home if (expanded := os.path.expanduser("~")) and expanded != "~": @@ -111,9 +112,9 @@ def _resolve_home_dir() -> str: try: import pwd - return pwd.getpwuid(os.getuid()).pw_dir.strip() or "/tmp" # windows-footgun: ok — POSIX fallback inside try/except (pwd import fails on Windows) + return pwd.getpwuid(os.getuid()).pw_dir.strip() or tempfile.gettempdir() # windows-footgun: ok — POSIX fallback inside try/except (pwd import fails on Windows) except Exception: - return "/tmp" + return tempfile.gettempdir() def _build_subprocess_env() -> dict[str, str]: diff --git a/hermes_cli/hooks.py b/hermes_cli/hooks.py index 34e1435cd7..900b815387 100644 --- a/hermes_cli/hooks.py +++ b/hermes_cli/hooks.py @@ -156,7 +156,7 @@ _DEFAULT_PAYLOADS = { "child_summary": "Synthetic summary for hooks test", "child_status": "completed", "tool_call_history": [{ "tool_name": "write_file", - "tool_input": {"argument_keys": ["content", "path"], "targets": {"path": "/tmp/report.txt"}}, + "tool_input": {"argument_keys": ["content", "path"], "targets": {"path": "notes/report.txt"}}, "input_bytes": 128, "output_bytes": 32, "status": "ok", }], "duration_ms": 1234, diff --git a/hermes_cli/send_cmd.py b/hermes_cli/send_cmd.py index ec3c5efc39..efb9ba62dd 100644 --- a/hermes_cli/send_cmd.py +++ b/hermes_cli/send_cmd.py @@ -281,10 +281,10 @@ def register_send_subparser(subparsers) -> argparse.ArgumentParser: "Examples:\n" " hermes send --to telegram \"deploy finished\"\n" " echo \"RAM 92%\" | hermes send --to telegram:-1001234567890\n" - " hermes send --to discord:#ops --file /tmp/report.md\n" + " hermes send --to discord:#ops --file ./report.md\n" " hermes send --to slack:#eng --subject \"[CI]\" --file build.log\n" " hermes send --to whatsapp:GROUP@g.us --mention 15551234567 \"@15551234567 hello\"\n" - " hermes send --to telegram \"MEDIA:/tmp/chart.png\" # send a media attachment\n" + " hermes send --to telegram \"MEDIA:./chart.png\" # send a media attachment\n" " hermes send --list # all platforms\n" " hermes send --list telegram # filter by platform\n" "\n" diff --git a/hermes_cli/subcommands/approvals.py b/hermes_cli/subcommands/approvals.py index b5cb6ff489..eeb2f8704c 100644 --- a/hermes_cli/subcommands/approvals.py +++ b/hermes_cli/subcommands/approvals.py @@ -52,7 +52,7 @@ def build_approvals_parser(subparsers, *, cmd_approvals: Callable) -> None: "executing the command, prompting anyone, or persisting anything. " "Exit codes: 0 allow, 2 ask-approval, 3 deny (hardline or user " "deny rule). Tip: use `--` before the command so its own flags " - "aren't parsed: hermes approvals test -- rm -rf /tmp/x") + "aren't parsed: hermes approvals test -- rm -rf ./build") test_parser.add_argument( "--env-type", dest="env_type", default="local", help="Terminal backend type to evaluate against (default: local; " diff --git a/mini_swe_runner.py b/mini_swe_runner.py index 62fb8179a8..62410d8e33 100644 --- a/mini_swe_runner.py +++ b/mini_swe_runner.py @@ -8,7 +8,7 @@ trajectory_compressor.py. Supports single tasks and JSONL batch mode. Usage: python mini_swe_runner.py --task "Create a hello world Python script" --env local - python mini_swe_runner.py --task "List files in /tmp" --env docker --image python:3.11-slim + python mini_swe_runner.py --task "List files in the working directory" --env docker --image python:3.11-slim python mini_swe_runner.py --prompts_file prompts.jsonl --output_file trajectories.jsonl --env docker """ @@ -16,6 +16,7 @@ import importlib import json import logging import os +import tempfile from datetime import datetime from typing import List, Dict, Any, Optional @@ -96,13 +97,17 @@ HERMES_SYSTEM_SUFFIX = ( _OPENROUTER_URL = "https://openrouter.ai/api/v1" -def create_environment(env_type: str = "local", image: str = "python:3.11-slim", cwd: str = "/tmp", timeout: int = 60, **kwargs): - """Create a Hermes execution environment (``local`` ignores ``image``/``kwargs``).""" +def create_environment(env_type: str = "local", image: str = "python:3.11-slim", cwd: str | None = None, timeout: int = 60, **kwargs): + """Create a Hermes execution environment (``local`` ignores ``image``/``kwargs``). + + ``cwd=None`` means the host temp dir locally and the sandbox's own ``/tmp`` inside a container. + """ if env_type == "local": from tools.environments.local import LocalEnvironment - return LocalEnvironment(cwd=cwd, timeout=timeout) + return LocalEnvironment(cwd=cwd or tempfile.gettempdir(), timeout=timeout) if env_type not in ("docker", "modal"): raise ValueError(f"Unknown environment type: {env_type}. Use 'local', 'docker', or 'modal'") + cwd = cwd or "/tmp" # container-side path, not the host temp dir module = importlib.import_module(f"tools.environments.{env_type}") return getattr(module, f"{env_type.capitalize()}Environment")(image=image, cwd=cwd, timeout=timeout, **kwargs) @@ -126,7 +131,7 @@ class MiniSWERunner: """Tool-calling agent loop over a Hermes execution environment, emitting Hermes trajectories.""" def __init__(self, model: str = "anthropic/claude-sonnet-4.6", base_url: str = None, api_key: str = None, - env_type: str = "local", image: str = "python:3.11-slim", cwd: str = "/tmp", + env_type: str = "local", image: str = "python:3.11-slim", cwd: str | None = None, max_iterations: int = 15, command_timeout: int = 60, verbose: bool = False): self.model, self.max_iterations, self.command_timeout, self.verbose = model, max_iterations, command_timeout, verbose self.env_type, self.image, self.cwd = env_type, image, cwd @@ -349,7 +354,7 @@ def main( api_key: str = None, env: str = "local", image: str = "python:3.11-slim", - cwd: str = "/tmp", + cwd: str | None = None, max_iterations: int = 15, timeout: int = 60, verbose: bool = False, @@ -366,7 +371,7 @@ def main( api_key: API key (optional, uses env vars) env: Environment type - "local", "docker", or "modal" image: Docker/Modal image (default: python:3.11-slim) - cwd: Working directory (default: /tmp) + cwd: Working directory (default: host temp dir locally, /tmp inside a container) max_iterations: Maximum tool-calling iterations (default: 15) timeout: Command timeout in seconds (default: 60) verbose: Enable verbose logging diff --git a/plugins/google_meet/meet_bot.py b/plugins/google_meet/meet_bot.py index 35124408d9..5c65f43481 100644 --- a/plugins/google_meet/meet_bot.py +++ b/plugins/google_meet/meet_bot.py @@ -4,7 +4,7 @@ Standalone subprocess spawned by ``process_manager.py``; configured via ``HERMES status + transcript written under ``$HERMES_MEET_OUT_DIR`` (filesystem is the only IPC). No WebRTC audio parsing: Meet's live captions are watched via a MutationObserver — lossy and English-biased, but deterministic (no STT billing) and stable thanks to the ARIA role. -Debug: ``HERMES_MEET_URL=... HERMES_MEET_OUT_DIR=/tmp/x HERMES_MEET_HEADED=1 \\ +Debug: ``HERMES_MEET_URL=... HERMES_MEET_OUT_DIR=./meet-out HERMES_MEET_HEADED=1 \\ python -m plugins.google_meet.meet_bot`` """ diff --git a/scripts/ci/live_comment.py b/scripts/ci/live_comment.py index 708906d3ae..f710b855e2 100644 --- a/scripts/ci/live_comment.py +++ b/scripts/ci/live_comment.py @@ -58,6 +58,7 @@ import json import os import shutil import sys +import tempfile import time import urllib.error import urllib.request @@ -451,7 +452,7 @@ def fetch_all_review_statuses( Artifacts that don't exist yet or fail to parse are silently skipped. """ all_statuses: list[dict] = [] - temp_base = Path("/tmp/review-status-artifacts") + temp_base = Path(tempfile.gettempdir()) / "review-status-artifacts" try: artifacts = _list_artifacts(token, repo, run_id) diff --git a/scripts/desktop-update/repro.sh b/scripts/desktop-update/repro.sh index f2d39a3bf8..55bb8ecd38 100755 --- a/scripts/desktop-update/repro.sh +++ b/scripts/desktop-update/repro.sh @@ -2,7 +2,7 @@ # repro.sh -- reproduce desktop-update paths against a sandboxed HERMES_HOME. # # Nothing here touches your real ~/.hermes or checkout. Each mode builds (or -# reuses) a disposable install under /tmp and drives the REAL code path -- +# reuses) a disposable install under $TMPDIR and drives the REAL code path -- # the actual installer, the actual orchestrator, the actual `hermes update`. # # repro.sh shim shim UI only: success event after 6s @@ -18,7 +18,7 @@ # sandbox preflight, opt-out fallbacks) -- asserts # every outcome without touching a real install # -# The sandbox persists between runs (~/tmp is fine to nuke): fresh reuses +# The sandbox persists between runs (the scratch dir is fine to nuke): fresh reuses # nothing, behind/error reuse the last sandbox install when present because # a from-scratch install is minutes. # @@ -93,9 +93,9 @@ case "$MODE" in ;; gate) # Pure-decision matrix for the linux relaunch gate. Builds a fake - # checkout layout under /tmp; --self-test-gate prints the decision and + # checkout layout under $TMPDIR; --self-test-gate prints the decision and # exits without running an update. - G="/tmp/hermes-gate-test.$$" + G="$(mktemp -d -t hermes-gate-test.XXXXXX)" UNPACKED="$G/hermes-agent/apps/desktop/release/linux-unpacked" mkdir -p "$UNPACKED" touch "$UNPACKED/hermes" && chmod +x "$UNPACKED/hermes" @@ -136,7 +136,7 @@ case "$MODE" in # of the outcome. Each case runs the REAL orchestrator (--no-ui) against # a fake install whose `hermes` stub exits 0 instantly, so the flow # reaches finish() with FINAL_CODE=0 and exercises the launch leg. - L="/tmp/hermes-launch-test.$$" + L="$(mktemp -d -t hermes-launch-test.XXXXXX)" fails=0 expect_msg() { # name python-expr if python3 -c "import json,sys; d=json.load(open('$L/.hermes-update-result.json')); sys.exit(0 if ($2) else 1)"; then diff --git a/scripts/install.sh b/scripts/install.sh index 9316739996..a53eef6896 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -610,8 +610,8 @@ install_uv() { # `curl | sh` masks curl failures (sh exits 0 on empty stdin) # and conflates network errors with installer errors. local _uv_install_log _uv_installer - _uv_install_log="$(mktemp 2>/dev/null || echo "/tmp/hermes-uv-install.$$.log")" - _uv_installer="$(mktemp 2>/dev/null || echo "/tmp/hermes-uv-installer.$$.sh")" + _uv_install_log="$(mktemp 2>/dev/null || echo "${TMPDIR:-$HERMES_HOME}/hermes-uv-install.$$.log")" + _uv_installer="$(mktemp 2>/dev/null || echo "${TMPDIR:-$HERMES_HOME}/hermes-uv-installer.$$.sh")" if ! curl -LsSf https://astral.sh/uv/install.sh -o "$_uv_installer" 2>"$_uv_install_log"; then log_error "Failed to download uv installer from https://astral.sh/uv/install.sh" log_info "curl output:" diff --git a/scripts/launch_capture_probe.sh b/scripts/launch_capture_probe.sh index 45dc5ee06f..e630040ef9 100755 --- a/scripts/launch_capture_probe.sh +++ b/scripts/launch_capture_probe.sh @@ -39,18 +39,20 @@ echo "OK" echo "--- treatment 1: source shape (npm exec -- electron .) captured, not spawned" rm -f "$WORK"/spec.json* run_py yes -c ' -import subprocess -r = subprocess.run(["npm", "exec", "--", "electron", "."], cwd="/tmp", env={"HERMES_DESKTOP_CWD": "/tmp", "PATH": "/usr/bin"}) +import subprocess, tempfile +tmp = tempfile.gettempdir() +r = subprocess.run(["npm", "exec", "--", "electron", "."], cwd=tmp, env={"HERMES_DESKTOP_CWD": tmp, "PATH": "/usr/bin"}) assert r.returncode == 0, r ' [ -e "$WORK/spec.json" ] || fail "treatment 1: no spec written" [ "$(cat "$WORK/spec.json.captured")" = "source" ] || fail "treatment 1: wrong shape" python3 - "$WORK/spec.json" <<'EOF' -import json, sys +import json, sys, tempfile +tmp = tempfile.gettempdir() spec = json.load(open(sys.argv[1])) assert spec["argv"] == ["npm", "exec", "--", "electron", "."], spec["argv"] -assert spec["cwd"] == "/tmp", spec["cwd"] -assert spec["env"]["HERMES_DESKTOP_CWD"] == "/tmp", "env= kwarg not captured" +assert spec["cwd"] == tmp, spec["cwd"] +assert spec["env"]["HERMES_DESKTOP_CWD"] == tmp, "env= kwarg not captured" assert spec["matchedShape"] == "source" print("spec contents OK") EOF @@ -59,9 +61,9 @@ echo "OK" echo "--- treatment 2: packaged shape captured, not spawned" rm -f "$WORK"/spec.json* run_py yes -c ' -import subprocess +import subprocess, tempfile exe = "/x/apps/desktop/release/linux-unpacked/Hermes" -r = subprocess.run([exe, "--no-sandbox"], cwd="/tmp", env={"PATH": "/usr/bin"}) +r = subprocess.run([exe, "--no-sandbox"], cwd=tempfile.gettempdir(), env={"PATH": "/usr/bin"}) assert r.returncode == 0, r # a real spawn of this path would ENOENT ' [ "$(cat "$WORK/spec.json.captured")" = "packaged" ] || fail "treatment 2: wrong shape" diff --git a/scripts/profile-tui.py b/scripts/profile-tui.py index 4d86aa057b..d13fb70cac 100755 --- a/scripts/profile-tui.py +++ b/scripts/profile-tui.py @@ -31,6 +31,7 @@ import select import signal import sqlite3 import sys +import tempfile import time from pathlib import Path from typing import Any @@ -487,9 +488,9 @@ def main() -> int: p.add_argument("--tui-dir", default=str(DEFAULT_TUI_DIR)) p.add_argument("--log", default=str(DEFAULT_LOG)) p.add_argument("--save", metavar="LABEL", - help="save the final metrics as /tmp/perf-