mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Repo hygiene for a public OSS project: removes committed `node_modules`, stray/internal/draft markdown, and commercial-surface references — keeping every real doc (the published docs site, the wiki guides, and all component READMEs) intact. Every file was content-audited before removal, and load-bearing files were verified against the code/CI and kept. Net: **1,695 files changed, +23 / −266,409** (the deletions are dominated by a committed `node_modules` tree). Closes # (no tracking issue) ## Type of Change - [ ] 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 - [x] Code refactoring (no functional changes) ## Changes Made **Removed (verified to have no code/CI dependencies):** - `examples/vercel-ai-sdk-pr/` — 1,649 committed `node_modules` files (zero example source); `node_modules/` added to `.gitignore`. - `docs/spec/` (23 draft "Living Specification" files — orphaned, `1.0.0-draft`, drifted from the code), `docs/superpowers/` (2 agent plans), `docs/proposals/` (2 internal/commercial memos). - 6 orphan `docs/*.md` (auth-modes, bedrock, claude-code-vertex-headroom, cortex-code, output-token-reduction-guide, rtk-loop-weighting). - `PR.md` (committed PR draft), `ENTERPRISE.md`, `.github/FUNDING.yml`. **Content scrubs:** - Removed unreleased "Headroom Cloud" / `api.headroom.ai` / `hr_` references from `configuration.mdx`, `wiki/configuration.md`, `wiki/typescript-sdk.md`, `sdk/typescript/README.md` (reworded to neutral, accurate phrasing). - Dropped a stale "awaiting maintainer before merge" line from `plugins/headroom-oauth2/SPEC.md`; tidied `.gitignore` comments (kept the protective `headroom-managed/` ignore rule). - Fixed the now-dangling links into removed files (README nav/`output-token-reduction` link, `scripts/README`, `wiki/vertex`). **Explicitly KEPT (load-bearing — would orphan in-code citations if removed):** - `.changelog.md` — consumed by `.github/workflows/release.yml` (read as the release-notes file). - `REALIGNMENT/`, `docs/observability.md`, `docs/rtk-architecture.md`, `wiki/plans/`, `TESTING-copilot-subscription.md` — referenced by the Rust core / Python / tests as design docs. ## Testing - [ ] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [ ] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text # Docs/markdown + .gitignore only — no Python/Rust source changed, so the # behavioral test suite is unaffected. Verified the cleanup did not orphan # references or break the published docs site: $ git ls-files 'docs/content/docs/*.mdx' | wc -l # published site intact 42 $ # meta.json nav unchanged; no published page removed. $ grep -rnI "Headroom Cloud|api.headroom.ai|'hr_" $(git ls-files '*.md' '*.mdx') >>> none $ # dangling refs to removed files (excl pre-existing P0/P2 spec stubs that $ # never existed in git): none remaining. ``` ## Real Behavior Proof - Environment: macOS, local git clone of the repo (markdown/.gitignore changes only — no runtime). - Exact command / steps: 4 read-only content-audit agents classified every `.md`/`.mdx` file; each removal candidate was cross-checked against the codebase (`grep` for citations in `.rs`/`.py`/tests, workflows, and configs); only files with no dependents were removed; the tree was re-grepped after removal to confirm no new dangling references; verified the published docs site page count (`git ls-files 'docs/content/docs/*.mdx' | wc -l` = 42, unchanged). - Observed result: the 42-page published docs site and all wiki guides are untouched; no source or workflow references a removed file; `.changelog.md` (consumed by release.yml) and the code-cited design docs were detected as dependencies and kept; the committed `node_modules` tree is removed and `node_modules/` is gitignored so it can't be re-committed; zero "Headroom Cloud"/`headroom.dev` references remain. - Not tested: N/A — no executable code changed (only markdown, `.mdx`, and `.gitignore`), so the behavioral test suite is unaffected. ## 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 - [ ] 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 branch deletes `.github/FUNDING.yml` while PR #1526 edits it — the two will be sequenced at merge (delete wins). - A follow-up option (not in this PR): also remove the internal design docs that are currently cited by the code (`REALIGNMENT/`, `docs/observability.md`, `docs/rtk-architecture.md`, `wiki/plans/`) — that requires scrubbing ~15–20 in-code citations so nothing dangles, so it's deliberately deferred. - Untracked local working files (`benchmarks/hf_pilot/`, `tools/copilot-test/`) are intentionally left out of git (not committed).
112 lines
6.4 KiB
Markdown
112 lines
6.4 KiB
Markdown
# Spec — `headroom-oauth2` (generic OAuth2 client-credentials upstream auth)
|
|
|
|
## Summary
|
|
|
|
A vendor-neutral proxy extension (registers on Headroom's `headroom.proxy_extension` seam) that
|
|
mints an OAuth2 **client-credentials** (RFC 6749 §4.4) bearer from a configured token endpoint and
|
|
injects it as the upstream `Authorization` on every proxied request. Lets Headroom front any
|
|
gateway that requires a minted machine token (not a static API key) — with **zero core changes**
|
|
and **no vendor specifics** (the gateway is entirely config/env).
|
|
|
|
It complements `#510` (env-var auth), which assumes a long-lived static key; this covers the
|
|
"mint-then-refresh a short-lived token" case.
|
|
|
|
## API surface (config / CLI / env)
|
|
|
|
Opt-in only, via Headroom's existing flags — **no new CLI flags**:
|
|
|
|
headroom proxy --backend litellm-openai --proxy-extension oauth2
|
|
# or HEADROOM_PROXY_EXTENSIONS=oauth2
|
|
|
|
All configuration is env (12-factor; nothing baked in):
|
|
|
|
| Env var | Required | Meaning |
|
|
|---|---|---|
|
|
| `HEADROOM_OAUTH2_TOKEN_URL` | yes (else no-op) | OAuth2 token endpoint; must be `https` (loopback `http` allowed for tests) |
|
|
| `HEADROOM_OAUTH2_CLIENT_ID` / `_CLIENT_SECRET` | yes | client credentials |
|
|
| `HEADROOM_OAUTH2_SCOPES` | no | space/comma-separated scopes |
|
|
| `HEADROOM_OAUTH2_AUDIENCE` | no | `audience` form param |
|
|
| `HEADROOM_OAUTH2_RESOURCE` | no | RFC 8707 target `resource` form param |
|
|
| `HEADROOM_OAUTH2_GRANT_TYPE` | no | default `client_credentials` |
|
|
| `HEADROOM_OAUTH2_AUTH_STYLE` | no | `post` (form creds) or `basic` (HTTP Basic) |
|
|
| `HEADROOM_OAUTH2_HEADERS` | no | static upstream headers, `K=V,K2=V2` (control chars rejected) |
|
|
| `HEADROOM_OAUTH2_TIMEOUT` / `_SKEW` | no | token request timeout / pre-expiry refresh skew (s) |
|
|
| `HEADROOM_OAUTH2_ALLOW_INSECURE` | no | `1` to allow a non-loopback `http` token_url (discouraged) |
|
|
|
|
Public Python API: `OAuth2ClientCredentials`, `OAuth2Middleware`, `OAuth2Error`, `install`,
|
|
`provider_from_env`, `parse_headers`.
|
|
|
|
## Changes to existing behavior / defaults / compatibility
|
|
|
|
- **None unless explicitly enabled.** The entry point is dormant until `--proxy-extension oauth2`
|
|
is passed, and even then a **no-op** unless `HEADROOM_OAUTH2_TOKEN_URL` is set.
|
|
- When active, it **overwrites the request `Authorization` header** with the minted bearer before
|
|
the backend runs. The client's own `Authorization`/`x-api-key` is intentionally replaced (the
|
|
proxy authenticates to the gateway on the client's behalf). *Compatibility note:* because the
|
|
request then carries a bearer, Headroom classifies it as OAuth-mode auth — same as supplying a
|
|
bearer yourself; no new classification path.
|
|
- No change to defaults, the request/response body, model routing, or compression.
|
|
|
|
## User stories (Given / When / Then)
|
|
|
|
- **Golden path** — *Given* a proxy started with `--proxy-extension oauth2` and valid
|
|
`TOKEN_URL`/`CLIENT_ID`/`CLIENT_SECRET`, *When* a client sends `/v1/messages`, *Then* the
|
|
extension mints (or reuses a cached) bearer and the upstream receives `Authorization: Bearer
|
|
<minted>` plus any static headers; the client never sees the secret.
|
|
- **Edge: token endpoint down** — *Given* an unreachable/erroring `TOKEN_URL`, *When* a request
|
|
arrives, *Then* the proxy returns `502 upstream_auth_error` (no upstream call, no secret/body
|
|
leak) and stays up; the next request retries.
|
|
- **Edge: wrong backend** — *Given* `--backend bedrock` (env-auth), *When* the extension installs,
|
|
*Then* it logs a loud warning that the injected bearer will have no effect and to use an
|
|
OpenAI-compatible/passthrough backend.
|
|
|
|
## Failure modes & recovery
|
|
|
|
| Failure | Behavior |
|
|
|---|---|
|
|
| Missing/invalid config at startup | `install()` raises -> proxy **fails closed** (won't start mis-auth'd) |
|
|
| Token endpoint unreachable / non-2xx / non-JSON / no `access_token` | `OAuth2Error` -> `502`, per-request, proxy stays up, retried next request |
|
|
| `expires_in` = 0/negative/absent | clamped to a positive TTL (never stale, never per-request mint) |
|
|
| Concurrent first requests | single-flight lock -> exactly one mint per refresh |
|
|
| Malformed `HEADROOM_OAUTH2_HEADERS` (CR/LF) | offending pair dropped with a warning (no header injection) |
|
|
|
|
## Resilience (Docker / native / wrappers / providers / multi-process)
|
|
|
|
- **Native & Docker:** identical; pure env-driven, std-lib only. Token minted via `urllib` against
|
|
the **system cert store**, so a corporate-injected CA is trusted with no bundled roots (works in
|
|
SSL-inspection networks).
|
|
- **Wrappers (`headroom wrap`, agent hooks):** the extension lives at the proxy layer, so anything
|
|
routed through the proxy inherits it transparently.
|
|
- **Providers:** effective for OpenAI-compatible / passthrough litellm backends (those that forward
|
|
the request bearer upstream). `bedrock`/`vertex`/`sagemaker` authenticate from env and ignore the
|
|
bearer -> the extension warns and is a no-op there.
|
|
- **Multi-process (multiple workers):** the token cache is per-process; each worker mints/refreshes
|
|
independently. Acceptable for client-credentials (idempotent, low rate); no shared state, no
|
|
cross-process lock needed. Documented so operators can size token-endpoint rate limits.
|
|
|
|
## Security & privacy
|
|
|
|
- Secrets are env-only; **never logged** and **never returned** to the client.
|
|
- The IdP error body is **drained, not surfaced** (may echo sensitive context).
|
|
- `token_url` is **https-enforced** (loopback exception for tests; explicit opt-out env).
|
|
- The minted bearer is sent only to the configured upstream; the client's inbound credential is
|
|
replaced, not forwarded onward.
|
|
|
|
## Observability / logging / telemetry
|
|
|
|
- `INFO` on install (token_url + auth style, no secrets) and on each mint (`ttl`, scopes).
|
|
- `WARNING` on mint failure, env-auth-backend no-op, and dropped malformed static headers.
|
|
- No metrics/telemetry emitted; piggybacks on Headroom's existing request logging. (A future
|
|
counter for mint/refresh/failure could be added if maintainers want it.)
|
|
|
|
## Rollback / migration
|
|
|
|
- **No migration** — additive and opt-in; existing deployments are unaffected.
|
|
- **Instant rollback:** drop `--proxy-extension oauth2` (or unset `HEADROOM_PROXY_EXTENSIONS`), or
|
|
uninstall the package. No state to clean up, no config format changes.
|
|
|
|
## Dependencies
|
|
|
|
- **Runtime:** standard library only (no new core dependency). `litellm` is touched **only** if
|
|
`HEADROOM_OAUTH2_HEADERS` is set, and it is already a Headroom backend dependency — declared here
|
|
as the optional `[litellm]` extra, not a hard requirement.
|