mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
18 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
007446c73a
|
feat(copilot): proxy VS Code models transparently (#2687)
## Description Make Headroom a transparent proxy for VS Code GitHub Copilot. Users keep using Copilot's normal model picker—GPT-4.1, Claude Sonnet, Claude Opus, and other models in their entitlement—while Headroom silently forwards the selected model instead of registering or requiring a separate "Headroom" model. This also fixes GitHub's device OAuth exchange by sending form-encoded request bodies, matching the endpoint contract. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Add `headroom wrap vscode` to start a Copilot-seeded subscription proxy and safely configure VS Code's shipped Copilot proxy override. - Add `headroom unwrap vscode` for reversible cleanup. - Preserve VS Code's selected model by changing only the proxy URL/auth override; no custom model is registered and no model preference is written. - Support stable VS Code settings locations on macOS, Windows, and Linux, plus `--settings-file` for Insiders, portable, and other installations. - Edit JSONC settings with a marker-owned block while preserving unrelated bytes, comments, ordering, and trailing commas. - Refuse malformed markers, invalid JSONC, or unmanaged existing Copilot overrides instead of overwriting user configuration. - Fix SIGINT cleanup so the managed settings block is removed and normal shutdown exits successfully. - Fix Copilot device OAuth start/poll requests to use `application/x-www-form-urlencoded`. - Add a compatibility matrix, setup/removal flow, credential behavior, remote-development guidance, enterprise notes, troubleshooting, and verification documentation. ## Testing - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/pytest -q tests/test_provider_copilot_vscode_config.py tests/test_cli/test_wrap_vscode.py tests/test_cli/test_wrap_helpers.py tests/test_copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_provider_copilot_wrap.py tests/test_cli/test_wrap_copilot.py tests/test_copilot_provider_label.py tests/test_copilot_subscription_smoke.py 244 passed in 0.59s $ .venv/bin/ruff check <changed Python files and tests> All checks passed! $ .venv/bin/mypy headroom/providers/copilot/vscode.py Success: no issues found in 1 source file $ cd docs && npm run types:check fumadocs-mdx && next typegen && tsc --noEmit # exited 0 $ git diff --check # exited 0 ``` The full 10,179-test suite was also sampled through approximately 83%, but was stopped because of its runtime. It exposed existing failures in `test_recover_codex.py`, `test_wrap_stale_marker.py`, and `test_proxy_health.py`; therefore the broad `pytest`, repository-wide Ruff, and repository-wide mypy boxes are intentionally not checked. ## Real Behavior Proof - Environment: macOS arm64, VS Code 1.131.0, built-in GitHub Copilot 0.59.0, Headroom 0.33.1-dev. - Exact command / steps: 1. Completed `headroom copilot login` with GitHub's device flow. 2. Ran `.venv/bin/headroom wrap vscode --port 8788`. 3. Confirmed VS Code retained its ordinary Copilot model catalog and made `GET /models` through Headroom with `GitHubCopilotChat/0.59.0` and `editor-version: vscode/1.131.0`. 4. Sent native Copilot `/p/headroom/chat/completions` requests through the same endpoint using `gpt-4.1`, `claude-sonnet-4.6`, and `claude-opus-4.7`. - Observed result: - All three completion requests returned HTTP 200. - GPT-4.1 resolved upstream to `gpt-4.1-2025-04-14`; Sonnet and Opus retained their exact selected IDs. - All returned the requested exact marker content. - VS Code's settings contained only the Headroom proxy URL and token auth override—no Headroom model or model-selection setting. - The proxy health endpoint remained ready with `openai_api_url` set to `https://api.githubcopilot.com`. - Not tested: - Physical Windows or Linux hosts (their path/config behavior is covered by unit tests). - WSL, dev containers, SSH remotes, VS Code Insiders, or enterprise Copilot deployments end-to-end. - Every model in the live Copilot catalog. - A fully submitted chat from VS Code's UI automation; the real extension's catalog request and native completion paths were verified separately. ## 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 - [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; this integration intentionally has no separate UI or model entry. ## Additional Notes The integration uses VS Code Copilot's shipped advanced/debug proxy endpoint seam. The managed settings block is deliberately narrow and reversible. Remote extension hosts may need their own reachable proxy/configuration as documented. --------- Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com> |
||
|
|
4a8157fa0a
|
fix(copilot): derive GHE credential host from API URL (#800) (#2511)
## Description GHE Copilot credential discovery falls back straight to `github.com` when `GITHUB_COPILOT_HOST` is unset, even if the documented `GITHUB_COPILOT_API_URL` points at an enterprise host. This change keeps explicit-host precedence, then reuses the configured enterprise domain or a normalized custom API URL hostname for credential lookup, so Windows, macOS, Linux, GH CLI, and credential-file discovery search the same custom host instead of the public default. Closes #800. Attribution: https://github.com/headroomlabs-ai/headroom/issues/800#issuecomment-5044382263 narrowed the shared credential-host mismatch. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds new functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring ## Changes Made - Preserve explicit-host precedence, then fall back to the configured enterprise domain or a normalized custom API URL hostname only when the configured value is usable. - Normalize `api.` and `copilot-api.` prefixes before routing credential lookup, while keeping exact and segmented GitHub-hosted public domains plus public enterprise or malformed enterprise or API configuration fallback on `github.com`. - Add focused coverage for the base/head reproduction, explicit-host precedence, configured-enterprise precedence, public-enterprise, malformed-enterprise, and invalid-port fallback, prefixed-host normalization, adjacent-host exclusion, and GH CLI plus keychain forwarding. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_copilot_auth.py -q`) - [x] Linting passes - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_copilot_auth.py -q 105 passed in 0.58s uvx --from ruff==0.15.17 ruff check headroom/copilot_auth.py tests/test_copilot_auth.py All checks passed! uvx --from ruff==0.15.17 ruff format --check headroom/copilot_auth.py tests/test_copilot_auth.py 2 files already formatted git diff --check clean ``` ## Real Behavior Proof - Environment: Windows, isolated temporary credential file, local `origin/main` checkout plus this branch - Exact command / steps: With only `GITHUB_COPILOT_API_URL=https://api.ghe.example.com:8443/copilot` set and all other token sources disabled, run the same credential-file discovery reproduction against `origin/main` and this branch. - Observed result: `origin/main` selected `github.com` and resolved no token; the review branch selected `ghe.example.com` and resolved `gho-ghe`. - Not tested: live GitHub Enterprise Copilot tenant ## 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 own 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`; Headroom generates release notes from the PR title ## Additional Notes The change does not alter API routing, token exchange, discovery order, or credential matching breadth, and it keeps the live tenant claim out of the PR body until an enterprise user reruns it. |
||
|
|
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. |
||
|
|
c400f90810
|
fix(copilot): preserve /v1 for the Anthropic /v1/messages endpoint (#2409) (#2414)
## Description Fixes #2409. GitHub Copilot Claude requests routed through Headroom return `404 page not found`. Copilot serves Claude models at `/v1/messages`, but Headroom forwards them to `/messages`, so the upstream 404s (observed on OpenCode's GitHub Copilot provider for `claude-haiku-4.5` / `claude-sonnet-4.6`, Headroom 0.32.0). ## Root cause `build_copilot_upstream_url` strips the `/v1` prefix from every Copilot path: ```python if normalized_path.startswith("/v1/"): normalized_path = normalized_path[3:] ``` That is correct for Copilot's **OpenAI-compatible** surface, which has no `/v1` (`/chat/completions`, `/responses`, `/embeddings`). But Copilot's **Anthropic** surface for Claude models is `/v1/messages` — with the `/v1`. Stripping it produces `https://api.githubcopilot.com/messages`, which 404s. Confirmed against the current code: ```text build_copilot_upstream_url("https://api.githubcopilot.com", "/v1/messages") -> "https://api.githubcopilot.com/messages" # 404 ``` ## Fix Keep `/v1` for the messages endpoint; still strip it for the OpenAI paths: ```python if normalized_path.startswith("/v1/") and not normalized_path.startswith("/v1/messages"): normalized_path = normalized_path[3:] ``` Now `/v1/messages` (and `/v1/messages/batches`) route to `.../v1/messages`, while `/v1/chat/completions` -> `/chat/completions` and `/v1/responses` -> `/responses` are unchanged, on both the public and GHE Copilot hosts. Non-Copilot upstreams are untouched. ## 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/copilot_auth.py`: exclude `/v1/messages` from the `/v1`-strip in `build_copilot_upstream_url`. - `tests/test_copilot_auth.py`: assert `/v1/messages` (+ batches, + GHE host) keep `/v1` while the OpenAI paths still strip it. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uvx ruff@0.15.17 check headroom/copilot_auth.py tests/test_copilot_auth.py All checks passed! $ uvx mypy@1.20.2 --ignore-missing-imports headroom/copilot_auth.py Success: no issues found in 1 source file # copilot_auth is import-light, so I ran the real function in the project venv # (uv sync): /v1/messages -> .../v1/messages, /v1/chat/completions -> /chat/completions. ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12, project venv (`uv sync --extra proxy`), `uvx ruff@0.15.17` / `uvx mypy@1.20.2`. - Exact command / steps: called the real `build_copilot_upstream_url` before and after the change for `/v1/messages`, `/v1/messages/batches`, `/v1/chat/completions`, `/v1/responses`, `/v1/embeddings`, and a non-Copilot host. - Observed result: before, `/v1/messages` -> `.../messages` (the 404); after, `.../v1/messages`. Batches keep `/v1` too; the OpenAI paths still strip `/v1` (`/chat/completions`, `/responses`, `/embeddings`); `https://api.anthropic.com/v1/messages` is unchanged. Ran against the actual module. - Not tested: a live OpenCode -> Copilot Claude round trip; the added unit tests assert the URL construction directly. ## 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 `copilot_auth` is a light module, so I verified the fix against the real function in the venv (output above) in addition to the unit tests. Scope is limited to the messages endpoint (the reported 404); every other Copilot path is byte-identical to before. |
||
|
|
e5b3a634df
|
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189)
## Description Running Claude Code (Anthropic) and Codex (OpenAI) against the **same** Headroom proxy instance on one port produced incorrect, unstable dashboard data. The proxy core is provider-isolated and multi-provider-safe by design; the defect was in the observability layer. The Codex `/v1/responses` **WebSocket** handler was the only path in the proxy that wrote to the request logger by hand instead of through the unified `emit_request_outcome` funnel, and it did so twice per session close: the per-turn funnel record plus an unconditional cumulative session-summary `RequestLog`. This PR removes the duplicate summary log so Codex WS emits exactly one request log per turn, matching the HTTP provider paths. ## 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 - Dropped the duplicate cumulative session-summary `RequestLog` in the Codex WS handler while preserving the per-turn `emit_request_outcome` path. - Preserved gated `request_messages` and `turn_id` on residual outcomes so dashboard telemetry keeps the useful attribution without double-counting tokens. - Ensured explicit `--anyllm-provider` wins over a leaked `HEADROOM_ANYLLM_PROVIDER` environment variable. - Registered retry delay settings that had drifted out of the settings registry. - Hardened tests against developer-shell `HEADROOM_*` / `ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused proxy/wrap test fixtures. ## 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/bin/pytest tests/ -q -p no:cacheprovider 8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36) $ .venv/bin/ruff check <touched files> All checks passed! ``` ## Real Behavior Proof - Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff via project venv, branch `fix/multi-provider-runtime`. - Exact command / steps: Ran the full test suite without pytest cache provider and Ruff on all touched files; used `git stash` to confirm the stale fake-config failures pre-existed this change. - Observed result: Full suite passed with no failures; Ruff passed; Codex WS now routes end-of-session logging through `emit_request_outcome`, emitting one request log per turn with the same accounting model as Anthropic HTTP turns. - Not tested: Live simultaneous Claude + Codex dashboard run. `mypy headroom` was not run to completion; a scoped run reported one pre-existing `settings_store.py:470` coercion error outside this diff. ## 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 ## Screenshots (if applicable) N/A - server-side observability fix; no UI markup changed. ## Additional Notes - The proxy's multi-provider routing, header/auth isolation, and per-model cache keying are already correct and unchanged here; only the WS observability write path was double-counting. - Architectural assessment: `plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`; root-cause + resolution trail: `plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`. - No live simultaneous Claude + Codex dashboard run was performed; validation is from test coverage and code review of the WS logging path. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
5dbe3314a1
|
fix(auth): support GitHub Enterprise Copilot OAuth domain (#2192)
## Description Adds GitHub Enterprise OAuth domain support for Copilot auth. When `GITHUB_COPILOT_ENTERPRISE_URL` is set, the default OAuth domain resolves to that enterprise host; explicit `--domain` values still take precedence. Closes #1152 ## 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/copilot_auth.py`: derive the default OAuth domain from `GITHUB_COPILOT_ENTERPRISE_URL` when present, falling back to `github.com` for unset or blank values. - `headroom/cli/copilot_auth.py`: keep explicit CLI domain overrides authoritative even when the enterprise env var is set. - `README.md`: document the enterprise OAuth environment setting and precedence. - Added regression tests for enterprise URL handling, blank/unset fallback, and explicit CLI override precedence. ## 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 uv run pytest tests/test_copilot_auth.py tests/test_cli/test_copilot_auth.py -q 69 passed in 1.05s uvx ruff@0.15.17 check headroom/cli/copilot_auth.py headroom/copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_copilot_auth.py All checks passed! uvx ruff@0.15.17 format --check headroom/cli/copilot_auth.py headroom/copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_copilot_auth.py 4 files already formatted ``` ## Real Behavior Proof - Environment: Windows 11 development checkout, Python 3.13.3, with focused Copilot auth tests using monkeypatched enterprise env vars. - Exact command / steps: ran the Copilot auth unit/CLI tests plus ruff check and format-check against the touched auth files and tests. - Observed result: `GITHUB_COPILOT_ENTERPRISE_URL=https://ghe.example.com` resolves `default_oauth_domain()` to `ghe.example.com`; unset/blank enterprise env vars fall back to `github.com`; and `headroom copilot-auth login --domain github.com` still honors the explicit override when enterprise env vars are set. - Not tested: live OAuth against a real GitHub Enterprise Server instance. ## 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 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) N/A - CLI/auth behavior and README update. ## Additional Notes `GITHUB_COPILOT_ENTERPRISE_URL` takes precedence over `GITHUB_COPILOT_ENTERPRISE_DOMAIN`; explicit `--domain` remains authoritative for the login command. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
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. |
||
|
|
f52ca19db1
|
fix(copilot-auth): stop discarding the caller's valid Copilot auth token (#1879)
## Description `apply_copilot_api_auth()` treats any incoming `gho_`/`ghs_`/`ghp_`/`github_pat_`-prefixed GitHub OAuth bearer token as "not a suitable Copilot API token" (`_is_copilot_api_token()`) and silently replaces it with a token independently fetched/exchanged by Headroom itself, even when the caller already sent its own valid, correctly-entitled Copilot credential. This closes headroomlabs-ai/headroom#1813, which reports the exact same defect from OpenCode's native GitHub Copilot integration: Headroom replaces its native `gho_` bearer token, changing the effective client/integrator lane Copilot's backend sees and breaking model discovery/inference parity with native (non-proxied) behavior. I hit the same root cause independently, through a different trigger path, while building a new dedicated GitHub Copilot CLI redirect integration (so Copilot CLI traffic gets the same full Headroom pipeline treatment Claude Code already gets): a live Copilot CLI session's own `gho_`-prefixed token worked end-to-end for model `claude-sonnet-5` when sent directly to Copilot's real API, but the exact same request got `400 model_not_supported` once routed through Headroom, because Headroom silently swapped in a different, independently-exchanged token with different (lesser) model entitlements. Closes #1813 ## 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 `_is_forwardable_copilot_bearer_token()` in `headroom/copilot_auth.py`: a new, narrowly-scoped helper used only by `apply_copilot_api_auth()` (the chat-completion/inference auth path) that accepts both short-lived Copilot API tokens (`tid_`) AND GitHub OAuth tokens (`gho_`/`ghs_`/`ghp_`/`github_pat_`) as forwardable, since live evidence (this PR + #1813) shows both are valid, correctly-entitled Copilot bearer credentials when supplied directly by a Copilot-aware client. - **Deliberately did NOT touch `_is_copilot_api_token()`** — that helper is used by a separate, unrelated code path (`resolve_subscription_bearer_token_details()`, for Copilot subscription/user-info API resolution) with different endpoint requirements. Broadening it in place would have changed subscription-resolution behavior as an unintended side effect; the new helper keeps that concern isolated. - `apply_copilot_api_auth()` now calls `_is_forwardable_copilot_bearer_token()` instead of `_is_copilot_api_token()` when deciding whether to forward the caller's existing bearer token unchanged. Non-Copilot-shaped or blank/whitespace-only tokens still fall through to Headroom's own fetch/exchange, unchanged. - Fixed a companion `/v1/responses` WebSocket ordering bug in `headroom/proxy/handlers/openai.py` (filed as headroomlabs-ai/headroom#1880): the `OPENAI_API_KEY` fallback used a stale `_lower_headers` snapshot computed before `apply_copilot_api_auth()` ran, and ran *before* it. Once the upstream is known, Copilot auth is now resolved first, and the fallback checks live header state instead — this matters more now that a correctly-forwarded Copilot bearer token must not be shadowed by an unrelated OpenAI-key fallback injected earlier in the flow. ## 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 $ pytest tests/test_copilot_auth.py tests/test_proxy_copilot_auth_hooks.py tests/test_provider_copilot_wrap.py -k "not falls_back_to_gh_cli" -q ============================= test session starts ============================== platform darwin -- Python 3.12.12, pytest-9.0.3, pluggy-1.6.0 collected 95 items / 1 deselected / 94 selected tests/test_copilot_auth.py ............................................. [ 47%] ............ [ 60%] tests/test_proxy_copilot_auth_hooks.py ... [ 63%] tests/test_provider_copilot_wrap.py .................................. [100%] ======================= 94 passed, 1 deselected in 0.52s ======================= # The 1 deselected test (test_read_cached_oauth_token_falls_back_to_gh_cli) is a # pre-existing, environment-dependent failure unrelated to this change: it leaks a # real gho_ token from this machine's own authenticated `gh` CLI keychain instead of # using the mocked one. Confirmed identical on main before this change. $ pytest tests/test_openai_codex_ws_lifecycle.py tests/test_openai_codex_routing.py tests/test_proxy_openai_responses_integration.py tests/test_codex_ws_compression_scheduler.py tests/test_openai_codex_ws_timings.py -q ================== 53 passed, 15 skipped, 1 warning in 16.19s ================== # (15 skips are pre-existing, environment-gated, unrelated to this change) $ ruff check headroom/copilot_auth.py headroom/proxy/handlers/openai.py tests/test_copilot_auth.py All checks passed! ``` ## Real Behavior Proof - Environment: real GitHub Copilot CLI session (`gho_`-prefixed OAuth token), routed through a dedicated Headroom redirect-proxy integration (new, currently opt-in/staging-only work, not part of this PR) that forwards Copilot CLI's `/v1/messages` and `/chat/completions` traffic through Headroom. - Exact command / steps: live Copilot CLI request for model `claude-sonnet-5`, sent once directly to `api.business.githubcopilot.com` and once routed through Headroom with this fix applied. - Observed result: both paths return `200 OK` with identical token/cost usage. Before this fix, the proxied path returned `400 model_not_supported` for the exact same request, because Headroom substituted a different, less-entitled token for the client's own valid `gho_` token. - Not tested: the companion WS-ordering fix (`/v1/responses` fallback ordering) was validated via the existing WS lifecycle/routing/timing unit test suites (see Test Output) rather than a live WebSocket session — I don't have a live client that exercises that exact `OPENAI_API_KEY`-fallback-into-Copilot-upstream path end-to-end. ## 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 ## Screenshots (if applicable) N/A ## Additional Notes This PR was rebased onto current `main` after it turned out `_is_copilot_api_token()` had already been introduced upstream (#557, merged before this PR was opened) with the opposite assumption — that `gho_`/`ghs_`/`ghp_`/`github_pat_` tokens always need exchanging and must not be forwarded. That assumption is exactly what #1813 reports as broken. Rather than reverting that classification wholesale (my original approach in this branch), this version keeps it intact for its original caller and adds a second, narrowly-scoped helper for the inference/chat-completion auth path specifically — see "Changes Made" above for the reasoning. mypy is left unchecked above only because I didn't have it configured/runnable in my local environment for this pass — happy to run it if a maintainer flags a concern, or CI will catch it. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
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. |
||
|
|
16c638bc21
|
fix: recover persistent proxy feature checks and reject non-Copilot exchange URL (#1465)
## Description This PR fixes two related reliability issues in Copilot wrap/subscription flows: 1. Recovered persistent proxy instances could be reused too early, before validating requested feature-sensitive config (especially `openai_api_url`), which could lead to wrong upstream routing. 2. Subscription token-exchange payloads could provide a non-Copilot API URL; this is now rejected and we safely fall back to user-info/default Copilot endpoint resolution. Related: #488 ## 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 - Updated persistent proxy recover path to: - continue into feature checks when feature-sensitive options are requested - restart persistent deployment when config is missing/mismatched after recovery - keep historical fast return for plain recover-only calls - Hardened subscription exchange URL resolution: - accept exchange `api_url` only when it is a Copilot host - log warning and fall back when non-Copilot host is provided - Added regression tests for: - recovered persistent proxy feature mismatch and config-unavailable restart behavior - non-Copilot exchange host rejection with/without user-info fallback ## 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 $ python -m pytest -q tests/test_copilot_auth.py tests/test_cli/test_wrap_persistent.py ============================= test session starts ============================= platform win32 -- Python 3.12.8, pytest-9.1.1, pluggy-1.6.0 rootdir: C:\Users\ralf.escher\Documents\headroom collected 82 items tests\test_copilot_auth.py ............................................. [ 54%] ........... [ 68%] tests\test_cli\test_wrap_persistent.py .......................... [100%] ============================= 82 passed in 1.60s ============================== ``` ## Real Behavior Proof - Environment: - Windows - Python 3.12.8 - Local Headroom branch with this patch - Copilot subscription route through local proxy - Exact command / steps: 1. Start local proxy and run Copilot wrap in subscription mode. 2. Execute chat-completions requests through proxy. 3. Inspect runtime proxy logs for outbound target and inbound status. 4. Run focused regression tests: - `python -m pytest -q tests/test_copilot_auth.py tests/test_cli/test_wrap_persistent.py` - Observed result: - Outbound requests routed to Copilot business host: - `path=https://api.business.githubcopilot.com/chat/completions` - Successful proxy responses observed: - `path=/v1/chat/completions status=200` - Model activity logged during successful requests: - `PERF model=gpt-4.1 ...` - Regression tests pass (`82 passed`), covering both fixes. - Not tested: - Full repository test suite - Full lint/typecheck across entire project - Non-Windows runtime verification in this 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 - [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 - This PR intentionally excludes incidental local edits to `.github/copilot-instructions.md`. - Scope is limited to this bug fix and regression coverage; linked as related work to #488. |
||
|
|
d633e8172c
|
fix(windows): pin UTF-8 encoding on text-mode subprocess calls (#1311)
Fixes #1310. ## Description On Windows, `headroom` startup crashes a subprocess reader thread: ``` UnicodeDecodeError: 'charmap' codec can't decode byte 0x8d in position 7894: character maps to <undefined> ... subprocess.py _readerthread -> buffer.append(fh.read()) ... encodings/cp1252.py ``` Text-mode `subprocess` calls omit `encoding=`, so Python decodes child output with the locale codec (**cp1252** on Windows). Children that emit UTF-8 ??? `cbm index_repository` (indexing sources with chars like `???`/`???`), `claude mcp get/add`, the memory-sync process ??? produce bytes invalid in cp1252 and kill the reader thread. Linux/macOS default to UTF-8, so it's invisible there. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Add `encoding="utf-8", errors="replace"` to every text-mode (`text=True` / `universal_newlines=True`) subprocess call in the `headroom/` package (~50 call sites; several already had it). - `errors="replace"` (not `ignore`) so corrupt bytes surface as `???` rather than vanishing from parsed output. - Add `tests/test_cli/test_subprocess_utf8_encoding.py`: an AST guard asserting every text-mode subprocess call pins `encoding=`. The runtime crash can't reproduce on UTF-8 CI, so the invariant is enforced at the source level instead. ## Testing - [x] Unit tests pass (`pytest`) - New guard test passes (validates 51 call sites). - `tests/test_install`, `tests/test_cli/test_mcp.py`, `tests/test_mcp_registry` pass. (`test_runtime_start_lock_blocks_another_process` fails on this Windows box, but it fails identically on unmodified `main` ??? a pre-existing `msvcrt` lock flake, unrelated.) ### Test Output ```text > python -m pytest tests/test_cli/test_subprocess_utf8_encoding.py -q 1 passed in 0.12s > python -m pytest tests/test_install/ tests/test_cli/test_mcp.py tests/test_mcp_registry/ -q 133 passed, 2 skipped in 15.34s ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13.13. - Exact command / steps: Started `headroom` without `PYTHONUTF8=1` on a repo with UTF-8 chars in indexable files. Observed the `UnicodeDecodeError` crash. Applied the fix (pinning `encoding="utf-8"` on all text-mode subprocess calls). Re-ran. No crash. The AST guard enforces the invariant on CI (which runs UTF-8 locales and cannot reproduce the cp1252 crash natively). - Observed result: Subprocess reader threads no longer crash on UTF-8 output under cp1252 locale. - Not tested: All third-party tools that `headroom` shells out to; each was given `errors="replace"` as a safety net. ## Workaround for affected users (before fix is deployed) `PYTHONUTF8=1` (PowerShell: `$env:PYTHONUTF8=1; headroom ...`). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
6b0c09ffd5
|
fix: harden Copilot API auth token handling (#557)
## Description Improve Copilot API authentication behavior by correctly handling incoming bearer tokens and ensuring required Copilot headers are present. ## 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 token classification logic to distinguish short-lived Copilot API tokens (`tid_`) from GitHub OAuth tokens. - Updated auth flow to pass through valid existing Copilot API bearer tokens and replace unsuitable bearer tokens. - Added default `Copilot-Integration-Id` and `editor-version` headers when missing. - Improved Windows credential lookup to consider both GitHub CLI (`gh:`) and Copilot CLI credential target prefixes. - Added regression tests for pass-through, replacement, header injection, and token prefix classification. ## Testing Describe the tests you ran to verify your changes: - [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 pytest -q tests/test_copilot_auth.py 29 passed in 0.40s ``` ## 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 ## Screenshots (if applicable) N/A ## Additional Notes This change is intentionally scoped to auth behavior and tests in: - `headroom/copilot_auth.py` - `tests/test_copilot_auth.py` --------- Co-authored-by: Abhinav Kaurav <abhinav.kaurav@e2open.com> |
||
|
|
6ed43027b7 |
style(copilot): ruff-format test_copilot_auth.py
Parenthesize a multi-line conditional lambda so 'ruff format --check .' passes (the original commit added it unformatted). |
||
|
|
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 |
||
|
|
dcb3e8bdcb |
test: cover copilot auth branches
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
d60cf7914c |
fix: support live copilot oauth runtime
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
7989581350 |
fix: support copilot oauth sessions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |