Skip to content

Apply managed http_headers to Claude Code's custom headers - #772

Open
tt-le wants to merge 13 commits into
mainfrom
tien/claude-managed-http-headers
Open

tt-le wants to merge 13 commits into
mainfrom
tien/claude-managed-http-headers

Conversation

@tt-le

@tt-le tt-le commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

What

Admin-supplied http_headers in the managed coding-agent config were parsed and publishable but never reached any agent. Wire them into Claude Code. Independent of the Codex PR (#773) — both target main.

  • managed_resolve.managed_state_overrides emits {tool}_http_headers.
  • claude.render_overlay merges the admin headers into ANTHROPIC_CUSTOM_HEADERS.

Behavior

  • Admin wins on a case-insensitive name collision — replaces ucode's own line in place, appends new ones.
  • Clean removal, no ug state — the ucode-owned set of header names is derived from the files themselves: ucode owns ucode-settings.json wholesale (every header there is ucode's, so a dropped managed header can't linger), and for the OS-managed file only the names ucode last wrote (last_applied_by_ug) are treated as ucode's, leaving IT-authored headers untouched. (An earlier revision tracked names in ug state; that isn't preserved across runs, so a removed header lingered — fixed here.)

Tests

  • render_overlay / _merge_anthropic_custom_headers: add, in-place override, removal, IT-header preservation.
  • write_tool_config: admin header written; dropped after removal on the ucode file; IT header preserved on the managed file via last_applied_by_ug.
  • managed_resolve: http_headers{tool}_http_headers override.
  • Wire-level (tests/test_e2e_user_agent.py): the capture-server harness launches the real Claude binary and asserts an admin header arrives on the inference request.

Note on overlap with #773

Shares the small managed_resolve plumbing and the _header test helper with #773; identical in both, trivial overlap on whichever merges second. Neither depends on the other.

This pull request and its description were written by Isaac.

Admin-supplied `http_headers` in the managed coding-agent config were parsed and
publishable but never reached any agent. Wire them into Claude Code:
`managed_state_overrides` now emits `{tool}_http_headers`, and `render_overlay`
merges them into `ANTHROPIC_CUSTOM_HEADERS`.

Admin headers win on a case-insensitive name collision, replacing ucode's own
line in place and appending any new ones. The header names ucode writes are
tracked in state so a header an admin later removes is dropped rather than
lingering as an unrecognized user header, while hand-added user headers are
preserved.

Co-authored-by: Isaac <no-reply@databricks.com>
tt-le and others added 2 commits September 21, 2026 16:00
Reduce the inline commentary added with the managed http_headers wiring.

Add a capture-server check to tests/test_e2e_user_agent.py that launches the
real Claude binary and asserts an admin-supplied managed header arrives on the
inference request, alongside the existing User-Agent assertion.

Co-authored-by: Isaac <no-reply@databricks.com>
Extend tests/test_e2e_user_agent.py so the capture-server harness launches the
real Claude binary and asserts an admin-supplied managed header arrives on the
inference request, alongside the existing User-Agent assertion. Adds a shared
case-insensitive header lookup helper.

Co-authored-by: Isaac <no-reply@databricks.com>
@tt-le tt-le added the ug0.2-launch-blocker Launch Blocker for UG 0.2 label Sep 21, 2026
The prior removal tracking stored the header names ucode wrote in ug state, but
ug state is not reliably preserved across runs, so a removed managed header
lingered while the new one was added (both appeared).

Derive the ucode-owned header set from the files instead: ucode owns its own
ucode-settings.json wholesale (every header there is ucode's, so a dropped
managed header cannot linger), and for the OS-managed file only the names ucode
last wrote (last_applied_by_ug) are treated as ucode's, leaving IT-authored
headers untouched. Removes the ug-state tracking key entirely.

Co-authored-by: Isaac <no-reply@databricks.com>
tt-le added a commit that referenced this pull request Sep 21, 2026
Wire the managed coding-agent config's `http_headers` into Codex, independent of
the Claude change: `managed_state_overrides` emits `{tool}_http_headers`, and
`_provider_block` merges them into the Databricks provider's `[http_headers]`
table (current and legacy layouts).

Admin headers win on a case-insensitive name collision, replacing ucode's own
header (including routing headers) rather than emitting two spellings of one HTTP
header. ug is the sole author of that table, so it is pruned wholesale before
each merge and rewritten from render_overlay — dropping routing headers that no
longer apply and any admin header removed from managed config, with no ug state.

Adds a wire-level check to tests/test_e2e_user_agent.py that launches the real
Codex binary and asserts an admin header arrives on the inference request.

