headroomlabs-ai/headroom · warning
comparator ccr not implemented (Phase 0)
Error message
comparator ccr not implemented (Phase 0)
What it means
HTTP 403 from require_same_origin (loopback_guard.py:248): the Origin header names a host that is not loopback. This is the CSRF gate for mutating routes — a remote web page's JavaScript POSTing directly to http://127.0.0.1:port is rejected even though the Host header is loopback, because the Origin betrays the cross-site caller.
Source
Thrown at crates/headroom-parity/src/lib.rs:173
// * `cache_aligner` — needs the volatile-content detector, which lives in
// `headroom-proxy` while this crate depends only on `headroom-core`. Either
// add the dependency or move the detector down into core.
// * `ccr` — the fixtures compare `ccr_retrieve` tool-definition injection
// (`headroom/ccr/tool_injection.py`), which has no Rust port at all.
macro_rules! stub_comparator {
($ty:ident, $name:literal) => {
pub struct $ty;
impl TransformComparator for $ty {
fn name(&self) -> &str {
$name
}
fn run(
&self,
_input: &serde_json::Value,
_config: &serde_json::Value,
) -> Result<serde_json::Value> {
anyhow::bail!(concat!("comparator ", $name, " not implemented (Phase 0)"))
}
}
};
}
stub_comparator!(CacheAlignerComparator, "cache_aligner");
stub_comparator!(CcrComparator, "ccr");
/// Real comparator for the `log_compressor` transform.
///
/// Two wrinkles beyond the usual adapter shape:
///
/// * **bias.** Python's signature is `compress(content, context="", bias=1.0)`
/// and the recorder captured only `content`, so every fixture was produced at
/// the default `bias = 1.0`. Rust takes `bias` positionally — pass 1.0.
/// * **CCR store.** The Python compressor owns its store internally, while Rust
/// mints a `cache_key` only when one is handed to `compress_with_store`.
/// Without a store the CCR branch bails out with `"no store provided"` andView on GitHub (pinned to 322425c43b)
Solutions
- Serve your dashboard/control page from the same loopback origin as the proxy
- Add the dashboard's domain to the trusted-dashboard-client allowlist the server composes around these dependencies (server.py wraps them with an escape hatch)
- Use a non-browser client for automation — it sends no Origin and is not affected
Example fix
// before: page served from https://dashboard.example
fetch('http://127.0.0.1:8080/settings', {method: 'POST', ...})
// after: serve the control page from the proxy origin and use a relative URL
fetch('/settings', {method: 'POST', ...}) Defensive patterns
Strategy: validation
Validate before calling
// Only call mutating endpoints from the proxy's own origin
if (location.hostname !== "127.0.0.1" && location.hostname !== "localhost") {
throw new Error("cross-origin calls are rejected by the same-origin guard");
} Try / catch
if (resp.status === 403) { /* re-serve the control page from the proxy origin */ } Prevention
- Host dashboards on the proxy origin or use the trusted-dashboard-client allowlist
- Expect 403 for browser cross-origin POSTs even with correct Host headers
When it happens
Trigger: JavaScript on https://evil.example running fetch('http://127.0.0.1:8080/settings', {method:'POST', body:...}) with a simple Content-Type; any browser page on a non-loopback origin targeting the proxy's mutating endpoints.
Common situations: Drive-by localhost CSRF attempts (the reason the guard exists); a legitimately hosted dashboard on another domain trying to drive the local proxy; browser extensions injecting requests from their own origin.
Related errors
- comparator cache_aligner not implemented (Phase 0)
- recommendations TOML parse error: {0}
- fixture {} declares transform={} but lives under {}
- sha256 mismatch for {path.name}: expected {expected}, got {g
- Invalid {TRUSTED_DASHBOARD_CLIENT_CIDRS_ENV} entry: {exc}
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/b3ac0b05c06c0a49.
Report an issue: GitHub.