Skip to content

build(docker): add the OpenClaw sandbox image - #19

Open
geojaz wants to merge 11 commits into
feat/sandbox-all-harnessesfrom
feat/openclaw-sandbox-image
Open

build(docker): add the OpenClaw sandbox image#19
geojaz wants to merge 11 commits into
feat/sandbox-all-harnessesfrom
feat/openclaw-sandbox-image

Conversation

@geojaz

@geojaz geojaz commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

BENCH_SANDBOX_IMAGE needs an image per harness, and only the Gemini CLI one existed. A sandboxed OpenClaw run had nothing to point at, and until now the image had to be built by hand off-tree.

This follows the existing one-image-per-harness structure (a sibling of Dockerfile.gemini-cli rather than a shared multi-stage base with per-agent targets), for the reason that file already gives: the CLIs pin their own runtimes, so a shared stage buys little while coupling every agent's rebuild, and a version bump for one agent would invalidate the others' baselines.

Two details in the new Dockerfile are non-obvious and are commented in place:

  • npm registers the CLI's bin as openclaw, not oc. Without an explicit symlink, oc resolves to nothing inside the image and every sandboxed run fails looking for the binary.
  • The anthropic-vertex provider is only discovered under the CLI's own stock extensions root. Installing the package alone leaves it unregistered and absent from oc plugins list. It's copied into that root and the npm copy removed, so there's one on-disk copy rather than two that can drift.

Both steps are asserted at build time, so a silent regression fails the build instead of a run.

Also bumps kubectl and helm in Dockerfile.gemini-cli to match the versions the new image pins, so the two sandbox images agree on the tools they hand the agent. GEMINI_CLI_VERSION is deliberately untouched: it's the agent under test, not part of the toolbox, and bumping it would invalidate existing baselines.

Test plan

  • Both images build clean
  • OpenClaw image reports the pinned CLI version, lists anthropic-vertex as enabled, runs as an unprivileged user with a writable HOME, and contains no gcloud, docker, or gemini binaries
  • Gemini image was rebuilt and verified to report kubectl v1.36.4, helm v3.21.4, gemini 0.54.4

Noticed while working here, not fixed in this PR

The bookworm-backports apt line in Dockerfile.gemini-cli appears to be a no-op. The built image ships git 1:2.39.5-0+deb12u3, which is plain bookworm rather than backports, so the comment's stated reason (reftable support, which landed in git 2.45) doesn't hold today. Left alone since it's pre-existing and orthogonal to this change.

pradeepvrd and others added 11 commits September 8, 2026 00:16
Only gemini_cli was migrated, so a sandboxed matrix could not actually run the
other three arms: AgentHarness.run refuses a sandboxed harness that has not
been migrated, rather than silently running it on the host. The published
antigravity and claude_code runs WERE sandboxed, but by an uncommitted patch and
an untracked tree on the bastion -- neither in any PR, neither reviewed, and
neither present in this lineage.

Each harness now routes its agent turn through run_agent_cmd and declares
supports_sandbox. Fixture mounts need no per-harness work: the eval harness
already builds them centrally into the spec.

openclaw needed more than a flag. Its state dir and config path cross the
boundary as env VALUES, and the host spelling means nothing inside the
container, so the agent turn now gets container-translated paths while the
post-run 'oc sessions' / export-trajectory calls keep the host spelling. Both
read the same bytes: the state dir lives under the workspace, which is the bind
mount itself. The container also has its own oc on PATH, so the host binary path
is not passed through. container_path() is lifted out of
SandboxExecutor.map_host_path so the cwd mapping and these value translations
cannot drift apart.

secret-rotation opts out via a new Task.requires_unsandboxed. The task IS
credential work -- it rotates a Secret Manager version through ADC, and ADC is
exactly what the boundary strips, so sandboxing it would not harden it, it would
make it impossible. The harness skips the sandbox for that task only and logs a
warning, so a sandboxed matrix shows which task did not get a boundary. The flag
clears config.sandbox rather than just skipping spec completion: the agent's own
gate reads that field, and a leftover skeletal spec would refuse the run.
… all

Provider deduction keys off the stack DIRECTORY NAME and only exactly
'kind'/'vcluster' deduce. The directory is 'cp-recovery-kind', which does not,
so the run died at provisioning with 'stack requires an explicit provider'
before the agent ever started. Verified: _select_provider raises for this task
and for no other -- every b-00xx task already declares provider: kind, and this
was the only ported kind task that did not.

Infrastructure selection, not grading: nothing about the prompt, the
verification spec or the expected output changes, so the task remains
comparable with the rows already scored against it.
…cs-server on kind

The stack configured provider "kubernetes" from module.cluster.endpoint,
whose value falls back to the vcluster submodule. That submodule's own
resources are served by the same provider, so tofu refused the graph with
"Cycle: module.cluster.module.vcluster..." before planning anything -- for
every infra_provider, which left the task unable to provision at all.

Add a vcluster-free managed_endpoint output and read that instead: the
ordering edge on the cluster survives, the cycle does not.

Also install metrics-server when infra_provider=kind. The HPA objective grades
ScalingActive=True, which needs a live metrics pipeline that a stock kind
cluster lacks; without it the objective fails for provider reasons rather than
for anything the agent did.
The task was pinned to gcp for one reason: the HPA objective grades
ScalingActive=True and a stock kind cluster ships no metrics pipeline, so the
objective failed for provider reasons rather than for anything the agent did.
The stack now installs metrics-server under infra_provider=kind, so that
reason is gone and the objective is decided by the agent again.

