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:
@@ -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."
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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; "
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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``
|
||||
"""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user