Skip to content

fix(cli): harden all CLI surfaces + fix docs accuracy#1491

Merged
chopratejas merged 8 commits into
mainfrom
fix/cli-hardening
Jun 27, 2026
Merged

fix(cli): harden all CLI surfaces + fix docs accuracy#1491
chopratejas merged 8 commits into
mainfrom
fix/cli-hardening

Conversation

@chopratejas

Copy link
Copy Markdown
Collaborator

Summary

Full CLI audit + documentation accuracy pass. All 5 commits on this branch:

CLI Hardening (4 commits)

  • Clean errors instead of tracebacks: corrupt manifests, missing Docker, malformed JSONL, bad --profile, invalid env-var values all now raise click.ClickException with helpful messages
  • Range validation: ~25 numeric flags across 10 files now use click.IntRange/FloatRange--port 0, --hours -1, --limit 0 etc. produce clean usage errors instead of silent wrong behavior
  • Flag combination warnings: conflicting combos (--no-rate-limit + --rpm, --no-optimize + --target-ratio, --telemetry + --no-telemetry) emit yellow warnings on stderr
  • memory --db-path default fixed: was resolving to headroom_memory.db (wrong bare file); now uses project store ./.headroom/memory.db if present, else ~/.headroom/memory.db
  • memory list --search + filters: --scope/--session/--since were silently ignored when --search was also set; now filters are applied to search results
  • learn --verbosity --apply now works: the output shaper is off by default (HEADROOM_OUTPUT_SHAPER); --apply now hot-enables it via POST /admin/runtime-env on a running proxy, or prints explicit export HEADROOM_OUTPUT_SHAPER=1 instructions when no proxy is running
  • perf --hours overflow: 1e9 hours no longer raises OverflowError; treated as "all data"
  • evals memory --categories invalid input: abc,1,2 now raises BadParameter instead of a raw ValueError traceback

Documentation (1 commit, 20 files)

Corrected factual errors found by 3 parallel audit agents across root docs, wiki, and the published Fumadocs site:

Critical (caused runtime errors or wrong behavior if followed):

  • simulation.mdx: plan.transforms_applied -> plan.transforms; plan.savings_percent -> computed from available fields (both raised AttributeError)
  • shared-context.mdx: import { SharedContext } from "headroom" -> "headroom-ai" (5x ImportError)
  • claude-code-azure-foundry.mdx: pip install headroom -> pip install headroom-ai
  • api-reference.mdx + configuration.mdx: from headroom import GoogleProvider -> from headroom.providers import GoogleProvider
  • ccr.mdx: CCR TTL default 300s -> 1800s (30 min)

Fabricated flags removed:

  • wiki/proxy.md + wiki/cli.md: --no-intelligent-context, --no-intelligent-scoring, --no-compress-first (none exist); replaced with real CCR flags
  • wiki/configuration.md: --no-ccr-responses, --no-ccr-expansion (none exist); replaced with real flags
  • wiki/troubleshooting.md, wiki/metrics.md, docs/troubleshooting.mdx: headroom proxy --log-level debug (flag doesn't exist)

Stale content corrected:

  • llms.txt: telemetry stated as enabled-by-default (it's opt-in); wrap list had 5 tools (now 11)
  • README.md: compatibility matrix added 5 missing wrap targets; unwrap, doctor, init/install, savings-analytics now mentioned
  • SECURITY.md: supported version table showed 0.2.x (current: 0.27.x)
  • wiki/learn.md: 5 missing flags added; verbosity shaper-off behavior documented
  • wiki/quickstart.md: "Configuration Reference" linked to api.md (wrong) -> configuration.md
  • CacheAlignerConfig.enabled default corrected: True -> False
  • opencode.mdx: --port default wrong ("random") -> 8787; openai backend removed
  • CONTRIBUTING.md: broken Markdown table cell fixed
  • docs/meta.json: claude-code-azure-foundry added to nav (was unreachable orphan page)
  • configuration.mdx: SDK modes vs proxy --mode now clearly distinguished

Test plan

  • python -m pytest tests/ -x -q — 857 passed, 0 failures
  • 41-combination CLI smoke test (all flag combos across 8 commands) — 0 tracebacks
  • ruff check on all modified Python files — clean
  • Docs changes are removals/corrections of fabricated or stale content; no new claims introduced

…on card

`headroom perf` read only proxy.log compression records, so RTK savings (its
own lifetime counter, never logged to proxy.log) were invisible — perf reported
"token savings" while silently dropping the entire CLI-filtering layer. The
dashboard Session card likewise showed only the session-delta (~0 right after a
proxy restart), unlabeled and with no lifetime figure.

- perf: format_report/build_perf_summary now attach RTK lifetime savings
  (best-effort via _get_context_tool_stats().lifetime; omitted when no tool is
  installed), in both the text report and JSON output.
- dashboard: Session card labels the RTK number 'this session', uses the real
  session_savings_pct, and shows lifetime alongside it.
- tests: real _get_context_tool_stats() session-delta regression + perf
  RTK-surfacing tests.
Several commands surfaced raw Python tracebacks on bad input instead of a
clean error message + nonzero exit:

- evals memory/-v2 --categories: validate as comma-separated ints 1-5
  (was ValueError on non-numeric)
- perf --hours: guard datetime overflow on huge values (treat as all-data)
- capture network-diff: skip malformed JSONL lines instead of crashing
- agent-savings --profile: clean BadParameter for unknown profiles
- proxy: malformed numeric env vars now raise a clean ClickException
- mcp status: catch all httpx errors (malformed configured proxy URL)
- install/init: a corrupt deployment manifest raises a typed ManifestError;
  lifecycle commands report it cleanly and the session-start hook
  (init hook ensure) degrades gracefully instead of crashing every launch
- install apply/start/restart: docker-missing and subprocess failures become
  clean errors; apply failure path no longer re-raises raw exceptions
Negative/zero/out-of-range numeric flags previously misbehaved silently
(returned all data, produced invalid URLs, or crashed). Add click
IntRange/FloatRange so they fail fast with a clear usage error:

- learn --workers: IntRange(min=1)
- savings --days: IntRange(min=1)
- memory list --limit: IntRange(min=1)
- perf --hours / agent-savings --hours: FloatRange(min=0) (0 = all data)
- --port: IntRange(1, 65535) on dashboard, doctor, proxy, install, init,
  and all wrap/unwrap subcommands (copilot no longer OverflowErrors)
- proxy --read-maturation-{quiesce,max-hold}-turns / --min-evidence:
  IntRange(min=1); --read-maturation-min-size-bytes: IntRange(min=0);
  --memory-qdrant-port: IntRange(1, 65535)
…arch

Flags that were silently ignored in certain modes/combinations now warn or
error, and a few misleading defaults/outputs are corrected:

learn:
- reject --all + --project (mutually exclusive)
- error --llm-judge without --verbosity
- reject --verbosity --all --apply (would persist only the last project's level)
- note which flags (--target/--main-only/--workers/--model) are ignored in
  --verbosity mode
- explicitly-selected agent with no data now prints a message instead of
  exiting silently

memory:
- default --db-path now resolves the store the proxy/install actually use
  (./.headroom/memory.db if present, else ~/.headroom/memory.db) instead of a
  bare headroom_memory.db that pointed at neither
- memory list --search now honors --scope/--session/--since instead of
  silently ignoring them

proxy:
- warn on --no-rate-limit + --rpm/--tpm, --no-optimize + --target-ratio, and
  --telemetry + --no-telemetry

install/init:
- warn when --anyllm-provider is set without --backend anyllm

wrap:
- warn that --backend/--region/--anyllm-provider have no effect with --no-proxy

perf:
- report header reads 'all data' instead of a misleading 'last 0h' when no
  time cutoff applies (--hours 0 or overflow)

evals:
- group help now covers adversarial/probes, not just memory
Devs reported that 'learn --verbosity --apply' didn't actually do anything.
Root cause: it wrote a valid verbosity.json (the resolver and shaper consume
it correctly — verified L3 steering reaches the system prompt), but the output
shaper is a live, off-by-default knob. The learned level stays inert until
HEADROOM_OUTPUT_SHAPER is enabled in the proxy serving traffic, and --apply
neither enabled it nor said so — its 'the output shaper now uses this level'
message read as if it were already active.

Now --apply:
- hot-enables a running local proxy via /admin/runtime-env (same no-restart
  channel wrap uses), so the level is live immediately, and
- when no proxy is reachable, prints an honest warning that the shaper is OFF
  with the exact steps to enable it (export HEADROOM_OUTPUT_SHAPER=1 + wrap).
High-impact factual corrections from a full 3-agent audit:

- llms.txt: telemetry is OFF by default (was wrongly stated as enabled);
  expand wrap agent list from 5 to 11 tools; fix failure-learning default
  target (CLAUDE.local.md, not CLAUDE.md)
- README: match wrap matrix to all 12 real `headroom wrap` subcommands;
  add `unwrap`, `headroom doctor`, persistent-install and savings-analytics
  links; note `[all]` extra gap for framework adapters; fix learn default target
- SECURITY.md: version table updated to 0.27.x (was 0.2.x); scope expands to
  all official integrations (LangChain, Agno, Strands, LiteLLM, SDKs, MCP)
- CONTRIBUTING.md: fix broken Markdown cell in the question-type table
- wiki/proxy.md: remove 3 fabricated flags (--no-intelligent-context,
  --no-intelligent-scoring, --no-compress-first); replace with real CCR flags
- wiki/configuration.md: replace 2 fabricated CCR flags (--no-ccr-responses,
  --no-ccr-expansion) with the real --no-ccr-inject-tool/--no-ccr-marker/
  --no-ccr-proactive-expansion flags
- wiki/cli.md: same 3 fabricated proxy flags replaced; --db-path default
  corrected to project-then-global resolution on all 9 memory subcommands
- wiki/troubleshooting.md, wiki/metrics.md: remove non-existent --log-level
  debug flag; replace with --log-messages / --log-file
- wiki/learn.md: add 5 missing flags (--target, --workers, --main-only,
  --verbosity, --llm-judge); document shaper-off-by-default behavior; fix
  default write target (CLAUDE.local.md)
- wiki/quickstart.md: fix broken "Configuration Reference" link (api.md →
  configuration.md)
- docs/troubleshooting.mdx: fix 2x non-existent --log-level debug
- docs/ccr.mdx: fix CCR TTL default (300s → 1800s / 30 minutes)
- docs/opencode.mdx: fix --port default (not random, is 8787); fix --backend
  list (remove invalid `openai`, add litellm-<provider>)
- docs/shared-context.mdx: fix TS import ("headroom" → "headroom-ai")
- docs/simulation.mdx: fix 3 Python attribute errors (transforms_applied →
  transforms; savings_percent → computed from tokens_saved/tokens_before)
- docs/api-reference.mdx: fix CacheAlignerConfig.enabled default (True → False);
  fix GoogleProvider import path (headroom → headroom.providers)
- docs/configuration.mdx: clarify SDK modes vs proxy --mode; fix GoogleProvider
  import
- docs/claude-code-azure-foundry.mdx: fix pip install (headroom → headroom-ai)
- docs/meta.json: add claude-code-azure-foundry to site navigation (was orphan)
@github-actions

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Missing required section Description.
  • Missing required section Type of Change.
  • Missing required section Changes Made.
  • Missing required section Testing.
  • Missing required section Real Behavior Proof.
  • Missing required section Review Readiness.
  • Check I have performed a self-review before requesting human review.
  • Check This PR is ready for human review or convert the PR back to draft.

Please update the PR body, or move the PR back to draft while it is still in progress.

@github-actions github-actions Bot added status: has conflicts Pull request has merge conflicts with the base branch status: needs author action Pull request body or readiness checklist still needs author updates labels Jun 27, 2026
- README.md: keep full 12-tool wrap list (our fix) vs main's stale 5-tool list
- opencode.mdx: take main's updated formatting + keep our port-default correction (8787, not random)
@github-actions github-actions Bot removed the status: has conflicts Pull request has merge conflicts with the base branch label Jun 27, 2026
The test exercises the persistent-docker runtime-selection path, but
_start_deployment guards that preset with shutil.which("docker"). That
check was unmocked, so the test passed only on hosts with Docker on
PATH and failed on CI runners without it (macos-latest in #1491). Mock
headroom.cli.install.shutil.which so the test asserts the runtime
selection itself, independent of the host's docker install.
@chopratejas chopratejas merged commit bd76235 into main Jun 27, 2026
33 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 27, 2026
chopratejas pushed a commit that referenced this pull request Jun 29, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.28.0</summary>

##
[0.28.0](v0.27.0...v0.28.0)
(2026-06-29)


### Features

* add --disable-kompress-fallback to restore legacy PASSTHROUGH fallback
([#1185](#1185))
([f309244](f309244))
* add first-class OpenCode support (wrap, learn, mcp install)
([#559](#559))
([91cd210](91cd210))
* add HEADROOM_KEEPALIVE_EXPIRY to keep upstream connections warm
([#1124](#1124))
([85786b3](85786b3))
* **azure-foundry:** derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE
([#1138](#1138))
([e5031b0](e5031b0))
* **cache:** attribute prompt-cache misses to TTL lapse vs prefix change
([#1313](#1313))
([#1343](#1343))
([4658721](4658721))
* **code:** add Perl support to code-aware compressor
([#1125](#1125))
([f39858c](f39858c))
* headroom wrap opencode / unwrap opencode CLI
([#1105](#1105))
([b4571cc](b4571cc))
* **learn:** weight loops in Headroom Learn + RTK-loop eval
([#1160](#1160))
([14e8dc4](14e8dc4))
* **learn:** write per-project learnings to CLAUDE.local.md by default
([#1115](#1115))
([ced75e4](ced75e4))
* **proxy:** add request timeout config
([#738](#738))
([c0745d4](c0745d4))
* **proxy:** pilot hardening — inbound auth, security headers, audit
log, air-gap switch
([#1537](#1537))
([546ab55](546ab55))
* **proxy:** support glob patterns in exclude_tools
([#870](#870))
([#1259](#1259))
([a2159c0](a2159c0))
* **read-maturation:** activity-based hold-back Read maturation
(Mechanism B)
([#1068](#1068))
([723b80c](723b80c))
* **savings:** durable savings ledger + headroom savings command
([#1127](#1127))
([978ffa0](978ffa0))
* **wrap:** add --1m to preserve the 1M context window on wrap claude
([#1158](#1158))
([#1351](#1351))
([b50d9c1](b50d9c1))
* **wrap:** make tokensave the primary coding-task compressor, Serena
the backup
([#1230](#1230))
([dca9853](dca9853))


### Bug Fixes

* **agent-evals:** Phase 0 — coding-agent accuracy A/B framework
([#1037](#1037))
([84f9871](84f9871))
* **agno:** tolerate streaming tool-call SDK objects in parser
([#1312](#1312))
([#1336](#1336))
([5986c22](5986c22))
* **bedrock:** add boto3 1.41 + CRT for aws login credentials
([#1486](#1486))
([4db3bc9](4db3bc9))
* bump codebase-memory-mcp to v0.8.1
([#1284](#1284))
([530318b](530318b))
* **ccr:** make headroom_retrieve a hash-only full-content lookup
([#1532](#1532))
([c2fc4d3](c2fc4d3))
* **ccr:** propagate --no-ccr-marker flag to all compressors
([#1022](#1022))
([#1197](#1197))
([0c9b42a](0c9b42a))
* **ccr:** skip Anthropic marker emission when tool injection is
deferred
([#1273](#1273))
([2cae13d](2cae13d))
* **ci:** extend gitleaks allowlist to cover test fixtures + verified
examples
([#1539](#1539))
([d2565a6](d2565a6))
* **ci:** guarantee model present in test shards to end cache-miss
flakiness
([#1399](#1399))
([2e29c72](2e29c72))
* **ci:** normalize Windows CRLF line endings in PR governance script
([#1012](#1012))
([5194388](5194388))
* **cli:** add explicit UTF-8 encoding to file I/O in wrap commands
([#1126](#1126))
([#1164](#1164))
([a0cb798](a0cb798))
* **cli:** fall back gracefully when embedding-server sidecar is absent
([#1206](#1206))
([38f1404](38f1404))
* **cli:** harden all CLI surfaces + fix docs accuracy
([#1491](#1491))
([bd76235](bd76235))
* **cli:** wire --http2/--no-http2 (HEADROOM_HTTP2) into proxy command
([#1373](#1373))
([e06b616](e06b616))
* **cli:** wire --rpm/--tpm and HEADROOM_RPM/HEADROOM_TPM to the Click
proxy command
([#1375](#1375))
([8aab8f2](8aab8f2))
* **code:** slice tree-sitter byte offsets as UTF-8
([#1332](#1332))
([8238402](8238402))
* **code:** validate Python compressed syntax
([#1302](#1302))
([cbd361d](cbd361d))
* **code:** verify a real parse in tree-sitter availability check
([#1231](#1231))
([#1299](#1299))
([5e0bb69](5e0bb69))
* **codex:** retag threads on init so Codex Desktop history stays
visible ([#961](#961))
([#1349](#1349))
([e6bbc40](e6bbc40))
* **codex:** stop pinning Codex memory MCP to one project db
([#1269](#1269))
([ad7993b](ad7993b))
* **dashboard:** include RTK stats in the historical tab
([#1324](#1324))
([35939c3](35939c3))
* **deps:** remediate dependency CVEs and publish SBOM
([#1509](#1509))
([5771a80](5771a80))
* **docker:** persist session history across container revisions
([#1118](#1118))
([5912d65](5912d65))
* **gemini:** offload compression to the executor
([#1382](#1382))
([615848e](615848e))
* **gemini:** resolve Google model capabilities through ModelRegistry
([#1276](#1276))
([17ecad9](17ecad9))
* **install:** guard install_agent_ensure against duplicate runtime
spawns
([#1301](#1301))
([8da0b4e](8da0b4e))
* **install:** repair macOS launchd restart/start lifecycle
([#1290](#1290))
([da1a397](da1a397))
* **install:** stop duplicating ENTRYPOINT in persistent-docker runtime
command ([#833](#833))
([#1348](#1348))
([feedead](feedead))
* **io:** use UTF-8 with locale fallback and preserve line endings on
config/text I/O
([#1498](#1498))
([1baa04e](1baa04e))
* **kompress:** hard override keeps must-keep tokens regardless of model
score ([#1400](#1400))
([42612c8](42612c8))
* **langchain:** disable streaming on wrapped model during ainvoke()
([#1287](#1287))
([3590046](3590046))
* **mcp:** register managed installs with a resolvable headroom command
([#1386](#1386))
([22def93](22def93))
* **mcp:** report correct savings_percent in headroom_compress
([#1106](#1106))
([f216e43](f216e43))
* **opencode:** write local MCP config
([#1381](#1381))
([6c83790](6c83790))
* **packaging:** move hnswlib to optional [vector] extra so [all] needs
no C++ toolchain
([#1499](#1499))
([80fa086](80fa086))
* patch rtk hook script to use absolute path after register_claude_hooks
([#571](#571))
([b618d2d](b618d2d))
* **perf:** surface RTK/CLI context-tool savings in perf and the session
card ([#1433](#1433))
([9362747](9362747))
* **proxy:** add --protect-tool-results to prevent lossy compression of
exact-output Bash results
([#1374](#1374))
([51d4bcf](51d4bcf))
* **proxy:** add an Anthropic buffered read-timeout override
([#1331](#1331))
([3be2526](3be2526))
* **proxy:** add versionless Vertex AI routes for Claude Code
compatibility
([#1321](#1321))
([bb3e040](bb3e040))
* **proxy:** bind before eager preload so a hung compressor load can't
block startup
([#1500](#1500))
([d5ac07f](d5ac07f))
* **proxy:** build SSL contexts for custom CA bundles
([#1134](#1134))
([561ba17](561ba17))
* **proxy:** forward request-id headers on the streaming path
([#1100](#1100))
([#1258](#1258))
([3d59df7](3d59df7))
* **proxy:** gate CCR retrieve/compress endpoints to loopback
([#1338](#1338))
([acafb2d](acafb2d))
* **proxy:** honor force_kompress routing profile
([#996](#996))
([b4682d6](b4682d6))
* **proxy:** keep large compression results on the critical path
([#296](#296))
([#1352](#1352))
([90734b6](90734b6))
* **proxy:** offload /v1/compress to the compression executor to stop
blocking the loop
([#1501](#1501))
([27e010e](27e010e))
* **proxy:** preserve Responses memory continuations with store=false
([#1103](#1103))
([cdfeeac](cdfeeac))
* **proxy:** queue mid-turn user messages on non-Bedrock streaming path
([#1377](#1377))
([b09f027](b09f027))
* **proxy:** register interceptor in explicit transforms list when
HEADROOM_INTERCEPT_ENABLED
([#1376](#1376))
([55c700c](55c700c))
* **proxy:** report real input tokens on streaming message_start
([#1132](#1132))
([#1305](#1305))
([70cc96a](70cc96a))
* **proxy:** retry upstream 429 with Retry-After on both forwarders
([#1329](#1329))
([90bee89](90bee89))
* **proxy:** retry upstream 529 overloaded like 429 on both forwarders
([#1495](#1495))
([547b15d](547b15d))
* **proxy:** stop re-compressing headroom_retrieve output and emitting
unredeemable markers
([#1323](#1323))
([43494ff](43494ff))
* **proxy:** strip Codex lite header from OpenAI WebSockets
([#1543](#1543))
([5d3803a](5d3803a))
* **read-lifecycle:** persist STALE Read originals in the CCR store
([#1488](#1488))
([9157173](9157173))
* recover persistent proxy feature checks and reject non-Copilot
exchange URL
([#1465](#1465))
([16c638b](16c638b))
* remove agents.md
([#1540](#1540))
([a7d3360](a7d3360))
* respect COPILOT_PROVIDER_TYPE env var when provider_type is auto
([#549](#549))
([24cf256](24cf256))
* restore token-mode compression on frozen prefixes
([#1489](#1489))
([8e0dadf](8e0dadf))
* **router:** degrade to pure-Python detection on native panic
([#1123](#1123))
([#1260](#1260))
([a00fb67](a00fb67))
* **rtk:** stop hook registration timing out on a forked daemon
([#1314](#1314))
([9758817](9758817))
* **smart-crusher:** honor enable_ccr_marker on the opaque-blob path
([#1130](#1130))
([27d6f8e](27d6f8e))
* **subscription:** only reset 5h contribution on real rollover, not API
jitter
([#1255](#1255))
([8d6c175](8d6c175))
* **subscription:** run transcript token scan off the event loop
([#1263](#1263))
([f03021f](f03021f))
* surface output reduction without a restart, and explain $0.00 savings
on Python 3.14
([#1296](#1296))
([c30ec4c](c30ec4c))
* **tests:** reset whole headroom logger subtree so caplog stays
deterministic
([#1117](#1117))
([fda4670](fda4670))
* **tls:** add HEADROOM_TLS_STRICT=0 toggle for corporate SSL inspection
([#1308](#1308))
([#1341](#1341))
([52068dd](52068dd))
* **tokenizers:** price CJK/Kana/Hangul at ~1 token per char in
EstimatingTokenCounter
([#1093](#1093))
([a35fe86](a35fe86))
* **transforms:** gate tool string output from lossy compression
([#1307](#1307))
([#1387](#1387))
([c6c921a](c6c921a))
* **websocket:** harden responses websocket origin handling
([#1481](#1481))
([c632023](c632023))
* **windows:** pin UTF-8 encoding on text-mode subprocess calls
([#1311](#1311))
([d633e81](d633e81))
* **wrap:** add Copilot unwrap command
([#1251](#1251))
([b4fde0c](b4fde0c))
* **wrap:** isolate proxy stdio from proxy.log on Windows
([#1191](#1191))
([959ab0d](959ab0d))
* **wrap:** keep agent savings opt-in
([#1294](#1294))
([b829ceb](b829ceb))
* **wrap:** show the dashboard URL when the proxy is already running
([#1313](#1313))
([b0146c4](b0146c4))


### Performance Improvements

* **compression:** take large cold-start contexts off the synchronous
kompress path
([#1171](#1171))
([#1298](#1298))
([6c68ff4](6c68ff4))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
JerrettDavis pushed a commit that referenced this pull request Jul 9, 2026
…pt manifests (#1303)

## Description

Make deployment-manifest persistence in `headroom/install/state.py`
crash-safe by writing the manifest **atomically**. `save_manifest` used
a plain `path.write_text(...)` (truncate-then-write), so an interrupted
save (Ctrl-C, system restart, container OOM/SIGKILL) could leave a
truncated `manifest.json` on disk.

> **Note (rebased onto current `main`):** since this PR was opened,
#1491 hardened `load_manifest` to raise a typed `ManifestError` on a
corrupt manifest. I've rebased and **dropped my original `load_manifest
→ return None` change in favour of that deliberate typed-error design**,
so this PR now scopes down to the still-missing piece: the **atomic
write** (upstream `save_manifest` is still a plain `write_text`), plus a
regression test for the `ManifestError` path that `main` added without
test coverage.

Closes #

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Add `_atomic_write_text(path, data)`: write to a same-directory temp
file → `flush()` + `os.fsync()` → `os.replace()` (atomic rename on POSIX
and Windows); the temp file is cleaned up if anything fails.
- `save_manifest` now persists via `_atomic_write_text` instead of
`path.write_text(...)`, so a crash between truncate and full write
leaves either the previous file or the complete new one — never a
truncated manifest.
- `load_manifest` is left exactly as `main` has it (raises
`ManifestError` on a corrupt payload) — no behavioural change from me
there.
- Tests: add `test_save_manifest_writes_atomically` (no leftover temp
file; manifest round-trips) and
`test_load_manifest_raises_manifest_error_on_corrupt_payload` (covers
the `ManifestError` path #1491 introduced but did not test).

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ pytest tests/test_install/test_state.py -q
.....                                                                    [100%]
5 passed in 0.11s

$ ruff check headroom/install/state.py tests/test_install/test_state.py
All checks passed!

$ ruff format --check headroom/install/state.py tests/test_install/test_state.py
2 files already formatted

$ mypy headroom/install/state.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- **Environment:** macOS (Darwin), Python 3.13, rebased onto current
`main`.
- **Exact command / steps:** `save_manifest(manifest)` then inspect the
profile dir and reload.
- **Observed result:** after a save the profile directory contains only
`manifest.json` (no leftover `.manifest.json.*.tmp`), and
`load_manifest("default")` round-trips the persisted manifest. A
deliberately-corrupt `manifest.json` (`"{not json"`) makes
`load_manifest` raise `ManifestError` (typed), not a raw
`JSONDecodeError`.
- **Not tested:** the physical-crash-mid-write window is reasoned about
via `os.replace()` atomicity, not fault-injected.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

Documentation / CHANGELOG boxes are unchecked as N/A — this is an
internal persistence-hardening fix with no user-facing surface. The diff
is now small (atomic write + two tests); the corrupt-manifest handling
itself lives in `main` via #1491.
JerrettDavis pushed a commit to peterlodri-sec/headroom that referenced this pull request Jul 14, 2026
…i#1491)

## Summary

Full CLI audit + documentation accuracy pass. All 5 commits on this
branch:

### CLI Hardening (4 commits)
- **Clean errors instead of tracebacks**: corrupt manifests, missing
Docker, malformed JSONL, bad `--profile`, invalid env-var values all now
raise `click.ClickException` with helpful messages
- **Range validation**: ~25 numeric flags across 10 files now use
`click.IntRange`/`FloatRange` — `--port 0`, `--hours -1`, `--limit 0`
etc. produce clean usage errors instead of silent wrong behavior
- **Flag combination warnings**: conflicting combos (`--no-rate-limit` +
`--rpm`, `--no-optimize` + `--target-ratio`, `--telemetry` +
`--no-telemetry`) emit yellow warnings on stderr
- **`memory --db-path` default fixed**: was resolving to
`headroom_memory.db` (wrong bare file); now uses project store
`./.headroom/memory.db` if present, else `~/.headroom/memory.db`
- **`memory list --search` + filters**: `--scope`/`--session`/`--since`
were silently ignored when `--search` was also set; now filters are
applied to search results
- **`learn --verbosity --apply` now works**: the output shaper is off by
default (`HEADROOM_OUTPUT_SHAPER`); `--apply` now hot-enables it via
`POST /admin/runtime-env` on a running proxy, or prints explicit `export
HEADROOM_OUTPUT_SHAPER=1` instructions when no proxy is running
- **`perf --hours` overflow**: `1e9` hours no longer raises
`OverflowError`; treated as "all data"
- **`evals memory --categories` invalid input**: `abc,1,2` now raises
`BadParameter` instead of a raw `ValueError` traceback

### Documentation (1 commit, 20 files)

Corrected factual errors found by 3 parallel audit agents across root
docs, wiki, and the published Fumadocs site:

**Critical (caused runtime errors or wrong behavior if followed):**
- `simulation.mdx`: `plan.transforms_applied` -> `plan.transforms`;
`plan.savings_percent` -> computed from available fields (both raised
`AttributeError`)
- `shared-context.mdx`: `import { SharedContext } from "headroom"` ->
`"headroom-ai"` (5x `ImportError`)
- `claude-code-azure-foundry.mdx`: `pip install headroom` -> `pip
install headroom-ai`
- `api-reference.mdx` + `configuration.mdx`: `from headroom import
GoogleProvider` -> `from headroom.providers import GoogleProvider`
- `ccr.mdx`: CCR TTL default 300s -> 1800s (30 min)

**Fabricated flags removed:**
- `wiki/proxy.md` + `wiki/cli.md`: `--no-intelligent-context`,
`--no-intelligent-scoring`, `--no-compress-first` (none exist); replaced
with real CCR flags
- `wiki/configuration.md`: `--no-ccr-responses`, `--no-ccr-expansion`
(none exist); replaced with real flags
- `wiki/troubleshooting.md`, `wiki/metrics.md`,
`docs/troubleshooting.mdx`: `headroom proxy --log-level debug` (flag
doesn't exist)

**Stale content corrected:**
- `llms.txt`: telemetry stated as enabled-by-default (it's opt-in); wrap
list had 5 tools (now 11)
- `README.md`: compatibility matrix added 5 missing `wrap` targets;
`unwrap`, `doctor`, `init`/`install`, savings-analytics now mentioned
- `SECURITY.md`: supported version table showed 0.2.x (current: 0.27.x)
- `wiki/learn.md`: 5 missing flags added; verbosity shaper-off behavior
documented
- `wiki/quickstart.md`: "Configuration Reference" linked to `api.md`
(wrong) -> `configuration.md`
- `CacheAlignerConfig.enabled` default corrected: `True` -> `False`
- `opencode.mdx`: `--port` default wrong ("random") -> 8787; `openai`
backend removed
- `CONTRIBUTING.md`: broken Markdown table cell fixed
- `docs/meta.json`: `claude-code-azure-foundry` added to nav (was
unreachable orphan page)
- `configuration.mdx`: SDK modes vs proxy `--mode` now clearly
distinguished

## Test plan

- [x] `python -m pytest tests/ -x -q` — 857 passed, 0 failures
- [x] 41-combination CLI smoke test (all flag combos across 8 commands)
— 0 tracebacks
- [x] `ruff check` on all modified Python files — clean
- [x] Docs changes are removals/corrections of fabricated or stale
content; no new claims introduced
JerrettDavis pushed a commit to peterlodri-sec/headroom that referenced this pull request Jul 14, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.28.0</summary>

##
[0.28.0](headroomlabs-ai/headroom@v0.27.0...v0.28.0)
(2026-06-29)


### Features

* add --disable-kompress-fallback to restore legacy PASSTHROUGH fallback
([headroomlabs-ai#1185](headroomlabs-ai#1185))
([f309244](headroomlabs-ai@f309244))
* add first-class OpenCode support (wrap, learn, mcp install)
([headroomlabs-ai#559](headroomlabs-ai#559))
([91cd210](headroomlabs-ai@91cd210))
* add HEADROOM_KEEPALIVE_EXPIRY to keep upstream connections warm
([headroomlabs-ai#1124](headroomlabs-ai#1124))
([85786b3](headroomlabs-ai@85786b3))
* **azure-foundry:** derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE
([headroomlabs-ai#1138](headroomlabs-ai#1138))
([e5031b0](headroomlabs-ai@e5031b0))
* **cache:** attribute prompt-cache misses to TTL lapse vs prefix change
([headroomlabs-ai#1313](headroomlabs-ai#1313))
([headroomlabs-ai#1343](headroomlabs-ai#1343))
([4658721](headroomlabs-ai@4658721))
* **code:** add Perl support to code-aware compressor
([headroomlabs-ai#1125](headroomlabs-ai#1125))
([f39858c](headroomlabs-ai@f39858c))
* headroom wrap opencode / unwrap opencode CLI
([headroomlabs-ai#1105](headroomlabs-ai#1105))
([b4571cc](headroomlabs-ai@b4571cc))
* **learn:** weight loops in Headroom Learn + RTK-loop eval
([headroomlabs-ai#1160](headroomlabs-ai#1160))
([14e8dc4](headroomlabs-ai@14e8dc4))
* **learn:** write per-project learnings to CLAUDE.local.md by default
([headroomlabs-ai#1115](headroomlabs-ai#1115))
([ced75e4](headroomlabs-ai@ced75e4))
* **proxy:** add request timeout config
([headroomlabs-ai#738](headroomlabs-ai#738))
([c0745d4](headroomlabs-ai@c0745d4))
* **proxy:** pilot hardening — inbound auth, security headers, audit
log, air-gap switch
([headroomlabs-ai#1537](headroomlabs-ai#1537))
([546ab55](headroomlabs-ai@546ab55))
* **proxy:** support glob patterns in exclude_tools
([headroomlabs-ai#870](headroomlabs-ai#870))
([headroomlabs-ai#1259](headroomlabs-ai#1259))
([a2159c0](headroomlabs-ai@a2159c0))
* **read-maturation:** activity-based hold-back Read maturation
(Mechanism B)
([headroomlabs-ai#1068](headroomlabs-ai#1068))
([723b80c](headroomlabs-ai@723b80c))
* **savings:** durable savings ledger + headroom savings command
([headroomlabs-ai#1127](headroomlabs-ai#1127))
([978ffa0](headroomlabs-ai@978ffa0))
* **wrap:** add --1m to preserve the 1M context window on wrap claude
([headroomlabs-ai#1158](headroomlabs-ai#1158))
([headroomlabs-ai#1351](headroomlabs-ai#1351))
([b50d9c1](headroomlabs-ai@b50d9c1))
* **wrap:** make tokensave the primary coding-task compressor, Serena
the backup
([headroomlabs-ai#1230](headroomlabs-ai#1230))
([dca9853](headroomlabs-ai@dca9853))


### Bug Fixes

* **agent-evals:** Phase 0 — coding-agent accuracy A/B framework
([headroomlabs-ai#1037](headroomlabs-ai#1037))
([84f9871](headroomlabs-ai@84f9871))
* **agno:** tolerate streaming tool-call SDK objects in parser
([headroomlabs-ai#1312](headroomlabs-ai#1312))
([headroomlabs-ai#1336](headroomlabs-ai#1336))
([5986c22](headroomlabs-ai@5986c22))
* **bedrock:** add boto3 1.41 + CRT for aws login credentials
([headroomlabs-ai#1486](headroomlabs-ai#1486))
([4db3bc9](headroomlabs-ai@4db3bc9))
* bump codebase-memory-mcp to v0.8.1
([headroomlabs-ai#1284](headroomlabs-ai#1284))
([530318b](headroomlabs-ai@530318b))
* **ccr:** make headroom_retrieve a hash-only full-content lookup
([headroomlabs-ai#1532](headroomlabs-ai#1532))
([c2fc4d3](headroomlabs-ai@c2fc4d3))
* **ccr:** propagate --no-ccr-marker flag to all compressors
([headroomlabs-ai#1022](headroomlabs-ai#1022))
([headroomlabs-ai#1197](headroomlabs-ai#1197))
([0c9b42a](headroomlabs-ai@0c9b42a))
* **ccr:** skip Anthropic marker emission when tool injection is
deferred
([headroomlabs-ai#1273](headroomlabs-ai#1273))
([2cae13d](headroomlabs-ai@2cae13d))
* **ci:** extend gitleaks allowlist to cover test fixtures + verified
examples
([headroomlabs-ai#1539](headroomlabs-ai#1539))
([d2565a6](headroomlabs-ai@d2565a6))
* **ci:** guarantee model present in test shards to end cache-miss
flakiness
([headroomlabs-ai#1399](headroomlabs-ai#1399))
([2e29c72](headroomlabs-ai@2e29c72))
* **ci:** normalize Windows CRLF line endings in PR governance script
([headroomlabs-ai#1012](headroomlabs-ai#1012))
([5194388](headroomlabs-ai@5194388))
* **cli:** add explicit UTF-8 encoding to file I/O in wrap commands
([headroomlabs-ai#1126](headroomlabs-ai#1126))
([headroomlabs-ai#1164](headroomlabs-ai#1164))
([a0cb798](headroomlabs-ai@a0cb798))
* **cli:** fall back gracefully when embedding-server sidecar is absent
([headroomlabs-ai#1206](headroomlabs-ai#1206))
([38f1404](headroomlabs-ai@38f1404))
* **cli:** harden all CLI surfaces + fix docs accuracy
([headroomlabs-ai#1491](headroomlabs-ai#1491))
([bd76235](headroomlabs-ai@bd76235))
* **cli:** wire --http2/--no-http2 (HEADROOM_HTTP2) into proxy command
([headroomlabs-ai#1373](headroomlabs-ai#1373))
([e06b616](headroomlabs-ai@e06b616))
* **cli:** wire --rpm/--tpm and HEADROOM_RPM/HEADROOM_TPM to the Click
proxy command
([headroomlabs-ai#1375](headroomlabs-ai#1375))
([8aab8f2](headroomlabs-ai@8aab8f2))
* **code:** slice tree-sitter byte offsets as UTF-8
([headroomlabs-ai#1332](headroomlabs-ai#1332))
([8238402](headroomlabs-ai@8238402))
* **code:** validate Python compressed syntax
([headroomlabs-ai#1302](headroomlabs-ai#1302))
([cbd361d](headroomlabs-ai@cbd361d))
* **code:** verify a real parse in tree-sitter availability check
([headroomlabs-ai#1231](headroomlabs-ai#1231))
([headroomlabs-ai#1299](headroomlabs-ai#1299))
([5e0bb69](headroomlabs-ai@5e0bb69))
* **codex:** retag threads on init so Codex Desktop history stays
visible ([headroomlabs-ai#961](headroomlabs-ai#961))
([headroomlabs-ai#1349](headroomlabs-ai#1349))
([e6bbc40](headroomlabs-ai@e6bbc40))
* **codex:** stop pinning Codex memory MCP to one project db
([headroomlabs-ai#1269](headroomlabs-ai#1269))
([ad7993b](headroomlabs-ai@ad7993b))
* **dashboard:** include RTK stats in the historical tab
([headroomlabs-ai#1324](headroomlabs-ai#1324))
([35939c3](headroomlabs-ai@35939c3))
* **deps:** remediate dependency CVEs and publish SBOM
([headroomlabs-ai#1509](headroomlabs-ai#1509))
([5771a80](headroomlabs-ai@5771a80))
* **docker:** persist session history across container revisions
([headroomlabs-ai#1118](headroomlabs-ai#1118))
([5912d65](headroomlabs-ai@5912d65))
* **gemini:** offload compression to the executor
([headroomlabs-ai#1382](headroomlabs-ai#1382))
([615848e](headroomlabs-ai@615848e))
* **gemini:** resolve Google model capabilities through ModelRegistry
([headroomlabs-ai#1276](headroomlabs-ai#1276))
([17ecad9](headroomlabs-ai@17ecad9))
* **install:** guard install_agent_ensure against duplicate runtime
spawns
([headroomlabs-ai#1301](headroomlabs-ai#1301))
([8da0b4e](headroomlabs-ai@8da0b4e))
* **install:** repair macOS launchd restart/start lifecycle
([headroomlabs-ai#1290](headroomlabs-ai#1290))
([da1a397](headroomlabs-ai@da1a397))
* **install:** stop duplicating ENTRYPOINT in persistent-docker runtime
command ([headroomlabs-ai#833](headroomlabs-ai#833))
([headroomlabs-ai#1348](headroomlabs-ai#1348))
([feedead](headroomlabs-ai@feedead))
* **io:** use UTF-8 with locale fallback and preserve line endings on
config/text I/O
([headroomlabs-ai#1498](headroomlabs-ai#1498))
([1baa04e](headroomlabs-ai@1baa04e))
* **kompress:** hard override keeps must-keep tokens regardless of model
score ([headroomlabs-ai#1400](headroomlabs-ai#1400))
([42612c8](headroomlabs-ai@42612c8))
* **langchain:** disable streaming on wrapped model during ainvoke()
([headroomlabs-ai#1287](headroomlabs-ai#1287))
([3590046](headroomlabs-ai@3590046))
* **mcp:** register managed installs with a resolvable headroom command
([headroomlabs-ai#1386](headroomlabs-ai#1386))
([22def93](headroomlabs-ai@22def93))
* **mcp:** report correct savings_percent in headroom_compress
([headroomlabs-ai#1106](headroomlabs-ai#1106))
([f216e43](headroomlabs-ai@f216e43))
* **opencode:** write local MCP config
([headroomlabs-ai#1381](headroomlabs-ai#1381))
([6c83790](headroomlabs-ai@6c83790))
* **packaging:** move hnswlib to optional [vector] extra so [all] needs
no C++ toolchain
([headroomlabs-ai#1499](headroomlabs-ai#1499))
([80fa086](headroomlabs-ai@80fa086))
* patch rtk hook script to use absolute path after register_claude_hooks
([headroomlabs-ai#571](headroomlabs-ai#571))
([b618d2d](headroomlabs-ai@b618d2d))
* **perf:** surface RTK/CLI context-tool savings in perf and the session
card ([headroomlabs-ai#1433](headroomlabs-ai#1433))
([9362747](headroomlabs-ai@9362747))
* **proxy:** add --protect-tool-results to prevent lossy compression of
exact-output Bash results
([headroomlabs-ai#1374](headroomlabs-ai#1374))
([51d4bcf](headroomlabs-ai@51d4bcf))
* **proxy:** add an Anthropic buffered read-timeout override
([headroomlabs-ai#1331](headroomlabs-ai#1331))
([3be2526](headroomlabs-ai@3be2526))
* **proxy:** add versionless Vertex AI routes for Claude Code
compatibility
([headroomlabs-ai#1321](headroomlabs-ai#1321))
([bb3e040](headroomlabs-ai@bb3e040))
* **proxy:** bind before eager preload so a hung compressor load can't
block startup
([headroomlabs-ai#1500](headroomlabs-ai#1500))
([d5ac07f](headroomlabs-ai@d5ac07f))
* **proxy:** build SSL contexts for custom CA bundles
([headroomlabs-ai#1134](headroomlabs-ai#1134))
([561ba17](headroomlabs-ai@561ba17))
* **proxy:** forward request-id headers on the streaming path
([headroomlabs-ai#1100](headroomlabs-ai#1100))
([headroomlabs-ai#1258](headroomlabs-ai#1258))
([3d59df7](headroomlabs-ai@3d59df7))
* **proxy:** gate CCR retrieve/compress endpoints to loopback
([headroomlabs-ai#1338](headroomlabs-ai#1338))
([acafb2d](headroomlabs-ai@acafb2d))
* **proxy:** honor force_kompress routing profile
([headroomlabs-ai#996](headroomlabs-ai#996))
([b4682d6](headroomlabs-ai@b4682d6))
* **proxy:** keep large compression results on the critical path
([headroomlabs-ai#296](headroomlabs-ai#296))
([headroomlabs-ai#1352](headroomlabs-ai#1352))
([90734b6](headroomlabs-ai@90734b6))
* **proxy:** offload /v1/compress to the compression executor to stop
blocking the loop
([headroomlabs-ai#1501](headroomlabs-ai#1501))
([27e010e](headroomlabs-ai@27e010e))
* **proxy:** preserve Responses memory continuations with store=false
([headroomlabs-ai#1103](headroomlabs-ai#1103))
([cdfeeac](headroomlabs-ai@cdfeeac))
* **proxy:** queue mid-turn user messages on non-Bedrock streaming path
([headroomlabs-ai#1377](headroomlabs-ai#1377))
([b09f027](headroomlabs-ai@b09f027))
* **proxy:** register interceptor in explicit transforms list when
HEADROOM_INTERCEPT_ENABLED
([headroomlabs-ai#1376](headroomlabs-ai#1376))
([55c700c](headroomlabs-ai@55c700c))
* **proxy:** report real input tokens on streaming message_start
([headroomlabs-ai#1132](headroomlabs-ai#1132))
([headroomlabs-ai#1305](headroomlabs-ai#1305))
([70cc96a](headroomlabs-ai@70cc96a))
* **proxy:** retry upstream 429 with Retry-After on both forwarders
([headroomlabs-ai#1329](headroomlabs-ai#1329))
([90bee89](headroomlabs-ai@90bee89))
* **proxy:** retry upstream 529 overloaded like 429 on both forwarders
([headroomlabs-ai#1495](headroomlabs-ai#1495))
([547b15d](headroomlabs-ai@547b15d))
* **proxy:** stop re-compressing headroom_retrieve output and emitting
unredeemable markers
([headroomlabs-ai#1323](headroomlabs-ai#1323))
([43494ff](headroomlabs-ai@43494ff))
* **proxy:** strip Codex lite header from OpenAI WebSockets
([headroomlabs-ai#1543](headroomlabs-ai#1543))
([5d3803a](headroomlabs-ai@5d3803a))
* **read-lifecycle:** persist STALE Read originals in the CCR store
([headroomlabs-ai#1488](headroomlabs-ai#1488))
([9157173](headroomlabs-ai@9157173))
* recover persistent proxy feature checks and reject non-Copilot
exchange URL
([headroomlabs-ai#1465](headroomlabs-ai#1465))
([16c638b](headroomlabs-ai@16c638b))
* remove agents.md
([headroomlabs-ai#1540](headroomlabs-ai#1540))
([a7d3360](headroomlabs-ai@a7d3360))
* respect COPILOT_PROVIDER_TYPE env var when provider_type is auto
([headroomlabs-ai#549](headroomlabs-ai#549))
([24cf256](headroomlabs-ai@24cf256))
* restore token-mode compression on frozen prefixes
([headroomlabs-ai#1489](headroomlabs-ai#1489))
([8e0dadf](headroomlabs-ai@8e0dadf))
* **router:** degrade to pure-Python detection on native panic
([headroomlabs-ai#1123](headroomlabs-ai#1123))
([headroomlabs-ai#1260](headroomlabs-ai#1260))
([a00fb67](headroomlabs-ai@a00fb67))
* **rtk:** stop hook registration timing out on a forked daemon
([headroomlabs-ai#1314](headroomlabs-ai#1314))
([9758817](headroomlabs-ai@9758817))
* **smart-crusher:** honor enable_ccr_marker on the opaque-blob path
([headroomlabs-ai#1130](headroomlabs-ai#1130))
([27d6f8e](headroomlabs-ai@27d6f8e))
* **subscription:** only reset 5h contribution on real rollover, not API
jitter
([headroomlabs-ai#1255](headroomlabs-ai#1255))
([8d6c175](headroomlabs-ai@8d6c175))
* **subscription:** run transcript token scan off the event loop
([headroomlabs-ai#1263](headroomlabs-ai#1263))
([f03021f](headroomlabs-ai@f03021f))
* surface output reduction without a restart, and explain $0.00 savings
on Python 3.14
([headroomlabs-ai#1296](headroomlabs-ai#1296))
([c30ec4c](headroomlabs-ai@c30ec4c))
* **tests:** reset whole headroom logger subtree so caplog stays
deterministic
([headroomlabs-ai#1117](headroomlabs-ai#1117))
([fda4670](headroomlabs-ai@fda4670))
* **tls:** add HEADROOM_TLS_STRICT=0 toggle for corporate SSL inspection
([headroomlabs-ai#1308](headroomlabs-ai#1308))
([headroomlabs-ai#1341](headroomlabs-ai#1341))
([52068dd](headroomlabs-ai@52068dd))
* **tokenizers:** price CJK/Kana/Hangul at ~1 token per char in
EstimatingTokenCounter
([headroomlabs-ai#1093](headroomlabs-ai#1093))
([a35fe86](headroomlabs-ai@a35fe86))
* **transforms:** gate tool string output from lossy compression
([headroomlabs-ai#1307](headroomlabs-ai#1307))
([headroomlabs-ai#1387](headroomlabs-ai#1387))
([c6c921a](headroomlabs-ai@c6c921a))
* **websocket:** harden responses websocket origin handling
([headroomlabs-ai#1481](headroomlabs-ai#1481))
([c632023](headroomlabs-ai@c632023))
* **windows:** pin UTF-8 encoding on text-mode subprocess calls
([headroomlabs-ai#1311](headroomlabs-ai#1311))
([d633e81](headroomlabs-ai@d633e81))
* **wrap:** add Copilot unwrap command
([headroomlabs-ai#1251](headroomlabs-ai#1251))
([b4fde0c](headroomlabs-ai@b4fde0c))
* **wrap:** isolate proxy stdio from proxy.log on Windows
([headroomlabs-ai#1191](headroomlabs-ai#1191))
([959ab0d](headroomlabs-ai@959ab0d))
* **wrap:** keep agent savings opt-in
([headroomlabs-ai#1294](headroomlabs-ai#1294))
([b829ceb](headroomlabs-ai@b829ceb))
* **wrap:** show the dashboard URL when the proxy is already running
([headroomlabs-ai#1313](headroomlabs-ai#1313))
([b0146c4](headroomlabs-ai@b0146c4))


### Performance Improvements

* **compression:** take large cold-start contexts off the synchronous
kompress path
([headroomlabs-ai#1171](headroomlabs-ai#1171))
([headroomlabs-ai#1298](headroomlabs-ai#1298))
([6c68ff4](headroomlabs-ai@6c68ff4))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
JerrettDavis pushed a commit to peterlodri-sec/headroom that referenced this pull request Jul 14, 2026
…pt manifests (headroomlabs-ai#1303)

## Description

Make deployment-manifest persistence in `headroom/install/state.py`
crash-safe by writing the manifest **atomically**. `save_manifest` used
a plain `path.write_text(...)` (truncate-then-write), so an interrupted
save (Ctrl-C, system restart, container OOM/SIGKILL) could leave a
truncated `manifest.json` on disk.

> **Note (rebased onto current `main`):** since this PR was opened,
headroomlabs-ai#1491 hardened `load_manifest` to raise a typed `ManifestError` on a
corrupt manifest. I've rebased and **dropped my original `load_manifest
→ return None` change in favour of that deliberate typed-error design**,
so this PR now scopes down to the still-missing piece: the **atomic
write** (upstream `save_manifest` is still a plain `write_text`), plus a
regression test for the `ManifestError` path that `main` added without
test coverage.

Closes #

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Add `_atomic_write_text(path, data)`: write to a same-directory temp
file → `flush()` + `os.fsync()` → `os.replace()` (atomic rename on POSIX
and Windows); the temp file is cleaned up if anything fails.
- `save_manifest` now persists via `_atomic_write_text` instead of
`path.write_text(...)`, so a crash between truncate and full write
leaves either the previous file or the complete new one — never a
truncated manifest.
- `load_manifest` is left exactly as `main` has it (raises
`ManifestError` on a corrupt payload) — no behavioural change from me
there.
- Tests: add `test_save_manifest_writes_atomically` (no leftover temp
file; manifest round-trips) and
`test_load_manifest_raises_manifest_error_on_corrupt_payload` (covers
the `ManifestError` path headroomlabs-ai#1491 introduced but did not test).

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ pytest tests/test_install/test_state.py -q
.....                                                                    [100%]
5 passed in 0.11s

$ ruff check headroom/install/state.py tests/test_install/test_state.py
All checks passed!

$ ruff format --check headroom/install/state.py tests/test_install/test_state.py
2 files already formatted

$ mypy headroom/install/state.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- **Environment:** macOS (Darwin), Python 3.13, rebased onto current
`main`.
- **Exact command / steps:** `save_manifest(manifest)` then inspect the
profile dir and reload.
- **Observed result:** after a save the profile directory contains only
`manifest.json` (no leftover `.manifest.json.*.tmp`), and
`load_manifest("default")` round-trips the persisted manifest. A
deliberately-corrupt `manifest.json` (`"{not json"`) makes
`load_manifest` raise `ManifestError` (typed), not a raw
`JSONDecodeError`.
- **Not tested:** the physical-crash-mid-write window is reasoned about
via `os.replace()` atomicity, not fault-injected.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

Documentation / CHANGELOG boxes are unchecked as N/A — this is an
internal persistence-hardening fix with no user-facing surface. The diff
is now small (atomic write + two tests); the corrupt-manifest handling
itself lives in `main` via headroomlabs-ai#1491.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs author action Pull request body or readiness checklist still needs author updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant