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:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user