Verified on kind end to end: the cluster provisions, the seeded workload rolls
out, and kubectl top pods returns live CPU for the target deployment.

Only the infrastructure provider changes. The prompt, expected_output and
verification_spec are untouched, so the task grades exactly what it did
before. INFRA_PROVIDER=gcp still selects GKE.
It was gcp before: the k-sigs migration (kubernetes-sigs#143) declared provider: gcp
explicitly, and that is the definition every published run of this task was
graded against. Switching the provider changes what the task measures, so it
should not move as a side effect of making kind viable.

The metrics-server install and the vcluster cycle fix stay. They were the real
bugs: the cycle blocked provisioning on EVERY provider, and without
metrics-server the HPA objective failed on kind for provider reasons rather
than agent ones. kind is now a genuinely working alternative for local
iteration.

What still argues for gcp is the load path, not metrics: on GKE the Service is
a LoadBalancer the chaos generator reaches directly, while on kind it is a
ClusterIP behind the harness port-forward. The planned spike failed to inject
in 8 of 8 published runs, so the fewer moving parts in that path the better
until a spike demonstrably lands.

Either way every runner must use the same provider for this task -- the Service
type and load path differ, so a kind arm and a GKE arm are not comparable.
Same defect optimize-scale had: the kubernetes and helm providers were
configured from module.cluster.endpoint, whose value falls back to the vcluster
submodule, and that submodule's resources are served by those same providers.
Tofu rejected the graph with "Cycle: ...module.vcluster..." before planning
anything, so the task could not provision on any provider -- it failed on both
arms in the 2026-09-08 rerun campaign.

Read managed_endpoint instead, forwarded through this stack's local cluster
wrapper. tofu plan now succeeds: 18 to add, no cycles.
b-0059 and b-0061 grade NetworkPolicy enforcement, and kindnet does not
enforce policy, so both stacks pass disable_default_cni = true to the cluster
module. The module never declared that variable: tofu failed at init with
"An argument named disable_default_cni is not expected here", so neither task
has ever provisioned. Their setup scripts already document the intent -- Calico
is expected to be installed by the cluster module before they run.

Implement it: swap kindnet for Calico v3.27.3 behind the variable. Two details
that are not obvious.

Nodes stay NotReady until a CNI is running, so kind_cluster.wait_for_ready has
to be off in that case or the apply blocks until timeout on exactly the
clusters that need Calico. null_resource.calico does the waiting instead, and
duplicate_context now waits on it too.

pod_subnet is set to Calico's own default pool (192.168.0.0/16) rather than
kind's 10.244.0.0/16; left mismatched, Calico allocates outside the subnet the
node spec and kube-proxy expect.

The variable defaults to false and the networking block is emitted only when it
is true, so every existing kind task plans exactly as before (verified on
b-0024: no Calico resource, wait_for_ready still true, same 3 resources).

Verified end to end on a real kind cluster: node Ready, calico-node 1/1,
podCIDR 192.168.0.0/24, and a default-deny ingress policy actually enforced --
curl returns 200 before the policy and fails after it. Under kindnet the same
probe would keep returning 200.
_build_agent_config_snapshot rebuilds AgentConfig field by field, and
extra_flags was not among the fields it copied. AGENT_EXTRA_FLAGS parsed
correctly into AgentConfig.from_env and was then dropped on the floor before
the agent ever saw it, with nothing logged.

The visible symptom is agy keeping its 5m default --print-timeout on every
benchmark run: a task whose agent turn runs longer dies with "timeout waiting
for response", an errored run with a truncated trajectory. b-0060 hit it twice
at 313s and 308s while every run that finished did so under 250s. The
documented mitigation for that flake is to pass a larger --print-timeout, which
was impossible through the harness.

Verified: the snapshot now carries (\x27--print-timeout=24h\x27) from the env, and
the 249 agent/evalharness unit tests still pass.
The judge and the chaos driver resolve models differently: the judge reaches
Vertex, where gemini-3.1-pro exists, while the chaos driver goes through the
API key, whose endpoint only publishes gemini-3.1-pro-preview and 404s the
plain id. The existing note covers the judge, so a working judge reads as
evidence the whole config is fine.

It cost a run to find. optimize-scale came back chaos_invalidated with the
spike never injected, which looks like a fault-injection problem rather than
the driver being unable to call a model at all.
BENCH_SANDBOX_IMAGE needs an image per harness and only the Gemini CLI
one existed, so a sandboxed OpenClaw run had nothing to point at and
needed an image built by hand off-tree.

Built as a sibling of Dockerfile.gemini-cli rather than a shared
multi-stage base with per-agent targets, for the reason that file
already gives: the CLIs pin their own runtimes, so a shared stage buys
little while coupling every agent's rebuild, and a version bump for one
agent would invalidate the others' baselines.

Two details are load bearing and are commented in the file. npm
registers the CLI's bin as `openclaw`, not `oc`, so without an explicit
symlink `oc` resolves to nothing inside the image and every sandboxed
run fails looking for the binary. And the anthropic-vertex provider is
only discovered under the CLI's own stock extensions root, so installing
the package alone leaves it unregistered and absent from `oc plugins
list`; it is copied into that root and the npm copy removed, so there is
one on-disk copy rather than two that can drift. Both steps are asserted
at build time so a silent regression fails the build instead of a run.

Also bumps kubectl and helm in Dockerfile.gemini-cli to match the
versions the new image pins, so the two sandbox images agree on the
tools they hand the agent. The Gemini CLI version itself is deliberately
untouched: it is the agent under test, not part of the toolbox, and
bumping it would invalidate existing baselines.
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