Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.
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
11 changes: 11 additions & 0 deletions hermes_cli/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ def run_import(args) -> None:
_QUICK_STATE_FILES = (
"state.db",
"config.yaml",
"config.yaml.sha256", # seal file for Config Integrity Watchdog
".env",
"auth.json",
"cron/jobs.json",
Expand Down Expand Up @@ -655,6 +656,16 @@ def restore_quick_snapshot(
logger.error("Failed to restore %s: %s", rel, exc)

logger.info("Restored %d files from snapshot %s", restored, snapshot_id)

# Reseal if config.yaml was among the restored files so the Config
# Integrity Watchdog sees an authorized restore, not tampering.
if restored > 0 and "config.yaml" in meta.get("files", {}):
try:
from hermes_cli.config import seal_config
seal_config()
except (OSError, ImportError):
pass

return restored > 0


Expand Down
44 changes: 44 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""

import copy
import hashlib
import logging
import os
import platform
Expand Down Expand Up @@ -4582,6 +4583,49 @@ def save_config(config: Dict[str, Any]):
)
_secure_file(config_path)
_LAST_EXPANDED_CONFIG_BY_PATH[str(config_path)] = copy.deepcopy(current_normalized)
# Keep the integrity seal in sync so authorised saves (e.g. model
# scanner, /model command) don't trigger the Config Integrity Watchdog.
try:
seal_config()
Comment thread
dizhaky marked this conversation as resolved.
except OSError:
pass


def get_config_seal_path() -> Path:
"""Return the path to the config integrity seal file."""
return get_config_path().parent / (get_config_path().name + ".sha256")


def seal_config() -> str:
"""Compute SHA256 of the current config.yaml and write it to the seal file.

Call this after any authorised out-of-band config mutation (e.g. a restore
script) so the Config Integrity Watchdog does not raise a false alarm.
Returns the hex digest that was written.
"""
config_path = get_config_path()
seal_path = get_config_seal_path()
digest = hashlib.sha256(config_path.read_bytes()).hexdigest()
seal_path.write_text(digest + "\n", encoding="utf-8")
Comment thread
dizhaky marked this conversation as resolved.
_secure_file(seal_path)
return digest


def verify_config_integrity() -> tuple[bool, str, str]:
"""Check whether config.yaml matches its integrity seal.

Returns (ok, current_digest, sealed_digest).
ok is True when the file matches the seal (or no seal exists yet).
"""
config_path = get_config_path()
seal_path = get_config_seal_path()
if not config_path.exists():
return True, "", ""
current = hashlib.sha256(config_path.read_bytes()).hexdigest()
if not seal_path.exists():
return True, current, ""
sealed = seal_path.read_text(encoding="utf-8").strip()
return current == sealed, current, sealed


def load_env() -> Dict[str, str]:
Expand Down
Loading