Skip to content

Add ViBench resources server + agent (PRD-to-web-app benchmark) - #2674

Merged
bxyu-nvidia merged 33 commits into
NVIDIA-NeMo:mainfrom
deepbuilder:vibench-env
Aug 31, 2026
Merged

Add ViBench resources server + agent (PRD-to-web-app benchmark)#2674
bxyu-nvidia merged 33 commits into
NVIDIA-NeMo:mainfrom
deepbuilder:vibench-env

Conversation

@deepbuilder

@deepbuilder deepbuilder commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Wraps ViBench as a NeMo Gym environment: a model builds a web app from a PRD, and grading stands the app up for real, seeds it through its own UI, and drives it in a browser against a human-written test plan. The score reflects what a user can reach, not what the diff contains.

Adds resources_servers/vibench and responses_api_agents/vibench_agent.

Design

The agent owns the build sandbox and copies the finished app out as a tarball; the resources server unpacks it and grades it in a fresh compose project, one per test plan, via ViBench's run-seed.pyrun-evaluate-post-seeding.py.

The sandbox is never shared. Reaching into the agent's box needs serialize()/connect(), which only the OpenSandbox provider implements, so the shape swebench uses cannot run on Docker, Apptainer or enroot. See #2082 for the design discussion; this PR has no dependency on it or on #2085.

vibench_agent subclasses OpenCodeSandboxedAgent and overrides only sandbox acquisition and harvesting.

Reward

One row is one (app, artifact) pair: built once, graded across that artifact's test plans. Reward is the mean normalized score, continuous rather than binary, with per-plan values in reward_components. build_failed, seeding_failure_rate and plans_graded_rate are reported separately, because a mean reward cannot distinguish a weak app from one that never built or could not be seeded.

REVERIFY_MODE is UNSUPPORTED — grading depends on live app and database state.

Validation

Five example tasks graded end to end on a Docker host. Scores spread from 0.00 to 1.00 with partial credit in between, and every zero carried a real scorecard with per-step diagnoses (HTTP 500s, SQL errors) rather than a harness failure.

The reward tracks capability: a weaker policy model scored 0.0 on a task with 0/19 steps, its app unreachable, where a stronger model scored 1.0 on the same task with the same graders.

Scores also vary across repeats of the same model, since it does not build the same app twice. Quantifying that — and separating it from any variance in the LLM-driven verifier — is what reward profiling is for, so verified: false stands.

data/example_rollouts.jsonl holds a five-task run.

Reviewer notes

  • network: host is deliberately not used. The in-sandbox harness reaches the policy model at get_server_url(...)127.0.0.1:<port>, which inside a bridged container is the container itself; the harness then makes zero LLM calls and exports an empty app with nothing logged. configs/docker.yaml instead adds host.docker.internal via host-gateway and the agent rewrites loopback model URLs to it. Only policy_model binds 0.0.0.0; the servers holding grader credentials stay on loopback. Residual: the sandbox can reach host-published TCP ports, which inference requires.
  • Grading timeouts interrupt rather than terminate. ViBench tears its compose project down in a finally, which CPython does not unwind on SIGTERM. Escalation is SIGINT → SIGTERM → SIGKILL. Verified with a forced timeout on a real host: SIGTERM leaked two containers and a network, SIGINT cleaned up fully.
  • artifact_dir is a filesystem path shared by agent and resources server. Not a new constraint — grading already shells into a local Docker daemon.
  • Requires docker-compose on PATH. ViBench's scripts use the legacy name; Docker 29.x ships only the plugin. The failure misleads, so it is called out in the README.
  • Task fields are top-level, not under verifier_metadata, following swebench since seed_session and verify share one typed request model. Happy to move them.
  • Grading runs on the resources server's Docker daemon, not in a Gym sandbox, because ViBench's grading stack is multi-container. Single-image packaging is the prerequisite for multi-host.
  • mvp artifacts only. ViBench ships reference implementations for 21 of 24 apps, so feature-ri is a follow-up; it needs a starting codebase staged into the sandbox the way the PRD already is.
  • Row size: rollout rows carry the harness's own stdout/stderr, so example_rollouts.jsonl is ~500 KB for five tasks. Happy to trim.

Security

The tarball comes from a box the model controlled, so it is untrusted: unpacking refuses members and links resolving outside the app dir and passes filter="data", and artifact paths outside artifact_dir are refused and never deleted. Grader credentials are scrubbed from captured output before it is stored, since that output ships in the rollout JSONL. Test plans and test_assets/ are never staged into the build box.

Testing

gym env test --resources-server vibench passes. 75 unit tests (60 resources server, 15 agent), coverage 99%. pytest tests/unit_tests/ 2621 pass. pre-commit run --all-files clean.

Checklist

  • I have read the contributing guidelines.
  • The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs.
  • Tests added or updated and pass locally.
  • Pre-commit checks pass locally (pre-commit run --all-files).
  • All commits have DCO sign-off (git commit -s).

@copy-pr-bot

copy-pr-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@nemo-automation-bot nemo-automation-bot Bot added the community-request Issue reported or requested by someone from the community label Aug 22, 2026
@deepbuilder deepbuilder changed the title Add ViBench resources server (PRD-to-web-app benchmark) Add ViBench resources server + agent (PRD-to-web-app benchmark) Aug 23, 2026
@deepbuilder
deepbuilder marked this pull request as ready for review August 26, 2026 02:36
@cmunley1

Copy link
Copy Markdown
Contributor

/claude review

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

SHIP WITH CARE — one RISK finding (metrics contract), otherwise a well-built environment with strong test coverage.

The reward path itself is sound: verify() computes reward in-process, isolates each test-plan failure into a zeroed PlanResult instead of 500-ing the rollout, counts every plan toward the denominator (seeding failures drag the mean down rather than vanishing), and guards div-by-zero on full_points. Async is correct — subprocess grading via create_subprocess_exec, asyncio.Semaphore bounding concurrent compose stacks, no httpx/ray.get(). Security posture is careful: tar-member and symlink escape checks, artifact-path validation before any unlink, credential redaction on captured output, and grader keys kept off the builder. New-file SPDX headers present; verified: false correctly retained.

RISK (inline, app.py:616): the compute_metrics/get_key_metrics overrides use the wrong AggregateMetricsMixin signatures. In standard eval they're never reached (the OpenCode-derived agent doesn't proxy /aggregate_metrics to the resources server), so the documented failure-breakdown metrics silently never appear; on the reverify path they're posted to the resources server and raise AttributeError/TypeError. No effect on reward — this is observability. See inline for the fix.

Notes (author's call): _grader_env caches secrets in _cached_grader_env; fine given process lifetime. The Docker-only docker-compose requirement and stochastic LLM verifier are documented as P0 limitations with verified: false, which is the right call.

Comment thread resources_servers/vibench/app.py Outdated
"plans_graded_rate": (graded / plans) if plans else 0.0,
}

def get_key_metrics(self) -> List[str]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

RISK — custom metrics don't work against the framework contract; silently absent in eval, crash on reverify.

AggregateMetricsMixin (nemo_gym/reward_profile.py:590) defines compute_metrics(self, tasks: List[List[Dict]]) — verify responses grouped by task — and get_key_metrics(self, agent_metrics: Dict) (one positional arg). Both overrides here have the wrong signature: compute_metrics(self, verify_responses) iterates the argument as flat dicts, and get_key_metrics(self) takes no arg.

Two consequences:

  1. Silently absent in standard eval. _call_aggregate_metrics (nemo_gym/rollout_collection.py:976) POSTs /aggregate_metrics to the agent server. vibench_agentOpenCodeSandboxedAgent does not proxy /aggregate_metrics to the resources server (contrast simple_agent app.py:354, which does). So the base agent's default compute_metrics ({}) runs and these methods are never invoked — the whole point of this override (build_failure_rate, mean_seeding_failure_rate, plans_graded_rate, and the get_key_metrics headline set) never reaches aggregate metrics.

  2. Crashes on reverify. nemo_gym/rollout_reverification.py:577 POSTs /aggregate_metrics to the resources server. There compute_metrics receives List[List[Dict]]; r.get("reward") on a list → AttributeError, and get_key_metrics(self) called as get_key_metrics_fn(serialized_agent)TypeError. Aggregation 500s.

FIX: (a) match the mixin signatures — flatten the grouped tasks in compute_metrics, and accept agent_metrics in get_key_metrics returning a dict subset; and (b) add an aggregate_metrics proxy on vibench_agent that forwards to the resources server (as simple_agent does), or these metrics never surface regardless of signature. Reward itself is unaffected (computed in verify), so this is observability, not score corruption.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e1e4bad — both parts confirmed against the framework before changing anything.

