Commit graph

2630 commits

Author SHA1 Message Date
JerrettDavis
1d440023b6 Merge upstream/main into fix/copilot-oauth-runtime
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:44:58 -05:00
JerrettDavis
94cf57ac4d test: sync release workflow assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:38:01 -05:00
JerrettDavis
0a8a6dca1e test: derive canonical release version
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:32:10 -05:00
Tejas Chopra
5738339524
Merge pull request #219 from JerrettDavis/fix/python-github-packages-publish
ci: publish Python distributions to GitHub releases
2026-04-21 21:30:16 -07:00
JerrettDavis
af784465df test: restore cli package state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:23:58 -05:00
JerrettDavis
e5cad5ed92 fix: satisfy cross-platform mypy for copilot auth
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:10:21 -05:00
JerrettDavis
f5b959a470 test: isolate copilot oauth suites
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:01:31 -05:00
JerrettDavis
d60cf7914c fix: support live copilot oauth runtime
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 22:48:57 -05:00
chopratejas
7ed2b0ba34 Sync plugins to 0.9.2, pyproject canonical at 0.9.1 [skip ci] 2026-04-21 20:36:51 -07:00
Tejas Chopra
1f978f9235
Merge pull request #229 from JerrettDavis/feat/copilot-oauth
fix: support GitHub Copilot OAuth sessions
2026-04-21 20:14:36 -07:00
JerrettDavis
7989581350 fix: support copilot oauth sessions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 22:11:30 -05:00
Tejas Chopra
da54d4a80a
Merge pull request #224 from gglucass/codex/compact-stats-history-default
Compact /stats-history responses by default
2026-04-21 20:09:15 -07:00
Tejas Chopra
ed7d4942aa
Merge pull request #226 from JerrettDavis/feat/init-agent-hooks
feat(init): Add durable headroom init command for agent hooks
2026-04-21 20:08:49 -07:00
JerrettDavis
9ba9a59f78 test: isolate windows init branches from os globals
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 21:11:03 -05:00
JerrettDavis
c1b648664e test: raise init command branch coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:59:24 -05:00
JerrettDavis
56f6307665 fix: support py310 version sync scripts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:42:56 -05:00
JerrettDavis
b852460af9 chore: normalize line endings in init diffs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:17:14 -05:00
JerrettDavis
a278a7b0ba test: cover init install flows end to end
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:15:11 -05:00
JerrettDavis
c5d795c2af build: sync agent hook manifests to repo semver
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:03:14 -05:00
JerrettDavis
5f361f4eb5 docs: add codecov badge to README 2026-04-21 19:54:47 -05:00
JerrettDavis
3a999d1562 feat: add durable init command for agent hooks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 19:39:06 -05:00
Tejas Chopra
80920ed0e8
Merge pull request #212 from Kayzo/fix/onnx-memory-retention
fix(onnx): reduce retained cpu memory
2026-04-21 13:06:08 -07:00
Kayzo
1961cebd00 chore: format proxy route tests with ruff 2026-04-21 19:38:32 +00:00
Garm
a858a0fd4b style: format test_proxy_savings_history.py for CI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 18:59:24 +02:00
Garm
ea0f024b67 Compact /stats-history responses by default 2026-04-21 18:08:33 +02:00
JD Davis
e854ba1771
Merge branch 'main' into fix/python-github-packages-publish 2026-04-21 07:10:17 -05:00
Tejas Chopra
0d6ba9972a
Merge pull request #220 from JerrettDavis/feat/transformations-live-feed-v3
feat(dashboard): live feed sidebar with message transformations
2026-04-21 00:15:50 -07:00
Tejas Chopra
329956e197
Merge pull request #218 from JerrettDavis/fix/commitlint-merge-commits
fix: skip commitlint for PR merge commits
2026-04-21 00:15:36 -07:00
Tejas Chopra
b789dca81f
Merge pull request #194 from gglucass/feat/track-embedded-installs
fix(telemetry): add headroom_stack and install_mode identity fields
2026-04-21 00:14:57 -07:00
Garm
66f12b4f68 Merge branch 'main' into feat/track-embedded-installs
# Conflicts:
#	headroom/proxy/server.py
2026-04-21 09:12:04 +02:00
Tejas Chopra
724c2987b3
Merge pull request #221 from chopratejas/docs/port-wiki-pages-to-fumadocs
docs: port Docker-native, filesystem-contract, and persistent-installs pages to Fumadocs
2026-04-20 23:07:40 -07:00
chopratejas
6d250554f7 docs: port Docker-native, filesystem-contract, and persistent-installs pages to Fumadocs
These three wiki pages documented shipped features (PRs #139, #145, #191)
but were never ported into the new Fumadocs site introduced in 911eb85
("new docs UI + ts doc coverage"). Users browsing docs.* couldn't find
the Docker-native install flow, the canonical filesystem contract, or
the persistent-install CLI surface.

- Add docs/content/docs/docker-install.mdx (one-line installer, native
  wrapper behavior, persistent-docker lifecycle, Compose runtime).
- Add docs/content/docs/filesystem-contract.mdx (two-root model,
  precedence, bucket assignments, Docker overlap between
  HEADROOM_WORKSPACE and HEADROOM_WORKSPACE_DIR).
- Add docs/content/docs/persistent-installs.mdx (runtime matrix,
  presets, scopes, health/wrap behavior, Docker-native relationship).
- installation.mdx: add a Callout in the Docker section linking to the
  new docker-install page so pip/npm/docker landing users find it.
- meta.json: surface the three new pages in the sidebar under
  Getting Started and Configuration.

Wiki source files in wiki/ are left in place for now; they can be
deprecated in a follow-up once the new site is confirmed as canonical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:05:15 -07:00
JerrettDavis
c15f9836b6 test(dashboard): fix playwright importorskip placement
Move importorskip after playwright import so module-level
import error triggers skip rather than collection error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 00:10:05 -05:00
JerrettDavis
cc7ad822f4 test(dashboard): add playwright fixture and skip when not available
Add browser/page fixtures and importorskip guard so dashboard
E2E tests are skipped (not failed) when playwright is not installed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 00:01:22 -05:00
JerrettDavis
0aae886f77 feat: add live transformations feed to dashboard
- New /transformations/feed endpoint returning message diffs
- Alpine.js drawer UI with virtual scrolling and auto-stream pause
- Live Feed button hidden when log_full_messages=false
- Added --log-messages CLI flag to enable full message logging
- Backend stores request/response messages when enabled

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 23:47:43 -05:00
JerrettDavis
8bf11d22ad ci: retry macos pytest install 2026-04-20 23:36:02 -05:00
JerrettDavis
f5dfdda253 ci: publish Python distributions to GitHub releases 2026-04-20 23:32:31 -05:00
Tejas Chopra
21d909a7ed
Merge pull request #210 from chopratejas/feat/bundled-cli-tools-and-interceptors
feat: bundle ast-grep/difftastic/scc + generic tool_result interceptor framework
2026-04-20 20:52:38 -07:00
JerrettDavis
c6c43f41f1 fix: skip commitlint for PR merge commits 2026-04-20 22:52:20 -05:00
Tejas Chopra
043045feb1
Merge pull request #217 from JerrettDavis/fix/openclaw-local-node-publish
Fix OpenClaw GitHub Packages build
2026-04-20 20:45:58 -07:00
JerrettDavis
ce405b0a5a Fix OpenClaw GPR package build 2026-04-20 22:44:11 -05:00
Tejas Chopra
8ba1ebd88e
Merge pull request #214 from adryanev/fix/proxy-followup-resilience-guards
Add timeout guards to Anthropic pre-upstream work
2026-04-20 20:26:41 -07:00
Tejas Chopra
16d460849b
Merge pull request #213 from JerrettDavis/ci-fork-release-publishing
fix: complete fork-friendly release publishing
2026-04-20 20:25:54 -07:00
chopratejas
504f20a1f7 fix(review-3): address third-pass PR #210 feedback
7 new "important" findings from the third review pass:

1. base.py:158 — JSONDecodeError on OpenAI tool_call arguments is now
   logged at DEBUG (was silently replaced with {}). Callers can now
   diagnose why range-key checks didn't fire on malformed calls.

2. base.py:234,248 — logger.warning for interceptor transform() and
   key() failures now passes `exc_info=True` so stack traces are
   preserved in logs.

3. base.py:306 — progressive disclosure now pre-seeds `fired` from the
   frozen prefix. A file first Read in the cached prefix no longer gets
   re-outlined when the model Reads it again in the mutable tail.
   apply_to_messages() now takes a `frozen_count` kwarg and handles the
   split internally; the Transform adapter simplifies.

4. astgrep.py:114 — broadened `except` around binaries.resolve() to
   catch the full BinaryError hierarchy plus KeyError + OSError. In
   offline mode this is the difference between a debug log and a
   warning on every single request.

5. astgrep.py:181 — chmod 0700 failure on the temp dir now logs at
   DEBUG. Silent swallow meant a multi-tenant host could leave
   untrusted content world-readable with no indication the hardening
   skipped.

6. cli/proxy.py:297 — explicit `--intercept-tool-results` now fails
   fast (`sys.exit(1)`) when the critical tool can't be installed.
   Previously it warned and started with non-functional interceptors.

7. pipeline.py:85 — interceptor gate now checks
   `HeadroomConfig.intercept_tool_results` first, env var second.
   Non-CLI callers (SDK, tests, embedded) can enable via config
   instead of having to touch os.environ. Added the config field with
   default False.

Plus a new test: `test_progressive_disclosure_respects_frozen_prefix_history`
proves a file Read in the frozen prefix blocks re-outline in the tail.

46 tests total, ruff + mypy clean.
2026-04-20 20:24:47 -07:00
Adryan Eka Vandra
7e53362e62
style: format files for CI 2026-04-21 09:49:26 +07:00
Adryan Eka Vandra
4e9348dec4
fix: add anthropic pre-upstream timeouts 2026-04-21 09:48:32 +07:00
chopratejas
a847387b09 chore: run ruff format on recently merged test files
Three test files merged in #196 via Pi/Codex route-aliases PR failed
ruff format --check, blocking CI on this PR. Auto-formatting them here
so the branch passes. No logic changes — only whitespace / trailing
comma normalization that ruff format applies.
2026-04-20 17:36:44 -07:00
chopratejas
78d935fcf9 fix: docker install fails with PermissionError in readonly cache dir
Two bugs collided to break the Docker-native install CI:

1. `ensure_tools()` ran unconditionally at every proxy startup, even when
   `--intercept-tool-results` was not passed. The feature is opt-in, so
   there's no reason to pay the binary-fetch cost (or risk a failure) when
   nothing will use them.

2. The fetch loop caught PlatformNotSupported / OfflineError /
   BinaryFetchError / Sha256Mismatch but not `PermissionError`. In
   containerized environments where the home dir / cache dir isn't
   writable, `binary_path.parent.mkdir()` raises PermissionError
   (subclass of OSError), which propagated out of ensure_tools() and
   crashed proxy startup.

Fixes:
- Move ensure_tools() inside the `if intercept_tool_results:` branch in
  cli/proxy.py so the base case never triggers a fetch.
- Catch OSError (covers PermissionError, ENOSPC, etc.) in ensure_tools()
  so sandboxed / readonly filesystems degrade to no-op instead of
  crashing. Interceptors fall back to pass-through when their tool isn't
  resolvable.

Adds regression test `test_ensure_tools_survives_readonly_cache_dir` that
points the cache at a chmod-0500 parent and asserts ensure_tools()
returns without raising.
2026-04-20 17:36:44 -07:00
chopratejas
e165a579f8 fix(review): address PR #210 feedback — race, query-params, SHA logging, frozen prefix, etc
Addresses all 24 inline comments across the two review passes.

**CRITICAL fixes:**
- binaries.py: PID-scoped partial-file name prevents concurrent `headroom proxy`
  starts from clobbering each other's downloads.
- binaries.py: strip URL query params before computing the download filename
  (was breaking archive-type detection for mirror URLs with `?token=...`).
- cli/tools.py: `--force` cleanup now logs failures and bumps exit_code
  instead of silently swallowing exceptions.

**HIGH fixes:**
- binaries.py: log at INFO when SHA256 is unpinned; expose `sha_pinned` in
  doctor's status output.
- proxy/interceptors/base.py: `_FAILURES` counter + `interceptor_failure_counts()`
  getter; incremented on every `matches()`/`transform()`/`key()` exception so
  dashboards can distinguish "nothing eligible" from "everything crashing".
- cli/proxy.py: validate critical tools resolved when
  `--intercept-tool-results` is set; warn (don't fail) if a dependency is
  missing.
- proxy/interceptors/base.py: compute `tokens_before` from the original
  messages via `count_messages()` instead of back-calculating from
  `tokens_after + sum(saved)` (which double-counted message-level overhead).
- proxy/interceptors/astgrep.py: write untrusted tool_output into a private
  mode-0700 `tempfile.mkdtemp()` directory, not directly into shared `/tmp`.
- proxy/interceptors/base.py: `ToolResultInterceptorTransform.apply()` now
  honors `frozen_message_count` — leading cached-prefix messages are passed
  through untouched to preserve provider prefix caches.

**MEDIUM fixes:**
- proxy/interceptors/base.py: pre-built O(1) tool_use index replaces the
  O(n²) per-tool-result linear scan.
- proxy/interceptors/base.py: broken `progressive_disclosure_key()` now
  skips the interceptor entirely rather than firing without key protection.
- proxy/interceptors/astgrep.py: distinguish ast-grep rc=1 (no matches) from
  rc>=2 (real errors — bad syntax, missing grammar, corrupt binary).
- proxy/interceptors/astgrep.py: count JSON parse failures; warn when all
  lines fail to parse (indicates version mismatch).
- binaries.py: musl detection falls back to checking `/lib/ld-musl-*.so.1`
  when `ldd` is absent (Alpine).
- proxy/interceptors/astgrep.py: use `tempfile.mkdtemp()` + `shutil.rmtree`
  instead of `NamedTemporaryFile(delete=False)`; cleans up on Windows.
- binaries.py: chmod failures on POSIX now log a warning (only swallow on
  Windows where .exe is implicitly executable).
- tests/test_binaries.py: `test_mirror_substitution` now uses
  `monkeypatch.setenv()` instead of raw `os.environ` manipulation.
- tools.json: add `linux-x86_64-musl` and `linux-aarch64-musl` entries for
  `difft`; document the shared-asset strategy for both tools.
- proxy/interceptors/astgrep.py: log a debug line when
  `progressive_disclosure_key()` returns None for a tool whose tool_input
  shape we don't recognize.
- proxy/interceptors/base.py: moved `import json` to module top (was inside
  `_find_tool_use` hot loop).
- binaries.py: fix bare `.gz` detection — now explicitly excludes
  `.tar.gz`/`.tgz` instead of relying on a brittle "no dots" heuristic.
- proxy/interceptors/astgrep.py: provenance comment on each `_RANGE_KEYS`
  entry so future maintainers know which tool defined which key.
- cli/tools.py: comment explaining os.execv's lack of Python finalizer
  cleanup.
- proxy/interceptors/base.py: `InterceptionResult` now `frozen=True`.

**Test gaps closed:**
- Interceptor failure isolation (transform() raises → request survives,
  counter increments).
- Broken key() skips interceptor entirely.
- Refuse-to-enlarge guard (rewrite larger than original → pass through).
- Orphaned tool_result (no matching tool_use) doesn't crash.
- ToolResultInterceptorTransform.apply() happy path + frozen_message_count.
- ensure_tools() partial failure (one tool fetch fails, others succeed,
  proxy still starts).
- Mirror URL with query params doesn't leak into download filename.

44 tests total; ruff + mypy clean.
2026-04-20 17:36:44 -07:00
chopratejas
7cd67ef06d fix: bundle ast-grep/difftastic/scc + generic tool_result interceptor framework
What this does, in plain terms:

Headroom's proxy now ships with three CLI tools (ast-grep, difftastic,
scc) that it can use to shrink tool_result payloads before they reach
the model. The goal is simple: when Claude Code (or Codex, Aider, etc.)
asks the model to reason about a big file or diff, we swap the verbose
output for a compact, same-meaning version. Fewer tokens per turn, same
answers, lower bill.

Today a single interceptor is wired: ast-grep on Read. When an agent
reads a large code file, the proxy replaces the file body with an
outline of its top-level functions/classes plus docstrings. In live
tests that cut prompt tokens 74–76% on both OpenAI and Anthropic,
same answer either way.

How it works:
- `pip install headroom-ai` now installs ast-grep via a PyPI wheel
  (core dep). difftastic and scc are fetched once at proxy startup
  from pinned upstream GitHub releases and cached per-user.
- A generic registry (`headroom/proxy/interceptors/`) lets us add more
  tool-aware rewrites in one file each: declare `matches()` and
  `transform()`, call `register()`, done. No proxy or metrics plumbing
  per tool.
- Safety rails built in: pass-through when a Read specifies a line
  range; second Read of the same file in a conversation returns full
  content (progressive disclosure); any failing interceptor logs and
  skips, never crashes a request.

Opt-in for now:
- Off by default while this ships. Turn on with
  `headroom proxy --intercept-tool-results` or
  `HEADROOM_INTERCEPT_ENABLED=1`, so we can measure before flipping
  defaults.

What users see after turning it on:
- First `headroom wrap claude` boot is ~5s longer (binaries fetched).
  Every subsequent run is cache-only.
- Existing `transforms_applied` field in metrics gets entries like
  `interceptor:ast-grep`, so savings show up in current dashboards
  and HTML reports with no UI change.

Other housekeeping in this PR:
- uv.lock moved to .gitignore — regenerated locally per environment.
- 35 unit + integration tests, ruff + mypy clean.
- Dead-code audit done: removed `binaries.run()`, `needs_filesystem`
  plumbing, unused `_kind` tuple elements, unused `tool_output`
  parameter, and the never-set HEADROOM_SKIP_TOOLS_BOOTSTRAP env.
2026-04-20 17:36:44 -07:00