feat(cli): add headroom wrap auggie - #2720
Conversation
PR governanceThis PR does not yet satisfy the required template fields:
Please update the PR body, or move the PR back to draft while it is still in progress. |
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
JerrettDavis
left a comment
There was a problem hiding this comment.
The dedicated-proxy path is thoughtfully isolated by tenant, but --no-proxy bypasses that safety check entirely. The preflight guard is wrapped in if not no_proxy and _check_proxy(port), and _ensure_proxy only warns when no_proxy=True; it does not compare augment_api_url. The wrapper then injects the OAuth-bearing session with tenantURL=http://127.0.0.1:<port> regardless. Thus headroom wrap auggie --no-proxy can send the Augment access token to a non-Auggie Headroom proxy (which will route /chat-stream elsewhere/404) or any unrelated local service on that port, contradicting the docs’ promise that a non-Auggie proxy is rejected. Please require an existing healthy Headroom proxy whose normalized config.augment_api_url exactly matches the resolved tenant before launching in --no-proxy mode; otherwise fail closed with a dedicated-port instruction. Add regressions for no service, non-Headroom service/unavailable config, non-Auggie config, wrong tenant, and matching tenant. The branch is also currently conflicting with main; once this safety boundary is fixed, resolve the drift while preserving main’s newer proxy-target/routing changes. Since this remains a draft, I am not approving it as merge-ready.
|
Someone is attempting to deploy a commit to the Headroom Labs Team on Vercel. A member of the Team first needs to authorize it. |
JerrettDavis
left a comment
There was a problem hiding this comment.
Re-reviewed the refreshed head after merging current main; the focused Auggie suite is green locally (52 tests), and the merge resolution is clean. The prior security blocker remains: AUGMENT_SESSION_AUTH contains the OAuth-bearing redirected session, while the wrapped child can still receive --no-proxy (or another argument path that prevents the local proxy from being used). That can leave the real Augment tenant URL paired with the injected credential-bearing environment value. Please reject proxy-bypass arguments for wrap auggie, or otherwise prove the credential-bearing redirected session cannot be sent directly to the tenant. A focused regression test for the bypass case would make the invariant explicit.
|
I'll work on these minor improvements when the other 3 PRs I have accepted for 3-5 weeks are merged. |
JerrettDavis
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 4af4b2a after the main sync. The security invariant remains unresolved: wrap auggie still accepts --no-proxy and still injects the OAuth-bearing redirected AUGMENT_SESSION_AUTH, allowing the child to launch without establishing the local proxy path. Please reject that combination (or otherwise prove direct tenant use is impossible) and add the focused regression requested previously.
Route an AugmentCode Auggie session through the local Headroom proxy with a single command, mirroring `headroom wrap droid` / `wrap copilot`. Auggie reads its tenant URL from its OAuth session (~/.augment/session.json) and calls that host for every request. `wrap auggie` rewrites ONLY the session `tenantURL` to the local proxy (the access token is preserved byte-for-byte), passes the rewritten session via AUGMENT_SESSION_AUTH (env-only, so the token is never exposed on the process command line), and forwards every tenant path verbatim to the real tenant. The `POST /chat-stream` inference call gets an explicitly-registered route so its telemetry is tagged the `augment` provider in /stats; the upstream is an explicit argument (not a client header), so there is no routing-header spoofing vector. All models, including Prism routing, stay on the user's Augment subscription with no API keys and no persistent config edits. Ports are automatic: reuse a running Auggie proxy for the same tenant, otherwise start a dedicated proxy on the next free port (never disrupt a shared `wrap claude` or a different tenant, never fail asking for `--port`). Auggie's tenant wire is Augment-proprietary, so v1 forwards inference bodies unchanged (redirect + telemetry/observability); request compression and output-shaping for the Augment shape are follow-ups. New `--augment-api-url` proxy option (env AUGMENT_TARGET_API_URL) plus ProxyConfig.augment_api_url register the passthrough only when configured, so all other proxy behavior is unchanged (mirrors the --bedrock-api-url precedent). New provider module headroom/providers/augment/ with defensive session loading (friendly errors on missing/malformed session) and inherited-AUGMENT_*-env scrubbing. Also fixes a shared `_launch_tool` cosmetic bug: on proxy port fallback the printed banner now matches the corrected child env (both point at the live port). Tests: tests/test_cli/test_wrap_auggie.py (session rewrite, upstream precedence, env hygiene, automatic port fallback + same-tenant reuse, friendly errors) plus route present/absent, passthrough-forward, and malformed-body passthrough cases.
…ie docs CI added a no-manual-changelog guard: release-please generates CHANGELOG.md from the Conventional Commit PR title, so a hand-written entry fails the gate. Removed it; the auggie feature entry will appear automatically in the next release PR. docs/content/docs/auggie.mdx also had a stray trailing `</content>` line with no matching opening tag, which fumadocs-mdx rejects as invalid MDX and fails the Next.js docs build. Removed it.
Codecov flagged patch-coverage gaps on this PR. Added direct coverage for: - _start_proxy's augment_api_url wiring (forwarding --augment-api-url to the actual proxy subprocess), which no existing test exercised since every wrap-auggie test mocks _start_proxy away. - load_session raising ValueError when the session JSON parses but is not a dict (list/scalar), the one load_session error path with no test. - The CLI-level ValueError -> click.ClickException translation for a malformed session, previously only exercised at the load_session() unit level, not through the full `wrap auggie` command. Rebased onto the latest upstream/main.
… wrap auggie
Closes the rest of the Codecov gap on this PR:
- _launch_tool's port-fallback branch (rewriting env and the printed
env_vars_display lines when _ensure_proxy falls back to a different
port than requested) had no direct test.
- resolve_augment_upstream can return an empty string even when
load_session's truthiness check passed, if tenantURL strips down to
empty (all slashes). Added the CLI-level test for the friendly error
this produces.
Also fixes a live CodeQL / incomplete-url-substring-sanitization finding
(github.com/headroomlabs-ai/headroom/security/code-scanning/158): the
missing-binary test asserted a bare hostname substring
("docs.augmentcode.com" in result.output), which the query flags as a
potential hostname-validation anti-pattern regardless of whether the
code is actually validating anything. Asserting the full literal URL
instead resolves it.
100% patch coverage on this PR's own diff, verified locally by
cross-referencing exact added line numbers against coverage --cov-report.
…se positives Same fix as the sibling wrap-droid-factory PR. CodeQL's security-extended pack flags these test assertions as if they were security-relevant URL validation, even after using the full literal URL instead of a bare hostname fragment (both trigger it, the query pattern-matches on any domain-shaped string substring check regardless of scheme prefix). These are plain assertions on captured CLI stdout in a test, not a trust decision about an untrusted URL: the documented false-positive case for this query (no attacker-controlled input reaches these checks). Suppressed with the current-format inline annotation (# codeql[rule-id] on the line before the flagged line; the legacy same-line "lgtm[...]" syntax is deprecated and does not work with GitHub's current code scanning).
…ust via suppression comment Same root cause as the sibling wrap-droid-factory PR: the inline # codeql[...] suppression comment did not actually suppress the alert (this repo evidently restricts in-code alert dismissal, so a contributor's suppression comment is silently ignored). Fixed structurally instead: moved the two flagged URL literals to named constants / an f-string, which are different AST nodes than the StringLiteral the query's Compare-node check matches on.
Codecov's last report showed 1 partial line (98.9% patch coverage). Traced to two untested branches via --cov-branch: - _start_proxy's `if augment_api_url:` guard only had the True side tested (forwarding --augment-api-url). Added the False side: called without augment_api_url, the flag must not appear in the subprocess cmd. - The port-fallback block only had the "found a different port" branch tested. Added the case where _find_available_port returns the same port back (no free port found nearby): no fallback message printed, requested port kept. 100% patch coverage now, verified locally with branch coverage enabled.
Description
Add
headroom wrap auggie, which routes an AugmentCode Auggie session through the local Headroom proxy with a single command, mirroring the shippedwrap droid/wrap copilot.Auggie reads its tenant URL from its OAuth session (
~/.augment/session.json) and calls that host for every request. This command rewrites ONLY the sessiontenantURLto the local proxy (the access token is preserved byte-for-byte), passes the rewritten session to Auggie viaAUGMENT_SESSION_AUTH, and starts the proxy in Auggie mode. The proxy forwards every Auggie tenant path verbatim to the real tenant and tags thePOST /chat-streaminference call as theaugmentprovider in/stats. All models (including Prism routing) stay on the user's Augment subscription, with no API keys and no persistent config edits.Auggie's tenant wire is Augment-proprietary (
/chat-streamuses a bespoke request/response shape, not Anthropic/OpenAI), so this first version forwards inference bodies unchanged. The value delivered is the single-command redirect plus proxy observability. Request compression and output-token shaping for the Augment shape are follow-ups.Closes #2719
Type of Change
Changes Made
headroom/proxy/models.py: addProxyConfig.augment_api_url: str | None.headroom/proxy/server.py: threadaugment_api_urlthrough the/healthconfig report,_proxy_config_from_env(AUGMENT_TARGET_API_URL), the argparse--augment-api-urlflag, and the CLI config build.headroom/cli/proxy.py: add the--augment-api-urlclick option (envAUGMENT_TARGET_API_URL).headroom/providers/proxy_routes.py: whenaugment_api_urlis set, registerPOST /chat-stream->handle_passthrough(request, <tenant>, "chat-stream", "augment"). Registered only when configured (mirrors the Bedrock precedent). The upstream is an explicit argument, not resolved from a client-controllable header, so there is no routing-header spoofing vector to harden against.headroom/providers/proxy_targets.py:select_passthrough_base_urlreturns the Augment tenant upstream first when set, so every other Auggie REST call (/get-models,/agents/list-remote-tools,/settings/get-mcp-*-configs,/find-missing) reaches the tenant via the catch-all.headroom/providers/augment/(new):runtime.py(resolve_augment_upstream,build_redirected_session,load_sessionwith defensive errors,proxy_base_url),__init__.py.headroom/cli/wrap.py: new@wrap.commandauggie. Reads the session (friendly errors on missing/malformed), rewritestenantURL, scrubs inheritedAUGMENT_*env, setsAUGMENT_SESSION_AUTH, and launches via_launch_tool(..., agent_type="augment", augment_api_url=<tenant>).augment_api_urlis threaded through_launch_tool->_ensure_proxy->_start_proxy(new optional param, defaultNone, so all other wrappers are unchanged). A reuse guard refuses to reuse a non-Auggie proxy on the target port, since its missing/chat-streamroute would misroute Auggie, and points at a dedicated port instead.docs/content/docs/auggie.mdx(registered inmeta.json);README.mdfeature list and compatibility matrix;llms.txt. NoCHANGELOG.mdedit, release-please generates that entry from this PR title.tests/test_cli/test_wrap_auggie.py(16) plus/chat-streamroute present/absent and passthrough-forward cases intests/test_provider_proxy_routes.py, and an Augment target case intests/test_provider_proxy_targets.py.Testing
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
@augmentcode/auggie(interactive), modelhaiku4.5(cheapest), Headroom current-tree build, isolated proxy port 8790 (not the shared 8787).headroom wrap auggie --port 8790 -- -m haiku4.5, then at the Auggie prompt:Reply with exactly the single word: PONG.Auggie tenant URL rewritten -> http://127.0.0.1:8790andAuggie tenant upstream: https://xlb.api.augmentcode.com; Auggie repliedPONG.GET http://127.0.0.1:8790/statsshowed"by_provider": {"augment": 2}and"agent_usage"attributing both requests to theaugmentprovider, confirming real subscription traffic flowed through the proxy via the rewritten session. The model shows aspassthrough:chat-streambecause v1 does not parse Auggie's proprietary response for the real model id or token counts, which is the documented follow-up. Session-integrity check: md5 of~/.augment/session.jsonwas unchanged before and after a wrapped run, and itstenantURLremained the real tenant, confirming the env-only redirect writes nothing to disk.third_party_overridemodel (no third-party provider key available in the test env); request/response compression (out of scope for v1, proprietary wire); streaming UX beyond the buffered response shown above (token-by-token streaming and JSON-lines usage parsing is a follow-up).A real session, a screenshot of my terminal, how AugmentCode/Auggie has been supported, the real proofs:

Detection in dashboard (telemetry):

Review Readiness
Checklist
CHANGELOG.md, it is generated by release-please from my Conventional Commit PR titleAdditional Notes
One logical change, minimal diff, mirrors
wrap droidexactly. No changes to default proxy behavior, since the route and target selection are gated onaugment_api_url. No persistent config writes and nounwrap auggieneeded, since nothing durable is written. No credential is stored, logged, or fabricated, the token is preserved byte-for-byte and forwarded untouched.Follow-ups not in this PR: parsing the
/chat-streamJSON-lines response for the real model id, token usage, and cost so/statsshows real tokens and cost; streaming/chat-streaminstead of buffering; an Augment-shape request compressor and output-token shaping (the response reportstool_definitions_tokens ~= 34kper turn).