Add config-file pinning: detect drift on canonical config files#22
Merged
Conversation
Hash-pins the canonical config files (crosscheck.config.json and
config/pricing.json by default) and detects drift away from the pinned
hashes. Threat model is operator-set: protect against accidental edits
of cost tables or panel configs that could go silent (someone setting a
model's cost to 0, or adding an unintended provider to the active set).
Defaults to DETECT-AND-WARN: every server startup emits a
`config_pin_check` event with the drift list; tool calls are NEVER
blocked unless reject mode is explicitly enabled.
Reject mode (opt-in):
- CFG.config_pinning.reject_drift: true, OR
- CROSSCHECK_REJECT_CONFIG_DRIFT=1 env var
When on AND a pin file exists AND current hashes drift, every tool
EXCEPT `config_pin` itself returns a CONFIG_PIN_DRIFT_BLOCKED error
until the operator runs `config_pin(action: "accept_drift")`. The
config_pin escape hatch is intentional — operators need a way to fix
drift without disabling the gate.
Implementation:
- `_config_pin_hash_file` normalizes line endings (CRLF -> LF) before
hashing so cross-platform checkouts don't trip the detector.
- Pin file: `.crosscheck/config_pins.json` (path overridable via
CFG.config_pinning.pin_file), atomic-written via the existing
`_atomic_write_json` helper.
- Tracked paths: defaults to ["crosscheck.config.json",
"config/pricing.json"]. Override via CFG.config_pinning.paths.
Missing files are silently dropped from the current-hash set (so a
non-existent panel.json doesn't fabricate a drift signal).
Tool:
- `config_pin(action: "show" | "set" | "accept_drift" | "clear")`
- show: compute current vs. pinned, surface drift + would_block
- set: record current hashes as canonical
- accept_drift: refresh the pin ONLY when drift is present
(semantic guardrail to avoid pinning a clean tree by mistake)
- clear: remove the pin file entirely
- Error taxonomy: CONFIG_PIN_BAD_ACTION, CONFIG_PIN_NO_PIN_FILE,
CONFIG_PIN_NO_DRIFT, CONFIG_PIN_CLEAR_FAILED.
Tests (scripts/test_config_pin.py):
- show with no pin file -> has_pin_file=false, populated current map
- set writes the pin file; show after set -> drift=[]
- Modifying a tracked file -> show reports it in `drift`
- accept_drift refreshes the pin and clears drift; rejects when there's
no drift (CONFIG_PIN_NO_DRIFT) or no pin file (CONFIG_PIN_NO_PIN_FILE)
- clear removes the pin file
- Bad action -> CONFIG_PIN_BAD_ACTION
- Reject mode end-to-end: enable reject_drift, drift a file, dispatch
`verify` through `handle()` -> blocked with CONFIG_PIN_DRIFT_BLOCKED;
dispatch `config_pin` -> NOT blocked; accept_drift restores access
- Env-var bypass: CROSSCHECK_REJECT_CONFIG_DRIFT=1 enables reject mode
independently of CFG
- CRLF normalization: same content with CRLF endings does not drift
Full suite (34 scripts) passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Hash-pin the canonical config files (`crosscheck.config.json` + `config/pricing.json` by default) and detect drift. Closes the last deferred backlog item.
Threat model = operator-set: protect against accidental edits of cost tables or panel configs that could go silent (someone setting a model's cost to 0, or adding an unintended provider to the active set).
Defaults to DETECT-AND-WARN — every server startup emits a `config_pin_check` event with the drift list; tool calls are never blocked unless reject mode is explicitly enabled via:
When reject mode is on and drift is present, every tool except `config_pin` itself returns a `CONFIG_PIN_DRIFT_BLOCKED` error until the operator runs `config_pin(action: "accept_drift")`. The `config_pin` escape hatch is intentional — operators need a way to fix drift without disabling the gate.
Test plan
🤖 Generated with Claude Code