fix(docker): rebootstrap re-seed temp is randomly named so a stale temp never blocks recovery

reseed_if_terminal created its temp as <auth>.rebootstrap.<pid>.tmp with
O_CREAT|O_EXCL. Boot-hook PIDs inside a container are near-deterministic,
so a run SIGKILL'd between create and replace leaves a same-named file and
every later boot hits FileExistsError - which main() swallows as
"error (ignored)", leaving the terminal-session recovery path dead until
someone deletes the temp by hand.

tempfile.mkstemp in the auth dir gives a random name at 0600 (stdlib only,
matching the script's no-hermes-imports rule); the fsync + os.replace +
unlink-on-failure semantics are unchanged.
This commit is contained in:
teknium1
2026-09-13 00:08:21 -07:00
committed by Teknium
parent 714013c493
commit b2688440a9
2 changed files with 21 additions and 2 deletions

View File

@@ -39,6 +39,7 @@ from __future__ import annotations
import json
import os
import sys
import tempfile
from datetime import datetime, timezone
from typing import Any, Optional
@@ -189,8 +190,10 @@ def reseed_if_terminal(auth_path: str, seed_raw: str) -> str:
# 0600 from creation: the seed holds a refresh token and must never sit at umask, even briefly.
# (stdlib only by design — see module docstring — so this mirrors utils.atomic_json_write by hand.)
tmp_path = f"{auth_path}.rebootstrap.{os.getpid()}.tmp"
fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
# Randomly named: boot-hook PIDs inside a container repeat, so a PID-named temp left by a
# SIGKILL'd run would collide with O_EXCL forever and main() would swallow the FileExistsError.
fd, tmp_path = tempfile.mkstemp(
dir=os.path.dirname(auth_path) or ".", prefix=os.path.basename(auth_path) + ".rebootstrap.", suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
json.dump(store, fh)

View File

@@ -9,6 +9,7 @@ These are pure-stdlib tmp_path tests (no container build).
from __future__ import annotations
import importlib.util
import os
import json
from pathlib import Path
@@ -113,3 +114,18 @@ def test_terminal_entry_missing_marker_is_not_terminal(tmp_path):
entry) → not terminal, no re-seed."""
auth = _write_auth(tmp_path, {"nous": {"client_id": "hermes-cli-vps"}})
assert mod.reseed_if_terminal(auth, _FRESH_SEED) == "not_terminal"
def test_stale_temp_from_a_killed_prior_run_does_not_block_reseed(tmp_path):
"""Boot-hook PIDs repeat inside a container: a temp left by a SIGKILL'd run must never make
the next re-seed fail (main() swallows the exception, so the recovery path would be dead)."""
home = tmp_path / "home"
home.mkdir()
auth = _write_auth(home, {"nous": _terminal_nous_state()})
stale = Path(f"{auth}.rebootstrap.{os.getpid()}.tmp")
stale.write_text("{torn", encoding="utf-8")
assert mod.reseed_if_terminal(auth, _FRESH_SEED) == "reseeded"
store = json.loads(Path(auth).read_text())
assert store["providers"]["nous"]["refresh_token"] == "FRESH-rt"
assert sorted(p.name for p in home.iterdir()) == sorted(["auth.json", stale.name]), "no new temp survives"