Skip to content

Add ug --header / UG_HEADERS for per-launch request headers - #1248

Merged
andy-xu-db merged 3 commits into
mainfrom
andy/ug-headers
Oct 23, 2026
Merged

andy-xu-db merged 3 commits into
mainfrom
andy/ug-headers

Conversation

@andy-xu-db

@andy-xu-db andy-xu-db commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

🥞 Stacked PR

Use this link to review incremental changes.


Summary

ug --header "Name: value" (repeatable) or UG_HEADERS (one Name: value per line) adds headers to one launch's Databricks requests, for example a LiteSwap traffic ID:

ug --header "x-databricks-traffic-id: testenv://liteswap/<pod>" codex
  • Codex: the headers are added to the launch's -c provider headers, for both plain and smart-routed launches. They're never written to ~/.codex/ucode.config.toml, so concurrent launches don't share them. A header name that ug or the admin already sends keeps its value. Codex merges config layers leaf by leaf, so a new name from -c survives /etc/codex/managed_config.toml, and a name the managed file already sets wins.
  • ug's own requests: workspace API calls (databricks.py), the MCP proxy, web search, smart-routing route selection, and the MCP OAuth probe. The MCP proxy and web search run as children of the agent and inherit UG_HEADERS. A header name ug already sets, such as Authorization or Content-Type, keeps its value in any case: each request lists UG_HEADERS first and ug's own headers after, and urllib keeps the last value for a name.
  • Validation: the flag and the env var go through one parser (ug_headers.py). A valid line has an HTTP token name and a value with no control characters, in Latin-1. A later line replaces an earlier one with the same name in any case.
    • A bad --header fails the command. The error names only the flag's position, never the value.
    • A bad inherited UG_HEADERS line is skipped, so it never blocks a command, ug revert included.

Claude Code support is in the stacked PR #1240. Claude takes ANTHROPIC_CUSTOM_HEADERS whole from managed settings, so it needs a mod.

