Skip to content

Add config-file pinning: detect drift on canonical config files#22

Merged
fxspeiser merged 1 commit into
mainfrom
feature/config-pinning
May 26, 2026
Merged

Add config-file pinning: detect drift on canonical config files#22
fxspeiser merged 1 commit into
mainfrom
feature/config-pinning

Conversation

@fxspeiser
Copy link
Copy Markdown
Owner

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:

  • `CFG.config_pinning.reject_drift: true`, or
  • `CROSSCHECK_REJECT_CONFIG_DRIFT=1` env var

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.

  • Pin file: `.crosscheck/config_pins.json` (atomic write, path overridable)
  • CRLF normalization so cross-platform checkouts don't fabricate drift
  • Tool: `config_pin(action: "show" | "set" | "accept_drift" | "clear")`
  • Error taxonomy: `CONFIG_PIN_BAD_ACTION`, `CONFIG_PIN_NO_PIN_FILE`, `CONFIG_PIN_NO_DRIFT`, `CONFIG_PIN_CLEAR_FAILED`, `CONFIG_PIN_DRIFT_BLOCKED`

Test plan

  • New `scripts/test_config_pin.py` — show / set / accept_drift / clear, error taxonomy, end-to-end reject-mode through `handle()`, env-var bypass, CRLF normalization
  • Full suite (34 scripts) passes locally

🤖 Generated with Claude Code

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>
@fxspeiser fxspeiser merged commit 02a35b1 into main May 26, 2026
1 check passed
@fxspeiser fxspeiser deleted the feature/config-pinning branch May 26, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant