Repository navigation
Add a Claude Code mod so ug --header reaches Claude - #1240
andy-xu-db wants to merge 5 commits into
Conversation
64dd8c3 to
0bb5282
Compare
ug --header for per-session Claude Code gateway headersug --header reaches Claude
|
/ug-review |
Polly AI ReviewBlocking issuesOne blocking security issue is detailed below. Security vulnerabilities
Non-blocking notes
ApproachThe organization-tier mod is consistent with the stated managed-environment restriction, and reusing the privileged writer is appropriate. Installing the files before enabling their managed entries would simplify partial-failure handling. SummaryDiff size (computed): 13 files; +378 / -16 lines (394 changed text lines); 0 binary files.
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. ChangesThe PR adds a launch-local header-merging mod, privileged installation, managed plugin registration, and an inactive-mod warning. It also updates the sudo allowlists, directory-creation permissions, test support, and documentation. No dependencies or generated assets change. The implementation is proportional to the intended outcome. The concrete reduction opportunity is duplicated test setup and allowlist assertions, rather than removing a functional layer. TestsTest-by-test assessmentStatic review only; no tests were executed. The frozen snapshot checksum matched, and both required trusted policy files were read in full. The relevant reconciliation and privileged-worker context matched the diff’s base blobs.
Existing reconciliation and shell-allowlist parity tests provide useful supporting coverage. They do not cover the demonstrated guard-loss transition, per-request restoration after a settings reset, or the new warning/install-failure branches. The component and native-harness additions identified above are the smallest suitable extensions; another broad E2E suite is not needed for those assertions. For manual verification, use a disposable installation: configure with no ScopeAll changed files support forwarding session headers to Claude or installing, documenting, and validating that behavior. The nested-directory permission change is necessary for the installed mod to be readable. No clearly unrelated changes were found. Windows support and removing inactive mod files on revert are explicitly excluded from the stated outcome. Automated review by Polly · workflow run |
c27fdb3 to
ad66f00
Compare
68efcd3 to
39f7522
Compare
ad66f00 to
0d1b171
Compare
…icks#1248) ## 🥞 Stacked PR Use this [link](https://github.com/databricks/unity-gateway/pull/1248/files) to review incremental changes. - [**andy/ug-headers**](databricks#1248) [[Files changed](https://github.com/databricks/unity-gateway/pull/1248/files)] ← _this PR_ - [andy/claude-header-mod](databricks#1240) [[Files changed](https://github.com/databricks/unity-gateway/pull/1240/files/68efcd391568224674249a3f973c59fa5db5be80..7c47188093226a1654efc3a836cdd18c7f972784)] --------- ## 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: ```bash 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 databricks#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. --------- Co-authored-by: Isaac <no-reply@databricks.com>
Claude takes ANTHROPIC_CUSTOM_HEADERS whole from managed settings, so a header from `ug --header` / UG_HEADERS in ug's own settings file or in the environment never reached the gateway, and writing it into the machine-wide file would leak it into every session. `ug configure` (and an interactive launch) now installs a small Claude mod in a root-owned `ucode-plugins` folder beside the managed settings, on Linux and macOS, through the existing sudo worker; its four files join the allowlist. The managed file enables it as the organization's mod, ahead of the built-in guard, the one tier allowed to change a variable managed settings set. Before each request the mod adds the session's UG_HEADERS lines to ANTHROPIC_CUSTOM_HEADERS in that Claude process only; a header already present keeps its value. Revert removes the managed entries; a launch warns when the mod isn't active. The sudo script now creates each missing parent folder root-owned and 0755: `mkdir -p` left the folders above the innermost one with the caller's umask, so under 027 neither ug nor Claude could read the mod. Co-authored-by: Isaac <no-reply@databricks.com>
The sudo script created the mod's parent folders with the caller's umask (027 left them unreadable to ug and Claude). Setting `umask 022` in the root script fixes that for every folder `mkdir -p` makes, so the per-folder creation helper isn't needed. Co-authored-by: Isaac <no-reply@databricks.com>
Address review feedback on the Claude header mod: - Install the mod's files before the managed settings name them, and add the entries only when every file is in place (taking ug's entries back out otherwise), so a failed install never enables a missing plugin for every user on the host. The install now shows ug's sudo notice itself. - `ug revert` removes the mod's root-owned files and the folders they leave empty. When an admin has extended `prependPlugins` since configure, revert keeps `sec-default@builtin`, since a non-empty list without it turns the built-in guard off. - `header_mod_active` checks every mod file and all three managed entries. - The mod skips malformed and repeated UG_HEADERS lines and reads CRLF header strings, for launches that don't go through ug. Co-authored-by: Isaac <no-reply@databricks.com>
Enable the mod only when this run installed its files, leaving the managed file as is otherwise, instead of taking ug's entries back out; a run that can't write as root no longer needs to check the files. Remove the mod's folder on revert with one `rm -rf` of ug's fixed `ucode-plugins` path. `header_mod_active` checks every file and `enabledPlugins`. Co-authored-by: Isaac <no-reply@databricks.com>
A plain launch re-added the mod's managed-settings entries whenever another writer of the file dropped them, so every session rewrote it through sudo (CUJ 8). `ug configure` and launches with UG_HEADERS still install it. Also stub refresh_managed_config in the header mod tests, which reached the databricks CLI on CI. Co-authored-by: Isaac <no-reply@databricks.com>
0d1b171 to
fb7ad1a
Compare
Follows #1248 (merged), which added
ug --header/UG_HEADERSfor Codex and ug's own requests. This PR makes the same headers reach Claude Code:ug --header "x-databricks-traffic-id: testenv://liteswap/<pod>" claudeWhy a mod
Claude takes
ANTHROPIC_CUSTOM_HEADERSwhole from the highest-precedence settings. That'smanaged-settings.jsonwhenever ug or Isaac writes the key there, so a header in ug's own--settingsfile or in the environment never reaches the gateway. Writing the header into the managed file instead would make it machine-wide, require sudo on every launch, and race with every other session and with Isaac.A Claude mod can change
ANTHROPIC_CUSTOM_HEADERSin its own process with$.env.set, and Claude reads that variable again on every request. The built-in guard refuses this change from a user's mod when managed settings set the variable. It allows it from an organization's mod: one enabled in managed settings, loaded in place from a root-owned directory marketplace, and listed inprependPlugins.What changes
agents/claude_header_mod/register.ts): onsession.start, and before every request (turn.step), it adds the session'sUG_HEADERSlines toANTHROPIC_CUSTOM_HEADERS. A header name already present keeps its value, so a launch can't replace ug's or an admin's headers. Malformed and repeated lines are skipped, for launches that don't go through ug. Nothing is written to disk.ug configure, and interactive launches, write the mod's four files to/etc/claude-code/ucode-plugins/on Linux or/Library/Application Support/ClaudeCode/ucode-plugins/on macOS. The writes go through the existing sudo worker, and the exact paths are added to its allowlist in bothmanaged_files.pyandmanaged_replace.sh. A file is only rewritten when its contents change. The sudo script now setsumask 022. Before, the foldersmkdir -pcreated took the caller's umask, and under umask 027 the mod was unreadable.extraKnownMarketplaces.ucode-managed,enabledPlugins["ug-headers@ucode-managed"], and puts the mod first inprependPlugins.[mod, "sec-default@builtin"], so the built-in guard stays loaded.ug revertremoves the entries and the mod's root-owneducode-pluginsfolder. When an admin has extendedprependPlugins, it keepssec-default@builtin.UG_HEADERSis set but the mod isn't active (Windows,--user/UG_SKIP_OS_SETTINGS, or ug was never configured interactively), the launch warns.AGENTS.mdfields table, andtests/README.md.Not covered
Security review: this installs an organization-tier mod, which the guard lets change any variable that managed settings set. The mod only adds header lines and never replaces an existing name.
Testing
tests/test_claude_header_mod.py, which goes through the real reconcile flow:prependPluginsis keptheader_mod_activeneeds every filetests/test_managed_files.py: the real shell script, run with umask 027 and two missing folders, creates both with mode 0755. This test fails on the old script.native_claude/test_mod.pywithheader_mod_tests/:claude plugin validateplus the mod's merge tests underclaude plugin test(Claude 2.1.304).test_e2e_user_agentis excluded because it fails on this dev box, where managed settings override its capture server's URL./etc/claude-codereplaced by a tmpfs in a private mount namespace, using ug-generated mod files and a managed file that setsANTHROPIC_CUSTOM_HEADERS. In every run the mod loaded withtier prependand the guard refused nothing.UG_HEADERScompleted a tool-calling turn.header 3 of 3 parsed from ANTHROPIC_CUSTOM_HEADERS, so the managed headers plus the mod's header were sent.ug configurewith sudo, and it didn't check the header on the gateway side.This pull request and its description were written by Isaac.
E2E Test:
Screen.Recording.2026-10-22.at.5.59.09.PM.mov