This repository was archived by the owner on Sep 8, 2026. It is now read-only.
forked from NousResearch/hermes-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(devops): config-integrity-watchdog — git-backed config fingerprinting #67
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
abf2003
feat(devops): add config-integrity-watchdog skill with git-backed fin…
claude 7c3421c
fix(tests): add type ignore for pytest import and None guard for rege…
claude c6a233f
feat(cli): add hermes config seal/verify/restore commands
claude 9c3bdb0
fix(cli): fallback import path, move PLAN.md to .plans/, add restore …
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Config Integrity Watchdog — Implementation Plan | ||
|
|
||
| **Slack thread:** https://mfc-nyc.slack.com/archives/C0BD8QBUSJF/p1782742870774319 | ||
|
|
||
| ## Problem | ||
|
|
||
| The Config Integrity Watchdog has triggered 25+ times in 19 days. Root cause: the `.sha256` sidecar file is mutable — any process that writes `config.yaml` can also overwrite the fingerprint, masking tampering. | ||
|
|
||
| ## Solution | ||
|
|
||
| Replace mutable sidecar with git-backed append-only integrity log stored in the dotfiles repository. | ||
|
|
||
| ## Issues (Linear not available — tracked here) | ||
|
|
||
| | # | Title | Status | | ||
| |---|---|---| | ||
| | 1 | Create config-integrity-watchdog skill scaffold | Done | | ||
| | 2 | Implement seal.py | Done | | ||
| | 3 | Implement verify.py | Done | | ||
| | 4 | Implement restore.py | Done | | ||
| | 5 | Write tests | Done | | ||
| | 6 | Open PR | Done | | ||
| | 7 | Add hermes config seal/verify/restore CLI commands | Done | | ||
| | 8 | Write CLI integration tests | Done | | ||
|
|
||
| ## Assumptions | ||
|
|
||
| - Dotfiles git repo is at `~/Dev/dotfiles` (configurable via `HERMES_DOTFILES_DIR`) | ||
| - `config.yaml` may be a symlink; scripts follow symlinks for hashing | ||
| - Canonical model decision (deepseek-v4-pro vs Nemotron-free) deferred — restore.py uses whatever is in the sealed baseline, not a hardcoded model | ||
|
|
||
| ## Out of scope | ||
|
|
||
| - Changing the canonical model ID (requires user decision) | ||
| - Config integrity for non-config files | ||
| - 1Password integration (future enhancement) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| """CLI handlers for ``hermes config seal/verify/restore``. | ||
|
|
||
| Integrates the config-integrity-watchdog skill into the Hermes CLI. | ||
| Subcommands: | ||
| seal — hash config.yaml and append a signed entry to the git-committed | ||
| integrity log, creating a tamper-evident anchor. | ||
| verify — check the current hash against the sealed baseline; exit 1 if | ||
| the config has been tampered with. | ||
| restore — revert config.yaml to the sealed baseline if it has been tampered. | ||
|
|
||
| Exit codes for verify: | ||
| 0 — config matches canonical baseline | ||
| 1 — config has been tampered | ||
| 2 — integrity log itself has uncommitted changes (log tampering) | ||
| 3 — no baseline found (run seal first) | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Register subcommands on an existing config subparsers object | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def register_subcommands(config_subparsers: argparse.Action) -> None: | ||
| """Attach seal/verify/restore parsers to the ``hermes config`` subparser. | ||
|
|
||
| Called from ``hermes_cli.main`` after the base config subparsers are set up. | ||
| """ | ||
| config_subparsers.add_parser( | ||
| "seal", | ||
| help="Hash config.yaml and anchor it in the git-backed integrity log", | ||
| ) | ||
|
|
||
| config_subparsers.add_parser( | ||
| "verify", | ||
| help=( | ||
| "Check current config.yaml against sealed baseline; " | ||
| "exits 1 if tampered" | ||
| ), | ||
| ) | ||
|
|
||
| config_subparsers.add_parser( | ||
| "restore", | ||
| help="Revert config.yaml to the sealed git baseline if tampered", | ||
| ) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Handlers — called from hermes_cli.config.config_command dispatch | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _import_core(): | ||
| """Import the shared core module from the skill scripts directory. | ||
|
|
||
| We add the skill directory to sys.path on first use rather than at | ||
| module-import time so that the import stays lazy (fast startup) and | ||
| doesn't conflict with any top-level package names. | ||
|
|
||
| Search order: | ||
| 1. ``~/.hermes/skills/devops/config-integrity-watchdog`` (post-sync location) | ||
| 2. Repo-relative ``skills/devops/config-integrity-watchdog`` (pre-sync / dev) | ||
| """ | ||
| candidates = [ | ||
| Path.home() / ".hermes" / "skills" / "devops" / "config-integrity-watchdog", | ||
| Path(__file__).parent.parent / "skills" / "devops" / "config-integrity-watchdog", | ||
| ] | ||
| for skills_root in candidates: | ||
| if skills_root.exists(): | ||
| if str(skills_root) not in sys.path: | ||
| sys.path.insert(0, str(skills_root)) | ||
| try: | ||
| import config_integrity # noqa: PLC0415 | ||
| return config_integrity | ||
| except ImportError: | ||
| continue | ||
| print( | ||
| "ERROR: config-integrity-watchdog skill not found. " | ||
| "Run 'hermes skills sync' first." | ||
| ) | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| def cmd_seal(args: argparse.Namespace) -> int: | ||
| """Handle ``hermes config seal``.""" | ||
| core = _import_core() | ||
| return core.seal() | ||
|
|
||
|
|
||
| def cmd_verify(args: argparse.Namespace) -> int: | ||
| """Handle ``hermes config verify``.""" | ||
| core = _import_core() | ||
| return core.verify() | ||
|
|
||
|
|
||
| def cmd_restore(args: argparse.Namespace) -> int: | ||
| """Handle ``hermes config restore``.""" | ||
| core = _import_core() | ||
| return core.restore() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # config-integrity-watchdog | ||
|
|
||
| A tamper-evident integrity system for `~/.hermes/config.yaml` using git-backed fingerprinting. | ||
|
|
||
| ## After Install | ||
|
|
||
| Hermes automatically syncs this skill to `~/.hermes/skills/devops/config-integrity-watchdog/` on next startup. | ||
|
|
||
| **Initial seal** (run once after install to establish baseline): | ||
| ```bash | ||
| python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/seal.py | ||
| ``` | ||
|
|
||
| ## Why git-backed? | ||
|
|
||
| The existing `.sha256` sidecar file is mutable — any process that can write `config.yaml` can also overwrite the sidecar, masking the tampering. By committing the integrity log to the dotfiles git repository, the fingerprint gains the tamper-evidence of git history: a malicious process without git commit credentials cannot silently forge an entry. | ||
|
|
||
| ## Configuration | ||
|
|
||
| | Env var | Default | Description | | ||
| |---|---|---| | ||
| | `HERMES_CONFIG` | `~/.hermes/config.yaml` | Path to the config file to protect | | ||
| | `HERMES_DOTFILES_DIR` | `~/Dev/dotfiles` | Path to the dotfiles git repository | | ||
|
|
||
| ## Cron job setup | ||
|
|
||
| Replace or augment the existing Config Integrity Watchdog cron job: | ||
|
|
||
| **verify** (runs every hour): | ||
| ```bash | ||
| python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/verify.py | ||
| ``` | ||
|
|
||
| **restore** (runs on verify failure): | ||
| ```bash | ||
| python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/restore.py | ||
| ``` | ||
|
|
||
| ## Graceful fallback | ||
|
|
||
| If the dotfiles directory is not a git repository, `seal.py` still writes the log file but skips the git commit and prints a warning. Verification still works (hash comparison), but the log itself is not tamper-evident in that mode. | ||
|
|
||
| ## Exit codes | ||
|
|
||
| | Code | Meaning | | ||
| |---|---| | ||
| | 0 | OK | | ||
| | 1 | Tampered or error | | ||
| | 2 | Log file has uncommitted changes (log tampering) | | ||
| | 3 | No baseline sealed yet | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| --- | ||
| name: config-integrity-watchdog | ||
| description: Detects and restores tampered Hermes config via git log. | ||
| version: 1.0.0 | ||
| author: dizhaky | ||
| platforms: [linux, macos] | ||
| metadata: | ||
| hermes: | ||
| tags: [devops, security, config, integrity, watchdog] | ||
| related_skills: [ugw-health-check] | ||
| --- | ||
|
|
||
| ## When to Use | ||
|
dizhaky marked this conversation as resolved.
|
||
|
|
||
| Use this skill when you need to detect or recover from unauthorized changes to `~/.hermes/config.yaml`. The watchdog stores a tamper-evident fingerprint in the dotfiles git repository — unlike a mutable `.sha256` sidecar, a git commit cannot be silently overwritten. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - `~/.hermes/config.yaml` must exist (symlink or real file) | ||
| - The dotfiles directory must be a git repository (configurable via `HERMES_DOTFILES_DIR`, defaults to `~/Dev/dotfiles`) | ||
| - Python 3.9+, no third-party dependencies | ||
|
|
||
| ## How to Run | ||
|
|
||
| ```bash | ||
| # Seal the current config as canonical | ||
| python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/seal.py | ||
|
|
||
|
dizhaky marked this conversation as resolved.
|
||
| # Verify config integrity | ||
| python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/verify.py | ||
|
|
||
| # Restore canonical config if tampered | ||
| python3 ~/.hermes/skills/devops/config-integrity-watchdog/scripts/restore.py | ||
| ``` | ||
|
|
||
| ## Quick Reference | ||
|
|
||
| | Exit code | Meaning | | ||
| |---|---| | ||
| | 0 | Config matches canonical baseline | | ||
| | 1 | Config has been tampered | | ||
| | 2 | Integrity log itself has been modified (log tampering) | | ||
| | 3 | No baseline found (run seal.py first) | | ||
|
|
||
| ## Procedure | ||
|
|
||
| 1. After any intentional config change, run `seal.py` to commit the new baseline. | ||
| 2. Schedule `verify.py` as a cron job to detect tampering. | ||
| 3. If tampering is detected, run `restore.py` to revert and re-seal. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.