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.
This commit is contained in:
teknium1
2026-09-19 01:38:12 -07:00
committed by Teknium
parent 2dcebe6471
commit e16fee4db1
17 changed files with 55 additions and 43 deletions

View File

@@ -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."

View File

@@ -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]:

View File

@@ -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,

View File

@@ -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"

View File

@@ -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; "

View File

@@ -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

View File

@@ -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``
"""

View File

@@ -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)

View File

@@ -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

View File

@@ -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:"

View File

@@ -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"

View File

@@ -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-<LABEL>.json for later --compare")
help="save the final metrics as <tempdir>/perf-<LABEL>.json for later --compare")
p.add_argument("--compare", metavar="LABEL",
help="diff against /tmp/perf-<LABEL>.json after running")
help="diff against <tempdir>/perf-<LABEL>.json after running")
p.add_argument("--loop", action="store_true",
help="watch for source changes, rebuild, rerun, and diff vs previous run")
p.add_argument("--extra-flag", dest="extra_flags", action="append", default=[],
@@ -507,17 +508,17 @@ def main() -> int:
metrics = key_metrics(data)
if args.save:
path = Path(f"/tmp/perf-{args.save}.json")
path = Path(tempfile.gettempdir()) / f"perf-{args.save}.json"
path.write_text(json.dumps(metrics, indent=2), encoding="utf-8")
print(f"\n• saved: {path}")
if args.compare:
path = Path(f"/tmp/perf-{args.compare}.json")
path = Path(tempfile.gettempdir()) / f"perf-{args.compare}.json"
if not path.exists():
print(f"\n⚠ no baseline at {path} — run with --save {args.compare} first")
else:
before = json.loads(path.read_text(encoding="utf-8"))
print(f"\n═══ A/B diff vs /tmp/perf-{args.compare}.json ═══")
print(f"\n═══ A/B diff vs {path} ═══")
print(format_diff(before, metrics))
if not data["react"] and not data["frame"]:

View File

@@ -87,8 +87,8 @@ else
# full, etc.) instead of "✗ Failed to install uv" with zero
# diagnostic. Two-stage to avoid `curl | sh` masking curl
# failures (sh exits 0 on empty stdin under no pipefail).
_uv_log="$(mktemp 2>/dev/null || echo "/tmp/hermes-uv-install.$$.log")"
_uv_installer="$(mktemp 2>/dev/null || echo "/tmp/hermes-uv-installer.$$.sh")"
_uv_log="$(mktemp 2>/dev/null || echo "${TMPDIR:-${HERMES_HOME:-$HOME/.hermes}}/hermes-uv-install.$$.log")"
_uv_installer="$(mktemp 2>/dev/null || echo "${TMPDIR:-${HERMES_HOME:-$HOME/.hermes}}/hermes-uv-installer.$$.sh")"
if ! curl -LsSf https://astral.sh/uv/install.sh -o "$_uv_installer" 2>"$_uv_log"; then
echo -e "${RED}✗${NC} Failed to download uv installer."
sed 's/^/ /' "$_uv_log" >&2

View File

@@ -469,7 +469,7 @@ def _env_temp_dir(env: Any) -> str:
for candidate in (temp_dir, tempfile.gettempdir()):
if isinstance(candidate, str) and candidate.startswith("/"):
return candidate.rstrip("/") or "/"
return "/tmp"
return tempfile.gettempdir()
def _format_interrupted_output(stdout_text: str) -> str:

View File

@@ -14,6 +14,7 @@ import shlex
import signal
import stat
import subprocess
import tempfile
import threading
import time
import uuid
@@ -965,7 +966,7 @@ class ProcessRegistry(ProcessCheckpointMixin):
return temp_dir.rstrip("/") or "/"
except Exception as exc:
logger.debug("Could not resolve environment temp dir: %s", exc)
return "/tmp"
return tempfile.gettempdir()
def _scope_argv(self, session: ProcessSession, safe_command: str, unit_suffix: str, label: str) -> List[str]:
"""Login-shell argv for *safe_command* (parity with LocalEnvironment: rc files

View File

@@ -10,6 +10,7 @@ import logging
import os
import re
import shlex
import tempfile
import threading
import time
@@ -18,7 +19,7 @@ from tools.budget_config import DEFAULT_PREVIEW_SIZE_CHARS, BudgetConfig, DEFAUL
logger = logging.getLogger(__name__)
PERSISTED_OUTPUT_TAG = "<persisted-output>"
PERSISTED_OUTPUT_CLOSING_TAG = "</persisted-output>"
STORAGE_DIR = "/tmp/hermes-results"
STORAGE_DIR = os.path.join(tempfile.gettempdir(), "hermes-results")
SPILLOVER_SUBDIR = "cache/spillover"
SPILLOVER_MAX_AGE_HOURS = 24
_BUDGET_TOOL_NAME = "__budget_enforcement__"

View File

@@ -320,13 +320,13 @@ def detect_audio_environment() -> dict:
"Voice INPUT (recording) still requires a PulseAudio bridge:\n"
" 1. Set PULSE_SERVER=unix:/mnt/wslg/PulseServer\n"
" 2. Create ~/.asoundrc pointing ALSA at PulseAudio\n"
" 3. Verify with: arecord -d 3 /tmp/test.wav && aplay /tmp/test.wav")
" 3. Verify with: arecord -d 3 test.wav && aplay test.wav")
else:
warnings.append(
"Running in WSL -- audio requires a forwarded sound server.\n"
" PulseAudio: export PULSE_SERVER=unix:/mnt/wslg/PulseServer\n"
" PipeWire: export PIPEWIRE_REMOTE=$XDG_RUNTIME_DIR/pipewire-0\n"
" Then verify: arecord -d 3 /tmp/test.wav && aplay /tmp/test.wav")
" Then verify: arecord -d 3 test.wav && aplay test.wav")
_probe_audio_libraries(warnings, notices, has_forwarded_audio=has_forwarded_audio,
termux_mic_cmd=termux_mic_cmd, termux_app_installed=termux_app_installed)