Signatures. compute_metrics now takes tasks: List[List[Dict]] and flattens the per-task groups; get_key_metrics(agent_metrics) returns a dict subset and preserves the mean/* default so nothing standard disappears.

Proxy. Added aggregate_metrics on vibench_agent forwarding to the resources server, as simple_agent does. You were right that without it the signatures were moot — build_failure_rate, mean_seeding_failure_rate and plans_graded_rate never ran at all.

Reward is computed in verify() and was unaffected, as you noted.

Tests now call these the way compute_aggregate_metrics does, including a conformance check that asserts the signatures match the mixin via inspect. The old tests passed throughout precisely because they called the methods the way my code expected — worth guarding against directly.

@deepbuilder

Copy link
Copy Markdown
Contributor Author

Thanks — the RISK finding was right on both counts, and I verified each against the framework before touching anything. Fixed in e1e4bad.

Signatures now match AggregateMetricsMixin: compute_metrics(tasks: List[List[Dict]]) flattens the per-task groups, and get_key_metrics(agent_metrics) returns a dict subset while preserving the mean/* default.

Proxy added on vibench_agent forwarding /aggregate_metrics to the resources server, as simple_agent does. This was the more important half — without it the metrics never ran regardless of signature.

Reward is computed in verify() and was unaffected.

Tests now exercise these the way compute_aggregate_metrics calls them, plus a conformance check asserting the signatures match the mixin via inspect. The previous tests passed the whole time because they called the methods the way my code expected rather than the way the framework does; the conformance check guards that class of mistake directly.

On the two notes: _cached_grader_env holding secrets for the process lifetime is deliberate — it replaced a synchronous subprocess.run on the async path that also re-derived the env six times per rollout. Happy to scope it down if you would prefer.

Gates: gym env test --resources-server vibench passes, 79 unit tests (63 resources server, 16 agent), pre-commit run --all-files clean.

return rewrite_loopback_url_for_docker(url)
return _origin(url)

def _create_opencode_config(self) -> Dict[str, Any]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude suggestion, please take a look:

[P1] This method no longer matches the parent on main

This branch forked at 354babf. Since then #2120 changed the parent to take a request:

main   opencode_sandboxed_agent/app.py:503   async def _create_opencode_config(self, request: Request)
here                                               def _create_opencode_config(self)

Rebased on origin/main, the agent tests go to 1 failed / 15 passed with
TypeError: _create_opencode_config() missing 1 required positional argument: 'request'.
The resources server tests stay green at 61 passed / 2 skipped.

This one is worth catching before merge because of what the method does. It is what rewrites the model
URL for the sandbox. Without the rewrite the harness talks to itself, makes no LLM calls, and exports an empty app with no error. Nothing reports that as a failure, including
build_failed, because the build technically succeeded.

There is a second problem in the same method. Main now builds the URL through base_url_for_run
(nemo_gym/base_responses_api_agent.py:147), which adds /ng-rollout/<id> and the token capture suffix.
Line 150 throws that away and rebuilds from get_server_url(), which quietly turns off the token capture
path #2120 added. It is off by default, so nothing breaks today.

Both fix with one change. Take the parent's URL and rewrite only the host.
rewrite_loopback_url_for_docker (line 90) already does that and keeps the path. _origin only strips a
trailing slash or /v1, so add /v1 back:

async def _create_opencode_config(self, request: Request) -> Dict[str, Any]:
    config = await super()._create_opencode_config(request)
    options = ((config.get("provider") or {}).get("nemo_gym") or {}).get("options")
    if isinstance(options, dict) and isinstance(options.get("baseURL"), str):
        options["baseURL"] = rewrite_loopback_url_for_docker(options["baseURL"]) + "/v1"
    return config

One thing to check. This drops the sandbox_model_base_url branch, so add it back if anyone uses
that option.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if the opencode_sandboxed_agent keeps changing under you, it may be more stable to implement all here, cc @bxyu-nvidia as author of the base agent class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in c49183d. Rebased onto current main first — reproduced exactly what you described: 1 failed / 15 passed with TypeError: _create_opencode_config() missing 1 required positional argument.

Took your suggested shape: the override is now async def _create_opencode_config(self, request) and rewrites only the host of the parent's URL, so the base_url_for_run path survives. Good catch on the token-capture suffix — I was rebuilding from get_server_url() and silently dropping /ng-rollout/<id>.

Kept sandbox_model_base_url: it replaces the origin but now preserves the path suffix, so an explicit address cannot drop the capture path either.

Added a conformance test asserting this signature matches the parent's via inspect. Both failure modes here are invisible at runtime — the harness just talks to itself and exports an empty app that build_failed does not catch — so a mismatch should fail loudly in CI rather than in a rollout.

Verified on the rebased tree: 63 resources server + 17 agent tests, gym env test --resources-server vibench 61 passed, pre-commit run --all-files clean.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair concern — this is the second time a silent drift in the parent has bitten this branch (the other was compute_metrics against AggregateMetricsMixin).

My read is that inlining trades one failure mode for a worse one. Subclassing inherits the whole OpenCode install-and-drive path — version pinning, remote install script, config generation, export parsing — and ViBench overrides only two things: where the sandbox comes from, and rewriting the model URL host. Copying the rest would be several hundred lines that then drift from the base silently and permanently, rather than failing at a signature boundary.

What made both incidents dangerous was that they were invisible at runtime, not that they happened. So I have added conformance tests asserting the overrides match the parent signatures via inspect — a future change like #2120 now fails in CI instead of producing an empty app with no error.

Happy to go either way if @bxyu-nvidia would rather the base class not be subclassed externally — in that case the cleaner split might be a small extension point on OpenCodeSandboxedAgent for the sandbox URL, which would serve any provider whose boxes cannot reach host loopback, not just ViBench.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Aug 26, 2026
@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-customer Waiting on the original author to respond label Aug 27, 2026
@cmunley1
cmunley1 requested a review from bxyu-nvidia August 27, 2026 04:23
@github-actions github-actions Bot added the sla:review-overdue Review response is over the one-business-day SLA label Aug 28, 2026
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Aug 29, 2026
@bxyu-nvidia

Copy link
Copy Markdown
Contributor

/ok to test ace821a

raghavendran ramakrishnan added 8 commits August 31, 2026 15:17
ViBench (https://github.com/ViBench/vibench-public) evaluates whether a model
can build a working web application from a PRD. Grading stands the finished app
up for real, seeds it through its own UI, and drives it in a browser against a
human-written test plan -- so the score reflects what a user can actually reach,
not what the diff contains.

This wraps ViBench as a single resources server rather than porting its
orchestration scripts, which Gym replaces. The topology mirrors
resources_servers/swebench:

  * seed_session starts the build sandbox from ViBench's base image and stages
    the PRD at /app/prd.txt. The agent never sees the test plans.
  * Any agent that consumes sandbox_handle can build; the config pairs with
    opencode_sandboxed_agent.
  * verify pulls /app out of the agent's sandbox and grades it by shelling into
    a ViBench checkout's run-seed-then-evaluate.py, once per test plan.

One row is one (app, artifact) pair, so the app is built once and graded N
times. Reward is the mean normalized score across that artifact's test plans,
with per-plan values in reward_components. A plan that fails to seed scores zero
rather than dropping out of the mean. Reward is continuous, not binary: ViBench
test plans are scored step lists and partial credit is the signal the benchmark
is built around.

prepare.py generates task rows from a local ViBench checkout; example.jsonl
covers 5 apps over 16 test plans. P0 is mvp artifacts only.

REVERIFY_MODE is UNSUPPORTED because grading depends on live app and database
state and cannot be recomputed from stored rollouts.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
That is the tag ViBench's own build_base_image_if_needed produces and reuses, so
the build sandbox and the grading stack share one base image on the host instead
of requiring a second, separately-tagged copy.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
gym env start builds an isolated venv per resources server from its
requirements.txt; it does not inherit the root venv. The file previously held
only a comment claiming no extra dependencies were needed, so the server died at
import with ModuleNotFoundError: No module named 'fastapi'. The sandbox extra is
required because app.py imports nemo_gym.sandbox, matching swebench.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
sandbox_provider is a reference to a provider instance, and every shipped
provider config binds that instance as 'sandbox' -- 'docker' is the child key
selecting the provider class, not the reference. Starting with
sandbox_provider: docker failed with 'Sandbox provider reference docker is not
defined in the merged config'.

The provider config also has to be merged in explicitly, so the start command
needs a second --config; swapping that one path is how you move to OpenSandbox,
Fargate or Enroot without editing this config. Matches swebench and anyswe.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The first cut had the resources server create the build sandbox and hand the agent
a sandbox_handle, copying swebench. That cannot work off OpenSandbox: only
OpenSandboxProvider implements serialize()/connect(), so on Docker the rollout
died with 'provider docker does not support serialize()/connect()'.

That is by design, not a gap. NVIDIA-NeMo#2082 excludes node-local providers
(Docker, Apptainer, enroot) from ConnectableProvider because the box has no
network identity, and routes them through the sandbox server instead -- which is
still unmerged in NVIDIA-NeMo#2085.

The same issue classifies ViBench correctly: it is 'case 2', where the rollout
copies an artifact out and the verifier grades it in a fresh box, so the sandbox
never needs sharing. The first cut had accidentally made it case 3, the one shape
that does need the sandbox server.

So sandbox ownership moves to a new responses_api_agents/vibench_agent, which
subclasses OpenCodeSandboxedAgent and overrides only sandbox acquisition and
harvesting; installing and driving OpenCode is inherited. It stages the PRD via
SandboxSpec.files, tars the built app into a shared artifact_dir, and passes the
path to /verify. The resources server now creates no sandbox: seed_session
returns PRD text, and verify unpacks the tarball.

The tarball comes from a box the model controlled, so unpacking rejects members
and links that escape the app dir, and artifact paths outside artifact_dir are
refused. Works on Docker today with no dependency on NVIDIA-NeMo#2085.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
BaseRunRequest has no extra=allow, so model_dump() dropped the ViBench task
fields and /seed_session rejected the request with 'app: Field required'.
OpenCodeSandboxedAgentRunRequest exists precisely to let benchmark params
propagate.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The first full rollout scored 0 with build_failed=true and no app. The OpenCode
export explained why: summary {files: 0}, tokens {input: 0, output: 0} -- the
model was never called, and nothing logged an error.

opencode_sandboxed_agent points the in-sandbox harness at
get_server_url(model_server), which is http://127.0.0.1:<port>. Inside a bridged
container that is the container itself, so the policy model is unreachable and
the harness silently does nothing. Sharing the host network namespace makes the
address mean the same thing on both sides -- the lever NVIDIA-NeMo#2082 uses
in its own example config.

Ships as a separate provider config used instead of the stock docker.yaml, since
network: host drops isolation for a container running model-written code and that
should be a deliberate choice, not a default. Also raises exec.default_timeout_s
from 180s, which would kill the build exec regardless.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The second rollout built an app (build_failed=false) but every test plan came
back seeding_failed with grading_time_s under a second. The cause was
run-seed-then-evaluate.py: its build context omits the seeding/ directory that
Dockerfile.completed-app requires, so it dies immediately at
'COPY seeding /seeding: not found'. That entry point looks convenient but is
broken -- every sibling creates the directory, and run-seed.py:95 makes an empty
one when none is supplied.

Switch to run-seed.py then run-evaluate-post-seeding.py, which is what ViBench's
own results tree drives. That path also takes --test-assets, which the evaluation
agent uploads while driving the app and which the previous single-call path had
no way to pass -- so rows now carry test_assets_dir. Those fixtures stay
grader-only and are never staged into the build sandbox.

A grading timeout is now a zero for that plan rather than an exception, so one
wedged compose stack cannot take down the rollout.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
raghavendran ramakrishnan added 17 commits August 31, 2026 15:17
Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Two defects found reviewing this branch, the first verified by test:

Arbitrary file deletion. verify() unlinked body.artifact_path in a finally
block, so a path rejected by _resolve_artifact for reading was still deleted --
any file the server could write. It now resolves first and only ever unlinks the
validated path.

Blocking call on the async path. _grader_env used synchronous subprocess.run
inside async _run_vibench_script, stalling the event loop for up to 120s, and ran
once per grading call (six times per rollout). It is now awaited and cached per
server.

Also: env_creator failure raises instead of degrading to an empty environment,
which previously surfaced as 'LLM API KEYS is not set' inside the container and
sent debugging to credentials rather than to the call; and a None return code
counts as failure rather than success.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
All five example tasks graded: mean reward 0.696, spread 0.00 to 1.00 with
partial credit at 0.51 and 0.97. Replaces the single-task example rollouts.

Scanned for secrets before committing -- known key formats, hub endpoints,
AGENT_*_API_KEY assignments, Postgres credentials, and a manual read of the
captured grader output in the failing task's error fields, which is the path a
leak would most likely take.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Redaction: captured grader stdout is stored on PlanResult.error and ships in the
committed rollout JSONL, while the environment those scripts run with holds
AGENT_*_API_KEY. Scrubbing at the capture point means a stray set -x upstream
cannot put a live key in a public file.

Aggregate metrics: a mean reward cannot distinguish a weak app from one that
never built or could not be seeded, and those need different responses. Each
cause is now reported separately, with plans_graded_rate as the check that the
verifier ran at all.

Tar extraction also passes filter="data", the stdlib guard and the 3.14 default;
the explicit checks stay because they name what was refused.

Tests: prepare.py went from 0 to 98 percent, app.py to 96, and the agent from no
tests at all to covering harvesting and sandbox staging -- including that a tar
failure yields no artifact, that artifact names cannot collide across concurrent
rollouts, and that test_assets reach evaluation but never the builder.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
All five example tasks graded, 16/16 plans, no build or seeding failures and no
tracebacks. Mean reward 0.834 with a spread from 0.20 to 1.00, so the scale is
neither saturated nor binary.

The previous validation ran against code that predated the review fixes. This run
used a build whose checksum matches the committed file, so the evidence describes
what is actually in the branch.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The README and prepare.py claimed feature artifacts were blocked because ViBench
ships no reference implementation. That is wrong: 542 RI files are tracked, real
app source for 21 of 24 apps, including the setup-environment.sh and
start-server.sh the grader requires. The actual blocker is this environment --
seed_session hands the agent a PRD and has no way to stage a starting codebase
into the build sandbox. feature-ri is therefore a follow-up, not a dead end;
feature-mvp is harder because it starts from a prior rollout's own output.

Also drops the 'NVIDIA-NeMo#2082 case 2' framing from code and configs, keeping one pointer
in the README. The label means nothing without reading an open issue, and its
taxonomy could still change; what a reader needs is the fact that reaching into
the agent's sandbox requires serialize()/connect(), which only OpenSandbox
implements.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Timeout now SIGTERMs the process group before SIGKILL. ViBench tears its compose
project down in a finally block, which SIGKILL skips -- so the wedged-stack case
this timeout exists for was leaking postgres, the app, Playwright, the image tag
and a port. Verified by test that SIGTERM comes first and SIGKILL only follows
for a process that ignores it.

The build contract is setup-environment.sh and start-server.sh, the scripts
ViBench's prompt requires and its grading stack invokes -- not package.json,
which scored any non-Node app as never built.

evaluation_timeout_s is now a deadline shared across seed and evaluate, so a plan
cannot consume two full windows.

A failed asset upload no longer leaks a started sandbox, and a single plan's
exception -- an unreadable plan file, a truncated scorecard -- is a zeroed plan
rather than a 500 that loses the whole rollout.

Only the model server binds 0.0.0.0, which the bridge gateway needs on Linux
Docker Engine; the resources server, agent and head server stay on loopback since
grader credentials live in those processes.

Also: docstring and config now name the scripts actually run, REVERIFY_MODE is a
ClassVar, and prepare.py uses a real temp file instead of writing into the
ViBench checkout.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
A five-task run scored market_place as a build failure with reward 0: its app
shipped a .venv, whose bin/python symlinks to the system interpreter outside the
app dir, so the verifier refused the entire tarball as an escaping link.

The guard is right; harvesting the venv was not. Dependency trees are excluded
for the same reason node_modules already was, and setup-environment.sh rebuilds
them. The same task scored 0.97 on an earlier run purely because that build
happened not to create one, so this would have kept resurfacing at random.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
The previous commit claimed to fix the docstring naming run-seed-then-evaluate.py
but never touched vibench.yaml, which still pointed the next reader at the script
this code deliberately does not run. It also re-broke the README feature
paragraph, dropping the clause that says the reference implementation already
exists and only needs staging.

_terminate_group's final wait was unbounded, so a process ignoring SIGKILL --
uninterruptible I/O -- would hang the rollout after we had already given up on
it. Now bounded by cleanup_grace_s; the hanging-process test drops from ~10s to
0.1s, which is the same defect showing up as test latency.

Adds the missing assertions on paths that were correct but unproven: a failed
asset upload stops the started sandbox, a truncated scorecard and an unreadable
plan file each zero only their own plan, and one plan raising inside gather
leaves its siblings' scores intact.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
A forced-timeout run against a real grading script leaked two containers and a
network, and they were still there 60s later. The SIGTERM escalation added
earlier did not work: CPython does not unwind finally on default SIGTERM, so
run-seed.py died before reaching cleanup_compose_project, and killing
docker-compose up does not remove anything -- only down does.

SIGINT raises KeyboardInterrupt, which does unwind finally. Escalation is now
SIGINT -> SIGTERM -> SIGKILL, with the full grace window on SIGINT since cleanup
shells out to docker-compose. The same probe now reports zero leftover containers
and networks immediately and at 60s.

The unit test asserting [SIGTERM, SIGKILL] passed throughout, which is why this
needed a real run. Its 'polite process' stub also left returncode as None
forever, so the early-exit path was never exercised; a real process sets
returncode once wait() returns.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
_run_vibench_script still described SIGTERM before SIGKILL, which is the
behaviour that leaked containers. It now defers to _terminate_group rather than
restating the mechanism, so the two cannot drift apart again.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
…gent

Review found the metrics overrides used the wrong signatures.
AggregateMetricsMixin passes compute_metrics rollouts grouped by task
(List[List[Dict]]) and calls get_key_metrics(agent_metrics) expecting a dict
back; these took a flat list and no argument, returning a list. On the reverify
path that raises AttributeError and TypeError.

It also never surfaced at all in standard eval: rollout collection POSTs
/aggregate_metrics to the agent, and neither vibench_agent nor its parent
forwarded it to the resources server, so build_failure_rate,
mean_seeding_failure_rate and plans_graded_rate silently never appeared. Adds the
proxy, as simple_agent does.

Reward is computed in verify() and was never affected.

Tests now call these the way the framework does, including a conformance check
that asserts the signatures match the mixin -- the previous tests passed because
they called the methods the way the code expected.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
… path

Rebased onto main, where NVIDIA-NeMo#2120 changed the parent to
async _create_opencode_config(self, request). This override still had the old
sync no-arg signature, so on main it raised TypeError -- and the method is what
rewrites the model URL for the sandbox. Without it the harness talks to itself,
makes no LLM calls, and exports an empty app that no failure field reports,
because the build technically succeeded.

Same method also rebuilt the URL from get_server_url, discarding the rollout
prefix and token-capture path that base_url_for_run adds. It now takes the
parent's URL and rewrites only the host, so that path survives.
sandbox_model_base_url still replaces the origin, but keeps the path too.

Tests cover the capture path surviving, the override behaviour, and a
conformance check that this signature matches the parent -- the mismatch that
caused this is invisible at runtime.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Harvest excludes were ./-anchored, which GNU tar applies only at the top level, so
nested node_modules and .venv were still tarred -- and a nested venv symlink
makes the verifier reject the whole artifact. Verified on GNU tar 1.34 that
unanchored patterns exclude nested trees. This means the earlier venv fix never
actually worked. Also excludes export.json, the harness transcript the inherited
responses() writes beside the app.

Security: env_creator stderr was embedded verbatim in a RuntimeError that reaches
PlanResult.error and the committed rollout JSONL, bypassing _redact. Now redacted
before the message is built. The 0.0.0.0 model bind is documented as publishing
that server and the run's token-capture path on every interface; the narrower
gateway bind is noted but not defaulted, since ViBench configures custom bridge
pools so the gateway is not a fixed value.

A grader misconfiguration was swallowed by gather(return_exceptions=True),
turning it into a dataset of silent zeros. It now raises GraderConfigError, which
verify re-raises rather than zeroing.

The timeout path cancelled communicate() without draining stdout, so an
interrupted script could block on a full pipe instead of finishing its
docker-compose down -- undoing the cleanup fix. A drain task now runs alongside
termination. env_creator's own timeout leaked its subprocess.

Also: grading scripts and env_creator now use ViBench's interpreter, as prepare.py
already did; VibenchVerifyResponse allows extras so the task fields in the splat
survive; add_evaluation_tags raises instead of silently no-opping; the artifact is
deleted after grading rather than before; extractall and rmtree moved off the
event loop; run() restores the parent's observability wiring so
ng_agent_observations is captured; prepare.py picks the goal from the artifact
instead of hardcoding zero-to-one; and the unused --workdir flag is gone.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
All five example tasks graded, 16/16 plans, no build or seeding failures, no
tracebacks and no leaked compose projects. Mean 0.785, spread 0.36 to 1.00.

market_place is the one to look at: it scored 0.00 with build_failed=True in the
previous run, rejected on a nested venv symlink, and grades at 0.97 now that the
harvest excludes are unanchored. That is the tar fix landing on the case that
exposed it.

Rollouts are also ~18 percent smaller because the harness transcript is no longer
tarred into the app being graded.

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
CI failed on every shard plus the core Test job, both from causes in this server.

test_task_data requires every resources server to ship a task_data.py describing
its dataset rows; vibench had none, so "no loadable schema" failed both
TestSchemaPresence and the committed-row sweep. The schema mirrors
VibenchTaskRequest, the shared model behind seed_session and verify, and records
the asset split that matters: asset_dirs is staged into the build sandbox,
test_assets_dir is grader-only and must never reach the builder.

Data validation then failed for a separate reason: vibench_repo_root was a bare
oc.env interpolation with no default, so merely resolving the config raised a
ConfigInterpolationError anywhere the variable was unset. Data tooling and CI
load this file without ever starting the server, so it now defaults to an
obviously-invalid path that resolves cleanly and fails loudly at runtime if left
unset.

gym env test +should_validate_data=true now exits 0 with "successfully validated".

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-maintainers Waiting on maintainers to respond label Aug 31, 2026
@bxyu-nvidia

Copy link
Copy Markdown
Contributor

/ok to test 8719abe

@bxyu-nvidia
bxyu-nvidia merged commit a2c5222 into NVIDIA-NeMo:main Aug 31, 2026
31 checks passed
cmunley1 pushed a commit that referenced this pull request Sep 1, 2026
`vibench_agent` shipped without a `README.md` in #2674. `ng_test_all`
filters testable components to directories containing one, then asserts
the filtered count matches the discovered count — so discovery found 174
components and tested 173, failing that assertion in all eight
server-suite shards even though every selected module test passed:

```
AssertionError: Mismatch on the number of total modules found (174) and the number of actual modules tested (173)!
Extra candidate paths:
- responses_api_agents/vibench_agent
```

`nemo_gym/cli/env.py:1114`, on `main` at a2c5222.

This adds the missing README. Docs-only — no code or config changes.

## Contents

- The build → harvest → verify flow, and that everything about driving
OpenCode is inherited from `opencode_sandboxed_agent` (only sandbox
acquisition and harvesting are overridden).
- Why the agent owns its sandbox instead of sharing one: reaching into
the agent's box needs `serialize()`/`connect()`, which only OpenSandbox
implements, so the `swebench` shape cannot run on Docker, Apptainer or
enroot. See #2082.
- Why `configs/docker.yaml` must be used instead of the stock provider
config — the stock 180s exec timeout kills long installs, and the
harness is told the model is at `http://127.0.0.1:<port>`, which inside
a bridged container is the container itself, so it makes zero LLM calls
and exports an empty app.
- An explicit warning not to "fix" that with `network: host`, and the
`0.0.0.0` exposure caveat for the policy-model bind.

Setup, task-row generation, reward definition and grading are not
duplicated here; they stay in the [resources server
README](https://github.com/NVIDIA-NeMo/Gym/blob/main/resources_servers/vibench/README.md).

## Note on the other CI failure

The `Test Wheel Use` failure in that run is independent of this change:
the wheel test enables prereleases/`unsafe-best-match`, unbounded
`mlflow>=3.15.1` resolved to 3.15.2, which selected `sqlalchemy
2.1.0rc1`, whose source metadata uv rejects (`duplicate normalized extra
name 'mssql-pymssql'`), aborting `arc_agi_resources_server` startup.
That needs a SQLAlchemy pin below 2.1 or prerelease containment in
transitive resolution — happy to open a separate issue or PR for it if
useful.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Co-authored-by: raghavendran ramakrishnan <raramakrishn@nvidia.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-request Issue reported or requested by someone from the community sla:review-overdue Review response is over the one-business-day SLA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants