Skip to content

feat(agents): optionally run CLI agents in a container with a scoped cluster identity - #72

Open
geojaz wants to merge 5 commits into
kubernetes-sigs:mainfrom
geojaz:ehole/agent-sandbox
Open

feat(agents): optionally run CLI agents in a container with a scoped cluster identity#72
geojaz wants to merge 5 commits into
kubernetes-sigs:mainfrom
geojaz:ehole/agent-sandbox

Conversation

@geojaz

@geojaz geojaz commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Agent transcripts showed CLI agents ranging well outside the task: reading files from
the operator's home directory, copying archives out of ~/Downloads, running rm -rf,
and in one case running find ~ | grep -i <task-name> to hunt for the benchmark's own
fixtures. On a workstation checkout that is the harness author's whole filesystem, and
any answer material sitting on disk is reachable.

This adds an opt-in container sandbox for CLI agents. When BENCH_AGENT_SANDBOX is set
to docker, 1, or true, the agent argv is wrapped in docker run with:

  • only a per-run scratch workspace and a generated kubeconfig mounted, so the host
    filesystem and the operator's environment are not visible
  • HOME pointed at the workspace, so agent config and history stay inside the run
  • the container joined to the kind network with the kubeconfig server rewritten to the
    control-plane node, so TLS still verifies against the existing cert
  • credentials passed explicitly rather than by scraping os.environ for well-known names

BENCH_AGENT_IMAGE names the image. hack/agent-sandbox.Dockerfile builds a suitable
one (node + kubectl + the gemini CLI). hack/agent-credential.sh mints a credential for
the agent: by default an API key restricted to generativelanguage.googleapis.com, or a
short-lived impersonated token with --vertex-sa, plus --revoke to clean up.

Off by default so it can be A/B'd against current behaviour while tasks are still being
debugged.

Worth being explicit about scope: this is filesystem and environment isolation only. It
does not scope what the agent can do to the cluster, which is a separate concern best
handled with per-task RBAC, and it does not help if answer material is seeded inside the
cluster itself.

…r identity

The CLI agent harnesses invoke the agent binary as a plain subprocess inheriting
the parent environment, so a shell tool under `--approval-mode yolo` has the
operator's entire filesystem. In observed runs an agent read another task's seed
and verification scripts off disk, searched the home directory for its own
task's fixtures by namespace name, and copied an unrelated archive out of
~/Downloads before running rm -rf. The agent's own workspace sandbox does not
help: it guards the native file tools, not the shell.

That is two problems. Task definitions, seed scripts and scoring rubrics living
on the same filesystem the agent can read makes results unverifiable; the rest
is an operator-safety hazard.

Adds an opt-in containerised execution path (BENCH_AGENT_SANDBOX=docker) with
two independent boundaries:

  * The CONTAINER removes the host filesystem and the operator's environment.
    The mount set is deliberately short, and what is absent matters most: no
    repository, no $HOME (HOME is repointed at the workspace so a bare ~ cannot
    resolve to the operator's profile), no Docker socket, and no Application
    Default Credentials. Model credentials cross by value as explicit -e flags
    from the harness's already-resolved config, never by mounting a config dir
    or inheriting whatever the operator exported.

  * A SCOPED TOKEN decides what the agent may do to the cluster. If a task seeds
    a `bench-agent` ServiceAccount, a short-lived token is minted for it, making
    the agent's identity part of the topology the task declares. Absent one, it
    falls back to the operator's credential and logs a warning, since that is a
    much larger grant than most tasks intend.

Neither boundary closes a third channel: an agent that can read a cluster can
read anything a task put IN that cluster, and a task needing the agent to
inspect a workload cannot use RBAC to hide that workload's definition. Answer
material must not be seeded into the cluster; the module documents this rather
than implying the sandbox covers it.

Networking: kind creates a Docker network of the same name, so the container
joins it and reaches the API server at https://<cluster>-control-plane:6443. The
kubeconfig kind writes points at 127.0.0.1, which is meaningless in a container,
so it is rewritten per run. kind's API server certificate covers the
control-plane node name, so TLS still verifies without --insecure-skip-tls-verify.

If a sandbox kubeconfig cannot be built the run is refused rather than silently
falling back to the host: a containment control that quietly degrades is worse
than none.

Includes hack/agent-sandbox.Dockerfile and hack/agent-credential.sh, which mints
a credential restricted to one API rather than reusing ADC.

Only the gemini harness is wired so far. openclaw needs the same treatment plus
a KUBECONFIG-ordering fix (it threads the path into its MCP config, which must
be the in-container path). antigravity ships a Mach-O binary and cannot run in a
Linux container without a Linux build.
@kubernetes-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: geojaz
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 1, 2026 03:54
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 59 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: 23116e4e-37d2-485f-8fb2-502e03755233

📥 Commits

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

📒 Files selected for processing (8)
  • devops_bench/agents/cli/gemini_cli/agent.py
  • devops_bench/agents/sandbox.py
  • devops_bench/evalharness/default.py
  • hack/agent-credential.sh
  • hack/agent-sandbox.Dockerfile
  • tests/unit/agents/test_agents_cli_gemini.py
  • tests/unit/agents/test_sandbox.py
  • tests/unit/evalharness/test_default_harness.py

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.

@kubernetes-prow kubernetes-prow Bot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Aug 1, 2026
@kubernetes-prow

Copy link
Copy Markdown

Hi @geojaz. 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.

Regular contributors should join the org to skip this step.

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/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 1, 2026
geojaz added 3 commits August 7, 2026 09:12
…their record

The gemini CLI agent discarded core.subprocess.run's captured stdout whenever
the run timed out, so a turn that worked the whole way through (kubectl calls,
generated parsers, a web search) left an empty trajectory once the process
hit the deadline. subprocess already buffers whatever the child wrote before
the kill; the agent now runs it through parse_stream_json instead of
throwing it away, and records the timeout / recovered-trajectory outcome on
AgentResult.metadata so the harness can tell a forensic partial run from a
clean one.

Signed-off-by: Eric Hole <ehole@onixnet.com>
_build_success_record hardcoded status: "success" regardless of whether the
agent process actually completed, so an AgentResult.errored() run (timeout,
non-zero exit, missing binary, SDK fault) still scored a normal-looking
OutcomeScore made entirely of the task's do_nothing baseline plus a perfect
safety axis. The record now reads agent_timeout / agent_error whenever the
agent reported an error, mirroring how a "failed" record already opts out of
scoring: _score excludes all three statuses from the metrics batch, so a
degraded run can no longer produce a composite score that reads as if the
agent had a fair turn.

Signed-off-by: Eric Hole <ehole@onixnet.com>
A sandboxed gemini run's `docker run --rm` container survived a timeout: the
harness only SIGKILLs the local docker client process, which never signals
the daemon to stop the container itself, so a wedged agent kept burning
model API calls for as long as it stayed up after the harness moved on.
Each run now gets a container named deterministically from its own workspace
dir, wrapped in a container_guard that kills it by name in a finally on
every exit from the run (normal, exception, or timeout), and the harness
sweeps stray containers from a prior crashed run at the start of each batch.

Signed-off-by: Eric Hole <ehole@onixnet.com>
@geojaz

geojaz commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Pushed three follow-ups that belong with this change rather than in a separate PR, since they are all sandbox and agent-turn lifecycle:

  • Capture the trajectory incrementally, so a run that times out still keeps the record of what the agent did. Previously a timeout discarded the whole trajectory, which threw away exactly the evidence needed to understand the timeout.
  • Treat a failed agent turn as a failed run. A turn that errored was still being counted as a successful run, which quietly inflated results.
  • Reap the sandbox container on every exit path. The container was leaking on the error and timeout paths, so a run that failed left a container behind holding its scoped cluster identity.

@kubernetes-prow kubernetes-prow Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 7, 2026
@geojaz

geojaz commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Noting a related follow-up that is deliberately not in this PR, plus one thing worth knowing about this branch.

A fix that expands ${CLUSTER_NAME} in verification and chaos spec values depends on the container reaping commit here, and separately on the hold mode work in #84. This branch does not carry hold mode, so the commit does not apply to it cleanly today. It is parked until both this PR and #84 land, then it comes in as a small follow-up.

More generally, this branch has drifted from the line the rest of the verification work is on. It has no hold mode, and its test_verification_wiring.py is roughly ninety lines shorter than the version the other PRs are built against. Whichever of this PR and #84 merges second should expect a real reconciliation rather than a clean merge. Calling it out now so it is not a surprise at merge time.

@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 11, 2026
@geojaz

geojaz commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

Note for reviewers: there is a staged follow-up commit for this PR that bind-mounts the harness scratch root into the sandbox container, read-write, at the same path inside and outside. That lets a task seed a host-side artifact (for example a bare git repo) that both the sandboxed agent and the host-side verifiers reach at one path. It depends on scratch_root() from #60, so I am holding it until #60 merges and this branch picks that up. Keep it in mind when reviewing the mount set here: the workspace-plus-kubeconfig-only posture in this diff will grow by one deliberate mount, and the owned-root guards in #60 are what make a read-write mount safe.

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