de2d6a1b93
CI / Detect affected areas (push) Has been cancelled
CI / OSV scan (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Supply-chain scan (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
The in-process last-known-good (the codex#31188 port) only protects a
long-running gateway. A CLI restart or `hermes config get` against broken
YAML fell through to DEFAULT_CONFIG and silently dropped every override,
including approvals.deny (#102945).
Every successful parse now leaves a `good` copy in backups/config/ through
the existing bounded, byte-deduped backup_config(); the fallback reads the
newest one, runs it through the normal canonicalize/expand/managed-overlay
pipeline, and says so on stderr. The broken config.yaml is never modified.
The backup is the raw file, so ${VAR} templates stay templates on disk.
Redo of #61796 (which added a config.validated.yaml sibling and
re-validated the whole config on every load) on top of the backups/config/
ruling from bf53ff00a7.
61 lines
2.4 KiB
Python
61 lines
2.4 KiB
Python
"""A fresh process recovers the last successfully parsed config.yaml when the file is broken.
|
|
|
|
The in-process last-known-good (#31188 port) only helps a long-running gateway. A CLI restart or
|
|
``hermes config get`` against broken YAML used to run on ``DEFAULT_CONFIG`` — dropping every
|
|
override, including ``approvals.deny`` (#102945). Successful loads now leave a ``good`` copy in
|
|
``backups/config/`` and the fallback reads it.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from hermes_cli.config_backups import list_config_backups
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
GOOD = (
|
|
"model:\n default: test/secure\n"
|
|
"approvals:\n deny:\n - 'curl*evil*'\n"
|
|
"custom_providers:\n - name: p\n base_url: https://x.invalid/v1\n api_key: ${LKG_TOKEN}\n"
|
|
)
|
|
BROKEN = "approvals:\n deny: [unclosed\n"
|
|
|
|
|
|
def _fresh_load(home: Path) -> tuple[dict, str]:
|
|
env = {**os.environ, "HERMES_HOME": str(home), "PYTHONPATH": str(REPO), "LKG_TOKEN": "expanded-secret"}
|
|
proc = subprocess.run(
|
|
[sys.executable, "-c", "import json; from hermes_cli.config import load_config; print(json.dumps(load_config()))"],
|
|
cwd=REPO, env=env, text=True, capture_output=True, check=True, stdin=subprocess.DEVNULL,
|
|
)
|
|
return json.loads(proc.stdout), proc.stderr
|
|
|
|
|
|
def test_fresh_process_recovers_last_good_config_and_leaves_broken_file_alone(tmp_path):
|
|
config_path = tmp_path / "config.yaml"
|
|
config_path.write_text(GOOD, encoding="utf-8")
|
|
first, _ = _fresh_load(tmp_path)
|
|
assert first["approvals"]["deny"] == ["curl*evil*"]
|
|
|
|
config_path.write_text(BROKEN, encoding="utf-8")
|
|
recovered, stderr = _fresh_load(tmp_path)
|
|
|
|
assert recovered["approvals"]["deny"] == ["curl*evil*"]
|
|
assert recovered["model"]["default"] == "test/secure"
|
|
assert recovered["custom_providers"][0]["api_key"] == "expanded-secret"
|
|
assert "LAST KNOWN GOOD" in stderr
|
|
assert config_path.read_text(encoding="utf-8") == BROKEN
|
|
|
|
|
|
def test_good_backup_keeps_env_templates_and_dedupes_repeat_loads(tmp_path):
|
|
config_path = tmp_path / "config.yaml"
|
|
config_path.write_text(GOOD, encoding="utf-8")
|
|
_fresh_load(tmp_path)
|
|
_fresh_load(tmp_path)
|
|
|
|
good = list_config_backups(config_path, "good")
|
|
assert len(good) == 1
|
|
text = good[0].read_text(encoding="utf-8")
|
|
assert "${LKG_TOKEN}" in text and "expanded-secret" not in text
|