Testing

  • test_ug_headers.py: the parser, plus --header appending to UG_HEADERS and rejecting a malformed line.
  • test_agent_codex.py: a plain launch sends the header in -c and leaves the profile file unchanged, and a smart-routed launch adds new names while keeping existing ones.
  • test_databricks.py: ug's GET and POST helpers carry the headers.
  • Full unit suite: 5330 passed. test_e2e_user_agent is excluded because it fails on this dev box, where managed settings override its capture server's URL. Ruff and ty are clean on src/.
  • The outdated-Codex --profile path (before 0.134, below ug's 0.145 minimum) doesn't get the headers.
  • Not checked: a real ug codex launch against the gateway, and Windows Codex.

This pull request and its description were written by Isaac.

@andy-xu-db

Copy link
Copy Markdown
Collaborator Author

/ug-review

Comment thread src/ucode/agents/codex.py
Comment thread src/ucode/agents/codex.py
Comment thread src/ucode/cli.py Outdated
Comment thread src/ucode/databricks.py
Comment thread src/ucode/databricks.py
Comment thread src/ucode/mcp_oauth.py Outdated
Comment thread src/ucode/mcp_web_search.py Outdated
Comment thread src/ucode/smart_routing/routing.py Outdated
Comment thread src/ucode/ug_headers.py Outdated
Comment thread src/ucode/ug_headers.py Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Polly AI Review

Blocking issues

  1. Existing request headers can be overwritten — src/ucode/databricks.py:338, src/ucode/databricks.py:445

    can we add extras case-insensitively without replacement? authorization: other replaces ug’s bearer, and Content-Type: text/plain replaces JSON encoding.

    The same precedence bug exists in src/ucode/mcp_web_search.py:152, src/ucode/smart_routing/routing.py:245, and src/ucode/mcp_oauth.py:81. This contradicts the documented promise that existing headers keep their values. A network-free urllib.request.Request probe confirmed replacement across differently cased names.

    can we extend tests/test_databricks.py::TestHttpUgHeaders::test_get_and_send_carry_ug_headers with lowercase authorization and content-type collisions?

  2. Invalid values escape startup validation — src/ucode/ug_headers.py:23

    can we validate value characters before requests? Embedded SOH passes validation and reaches HTTP serialization; X-Team: 日本 raises an uncaught UnicodeEncodeError later.

    The parser checks names and nonempty values, but not transport-compatible contents. This breaks the stated early-validation contract. Preserve valid whitespace rather than imposing an indiscriminate ASCII-only restriction.

    can we extend tests/test_ug_headers.py::test_ug_headers_reject_malformed_line with invalid controls and unsupported value encoding?

Security vulnerabilities

The first blocker affects authentication-header integrity. No additional credential leak, authorization bypass, or exploitable injection was established. Invalid control acceptance alone does not demonstrate request smuggling.

Non-blocking notes

  • tests/test_agent_codex.py:1507: can we combine the new smart-overlay case with TestCodexLaunch.test_smart_routing_overlay_exports_launch_headers, retaining both distinct assertions?
  • can we extend existing search, routing, OAuth, and MCP request tests to assert UG_HEADERS propagation and existing-header precedence? The new transport test covers only workspace helpers.

Approach

Keep the shared parser and launch-only Codex overlay. A shared case-insensitive, add-only merge helper would enforce the same precedence across request clients without duplicating Codex’s private helper.

Summary

Diff size (computed): 16 files; +184 / -2 lines (186 changed text lines); 0 binary files.

Category Files Added Deleted Share of changed text lines
Tests and test support 5 98 0 52.7%
Documentation 2 13 1 7.5%
Dependencies and lockfiles 0 0 0 0.0%
Implementation / other 9 73 1 39.8%

Shares use additions + deletions, including generated text and lockfiles. Binary files have no line count. Tests include fixtures/helpers under test paths and colocated test/spec files; categories are path-based, not coverage measurements.

Changes

The change adds the flag/environment parser, launch-scoped Codex overlays, header propagation through ug’s request clients, documentation, and focused tests. No dependencies or generated assets change.

The size is proportional to the cross-client outcome. The main implementation simplification is centralizing header precedence instead of repeating dictionary updates and spreads. Added source comments are concise.

Tests

Test-by-test assessment

No pytest suite was run during this review. Network-free probes verified header replacement and value-serialization behavior; the PR’s reported suite results were not independently reproduced.

Test / case Behavior protected Layer Needed? Action / rationale
tests/test_ug_headers.py::test_ug_headers_parse_one_per_line Multiline parsing, trimming, blank lines, and colons within values Unit keep Distinct parser behavior at the smallest reliable layer.
tests/test_ug_headers.py::test_ug_headers_reject_malformed_line — missing separator and empty-value parameters Malformed input rejection Unit keep Both cases exercise meaningful validation failures. Extend this matrix for blocker 2.
tests/test_ug_headers.py::test_header_flag_appends_to_ug_headers Repeated-flag rejection and environment-plus-flag composition CLI component keep Exercises the CLI boundary rather than only the parser. It does not establish rejection before requests or child inheritance.
tests/test_agent_codex.py::TestCodexLaunch::test_launch_adds_ug_headers_without_writing_the_profile Launch argv receives headers without changing profile bytes Component keep Protects delivery and non-persistence using existing launch setup.
tests/test_agent_codex.py::TestCodexLaunch::test_smart_routing_overlay_adds_ug_headers_keeping_existing_ones Overlay augmentation preserves existing names case-insensitively Component consolidate Existing test_smart_routing_overlay_exports_launch_headers exercises the same wrapper with a mocked renderer. Combine setup while retaining augmentation and export assertions.
tests/test_databricks.py::TestHttpUgHeaders::test_get_and_send_carry_ug_headers GET and POST request construction includes extras Component keep One transport stub covers both helpers. Add the collision assertions identified in blocker 1.
tests/test_databricks.py::_FakeResponse.status Response fake supports the sender’s status access Test support keep Reuses the existing fake instead of adding another response implementation.
tests/conftest.py::_isolate_ucode_state — UG_HEADERS cleanup Ambient launch headers do not contaminate tests Fixture keep Fits the existing environment-isolation fixture; it does not prove production environment restoration.
tests/README.md — header coverage entry Documents coverage boundaries and lack of gateway verification Documentation keep Useful coverage documentation. The smart-overlay case is wrapper-level evidence, not a real agent launch.

Existing launch-header export, managed-provider-header, request-authentication, OAuth, routing, and MCP tests provide reusable scenarios. They do not establish propagation of this newly introduced variable across every changed client. Child inheritance, Windows Codex, and the legacy profile path remain unverified; these gaps alone do not establish additional bugs.

For local verification, run uv run pytest tests/test_ug_headers.py tests/test_agent_codex.py tests/test_databricks.py. The regression checks should preserve existing authorization/content-type values and reject invalid values before HTTP serialization.

Scope

All inspected changes support per-launch request headers. No clearly unrelated changes were found. Claude’s native header integration remains explicitly outside this PR; the documented legacy-Codex and Windows limitations are validation boundaries, not unrelated scope.


Automated review by Polly · workflow run

andy-xu-db and others added 3 commits October 23, 2026 20:15
`ug --header "Name: value"` (repeatable) or `UG_HEADERS` (one `Name: value`
per line) adds headers to one launch's Databricks requests, e.g. a LiteSwap
`x-databricks-traffic-id`:

- Codex: added to the launch's `-c` provider headers, plain and smart-routed,
  never to a config file, so concurrent launches don't share them. A header
  ug or the admin already sends keeps its value.
- ug's own requests: workspace APIs, the MCP proxy, web search, smart-routing
  route selection, and the MCP OAuth probe.

ug validates the value up front, so a malformed line fails the command before
any request.

Co-authored-by: Isaac <no-reply@databricks.com>
Address review feedback on `ug --header` / UG_HEADERS:

- ug's own requests (workspace APIs, web search, smart routing, the MCP
  OAuth probe) add only header names they don't already set, in any case,
  so a launch header can no longer replace the bearer `Authorization`,
  `Content-Type`, or a caller's header. One `with_ug_headers` helper does it.
- A later line replaces an earlier one with the same name in any case, so
  `X-A` and `x-a` are never both sent and `--header` overrides an inherited
  value.
- Header values must be sendable: no control characters, Latin-1 only.
- A bad `--header` still fails the command, but the error names only the
  flag's position, never the value. A bad inherited UG_HEADERS line is
  skipped with a warning instead of blocking every command, `ug revert`
  included, and ug hands agents only the valid lines.
- The pre-0.134 Codex `--profile` launch warns that the headers aren't sent
  instead of dropping them silently.

Co-authored-by: Isaac <no-reply@databricks.com>
Spread `ug_headers()` first in each urllib header dict instead of a
`with_ug_headers` helper: urllib capitalizes names and keeps the last
value, so ug's own headers still win in any case. Drop the inherited-line
warning and rewrite, since the parser already skips bad lines, and the
pre-0.134 Codex warning, since ug requires Codex 0.145.

Co-authored-by: Isaac <no-reply@databricks.com>
@andy-xu-db
andy-xu-db added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 6d77a69 Oct 23, 2026
255 of 267 checks passed
@andy-xu-db
andy-xu-db deleted the andy/ug-headers branch October 23, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants