feat(plugins): add the scaled-evals plugin for large-scale agent evaluation - #1524
Draft
arpitsardhana wants to merge 5 commits into
Draft
feat(plugins): add the scaled-evals plugin for large-scale agent evaluation#1524arpitsardhana wants to merge 5 commits into
arpitsardhana wants to merge 5 commits into
Conversation
Contributor
|
arpitsardhana
force-pushed
the
scaled-evals-plugin/arpsingh
branch
from
August 25, 2026 18:23
b1e0a3f to
dd8b8ef
Compare
Contributor
|
I would check to see if you're missing any changes from gitlab (e.g. the direct harbor dataset reference snippets). Otherwise, looking good |
asutermo
approved these changes
Aug 25, 2026
| @@ -0,0 +1,67 @@ | |||
| -- SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |||
Contributor
There was a problem hiding this comment.
not sure if the alembic change in gitlab is useful here or not. potentially could combine all of these migrations (unless you're going to point to the original DB in phase1)
| @@ -0,0 +1,30 @@ | |||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| harbor_agent: |
| @@ -0,0 +1,39 @@ | |||
| <!-- SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. --> | |||
Contributor
There was a problem hiding this comment.
actually I just propose deleting this task in favor of a hello-world one
Adds a plugin that runs large-scale agent evaluations: it accepts a task pack or a task image, builds and admits the image, provisions an ephemeral sandbox on a pluggable runtime backend, runs a Harbor-driven evaluation in it, and records artifacts, provenance, and an SBOM for every terminal run. The service registers through `nemo.services` and is off by default, so a platform install does not pick it up. It owns its own Postgres rather than sharing the platform database, applies its schema and migrations at startup, and mounts its routers behind the platform's authorization decorators. `harbor-patches/` carries runtime overrides for Harbor that are applied inside the sandbox image, not imported by the plugin. Co-authored-by: Andrew Suter-Morris <asutermorris@nvidia.com> Signed-off-by: Arpit Singh (SW-CLOUD) <arpsingh@nvidia.com>
Two ways to stand the plugin up. Compose brings up Postgres, an object store, a BuildKit daemon, a registry, the API, and the dispatch worker for local work. The Kubernetes manifests run the same components against a cluster, taking the sandbox runtime and registry credentials from the environment. Neither path hardcodes a deployment identity: the registry, project, and bucket come from configuration, and the Postgres password is passed as PGPASSWORD so no connection string in a manifest carries a credential. Co-authored-by: Andrew Suter-Morris <asutermorris@nvidia.com> Signed-off-by: Arpit Singh (SW-CLOUD) <arpsingh@nvidia.com>
…ples Covers the two things most likely to break on someone else's machine: that the service registers and resolves its own database rather than the platform's, and that the schema and migrations apply cleanly and idempotently. The migration suite runs against a real Postgres when SCALED_EVALS_TEST_DATABASE_URL is set and skips otherwise. The examples are runnable task packs used by the smoke paths. Co-authored-by: Andrew Suter-Morris <asutermorris@nvidia.com> Signed-off-by: Arpit Singh (SW-CLOUD) <arpsingh@nvidia.com>
…g it The plugin is a workspace member but is deliberately absent from `enabled-plugins`, so a platform install and the default sync leave it out. That also left it invisible to CI, where the type gate reported 57 unresolved imports and pytest could not collect its tests. Add a non-default `scaled-evals` dependency group and name it only where coverage is wanted: the type lint asks for it directly, and the plugin's tests get their own job. Adding it to the shared unit-test run is not viable, because the platform then discovers the service and waits on a Postgres that is not there, which slowed that suite roughly sixfold and broke timing-sensitive daemon tests. The plugin's conftest skips its own directory when the group is absent. `harbor-patches/` is excluded from the type gate, as it imports packages that exist only inside the sandbox image. Co-authored-by: Andrew Suter-Morris <asutermorris@nvidia.com> Signed-off-by: Arpit Singh (SW-CLOUD) <arpsingh@nvidia.com>
arpitsardhana
force-pushed
the
scaled-evals-plugin/arpsingh
branch
from
August 25, 2026 20:44
dd8b8ef to
4a431f4
Compare
The repo-wide unit run collects `plugins/*/tests`, but this plugin is absent from `enabled-plugins`, so a default sync leaves it and psycopg uninstalled there. Six modules then failed to import and two more failed inside a test. Guard each module instead of ignoring the directory from conftest. A `collect_ignore_glob` there behaved as intended in a standalone reproduction, under both serial and xdist collection, but did not take effect in the CI run, and per-module skips do not depend on that mechanism. It also keeps the SPDX header test running everywhere, since it imports nothing from the plugin. Co-authored-by: Andrew Suter-Morris <asutermorris@nvidia.com> Signed-off-by: Arpit Singh (SW-CLOUD) <arpsingh@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
plugins/nemo-scaled-evals, a control plane for running container-isolated agentevaluations at scale. It serves a REST surface under
/apis/scaled-evals/v1that takes anuploaded task pack, builds it into a container image, runs a harness against that image in an
ephemeral sandbox, and returns a score alongside artifacts, a provenance manifest, and an SBOM.
The change is additive: the plugin owns its own Postgres and object storage, declares no
platform service dependencies, and is not listed in
enabled-plugins, so an existingdeployment behaves exactly as before until someone installs it explicitly.
Changes
src/nemo_scaled_evals_plugin/): aNemoServicethat mounts 11vendored routers under
/apis/scaled-evals, applies migrations on startup with boundedretries so a database that is not yet accepting connections self-heals, stamps platform
authorization rules onto every route, and redacts request bodies out of validation errors.
src/scaled_evals/api/): tasks, task revisions, benchmarks, benchmarkruns, evaluations, credentials, configuration profiles, users, teams, agent bundles, and an
admin surface. Includes the repository layer, Fernet-encrypted credential storage with key
rotation, object-store access for S3-compatible and Google Cloud backends, and secret
redaction helpers.
src/scaled_evals/api/build/): converts an uploaded task pack into animage through one of four backends — BuildKit, a managed cloud builder, an external image
builder service, or a pre-built reference — then validates the resulting image identity and
digest against a registry allowlist.
src/scaled_evals/dispatch/): a Postgres-queued worker usingFOR UPDATE SKIP LOCKEDthat claims evaluations, materializes caller-supplied credentialsinto the sandbox environment, launches and monitors the run, collects artifacts, and tears
the sandbox down. Ships one built-in runtime backend for Kubernetes sandboxes; Gym-based
runtimes register through an opt-in plugin hook.
harbor-patches/): a small overlay applied to the harness inside itsown virtual environment at image build time, so the runtime behavior this control plane
depends on does not require changes upstream.
src/scaled_evals/intake/): converts harness trajectories into ATIFand posts them to the platform's Intake service, creating one experiment per run.
db/): the baseline schema plus ordered, re-appliable migrations. Appliedautomatically on startup, or by an external job when that is turned off.
src/scaled_evals/cli/): a thin HTTP client over the same REST API for creatingtasks, credentials, profiles, and evaluations, and for following logs, events, artifacts,
and provenance.
deploy/): a Docker Compose stack for local use, and a Kustomize examplefor Kubernetes, each with an end-to-end smoke script.
examples/,tests/): one example task pack and 18 plugin tests.pyproject.toml,refreshes
uv.lock, and lists the plugin inplugins/README.md.Workflows supported
verifies a container image for it.
worker runs the harness in an isolated sandbox and records the score.
and injected only into that run's environment.
many tasks and aggregate the results.
CycloneDX SBOM, retrievable individually or as one archive.
trajectories for later analysis.
publishing settings so an evaluation request stays small.
Type of Change
Quality Gates
enabled-pluginsand has no user-facing entry point in the published docs yet; setup, configuration, and
both deployment paths are documented in
plugins/nemo-scaled-evals/README.mdand theper-directory READMEs that ship with the change.
Known limitations
These are deliberate for a first landing and are called out so review can focus on them.
but no identity provider is wired, so every caller resolves to one principal and the
deployment is effectively single-tenant. Mutating endpoints do not scope by owner, and the
admin router is not gated while authentication is disabled. This must not be exposed to
untrusted or multi-tenant traffic in its current form.
the harness overlay layout, validation redaction. The upstream project's much larger suite
is not vendored yet, so the control plane's own logic is largely covered by the end-to-end
smokes rather than by unit tests.
Kubernetes runtime uses is not published on PyPI, so building the image requires pointing
HARBOR_EXTRA_INDEX_URLat an index that carries it. Without that value the build failsrather than silently producing a broken image.
Build, Artifact Registry, Cloud Storage, and Workload Identity. The Compose path has no
such dependency, but a portable Kubernetes deployment is still outstanding.
SCALED_EVALS_DATABASE_URLand never the platform's database configuration, and it talksto object storage directly instead of through the Files service. Postgres is required — the
claim queues rely on
FOR UPDATE SKIP LOCKED, JSONB, advisory locks, and enum types, sothe platform's SQLite default is not a usable target. When the database is unreachable the
plugin degrades to a
503on/v1/readyzinstead of failing platform boot.before the API has finished applying migrations, so on a brand-new database it restarts a
couple of times before settling. Compose gates this with a health-conditioned dependency;
the Kubernetes manifests rely on restart backoff instead.
transcripts, and the configuration flag intended to gate that is not wired up, so prompts
and responses reach Intake unredacted.
upstream inference endpoint for certain credential providers, which overrides caller
configuration. This needs to become opt-in before the path is generally useful.
the matching CLI command cannot reach an endpoint, and its cloud-builder client is the one
code-scanning finding below that is unreachable rather than fixed. Deleting that surface is
a follow-up rather than part of this landing. One Gym runtime variant also pins a source
revision that is not resolvable from the public repository.
misspelled key is dropped silently rather than rejected.
Verification
Signed-off-by:traileruv run pre-commit run -apasses, or any blocked checks are identified belowTargeted validation:
Repository gate
Run after rebasing this branch onto the current
main. Every content hook passes —ruff,ruff format,tytypechecks, config-reference, copyright headers, uv.lock drift,the plugin import boundary, toolchain-version checks, and Flox locks. Two hooks fail for
local-toolchain reasons rather than on content, and both need confirmation from CI:
Run uv lock with platform uv— the hook requires uv0.9.14to regenerate the lock andthe local toolchain is uv
0.9.15. The separateCheck for uv.lock drifthook passes, souv.lockis consistent withpyproject.toml; this is a version guard, not drift.Helm Docs—helm-docsis not installed locally. This change touches no chart.Two hooks did modify files, and both results are folded into the commits below:
ruff formatrewrote 72 files, because the vendored sources were wrapped for a narrowerline length than this repository allows. Reflow only; the plugin tests pass on both sides
of it.
Fix copyright headersstamped 8 files once this branch picked upmain's version of thehook, which covers file types the plugin had not stamped.
tools/lint/lint-all.shwas also run in full. Everything that executes passes; thefailures left are the same local-toolchain gaps — four lints shell out through Flox, which
is not installed here, plus the two pre-commit hooks above.
Tests, lint, and types
This plugin is intentionally absent from
enabled-plugins, so the default sync CI's lint andtest entry points use leaves it and
psycopguninstalled; that is why the type gate initiallyreported 57 unresolved imports and pytest could not collect its tests. A non-default
scaled-evalsdependency group now carries it, named only where coverage is wanted: the typelint asks for it directly, and the plugin's tests get a dedicated job. Adding it to the shared
unit-test run is not viable — the platform then discovers the service and blocks on a Postgres
that is not there, which slowed that suite roughly sixfold and broke timing-sensitive daemon
tests — so the plugin's
conftest.pyskips its own directory when the group is absent.The type gate needed real fixes, not suppression. Each repository class defines a
listmethod, which shadows the builtin for annotations evaluated in the same class body, so
list[...]on 32 signatures referred to that method rather than to the type;from __future__ import annotationsis why it never raised at runtime. Those now saybuiltins.list[...], and a runtime check confirms every one resolves. The harness overlayimports the sandbox library it replaces, which by design exists only inside the image it is
injected into, so it joins the existing exclusions for runtime-only imports. Two remaining
diagnostics were a settings singleton whose required fields come from the environment, and a
CLI summary that now tolerates a server answering with a non-list field instead of raising.
The plugin's own dependencies come in through
--all-packages; a plainuv sync --frozenleaves
psycopgout and the tests then fail to import. The kustomize render is includedbecause the copyright hook added comment lines to two files that feed config maps — the
envs:generator drops comments, and the one mounted as a file is parsed by a reader thatalready skips
#lines.The platform's unit-marked suite was also run (
pytest -m unit) and is unaffected; this changeadds a new plugin directory and touches no existing package beyond the workspace list, the lock
file, and the plugin index. One unrelated service's tests fail to collect on this checkout for a
pre-existing reason and were excluded.
Static analysis
Code scanning raised two findings on this branch, and secret scanning raised ten. All are
addressed:
logged was the field name the docker config is stored under in the Secret, never a
credential; the variable was simply called
secret_key. It is now named for what it holds,and the log line reports the host and Secret rather than the field.
segment was already percent-encoded with
safe='', and its only caller is themodel-routing router that is deliberately not mounted, so the path is unreachable in a
running deployment. Cloud Build ids are UUIDs, so the client now rejects anything else
before it reaches a URL.
modules. None pointed at a reachable host, but the shape was real. The deployments now
take the password from
PGPASSWORD, which libpq reads directly, so no manifest carries acredential in a connection string; the test fixtures assemble their throwaway DSNs from
parts. Re-running the scanner over this branch reports none of them.
Compose stack, end to end
Built the image from source in this tree and ran the bundled smoke, which creates a task,
uploads a pack, finalizes it, builds through BuildKit, and pushes to the stack's registry:
The plugin's health endpoint reported
mode: scaled-evals-cpfrom the freshly built image,confirming the running code matched this branch rather than a cached image.
Kubernetes, end to end from a clean slate
Deleted the target namespace so Postgres came back on a new volume, then redeployed. Startup
migrations built the schema from empty, and all four workloads rolled out on the same image:
The evaluation smoke runs the harness against the example task with an oracle agent that
applies the task's own reference solution, so a healthy run scores exactly
1.0and needs nomodel credentials. Both runs did. The script itself reported a failure on one invocation
because two concurrent runs contended for the same local port-forward and it treats a dropped
tunnel as fatal; the runs themselves completed server-side.
Not verified
main. The delta since then ismain's own commits, copyright headers, thetype-annotation and naming fixes, and the
PGPASSWORDchange to the manifests. That lastone is the only change to deployment behavior, it applies to Kubernetes and not to Compose,
and it has not been re-run against a cluster.
kubectl kustomizerenders it, and libpqreads
PGPASSWORDwhen the connection string omits a password, but treat the Kubernetespath as needing one more smoke before it is trusted.