Note: shares the small managed_resolve plumbing and the `_header` test helper
with the Claude PR (#772); whichever merges second
resolves a trivial overlap.

Co-authored-by: Isaac <no-reply@databricks.com>
tt-le and others added 4 commits September 21, 2026 17:13
Removes the duplicate `if tool in ("claude", "codex")` in managed_state_overrides
by grouping the http_headers override with static_models. No behavior change; the
override is still emitted only for the agents whose writers consume it.

Co-authored-by: Isaac <no-reply@databricks.com>
Emit `{tool}_http_headers` for any tool rather than only claude/codex. Only those
writers consume it today, so the override is inert for other agents, and dropping
the gate keeps the resolver simple.

Co-authored-by: Isaac <no-reply@databricks.com>
CLAUDE_MANAGED_CUSTOM_HEADER_NAMES is a frozenset, so the bare union
(frozenset | set | set) evaluated to frozenset[str], violating the
declared -> set[str] return type. Wrap the union in set(...).

Co-authored-by: Isaac <no-reply@databricks.com>
tt-le added a commit that referenced this pull request Sep 21, 2026
## What

Wire the managed coding-agent config's `http_headers` into **Codex**.
Independent of the Claude PR (#772) — both target `main`.

- `managed_resolve.managed_state_overrides` emits `{tool}_http_headers`.
- `codex.write_tool_config` reads `codex_http_headers`;
`_provider_block` merges the admin headers into the Databricks
provider's `[http_headers]` table (current per-profile layout and legacy
`< 0.134.0` layout).

## Behavior

- **Admin wins on a case-insensitive name collision** — a managed header
replaces ucode's own header (including routing headers), never emitting
two spellings of one HTTP header.
- **Clean removal, no state** — ug is the sole author of the provider
`http_headers` table, so it's pruned wholesale before each merge and
rewritten from `render_overlay`. This drops routing headers that no
longer apply and any admin header removed from managed config;
`deep_merge` alone cannot delete keys.

## Tests

- `render_overlay`: admin header added; admin overrides ucode's routing
header case-insensitively with no duplicate spelling.
- `write_tool_config` end-to-end: admin header written to `config.toml`;
dropped after removal while `User-Agent` is kept.
- Wire-level (`tests/test_e2e_user_agent.py`): the capture-server
harness launches the real Codex binary and asserts an admin header
arrives on the inference request.

## Note on overlap with #772

This PR shares the small `managed_resolve` plumbing (the
`{tool}_http_headers` emit) and the `_header` test helper with #772.
They're identical in both; whichever merges second resolves a trivial
overlap. Neither depends on the other.

This pull request and its description were written by Isaac.

---------

Co-authored-by: Isaac <no-reply@databricks.com>
tt-le and others added 5 commits September 21, 2026 16:29
…ership

ug now writes the entire ANTHROPIC_CUSTOM_HEADERS value (its static headers
plus the admin's managed http_headers, already merged by render_overlay) to
both ucode-settings.json and the OS managed-settings.json, dropping the
per-header ownership inference.

The previous approach derived "names ug owns" from snapshots.last_applied_by_ug,
which records the whole previously-written merged document. A foreign/admin
header ug merely passed through on the first write was therefore adopted into
the owned set and silently dropped on the next reconcile -- the opposite of the
intended "leave an administrator's own headers untouched". Overwriting the whole
value removes the need to infer ownership, so a header ug no longer emits is
dropped and nothing stale or foreign lingers. This matches Codex's full-replace
model; env.ANTHROPIC_CUSTOM_HEADERS is already a ug-owned managed key path, so
the wholesale write is authorized.

Intentional behavior change: a custom header must arrive through the managed
config http_headers manifest to appear. A header hand-edited directly into the
managed file's ANTHROPIC_CUSTOM_HEADERS value is no longer preserved.

Removes CLAUDE_MANAGED_CUSTOM_HEADER_NAMES, _custom_header_names,
_managed_header_names, and _merge_anthropic_custom_headers. Rewrites the
managed-file header tests and adds a real three-generation reconcile round-trip.

Co-authored-by: Isaac <no-reply@databricks.com>
write_tool_config now decides how to write the Claude custom-header value from
whether an admin managed config is present this launch
(refresh_managed_config(state).manifest is not None, reusing the launch's warm
cache; a failed fetch degrades to unmanaged and never blocks the write):

- managed config present: overwrite the value wholesale with ug's static headers
  plus the admin's managed http_headers, so a header ug no longer emits is dropped
  and nothing stale or foreign lingers.
- no managed config: restore the pre-managed-config preserving merge -- keep the
  developer's own pre-existing headers, replacing only the header names ug manages
  (the static CLAUDE_MANAGED_CUSTOM_HEADER_NAMES set). This brings back the
  behavior from #442 for the unmanaged case without reintroducing the removed
  per-header ownership inference that dropped foreign headers on reconcile.

existing_custom_headers is read before deep_merge_dict mutates base["env"] in
place; reading it afterward would see the overlay's own value and collapse the
preserve merge into an overwrite.

Co-authored-by: Isaac <no-reply@databricks.com>
The static-name merge is covered by TestMergeAnthropicCustomHeaders unit tests,
so the write_tool_config-level tests only need one gate test per branch plus the
real-reconcile integration test. Remove three duplicates:
- test_managed_file_overwrites_anthropic_custom_headers_wholesale (subsumed by
  test_managed_file_overwrites_dropping_foreign_and_removed_headers)
- test_unmanaged_drops_stale_ucode_managed_header_no_longer_emitted (pure merge
  logic, covered by the unit test)
- test_unmanaged_wholesale_preserve_survives_real_reconcile_round_trip (reconcile
  plumbing covered by the managed round-trip; unmanaged preserve by unit + gate)

Co-authored-by: Isaac <no-reply@databricks.com>
Pure repositioning, no behavior change: the earlier wholesale->conditional
detour deleted and re-added _merge_anthropic_custom_headers (and its
TestMergeAnthropicCustomHeaders suite), landing them far from where main keeps
them and inflating the review diff. Move the helper back below write_tool_config
and restore the test class to its original spot (matching main verbatim), and
drop two stray comments so unchanged code shows no diff. The real change is now
just the managed-config gate in write_tool_config plus _apply_managed_header_lines.

Co-authored-by: Isaac <no-reply@databricks.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ug0.2-launch-blocker Launch Blocker for UG 0.2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant