Skip to content

feat(agents): add the Claude Code CLI agent harness - #88

Open
eugeneng04 wants to merge 5 commits into
kubernetes-sigs:mainfrom
eugeneng04:feat/agents-claude-code-harness
Open

feat(agents): add the Claude Code CLI agent harness#88
eugeneng04 wants to merge 5 commits into
kubernetes-sigs:mainfrom
eugeneng04:feat/agents-claude-code-harness

Conversation

@eugeneng04

@eugeneng04 eugeneng04 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Adds a claude CLI agent harness that drives the claude binary in headless
mode and folds its event stream into the canonical trajectory and token
buckets. Ported from gke-labs/devops-bench#199 and rewired onto the shared
token schema that landed here since.

What it does

Runs claude -p --output-format stream-json --verbose and parses the wrapped
SDK event stream on stdout — no session-file reads off disk. Capabilities go
through Claude Code's native cwd channels, written into the per-run working
directory:

Capability Channel
Rules CLAUDE.md in the cwd
Skills <cwd>/.claude/skills/<name>/SKILL.md
MCP servers <cwd>/.claude/mcp-config.json via --mcp-config

Auth is env-driven through the shared provider contract: config.api_key onto
the provider's key env var(s), or keyless Vertex / Bedrock via ADC / AWS
credentials. CLAUDE_CONFIG_DIR is redirected to a fresh per-run temp dir so
the CLI's mutable global state never races across concurrent evals.

Token buckets

The upstream PR carried a harness-local bucket tuple pending the shared schema.
This version maps the Anthropic usage block straight onto TOKEN_BUCKETS,
with cache reads and cache writes kept in separate buckets (writes bill at a
premium):

Provider field Bucket
input_tokens input
cache_read_input_tokens cached
cache_creation_input_tokens cache_write
output_tokens_details.thinking_tokens reasoning
output_tokens less the above output

Extended thinking is billed inside output_tokens and counted again under
output_tokens_details, so it is subtracted back out — output excludes
reasoning per the contract, and total still equals the provider's own
accounting. An unreported bucket stays None rather than a fabricated 0. A
cross-layer test asserts the emitted keys survive normalize_tokens onto the
dashboard row.

Alias canonicalization

claude-code joins gemini-cli as a friendly alias. Both the registry lookup
and the manifest write now go through one _canonical_agent_type helper, so an
arm selected by alias aggregates under the same harness / setup_id as the
canonical key instead of splitting into a second dashboard setup.

Review fixes

A review panel over the port turned up seven issues, each reproduced against
the real claude binary (2.1.228) before fixing, and fixed in the second
commit:

  • Thinking tokens misfiled into output. The port assumed Anthropic reports
    no separate thinking count. A live run returns
    "output_tokens":3069,"output_tokens_details":{"thinking_tokens":2778}, and
    the thinking count is included in output_tokens.
  • The per-turn usage accumulator reported output ~1000x too low. Per-turn
    usage.output_tokens on an assistant envelope is the message_start
    placeholder — a summed 3 against a terminal 3069. It only fires on the
    timed-out path, so every timed-out run recorded a plausible-looking wrong
    number. The bucket is now left unreported; the prompt-side fields do
    accumulate faithfully and are kept.
  • str.splitlines shredded events containing U+0085. The CLI escapes U+2028
    but leaves NEL raw inside JSON strings, which real command output carries when
    a log is mis-decoded as latin-1. On a reproduced stream that cost 2 of 3 tool
    calls and injected 6 bogus errors — enough to flip validated to False and
    drop the run off the leaderboard. Framing now keys on \n alone.
  • Tool results retained verbatim. Claude Code echoes the full body of every
    Read/Bash result (measured ~20 KB per call, one Read at 58 KB), and the whole
    trace is json.dumps'd into two LLM judge prompts with nothing truncating in
    between. Clipped to a head+tail slice with the middle marked elided.
  • --strict-mcp-config skipped on the baseline arm. It was only passed
    alongside --mcp-config. Verified: without it a stray .mcp.json in the
    workspace loads; with it, mcp_servers is empty. That silently granted MCP
    tools to the un-augmented arm, contaminating the comparison the bench exists
    to measure. Now unconditional.
  • No -- before the prompt. A prompt whose first token starts with - was
    read as a flag and aborted the run with rc=1 and no output.
  • Uncapped child stderr persisted to results. metadata["stderr"] was
    clipped at 2000 chars while the same stderr went unbounded into errors,
    which is what reaches disk. Both paths now share one cap.

Also verified and deliberately left alone: CLOUD_ML_REGION is the correct
Vertex region variable for this CLI (ANTHROPIC_VERTEX_LOCATION does not
appear in the binary at all).

Testing

  • 66 unit tests for this harness; 1248 across the suite, all passing.
  • ruff check / ruff format clean.
  • End-to-end against the real binary in a workspace seeded with a stray
    .mcp.json: clean run, one matched tool call, stray server suppressed, and
    total equal to the sum of the reported buckets.

Summary by CodeRabbit

  • New Features

    • Added support for running evaluations with the Claude Code CLI.
    • Added structured capture of assistant responses, tool activity, terminal output, token usage, and errors.
    • Added isolated per-run configuration and MCP setup for more reliable execution.
    • Added support for resolving the claude-code alias to the canonical Claude harness.
  • Documentation

    • Updated provider configuration guidance for Claude CLI usage.
  • Tests

    • Added comprehensive coverage for parsing, execution, error handling, configuration, and registry resolution.

Runs the claude binary headless (-p --output-format stream-json --verbose
--dangerously-skip-permissions) and folds the wrapped SDK event stream into
the canonical trajectory. Selectable as BENCH_AGENT_TYPE=claude, with a
claude-code alias.

Capabilities ride Claude Code's native cwd channels: rules to CLAUDE.md,
skills to <cwd>/.claude/skills, and MCP bindings to a --mcp-config document
pinned with --strict-mcp-config. Auth is env-driven through the shared
provider contract (Anthropic key, or keyless Vertex / Bedrock), and
CLAUDE_CONFIG_DIR is redirected per run so concurrent evals never race on
the CLI's global state.

Token usage maps onto the shared TOKEN_BUCKETS: Anthropic's input_tokens is
already the uncached prompt, cache reads and writes stay separate buckets,
and reasoning stays unreported because thinking is billed inside
output_tokens.

Aliases now canonicalize before the manifest is written, so an arm selected
as claude-code aggregates with claude instead of splitting the dashboard
setup.

Signed-off-by: Eugene Ng <ngeugene@google.com>
…ation

Review-panel findings against the new harness, each verified against the
real ``claude`` binary rather than the docs:

* Extended thinking is billed inside ``output_tokens`` and reported again
  under ``output_tokens_details.thinking_tokens``. Split it back out into
  ``reasoning`` so ``output`` honours the canonical contract while ``total``
  stays equal to the provider's own accounting.
* The per-turn usage accumulator summed ``output_tokens``, which on an
  assistant envelope is the ``message_start`` placeholder — a summed 3
  against a terminal 3069. Drop it, so a timed-out run leaves the bucket
  unreported instead of persisting an invented number.
* ``str.splitlines`` split on unescaped U+0085, which the CLI emits raw
  inside JSON strings. That shredded the event, lost its tool call, and
  injected decode errors that flip the run to unvalidated. Frame on ``\n``.
* Tool results were retained verbatim, and the trajectory is re-serialized
  into the judge prompts; clip to a head+tail slice with the middle marked.
* ``--strict-mcp-config`` was conditional on the harness writing its own
  config, so a stray ``.mcp.json`` in the workspace silently granted MCP
  tools to the baseline arm. Always pass it.
* Shield the prompt behind ``--``; the option parser otherwise reads a
  leading ``-`` as a flag and aborts the run.
* Clip the child stderr that reaches the persisted ``errors`` list, matching
  the cap the sibling ``metadata`` field already applies.

Signed-off-by: Eugene Ng <ngeugene@google.com>
@kubernetes-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: eugeneng04
Once this PR has been reviewed and has the lgtm label, please assign janetkuo for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubernetes-prow
kubernetes-prow Bot requested a review from janetkuo August 11, 2026 21:01
@kubernetes-prow kubernetes-prow Bot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Aug 11, 2026
@kubernetes-prow

Copy link
Copy Markdown

Hi @eugeneng04. Thanks for your PR.

I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Tip

We noticed you've done this a few times! Consider joining the org to skip this step and gain /lgtm and other bot rights. We recommend asking approvers on your previous PRs to sponsor you.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@kubernetes-prow kubernetes-prow Bot added needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@eugeneng04, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f055609-efda-452f-9783-183df0ee84dc

📥 Commits

Reviewing files that changed from the base of the PR and between 704a078 and 253e0ef.

📒 Files selected for processing (2)
  • devops_bench/agents/cli/claude_code/agent.py
  • tests/unit/agents/test_agents_cli_claude_code.py
📝 Walkthrough

Walkthrough

Adds a Claude Code CLI harness with stream-JSON parsing, provider and MCP configuration, isolated workspaces, failure recovery, canonical registry resolution, documentation updates, and unit tests.

Changes

Claude Code integration

Layer / File(s) Summary
Stream parser and token accounting
devops_bench/agents/cli/claude_code/parsing.py
Parses stream-JSON events into assistant output, tool trajectories, errors, MCP statuses, and normalized token usage.
Claude Code harness execution
devops_bench/agents/cli/claude_code/__init__.py, devops_bench/agents/cli/claude_code/agent.py
Adds the ClaudeCodeAgent harness. It builds CLI arguments and provider environments, materializes capabilities, isolates configuration, executes claude, and returns structured results.
Canonical registry and provider contract
devops_bench/evalharness/default.py, docs/components/model_providers.md, docs/how-to/add-a-model-provider.md
Registers Claude Code, maps claude-code to claude, records canonical harness identities, and documents shared provider resolution.
Harness and registry validation
tests/unit/agents/test_agents_cli_claude_code.py, tests/unit/evalharness/test_registry_resolution.py
Tests parsing, configuration, capability wiring, subprocess outcomes, workspace isolation, and alias resolution.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeCodeAgent
  participant Workspace
  participant ClaudeCLI
  participant StreamParser
  ClaudeCodeAgent->>Workspace: materialize rules, skills, and MCP configuration
  ClaudeCodeAgent->>ClaudeCLI: execute headless stream-JSON command
  ClaudeCLI-->>ClaudeCodeAgent: return stdout, stderr, and exit status
  ClaudeCodeAgent->>StreamParser: parse captured stdout
  StreamParser-->>ClaudeCodeAgent: return text, trajectory, usage, and errors
Loading

Possibly related PRs

Suggested reviewers: janetkuo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding the Claude Code CLI agent harness.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/unit/agents/test_agents_cli_claude_code.py (1)

49-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Annotate the fixture parameters and the remaining helper return types.

Test functions in this file annotate their return type but not their fixture parameters. monkeypatch and tmp_path are unannotated at Lines 742, 753, 823, 842, 856, 867, 888, 901, 912, 934, 945, 964, 984, 997, 1022, 1041, 1071, 1092, and 1107. The helpers _assistant and _user at Lines 49 and 53 have no return annotation.

♻️ Proposed annotations
-def _assistant(*blocks: dict) -> dict:
+def _assistant(*blocks: dict) -> dict:
     return {"type": "assistant", "message": {"content": list(blocks)}}


-def _user(*blocks: dict) -> dict:
+def _user(*blocks: dict) -> dict:
     return {"type": "user", "message": {"content": list(blocks)}}

Import the fixture types and apply them to every test that takes a fixture:

import pytest

def test_build_env_keyless_vertex_sets_switch_and_maps_project_region(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    ...


def test_execute_materializes_skills_into_workspace(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    ...

As per path instructions for tests/**/*.py: "Ensure test functions have proper type annotations and clean structure."

Also applies to: 742-742

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/agents/test_agents_cli_claude_code.py` around lines 49 - 54,
Annotate the `_assistant` and `_user` helper return types, and add the
appropriate pytest fixture imports. Update every listed test function accepting
`monkeypatch` or `tmp_path` to annotate those parameters as `pytest.MonkeyPatch`
and `Path`, respectively, while preserving their existing `-> None` return
annotations.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@devops_bench/agents/cli/claude_code/agent.py`:
- Around line 35-41: Update the module docstring’s reference to ensure it points
to an existing `docs/appendix/known_issues.md`, or add that document at the
referenced location with the keyless-Vertex `--parallel` guidance.
- Around line 273-279: Update the MCP configuration write flow in the block
assigning mcp_path to explicitly set the resulting mcp-config.json file
permissions to owner-only (0o600) immediately after write_text, ensuring
credentials in serialized server arguments are not group- or world-readable.
- Around line 138-153: Ensure the Claude Code binary used by the agent is
version 2.1.221 or newer before constructing or executing the argv in the
agent’s command flow, particularly when mcp_config_path enables --mcp-config.
Validate the AGENT_TARGET binary version and fail with a clear actionable error
if it is older, rather than allowing MCP-enabled runs to proceed.

In `@tests/unit/agents/test_agents_cli_claude_code.py`:
- Around line 753-757: Update test_build_env_vertex_region_defaults_to_global to
remove CLOUD_ML_REGION from the environment with monkeypatch.delenv before
calling _build_env, alongside the existing GCP_VERTEX_LOCATION cleanup, so the
test reliably exercises the "global" fallback.

In `@tests/unit/evalharness/test_registry_resolution.py`:
- Around line 95-105: Update both alias tests to call harness.resolve_agent(...)
before accessing AGENTS.get(...) for the canonical key. Ensure the resolved
agent is obtained first, then retrieve the canonical class from AGENTS and keep
the existing isinstance assertions.

---

Nitpick comments:
In `@tests/unit/agents/test_agents_cli_claude_code.py`:
- Around line 49-54: Annotate the `_assistant` and `_user` helper return types,
and add the appropriate pytest fixture imports. Update every listed test
function accepting `monkeypatch` or `tmp_path` to annotate those parameters as
`pytest.MonkeyPatch` and `Path`, respectively, while preserving their existing
`-> None` return annotations.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: be2b5e28-2d92-4724-b7b9-f3ec58e8be7e

📥 Commits

Reviewing files that changed from the base of the PR and between 4670d76 and cdca4c5.

📒 Files selected for processing (8)
  • devops_bench/agents/cli/claude_code/__init__.py
  • devops_bench/agents/cli/claude_code/agent.py
  • devops_bench/agents/cli/claude_code/parsing.py
  • devops_bench/evalharness/default.py
  • docs/components/model_providers.md
  • docs/how-to/add-a-model-provider.md
  • tests/unit/agents/test_agents_cli_claude_code.py
  • tests/unit/evalharness/test_registry_resolution.py

Comment thread devops_bench/agents/cli/claude_code/agent.py
Comment thread devops_bench/agents/cli/claude_code/agent.py
Comment thread devops_bench/agents/cli/claude_code/agent.py
Comment thread tests/unit/agents/test_agents_cli_claude_code.py Outdated
Comment thread tests/unit/evalharness/test_registry_resolution.py Outdated
* Drop the module docstring's pointer to ``docs/appendix/known_issues.md``,
  which does not exist in this tree — the surrounding text already explains
  the per-run config-dir isolation.
* Restrict ``mcp-config.json`` to owner-only. A binding's argv can carry a
  server credential and the file lands in the run workspace the harness
  later collects, so the umask default (0o644 on most machines) is too open.
* Clear ``CLOUD_ML_REGION`` as well as ``GCP_VERTEX_LOCATION`` in the
  default-region test; it is the second link in the same fallback chain, so
  an ambient value on a developer machine or CI runner shadowed the default.
* Resolve the agent before reading ``AGENTS`` in both alias tests. A builtin
  self-registers on the lazy import ``resolve_agent`` performs, so reading
  the registry first passed only when an earlier test happened to import the
  module — both tests failed when run alone.
* Annotate the ``monkeypatch`` / ``tmp_path`` fixture parameters, matching
  the convention the rest of the suite already follows.

Not applied: gating the run on Claude Code v2.1.221 for ``--mcp-config``.
The CLI reference attaches that floor to the wait-for-pending-servers
behaviour ("The wait requires Claude Code v2.1.221 or later"), not to the
flag, so a version probe would add a subprocess call per run for nothing.

Signed-off-by: Eugene Ng <ngeugene@google.com>
@eugeneng04

Copy link
Copy Markdown
Contributor Author

Thanks — addressed in 8499755. Four of the five actionable items applied, one skipped with reasoning below.

Applied

  • Dropped the dangling docs/appendix/known_issues.md pointer from the module docstring — that file doesn't exist in this tree; it was lost in the port.
  • chmod 0o600 on the written .claude/mcp-config.json. A binding's argv can carry a server credential and the file lands in the run workspace the harness later collects, so the umask default (0o644 on most machines) was wrong. Asserted via stat.S_IMODE in the existing MCP test.
  • monkeypatch.delenv("CLOUD_ML_REGION", raising=False) in the Vertex region-default test — an ambient value on a dev machine or CI runner would shadow the fallback the test is checking.
  • Resolve-before-read in the alias tests. Confirmed both failed in isolation with NotRegisteredError: 'claude' is not registered in the 'agents' registry; builtins self-register on the lazy import that resolve_agent performs, so reading AGENTS first only passed when an earlier test happened to import the module. Fixed the pre-existing gemini-cli one too rather than leaving a known-broken sibling.
  • Nit: annotated the monkeypatch / tmp_path fixture params (repo convention is 121 annotated vs 34 bare).

Not applied

Gating the run on Claude Code v2.1.221 for --mcp-config. The CLI reference attaches that floor to the wait-for-pending-servers behaviour — "The wait requires Claude Code v2.1.221 or later" — not to the flag itself, so a version probe would add a subprocess call per run for nothing.

ruff check / ruff format clean, 1248 tests passing, and both alias tests pass when run alone.

@janetkuo janetkuo added ok-to-test Indicates a non-member PR verified by an org member that is safe to test. and removed needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Aug 12, 2026
…sion

``--mcp-config`` under ``-p`` only waits for still-pending MCP servers to
connect before the first turn from Claude Code v2.1.221 on. An older binary
accepts the flag and starts the turn regardless, so an MCP-augmented arm can
run with none of its tools attached and still be scored as augmented -- the
same silent contamination ``--strict-mcp-config`` prevents from the other
direction, and worse than a loud failure for a benchmark.

Probe ``claude --version`` and refuse the run below the floor. The probe is
gated on a bound run, so a baseline arm pays nothing. An inconclusive probe
(non-zero exit, unparseable output, spawn failure) proceeds: ``config.target``
may be a wrapper script with its own ``--version`` surface, and blocking on a
probe that merely failed to parse would cost more than the risk it guards.

Reverses the one item skipped in 8499755. The CLI reference does attach the
floor to the wait rather than to the flag ("The wait requires Claude Code
v2.1.221 or later"), but losing the wait is itself a correctness bug here,
not a cosmetic one.

Signed-off-by: Eugene Ng <ngeugene@google.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/unit/agents/test_agents_cli_claude_code.py (2)

830-846: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add type annotations to the fake_run test doubles.

Each nested fake_run function has untyped argv and kwargs parameters. Add parameter and return annotations, or replace them with a typed reusable test helper. As per coding guidelines, "**/*.py: All Python code must include type hints."

Also applies to: 849-860, 863-870, 874-892, 895-905, 908-916, 919-938, 941-949, 952-961, 971-990, 993-1003, 1006-1034, 1043-1063, 1066-1080, 1092-1112, 1114-1130, 1133-1157, 1165-1185, 1188-1200, 1203-1219

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/agents/test_agents_cli_claude_code.py` around lines 830 - 846,
Annotate every nested fake_run test double in the affected Claude Code agent
tests, including those in test_execute_returns_typed_result_with_trajectory and
the listed neighboring tests. Add appropriate types for argv, kwargs, and the
returned subprocess-like result, or replace the repeated doubles with a typed
reusable helper while preserving each test’s existing behavior.

Source: Coding guidelines


1188-1200: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Always isolate CLAUDE_CONFIG_DIR for the child process.

This test asserts that the subprocess inherits /operator/claude when the parent environment sets CLAUDE_CONFIG_DIR. That permits concurrent evaluations to share mutable Claude configuration and makes results depend on operator state.

Generate and pass a unique per-run CLAUDE_CONFIG_DIR in extra_env even when the parent has one. Do not modify the parent environment. Update this test to assert that the child value differs from /operator/claude.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/agents/test_agents_cli_claude_code.py` around lines 1188 - 1200,
Update ClaudeCodeAgent.run and its child-process environment construction to
always generate and pass a unique per-run CLAUDE_CONFIG_DIR through extra_env,
overriding any parent-exported value without mutating os.environ. Revise
test_execute_respects_operator_config_dir to assert the captured child value is
present and differs from /operator/claude.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/unit/agents/test_agents_cli_claude_code.py`:
- Around line 830-846: Annotate every nested fake_run test double in the
affected Claude Code agent tests, including those in
test_execute_returns_typed_result_with_trajectory and the listed neighboring
tests. Add appropriate types for argv, kwargs, and the returned subprocess-like
result, or replace the repeated doubles with a typed reusable helper while
preserving each test’s existing behavior.
- Around line 1188-1200: Update ClaudeCodeAgent.run and its child-process
environment construction to always generate and pass a unique per-run
CLAUDE_CONFIG_DIR through extra_env, overriding any parent-exported value
without mutating os.environ. Revise test_execute_respects_operator_config_dir to
assert the captured child value is present and differs from /operator/claude.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a8ea4fd7-30be-43a9-b50b-b84bbfc68a36

📥 Commits

Reviewing files that changed from the base of the PR and between cdca4c5 and 704a078.

📒 Files selected for processing (3)
  • devops_bench/agents/cli/claude_code/agent.py
  • tests/unit/agents/test_agents_cli_claude_code.py
  • tests/unit/evalharness/test_registry_resolution.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/evalharness/test_registry_resolution.py
  • devops_bench/agents/cli/claude_code/agent.py

An ambient CLAUDE_CONFIG_DIR was honoured unconditionally as the operator's
escape hatch for reusing a cached OAuth login. Under ``--parallel`` that is
wrong: several benchmark processes share one host, and each would inherit the
same mutable Claude config dir -- the collision class ``core.run_env`` exists
to prevent for KUBECONFIG / CLOUDSDK_CONFIG / TF_DATA_DIR. The operator has
already declared concurrency, so isolation outranks the login cache; the
override warns rather than failing.

The hatch is kept for serial runs. Always isolating would leave an
OAuth-authenticated operator unable to run the bench at all without an API
key, since credentials live in the config dir the fresh temp dir replaces.

Also annotate the nested ``fake_run`` test doubles per AGENTS.md typing.

Signed-off-by: Eugene Ng <ngeugene@google.com>
@eugeneng04

Copy link
Copy Markdown
Contributor Author

Both outside-diff comments addressed in 253e0ef.

Type annotations on the fake_run doubles — done, all 19 in this file now carry (argv: list[str], **kwargs: object) -> SimpleNamespace.

Worth flagging for the repo rather than this PR: the convention across tests/ is currently 66 bare nested doubles to 12 annotated, and the sibling test_agents_cli_gemini.py is entirely bare with this exact signature shape. AGENTS.md does say "all Python code must include type hints", so this file is now the compliant one — but the guideline and the codebase disagree, and it's worth settling that in one sweep instead of file by file as PRs touch them.

CLAUDE_CONFIG_DIR isolation — the hazard is real and I've fixed it, but not by always isolating.

Always generating a per-run dir would leave an OAuth-authenticated operator unable to run the bench at all without an API key: credentials live in the config dir that the fresh temp dir replaces. That's the entire reason the escape hatch exists, so removing it trades one breakage for another.

What actually makes the hatch dangerous is concurrency, and this bench's concurrency is explicit and opt-in — several benchmark processes on one host under --parallel / BENCH_PARALLEL, which is the exact collision class core/run_env.py already isolates KUBECONFIG / CLOUDSDK_CONFIG / TF_DATA_DIR against. So the hatch is now refused in that mode:

  • Serial run, ambient CLAUDE_CONFIG_DIR set → honoured, unchanged. No concurrency, no hazard.
  • BENCH_PARALLEL set → ambient value ignored, per-run temp dir injected, warning logged naming the reason.
  • Unset → per-run temp dir, as before.

The parent environment is never modified in either path; the per-run value goes through extra_env only.

Verified in both modes: serial yields None (operator dir flows through os.environ untouched), parallel yields two distinct temp dirs across two runs, neither equal to the operator's. New test test_execute_overrides_operator_config_dir_under_parallel covers it.

ruff check / ruff format clean, 1254 tests passing.

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

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. ok-to-test Indicates a non-member PR verified by an org member that is safe to test. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants