v0.5.0
Released October 14, 2026.
-
Sessions: no verdict only for failures that are ours for certain. Once the agent has acted, a failure is its judged 0 (
policy_sandbox_failure) unless it is ours for certain: a network, provider or auth failure (HTTP 401 and 403 now count as infrastructure inis_infrastructure_failure), a verifier that couldn’t run (its upload or launch failed) or timed out before it read the agent’s output, an interrupt, or E2B or Modal retiring the sandbox at the end of its one-hour lifetime (newSandboxExpiredError, before aSandboxDiedError) unless the agent’s own actions took at least half the session’s time. A failure the rule can’t read is the agent’s 0. The rule isdeepgym.session.failure_result, which verl’sdeepgym_agentloop and Harbor’s agent phase also use; before, a 401 uploading verifier code or a verl final verifier that timed out before reading anything scored 0, and a Harbor agent that replaced its work directory with a link, grew it past the archive cap, made packing outrun its deadline, killed its sandbox or spent most of the provider lifetime in its script got no verdict; in Harbor any failure once the agent’s script has started is now its 0 unless it is ours for certain. Session verifiers now get the consulted mark single runs use, embedded in the verifier’s own script rather than on a command line: a step or final verifier that times out after reading the agent’s output (a FIFO it left, say) is a judged 0 withsolution_timeout(for a step, its reward, and the episode goes on), and so is a verifier that is killed, or gives no verdict, after reading it (solution_error), unless the agent’s code failed to launch. Each verifier also prints a start line first, so a timeout whose output the provider lost is a judged 0 rather than passing for one that never read anything. Listing the agent’s folder, or starting a program in it or on one of its files, now counts as reading its output, and the mark is written to the stdout file itself, so capturingsys.stdoutcan’t hide it, and cut from what the verifier printed exactly, so a line it fell inside reads whole. Daytona enforces command deadlines in the sandbox, so a timed-out command keeps what it printed. A FIFO the agent leaves where the harness writes is refused at once instead of hanging the write, the local backend shows output that isn’t UTF-8 instead of ending the episode, and E2B, Daytona and Docker/Apple uploads to a stopped sandbox raiseSandboxDiedError.deepgym_solution.run_solutionprints the consulted mark before it releases the solution, so a solution that dies the moment it starts can’t pass for one that never ran. -
QA gate: multi-turn and computer-use envs with a
reference_actions.jsonare checked: the reference replays at full marks every time, and agents that quit at once (empty_fails), use every action (busy_fails) or act at random (random_fails) must earn nothing. The computer-use built-ins ship their reference episodes (one per variant) and run on--mode daytona. Negative controls (empty, stub, idle agents) fail on any reward, not just full marks; attacks define the env’s real entry point; reports list the checks thatran.difficultyis reported, never gated: it gives mean and spread of rewards and flagstoo_easy,too_hardandno_variance(partial rewards count as variance), retries 429s, timeouts and 5xx with backoff, and is inconclusive rather than aborting when the endpoint stays down. Missing verdicts never pass: incomplete black-box scans areinconclusiveand never saved as ROBUST, an audit that judged nothing has no risk rating (statussays why), and RL exploit discovery reports suspected exploits with their signs from every round instead of a genuineness verdict. The exploit DB keys results by benchmark, verifier and attack mode (legacy tables are rebuilt in place), reuses one only for the same verifier contents and attack setup, and summaries filter by benchmark and mode. Reports and approvals are written atomically, and attack payloads carry JSON test values as valid Python. Built-ins:data_pipelineandweb_path_traversal_toyno longer pay for an empty answer, andanagram_check,palindrome_checkandvalid_parenthesesscore balanced recall on real booleans, so a constant answer earns nothing. Breaking:ExploitDB.getneedsbenchmark=,DiscoveryReportreplacesis_genuine_solution/exploit_discoveredwithsuspected_exploits/exploit_suspected, andVerifierAuditReport.risk_score/risk_levelare None when inconclusive. -
Environment authoring:
deepgym new <name> --kind function|script|repo-patch|multi-turnscaffolds a working env folder (task, a verifier that runs the solution only throughdeepgym_solutionand reports every case with a reason, reference, seededvariants.py, metadata).deepgym check <folder>validates the files, metadata schema and protocol output, runs the reference, empty and stub solutions, and prints warnings from an advisory lint over the judge code (never evidence of isolation, which comes fromload_solution/run_solution).deepgym qatakes folders (multi-turn ones replayreference_actions.json);--mutantsfails a verifier that passes any AST mutant of the reference and reports the mutation score, running each mutant’s whole judge confined in a local bwrap sandbox (DeepGym(confine_local_judge=True): writes only to the run’s temp directory, no network, a PID namespace that takes down everything the run started, CPU capped) so a mutant stays contained even if the verifier runs it in-process; macOS’s sandbox-exec can’t reap detached processes, so there mutants need docker, modal or e2b (sandbox providers run mutants only where the whole sandbox is offline: docker, modal, e2b);--approve --by NAME(which needs a rerun and every requested check passed) runs QA on a private copy of a self-contained folder and writes a read-onlyapproval.json(report, file hashes, version, time, lint warnings marked advisory) thatcheckflags once the files change. Reports list skipped checks undernot_run, and approvals name them.deepgym_solutiongainsapply_patch(refusing file creation, deletion, renames and copies) andload_files(a normal import of the patched tree) for repo-patch verifiers, and raisesLaunchErrorwhen the machine can’t start the solution, so verifiers end the run as an error instead of scoring 0.empty_failsandstub_failsare nowinconclusive, not passed, when the verifier errors instead of scoring,hack_resistantis skipped, not passed, when the verifier rejects every attack as a malformed answer,deterministicis skipped after a single reference run,reference_passesanddeterministicare inconclusive when a reference run gets no verdict, and so isdifficultywhen a sample gets none. Breaking:run_qa(target, dg, QAOptions(...))replaces thereruns/client/sampleskeywords. -
Traces:
dg.trace(env, AgentMeta(...))records a graded episode while it runs, in a versioned, frozen schema (deepgym.traces.Trace, schema 1.0). It holds provenance (environment name, variant and seed; SHA-256 of the environment definition, task, verifiers and reference; the image digest when the image pins one; deepgym version; provider; the agent, model and sampling config you pass; timestamps), every step (the action, the model messages behind it with optional token ids and logprobs, the observation, the step reward with the step verifier’s output, exit code, error) and the finalRunResultwithhas_verdict. Each event is appended and flushed to~/.deepgym/traces/<id>.jsonl(or$DEEPGYM_TRACES_DIR) as it happens, so an exception or a killed process keeps the steps so far and the trace is markedincomplete. A traced episode refuses actions afterscore(), so its verdict always matches the state it ends in.deepgym traces export --format jsonl|parquet|atif [--rows trace|rl] [--only-valid]reads those files and the console database and writes whole traces, SFT/RL rows (messages,reward,step_rewards, for TRL and verl) or ATIF (Agent Trajectory Interchange Format) v1.7 documents. Traces without a verdict are flagged (has_verdict: false, null reward) or left out with--only-valid. Exports are strict JSON (a NaN in verifier output becomes null), and ATIF export refuses a non-empty directory so stale documents never mix in. Parquet needs the newdeepgym[traces]extra (pyarrow). DeepGym now needs pydantic 2.7 or later.record_episodealso accepts a trace and keeps it in a newtracecolumn; existing databases gain the column on open, and plain trajectories record as before.Session.step_verdictexposes the last step verifier output and exit code; a step verifier that reports its own failure (exit 2, or anerror_typethat isn’t the model’s fault) leaves the trace’s step reward null instead of 0. A NaN or infinite per-caseexecution_time_msis now zeroed, since JSON can’t carry it. -
Axolotl: fake PRM data removed.
generate_prm_dataset,results_to_prm_record,write_prm_dataset,PRMRecordanddeepgym generate-prmare gone. They relabelled a solution’s per-test diagnostics (“Input: … Expected: … Got: …”) as reasoning steps with pass/fail labels, which is not process supervision. Real step-level rewards, from step verifiers in multi-turn episodes, are exported asstep_rewardsbydeepgym traces export --rows rl.generate_axolotl_config(method='prm')stays, for stepwise data you bring. -
Sessions without a final verifier have no verdict.
Session.score()on an environment with nofinal_verifier_codenow returnserror_type='no_verifier', sohas_verdictis false and trainers mask the episode. Before, it returned a plain 0.0 that read as a graded failure. -
Harbor tasks:
deepgym.harborreads Harbor task folders from a folder, a git URL or a Hugging Face dataset and runs each as aHarborEnvironmenton any provider. The model’s bash script runs as an unprivileged user with no network; a fresh sandbox from the same image grades a copy of its work directory, withtests/uploaded only then and the reward read from that sandbox, so a pre-written reward or a planted test runner scores nothing. Docker and Apple container buildenvironment/Dockerfiletasks.deepgym harbor importvalidates (and with--register, registers) tasks,deepgym qagates those with asolution/solve.sh, anddeepgym harbor exportwrites environments as Harbor tasks with a separate offline verifier and a non-root agent.TerminalBenchEnvironmentnow loads and grades its tasks this way; before, it trusted a reward file the commands could write. -
Console UI state: starting runs navigates once, to the run or its eval; before, closing the dialog then replaced that with the runs page. Run, episode and environment views now clear when their selection changes instead of showing the previous one’s data, and drop responses that arrive out of order, so a finished run no longer flips back to running while it polls. Episode replay shows a loading or error state for each frame’s screenshot and draws the action marker only once that frame’s image has loaded. “Run again” shows the API’s error when the run is refused.
-
Infrastructure and verifier failures no longer read as model failures.
eval()now setserror_typeon runs that raise (it dropped it, so they read as judged zeros), runs named environments without a verifier file, such as repo-patch tasks, instead of skipping them as unanswered, keeps suite order in both clients, and names each result in the newEvalResult.env_names. A provider failure while the verifier runs is aSandboxError, not aVerifierError. Docker, Apple, E2B, Modal and Daytona runs take the verdict from the verifier’s stdout only, so a JSON line on stderr can no longer replace it, and keep stderr inRunResult.stderr. A step verifier that exits 2 or reports anerror_typethat isn’t the model’s fault now raisesVerifierErrorand ends the episode, with no step reward (step_rewardsis one shorter thansteps), andscore()then returnserror_type='verifier_error'.GymInfocarrieserror_typeandhas_verdict. The verifier wrapper treats a return value that isn’t a float, bool or dict, or apassedortruncatedthat isn’t a real bool ("false"used to count as a pass; wrap numpy bools inbool()), as a verifier error (exit 2), and keeps a returnedtruncated=True. The console keeps no score for a run without a verdict, leaves it out of the score bins and counts it as an error; the CLI prints such runs asERROR (<error_type>)with their cause (totals: see the next entry).AsyncDeepGymin HTTP mode sends a file-backed verifier’s source inline, so built-in environments run remotely, andmax_parallelbelow 1 raisesValueErrorin itsrun_batchandevalinstead of hanging. -
Judged totals next to the headline.
BatchResultandEvalResultkeep their headline numbers conservative:passed,failed,avg_scoreandpass_ratecount a run without a verdict as a failed 0, so a run that breaks its verifier or sandbox can only lower them. New fields sit beside them:no_verdict_count,judged_count, and the judged-onlyjudged_pass_rateandjudged_mean_score. A run without a verdict never counts as passed, even if it reported a pass. The console’s mean score likewise counts a finished run without a verdict as 0 again (since the last change it left such runs out); its totals gainjudged_count,no_verdict_countandjudged_mean_score, and the runs page and environment health show a “No verdict” count with the judged pass rate and score beside the headline ones. The CLI prints the same, e.g.Pass Rate: 62.00% (judged 70.00%, 12.00% no verdict). A session’s final verifier now gives its verdict on stdout only, as the judges do, so a JSON line on stderr can no longer replace it, and its stderr is kept inRunResult.stderron every backend. -
Rewards in built-in envs, importers and CyberGym: a built-in or imported verifier whose judge fails (a full disk, a broken library) now exits 2 with
error_type='runtime_error', sohas_verdictis false; before, it printed an ordinary 0.level_order_traversalno longer builds nodes under missing parents, which marked a correct BFS wrong (18/25).string_manipulationdrops an undocumented penalty that capped a correct split-and-slice answer at 0.3.file_organizer’s prompt names thePROJECT_DIRthe verifier checks instead of/tmp/project.data_pipelinecompares every written record, so altered ages or cities no longer pass. Log4Shell triage scores source IPs by exact overlap, so an invented IP costs credit, and both cyber envs score a non-object JSON answer instead of crashing. HumanEval and MBPP tests run against the dataset’s own helpers and only the names the tests call, so a solution’s ownrange,set,encode_cyclicor patchedmathcan’t rig the asserts; correct answers that skip the prompt’s helper or the reference’s first function now pass, and MBPP prompts name the functions the tests call. EvalPlus deep-copies each reference call, so a reference that mutates its arguments no longer corrupts the embedded inputs (Mbpp/635, Mbpp/734). EvalPlus and BigCodeBench pass only when every scored test passes and keep partial credit as the score; BigCodeBench counts a skipped test as a failure and a fully skipped suite as no verdict, and an empty suite now scores 0 instead of a free 1.0 (“direct assertion tests passed”). The CyberGym runner scores what the patch changed on disk, not its text, and measures the reference the same way: changed lines outside a hunk and patches that change nothing score 0, paths outside the repository are refused, line endings and git-quoted file names survive, and the prompt names thediff --gitformat it checks. A qualified name likeother_benchmark/coin_changeno longer loadscoin_changeby its basename. CyberBench seed specs need finite weights in [0, 1] and write strict JSON, anddocs/cyberbench.mddrops the unsupported--mode local. -
Trainer integrations keep failed runs out of training. The TRL, Axolotl, DAPO and
RewardFunctionreward functions returnNoneinstead of 0.0 for a completion whose sandbox or verifier failed, so TRL 1.6+ leaves it out of its group’s baseline and gives it no advantage, and every entry point now runs the last fenced code block (verl’s batchmake_verl_reward_fn, DAPO,RewardFunctionand lm-eval used to run the raw text).make_trl_reward_fntakesmax_parallelandcomponent; the Axolotl and DAPO reward functions are TRL’s, so Axolotl’s now accept conversational completions. The verl single-turn agent loop drops a sample with no verdict under verl’s v1 trainer (the default since 0.9), so its group’s GRPO statistics come from the other samples, and masks it under the legacy trainer; the multi-turn loop does the same for a sandbox that fails to start or a final verifier that fails, which used to score a plain 0.0, and logserrorinreward_extra_info.make_lm_eval_tasknow returns a task forlm_eval.simple_evaluate(lm-eval 0.4.9+) and replacesregister_deepgym_tasks, which used a registry lm-eval no longer has; a verifier failure is left out ofdeepgym_scoreanddeepgym_passand shows in the newdeepgym_verdict.generate_axolotl_config(method='grpo')now scores withrow_reward_fn, which scores eachverl_rowsrow with its own environment, and no longer takesdataset_type. The OpenRLHF reward endpoint waits for its runs without holding a thread, so a slow verification no longer holds up other requests. -
No zeros from failed runs in verl, OpenRLHF or generated configs. verl’s
compute_score, and the functionsmake_verl_compute_scoremakes, raiseNoVerdictErrorfor a run whose sandbox or verifier failed instead of returning 0.0: verl’s v1 trainer (the default since 0.9) runs the reward function inside the sample’s agent-loop session, so the sample is dropped, and the legacy trainer, which can’t drop one, stops; the error says so and points to the agent loop below.make_verl_compute_scorenow returns the samescore/passed/errordict ascompute_score, and the batchmake_verl_reward_fnreturnsNonefor such a run. Thedeepgym_single_turnagent loop scores with the configuredreward.custom_reward_function(so a one-environment module works there too, as do float, dict and async reward functions) and logs the samescore/passed/errorkeys for every sample, dropping a failed sample under v1 and masking it under the legacy trainer.generate_dapo_verl_configselects that loop, from a config file that ships with deepgym, and drops thedaporeward manager, which doesn’t run on samples the loop scored. Only failures that aren’t the policy’s doing are kept out of training: the newdeepgym.exceptions.is_infrastructure_failuretells a network or provider failure (a connection or transport error, a provider call timing out, a provider 5xx or 429, Daytona, Modal and e2b provider errors, a spot eviction) from one the policy caused. The multi-turndeepgym_agentloop remembers such a failure during an action, so the episode gets no verdict instead of a final verifier’s 0 for the state the outage left. Any other failed action is shown to the model and the episode is scored, and a final verifier that can’t run in a sandbox the policy killed or hung scores 0 with the model-faulterror_typepolicy_sandbox_failure. The OpenRLHF endpoint never fails a request, since a failed one stops OpenRLHF’s training step: it grades a response once more after a short pause only whenis_infrastructure_failuresays the provider or the network failed it, runs every grading in the router’s newmax_parallelslots (32), and never waits past 150 s per request. A sample whose second grading fails too or runs out of time, one whose run never got a slot by the deadline, or a query that doesn’t contain its prompt gets the router’s newinfra_failure_score(a first run still going at the deadline scores 0, as the model’s code may be what slows it), logged as a warning. It’s now a required argument ofcreate_openrlhf_router, with no default:'nan'(recommended), which OpenRLHF’s dynamic filtering drops (without dynamic filtering a NaN breaks training), or'zero', which trains on DeepGym’s failure as a failed answer. Any other run without a verdict may follow from the model’s output: it scores 0 in either mode and is never graded again. The verl loops and the OpenRLHF endpoint log a warning when more than 5% of a batch gets no verdict (deepgym.integrations.reward.MaskedRate). Hub pushes refuse a blankenv_name(now a required argument ofpush_environment_to_hub), and loading all environments, or one by name, refuses a dataset with a blank or repeated name instead of silently dropping one. The generated Axolotl GRPO config setseval_batch_size: 8, so TRL’s check that each eval batch holds whole groups of its 8 generations passes on one GPU (the batch of 2 failed). -
Hub environments round-trip.
push_environment_to_hubwrites schema 2.0 rows that keep everyEnvironmentfield (support files, verifier mode, case timeout, image, snapshot, name, type and the rest), refuses subclasses, and returns the commit it made; pushing over a schema 1.0 repo also rewrites the features its dataset card lists, whichpush_to_hubkeeps and which would stop the new rows from loading. The loaders takerevision=to pin that commit and warn when nothing is pinned. A Hub row is untrusted, so pushes and loads run one shared check: unknown fields, wrong types, averifier_path,env_vars, averifier_modethe verifier does not read as, and support file names that are not plain relative paths are all refused, and a customimageorsnapshotloads only withtrust_image=True. Schema 1.0 rows still load.deepgym[hf]needs datasets 2.16 or later. -
API server lifecycle:
deepgym servecloses idle sessions on a 60-second sweep, without waiting for new traffic, and closes every session on shutdown. A session whose sandbox delete fails keeps its id and its slot:DELETEanswers 502 and can be retried, and the sweep retries it too, up to 10 attempts; then its sandbox id is logged as an error, the slot is freed, and/healthcounts it inleaked_sandboxes. Providers now report a failed delete (Sandbox.delete()returns False; it still never raises, and a sandbox that is already gone counts as deleted) andSandboxBackend.close()raises on it; leaving a session’s or trace’swithblock logs it as a warning instead, sorun_episode()keeps its result and an exception from the block still propagates. Request bodies over 32 MiB get 413 before they are read. Async jobs count against the job limit only while queued or running, and their request bodies may take at most 256 MiB in all (429 above); a job isqueueduntil a job thread is free, thenrunning. Finished jobs stay readable up to the newest 10,000 or 256 MiB of results. A cancelled job stays cancelled: a queued one is dropped with its request, a cancelled batch starts none of its remaining outputs, and runs already going finish within their timeout with results dropped. All requests and jobs share one limit of 100 concurrent runs, somax_parallelno longer adds up across requests. Registering an environment takes the same fields as an inline run (verifier_mode,case_timeoutandsupport_fileswere dropped), an invalid environment is a 422 before anything is queued (it was a 500, or a job stuck inqueued), listings include each environment’sid, and health, capabilities and the OpenAPI schema report the package version instead of 0.3.0. E2B commands are killed in the sandbox when they time out (E2B’s own timeout only stopped waiting), and a solution call’s deadline covers sending its request, so a solution that stops reading can’t stall the verifier.deepgym createprints the environment with its--nameand says it isn’t saved. A release fails if its tag isn’t the built version or the version is already on PyPI, andscripts/publish.sh, which could upload a wheel without the console, is gone. -
Security: the Daytona desktop lockdown fails closed. Every lockdown step must now take, and is checked twice: as the agent once applied (no terminal, IDLE, run dialog, panel, settings tool or “Open With” helper it can run, no Thunar custom-actions plugin it can load), and once the desktop runs —
xfce4-sessionruns as the agent, x11vnc runs with only the flags Daytona starts it with and reads its no-remote, no-connect options from a file its own user can read, and a live query confirms remote control is actually off rather than trusting x11vnc’s argv; the probe ignores any rc file (-norc), and silence counts as off only once the query shows it went out over this display’s X property channel, so a probe that never ran, found no display, used another channel or timed out refuses the desktop. A missing helper, a desktop already running, or a session Daytona would start as its sudoer now raisesSandboxError. The managed Chromium policy (DevTools off,chrome://,file://and the like blocked) is written, root-owned and not writable by the agent, to every dir a Chromium-family build reads (/etc/chromium,/etc/opt/chrome,/etc/chromium-browser), andlaunch_app.shruns exactly the browser setup’s own PATH finds (chromium, thenchromium-browser, thengoogle-chrome), looked up as the agent so a folder only root can enter is skipped, and pinned by the lockdown so the launch is that binary; no binary is inspected. Before,|| true,2>/dev/nulland unmatched globs skipped steps silently, and nothing checked whom the session ran as. Code a session verifier loads throughload_solutionnow runs asdgsolverwith its groups cleared, no-new-privs and an empty bounding set: sudo runssetprivas root under avisudo-checked rule pinned to that exact drop, so a stray image group membership (ashadowline) and the image’s setuid binaries (sudo,su,mount,chrome-sandbox) no longer reach it. Before, that code kept its groups and ran without no-new-privs. Session setup proves each session user’s uid and primary gid are that account’s alone and its only group, and each harness directory (staging, verifier, runtime) is created through a helper that refuses a symlink or a redirected path and forces root ownership and mode (mkdir -p -mleft a pre-existing one untouched); a work directory may no longer hold or sit in the runtime folder verifiers import from. Chromium drops--test-type, which only hid a warning bar kiosk mode never shows;--no-sandboxstays, with the reason inlaunch_app.sh: under the agent’s no-new-privs the setuid sandbox can’t start, and Daytona’s hosts deny user-namespace id maps to every non-root user. -
Security: patches can’t touch test infrastructure. The SWE-bench Pro / MiMo patch runner only put back the paths the hidden test patch touches. A diff that added, for example, a repo-root
conftest.py, a pytest plugin throughpytest.ini/setup.cfg, a.pthorsitecustomize.py, a GoTestMain, or a jest/vitest config could make the test command exit 0 without fixing anything. Now every other path the diff touches, as listed bygit apply --numstat -zforwards and reversed (raw, so quoted and non-ASCII names are matched as git writes them), is checked first. A diff that touches a test or its executable helpers, conftest/ini/runner config (includingtox.ini,tox.tomlandgo.work), the pytest sections andpytest11entry points ofpyproject.toml/setup.cfgand[tool.tox], the test scripts, runner config andworkspaceskeys ofpackage.json, Cargo’s target, dev/build-dependency and workspace tables ([workspace.package]metadata aside), ago.modadded or removed below the repo root, build files that run code (Makefile,pom.xml,build.gradle,build.rs), or the installed harness (node_modules,site-packages,vendor, virtualenvs,.cargo), or that creates a symlink or a file mode other than 100644/100755, scores 0 witherror_type='test_infra_tampering'. Config files are compared semantically and fail closed (a file that can’t be parsed and shown unchanged counts as tampering). To avoid giving a correct fix a false zero, the rules deliberately do not refuse data fixtures under a test directory, other entry points such asconsole_scripts, the root package’s ownbinand its other scripts, an exec-bit flip on an ordinary file, dependency and version edits (Cargo[dependencies]and[workspace.package]metadata, and an existinggo.mod, included), lockfiles includinggo.sum, or ordinary files that only share a word with a test name, such ascmd/test.goori18n.setup.ts(checked over the 642 SWE-bench Pro gold patches: 12 refused (1.9%), real test files such as*.spec.*,jest.setup.jsorpytest.iniplus Makefiles and a vendored dependency, which run code; the stub package a repointed dependency would need declares its ownbin, which makes the run uncertain). That is a model fault, so verl trains on it instead of masking it. Every path is compared in one form, git’s raw-zoutput throughnormpathwith case kept. An edit the guard can’t classify either way (a path with no such form inside the repo, a file the test command may name, by any normalized path its own path ends with, a folder it can’t know such as../or$VAR, or its Python module, so only a word naming another folder outright is not a mention, apackage.jsonscript a test script runs by name, a nested package’sbin, or a[workspace.package]key outside the known metadata) still runs the tests: a failure is the normal, trained 0, so the edit gains nothing, and a pass, which the edit may have caused, getserror_type='validation_uncertain'with no verdict, so trainers mask it. If git times out listing the paths, the result iserror_type='validation_timeout', and if it can’t find the repository top-level,error_type='validation_error': no verdict either way, so trainers mask it.deepgym qanow runs apatch_tamperingattack on self-contained patch tasks such asmimo_code. This is a stopgap, not a complete defence. A denylist of what a test runner loads can never be exhaustive, the config comparison knowingly reads only the keys above, and a diff to the code under test can still force an exit (os._exit/atexitat import) that the exit-code reward trusts. The real fix is a per-test report gate that takes the verdict from the test runner’s own report (pytest/go/jest/vitest), written to a path the patch can’t reach, and checks the hidden tests actually ran and passed; it lands in a follow-up and supersedes this denylist. -
Traces keep every action, every grade, and the code and image that made them (schema 1.1). A model turn that issued two actions (two commands, or a tool call and a command) dropped the whole trace from
--rows rlexports, and ATIF split it into one agent turn per action. Step each action and pass the turn’s messages with its first one only:Trace.turnsgroups them (a 1.0 trace, which used empty messages for “not recorded”, keeps one turn per step), RL rows keep the trace and gainsteps(every action with its observation text and screenshot, done flag, exit code, error, step reward and themessage_indexof the reply that produced it), and ATIF writes one agent step per turn with a tool call and a result per action, the step reward and step verifier output now in each result’sextra. Callingscore()twice kept only the last grade; earlier ones are now inprior_results, anddeepgym traces exportrefuses two copies of a trace that disagree instead of keeping one.Trace.to_trajectory()keeps an action the session applied even when its screenshot then failed (it dropped the action and its reward), and totals step rewards the way the session does (sum()rounds differently from Python 3.12 on). Provenance addsdeepgym_commit, the git commit of the deepgym checkout (null for an installed package), withdeepgym_dirty, set if the package folder’s files differ from that commit or changed after deepgym was imported, and the image the sandbox booted with the digest its provider resolved: Apple container and Docker from the container, Daytona from its snapshot (the default or a named one). Where there is none, as for an image Daytona builds, Modal, E2B or the local backend,digest_unavailablesays why. Schema 1.0 traces still load and export. -
Core reward path: each failure lands on whoever caused it. A verifier that prints a score and then crashes no longer keeps that score, in any verifier shape; before, Python’s exit 1 for an uncaught exception read as the protocol’s “failed” and the printed partial score reached the reward function. An exception hook now decides, while the script keeps running as
__main__with its own__file__: before the policy’s output is in play, the crash is the verifier’s own failure, with no verdict (runtime_error, returned rather than raised); once it is (a solution process started, or the verifier opened the solution’s file, or a session verifier opened a file in the agent’s work directory, so running the solution withexecand parsing malformed policy output count), the crash came from handling it, so it is a judged 0 witherror_type='solution_error', a new model fault. So is a result the verifier can’t normalize (NaN, not a number) after that point. Whatever the solution does (raise, exit, hang, die, return garbage, print unparseable output) is therefore its own 0 even when the verifier doesn’t handle it; before, an uncaught solution error became a verifier error with no verdict, asimport_errorortimeoutwhen the solution raisedImportErrororTimeoutErroritself, so a solution could dodge its score by breaking the verifier. Every failure from the solution process now reaches the verifier as adeepgym_solution.SolutionError, still of the builtin type it raised (except ValueErrorandexcept TimeoutErrorkeep working). A solution that runs past its time limit is a judged 0 witherror_type='solution_timeout', a new model fault, on every path;timeoutstays DeepGym’s own timeout, with no verdict. Load test data before the solution: a verifier that fails after starting it scores 0.LaunchErroris now aBaseException, so a verifier’sexcept Exceptioncan’t score the machine’s failure; uncaught, it ends the run withsandbox_error(no verdict). Before the first solution start, a preflight checks free disk and inodes and that the isolation launcher (sudo, setpriv, bwrap, sandbox-exec) starts code from a fresh directory, and a judge-side failure while preparing the run (a temp file, the loopback relay) is aLaunchErrortoo. Once solution code has run, a failed relaunch is the solution’s, aSolutionErrorscored 0, since it may have used up the disk or processes. The judge prints a per-run token on stdout once the policy’s output is in play. A single-turn sandbox that dies after that token (its Docker or Apple container stops, or a Modal exec ends with the sandbox finished, which return what was printed) is the policy’s doing: a judged 0 witherror_type='policy_sandbox_failure', where it wassandbox_error; so is a run that outruns its time limit after the token, locally or in a sandbox (solution_timeout), such as a verifier regex the solution’s output made backtrack. One that dies before the token (creation, setup, uploads, the verifier loading its own fixtures), one whose provider returns nothing printed (Daytona, an E2B or Modal call that raises), or one Daytona evicts or times out in its queue (as its error or its refreshed sandbox record says) keeps no verdict. Wrappers report it as the newdeepgym.exceptions.SandboxDiedError(also indeepgym.sandboxes), whichis_infrastructure_failurenever counts as the provider’s failure. Every verdict line the harness prints starts a fresh stdout line, so a partial line the verifier left unterminated can’t hide it, and a stderr the solution broke (a full disk under a file-backed stderr) can’t change the verdict or the exit code. Session verifiers use the same guard, and the code they load gets the step’s time limit as its deadline, as the judge does, with a few seconds’ grace for the verifier to report. A session step that fails after its action may have run (the provider failing while it applies the action, takes the screenshot or runs the step verifier, not only aVerifierError) now ends the episode: the action stays on the trajectory with no step reward, further steps are refused, andscore()returns no verdict for a network or provider failure (is_infrastructure_failure) or a failed step verifier; anything else follows from the agent’s actions (its sandbox died, a write failed, its files, a folder or a planted link, blocked the step-output file) and is a judged 0 (error_type='policy_sandbox_failure'). A sandbox that dies under the final verifier is a judged 0 too, once the agent has acted.run_episodereturns the result a failed step fixed instead of raising. Before, the session stayed runnable, so a retry applied a second action pastmax_steps, scored it, and the first action was never recorded. The new observation is now taken before the step verifier runs, so a failed screenshot no longer leaves a recorded reward and a stale observation behind for a retry to replay, and trace replay (Trace.to_trajectory()) keeps a recorded step that failed without an observation; trace capture also keeps a step interrupted after its action ran. Awrite_fileblocked by the agent’s own files (a folder or link in the way, found before anything is written) raisesActionError(rejected, the episode goes on); any other write failure is aSandboxError. The judge’s setup refuses an existingdgsolveraccount that is root or shares its uid, gid or groups with another account (the check session setup already ran, now shared and also refusing uid or gid 0), and the judge never drops the solution to uid or gid 0; before, an image with a root-aliasdgsolverran the solution as root. Nothing a solution starts outlives its verifier any more: with its first solution the verifier starts a watcher process, in its own session, that kills every solution process group (aload_solutionserver, arun_solutionscript and whatever they started) once the verifier ends, however it ends (a host timeout or cancel, a crash), even when the solution hogs the interpreter in a long C call; arun_solutionscript waits to start until the watcher knows it, and a verifier’s own exit waits for the watcher, so a sandbox user’s sweep never reaches the next grade (on macOS, a child that detaches into a session of its own still escapes, which is why subprocesses needallow_subprocessthere); before, such a server, or a busy child a solution started, kept running after the run. A cancelled or timed-out local run, sync or async, kills its verifier’s whole process group and reaps the verifier, so neither a verifier’s child nor one holding its output pipes outlives or stalls it, and a traced session’sclose()retries a sandbox delete that failed instead of returning silently. -
SWE-bench Pro / MiMo patch runner: two cases where a correct patch got no grade. In a task whose
repo_diris a subdirectory of the repository, the hidden test files were saved and restored at the wrong path, so a patch that also edited a hidden test kept that edit and the test patch then failed (test_patch_failed). The check that runs before the patch now only looks up programs and modules that run before an earlier command could set them up (onlycd,exportand similar builtins may come first); before, a test command that sets up its own environment, such aspython3 -m venv /tmp/env && /tmp/env/bin/python -m unittest, was refused (preflight_failed) because the venv did not exist yet. -
Imported and built-in env rewards. BigCodeBench checks the tests’ top-level imports before the solution runs, so a module the sandbox lacks gives no verdict instead of a 0. The EvalPlus importer no longer publishes a task whose reference fails on any input, and removes an earlier import’s copy of it; before, those cases left the suite, so a solution that failed them could score 1.0.
file_organizerchecks each README’s description line, not only its heading.cve_2021_44228_log_triagescores only the answer’s own fields (keywords elsewhere in the output earn nothing), needs the exact CVE ID andlog4shell, counts payload indicators that name thejndi:ldapandjndi:dnslookups, and gives no credit for remediation steps in a clause that holds a negation (“do not patch”, “patching is not needed”). -
QA evidence, sub-scores and exports. An unknown attack strategy is rejected before any attack runs (a 422 over the API); a misspelt one ran nothing and read as robust. An inconclusive retest of a changed verifier or attack drops its old stored result instead of leaving it to stand in. A NaN or infinite reward component, or an invalid case score, is no longer a trainable 0 under a valid overall score: the verifier wrapper checks them as it does the overall score, so one the solution’s output made invalid is its judged 0 (
solution_error), and one the verifier produced on its own raisesInvalidScoreError(no verdict). OpenRLHF’s router rewards a sample whose verifier failed or returned an invalid result itsinfra_failure_score(NaN in'nan'mode) instead of 0; the model’s own failures, and a run still going at the request deadline, still score 0. Harbor export refuses snapshot-backed environments instead of exporting them on a plain image, and RL trace rows carryinitial_observationandinitial_screenshot. -
Security: sessions refuse images where anyone but root can change the programs root runs. Root-run session commands and session verifiers find programs through a fixed PATH (
/usr/local/sbinto/bin). Once the agent owns its work directory, and before it runs anything, session setup checks every folder on that PATH, each folder above it, every entry in it, and every path a link among them leads through: each must be owned by root and closed to group and other writes. Links are followed, so/bin→usr/binpasses. Anything else, or a check that fails or prints anything but a pass, raisesSandboxError(no verdict) before the episode starts; mid-episode the agent can’t change any of it. Before, an image with a world-writable/usr/local/binlet the agent plant a program the step verifier’s wrapper ran as root. Images that give/usr/localor its folders to a non-root user or group (Debian’s oldroot:staffmode 2775 included) are now refused. -
Security: the console’s Host-header guard covers every loopback address.
deepgym consolenow rejects foreign Host headers whenever its host resolves to a loopback address (::1,127.0.0.2,127.1,::ffff:127.0.0.1, …), not only for127.0.0.1andlocalhost. It accepts the host as given and as browsers write its addresses (127.0.0.2for127.2), and prints IPv6 URLs in brackets. Before, it served any Host header there, and the page carries the launch token. On Starlette older than 1.7, which can’t parse a bracketed Host header, an IPv6 console answers 400 to every request.