Skip to content

feat(proxy): add loopback OpenAI Codex OAuth adapter - #91199

Open
BELGARATHbb wants to merge 1 commit into
NousResearch:mainfrom
BELGARATHbb:feat/codex-loopback-proxy
Open

feat(proxy): add loopback OpenAI Codex OAuth adapter#91199
BELGARATHbb wants to merge 1 commit into
NousResearch:mainfrom
BELGARATHbb:feat/codex-loopback-proxy

Conversation

@BELGARATHbb

Copy link
Copy Markdown

Summary

  • add canonical openai-codex subscription-proxy adapter with codex alias
  • forward Responses API and models only; reject Chat Completions
  • use the existing Codex credential pool with exact 401 refresh/coalescing and 429 rotation
  • synthesize native Codex account/origin headers while stripping client spoofing
  • enforce exact trusted Codex upstream and loopback-only bind at CLI and server layers
  • preserve raw query encoding and request/SSE bytes; abort truncated downstream streams
  • close upstream sessions/responses across cancellation, prepare failure, streaming failure, and EOF

Security findings closed

  • arbitrary base URL OAuth-bearer exfiltration
  • client ChatGPT-Account-ID spoof when JWT claim is absent
  • LAN/public bind spending the ChatGPT subscription
  • queued concurrent 401 race after token refresh
  • percent-decoded query corruption and query-secret debug logging
  • truncated SSE reported as clean success
  • ClientSession leak on request cancellation
  • upstream response/session leak on downstream prepare failure

Verification

  • canonical isolated runner: 46 related tests passed
  • exact raw %252f / %2F query preservation
  • byte-identical function-call-output request and Responses SSE
  • deterministic cancellation/prepare/truncated-stream cleanup tests
  • Ruff passed
  • git diff --check passed
  • real main Codex pool resolved exact trusted backend and required identity headers without printing token material
  • live loopback proxy returned PROXY_GPT_SOL_READY
  • live GPT Sol Controller spawned two parallel GPT Sol Hermes agents and completed CONTROLLER_DELEGATION_PASS
  • live GPT Sol Controller routed one child to Belgariad and a dependent GPT Sol verifier completed LOCAL_ROUTE_VERIFIED_BY_GPT_SOL

Operational boundary

Codex proxy is hard-blocked from non-loopback binds. It has no inbound authentication and must remain on 127.0.0.1/::1.

@alt-glitch alt-glitch added type/feature New feature or request comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools provider/openai OpenAI / Codex Responses API P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 21, 2026

@andrexibiza andrexibiza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head bb23a98be19ce32d1c6ca3a6abf4306acf7d59db against exact current main/base 8794e5a21c980a0f26532cb4883284b786cb3f25.

The transport work is strong: I checked the exact upstream allowlist, exact trusted Codex base-url gate, adapter-owned header stripping/overlay, raw query preservation, body/SSE byte preservation, retry cleanup, prepare/cancellation cleanup, pool-backed 401 coalescing, 429 rotation, and the loopback checks at both CLI and server entry points. The focused tests cover the right failure directions rather than only the happy path.

There is one stop-the-line authority-boundary issue on the other side of the shape:

Loopback proves network location, not caller authority

OpenAICodexAdapter.loopback_only prevents LAN/public exposure, but create_app() still accepts every local caller and replaces whatever bearer it supplied with the profile owner's ChatGPT OAuth credential. On a multi-user host, 127.0.0.1/::1 is machine-local, not UID-local: another OS user or service on the same machine can connect to this TCP listener even though it cannot read the owning Hermes profile's credential files. That makes this proxy a confused-deputy path from local reachability to subscription-spending authority.

The new docs state the behavior directly: “It accepts any inbound bearer and spends the OAuth account owned by the profile that started it.” That is not equivalent to an authenticated local client. The existing Hermes API-server surface also treats bearer identity separately from bind location, and the prior Codex-proxy lineage already exposed this missing half:

  • #62297 (@darkyy92) implemented native Responses/pool-backed Codex with owner-only per-client keys specifically for multi-user Hermes hosts.
  • #62510 (@dtownsel) implemented constant-time downstream bearer enforcement and stripped/replaced that bearer before forwarding, although its older token-file/refresh implementation had separate review defects.
  • #54877 (@blazing-mj) is earlier adjacent/superseded shared-pool/Cloudflare-header work with a different Chat-Completions translation choice.

#91199 is the strongest current-main consolidation and should become the canonical owner, but it should not discard the downstream-authority half of that lineage.

Required fix: keep the hard loopback-only Codex bind, and require a downstream credential/identity before attaching the ChatGPT bearer. A file-backed bearer is fine if the file itself is proven owner-only/regular (or use another OS-owned local identity mechanism); compare secrets in constant time; never forward the downstream credential. Add an adversarial witness that starts the proxy under principal/user A, attempts /v1/responses without/wrong authority (representing another local principal), and proves no upstream request is made; then prove the authorized client succeeds. The invariant is: network locality may constrain reachability, but only authenticated local authority may spend the profile-owned subscription.

Topology / landing evidence

Please explicitly interlock/supersede #62297, #62510, and #54877 in the PR body rather than leaving three older open implementations looking like independent competing owners, and preserve the contributors' credit for the pieces carried forward. They are not all duplicates: #62297 is the closest superseded native-Responses/client-auth implementation, #62510 is complementary downstream-auth precedent with older defects, and #54877 is superseded/adjacent translation-era work.

Exact-head hosted evidence is not green yet: CI 32433967389, Docker 32433966978, and Nix 32433967017 are all action_required with no jobs created, so the local 46-test/live-canary receipts are useful but cannot substitute for exact-head hosted runs. After the authority fix, require fresh CI/Nix/Docker on the resulting exact SHA.

Once the downstream-authority boundary is present, the rest of this current-main shape looks materially better than the older Codex-proxy branches.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

Strong hardening alongside the new adapter. Highlights: OAuth credentials refuse any upstream except the pinned Codex base URL (hermes_cli/proxy/adapters/codex.py _credential_from_entry — a clean SSRF/trust boundary), client-controlled identity headers (originator, ChatGPT-Account-ID, User-Agent) are stripped in all spellings and re-applied from the trusted generator, loopback-only binding is enforced in both cmd_proxy_start and run_server (defense in depth against embedders calling the server directly), and the query-string handling now forwards request.raw_path percent-encoding verbatim through URL(encoded=True) instead of interpolating aiohttp's decoded string — fixing real corruption for encoded payloads while keeping queries out of logs.

The 401 coalescing path in get_retry_credential is thoughtfully done: a queued request holding the stale bearer adopts the pool's refreshed credential instead of burning the obsolete token twice.

Suggestions:

  1. _open_upstream_request duplicates the try/except-close pattern for CancelledError and Exception; a single except BaseException: await session.close(); raise (after the pragma'd init guard) reads tighter — cosmetic.
  2. is_loopback_host accepts only literal loopback IPs or the exact name "localhost"; a hostname resolving to 127.0.0.1 is refused. That's the right conservative default, but one docstring sentence stating hostnames-that-resolve are intentionally rejected would preempt "bug?" reports.
  3. Worth one test asserting _merge_adapter_headers cannot let a client-supplied authorization survive (it should always be overwritten last by the credential line) — the ordering invariant is currently implicit in handler structure.

@elkimek

elkimek commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

I opened a stacked security follow-up against this PR’s exact head: BELGARATHbb#2

It adds downstream bearer authentication for /health and /v1/* before any Codex credential resolution or upstream request, plus owner-only token-file validation and adversarial coverage. Verified with 33 focused tests, Ruff, compilation, git diff --check, sabotage proof, and a live Codex canary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have provider/openai OpenAI / Codex Responses API sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants