Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agent/google_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@

# Regex patterns for fallback scraping from an installed gemini-cli.
import re as _re
from utils import atomic_replace
_CLIENT_ID_PATTERN = _re.compile(
r"OAUTH_CLIENT_ID\s*=\s*['\"]([0-9]+-[a-z0-9]+\.apps\.googleusercontent\.com)['\"]"
)
Expand Down Expand Up @@ -499,7 +500,7 @@ def save_credentials(creds: GoogleCredentials) -> Path:
fh.flush()
os.fsync(fh.fileno())
os.chmod(tmp_path, stat.S_IRUSR | stat.S_IWUSR)
os.replace(tmp_path, path)
atomic_replace(tmp_path, path)
finally:
try:
if tmp_path.exists():
Expand Down
3 changes: 2 additions & 1 deletion agent/nous_rate_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import tempfile
import time
from typing import Any, Mapping, Optional
from utils import atomic_replace

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -118,7 +119,7 @@ def record_nous_rate_limit(
try:
with os.fdopen(fd, "w") as f:
json.dump(state, f)
os.replace(tmp_path, path)
atomic_replace(tmp_path, path)
except Exception:
# Clean up temp file on failure
try:
Expand Down
3 changes: 2 additions & 1 deletion agent/shell_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
fcntl = None # type: ignore[assignment]

from hermes_constants import get_hermes_home
from utils import atomic_replace

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -568,7 +569,7 @@ def save_allowlist(data: Dict[str, Any]) -> None:
try:
with os.fdopen(fd, "w") as fh:
fh.write(json.dumps(data, indent=2, sort_keys=True))
os.replace(tmp_path, p)
atomic_replace(tmp_path, p)
except Exception:
try:
os.unlink(tmp_path)
Expand Down
5 changes: 3 additions & 2 deletions cron/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
logger = logging.getLogger(__name__)

from hermes_time import now as _hermes_now
from utils import atomic_replace

try:
from croniter import croniter
Expand Down Expand Up @@ -367,7 +368,7 @@ def save_jobs(jobs: List[Dict[str, Any]]):
json.dump({"jobs": jobs, "updated_at": _hermes_now().isoformat()}, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, JOBS_FILE)
atomic_replace(tmp_path, JOBS_FILE)
_secure_file(JOBS_FILE)
except BaseException:
try:
Expand Down Expand Up @@ -863,7 +864,7 @@ def save_job_output(job_id: str, output: str):
f.write(output)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, output_file)
atomic_replace(tmp_path, output_file)
_secure_file(output_file)
except BaseException:
try:
Expand Down
3 changes: 2 additions & 1 deletion gateway/pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from typing import Optional

from hermes_constants import get_hermes_dir
from utils import atomic_replace


# Unambiguous alphabet -- excludes 0/O, 1/I to prevent confusion
Expand Down Expand Up @@ -59,7 +60,7 @@ def _secure_write(path: Path, data: str) -> None:
f.write(data)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, str(path))
atomic_replace(tmp_path, path)
try:
os.chmod(path, 0o600)
except OSError:
Expand Down
3 changes: 2 additions & 1 deletion gateway/platforms/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class _MockContextTypes:
discover_fallback_ips,
parse_fallback_ip_env,
)
from utils import atomic_replace


def check_telegram_requirements() -> bool:
Expand Down Expand Up @@ -554,7 +555,7 @@ def _persist_dm_topic_thread_id(self, chat_id: int, topic_name: str, thread_id:
_yaml.dump(config, f, default_flow_style=False, sort_keys=False)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, config_path)
atomic_replace(tmp_path, config_path)
except BaseException:
try:
os.unlink(tmp_path)
Expand Down
3 changes: 2 additions & 1 deletion gateway/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def _hash_chat_id(value: str) -> str:
canonical_whatsapp_identifier,
normalize_whatsapp_identifier,
)
from utils import atomic_replace


@dataclass
Expand Down Expand Up @@ -705,7 +706,7 @@ def _save(self) -> None:
json.dump(data, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, sessions_file)
atomic_replace(tmp_path, sessions_file)
except BaseException:
try:
os.unlink(tmp_path)
Expand Down
3 changes: 2 additions & 1 deletion hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

from hermes_cli.config import get_hermes_home, get_config_path, read_raw_config
from hermes_constants import OPENROUTER_BASE_URL
from utils import atomic_replace

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -828,7 +829,7 @@ def _save_auth_store(auth_store: Dict[str, Any]) -> Path:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(tmp_path, auth_file)
atomic_replace(tmp_path, auth_file)
try:
dir_fd = os.open(str(auth_file.parent), os.O_RDONLY)
except OSError:
Expand Down
7 changes: 4 additions & 3 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ def get_container_exec_info() -> Optional[dict]:

# Re-export from hermes_constants — canonical definition lives there.
from hermes_constants import get_hermes_home # noqa: F811,E402
from utils import atomic_replace

def get_config_path() -> Path:
"""Get the main config file path."""
Expand Down Expand Up @@ -3666,7 +3667,7 @@ def sanitize_env_file() -> int:
f.writelines(sanitized)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, env_path)
atomic_replace(tmp_path, env_path)
except BaseException:
try:
os.unlink(tmp_path)
Expand Down Expand Up @@ -3769,7 +3770,7 @@ def save_env_value(key: str, value: str):
f.writelines(lines)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, env_path)
atomic_replace(tmp_path, env_path)
# Restore original permissions before _secure_file may tighten them.
if original_mode is not None:
try:
Expand Down Expand Up @@ -3825,7 +3826,7 @@ def remove_env_value(key: str) -> bool:
f.writelines(new_lines)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, env_path)
atomic_replace(tmp_path, env_path)
if original_mode is not None:
try:
os.chmod(env_path, original_mode)
Expand Down
3 changes: 2 additions & 1 deletion hermes_cli/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from typing import Optional

from hermes_constants import get_hermes_home
from utils import atomic_replace


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -79,7 +80,7 @@ def _save_pending(entries: list[dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(entries, indent=2), encoding="utf-8")
os.replace(tmp, path)
atomic_replace(tmp, path)
except OSError:
# Non-fatal — worst case the user has to run ``hermes debug delete``
# manually.
Expand Down
3 changes: 2 additions & 1 deletion hermes_cli/env_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathlib import Path

from dotenv import load_dotenv
from utils import atomic_replace


# Env var name suffixes that indicate credential values. These are the
Expand Down Expand Up @@ -127,7 +128,7 @@ def _sanitize_env_file_if_needed(path: Path) -> None:
f.writelines(sanitized)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path)
atomic_replace(tmp, path)
except BaseException:
try:
os.unlink(tmp)
Expand Down
3 changes: 2 additions & 1 deletion hermes_cli/model_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from typing import Any

from hermes_cli import __version__ as _HERMES_VERSION
from utils import atomic_replace

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -190,7 +191,7 @@ def _write_disk_cache(data: dict[str, Any]) -> None:
with open(tmp, "w") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
os.replace(tmp, path)
atomic_replace(tmp, path)
except OSError as exc:
logger.info("model catalog cache write failed: %s", exc)

Expand Down
3 changes: 2 additions & 1 deletion hermes_cli/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from typing import Dict

from hermes_constants import display_hermes_home
from utils import atomic_replace


_SUBSCRIPTIONS_FILENAME = "webhook_subscriptions.json"
Expand Down Expand Up @@ -52,7 +53,7 @@ def _save_subscriptions(subs: Dict[str, dict]) -> None:
json.dumps(subs, indent=2, ensure_ascii=False),
encoding="utf-8",
)
os.replace(str(tmp_path), str(path))
atomic_replace(tmp_path, path)


def _get_webhook_config() -> dict:
Expand Down
Loading
Loading