Commit graph

25 commits

Author SHA1 Message Date
JD Davis
997a47992c
fix(copilot): preserve native enterprise model routing (#2998)
## Description

GitHub Copilot Enterprise/Business users without a BYOK provider key
were routed through Copilot CLI's single-model provider override. Native
model aliases and runtime `/model` switches were therefore forwarded
literally to the override and rejected with `400 model not supported`.
This change routes implicit GitHub OAuth through Copilot's native API
surface while retaining explicit subscription and provider-key behavior.

Closes #1910

## 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

- Added explicit `--native` routing and made it automatic for implicit
GitHub OAuth without BYOK.
- Clears every Copilot BYOK variable before native launch.
- Routes both OpenAI and Anthropic protocol targets through the resolved
tenant Copilot host.
- Preserves Enterprise/Business native aliases and runtime model
switching.
- Rejects BYOK-only options when native routing is selected.
- Refuses known Copilot bundles that do not reference `COPILOT_API_URL`,
avoiding silent proxy bypass.
- Preserves explicit `--subscription` and provider-key BYOK semantics.
- Added coverage for unreadable and unverifiable Copilot CLI bundles.

## 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
884 passed, 4 skipped in 103.11s
ruff check .: All checks passed
ruff format --check .: 1412 files already formatted
mypy headroom/providers/copilot/wrap.py headroom/cli/wrap.py:
Success: no issues found in 2 source files
```

Exact-head CI is entirely green on
`0aca48c096`.

## Real Behavior Proof

- Environment: macOS arm64/Python 3.13 locally; GitHub-hosted macOS and
Ubuntu native-wrap jobs.
- Exact command / steps: invoke `headroom wrap copilot` with implicit
OAuth and an Enterprise model alias; inspect the captured child/proxy
environment and resolved target URLs; exercise explicit native conflicts
and bundle-support probes.
- Observed result: native launch uses `COPILOT_API_URL`, clears all BYOK
state, and points both protocol targets at the tenant host. Native-wrap
jobs are green on macOS and Ubuntu for the refreshed head.
- Not tested: live request against a real Enterprise tenant; the
repository has no organization Enterprise credential available to CI.

## Runtime Rollout Safety

- Rollout-managed feature(s): implicit native Copilot routing for GitHub
OAuth sessions without BYOK.
- Minimum rollout channel: normal patch release.
- Stable/default behavior changed: implicit OAuth now uses native
routing; explicit subscription and BYOK paths are unchanged.
- Kill switch / disable path: use an explicit supported provider-key
BYOK configuration; native mode also fails closed when CLI support is
known absent.
- Unsafe override required: none.
- Qualification impact: native-wrap macOS/Ubuntu, Docker wrapper, full
Python matrix, and Copilot focused suites must pass.
- Rollback path: human revert of this PR restores the fixed-wire OAuth
behavior; no configuration migration is persisted.

## 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
- [x] I have made corresponding changes to the documentation — CLI help
and inline routing documentation; no separate guide required
- [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
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable; CLI routing change.

## Additional Notes

Human review only. No merge or auto-merge is configured. Refreshed from
main after #2996; the MCP cap `mcp>=1.28.1,<2.0.0` is preserved.
2026-08-25 21:46:31 -05:00
Matt Van Horn
1db6d88ab4
fix(wrap): honor Copilot OAuth wire-api override and model default (#2387)
## Description

`headroom wrap copilot -- --model gpt-5.4` fails with `400 The requested
model is not available for integrator "copilot-language-server"`, even
though the same GitHub account uses GPT-5.4 fine in the native `copilot`
CLI. On the hosted Copilot OAuth path (authenticated via `headroom
copilot-auth`, no `--subscription`), `headroom/cli/wrap.py` forced the
`completions` wire API for every non-`--subscription` launch and ignored
a caller-supplied `COPILOT_PROVIDER_WIRE_API`. GPT-5.x / o-series
reasoning models need the `responses` wire API.

The OAuth path now honors a valid inherited `COPILOT_PROVIDER_WIRE_API`
(`completions` or `responses`) and otherwise uses the model-aware
default via `_copilot_default_wire_api_for_model(selected_model)`, so
GPT-5.x routes to `responses` while GPT-4.1 stays on `completions`. The
`--subscription` path is unchanged.

This supersedes the earlier closed #2243, which mixed the fix with
unrelated proxy/CI changes and hit merge conflicts. This PR ships only
the `headroom/cli/wrap.py` wire-api selection plus regression tests,
rebased clean on `main`.

Closes #2222

## 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

- Hosted Copilot OAuth launch resolves the wire API from an explicit
`COPILOT_PROVIDER_WIRE_API` env value when it is
`completions`/`responses`, else from
`_copilot_default_wire_api_for_model(selected_model)` instead of a
hardcoded `completions`. The `subscription`-only gating on the
model-aware default is removed so OAuth and subscription paths pick the
same model-correct wire API.

## Testing

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

### Test Output

```text
$ .venv/bin/python -m pytest tests/test_cli/test_wrap_copilot.py tests/test_provider_copilot_wrap.py -q
70 passed in 0.28s
```

New tests cover: the OAuth path honoring an inherited
`COPILOT_PROVIDER_WIRE_API`, the OAuth path resolving GPT-5.4 to
`responses`, and `default_wire_api_for_model("gpt-5.4")` returning
`responses`.

## Real Behavior Proof

- Environment: local checkout, Python 3.14, target tests only.
- Exact command / steps: `.venv/bin/python -m pytest
tests/test_cli/test_wrap_copilot.py tests/test_provider_copilot_wrap.py
-q`
- Observed result: 70 passed; the OAuth-path tests assert
`env["COPILOT_PROVIDER_WIRE_API"] == "responses"` for GPT-5.x and honor
an inherited override.
- Not tested: the end-to-end live Copilot `400` reproduction, which
needs a real Copilot OAuth session plus a GPT-5.x request. The wire-api
selection that caused the `400` is covered by the unit tests above.

## 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
- [ ] 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
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A — CLI behavior change covered by the unit tests above.

## Additional Notes

The change is scoped to the wire-api selection line; the
`--subscription` path and the existing explicit `--wire-api` CLI flag
are untouched.

AI was used for assistance.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-08-12 00:04:42 -05:00
Tejas Chopra
e0ce4b1d48
fix: remove rtk and lean-ctx CLI context tools (#2677)
## Description

Removes both third-party CLI context tools — **rtk** and **lean-ctx** —
and with them the context-tool selector itself. Headroom no longer
downloads, installs or configures either one, and there is no
replacement.

The previous pass (#2344) gated only three entry points inside
`headroom/cli/wrap.py`. That left the feature reachable in practice:

| Gap | Effect |
|---|---|
| `scripts/install.sh:1544`, `install.ps1:1681` | Ran `rtk init --global
--auto-patch` from bash/PowerShell, **bypassing the Python gate
entirely** — `curl \| sh` still wrote a Claude Code `PreToolUse` hook
regardless of `HEADROOM_RTK` |
| `wrap.py` `_setup_context_tool_for_agent` | **`wrap openhands` was
broken by default**: `rtk_required=True` met a gate returning `None` →
`SystemExit(1)`. Invisible because all 8 openhands tests patched
`_ensure_rtk_binary` to a fake path |
| `proxy/helpers.py`, `subscription/tracker.py` | Proxy shelled out to
`rtk gain` from `/stats`, the dashboard and `headroom perf`; the tracker
polled it per contribution (`_RTK_WIRING_DEFAULT = "enabled"`) |
| No cleanup path | Nothing removed artifacts an earlier default had
installed, so a machine that once ran the old default kept rtk in the
loop forever (#1669, #1955) |

Also worth noting: the rtk binary download had **no SHA or signature
verification** — only `rtk --version` as a smoke test.

## Type of Change

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

## Changes Made

**Removed** — `headroom/rtk/` and `headroom/lean_ctx/` packages,
`headroom/cli/wrap_rtk_metrics.py`, `_selected_context_tool` /
`_setup_context_tool_for_agent` / `_VALID_CONTEXT_TOOLS`, the `--rtk` /
`--no-rtk` / `--no-project-rtk` / `--keep-rtk` flags across all 18 wrap
subcommands, `HEADROOM_RTK*`, the proxy-side `rtk gain` polling, the
dashboard CLI-filtering panel (rows + all 8 `cliFiltering*` Alpine
getters), `paths.rtk_path()` / `lean_ctx_path()`, the SDK path helpers,
`benchmarks/rtk_loop_learn_eval.py`, and the `headroom/rtk/**` CI path
filters.

**Fails loudly, not silently** — `--context-tool` / `--no-context-tool`
/ `HEADROOM_CONTEXT_TOOL` are kept solely to error out. They live in
shell profiles, aliases and CI jobs, and accepting them as a no-op would
read as Headroom having quietly stopped working. The installers reject
them too, which matters more than it looks: their arg parsers forward
the first unknown flag **and everything after it** to the wrapped tool,
so a leftover `--no-rtk` would have silently swallowed a following
`--port` and then been ignored downstream.

**New `headroom/context_tool_cleanup.py`** — deleting the code cannot
help a machine that already ran the old default, since the hooks,
binaries and injected guidance are durable on disk.
`purge_context_tool_artifacts()` runs once per `wrap`/`unwrap` and
removes the registered hook entries, the generated hook scripts, the
Headroom-managed `~/.local/bin` symlinks, the vendored
`~/.headroom/bin/{rtk,lean-ctx}` binaries, the `lean-ctx` MCP server
entry and the marker-fenced instruction blocks. Deliberately
conservative: idempotent, **skips** a malformed config rather than
overwriting it, and only unlinks a symlink resolving inside Headroom's
own bin dir so a user's own build is untouched. It reports on
**stderr**, because `wrap/unwrap openclaw --prepare-only` emit
machine-readable JSON on stdout as their entire contract. Skipped for
`wrap selfheal` (runs from a SessionStart hook; must not race Claude
Code's writer for `~/.claude.json`) and for `--help`, which must stay
read-only.

**Client-config hardening** (discovered while investigating a "corrupted
Serena settings file" report) — `wrap.py` reset a settings file to `{}`
when an existing file would not parse, then wrote that back. One
hand-edited typo or a transient `EACCES`/`EINTR` on a valid file
destroyed the user's `permissions`, `env` and `hooks`, on **every
`headroom wrap claude`**. It now refuses to write. Separately,
`fsutil.write_text` is now atomic (temp file + `fsync` + `os.replace`),
fixing all 14 non-atomic client-config writes at once; it follows
symlinks rather than replacing them (dotfile managers) and preserves an
existing file's mode.

**Deliberately kept** — `rtk` stays in the wrapper-peel list in
`transforms/content_router.py`. It sits beside `sudo`/`env`/`timeout` as
shell-command grammar, so `rtk cat f` is still classified as a file read
for anyone running their own rtk install, which the purge intentionally
leaves alone.

## 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
$ ruff check headroom/ tests/ e2e/ --exclude headroom/dashboard/templates
All checks passed!

$ ruff format --check headroom/ tests/ e2e/ --exclude headroom/dashboard/templates
1255 files already formatted

$ mypy headroom/
Success: no issues found in 508 source files

$ pytest tests/test_context_tool_cleanup.py -q
11 passed

$ pytest tests/test_fsutil.py -q
12 passed

$ pytest tests/test_cli/test_wrap_codex.py -q            # 89 tests
89 passed in 431.68s
$ pytest tests/test_cli/test_wrap_opencode.py -q
39 passed in 257.46s
$ pytest tests/test_cli/test_wrap_helpers.py -q
45 passed
$ pytest tests/test_paths.py -q
75 passed
$ pytest tests/test_cli/test_unwrap_claude.py -q
14 passed
$ pytest tests/test_proxy_savings_history.py -q
39 passed
$ pytest tests/test_cli/test_wrap_copilot.py -q
27 passed
$ pytest tests/test_cli/test_wrap_zcode.py -q
20 passed
$ pytest tests/test_subscription_tracker.py -q
9 passed
$ pytest tests/test_proxy_dashboard_stats_cache.py -q
5 passed, 1 skipped
```

Repo-wide grep for 14 removed symbols (`headroom.rtk`,
`headroom.lean_ctx`, `_ensure_rtk_binary`, `_selected_context_tool`,
`_get_context_tool_stats`, `rtk_path`, `lean_ctx_path`,
`wrap_rtk_metrics`, `HEADROOM_RTK`, `cli_tokens_avoided`,
`tokens_saved_rtk`, …) across `*.py`, `*.ts`, `*.sh`, `*.ps1`, `*.yml`,
`*.html`: **zero hits**.

Notable test changes: `test_wrap_openhands.py` no longer patches
`_ensure_rtk_binary` and asserts `wrap openhands --prepare-only` exits 0
unpatched — the regression that was previously masked.
`test_wrap_continue.py` and `test_wrap_hintfile_agents.py` were removed
(every test drove RTK instruction injection). A new
`test_subscription_tracker.py::test_load_state_written_before_cli_context_tools_were_removed`
proves a pre-removal `subscription_state.json` still loads.

## Real Behavior Proof

- **Environment:** macOS 15.4 (darwin 25.4.0), Python 3.12.6, Headroom @
this branch, real `~/.headroom` and `~/.claude` on the dev machine.
- **Exact command / steps and observed result:**

```text
# 1. Retired flag fails loudly instead of silently no-op'ing
$ headroom wrap codex --prepare-only --context-tool rtk
Error: CLI context tools (rtk, lean-ctx) have been removed from Headroom: they
rewrote shell commands through a third-party binary Headroom no longer manages.
Drop --context-tool / --no-context-tool and unset HEADROOM_CONTEXT_TOOL;
`headroom wrap` uninstalls what they left behind on first run.

$ HEADROOM_CONTEXT_TOOL=lean-ctx headroom wrap codex --prepare-only
Error: CLI context tools (rtk, lean-ctx) have been removed from Headroom: ...

# 2. install.sh rejects the retired flags (extracted parse_wrap_args harness)
['--no-rtk', '--port', '9999']   rc=1  ERROR: CLI context tools ... Drop --no-rtk
['--context-tool=rtk']           rc=1  ERROR: CLI context tools ... Drop --context-tool
$ bash -n scripts/install.sh   # syntax OK

# 3. Purge ran against the real machine, which had all the orphaned artifacts
$ python -c "from headroom.context_tool_cleanup import purge_context_tool_artifacts; ..."
  removed ~/.headroom/bin/lean-ctx        (51 MB)
  removed ~/.headroom/bin/rtk             (7.7 MB)
  removed ~/.local/bin/rtk                (symlink into ~/.headroom/bin)
  removed ~/.claude/hooks/rtk-rewrite.sh
  removed 8 lean-ctx-* hook scripts
# ~/.claude.json afterwards: 90 top-level keys, 19 projects, mcpServers unchanged
# → ~59 MB reclaimed, no unrelated key touched

# 4. stdout stays machine-readable while the purge reports (planted a fake artifact)
$ headroom wrap openclaw --prepare-only --gateway-provider-id codex >out 2>err
$ cat out
{"enabled":true,"config":{"proxyPort":8787,...}}     # parses as JSON
$ cat err
Retired CLI context tool cleanup: removed /Users/tcms/.headroom/bin/rtk

# 5. --help is inert (planted artifact survives), a real run purges
$ headroom wrap codex --help   → artifact survived: CORRECT
$ headroom wrap openclaw --prepare-only → purged: CORRECT

# 6. MCP purge dry-run against a copy of the real 82 KB ~/.claude.json
top-level keys 90 -> 90;  projects 19 -> 19;  LOST keys: none
all content outside mcpServers byte-identical: True
```

Dashboard rendered via the Playwright test after the panel removal:
"Token Savings" shows only `Proxy 0 (0.0%)` / `Of total wire: 36.86%`,
and "Token Usage" reads Before Compression → Proxy Removed → After
Compression with no "Filtered (this session)" row. Nothing below the
removed panel broke.

- **Not tested:** Windows and Linux (macOS only) — `install.ps1` is
verified by brace-balance and inspection, not executed, since no `pwsh`
is available locally. The wrap e2e suite (`e2e/wrap/run.py`) was updated
but not run; it needs the Docker e2e image. `serena project index`
interaction is exercised in the stacked base PR.

## 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
- [x] 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
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

**Stacked on #2676** (`tejas/serena-config-bootstrap`) — please merge
that first; this PR's base should then be retargeted to `main`, or it
will read as containing that fix too.

**Breaking-change migration for users:**
- Drop `--rtk`, `--no-rtk`, `--no-project-rtk`, `--keep-rtk`,
`--context-tool`, `--no-context-tool` from any alias, script or CI job,
and unset `HEADROOM_RTK*` / `HEADROOM_CONTEXT_TOOL`. They now error
rather than being ignored, so the failure is immediate and
self-explaining.
- Previously-installed artifacts are purged automatically on the next
`wrap`/`unwrap`; no manual cleanup needed.
- `headroom perf --json` no longer carries a `cli_filtering` key, and
`/stats` no longer returns a `context_tool` section.

**Docs:** `docs/rtk-architecture.md` deleted; RTK/lean-ctx removed from
`README.md`,
`docs/content/docs/{configuration,opencode,grok-build,docker-install,filesystem-contract}.mdx`,
`docs/observability.md` and the matching `wiki/` pages.
`REALIGNMENT/09-phase-G-rtk-observability.md` is marked SUPERSEDED
rather than deleted, to keep the planning record.

**Follow-ups not in scope:** `_emit_wrap_interrupted` was deleted as
dead code — its only caller was the `except KeyboardInterrupt` guarding
the binary download, so with no download there is nothing slow left to
interrupt.
2026-07-30 22:59:41 -07:00
Rod Boev
2eca5ee114
fix(copilot): normalize subscription API routing (#2441) (#2455)
## Description

PR https://github.com/headroomlabs-ai/headroom/pull/2445 added the
missing OpenCode subscription path, but the shared Copilot subscription
resolver still lets Business and Enterprise payload hosts route through
segmented `*.githubcopilot.com` domains and still drops an explicit
`GITHUB_COPILOT_API_URL` pin on two resolution paths. This follow-up
moves the final hosted-route decision back into the shared resolver,
normalizes `api.business.githubcopilot.com` and
`api.enterprise.githubcopilot.com` to the generic host by default, and
makes the explicit pin win on token exchange, explicit API token, and
Copilot-token candidate resolution. Both `headroom wrap copilot
--subscription` and `headroom wrap opencode --copilot-subscription`
inherit the same fix because they already consume the same
`CopilotSubscriptionTokenResolution.api_url`. Refs #2441.

Attribution:
https://github.com/headroomlabs-ai/headroom/pull/2445#issuecomment-5026212395
reported and narrowed the Business or Enterprise regression, and
https://github.com/headroomlabs-ai/headroom/pull/2445#issuecomment-5026863498
scoped the shared-resolver follow-up that this change implements.

## 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

- Centralize subscription hosted-route selection so explicit
`GITHUB_COPILOT_API_URL` pins win on token exchange, explicit API token,
and Copilot-token candidate resolution.
- Normalize `api.business.githubcopilot.com` and
`api.enterprise.githubcopilot.com` to `https://api.githubcopilot.com` by
default, extending the existing individual-seat normalization.
- Extend focused auth and wrapper tests so both subscription wrappers
prove the corrected shared resolver output and the private-proxy
isolation contract stays intact.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_copilot_auth.py -q`)
- [x] Unit tests pass (`uv run pytest
tests/test_cli/test_wrap_copilot.py -q`)
- [x] Unit tests pass (`uv run pytest
tests/test_cli/test_wrap_opencode.py -q`)
- [x] Unit tests pass (`uv run pytest
tests/test_cli/test_wrap_persistent.py -q`)
- [x] Linting passes (`uv run ruff check .`)
- [x] Formatting check passes (`uv run ruff format . --check`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
uv run pytest tests/test_copilot_auth.py -q -> 84 passed
uv run pytest tests/test_cli/test_wrap_copilot.py -q -> 31 passed
uv run pytest tests/test_cli/test_wrap_opencode.py -q -> 44 passed in 143.46s
uv run pytest tests/test_cli/test_wrap_persistent.py -q -> 31 passed
uv run ruff check . -> All checks passed!
uv run ruff format . --check -> 1331 files already formatted
```

## Real Behavior Proof

- Environment: Windows
- Exact command / steps: Run the focused auth, Copilot wrapper, OpenCode
wrapper, and persistent-proxy pytest files after implementing the shared
resolver change, then ask lucasp1337 to rerun the Business or Enterprise
`--copilot-subscription` scenario from PR
https://github.com/headroomlabs-ai/headroom/pull/2445#issuecomment-5026212395
on a real seat.
- Observed result: Focused auth and wrapper pytest runs passed locally,
including the enterprise-host exchange reproduction row, explicit-pin
precedence on all three producer paths, both subscription wrapper
routes, and the private-proxy isolation regression. Live Business or
Enterprise success stays behind reporter retest.
- Not tested: live Business or Enterprise tenant run

## 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
- [ ] 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
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

No `CHANGELOG.md` edit is needed because Headroom generates release
notes from conventional commits.

Risk for maintainers: PR
https://github.com/headroomlabs-ai/headroom/pull/641 manually validated
a Business seat against the GitHub-returned hosted domain in June on
`gpt-5.4`, so generic-by-default could affect tenants that genuinely
require a dedicated host. This follow-up keeps the documented escape
hatch intact by making `GITHUB_COPILOT_API_URL` win on every path.

Live-seat proof boundary: lucasp1337 offered to retest on a Business or
Enterprise seat in PR
https://github.com/headroomlabs-ai/headroom/pull/2445#issuecomment-5026212395.
Keep any live success claim behind that rerun.
2026-07-20 17:15:57 -07:00
Tejas Chopra
44136ed042
fix(wrap): make RTK opt-in (off by default) across wrap subcommands (#2344)
## Description

RTK CLI-command filtering was set up **by default** across ~16 `wrap`
subcommands (copilot, codex, aider, cursor, cline, continue, goose,
openhands, opencode, grok, omp, openclaude, vibe, …) via `if not
no_rtk:` — so users got rtk hooks / instruction injection without opting
in. `wrap claude` was the lone exception (already gated on
`--context-tool`).

This makes RTK **opt-in (off by default)** everywhere, so Headroom's own
savings are what's measured unless a user explicitly wants rtk.

Closes #

## Type of Change
- [x] Bug fix (behavior change: default flip)

## Changes Made
- **Central gate** `_rtk_opt_in()` — RTK runs only when explicitly
enabled via `--rtk` or `HEADROOM_RTK=1`. Guards the 3 RTK entry points
(`_setup_rtk`, `_ensure_rtk_binary`, `_inject_rtk_instructions`) so they
no-op by default — one small change instead of editing ~30 call sites.
- **`--rtk` opt-in flag** on all 18 tool subcommands via a shared
eager-callback option (`expose_value=False`, sets `HEADROOM_RTK=1`; no
subcommand signature changes).
- `wrap claude`'s legacy `--context-tool` still opts in (mirrored into
the gate).
- **`--no-rtk` kept** as an accepted, now-redundant no-op (back-compat).
- lean-ctx and all non-RTK behavior untouched.

## Testing
```text
pytest tests/test_wrap_rtk_opt_in.py   -> 4 passed
ruff check / format                    -> clean
mypy headroom                          -> Success: no issues found in 504 source files
```
Verified `--rtk` appears in `wrap {claude,codex,copilot} --help`;
`_rtk_opt_in()` is False by default, True with `HEADROOM_RTK=1`; entry
points no-op + write nothing when off.

## Real Behavior Proof
- Env: local `.venv`, click CliRunner.
- Steps: import wrap; assert gate default-off / env-on; assert
`_setup_rtk`/`_ensure_rtk_binary` return None and
`_inject_rtk_instructions` returns False + writes no file when not opted
in; assert `--rtk` in subcommand help.
- Observed: all pass. Not tested: a live end-to-end wrap launch (proxy
spawn).

## Notes
Part 2 of 3 (RTK opt-in). Separate PRs cover the code-graph MCP engine
and the proxy-option cleanup. No `CHANGELOG.md` edit — release-please
generates it from the PR title (per the changelog guard).

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-17 16:47:19 -07:00
Rod Boev
4364eb8dc4
fix(copilot): refresh wrapped subscription tokens (#2156) (#2182)
## Description

`headroom wrap copilot --subscription` currently validates a Copilot
subscription credential once at launch, exchanges it once, and then pins
that short-lived API token into the proxy as an explicit override. When
the token expires, long-lived wrapped sessions start returning
`transient_auth_error` and then a final HTTP 401 until the entire
wrapped session is restarted.

This PR keeps the validated launch token for first-request determinism,
carries reusable OAuth refresh material into the proxy, and refreshes
inside `CopilotTokenProvider` when the seeded token is expired. The
explicit `GITHUB_COPILOT_API_TOKEN` override path stays unchanged when
no reusable OAuth token exists. The configured `GITHUB_COPILOT_API_URL`
also stays pinned across refresh, matching the current wrap contract.

Closes #2156.

## 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

- Extended the Copilot subscription token resolution path to carry
reusable OAuth refresh material and expiry metadata instead of
discarding it after the wrap-time exchange.
- Reworked `CopilotTokenProvider.get_api_token()` so it seeds the
wrapper-validated launch token once for the first request, then
refreshes through the existing exchange path when that token is expired
and reusable OAuth material exists.
- Rejected non-finite seeded expiry values such as `inf`, so malformed
`GITHUB_COPILOT_API_TOKEN_EXPIRES_AT` inputs cannot pin a stale launch
token forever.
- Preserved the explicit `GITHUB_COPILOT_API_TOKEN` override path when
no reusable OAuth token exists, so non-refreshable overrides keep
today's fixed behavior.
- Kept explicitly configured `GITHUB_COPILOT_API_URL` values pinned
across refresh rather than adopting a refreshed payload's host.
- Replaced wrapper-managed seeded `tid_` bearer passthrough with the
refresh-aware provider path, so the wrapped CLI no longer bypasses
expiry refresh just because it keeps sending the launch token back to
the proxy.
- Started a dedicated local proxy instance whenever a
subscription-seeded session targets a shared or persistent proxy port,
so per-session refresh material is not silently dropped on healthy-proxy
reuse or cross-wired between concurrent sessions.
- Scrubbed inherited Copilot refresh-seed environment variables from
both the Copilot child env and the proxy subprocess env before
re-injecting the explicit launch-time values.
- Added focused auth, wrap, proxy-env, and proxy-reuse regression
coverage for expiry refresh, non-finite expiry rejection, session-local
proxy isolation, explicit-override preservation, exchange-flag
independence, configured API URL pinning, and secret handling.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_copilot_auth.py
tests/test_cli/test_wrap_copilot.py tests/test_cli_proxy_env.py
tests/test_cli/test_wrap_persistent.py -q`)
- [x] Linting passes (`uv run ruff check headroom/copilot_auth.py
headroom/cli/wrap.py tests/test_copilot_auth.py
tests/test_cli/test_wrap_copilot.py tests/test_cli_proxy_env.py
tests/test_cli/test_wrap_persistent.py`)
- [x] Formatting passes (`uv run ruff format --check
headroom/copilot_auth.py headroom/cli/wrap.py tests/test_copilot_auth.py
tests/test_cli/test_wrap_copilot.py tests/test_cli_proxy_env.py
tests/test_cli/test_wrap_persistent.py`)
- [x] Type checking passes (`uv run mypy headroom/copilot_auth.py`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
195 passed, 1 skipped in 3.14s
```

```text
All checks passed!
```

```text
6 files already formatted
```

```text
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Python 3.12.13, `uv`, no live Copilot credentials.
- Exact command / steps: run the focused auth, wrap, proxy-env, and
proxy-reuse regression suite after seeding an expired launch token plus
reusable OAuth refresh material, then rerun lint and format checks on
the touched files.
- Observed result: base reproduces the bug because the explicit-token
branch never refreshes, accepts non-finite expiry inputs, and
shared-proxy reuse can keep the wrong per-session refresh seed alive;
head refreshes through the reusable OAuth token, rejects non-finite
seeded expiry, replaces the wrapper-managed seeded bearer instead of
blindly passing it through, preserves the valid-seed fast path and the
fixed override path when no refresh material exists, keeps the
configured API URL pinned across refresh, starts a dedicated local proxy
when the requested port already belongs to a shared or persistent proxy,
and keeps the reusable OAuth token confined to explicit proxy launch env
only. The focused suite passed with `195 passed, 1 skipped in 3.14s`.
- Not tested: live business-subscription session past the provider's
real token-expiry window.

## 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
- [x] 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
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- Scope stays provider-local. The fix remains inside
`headroom/copilot_auth.py` and the Copilot wrap handoff in
`headroom/cli/wrap.py`; it does not add generic 401 retry logic to
provider-neutral proxy layers.
- The line that disables token exchange for the Copilot CLI child env is
unchanged because it never reached the proxy env and was not the root
cause.
- Subscription-seeded sessions now get a dedicated local proxy whenever
the requested port already belongs to a shared or persistent proxy;
existing shared proxies are left alone to avoid disrupting attached
wrappers.
- Live provider confirmation still needs maintainer or reporter
validation because that truth is owned by GitHub's real subscription
APIs, not by local stubs.
- Headroom's release pipeline generates changelog entries from
conventional commits, so `CHANGELOG.md` is intentionally untouched.
2026-07-14 11:52:43 -04:00
Rod Boev
afd9cbdfaf
fix(copilot): normalize subscription routing host (#1836)
## Description

`headroom wrap copilot --subscription` can currently trust the
token-exchange host for individual Copilot seats, which routes newer
responses-API models like `gpt-5.4` to
`api.individual.githubcopilot.com` and reproduces the transient `502`
retry loop from issue #1694. This normalizes that public individual-seat
host back to the generic Copilot API host while preserving dedicated
business or explicitly pinned hosts. Closes #1694.

## 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

- Normalized exchanged Copilot subscription hosts through the existing
public-host classifier instead of trusting the raw token-exchange
payload.
- Added a regression proving `api.individual.githubcopilot.com`
downgrades to `https://api.githubcopilot.com` for subscription routing.
- Added a wrap-level regression proving subscription launches export the
normalized host into the proxy env.
- Preserved business-host and explicit `GITHUB_COPILOT_API_URL` routing
behavior.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_copilot_auth.py -q`)
- [x] Unit tests pass (`uv run pytest
tests/test_cli/test_wrap_copilot.py -q`)
- [x] Linting passes (`uv run ruff check headroom/copilot_auth.py
tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
uv run pytest tests/test_copilot_auth.py -q
57 passed, 1 warning in 0.34s

uv run pytest tests/test_cli/test_wrap_copilot.py -q
31 passed, 1 warning in 0.32s

uv run ruff check headroom/copilot_auth.py tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py
All checks passed!

uv run ruff format --check headroom/copilot_auth.py tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py
3 files already formatted
```

## Real Behavior Proof

- Environment: Windows, Python `uv` environment, mocked Copilot
token-exchange and wrap launch surfaces.
- Exact command / steps: run the focused Copilot auth and wrap
regression tests after teaching subscription token-exchange routing to
normalize the public individual-seat host.
- Observed result: exchanged subscription tokens that advertise
`https://api.individual.githubcopilot.com` now route through
`https://api.githubcopilot.com`, while business-host and explicit-host
pin cases stay unchanged.
- Not tested: a live GitHub Copilot subscription request against the
upstream service.

## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] 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
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

This is intentionally scoped to host selection for exchanged Copilot
subscription tokens. It does not change token discovery, token pinning,
or non-subscription OAuth routing.
2026-07-06 06:23:48 -07:00
Shengbo_Wang
a0cb7982e3
fix(cli): add explicit UTF-8 encoding to file I/O in wrap commands (#1126) (#1164)
## Description

On Windows, `Path.read_text()` and `open()` default to the system locale
encoding (cp1252, GBK, etc.) instead of UTF-8. This causes
`UnicodeDecodeError` when reading or writing instruction files that
contain multi-byte UTF-8 characters such as smart quotes or em dashes.

The RTK instructions block itself contains an em dash (U+2014, `—`), so
`_inject_rtk_instructions` crashes on any non-UTF-8 Windows locale when
writing to `.copilot_instructions`, `.clinerules`, `AGENTS.md`, or
similar hint files.

Closes #1126

## 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 `encoding="utf-8"` to all bare `read_text()`, `write_text()`, and
`open()` calls in `headroom/cli/wrap.py` that handle instruction or
config files (18 call sites)
- Update test assertions in `test_wrap_hintfile_agents.py`,
`test_wrap_copilot.py`, and `test_wrap_bridge.py` to read with
`encoding="utf-8"`
- Add `test_inject_rtk_handles_utf8_content` verifying that existing
hint files with smart quotes and em dashes survive RTK injection without
crashing

## Testing

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

### Test Output

```text
$ python -m pytest tests/test_cli/test_wrap_hintfile_agents.py tests/test_cli/test_wrap_copilot.py tests/test_cli/test_wrap_bridge.py -v
47 passed in 1.28s
```

## Real Behavior Proof

- Environment: Windows 11 China (GBK locale), Python 3.11, headroom main
(f03e77b)
- Exact command / steps: python -m pytest
tests/test_cli/test_wrap_hintfile_agents.py -v (on GBK Windows)
- Observed result: Before fix,
test_prepare_only_injects_rtk_into_hintfile fails with
UnicodeDecodeError 'gbk' at position 283 (the em dash in RTK block).
After fix, all 12 hintfile tests pass including new UTF-8 round-trip
test.
- Not tested: no manual `headroom wrap copilot` run against a real
Copilot installation

## 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
- [ ] 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

This is the same class of bug reported in #733 (GBK config.toml
corruption). This PR fixes the `wrap.py` call sites; other modules
(`learn/analyzer.py`, `install/providers.py`) have the same pattern and
could benefit from the same treatment in a follow-up.

---------

Signed-off-by: Yiming Zeng <yzeng424@gmail.com>
Signed-off-by: RTCartist <wangshengb@buaa.edu.cn>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 12:07:03 -05:00
Terminal Chai
b4fde0c3a4
fix(wrap): add Copilot unwrap command (#1251)
## Description

Adds the missing `headroom unwrap copilot` command so the durable setup
created by `headroom wrap copilot` can be removed without touching
user-authored Copilot instructions.

Closes #1172

## 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 `headroom unwrap copilot` with `--port` and `--no-stop-proxy`
options.
- Remove only Headroom's marker-fenced RTK block from
`.github/copilot-instructions.md`.
- Preserve user-authored content and leave malformed/unmatched markers
unchanged.
- Remove an instruction file that contains only Headroom's generated
block.
- Update the changelog.

## Testing

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

### Test Output

```text
> .\.venv\Scripts\python.exe -X utf8 -m pytest tests/test_cli/test_wrap_copilot.py -q
30 passed in 1.11s

> .\.venv\Scripts\ruff.exe check .
All checks passed!

> uv run --extra dev ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_copilot.py
2 files already formatted

> uv run --extra dev mypy headroom/cli/wrap.py
Success: no issues found in 1 source file
```

The new command test failed before the implementation with:

```text
Error: No such command 'copilot'.
```

## Real Behavior Proof

- Environment: Windows, Python 3.12.12, local editable Headroom
checkout.
- Exact command / steps: created an isolated project containing user
guidance plus a Headroom marker-fenced RTK block, then ran
`.venv\Scripts\headroom.exe unwrap copilot --no-stop-proxy`.
- Observed result: command exited `0`, printed `Removed Headroom rtk
instructions from Copilot.`, and the resulting file contained only `Keep
user guidance.`.
- Not tested: a live Copilot CLI session or terminating a real proxy
process; proxy shutdown delegates to the existing tested unwrap helper
and is covered here with a command-level mock.

## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] 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
- [x] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

Not applicable; this is a CLI-only change.

## Additional Notes

No dependencies were added. The unchecked comment item is not applicable
because the cleanup helper and command are straightforward and
documented with docstrings.

This pull request includes code written with the assistance of AI. The
changes have not yet been reviewed by a human.

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-22 22:55:36 -05:00
Umi_Ma
e67ee2af65
feat: add Copilot BYOK provider wrapper utilities and CLI support (#1041)
## Description

Fix `--model auto` causing `400 The requested model is not supported`
errors when
using Copilot BYOK mode. `auto` is a Copilot-internal virtual routing
token that
external providers (Anthropic, OpenAI) do not recognise as a valid model
name.

In subscription/OAuth mode the wrapper now strips `--model auto` before
launching
Copilot so its own native auto-selection takes effect. In BYOK mode
`auto` is treated
as unconfigured and a clear, actionable error message is shown.

Closes #972

## 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

- `headroom/providers/copilot/wrap.py`: added `is_auto_model()` and
`strip_auto_model_args()` helpers; updated `model_configured()` to treat
`auto` as unconfigured for BYOK
- `headroom/providers/copilot/__init__.py`: exported both new helpers
via `__all__`
- `headroom/cli/wrap.py`: strips `--model auto` in subscription mode
before launch; shows specific actionable error in BYOK mode
- `tests/test_provider_copilot_wrap.py`: 17 new parametrized test cases
for `is_auto_model`, `strip_auto_model_args`, and updated
`model_configured`

## Testing

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

### Test Output

```text
$ uv run pytest tests/test_provider_copilot_wrap.py -v
platform win32 -- Python 3.14.6, pytest-9.0.3, pluggy-1.6.0
collected 34 items
tests/test_provider_copilot_wrap.py::test_is_auto_model[auto-True] PASSED
tests/test_provider_copilot_wrap.py::test_is_auto_model[Auto-True] PASSED
tests/test_provider_copilot_wrap.py::test_is_auto_model[AUTO-True] PASSED
tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args0-expected0] PASSED
tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args1-expected1] PASSED
tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args2-expected2] PASSED
tests/test_provider_copilot_wrap.py::test_model_configured_detects_env_and_cli_variants PASSED
============================= 34 passed in 0.46s ==============================
$ ruff check headroom/providers/copilot/wrap.py headroom/cli/wrap.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.14.6, headroom-ai 0.25.0 editable
install from branch fix-automode-issue
- Exact command / steps: ran uv run pytest
tests/test_provider_copilot_wrap.py -v and ruff check on all four
changed files; reviewed CLI code path for both subscription and BYOK
modes
- Observed result: 34 passed, ruff All checks passed; --model auto is
stripped silently in subscription mode and rejected with a specific
actionable error in BYOK mode
- Not tested: live end-to-end Copilot CLI session, macOS/Linux keychain
auth, Docker/CI token-injection paths

## 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

mypy is not installed in the local venv so type checking was skipped;
the code uses standard type hints and passes ruff checks cleanly.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-16 12:07:10 -07:00
Joel Belanger
0b4a4bd483
fix: support Copilot Business subscription auth (#641)
## Description

Adds a first-party `headroom copilot-auth login` flow for Copilot
subscription
mode and uses the resulting Copilot OAuth token to perform GitHub's
Copilot
token exchange before launching the wrapped Copilot CLI.

This fixes Business/Enterprise Cloud accounts where a generic
GitHub/Copilot
token can read Copilot account metadata but is rejected by the Copilot
token
exchange endpoint. It also avoids treating GitHub.com Enterprise Cloud
account
URLs such as `github.com/enterprises/acme` as API hostnames.

Fixes #635
Related: #488, #610
Builds on #576

## Type of Change

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

## Changes Made

- Adds `headroom copilot-auth login` and `headroom copilot-auth status`.
- Stores a Headroom-specific Copilot OAuth token under Headroom's state
dir.
- Exchanges reusable Copilot OAuth tokens with Copilot Chat-compatible
headers before subscription-mode launch.
- Carries the resolved Copilot API endpoint into `headroom wrap copilot
--subscription`.
- Handles GitHub.com Enterprise Cloud URLs without synthesizing invalid
`api.github.com/enterprises/...` hosts.
- Adds focused unit tests and README guidance for subscription login.

## Testing

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

### Test Output

```console
ruff check headroom/copilot_auth.py headroom/cli/copilot_auth.py headroom/cli/main.py headroom/cli/__init__.py headroom/cli/wrap.py tests/test_copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py tests/test_copilot_subscription_smoke.py
# All checks passed!

ruff format --check headroom/copilot_auth.py headroom/cli/copilot_auth.py headroom/cli/main.py headroom/cli/__init__.py headroom/cli/wrap.py tests/test_copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py tests/test_copilot_subscription_smoke.py
# 9 files already formatted

python -m py_compile headroom/copilot_auth.py headroom/cli/copilot_auth.py headroom/cli/main.py headroom/cli/__init__.py headroom/cli/wrap.py tests/test_copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py tests/test_copilot_subscription_smoke.py

uv run --no-project --with pytest --with pytest-asyncio --with click --with rich --with opentelemetry-api --with pydantic --with tiktoken --with 'litellm==1.82.3' --with fastapi --with uvicorn --with 'httpx[http2]' --with openai --with mcp --with magika --with zstandard --with websockets --with onnxruntime --with transformers --with watchdog --with sqlite-vec pytest tests/test_copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py tests/test_cli_proxy_env.py tests/test_copilot_subscription_smoke.py
# 127 passed
```

Local note: `uv run pytest ...` against the project currently fails
before
running tests because `uv.lock` has an unrelated `gitpython`
wheel/version
mismatch.

## Manual Validation

I tested this with an existing GitHub Copilot Business subscription
associated with a GitHub.com Enterprise Cloud account.

The Enterprise Cloud value I tested was in the form:

```text
github.com/enterprises/<enterprise>
```

The tested flow was:

```text
headroom copilot-auth login
headroom wrap copilot --subscription -- --model gpt-5.4
```

This validated that Headroom does not treat
github.com/enterprises/<enterprise> as a Copilot API hostname. Instead,
token exchange uses GitHub.com and Headroom routes subscription-mode
traffic to the Copilot API endpoint returned by GitHub for the signed-in
account.

I did not test this with GitHub Enterprise Server or a custom enterprise
domain such as ghe.example.com.

No tokens, request IDs, or organization-specific identifiers are
included in this PR.

## Real Behavior Proof

- Environment: macOS Darwin, Python 3.12.7, local checkout on
`codex/copilot-business-auth`.
- Exact command / steps: Ran `headroom copilot-auth login`, then
launched `headroom wrap copilot --subscription -- --model gpt-5.4` with
a GitHub Copilot Business subscription tied to a GitHub.com Enterprise
Cloud account.
- Observed result: Headroom did not treat
`github.com/enterprises/<enterprise>` as a Copilot API hostname; token
exchange used GitHub.com and subscription traffic was routed to the
Copilot API endpoint returned for the signed-in account. The latest
focused Copilot auth/proxy tests pass locally (`127 passed`).
- Not tested: GitHub Enterprise Server or custom enterprise domains such
as `ghe.example.com`; Windows Credential Manager integration still needs
confirmation from someone on Windows.

## 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
- [x] 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 targeted unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A

## Additional Notes

Acknowledgement: the OAuth/token-exchange behavior was informed by
`anomalyco/opencode-copilot-auth` by Aiden Cline.

No tokens are printed by the new login/status commands; only a short
SHA-256
fingerprint is displayed for troubleshooting.

The interactive login is included because the missing piece is not just
an
Enterprise URL or routing hint. For GitHub.com Enterprise Cloud
accounts,
URLs like `github.com/enterprises/acme` identify the enterprise account
but
are not Copilot API hostnames; token exchange still happens through
GitHub.com
and then returns the account-specific Copilot API endpoint. A
command-line
enterprise argument can help for true GitHub Enterprise
Server/custom-domain
deployments, but it cannot produce the Copilot OAuth token class that
the
token-exchange endpoint accepts.

Ideally, Headroom would avoid an extra interactive login and reuse an
existing
GitHub/Copilot CLI session everywhere. In practice, some
reusable-looking
tokens can read Copilot account metadata but are rejected by Copilot
token
exchange, which leaves Business/Enterprise Cloud users with missing
model
catalogs. The explicit login command is the smallest independent way to
obtain
and persist the token needed for that exchange without asking users to
pass a
secret on the command line.

---------

Co-authored-by: jbelanger <your-username@users.noreply.github.com>
2026-06-12 20:46:38 -05:00
Focused Instability
914a60a2b0
feat(proxy): per-project savings breakdown on the dashboard (claude, codex, aider, copilot, cursor) (#803)
## Summary

Adds a **per-project savings breakdown** to the proxy dashboard,
covering **all wrap-supported agents: Claude Code, Codex, aider,
Copilot, and Cursor**. Spec and rationale in #802 (feature-request issue
— happy to adjust scope per maintainer feedback).

How it works — two attribution channels, by client capability:

**Header channel** (clients that can send custom headers):
- `headroom wrap claude` appends `X-Headroom-Project: <basename(cwd)>`
to `ANTHROPIC_CUSTOM_HEADERS` (a user-supplied x-headroom-project header
always wins; other user headers preserved).
- `headroom wrap codex` extends the injected
`[model_providers.headroom]` block with `env_http_headers = {
"X-Headroom-Project" = "HEADROOM_PROJECT" }` and sets `HEADROOM_PROJECT`
per launch — Codex only sends the header when the env var is set, so the
static config stays inert outside wrap.

**Base-URL prefix channel** (clients that cannot send custom headers):
- The proxy accepts `/p/<url-encoded-name>/...`; middleware strips the
prefix before routing (before Starlette caches the URL) and binds the
project. The explicit header wins over the prefix.
- `headroom wrap aider` points `OPENAI_API_BASE` / `ANTHROPIC_BASE_URL`
at the prefixed URL.
- `headroom wrap copilot` points `COPILOT_PROVIDER_BASE_URL` at the
prefixed URL (BYOK anthropic/openai provider types and the
GitHub-subscription path).
- `headroom wrap cursor` prints the prefixed Override Base URL in its
setup instructions, with a note explaining the attribution.

**Shared plumbing:**
- New `headroom/proxy/project_context.py`: header classification, `/p/`
prefix split/strip, URL-prefix builder, and a contextvar bound per
request (HTTP middleware + WS accept for the Codex responses bridge);
the outcome funnel resolves it (explicit `RequestOutcome.project` wins),
stamps `RequestLog.tags["project"]`, and forwards it through
`PrometheusMetrics.record_request` into the `SavingsTracker`.
- `SavingsTracker`: persisted state gains a `projects` map (requests,
tokens saved, savings USD, input tokens/cost, last activity). Schema v2
→ v3 with transparent forward migration (v2 files load cleanly,
`projects` starts empty). Names sanitized (printable-only, 128-char
cap); map capped at 50 projects, evicting the smallest bucket.
- `/stats` exposes `savings.per_project` (and
`projects`/`projects_limit` inside `persistent_savings`);
`/stats-history` exposes `projects`.
- Dashboard gains a "Per-Project Savings" table mirroring the per-model
table (Alpine `x-text` only — names are user-supplied, no HTML
injection).

**Behavior changes:** none for unattributed traffic — no header and no
prefix means no bucket, aggregate totals exactly as before
(regression-tested: legacy `/stats` and `/stats-history` shapes are
pinned by tests).

## Real behavior proof

**Setup tested on:** macOS (Darwin 25.5.0), Python 3.11.9, `pip install
-e ".[dev]"`, proxy on spare ports with isolated
`HEADROOM_SAVINGS_PATH`; real Claude Code CLI (subscription auth) and
real Codex CLI (ChatGPT auth) as clients.

**Header channel — exact steps:**

```
HEADROOM_SAVINGS_PATH=/tmp/headroom-proof-savings.json .venv/bin/python -m headroom.cli proxy --port 9123  # background
cd /tmp/proof-alpha && headroom wrap claude --port 9123 --no-rtk --no-mcp --no-serena -- -p "Reply with exactly: OK"
cd /tmp/proof-beta  && headroom wrap claude --port 9123 --no-rtk --no-mcp --no-serena -- -p "Reply with exactly: OK"
cd /tmp/proof-beta  && headroom wrap codex  --port 9123 --no-rtk --no-mcp --no-serena -- exec --skip-git-repo-check "Reply with exactly: OK"
```

**Observed** (copied live `/stats` output; all runs replied `OK` through
the proxy):

```json
{
 "proof-beta": {
  "requests": 3, "tokens_saved": 140, "compression_savings_usd": 0.0007,
  "total_input_tokens": 38581, "total_input_cost_usd": 0.360433,
  "last_activity_at": "2026-06-10T09:19:17Z", "savings_percent": 0.36
 },
 "proof-alpha": {
  "requests": 1, "tokens_saved": 0, "compression_savings_usd": 0.0,
  "total_input_tokens": 9050, "total_input_cost_usd": 0.32245,
  "last_activity_at": "2026-06-10T09:18:09Z", "savings_percent": 0.0
 }
}
```

`proof-beta` aggregates one Claude Code turn + one Codex `exec` run
(Codex attribution flows through `env_http_headers`).

**Prefix channel — exact steps** (the same mechanism the
aider/copilot/cursor wraps emit, driven by a real client):

```
.venv/bin/python -m headroom.cli proxy --port 9124  # background, isolated savings path
ANTHROPIC_BASE_URL="http://127.0.0.1:9124/p/aider-style-project" claude -p "Reply with exactly: OK"
```

**Observed:**

```json
{
 "aider-style-project": {
  "requests": 1, "tokens_saved": 0, "compression_savings_usd": 0.0,
  "total_input_tokens": 8571, "total_input_cost_usd": 1.018575,
  "last_activity_at": "2026-06-10T10:10:13Z", "savings_percent": 0.0
 }
}
```

`/stats-history` returned `schema_version: 3` with the same `projects`
keys; `GET /dashboard` HTML contains the new "Per-Project Savings"
table; persisted state survived a tracker reload; a hand-written v2
savings file loaded cleanly with an empty `projects` map.

**What I did NOT test live:** the actual aider/Copilot/Cursor binaries
end-to-end (their wraps emit exactly the prefixed URLs exercised above —
unit tests pin the emitted env/URLs); Codex subscription-mode routing
via the built-in `openai` provider (no provider headers there — such
traffic simply stays unattributed); multi-worker uvicorn; Windows.

## Tests

- `tests/test_proxy_project_savings.py` (17 tests): sanitization, header
classification, `/p/` prefix split + URL-builder round-trip, tracker
aggregation/persistence/migration/cardinality-cap/state-sanitization,
funnel→`/stats` end-to-end, middleware binding for header + prefix +
precedence, plus regression tests pinning legacy
`/stats`/`/stats-history` shape and unattributed-traffic totals.
- Wrap/provider tests extended: `test_cli/test_wrap_helpers.py`,
`test_cli/test_wrap_codex.py`, `test_provider_aider.py`,
`test_provider_cursor.py`, `test_provider_copilot_wrap.py` (prefixed
env/URLs, user-override wins, no duplicate header, TOML block contents,
block strip, setup-line note).
- Full `pytest` suite run locally; `ruff check` + `ruff format` clean on
all touched files.
- `CHANGELOG.md` updated.

## Dependencies

None added or bumped.

Closes #802 (pending maintainer 👍 per CONTRIBUTING — raised there first
with the full spec).

---------

Co-authored-by: Ash Rhodes <ashley.rhodes@king.com>
2026-06-10 21:04:45 -05:00
Kumario
84ac332d14
fix(copilot): use responses API for subscription reasoning models (#647)
Fixes #644

## Summary
- default `headroom wrap copilot --subscription` to the responses wire
API when the selected Copilot model is GPT-5/o1/o3-family
- normalize `--subscription` to the OpenAI-compatible provider mode
before validating `--wire-api responses`
- add provider and CLI regressions for model-derived defaults and
explicit `--wire-api responses`

## Tests
- `UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev python -m
pytest tests/test_provider_copilot_wrap.py
tests/test_cli/test_wrap_copilot.py -q`
- `UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev python -m
ruff check headroom/providers/copilot/wrap.py
headroom/providers/copilot/__init__.py headroom/cli/wrap.py
tests/test_provider_copilot_wrap.py tests/test_cli/test_wrap_copilot.py`
- `UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev python -m
compileall -q headroom/providers/copilot/wrap.py
headroom/providers/copilot/__init__.py headroom/cli/wrap.py
tests/test_provider_copilot_wrap.py tests/test_cli/test_wrap_copilot.py`

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:54:39 -05:00
Tejas Chopra
18925b8c6e
fix(copilot): restore generic endpoint for non-subscription OAuth (#610) (#612)
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610)

0.23.0 re-pointed the shared Copilot OAuth branch from the generic
api.githubcopilot.com host to the account-specific endpoints.api host
returned by /copilot_internal/user, and made resolve_copilot_api_url
ignore the GITHUB_COPILOT_API_URL override whenever a token resolved.

That change was meant to add --subscription, but it also altered the
pre-existing non-subscription OAuth flow that worked on 0.22.4. The
account host does not serve newer models (e.g. gpt-5.4) on the responses
API, so wrapped requests began failing with unsupported-model errors
while plain Copilot and 0.22.4 kept working.

Restore 0.22.4 routing for non-subscription OAuth (generic host, still
overridable) and keep account resolution only for --subscription.
resolve_copilot_api_url now honors GITHUB_COPILOT_API_URL first, so the
override escape hatch works for every path. BYOK is unaffected.

Add a regression suite that mocks a successful user-info response, the
real-world path the prior test never exercised (it relied on the network
call failing in CI and falling back to the generic host).

* fix(copilot): route subscription + OAuth through the generic host (#610)

The 0.23.0 endpoint resolution derived the Copilot API host from
/copilot_internal/user (endpoints.api), which returns a segmented host
(e.g. api.individual.githubcopilot.com) that does not serve newer models
on the responses API and is not the host the official Copilot client
routes with (that comes from the token-exchange endpoint). --subscription
used the identical resolution, so it carried the same latent regression
as the non-subscription OAuth path.

Make Copilot host resolution override -> generic for BOTH --subscription
and the implicit OAuth path, and stop using user-info to route. Accounts
that require a dedicated host (enterprise / data residency) pin it via
GITHUB_COPILOT_API_URL. resolve_copilot_api_url no longer makes a network
call; _fetch_copilot_user_info is retained for token validation.

Update the subscription smoke tests that encoded the old account-host
assumption, and add wrap-level + unit coverage that --subscription routes
to the generic host even when user-info advertises an account host, and
that the GITHUB_COPILOT_API_URL override flows through both paths.

* docs(copilot): document generic-host routing + enterprise override (#610)

Spell out the routing contract introduced by the #610 fix so enterprise
users have a supported path. Headroom routes wrapped Copilot hosted
traffic (--subscription and OAuth) to the generic api.githubcopilot.com,
and accounts on a dedicated host (Enterprise Cloud data residency, egress
proxy) pin it via GITHUB_COPILOT_API_URL.

- copilot --help: note the generic host + GITHUB_COPILOT_API_URL override.
- TESTING-copilot-subscription.md: add "API host & Enterprise / data
  residency" section; correct the stale api.*.githubcopilot.com claim; and
  invite enterprise tenants who want token-exchange-based auto-detection to
  open an issue.
- integration-guide.md: short hosted-host + override note in the Copilot
  section.
2026-06-04 16:27:54 -07:00
Tejas Chopra
72da461217 fix(copilot): deterministic subscription token handoff to the proxy
Pass the wrapper-resolved (and, for --subscription, GitHub-validated) Copilot
token to the proxy as an explicit launch argument instead of mutating the
parent process's global os.environ. The proxy pins it as
GITHUB_COPILOT_API_TOKEN, so upstream auth is deterministic rather than the
proxy re-running unvalidated token discovery (which could otherwise inject a
different token and 401). Removes the global-state mutation and the test
isolation it forced.

Add a hermetic cross-platform smoke suite (no Keychain/secret-tool/network)
proving the env-var token path resolves on any OS, each OS secret reader is
inert off-platform, and the proxy injects exactly the validated token.
2026-06-03 23:11:02 -07:00
Tejas Chopra
ff4a0c6bc6 fix(copilot): support subscription auth through Headroom
Route GitHub Copilot CLI subscription traffic through the Headroom
OpenAI-compatible proxy path and resolve the account-specific Copilot API
endpoint before launch.

Add source-aware Copilot token discovery for explicit Copilot env vars,
macOS Keychain, Windows Credential Manager, Linux Secret Service, credential
files, and generic GitHub fallbacks. Validate subscription candidates against
GitHub Copilot user metadata so generic GH_TOKEN/GITHUB_TOKEN values do not
shadow Copilot CLI auth.

Document the subscription command and platform status in README: macOS
Keychain auth reuse has been smoke-tested, while Windows, Linux, Docker, and
CI auth-discovery paths still need real OS validation.

Tests: .venv/bin/python -m pytest tests/test_copilot_auth.py
tests/test_copilot_macos_keychain.py tests/test_copilot_linux_secret.py
tests/test_cli/test_wrap_copilot.py tests/test_cli/test_wrap_persistent.py
tests/test_proxy_copilot_auth_hooks.py
2026-06-02 21:24:47 -07:00
chopratejas
967b0db439 fix: B1 — retire ICM, RollingWindow, scoring, relevance + dependents
Phase B step 1 of the live-zone-only realignment. Removes ~10K LOC of
"drop messages from history" machinery that became unreachable after
PR-A1 made `/v1/messages` a passthrough on the proxy. Live-zone-only
compression (PR-B2..B7) operates on content blocks within messages;
message-list mutation no longer happens in the pipeline.

Python deletes:
- headroom/transforms/intelligent_context.py (1077 LOC)
- headroom/transforms/rolling_window.py (395 LOC)
- headroom/transforms/progressive_summarizer.py (508 LOC)
- headroom/transforms/scoring.py (459 LOC)
- headroom/transforms/tool_crusher.py (338 LOC)
- 5 corresponding tests/test_transforms/* and tests/test_proxy_intelligent_context.py

Rust deletes:
- crates/headroom-core/src/context/* (manager, config, workspace,
  candidate, ccr_drop, strategy/, mod) + safety.rs replaced
- crates/headroom-core/src/scoring/* (mod, score, scorer, traits, weights)
- MessageScorerComparator from crates/headroom-parity (PR #338/#343
  becomes deletable; sunk cost stays sunk)
- 13 message_scorer fixtures + record_message_scorer.py

Rust adds (move + rewrite):
- crates/headroom-core/src/transforms/safety.rs — `tool_pair_indices`
  preserves the OpenAI/Anthropic tool_use ↔ tool_result pairing rule
  the live-zone dispatcher (PR-B2) needs. No IcmConfig dependency.

Surface refactors:
- HeadroomConfig: drop `tool_crusher`, `rolling_window`,
  `intelligent_context` fields; hoist `output_buffer_tokens` to top
  level (used by client.py).
- ProxyConfig: drop `intelligent_context*` fields.
- `headroom wrap` proxy server: retire IntelligentContextManager
  and RollingWindow imports + branch; pipeline is CacheAligner →
  ContentRouter (smart_routing) or CacheAligner → SmartCrusher
  (legacy).
- CLI: drop `--no-intelligent-context`, `--no-intelligent-scoring`,
  `--no-compress-first` flags.
- LangChain memory integration: rename `_apply_rolling_window` →
  `_apply_compression`, drop RollingWindowConfig dep. Threshold is
  now advisory — B6 will rework the contract.
- TransformPipeline.create_pipeline now takes only cache_aligner_config.
- headroom/__init__.py + headroom/transforms/__init__.py: strip
  exports of deleted symbols.

Bug fixes uncovered by full pytest sweep:
- providers/copilot/wrap.py: `environ or os.environ` collapsed
  empty-dict to falsy → callers passing `environ={}` accidentally
  pulled from os.environ. Use `environ if environ is not None else
  os.environ`.

Test correctness fixes:
- _DummyAnthropicHandler._retry_request gains **_kwargs to match
  the real handler signature post-A8.
- test_ws_http_fallback extracts JSON from `content=` (post-A3
  byte-faithful) rather than the obsolete `json=` kwarg.
- test_ccr_response_handler_extra fixture joins SSE events with
  `\n\n` per spec (post-A8 byte-buffer parser requirement).
- test_proxy_responses_phase_preservation: capture via direct
  handler attached to the named logger, so the assertion is
  order-independent (proxy `_setup_file_logging` flips
  `headroom.propagate=False` once any earlier test triggers it).
- conftest.py autouse fixture resets `headroom.propagate=True`
  before each test as a defensive measure for the same pollution.
- test_wrap_copilot_translated_backend_still_requires_byok:
  monkeypatch.delenv every provider key so the BYOK error
  actually fires.
- test_native_installers: skip when system bash < 4.3 (macOS ships 3.2).
- TestGeminiEmbedContent / TestGeminiBatchEmbedContents:
  pytest.mark.skip — proxy currently has no :embedContent route;
  feature gap, not regression.

Acceptance:
- cargo build --workspace + cargo clippy + cargo fmt --check: green.
- cargo test --workspace --exclude headroom-py: 777 passed.
- pytest: 4892 passed, 240 skipped, 0 failed.
- git grep returns only intentional comments referencing the deletion.

Per-PR-B1 plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 12:23:17 -07:00
Garm
efd2ac1ca4 chore: renormalize line endings to LF
`.gitattributes` declares `*.py text eol=lf` and `*.sh text eol=lf`, but
74 files (73 .py, 1 .sh) are stored in the index with CRLF line endings,
violating that contract. Every macOS/Linux clone reports these files as
"modified" on fresh checkout because git's diff engine sees the stored
bytes don't match the attribute contract, even though the working tree
and index match byte-for-byte.

Running `git add --renormalize .` rewrites each affected blob so the
stored form matches the attribute declaration. No semantic changes —
every affected file's diff is "N insertions, N deletions" with inserts
and deletes being the same lines modulo line endings.

Follow-up commit adds `.git-blame-ignore-revs` so `git blame` / GitHub
blame skip this mechanical commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:33:30 +02:00
JerrettDavis
64fe9763f5 test: skip rtk in BYOK copilot assertion
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:53:30 -05: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
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
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
chopratejas
89061fd430 Fix CI lint errors and test failures
- test_memory_sync.py: remove unused imports (asyncio, MagicMock,
  AgentMemory, AgentMemoryAdapter, SyncResult), fix import sorting
- test_ws_memory_relay.py: remove unused pytest import and unused
  output_index variable, fix import sorting
- test_wrap_copilot.py: provide dummy API keys in test env — the
  BYOK validation added in 7a7b8b6 requires ANTHROPIC_API_KEY or
  OPENAI_API_KEY to be set
- test_package_init_lazy.py: stop hardcoding version string that
  breaks on every bump; assert it's a non-empty string instead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:23:06 -07:00
JerrettDavis
9fa1763087 feat: add copilot CLI wrap support
Add headroom wrap copilot with backend-aware provider routing, health metadata for running proxy detection, focused Copilot tests, and docs updates across the main integration surfaces.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 01:20:17 -05:00