What changed in each BenchFlow release: 0.9.2 is the current release, and 0.10.0rc3 the current release candidate. Under [0.10.0rc1] are the changes its wheel has; the ones CHANGELOG.md also lists there but that came after the wheel are at the end of [0.10.0rc2]. What's in 0.10.0rc3 walks through what 0.10 adds.
[0.10.0rc3] - release candidate
The third 0.10 release candidate: everything here is new since 0.10.0rc2. It is a pre-release on PyPI, so install it by name: pip install benchflow==0.10.0rc3 (or pip install --pre benchflow).
What's new
bench preferencespairs outputs made outside BenchFlow beside the trials.pairs --items FILEtakes a JSON-lines file of outputs from other tools (a golden, a designer's version, a remix), each with its task, its output folder and the labels of its arm. Each joins the trials of its task's version (task_digestnames the version when the trials ran more than one), or pairs with the other listed outputs of a task no trial ran.--conditionlabels the trials' outputs, a condition tells two otherwise equal arms apart, andrank --by conditionrates the conditions.--entrynames the page to open in every output, recorded inassignment.json, and leaves out an output without it.--golden barkeeps a golden out of every pair, for a tool that shows it as the bar.- A vote can name the two outputs as the rater's tool showed them.
left_itemandright_item, by item id or an output'sexternal_id, replacepair_idfor a tool that makes its own pairs, and a rater's later vote on the same two outputs replaces the earlier one in either order.rater_kind,invited,againstandtagsare recorded with a vote outside its id, so a votes file from 0.10.0rc2 imports again without adding anything. bench preferences importreads the Frontier Design Benchmark vote site's export. Anfdb-votes/v1export, or one under the site's older names, is read as it comes: a skip is an abstention, a voter from a personal link is a person and any other a guest, a changed vote replaces the earlier one, and a vote a later export no longer has is recorded as withdrawn, so it stops counting (an export withdraws only votes cast before it was made, and anexportedAtthat isn't an ISO 8601 time with a zone refuses the file).--skip-unknownleaves out votes on outputs the store lacks, and--dry-runchecks everything and writes nothing.bench preferences rankrates people's votes, with guests and model judges apart. A vote'srater_kind, or a rater namedmodel:<name>, says who cast it. Guests' and model judges' votes are fitted in tables of their own (by_kind) and never pooled with people's;--ratersfits kinds together when asked. With no person's vote, the ratings are the first kind that voted, and the output says so. The docs'judge_model.pynow writesrater_kind.- Each model judge is measured against people.
rankreports how often each judge chose the output most people preferred, with a Wilson interval and split by the arms in the pair, beside one person's agreement with the others. A judge asked both ways round keeps both answers, how often they match is shown, and the pair counts once in its agreement. A judge's rubric scores are read as its choice on each pair and kept out of people's rubric table unless--ratersincludes judges; guests' rubric scores get a table of their own. - Intervals resample raters and tasks. With several raters and tasks the bootstrap draws whole raters and tasks (
--cluster, defaultauto), and the design effect, how much wider that makes the intervals, divides the votes in a new head-to-head table: each pair of arms' share, its interval and the smallest gap the votes could show. When the design effect can't be measured (--bootstrap 0, or no arm with both a clustered and a single-vote interval), the table gives each pair's votes and share without an interval or gap, and says why. An interval every resample gave one value is left out, and a resample that drew none of an arm's votes is left out of that arm's interval instead of counting as a rating of 1000. - Ranks wait until every pair has a vote.
ranksays how many pairs have none and lists the arms by name until each has one (--provisionalshows the ratings);--jsonhasranked,coverageandvoted_pairs.
Changed
- A training group reaches the trainer whole or not at all.
bench train stream --group-size Nno longer passes on part of a group: a group with an unscored member, with a member whose proxy log has no model calls, with token sequences on some members but not the others, or still short when the job ends is withheld.--withheld FILEwrites its records, each with awithheldfield saying why, andstream_rollouts(..., on_withheld=...)gets each such group as one list. With--group-advantage,bench train convert(prime-sft,trl-sft,verl-sft,skyrl-sft) writes no row of a group whose member has no score (unscored, or a trial that never wrote a readableresult.json: itsconfig.jsonplaces it in its group) or never reached the model (no model calls, or every call failed); it used to write the other members' rows with advantages that counted the missing member. A scored member keeps its group whole: a rollout that ended with an error now writes its rows, with the error in theirgroupblock so a trainer can mask them, as the stream passes it; one under--min-rewardloses only its own rows, the advantages being computed on the whole group first. The manifest'straining_signalnames each withheld group and why, and countswithheld_groupsandrows_withheld. The training-signal schema (still v1) addsgroup.withheld,group.errorandexcluded: "group_withheld". - Training exports and the stream read a task.toml-package runner's results as BenchFlow's. The group pass of
bench train convert --group-advantage,rollout_training_signalsandbench train streamread such a trial's reward fromverifier_result.rewards, as the rows already did; before, every such trial counted as unscored.
Fixed
bench hubsays when a Hub has no dataset registry, andpushticks only a run the Hub kept as sent. Against a Hub whose route list shows no dataset registry,bench hub show,bench hub publishandbench run -d <name> --hubsay that it has none, where 0.10.0rc2 said to check the dataset's name and version, andbench hub push --datasetto such a Hub stops before uploading anything.bench hub pushprinted a tick for every run the Hub answered with, even one it didn't link to the dataset it was sent or whose trial folders it didn't keep; such a run is now an error with the run's address, and the push exits 1. A key that can't read runs back is named as the reason the push couldn't be checked.- A denylist sandbox's egress proxy judges a JSON body with a model-request key instead of dropping it. In 0.10.0rc2 the proxy in a
network_mode: denylistsandbox could not load its request guard, so a request whose JSON body had amodel,messages,input,contentsortoolskey at any depth (a chat request, a GraphQL mutation'sinput, an upload'smodel) lost its connection with no reply and no line inegress_denylist.jsonl. The guard now ships with every egress proxy, and a body the proxy cannot judge is refused with a 403 and abody-check-failedline naming the error's type.
Known issues
- A multi-step task whose last step ends at its time limit has its final verify credited to the step before. Under
finala correct last step scores 0, and undermeanthe previous step's reward is replaced. - Which attempt of a retried trial counts can change when a job folder is copied. Readers take the scored attempt, then the newest by
result.json's modification time, so aftercp -R, a bucket sync or a download two scored attempts can swap. Copy with the times kept (cp -a,rsync -t). - A fresh run's
summary.jsonand exit code count a task's last attempt, whilebench results,bf.load_job, resume andreport.mdcount its scored one. When the two differ, one job folder gives two verdicts for that task, and a resume that runs nothing rewritessummary.jsonto the readers' answer.
[0.10.0rc2] - release candidate
The second 0.10 release candidate: everything here is new since 0.10.0rc1. It includes 0.9.2's five grading-integrity fixes. It is a pre-release on PyPI, so install it by name: pip install benchflow==0.10.0rc2 (or pip install --pre benchflow).
Security
- LiteLLM 1.103.3, and patched TLS and HTTP clients (as public #1196, and as in 0.9.1). LiteLLM 1.91.5 carried two advisories (GHSA-7hp6-4w63-5g45 and GHSA-g5ff-637f-6q2m). The floors are now cryptography>=50, certifi>=2026.10.19 and aiohttp>=3.14.4, and oauthlib is 4.0.1.
pip-auditfinds nothing in the lock or in the two hashed proxy pin files the wheel ships. The lock also movesmcpto 2.3.0. - An offline agent can no longer resolve names. Under
agent.network_mode: no-network, or an offline sandbox a model agent works in, the agent user's uid firewall admitted all loopback traffic, and Docker's embedded resolver answers on loopback (127.0.0.11) and forwards to the host's. The agent resolved any name, and a name it made up carried data out. The firewall now refuses the agent's DNS on loopback, as an allowlist already did (its DNS goes to the allowlist's filter). A denylist agent's resolver is unchanged. - A
script:agent underagent.network_mode: no-networkruns offline. It had the sandbox's whole network while the run card saidno-network: its sandbox got noNET_ADMINand no uid firewall was armed. It now gets the sandbox and the loopback-only firewall a model agent gets, DNS refused as above. - A
script:agent under an open network can't reach the cloud metadata endpoints. A model agent's uid gets the block there, but an operator script, which runs as the same user, ran without it. It now starts only once the block is proven on, as a model agent does, andcloud-metadata.jsonrecords it.--allow-cloud-metadatastill opts out. - Root's git in
test.shno longer runs the agent's hooks, filters or aliases. Grading makes the workspace root's, so root's git trusted the.gitfolders the agent wrote: a hook, anfsmonitorcommand, a filter or driver, an include, an alias or acommondirpointer ran as root whentest.shran git, and a wrong answer could score 1. Before grading, BenchFlow now removes each one the repository lacked before the agent's turn (all of them in a separate verifier), and the grader's git runs with hooks,fsmonitor, external diff and signing off. Atest.shthat needs one passes it withgit -c; a task whose answer is a hook or a driver setscleanup_git = falsein[verifier.hardening]. - A separate verifier no longer loads the agent's files through the image's
LD_PRELOADorLD_LIBRARY_PATH. A value naming the workspace (ENV LD_LIBRARY_PATH=/app/buildis common in C and C++ tasks) passed the check before the transfer, when nothing was there yet, and BenchFlow's root steps then loaded the agent's files from it. Values naming the workspace or/logs/artifactsno longer reach root's commands there, as in the agent's sandbox.
Speed
- Model-call capture no longer holds up the event loop (#1055). Parsing the gateway's callback log, writing each new exchange to
llm_trajectory.jsonlduring the trial, and the final import ran on the job's event loop, so a slow write delayed every timeout, idle watchdog and heartbeat of the job. They now run in worker threads, and the files keep their format. A cancelled trial waits for the write in flight before it cleans up, and what that write recorded still reaches the live view and the live token count. - Writing
results.jsonlno longer holds a trial's whole proxy log in memory. It readllm_trajectory.jsonland every request history it rebuilt into memory at once, so a 32 MiB log of failed calls took 64 MiB. It now checks the log line by line, keeps each call's offset and reads a call again when it needs it; on that log it stays under 12 MiB. The rows it writes are still held in memory.
Fixed
- Claude Code on a chat-completions model keeps its PDFs and images on LiteLLM 1.103.3. The model proxy's Messages-to-chat bridge still turns a PDF or text document into text, since LiteLLM 1.103.3 would send a PDF as an
image_urlpart and drop a text document. For a model that reads text only, a lone tool-result image still goes out as text, since an image part makes such a model refuse that request and every later one. LiteLLM 1.103.3 now opens a streamed answer with its first real block itself, so BenchFlow's filter for the empty block at index 0 is gone. - Terminal-Bench 4.1 tasks score. On the rc1 wheel every one ended unscored or at 0, since all 66 declare artifacts and grade them in a separate verifier. A declared artifact that is a single file is captured under bfx (its scan answered
not-directory).test.shimporting withpython3 -Ino longer writes byte code into the verifier image's packages, which the plugin guard then refused as changed; inside the guarded pytest nothing writes byte code either, and caches are read only underPYTHONPYCACHEPREFIX. An image whoseUSERis not root (USER nobody) gets its sandbox user set up as root, and the oracle launches from root there too, sosolve.shcan read its inputs and write the workspace. A separate verifier keeps the tests its image was built with instead of having the task'stests/uploaded over them. Aschema_version = "2.0"task with the 1.x keys reads as 1.x; unknown keys are still refused. - Tasks with
[[verifier.collect]]hooks run, and a hook inmainruns with the agent's rights. 0.9 refused a task that declared these task.toml hooks; Terminal-Bench 4.1 tasks use them to snapshot state after the agent's turn (shadow-relaywrites the agent's patch to/tmp/agent.patch). A hook names acommand, and may name aservice(mainby default), atimeout_sec(60) and auser. The hooks formainrun once, after the agent's processes stop and before the workspace and artifacts are frozen; another service's hooks run before its artifacts are read. Amainhook that names nouserruns as the sandbox user, the account the agent ran as, and that account's processes are stopped again after the hooks. Root gets no git trust over the agent's repositories, so git config the agent wrote into them (acore.fsmonitor, a filter driver) runs as the agent alone, never as root, while the hook can still read them: a no-op or model trial's patch is captured. For the oracle and with--sandbox-user nonethe agent ran as root, and the hook runs as the service's own user. A hook that needs root setsuser = "root". Hooks are best effort:collect-hooks.jsonrecords each one and the account it ran as, and a failed hook does not stop the trial. A step's own[[steps.verifier.collect]]hooks are still refused before launch (R401): only the task's hooks run. See Artifacts. - A denylist also refuses the relays that would fetch a blocked page. Under
network_mode: denylistthe egress proxy matched only the listed hosts and URLs. An agent could still reach a blocked page through a reader or archive service given its address, Google Translate's proxy (*.translate.goog), a mirror or API that serves a blocked GitHub or Hugging Face repository (jsDelivr'sgh/,raw.githack.com,cdn.statically.io,raw.githubusercontent.com,codeload.github.com,api.github.com,hf-mirror.com, the Hugging Face API and datasets server), or a request body that named it. The proxy now finds blocked addresses inside URLs and bodies, percent-encoded and IDNA spellings included. It reads a JSON or gzip- or deflate-coded body whole before it connects, up to 32 MiB, and refuses a larger one; it scans other text as it streams, so part of a text upload can reach its origin before the rest is refused. Images, audio, video, PDFs and archives pass without a scan. A body the proxy cannot decode (a content encoding other than gzip or deflate, two content types) is refused. A JSON object that names at least two ofmodel,messages,input,contentsandtools, or one of them on a model API path, is checked as a model request, and refused if it asks for web search, hosted search or file input. Other JSON is scanned as data, so an upload with amodelfield or a GraphQLinputstill reaches its origin. It is still a denylist: a mirror it does not know, or an address encoded some other way, can bring a page through, andallowlistorno-networkis the stronger policy. Seedenylist. - A failed LLM judge call keeps the API key out of the error. When a judge's provider call failed after its retries, BenchFlow raised the SDK's own exception and logged its message, and a traceback that prints local variables could show the key. It now logs only the provider and the number of attempts, and raises
JudgeCallError(Configured LLM judge call failed after retries) without the SDK's message or frames. Retries and the fallback between providers are unchanged. - A separate verifier keeps its image's tools, writable state and package folders. Terminal-Bench 4.1's tasks all grade in a separate verifier, and each of these made some of their reference solutions fail:
- The verifier image's own tools stay on its
PATH. Image tools owned by a user other than root were dropped, though no agent ran there; now only paths that came from the agent's run are left out, files from other Compose services included, and aPATHfolder the image has not made yet is kept for the verifier to install into. /logs/verifieris root's after BenchFlow clears it, with or without bfx, and stays writable for a verifier that is not root.- A root verifier gets an empty, writable git config of its own as
GIT_CONFIG_GLOBAL, not/dev/null, so test.sh's owngit config --globalworks (live-database-cutoversetssafe.directorythat way). The agent's and the image's git config are still not read. - BenchFlow's protected copy of a Fedora image's Python keeps Fedora's
lib64layout, so it runs. - A declared artifact inside one Python package's folder in
site-packagesordist-packagescrosses to the verifier. A library root itself, and a path that redirects elsewhere, are still refused. - A verifier that locks
/logs/agentkeeps its score. On Docker,distributed-dedupmakes every world-writable folder root's and 0700, the bind-mounted/logs/agentincluded, so reading the published trajectory back for the custody record failed after the verifier had scored, and the score was discarded as a sandbox failure. That copy is now recorded as unreadable.
- The verifier image's own tools stay on its
- The workspace freeze copies a file it cannot chown. Where Docker maps users (a user namespace), root cannot chown an image file whose owner is outside the mapping, so the freeze before grading stopped the trial. A regular file with one name is now replaced by a root-owned copy with the same content and mode. Directories, files with more than one name and read-only failures stop the freeze as before.
- An image whose user is the sandbox user's own account reaches grading. On Docker, PID 1 runs as the image's
USER. When that is the account the sandbox user is named for (Terminal-Bench 4.1'srs-archive-cloneends withUSER agent), the kill before grading ended the keep-alive, PID 1 exited and took the container down, and the oracle and the no-op both errored withthe sandbox's command channel stopped. Under an image user that is not root, the keep-alive is now PID 1 itself (exec sleep infinity). The kill leaves that PID 1 alone only when nothing in the sandbox could have traced it (Yama'sptrace_scope1 to 3, and at 1 or 2 noCAP_SYS_PTRACEin its bounding set). Every other process of the account is still killed, and any other PID 1 of the account fails hardening as before. - A failed read of the pytest plugin guard's markers is tried again, and named. After test.sh, BenchFlow reads the guard's markers back in one exec. When that exec failed on a loaded host, the trial scored 0 with
pytest plugin guard not confirmed, and the warning listed guard failures that had not happened: Terminal-Bench 4.1'smusic-harmony, whose checker never runs pytest, scored its correct oracle 0 this way. A read whose output does not come back is now tried up to three times. If the last try fails in transport or in the controller, the trial gets a named verifier error, an infrastructure failure, not a 0. Any other failure still scores 0, and its warning says BenchFlow could not read back the markers, and why. - A separate verifier's score survives a
--jobs-dirwith..in it. With--jobs-dir ../jobs, publishing the verifier's output refused its destination (Unsafe copy destination), so a passing verifier's 1.0 was recorded as 0. The publication root is now made absolute from its spelling alone, without following links or relaxing the destination checks. - A run without
--modelruns the model its agent's model variable names. With the agent's own model variable (ANTHROPIC_MODELfor Claude Code) orBENCHFLOW_PROVIDER_MODELin--agent-env, the plan, the preflight and the run card took the agent's default model, and Claude Code ran that default despite the variable. The variable now sets the model BenchFlow gives the agent, the gateway's routing and pricing, and the result's model label;--modelstill wins. The run card says where the model came from:sourcebesidegiveninrun-card.json(modelwhen the run named it, else the variable's name, elsedefault), and(from ANTHROPIC_MODEL)on its Model line.bench run --confignow picks the model after it applies--agent-envand--agent: a file that names no model takes the variable or that agent's default, and an agent with no default model needs one. - An agent's reasoning effort reaches Azure deployments and OpenAI-compatible models LiteLLM does not know. The gateway dropped
reasoning_effortwithout a word onazure-foundry-openai/routes and on OpenAI-compatible models missing from the pinned LiteLLM's capability table. It now passes it through, and the provider decides. A Responses API call's record inllm_trajectory.jsonlkeeps its own path (/v1/responses, where it said/v1/chat/completions) and itsinstructions,previous_response_idandmax_output_tokens. gemini,opencodeandmimostart on an image without Python under a web restriction. For a task with no internet, or with a denylist or an allowlist, their settings step ranpython3, which an image such asubuntu:24.04(the basebench newwrites) lacks, so the trial stopped withFailed to apply no-web policy ... exit code 127. The step now runs on the Node their install brings, and on Python only when no Node is there.- OpenCode sends images to a model that reads them. Its gateway wrapper registered the model as text-only, so an image the agent read never reached the model. It now takes the model's input types (text, image, audio, video, PDF) from the provider's metadata and turns attachments on when images are among them. A model with no metadata is set up as before.
bench skills eval's judge matches aground_truththat ends in punctuation, and reads an Anthropic reply that opens with thinking. Exact match wanted a word boundary at both ends, sogetPageCount()never matched and scored 0; it now needs only that no letter, digit or underscore touches the match. The LLM judge read the first block of an Anthropic reply, so a reply that began with a thinking block gave no verdict and the judge could not run. It now joins the reply's text blocks, and a reply with no text is still a judge error.- A saved reward that is not a finite number is no reward. Reading a
result.jsonback (metrics, summaries,bench results, training exports), BenchFlow tookrewards.rewardas written, andtruecounted as a pass. A boolean, a string, NaN, infinity or an integer too large for a float now reads as unscored. - A usage report that changes counts as ACP activity. An agent whose only updates for a while were
usage_updatereports with new figures hit the idle timeout while it worked. A changed report now counts toward the idle watchdog. A repeated one does not, and the wall-clock and pending-tool limits hold as before. - A layer build on a nearly full disk prunes idle layers first. BenchFlow pruned cached proxy and agent layers only after a successful build, so on a local Docker daemon below the free-space floor (
BENCHFLOW_AGENT_LAYER_MIN_FREE_GB, default 10 GB) a build could fail for room the idle layers held. It now prunes them before such a build too, and layers that running trials hold stay. This is cleanup, not a reservation: builds at the same time still need room. The agent-layer limits are now documented in Cached install layers. bench checkwarns about macOS._*files. An AppleDouble file in a task folder passed without a word and could end up in the image's task data. Each one is now named, with how to keep tar from adding them (COPYFILE_DISABLE=1). The warning fails no check and changes no file.--trialsand--matrixrepeat a task named by repository.bench run org/repo/path --trials 3(or--source-repo) was refused. Such a task now repeats as a task folder does, one job per trial under<jobs dir>/<agent or alias>/trial-NN, with the source's provenance in each. The refusal that remains names what can't repeat: a run config repeats with its job config'sn_attempts, and a hosted environment with--source-env-rollouts-per-example.--dry-runcounts every trial:rolloutsis the tasks times the trials times the matrix entries (it said 4 for--trials 3over four tasks), besidetrialsand each matrix entry's agent and model. A matrix of control runs alone gets solve rates inmatrix-summary.json, as its--fail-undergate already counted them.bench train convertkeeps a Claude Code run's tool calls. Claude Code records notices in the middle of a conversation (<total_tokens>... left</total_tokens>), which the Prime-SFT export turns into user messages, and the training window began at the last of them: the row held that notice and the final answer, with every tool call dropped. A user message right after a tool result now counts as a notice inside the agent's turn, and the window keeps the user messages just before it. The summary line also says how many rollouts were left out for not ending cleanly (an error, a time limit, a conversation cut short), which only the manifest said (skipped_terminal_error).- A scripted user gets the tests' output from
soft_verify().RoundResult.verifier_outputcame only from the task's own logs (/logs/verifier/*.log,output.txt), so a task that writes none gave every scripted userNone, and every--loop-strategyverify-retry an empty feedback section. It now falls back to test.sh's stdout and stderr as the verifier brought them back (test-stdout.txt, up to 1 MiB). bench check --level equivalenceruns. On a relative task path every check stopped withbattery could not run: ... is not in the subpath of ..., and every graded variant stopped withprotected verifier runtime was not built before the agent's turn. The check now resolves the path, and builds the verifier's runtime before the oracle runs, as a trial does.- A rerun replaces the placeholder of a trial that left no folder. A trial that failed before its folder existed (a full disk) left a placeholder whose
result.jsonhad no agent or model, sobf.load_jobandbench eval metricscounted it beside its rerun, one trial more thansummary.json, under an agent named"". The loader now takes its agent and model from itsconfig.json. - The partial-credit caveat counts only trials the reward rule failed. A rubric-reviewed trial that passed at reward 0.8 was counted among the rewards other than 0 or 1 that "count as not passed".
nonbinary_rewardsstill counts every such reward. bench explainmatches what the verifier does. Itsverifier_failuretext described a test.sh that exits before writing a reward, which isverifier_no_usable_reward; it now describes the cause left when BenchFlow names no finer one, an error that startsverifier crashed:.verifier_plugin_trust(a pytest plugin installed where the agent could write) says such a run scores 0, withVerifier scored 0: pytest plugin guard not confirmed, where it said the run was unscored.- Claude Code can call another provider's Anthropic-compatible endpoint without BenchFlow's model proxy. With
ANTHROPIC_BASE_URL(a host other than anthropic.com) andANTHROPIC_AUTH_TOKENset, noANTHROPIC_API_KEYand a model that is not Claude, the run stopped because the proxy is mandatory (LiteLLM proxy is mandatory). Such a run now starts no proxy and gives Claude Code the URL, the token and the provider's model id. It has no per-call record from the proxy and no cost: the run card's Capture line saysdirect endpoint; no proxy call capture or cost accounting.--usage-tracking requiredand a task with no internet refuse this route; give them a provider route through the proxy.
What's new
- The run card says when a retry can repeat the agent's work. Its new Retries line gives the number of retries (
--retry-attempts) and says a retryable failure (an idle timeout, an ACP error, a verifier that never started) can come after the agent acted, so a run that must not get a second attempt needs--retry-attempts 0. An operator script's card (--agent script:) says it runs no model and no harness. The Controls line says it read local jobs, and how each control matched (same task digests). When the oracle fails tasks, its run's closing line names them, up to five. bench runreads datasets from a BenchFlow Hub by name. A BenchFlow Hub keeps dataset versions,org/name@version, and the runs pushed to them; so far only development instances run, and their admin gives you a key (BENCHFLOW_HUB_TOKEN).bench run -d org/name@version --hub <url>(or--registry hub:<url>) downloads the version's task archives and checks each archive's sha256 and size, each task digest and the dataset digest against what the Hub publishes; a mismatch stops the run. Without either,-dreads BenchFlow's registry first, then, for anorg/namedataset it lacks, the Hub atBENCHFLOW_HUB_URL. Before a run spends anything,bench run -dprints the version's oracle and no-op results, who ran them (the Hub, an admin's import, or the publisher's own report) and the tasks that misbehaved;bench hub show org/name@versionadds each task's result and the one next step that is missing.bench hub publish <tasks-dir> --name org/name@versionuploads reproducible archives: the same content again changes nothing, and other content under a published version is refused, with the command that publishes a new one.bench hub push <job>uploads a finished run, leaving out symlinks,.env,.netrc,auth.jsonand.credentials.json. Trials record the dataset andsummary.jsonitsdataset_digest, and the run card names the Hub and the digest. The key goes only to its Hub: a redirect to another scheme, host or port dropsAuthorizationandCookie, a redirect from HTTPS to anything else is refused, plain HTTP is refused for a Hub not on this machine, and a task archive gets the checks every archive BenchFlow opens gets.--hubis inbench run --help-all. See Datasets from a BenchFlow Hub.terminal-bench@4.1in BenchFlow's registry.bench run -d terminal-bench@4.1runs Terminal-Bench 4.1's 66 tasks from release v4.1.0 (commit4070d6ce), each pinned by its digest.terminal-bench@2.0is unchanged.- Embodied tasks run on Daytona. An embodied package now carries its simulator's build context in
environment/simulator/, with BenchFlow's episode server and verifier, so the package's digest names its grader; it used to point at a folder on the host. The same package runs on Docker, remote Docker and Daytona: a verifier in a Compose service other thanmainnow runs on Daytona too, in the sandbox's VM, where the simulator image is built for each trial (213 to 226 s of setup for Robo Use's runtime in a 1-vCPU sandbox, most of it that build). The simulator mints the reference solution's token for each episode into a file only root can read, so a vision task's oracle needs nothing set; a task that names an[agent].userruns its oracle as that user, who cannot read it.--agent script:is refused when the run is planned on a backend without the operator-script runner, such as Daytona, where it used to fail after the sandbox started.cloud-metadata.jsonno longer counts a service withnetwork_mode: none, such as the simulator, among those that reach the metadata endpoints.bench embodied reportprints a pass rate per robot mode only when there are two or more, and says that pass@k over seeds spans initial states. See Embodied tasks. - An embodied verdict names its files, and
bench embodied verifychecks them. The simulator no longer mounts the trial'sartifacts/, which the agent's container shares: a link the agent planted there made the verifier overwrite the episode record and crash, and the trial counted as a plain 0. After verification BenchFlow placesartifacts/recording.mp4andartifacts/observations/from the downloaded record, replacing what the agent left at those names without following links.verifier/reward-details.jsonholds the sha256 of every file of the episode record and the runtime's Python, OS and package versions, read in the simulator's container.bench embodied verify <job>checks each trial's files, video copies and rewards against it, names a file edited, added or swapped since, and exits 1 unless every trial checks out. It doesn't re-run the simulator, and it can't tell who wrote the folder. See What a score rests on. - An artifact can come from another Compose service. A declared artifact takes
service, with an absolutesource, on Docker and remote Docker. The file is copied inside that service, downloaded toartifacts/services/<service>/, and listed with its SHA-256, size and mode inartifacts/services/manifest.json. A separate verifier gets it at its source path only while its bytes match the list, and never under a verifier-owned folder; the custody record names what crossed and what was refused. Terminal-Bench 4.1'sctr-optimizationgrades/shared/verify_snapshot.jsonfrom itsapiservice this way. See Artifacts. - Each trial says how its model ended, beside the score.
result.jsonhasagent_outcome:completed,refusal,provider_errororunknown, with the number of calls that got an HTTP error or were refused, andreport.mdshows it beside each verifier reward. A last call that failed with 429 or a 5xx is a provider error. A call is a refusal when it endedrefusalorcontent_filter, or when the answer itself was one (a Chat Completionsrefusal, a Responsesrefusalpart) though it endedstoporcompleted.completedneeds the prompt'send_turn, or no error or timeout and a last call that answered, so a timed-out trial is nevercompleted. An error or a refusal the agent recovered from is counted but does not set the outcome. The outcome never changes a reward, a pass or the trials a score is taken over.model-calls.jsongainslast_agent_call, model-call records are written once per trial, when its sandbox is torn down, and they read a Responses call's cached and reasoning tokens too. See Model outcomes. - A model refusal is a trial's own ending, counted beside the score. A trial whose model refused (Anthropic's
stop_reason: refusal, an OpenAI-compatiblefinish_reason: content_filter, a Chat Completions or Responses refusal, or the agent's ACPrefusalstop reason) ended like any other failed turn, told apart only byagent_outcome.status.result.jsonnow records the ending asmodel_refusal: what showed it (by), the refused call (itsindexintrajectory/model-calls.jsonl, its finish reason, source and model) and how many calls were refused;model-calls.json'slast_agent_callgains the call'sindex. The trial is graded on what the agent left and counts by that grade, 0 unless its tests passed, as Terminal-Bench counts a refusal: the scoring rule is unchanged. It is never retried and a resume never runs it again, also when the agent reported an error after the refusal, which is then graded the same way.summary.jsoncounts these trials beside the score (model_refusals,model_refusal_rate;scoring.model_refusalsandscoring.rates.model_refusal), apart from the plugin guard's refusals (guard_refusals), and so does each trial inscoring.json(model_refusal). The end-of-run lines,bench results,report.mdand the run card's after-run lines (Refusals:) give the rate, and a rate above 5% warns. Aresult.jsonfrom before reads as a model refusal when itsagent_outcome.statusisrefusal. See Model outcomes. - Per-trial token and turn budgets, enforced while the trial runs.
--token-budget Ncaps each trial's tokens (input, cached input included, plus output, astotal_tokenscounts them) and--turn-budget Nits turns, one turn being one model call. A run config takestoken_budgetandturn_budget, a task[agent] token_budgetandturn_budget, and a measure the run sets replaces the task's. BenchFlow's model proxy counts every call it forwards for the trial, a subagent's or a side call's among them; once a measure is spent it refuses the next call with HTTP 400 and records why, and BenchFlow stops the agent. The call that spends the token budget completes, so a trial can pass it by that call and any call already under way; a turn budget is exact. The verifier grades what the agent left, as after a timeout, so the trial counts 0 unless its tests passed; its error istrial budget reached: ...(categorytrial_budget,bench explain trial_budget),agent_outcomereads it as BenchFlow's ending (ended_by: harness), and it is never retried.result.json'strial_budgetholds the budget and where each measure came from, what the trial used, what its subagents used where the agent's own session log says (Claude Code and Codex; BenchFlow keeps that log for a trial with a budget), the measure that stopped it and the proxy's count at the refusal.summary.jsoncounts these trials beside the score (budget_stops,budget_stop_rate) and totals the trials' use inrun_checks.trial_budget; the run card'sPer trial:line gives the budget before the run and the use against it after. An agent whose calls go around the proxy (a subscription login, an endpoint of its own, an agent that calls its own model) cannot be held to a budget: the run is refused before its job is made, and a trial whose task sets one is refused before its agent starts (agent integration failure [agent_budget]). See Per-trial budgets. - Trials record the harness versions they ran. Before the agent starts, BenchFlow reads the installed adapter, CLI and engine versions in the sandbox (
claude-agent-acp,codex-acp,gemini,opencode,mimo,pi-acp), cached and skip-install sandboxes included. They are recorded asharness_versionsinconfig.jsonandresult.json, and asharness_adapter,harness_cliandharness_enginein CSV and record exports. A version the probe cannot read isunavailable, and a scripted trial's arenot-applicable. A resumed job reruns a trial whose installer changed, or whose recorded version of a package the installer pins differs from the pin; an unreadable version counts as unknown, not as a difference.bench preferenceskeeps different installed versions of one agent and model in separate arms;--by agentor--by modelstill pools them. - OpenTelemetry out and in. Every trial writes
trajectory/otel-spans.json, an OTLP/JSON trace: atrial <task>span (task, agent, model, reward, error and each phase's seconds) and onechat <model>span per model call, with the GenAI attributes and the call's source. WithOTEL_EXPORTER_OTLP_ENDPOINT(orOTEL_EXPORTER_OTLP_TRACES_ENDPOINT) set wherebenchruns, each trial's spans also go there as OTLP/HTTP JSON when it finishes; a collector that is down never fails a trial. A run that turns the agent's exporter on (--agent-env CLAUDE_CODE_ENABLE_TELEMETRY=1, or anOTEL_*_EXPORTERnamingotlp) and names no collector of its own gets a loopback receiver, run as root in the sandbox, which keeps each export intrajectory/agent-otel.jsonl. It needs Python 3 in the image; without it the run goes on and says why. Claude Code'sclaude_code.api_requestevents becomeclaude-code-otelmodel-call records: on a two-call trial through the proxy they matched the proxy's calls, tokens and effort, and carried Claude Code's own cost estimate, which is its price table's figure, not the bill. See OpenTelemetry. bench train convertwrites verl, SkyRL and OpenEnv data. There are three new--formatvalues, and each was read by its library's own code from a real 30-call Claude Code run:verl-sft, parquet for verl 0.9.1's multi-turn SFT trainer (it needspyarrow, and verl'sdata.custom_clsset to theBenchFlowSFTDatasetfile the command prints, since verl's own class can't read the rows);skyrl-sft, JSONL for SkyRL 0.4.0's SFT trainer (--row-mode exchangerows for its defaulttrain_on_what=last_assistant_message); andopenenv-episodes, aresults.jsonlof OpenEnv 0.11.0EpisodeRecords, one per scored rollout. All three train exactly the callstrl-sftholds as completions: a call cut at the output cap, or one whose tool input is not JSON, is never a training target, and the summary line counts those calls. No token-level OpenEnvTrainingTraceis written: a rollout recorded through a provider API has no per-token log probabilities. See verl SFT parquet.- One rule for every score:
benchflow.score.v1. Every finished trial counts, and only a scored pass counts 1.summary.jsonnames the rule (scoring_rule) and gives its account underscoring: the score with a 95% interval over tasks and the task-clustered standard error, the trials with no score by who ended them and by error class, and the warnings. The end-of-run line,report.md,bench eval metrics,bench results, the job listing, pass@k, the run card and the comparison and job JSON documents all count by it, and the run summary andbench resultsname unscored trials in the same words.bench hub pushuploads ascoring.jsonwith the rule, each trial's classification and each setting's figures, so the Hub can count the same way. - Results are split by what ran. A folder whose trials ran different settings (the agent, the harness and its installed version, the served model, the route, the effort the calls carried, the task set and its versions, the BenchFlow version) gets one line per setting and a "mixed settings" warning, never one pooled score: runs that differ only in effort are never pooled. A row labelled one effort whose calls carried another is reported under the effort sent, with a warning, and a job resumed with
--restart-changedreads as mixed settings. - Intervals, warnings and seeds. BenchFlow warns when a task has fewer than 3 trials, and when more than 5% of trials errored, timed out or met a rate limit.
bench results A Bandbench eval comparegive both scores on the tasks both sides ran and the gap with a 95% interval paired by task, and warn when the gap is inside it or under 3 points.summary.json,report.mdand the run card record the seeds (tasks start in name order, with no task-order seed); a sampling seed a route dropped is a warning. A trial folder no reader can read is named, not dropped. - A job checks that its model calls carried what the run asked for. After each trial, the job compares the model, the effort, the output cap and any sampling the run set with what the agent's main calls carried. Side calls are counted apart: calls to a small model, a subagent's calls, and a call to the run's own model that offers no tools while the agent's other calls do (the session title Claude Code's ACP session asks for). Each record in
model-calls.jsonlsays how many tools its request offered (tools) and the sampling seed it carried (seed); such records arebenchflow.model-call.v2, and thev1schema stays as 0.10.0rc1 published it, for the records rc1 wrote. A dated or routed id of the same model counts as the same model. A different value stops the job after the trial that shows it and fails the run (Run check failed: …, exit 1).summary.json'srun_checksnames the setting, what was asked for and where it came from, what was sent and in how many calls. A setting no call carried, and BenchFlow's own default output cap when the agent lowers it, give a warning instead. - Truncation is a rate, with a warning and a refusal.
result.json'sagent_outcomelists every prompt turn's stop reason (stop_reasons), the model calls' finish reasons (finish_reasons) and whether the trial was cut at the output cap (truncated,truncated_by). A job warns when 10% of its trials with a known ending were cut. At 50%, once at least five trials (or every planned one) have finished, it starts no more trials andbench runexits 1.summary.jsonand the run card's after-run section record the rate. - The run card names the settings that change what a trial measures. Each variable BenchFlow knows (
CLAUDE_CODE_MAX_OUTPUT_TOKENS,MAX_THINKING_TOKENS,BENCHFLOW_BEDROCK_THINKING_EFFORTand the others), with its value and where it came from:--agent-env, a.envfile, the host's environment or BenchFlow's default. - The run card shows the context window. It shows the window Claude Code assumes beside the window the model serves, and warns when the first is under half the second. Each Claude Code trial's
result.jsoncounts its compactions (compactions), and the after-run section counts the trials that compacted. - Each trial records the effort that ran.
config.jsonhasreasoning_effort_applied(what the agent's session took) andreasoning_effort_route(an effort the model route sets).result.json'seffortputs the requested and applied efforts beside the efforts the model calls carried, and a difference is a warning on the trial.CLAUDE_CODE_EFFORT_LEVELin the agent's environment is the effort Claude Code applies, over the run's. - Trials and jobs count failed model calls. Each trial counts its model calls by HTTP status, the failed calls the agent retried and the model proxy's own retries, and
summary.jsonadds them up. - Whether each task input reached the model. For each task input the agent read,
result.json'sinput_deliverysays whether its content reached the model, or only a placeholder such as "PDF file read: /root/proposal.pdf (47KB)". A placeholder is a warning naming the file and the route, and the job counts them. The system reminders Claude Code appends to a tool result are not counted as the file's content.
Changed
- Claude Code runs with claude.ai connectors and MCP servers turned off, and needs a sandbox user. Before each connection, on both harnesses, BenchFlow writes root-owned managed settings (
/etc/claude-code/managed-settings.json) that turn off claude.ai connectors and allow no MCP server, and checks them with its pinned CLI and launcher; user, project and command-line settings cannot relax them. So Claude Code needs a sandbox user that is not root:--sandbox-user noneis refused, as areNODE_OPTIONSandNODE_PATHin the agent's environment, a launch command other than BenchFlow's, and an image whose own managed settings conflict. A task that declares MCP servers is refused for Claude Code before it starts, since its servers would be blocked and it would run without its tools. Run such a task with an agent that takes task MCP servers, such as Codex or OpenCode. See Claude Code's connector policy. - A task.toml task's separate verifier gets only its declared artifacts from the workspace. When the verifier is built from
tests/Dockerfileand the task declares artifacts, the verifier's workspace keeps the files its image put there, and only the declared artifacts cross from the agent's, each at its path. A declared folder is emptied first, an artifact that is the workspace itself sends it whole, and a task that declares no artifacts still gets the whole workspace. The format gives such a verifier each declared artifact at its path, over its image's files; BenchFlow emptied the verifier's workspace and unpacked the agent's whole workspace into it. Terminal-Bench 4.1'slive-database-cutovergrades the agent's patch against a git baseline its image bakes into/app/api, anddistributed-dedupbuilds with its own build file, so both reference solutions scored 0. If such a verifier reads other files from the agent's workspace, declare them. The transfer record saysartifacts_onlyand lists the emptied folders. See What crosses. result.jsonandconfig.jsonwrite times with their UTC offset.started_atandfinished_atwere local times with a space before the time and no zone. They are now ISO 8601, with aTand the host's offset. A reader that parsed the old form must accept the new one; Python'sdatetime.fromisoformatreads both. BenchFlow reads both too, an old time as the writer's local time, so metrics and job exports work over older and newer results together.- A Codex trial fails when Codex's catalog lacks the model. When Codex finds no metadata for its model (
Model metadata for ... not found), it goes on with a fallback tool set (noapply_patch, no subagents), so the trial ran with tools that were not the model's, and BenchFlow only recorded the notice. On either harness the trial now stops at that notice, or at a fallback tool list in the first request, asagent_model_catalog: a setup problem, unscored and not retried.result.json'stool_surfacenames the tools the agent was offered, andsummary.jsonlists each tool set the job ran on. Use a model the pinned engine knows, or an engine whose catalog has it. codex-acpruns Codex CLI 0.159.3, as the native harness does. The adapter's own dependency (@openai/codex ^0.159.1) let an install on another day get another engine. The install now puts the pinned CLI in the adapter's own tree and checks that the engine it runs iscodex-cli 0.159.3; otherwise the install fails (Pinned Codex engine version check failed).- Claude Code asks for summarized thinking. BenchFlow passes
--thinking-display summarizedto the CLI on new and resumed ACP sessions, and native Claude Code asks for the same: recent Claude models leave their thinking text out unless asked, so trajectories held none. The thinking mode (adaptive or off) and its token budget stay the agent's. - A named job whose settings changed is refused. When the settings or a task's files had changed,
bench run --job,bench eval run --job-name,bench eval resumeandEvaluation(job_name=…)moved the job's finished trials to<job>/.set-aside/and ran them again, with only a warning. Such a run now stops before anything in the job is copied, moved or run. The error names each change with its old and new value (--agent-enventries by name only) and how many finished trials ran with the old settings, and the command exits 2.--restart-changed(restart_changed=True) restarts those trials on request, andsummary.jsonandrun-card.jsonrecord it asrestarted_after_change. An unnamed run is unchanged: other settings start a new job. - Codex refuses a reasoning effort the model doesn't offer. codex-acp used to fall back to
mediumwhen its session didn't list the requested effort for the model, and only the requested effort was recorded. The trial now fails before the agent starts, with the efforts the session offers (agent integration failure [agent_effort]); it is unscored and not retried. - A turn that ended on a failed model call is the provider's error. A trial whose agent's turn ended on a 429 or 5xx response is an unscored provider error, retried under the infrastructure retry policy, rather than a score. An ACP error after a 500, 502, 504 or 529 response is the provider's, as one after a 503 already was, when that response was the trial's last answered call: a failure the agent got past earlier is not why it stopped.
- A memory kill needs evidence. An agent process that exits is called a memory kill only when the sandbox's cgroup counted an out-of-memory kill.
- The idle limit's default follows the effort. A run that sets no idle limit gets 30 minutes at effort
xhighormax, and 10 minutes otherwise.bf.run, the SDK'srunandbench eval branchget the same default.--agent-idle-timeoutstill wins, and the run card says where the limit came from. A job resumed or rerun under its name without an idle limit keeps the one it ran with, so a job an earlier build ran at max effort keeps its 600 s and is neither refused nor restarted. A trial the idle watchdog stopped is recorded as ended by the harness.
Known issues
- Under
python3 -m pytest, a workspace module named like a standard-library module still loads first. Before grading, BenchFlow removes a workspacepytest,_pytest,pluggy,iniconfigorpymodule the task did not ship, but not one named like a module pytest imports after start-up (argparse.py, for example). A task can run thepytestconsole script instead, or setPYTHONSAFEPATH=1in test.sh. - The model proxy's lazy start is off with LiteLLM 1.103.3. It defers route analysis and the tokenizer load only for the LiteLLM, FastAPI and pydantic versions it was checked against, and this lock moved LiteLLM and pydantic; the proxy starts as LiteLLM does.
For maintainers
- A pre-release publishes through
public-release.yml. AvX.Y.ZrcN(oraN,bN) tag now passes the version check and goes to PyPI as a pre-release, with the same acceptance record and gates as a final release; its GitHub release is marked as a pre-release..devand local versions are still refused, and the preview workflow treats an a/b/rc version onmainas release staging. - The release leak gate names no partner. The code-token spellings it lets through for the partner rule now come from the private patterns (
partner-code-tokens); without that entry, every partner match is a finding.
After the 0.10.0rc1 wheel
20 more changes came after the 0.10.0rc1 wheel, though CHANGELOG.md lists them under [0.10.0rc1].
Packaging
- The sdist holds only what builds and describes the package. Its include list named
tests/, so the 0.9 sdist carried every test, fixture and captured run. It now shipssrc,README.md,CHANGELOG.md,LICENSEandpyproject.toml; the tests, their fixtures and the docs stay in the repository, and the docs reach users from the docs site.
Speed
- Measured on the workloads BenchFlow runs today (2026-11-04). On an idle 16-vCPU machine with Docker, 0.9.1 and this release ran the same tasks in alternating order with the
nopagent, three trials a task after a warm-up: nine CPU-only Terminal-Bench 4.1 tasks and three research and design tasks. Every task took the same time on both, within 0.3 s of its 12 to 27 s, phase by phase. Their task folders hold 1.7 to 29 MB, too little for the task-folder change above to show. The agent path costs more. With Claude Code on MiMo through the gateway (a hello-world task, three paired trials on a busy shared machine), the cloud metadata block took 6.5 s a trial against 0.4 s, now that it checks the image's file privileges, and the hardened final check 3.5 s against 1.7 s, while the sandbox was ready 2 s sooner.
Fixed
- A trial on a current model records its cost, and DeepSeek's cost is DeepSeek's. The gateway now reads LiteLLM's installed price table rather than downloading GitHub's moving one at startup. That table (LiteLLM 1.91.5) has no Claude 5.x, GPT-6.x, Gemini 3.5 or later, or MiMo V2.6 model, so a trial on one recorded no cost, and on a host that could reach GitHub a cost that depended on the day. BenchFlow now carries each vendor's published rates for 18 current models, read on 2026-11-04 with the date and source beside them, cache reads included. Haiku 5.5's prompts over 100,000 tokens cost its higher rates: LiteLLM's router drops that tier from a route's own prices, so it rides the route's
model_info. DeepSeek V4 routes used an unverified $0.28 / $0.42 per million placeholder; they now cost DeepSeek's peak rates, an upper bound, since off-peak hours bill half. Claude Code's own session cost was already known: an Opus 5.5 smoke on the rc1 wheel recorded $0.0473, its list price. - The no-web denylist has a refresh step, and knows the self-searching models LiteLLM added since the pin. A model that searches by itself has no tool to refuse, so a no-web run refuses it by name, and that list knew only the models someone had added.
python -m tools.hosted_retrieval_scannow lists the search-like models in LiteLLM's current price table that no entry refuses; on 2026-11-04 it found resellers' Perplexity deep research and search models and a Gemini search variant, now refused. Its test read the price table LiteLLM downloads at import, so it failed on any machine with network once LiteLLM added such a model; it reads the pinned table now. bench regradescores a separate-verifier trial again, and can rerun its rubric review, from saved outputs. Correcting scores after a grading bug meant running every trial again. A trial that ran with a separate verifier was refused; it is now regraded in a fresh sandbox built from the fixed task's verifier image, with the run's saved workspace, artifacts and include paths carried in as the run carried them, and no agent sandbox.--review(Python:review=True) also reruns the rubric review on the same saved outputs, with the reviewer the trial recorded unless--reviewer-*flags say otherwise, andregrade.jsonrecords the old and new combined scores and both rubric hashes. A separate-verifier run saves its workspace without--save-outputs, so such trials can be regraded already. A regrade of a rubric-reviewed trial compares the test with the test. Also: a--separate-verifier-includepath that no declared artifact covered failed every transfer (captured artifact /root has no declaration); it now crosses.bench bundlewrites one zip of a run's evidence to share. Per trial it holds the result, trajectory, verifier output, rubric reviews, regrades, artifacts and saved workspace, and for a job its run card, summary and report. Every text file is redacted as a trajectory upload is,bundle.jsonlists each file with its SHA-256 and what was masked, and the same folder gives the same zip. See Share a run's evidence.- Files the agent writes outside a saved workspace are named. When a task's agent wrote its answer to
/rootwhile the workspace was/app, the saved evidence a rubric review or a regrade reads had none of it, on Docker and on Daytona. When a trial saves its workspace (--save-outputs, a rubric review, a separate verifier), files written outside it during the agent phase are now listed, and the trial's warnings say how many there are by folder, with what to do: declare them in[[artifacts]], or set[environment].workdir. - Grading that ran out of time, and grading that downloads, are named. Tests still running at the verifier's time limit still score 0, since slow code can cause that, but the outcome lines now name these trials apart (
the tests were still running at the verifier's time limit). They point tobench regrade, which reruns only the verifier on saved outputs, andsummary.jsoncounts them asgrading_timeouts.bench tasks checkwarns when a verifier script downloads while it grades (pip install,uv pip install,apt-get,curlto a remote host,git clone) and says to install it into the image instead. - The ACP and native harnesses are checked for the model, effort and sub-agent tool they send. The deterministic tier now runs Claude Code (Opus 5.5 at max) and Codex (gpt-5.5 at max) on both harnesses. It checks every request's model and effort and the sub-agent tool, at the gateway and past it. Two fixes came out of it. The per-call record (
model-calls.jsonl) now names a Codex call's effort, which it had left empty. The native Codex harness now takes--reasoning-effort max, as codex-acp does; it had failed every such trial. One difference is known and kept: on Opus 5.5 the two Claude Code harnesses get opposite advice about Bash in their system prompts, because the ACP session runs in the default permission mode and the native one bypasses permissions. - A plugin guard refusal in a rubric-reviewed trial stays a failure. The review keeps the verifier error it refused as
ValueError: ..., and the guard's error was recognized only without that prefix. So a refusal whose path held words likeconnection resetread as an infrastructure failure: it was marked retryable and left the error-excluding pass rate. It is the guard's again: never retried, and a failure in every score. - A verifier that leaves no usable reward is an error, not a silent 0. No reward file, an empty one, one BenchFlow cannot read, or one with no number now gives the trial a verifier error,
verifier wrote no usable reward: <why>, instead of a 0 that reads as a wrong answer. The run exits 1, and the outcome lines name the cause (bench explain verifier_no_usable_reward). The trial is never retried, and it stays a failure in every score: the error-excluding pass rate and the scored-only mean count it as 0, as they do a plugin guard refusal, because the reward files are evidence the agent can reach.summary.jsoncounts these trials asunusable_rewards. A number outside the task's range still scores 0, named as before. Other zeros the verifier forces (a guard marker, a refusal marker) are counted asverifier_scored_zero:1 of the failed: the verifier scored them 0 on its own evidence, not on an answer.
What's new
- One record per model call. Every trial writes
trajectory/model-calls.jsonl, one line per model call from the source that saw it (BenchFlow's model proxy, else Claude Code's or Codex's own session log): the model asked for and the one that answered, the route, effort, thinking, sampling and output cap sent, why the answer ended (finish_reason,truncated,refused), tokens, cost and time, and a field the source cannot see is left empty.model-calls.jsontotals them per trial with where each figure came from, andbench eval metricsadds the job's calls, the ones cut at the output cap, refused or failed at the provider. JSON Schemas for both are indocs/reference/schemas/. - ATIF trajectories are read.
atif_readerreads ATIF v1.0 to v1.8, checks step order, sources, tool calls and the observations that name them, refuses other versions, and gives chat messages, one model-call record per call and totals; tool calls stay structured when the trajectory lists the agent's tool definitions.
Changed
- OpenCode 1.19.5 (from 1.18.11), BenchFlow's default rubric reviewer. Its effort option now also offers
defaultand it builds its reasoning variants itself; the model is still set before the effort. - OpenHands CLI at its last commit (
283c6ac, 2026-10-22, from2df8a28): it raises certifi to 2026.10.19, which distrusts the eMudhra and emSign roots. The CLI has been unmaintained since 2026-08-11 and still pins openhands-sdk 1.28.1, so the SDK stays there. - Pi 1.0.0 under pi-acp 0.0.34 (from 0.81.1). Its model catalog has Opus 5.5, Sonnet 5.5, Fable 5.1 and gpt-6.1-sol, which 0.81.1's lacks, and it is the last Pi release that ships a lockfile (1.0.1 and later float their dependencies).
gemini(Gemini CLI) is a legacy agent that takes an API key only. Google folded Gemini CLI into the Antigravity CLI and stopped serving AI Pro, Ultra and free accounts on it on 2026-06-18. A Google sign-in no longer stands in forGEMINI_API_KEY: the run stops at env resolution naming the key, instead of copying the sign-in's OAuth files into the sandbox for a run that could not use them (every Gemini run goes through the gateway, which needs the key).--agent antigravityis its successor.- Claude Code 2.1.316, and Claude Haiku 5.5 is the default model. The pinned Claude Code CLI moves from 2.1.280 to 2.1.316; claude-agent-acp stays 0.81.2, because 0.82 and later switch to a tool-call contract that drops Write and Edit inputs from the trajectory. 2.1.280 predates Sonnet 5.5 and Haiku 5.5, and its
haikualias meant Haiku 4.5, so a subscription run refusedclaude-haiku-5-5at connect. With 2.1.316 a subscription login is offered only current models:claude-haiku-5-5,claude-sonnet-5-5,claude-opus-5-5,claude-fable-5-1and their aliases.claude-haiku-4-5,claude-sonnet-5,claude-sonnet-4-6,claude-opus-4-8andclaude-opus-5are refused at connect, with the list; a run through the gateway with an API key is not affected. The default model (no--model),bench eval smoke's Claude model and the multi-agent authoring profile's reviewer are nowclaude-haiku-5-5andclaude-sonnet-5-5. Claude Code 2.1.316's Bash tool also refuses a standalonesleep Nand tells the agent to wait another way. - An LLM judge that names no model is Claude Sonnet 5.5, not the legacy Sonnet 4.6. This is a scoring change: a task without
[verifier.judge].model(or a rubric without[judge].model) is judged byclaude-sonnet-5-5, so its scores can differ from 0.9's. Nameclaude-sonnet-4-6there to keep comparing with earlier runs; Anthropic serves it until at least 2027-02-17. Sonnet 5.5 thinks unless told not to, and its thinking counts toward the judge's 2,048-token budget, so a 5-family judge now gets 16,000 tokens, and a verdict cut off by the budget is a judge error instead of an empty answer. bench skills validateuses NVIDIA SkillEvaluator 0.6.0 (it was 0.5.0). SkillSpector stays at 2.12.0, the release 0.6.0's own install instructions pin. Install withuv tool install --python 3.13 "skillevaluator[all] @ git+https://github.com/NVIDIA/SkillEvaluator.git@v0.6.0"; an older SkillEvaluator gets a version warning. On the 276 skills of SkillsBenchf860ecf0the findings are the same as with 0.5.0 (4,344 findings, every skill blocking at--fail-on high): none of those skills has arequirements.txt, apyproject.toml, a.mdcfile or a symlink, the inputs 0.6.0 changed. For a skill with apyproject.tomlthe dependency audit changes: 0.5.0 audited its own environment and reported that environment's CVEs as the skill's, 0.6.0 audits the exact==pins the skill declares, and a range or a URL is listed as an incompletepip-auditscan. See Skill checks.
[0.10.0rc1] - release candidate, not on PyPI
The first 0.10 release candidate. Its wheel and sdist are built and checked, but it isn't published yet: pip install benchflow==0.10.0rc1 finds nothing, the repository's releases page doesn't list it, and its source isn't public. If you have its wheel, install it with the constraints file built with it:
uv tool install --python 3.12 ./benchflow-0.10.0rc1-py3-none-any.whl --constraints constraints.txtor build the wheel yourself with uv build --wheel in a checkout of the release candidate's commit. Fixes it shares with 0.9.1 say "Also in 0.9.1".
Packaging
- The build backend is pinned (
hatchling==1.32.4). Built from the same source with hatchling 1.33.0 (released 2026-10-28), the 0.9.0 wheel differed from its accepted SHA-256 in one line ofWHEEL, so a release rebuilt later would stop at the exact-artifact check.
Speed
- Daytona teardown no longer waits 12 s for a model proxy nobody called. The in-sandbox LiteLLM proxy's stop polled its callback log until the whole 12 s settle deadline whenever the agent never sent it a request (a custom agent that brings its own endpoint), because it ignored the request and logged marks the host proxy already reads. It now reads the log size and both marks in one exec per poll and applies the host's rule: no request ever reached the proxy, so a quiet log is final after 0.5 s; every request has its whole record, so it is final at once; a marked request without its record, or a probe that cannot be read, keeps the old wait. On a 2026-10-28 Daytona run (40 Terminal-Bench 2 tasks, 80 sandboxes at once) this was 12 s of the 17.8 s median
cleanupper trial. - A trial's setup reads its task folder once. Before the sandbox was created, a trial hashed every source file twice to build a view of the task that two launch checks read only for its paths, walked and measured the whole folder for the kept-services digest before applying that digest's 64 MiB cap, and spent most of its digest time on one
Path.relative_toper file. The view now hashes its files only when a caller readssource_hashes(task export andTaskPackage.to_dict()), the kept-services digest stops listing where the files pass the cap, and both digests cut relative paths from the walk's own strings and hash files in chunks. Every digest is unchanged. On an 877 MB, 14,561-file task folder on an idle 16-vCPU machine,Rollout.setup()before the Daytona create went from 8.5 s to 2.5 s, and the run's own start before the trial from 5.0 s to 3.9 s. On Daytona, with 40 such trials at once from one 16-vCPU machine, the median setup before the create fell from 58.5 s to 10.2 s, and a trial reached a ready sandbox 36.5 s sooner (paired median over 20 tasks), with the same outcomes and digests. A task with a small folder, such as a stock Terminal-Bench 2 task, was already fast and is unchanged.
Fixed
- Claude Code runs straight against another provider's Anthropic-compatible API again. With the provider's own model id in
ANTHROPIC_MODEL(MiMo'smimo-v2.6-pro, for one), the pinnedclaude-agent-acp0.81.2 refused the id as a value of its model option, so every such trial stopped at connect asagent_model(0.9.0 and 0.9.1 too). When the agent does not list the model, the env names it and the endpoint is not Anthropic's, the option is now left unset:ANTHROPIC_MODELis the adapter's default. Against Anthropic, an unlisted model still fails as before. - The run card's control hint finds its controls. With no controls found, the card named
bench run <tasks> --agent oraclewithout a--jobs-dir, so a run inrc-jobs/claudegot its control in./jobsand the next card still found none; the hint now names the folder beside the run's (rc-jobs/oracle). A pass of--trialsover the task set is called a repeat on the card and the Matrix line, so a trial is always one attempt at one task. A dry run the card would refuse exits 1, andbench run --helplists--trials,--integrity,--accept-deviations,--max-cost-usdand--max-tokens, which the card names. - A multi-step task's soft checks leave their output. Each soft check's verifier log (or its error) is kept as
rounds/round-<n>-check.txt, named bycheck_outputinsteps.json. A failedmeantask's line gives each step's reward and where it came from, and the card notes ameantask, whose earlier steps score from soft checks. bench sandbox releasesays when Docker kept something. It ignored a network or volume Docker would not remove and reported the sandbox released; it now names what is left, and the sandbox stays kept so the release can run again.- Doctor no longer masks plain words from a login file. Every string in
~/.codex/auth.jsoncounted as a secret, so a Codex login with"auth_mode": "apikey"turned doctor's Google AI Studio link intohttps://aistudio.google.com/***. Mode, plan, scope and time fields are no longer masked; tokens and keys still are. bench preferences ranknames the trials the pairs left out (an empty output, an error), so an arm rated on one of its two outputs says so, and prints the rating scale. A vote that reuses a recorded vote id with other content says where the id came from and how to change an answer (a laterrated_at).bench preferencesreads the task's own preference table as documented. From the Frontier Design Benchmark acceptance run (J7): the table was dropped for a run of one task and taken from whichever trial came first;protocol = "rubric"still made pairs; an errored trial's empty output went into the blind set; the copies raters see kept each trial's file times, which date its run; and a task.md in front-matter form could not declarepreference:at all. Now the table is found for one-task runs too, kept per task version and left out (with a note in the store) when the task changed since the trials ran; a rubric task gets items to score and no pairs; errored trials and outputs with no files are left out; every copy carries one time and an absolute path;preference:is a task.md and task.toml key whose known settings are type-checked; and with fixed sidesrankno longer reads the left-side count as rater bias.- A Docker without buildx runs model trials and rubric reviews again. The legacy builder cannot build a cached layer's
RUN --mountand deletes its--iidfilefirst; the layer build read that file before it checked the build, so every trial that needed the in-sandbox model proxy, and every rubric review, stopped withFileNotFoundError ... benchflow-layer-*.iid. A failed layer build is now a layer error, and the trial installs the proxy or agent in the sandbox, as the layer cache intends. On Docker 29.1.3 without buildx, a no-network trial of pi-acp on DeepSeek Flash went from that error to 1.00. - A crashed rubric review is reported as the infrastructure's. The outcome line said
the verifier failed (task problem), and the summary warned ofa systemic verifier bug, for a review that crashed beside tests that passed. It now says the review or its sandbox failed and gives thebench eval score ... --forcethat runs it again. - A review that copied the schema's
$defskeeps its verdict. A v0.2 rubric's validator refused the whole review for that one extra key. - The no-web refusal names the model id you typed (
xiaomi/mimo-v2.6-pro, not the upstreamopenai/mimo-v2.6-pro), andBENCHFLOW_RESTRICTED_WEB_MODELSaccepts either. It leads with the verdict and the next step:Refused '<model>' with web access restricted: ..., thennext:. - Teardown on a busy Docker daemon says what timed out. A force-kill or image listing that outlived its timeout printed an empty reason, and the force-kill left its docker call running, so asyncio printed
Event loop is closedtracebacks once the job ended. Each call is now killed and waited for, and each warning names the step and what is left. - Default models their vendors no longer serve are replaced.
bench eval smoketried Codex withgpt-5.5, which left ChatGPT sign-in on 2026-10-14, and Gemini withgemini-2.5-flash, which Google has closed to new API keys. It now usesgpt-6-luna(on every ChatGPT plan) andgemini-3.5-flash-lite, which is also the default for--agent geminiwithout--modeland forbench review. The README's run-config example and the three*-gemini-flash-lite.yamlconfigs usedgemini-3.1-flash-lite-preview, which Google has shut down. A test names any default or shipped example that uses a model on the retired list. Also in 0.9.1. - The cloud metadata block no longer starts the agent when it cannot check the image's file privileges, and stock images still start. For an agent that runs as a user, the block checks the whole file system for set-ID files and file capabilities before the agent starts. Any of them could let a process outside the agent's launch gain root or NET_ADMIN and remove the block. Until now that check needed libcap's
getcapin the image. Withoutgetcap, and after a timeout, a failed command or an unreadable report, the check was recorded asunavailableand the agent started anyway. So Debian and Ubuntu's setuidsu, or a file such ascap_net_admin+epon a binary, went unnoticed. The result could also change from run to run:git-multibranchwas refused in three trials and started in one. A file that disappeared during the check (a temp file another process renames) made the check fail, and a failed check let the agent start. Now:-
A set-ID file the sandbox user owns is not counted when it grants nothing beyond the agent's own identity: setuid to the sandbox user, or setgid to a group it already belongs to. The agent may be asked to fix such a file. Setgid to any other group, and a file capability, still count.
-
BenchFlow reads file capabilities with its own Python. It uses its protected copy of the image's Python, else its managed CPython. When the image has no Python, it stages its pinned CPython 3.12.15 from the host, checked against its sha256. The image's
getcapis used only when none of these can be had. -
What the check counts is disarmed for the agent's turn instead of refusing the trial:
- Root records each file's mode, owner, inode, link count, size, sha256 and capability.
- It takes the set-ID bits and the capability off, then checks again. Nothing may count.
- Before grading, once the agent's processes are killed, root puts them back, but only on a file reached at its path without a link whose owner, size, sha256, link count and stripped mode are unchanged. The sha256 shows the content is the same, the owner that nobody re-owned it, and the link count that no hard link was added. The inode is not compared, so a sandbox restored from a snapshot (a branch) gets its files back too.
- Nothing goes back while a process that is not root runs without
no_new_privs, such as ansshdsession or an image'swww-dataservice; the records name it. Where files were disarmed, a kept service (x-keep-services) that runs withoutno_new_privsis killed instead of kept. - A restore that fails for BenchFlow's own reasons (a timeout, an I/O error) leaves the trial unscored, never 0.
cloud-metadata.jsonandresult.jsonname what was disarmed (delegates_disarmed) and what was restored (delegates_restored). A file whose content, owner, link count or mode changed stays disarmed while the verifier runs, so a test that needs its set-ID bit fails.delegates_not_restoredand a trial warning name it. -
A file that disappears during the check is skipped. These still stop the trial before the agent starts:
- a file that cannot be disarmed (a read-only mount, say);
- a check that fails or times out;
- an image where BenchFlow's own Python cannot run: a machine other than x86_64 or aarch64, a C library other than glibc or musl, or no
tar.
The message names each file and why it counts, for example
/usr/bin/su (setuid root)or/opt/x (file capabilities cap_net_admin=ep). The trial'scloud-metadata.jsonand thecloud_metadatafield ofresult.jsonrecord the state asrefusedbefore the trial stops. A refusal for something in the image is not retried. A refusal after a failed command is. -
To run a refused image anyway, remove the set-ID bit or the capability in the task's Dockerfile (
chmod ug-s <file>,setcap -r <file>). Or let the agent reach the metadata endpoints: setallow_cloud_metadata = truein the task's[environment]table, or run with--allow-cloud-metadata. -
All 28 Terminal-Bench 2 images we checked still start. Each ships
susetuid root, which is now disarmed while the agent runs and restored before grading.
-
- Assessment references must retain the sandbox recorded in the saved result. Collector assignments without a frozen rubric binding remain research history and cannot produce labels.
bench train run sft --loss-normalization sample_meanworks with prime-rl v0.10. prime-rl v0.10.0 moved the SFT loss into the model's output layer (prime-rl #3673), so BenchFlow no longer found the code it rewrites to average the loss per training example, and the--pack-function stackit required is a setting prime-rl 0.10 rejects; the trainer could not start. BenchFlow now recognizes v0.10.0 and prime-rl main through 2026-10-27: it takes per-token log-probabilities from the output layer and averages each example that prime-rl packed into a row. Leave--pack-functionunset there. prime-rl v0.5.0 to v0.7.0 get the same rewrite as before and still need--pack-function stack; without it the trainer now stops at startup, where BenchFlow used to refuse to launch. Any other version, v0.8.x and v0.9.x included, still stops the trainer, and the message now names the installed version, the supported ones and the first line that differs. See Per-example loss.- A score in
reward.txtbeside areward.jsonof details is scored (#1192). A verifier that writes its score toreward.txtand its details to areward.jsonwithout areward, as KUMO does (1and{"answer": "Empowering", "is_exact_match": true}), scored 0: the details were checked as a map of metrics, which allows only numbers.reward.txt's score now counts as that file'sreward, so the details that are not numbers are kept beside it and not scored, as they already were beside arewardinreward.json(#1178). A number among them is still a metric held to the reward range: KUMO's"seed": 3still scores 0, and the warning now says to write such details toreward-details.json. See Reward rules. Also in 0.9.1. bench results <job> --jsonreads a job 0.9 resumed from 0.7. Such a job keeps its old files in.pre-0.8/, asummary.jsonamong them, sobench resultstook it for a folder of jobs and--jsonexited 2. A hidden folder is no longer counted as a job. Found by the 0.9.0 first-run journeys. Also in 0.9.1.- A repository's own unchanged
conftest.pyis kept for grading (#1171, #1177). 0.9 removed everyconftest.pyin the workspace before the verifier ran, the repository's own included, so a correct answer whose tests need its fixtures scored 0 (fixture 'app' not foundon SWE-bench Verified's Sphinx and Flask tasks,pytesteron pytest's,long_dfon seaborn's). A workspaceconftest.pynow stays when it is byte for byte the file in the pre-agent copy and the agent ran as a user other than root; a planted or edited one still goes. A separate verifier sandbox has no pre-agent copy, so it still removes every workspaceconftest.py. - A workspace
pytest.pyno longer replaces pytest. When test.sh ranpython3 -m pytestfrom the workspace, an agent's/app/pytest.py(or a_pytest,pluggy,iniconfigorpymodule or package) ran as root in place of the grader, and an unsolved task scored 1.0. Before grading, BenchFlow now removes each such module the pre-agent copy of the workspace has no entry for; a repository's own stays. Also in 0.9.1. - The verifier no longer uses the agent's workspace as its home. With
WORKDIR /root, the verifier ran as root withHOME=/root, the agent's workspace. Files the agent left there ran or applied while it graded, such as the env script the uv installer's preamble sources (~/.local/bin/env) or a~/.curlrcthat adds a URL to an installer piped tosh. In SkillsBench's default set (87 tasks at f860ecf), 45 source that env script as root, and in 32 of them the workspace is /root; a planted one that writes the reward scored 1 on 32 of 32 tried with BenchFlow 0.6.3 and on 7 of 7 tried with 0.9.0. In real runs an agent that never saw the expected answer scored 1 both ways. When the agent could write/root, the verifier now gets a new root-onlyHOMEwith the XDG folders inside it and empty.curlrcand.wgetrc, so both runs score 0 and the oracles still score 1. When/rootis root's alone (the workspace is elsewhere),HOMEstays/root, so the image's own files there still work. Tests that read the agent's output through~withWORKDIR /rootmust use its path, or setHOME = "/root"in[verifier] env, which keeps the old behaviour and its risk. pip's (PIP_CONFIG_FILE=/dev/null) and git's per-user config were already off on 0.9. Also in 0.9.1. verifier.restore_workspaceis refused with what to do instead. It was never a task setting (BenchFlow never puts the whole workspace back, which would delete the agent's output). Tasks reached for it to keep inputs the agent could edit honest; the refusal now says to check their digests in test.sh against a list in the tests folder. Also in 0.9.1.- openhands runs a model on Daytona again. openhands is installed from git with uv, which leaves a git lock file in BenchFlow's uv cache (
git-v1/locks/<hash>) writable by everyone. The step that takes write off uv's lock files looked only for*.locknames, so the in-sandbox model proxy refused the cache ("BenchFlow's uv cache … is not root's alone") and the trial failed before the first prompt. Found by the 0.9.0 first-run journeys; 0.9.0 has the same fault. Also in 0.9.1. pi-acpconnects again. Its install took@mariozechner/pi-coding-agentunpinned, which stopped at 0.73.1, deprecated in favour of@earendil-works/pi-coding-agent(same maintainers), while thepi-acpadapter it installed beside it (0.0.34) needs Pi 0.81.0 or later:session/newnever answered, so everypi-acptrial failed at connect. Both are now pinned:@earendil-works/pi-coding-agent@0.81.1andpi-acp@0.0.34(as public #1158). Also in 0.9.1.
What's new
- A run card before every run. Every job writes
run-card.jsonand logs a short card before its first trial: the tasks and their source commit, the agent and the packages it installs, the model and effort, the trials per task and in all (--trials Ncounts every trial), concurrency, the agent and verifier time limits, the network policy, how grading runs, the oracle andnopcontrols already run on the same task content, the longest the run can take, and any budget. A task set can name the setup its published numbers used inrun-reference.tomlbeside its tasks: the card lists every setting where the run differs, and lists the ones BenchFlow does not set for agent trials (temperature, top_p, thinking, a step limit) for you to check. A reference withstrict = truerefuses a run that differs before its job folder exists, unless the run says why with--accept-deviations(accept_deviations:in a run config); the note is kept on the card. Control runs are never held to the reference.--dry-runincludes the card. See the run card. - The run card names the vendor's sampling; a running job says how busy it is and what sampling it sends; every job writes a report. For a model whose vendor publishes a recommended setting BenchFlow has on file (so far Qwen3.8-27B and the 2025 Qwen3 models), the card's Vendor line gives it beside the agent's own sampling, with the page it was read from, and says when the task set's reference asks for other sampling; any other model gets a line telling you to read its model card. On a terminal the card now prints above the live dashboard before the first trial (it reached only
run-card.jsonthere before), and--dry-runprints it on stderr beside its JSON plan. Each request's temperature, top_p, top_k, min_p, presence penalty, output cap andchat_template_kwargs(which turns a Qwen model's thinking off on vLLM) are kept in the trial's model trajectory. Two minutes into a job, then every five minutes and at its end, the log and the dashboard show the slots running (in setup, with the agent, grading), the tasks waiting and how much of the agents' time a model request was in flight, with a warning when slots sit empty while tasks wait, when setup holds most of them, or when the model waits on the agents most of the time; the first such line with requests on record checks the sampling they carried against the Vendor line and the reference. Every job writesreport.mdbesidesummary.json, counting trials as the summary does (a retried task is one trial; a rubric pass below reward 1 passes): per task, the trials, passes, a 95% interval and each try's reward, then the planned tasks that never finished, tokens, cost, trial time and the sampling the requests carried; a--trialsrun writes one over all its repeats. These were added after a baseline was launched with eight tries per task where the paper used one, thinking off against the vendor's setting, and one of four GPUs busy, each noticed only after launch. See The run card. - Multi-step tasks run. A task.toml with
[[steps]]gives the agent one turn per step in one sandbox, each step scored by its own tests (steps/<name>/tests, else the task's); a step below itsmin_rewardends the task, the last step that ran is verified with full hardening, andmulti_step_reward_strategyisfinal(default) ormean. The oracle runs each step's solution, andnophas each step's tests score the untouched workspace.steps.jsonrecords every step, and the log names each step with its reward and ends with how the task's reward was made from them; the last step that ran keeps both its soft and its final reward. Before, every such task was refused. See Multi-step tasks. - Human preferences, ranked (
bench preferences).bench preferences pairspairs the outputs of finished trials (the same task version, different agent or model) into a blind assignment for any rating tool: opaque ids, shuffled sides with a recorded seed, and with--bundlea copy of each output under its id.bench preferences importtakes the votes back (left, right, tie or abstain, optionally per rubric aspect) and rubric scores per output, from JSON lines or CSV. It checks the whole file against the store first (known pairs and items, criteria of the frozen rubric, scores in range, times after the pairs were issued, outputs unchanged), then appends each vote as a hash-chained canonical JSON line; re-importing adds nothing.bench preferences rankgives Bradley-Terry ratings on the Elo scale with bootstrap intervals, per aspect, rubric means per arm, and, printed first, rater checks: the votes counted and left out, left-side share, agreement with the chance agreement and Cohen's kappa beside it (againstmin_agreement), and coverage againstraters_per_pair.pairscopies every output under its opaque id into a folder beside the store (or--bundle), never into the store, which maps items to agents; it says which trials it left out and why (a trial without a task digest among them), andimportchecks those copies too. With--by agentor--by model, votes between outputs of one arm are neither fitted nor counted. A conflicting vote is named by its file, line, rater and pair. Votes are never rewards. task.md's[preference]table no longer stops a run;bench preferencesapplies it. See Human preferences. - Keep a failed trial's sandbox and attach to it.
--keep-sandbox on-failure(oralways) onbench runandbench eval runkeeps a trial's sandbox instead of deleting it, and writeskept-sandbox.jsonbeside its result.bench sandbox attach <trial>opens a root shell in it (or runs a command after--), andbench sandbox release <trial>deletes it. Docker and Daytona. A kept Docker sandbox is marked on the Docker daemon (abf-kept-<project>volume), so a leftover sweep run with another HOME, by another user of the machine or by CI, spares it too. The kept sandbox is the graded state: the agent's processes were stopped and the workspace frozen before grading. See Look inside a failed sandbox. - Agents that bring their own model endpoint. A custom agent's manifest can declare
model_route = "agent"(manifest contract 1.1). BenchFlow then starts no model proxy for that agent. On Daytona that saves about 25 seconds of setup per trial (a 2026-10-28 run measured a median of 17 to 19 s to install the proxy and 6.7 s to wait for it), and 1 GB of sandbox disk. A model becomes optional, and a model you pass is only a label. The agent gets only the keys it declares. You give up BenchFlow's token and cost metering and the model trajectory: these trials recordmodel_route: "agent"and usageagent-reportedornone, and are never counted with metered ones.--usage-tracking requiredand tasks that restrict the agent's network (no network, an offline agent, an allowlist) are refused before any sandbox starts. Contract 1.0 manifests load unchanged. See An agent with its own model endpoint. - Blind pairwise assessments, beside the outputs.
bench assessandbenchflow.assessmentturn two completed rollouts of one task into a durable, hash-chained sidecar: frozen rubric and judge protocol, proof that every sandbox was released, the review collector's blind assignment and vote, corrections and retractions, and a deterministic label (1,0,0.5, or withheld for an abstention) inbenchflow.assessment-label.v1. It is opt-in: execution rewards and existing result, regrade and rollout-stream formats remain unchanged. See Assessments. - The egress proxy logs what it lets out. Under
denylistandallowlist, every request or tunnel the proxy forwards is written totrajectory/egress_allowed.jsonl: anallowedline before its first byte leaves (method, URL with its port and without its query, the rule that admitted it:allowed-host,allowed-range,task-service,model-gateway,native-model,not-denylisted) and aclosedline when it ends (status, bytes each way, duration), joined byid. A request the proxy cannot log is not forwarded. Refusals stay inegress_denylist.jsonl, which keeps its meaning. See Egress logs. - An allowlist can name the task's own Compose services. On
dockerandremote-docker, a service name inallowed_hostsadmits that container's addresses on the networks it shares withmain, one address each, read from the daemon at every connection. A private range no longer has to be listed, and the rest of that network, including its gateway (the Docker host), stays closed.
Changed
- An empty
allowed_hostsis a policy.network_mode: allowlistwith no hosts parses and admits only the agent's model endpoint, so a task whose services are its own runs with no outside host at all. It used to be refused at load. - An allowlist task's services must stay inside the sandbox. A running Compose service on a network that is not
internal: true, on the host's network, or in the network namespace ofmainor of a routed container, stops the trial before the agent or oracle starts (sandbox startup refused: network_mode='allowlist' needs the task's services kept inside the sandbox), not retried. The agent could otherwise have such a service fetch what the allowlist refuses. Put the services on an internal network thatmainjoins beside its own. A named service with no address on a networkmainjoins after 30 seconds also stops the trial (retryable). The documented recipe of listing a service's name with172.16.0.0/12no longer runs for a service ondefault. - An allowlist task with a compose file is refused on
daytona, before any sandbox starts: onlydockerandremote-dockerread the project to check its services.
[0.9.2]
0.9.1 with five grading-integrity fixes from the 0.10 line, without its new features. Each comes with a test that fails on 0.9.1.
Security
- An offline agent can no longer resolve names. Under
agent.network_mode: no-network, the agent user's uid firewall admitted all loopback traffic, and Docker's embedded resolver answers on loopback (127.0.0.11) and forwards to the host's. The agent resolved any name, and a name it made up carried data out. The firewall now refuses the agent's DNS on loopback, as an allowlist already did (its DNS goes to the allowlist's filter). A denylist agent's resolver is unchanged. - A
script:agent underagent.network_mode: no-networkruns offline. It had the sandbox's whole network: its sandbox got noNET_ADMINand no uid firewall was armed. It now gets the sandbox and the loopback-only firewall a model agent gets, DNS refused as above. - A
script:agent under an open network can't reach the cloud metadata endpoints. A model agent's uid gets the block there, but an operator script, which runs as the same user, ran without it. It now starts only once the block is proven on, as a model agent does, andcloud-metadata.jsonrecords it.--allow-cloud-metadatastill opts out. - Root's git in
test.shno longer runs the agent's hooks, filters or aliases. Grading makes the workspace root's, so root's git trusted the.gitfolders the agent wrote: a hook, anfsmonitorcommand, a filter or driver, an include, an alias or acommondirpointer ran as root whentest.shran git, and a wrong answer could score 1. Before grading, BenchFlow now removes each one the repository lacked before the agent's turn (all of them in a separate verifier), and the grader's git runs with hooks,fsmonitor, external diff and signing off. Atest.shthat needs one passes it withgit -c; a task whose answer is a hook or a driver setscleanup_git = falsein[verifier.hardening]. - A separate verifier no longer loads the agent's files through the image's
LD_PRELOADorLD_LIBRARY_PATH. A value naming the workspace (ENV LD_LIBRARY_PATH=/app/buildis common in C and C++ tasks) passed the check before the transfer, when nothing was there yet, and BenchFlow's root steps then loaded the agent's files from it. Values naming the workspace or/logs/artifactsno longer reach root's commands there, as in the agent's sandbox.
Known issues
- Under
python3 -m pytest, a workspace module named like a standard-library module still loads first. Before grading, BenchFlow removes a workspacepytest,_pytest,pluggy,iniconfigorpymodule the task did not ship, but not one named like a module pytest imports after start-up (argparse.py, for example). A task can run thepytestconsole script instead, or setPYTHONSAFEPATH=1in test.sh. - 0.9.1's two known issues remain: the model proxy's lazy start is off with LiteLLM 1.103.3, and a repository's own
conftest.pyin the workspace is still removed before grading (#1171;cleanup_conftests = falsekeeps it).
[0.9.1]
0.9.0 with fixes for problems found after its acceptance. Each was fixed first on the 0.10 line or on public main, with a test that fails on 0.9.0 and a real run.
Security
- LiteLLM 1.103.3, and patched TLS and HTTP clients (as public #1196). LiteLLM 1.91.5 carried two advisories (GHSA-7hp6-4w63-5g45 and GHSA-g5ff-637f-6q2m), a streaming stall when a cached upstream handler was collected mid-stream, and a
TypeErrorin the cleanup of a disconnected stream whose request had"metadata": null. The floors are nowcryptography>=50,certifi>=2026.10.19andaiohttp>=3.14.4, and oauthlib is 4.0.1 (CVE-2026-49272):pip-auditof the lock and of both proxy pin files the wheel ships finds nothing. The Bedrock adaptive-thinking patch and the Gemini pass-through patch follow LiteLLM 1.103's calls. Bedrock Invoke's system-message patch (#1135) now runs on top of LiteLLM's own handling: it still keeps a mid-conversation system message in place only on the models Bedrock was seen to accept one from, with only its content, and moves Claude Code's cache breakpoint off trailing system messages. The restricted-model allowlist is rebuilt from the new model map, and the no-web guard refuses every deep research model, whatever its provider calls it: 1.103's map adds Perplexity's deep research models under another provider's name.
Fixed
-
A long streamed thought is merged in linear time (as public #1165). Text runs were joined with
+=per chunk, so a thought of 20,000 chunks took 33 ms per live snapshot; they are merged with one join. -
A cancelled run's trajectory keeps its last updates (as public #1165). The live trajectory is written at most every 250 ms, and a cancelled run never reached the final write, so the file missed up to 250 ms of updates; the held-back tail is now published when the run is torn down.
-
gemini on Daytona starts again (#1208). Daytona refuses SSH access longer than 1440 minutes, and 0.9.0 asked for 2880, so every launch on the SSH transport failed with
expiresInMinutes must be between 1 and 1440. -
A root-owned reward on rootful Docker is read (#1202). A verifier that wrote its reward as root with mode 0600 on mounted outputs scored 0 without an error.
-
A workspace
pytest.pyno longer replaces pytest. When test.sh ranpython3 -m pytestfrom the workspace, an agent's/app/pytest.py(or a_pytest,pluggy,iniconfigorpymodule or package) ran as root in place of the grader, and an unsolved task scored 1.0. Before grading, BenchFlow now removes each such module the pre-agent copy of the workspace has no entry for; a repository's own stays. -
pi-acp connects again (as public #1158). It installed the deprecated
@mariozechner/pi-coding-agent(0.73.1), whosesession/newnever answered. Pinned to@earendil-works/pi-coding-agent@0.81.1andpi-acp@0.0.34. -
openhands runs a model on Daytona. uv's git cache lock was left writable by everyone, so the in-sandbox model proxy refused BenchFlow's uv cache and the trial failed before the first prompt.
-
A Docker without buildx runs local trials. Its legacy builder cannot build a cached layer's
RUN --mountand deletes the image-id file when it fails, so a trial that needed the model proxy's, the agent's or the verifier's layer stopped withFileNotFoundErroronbenchflow-layer-*.iid. A failed layer build is now reported with the build's last lines, and the trial installs in the sandbox instead. -
A score in
reward.txtbeside areward.jsonof details scores (#1192). Details that are not numbers are kept beside the score; a number among them is still a metric held to the reward range. -
bench results <job> --jsonreads a job 0.9 resumed from 0.7. The job's.pre-0.8/backup made it look like a folder of jobs, and--jsonexited 2. -
The verifier no longer uses the agent's workspace as its home. With
WORKDIR /rootthe verifier ran as root withHOME=/root, so an env script the uv installer's preamble sources (~/.local/bin/env) or a~/.curlrcthe agent left there ran while it graded. In SkillsBench's default set (87 tasks at f860ecf), 45 source that env script as root, and in 32 of them the workspace is /root; a planted one that writes the reward scored 1 on 32 of 32 tried with BenchFlow 0.6.3 and on 7 of 7 tried with 0.9.0. When the agent could write/root, the verifier now gets a new root-onlyHOME(XDG folders inside, empty.curlrcand.wgetrc). A root-only/rootstays. Tests that read the agent's output through~withWORKDIR /rootuse its path, or setHOME = "/root"in[verifier] env. -
verifier.restore_workspaceis refused with what to do instead: check input digests in test.sh. -
Default models their vendors no longer serve are replaced.
bench eval smoketried Codex withgpt-5.5, which left ChatGPT sign-in on 2026-10-14, and Gemini withgemini-2.5-flash, which Google has closed to new API keys. It now usesgpt-6-luna(on every ChatGPT plan) andgemini-3.5-flash-lite, which is also the default for--agent geminiwithout--modeland forbench review. The README's run-config example and the three*-gemini-flash-lite.yamlconfigs usedgemini-3.1-flash-lite-preview, which Google has shut down; they usegemini-3.5-flash-lite. A test names any default or shipped example that uses a model on the retired list.
Known issues
- The model proxy's lazy start is off with LiteLLM 1.103.3. It defers route analysis and the tokenizer load only for the LiteLLM, FastAPI and pydantic versions it was checked against, and this lock moved LiteLLM and pydantic; the proxy starts as LiteLLM does.
- A repository's own
conftest.pyin the workspace is still removed before grading (#1171), so a correct answer whose tests need its fixtures scores 0, as on SWE-bench Verified's Sphinx, Flask, pytest and seaborn tasks. Fixed in 0.10.0rc1. On 0.9.1,cleanup_conftests = falsein the task's[verifier.hardening]keeps them, and also keeps anyconftest.pythe agent planted, which can then change the result.
Packaging
- The build backend is pinned (
hatchling==1.32.4). Built from 0.9.0's source with hatchling 1.33.0, the wheel differed from its accepted SHA-256 in one line ofWHEEL.
[0.9.0] - accepted, never published (0.9.1 replaces it)
BenchFlow 0.9.0 rebuilds and expands the Python SDK and CLI after 0.7.11. Python remains the main implementation, with the static Linux bfx helper for sandbox operations. The Rust engine, released on its own (bench 0.8.1 is its latest release), is a separate implementation. Upgrading from 0.7? Read What's new in 0.9 first.
Before you compare scores with a 0.7.11 run. These changes can move a score:
- Solve rate, pass@k, pass^k and mean reward now count every trial. A trial with no score (an error, a verifier crash) counts as 0. The 0.7 figures stay beside them as
*_scored_only. - A reward outside the task's range is no longer clipped. It scores 0 with a
reward_out_of_rangewarning. - Reported verifier guard problems score 0 with a named warning. Automatic rubric review still requires a completed verdict; capture/export failures may remain unscored even after script verification. See the scoring contract.
- A pytest plugin that test.sh installs after the agent stops is never loaded. Pin it in
[verifier] pytest_plugin_releases, or install it into the image. - The verifier's pip reads no configuration file. Pass index or wheelhouse options in test.sh.
- Running the same command again resumes a matching job based on the recorded settings BenchFlow can compare. Missing legacy fields remain unknown; use a fresh job for a fresh comparison. See Resume.
- Several kinds of task that were graded wrongly are graded correctly now; see Fixed.
What's new
- One command set.
bench new,check,import,run,branch,results,view,regrade,export,doctorandagents, the same verbs as the Rust engine.bench runtakes everybench eval runsetting, and without-ait runs the oracle. The older spellings (bench eval run,bench tasks check, …) still work and print one note with the new spelling. - Errors say what happened, whose problem it is and what to do next. Most errors end with a
next:line.bench explain [CODE]explains why a trial has no score. On commands that accept--json(such asexplain,resultsandhub), errors are JSON objects on stdout (error,code,owner,next,detail,exit_code). bench checkrefuses whatbench runrefuses, before any sandbox starts: a task feature the chosen sandbox cannot run (code R401), or a judge or proxy extra that is missing.- Services the agent leaves running. A task can name up to 8 TCP ports in
x-keep-services. The agent's processes listening on them survive the kill before grading, paused while the grading inputs are closed. Published Terminal-Bench 2 tasks that need this (hf-model-inference, pypi-server and 8 more) are recognised by their content. - Grade any run in a separate sandbox.
--verifier-sandbox separate(or--separate-verifier) runs every task's verifier in a fresh sandbox that receives only the workspace and the files the task declares.--separate-verifier-include PATHcarries outputs from outside the workspace. A task it cannot run is refused before a job folder exists. - Agent-judged rubric criteria in task.md draft 2 tasks. The judge reads the saved submission, can run it as an unprivileged user with no network, and records each session in
verifier/judgments.jsonl. - Job configs with a matrix of agents and datasets. A job config that lists
agents,datasetsandn_attemptsruns every agent on every dataset,n_attemptstimes each, and writesmatrix-summary.jsonwith pass@k per cell. A setting BenchFlow cannot honour exactly is refused by file, line and key. bench cache listandbench cache cleanshow and remove BenchFlow's cached agent, proxy and verifier layers, its Daytona content snapshots and the task images it pulled.cleanremoves nothing without--remove.- Agents. Meta Muse Code (
muse-code). Antigravity's agy 1.3.5, with any Gemini API model. OpenCode on Gemini with its own Google client. mini-swe works again (mini-swe-agent 2.4.6, every dependency pinned).claude-agent-acp0.81.2 with Claude Code 2.1.280, which runsclaude-opus-5-5.codex-acp2.0.1, which knowsgpt-6.1-sol. Each release pins the agents catalog to one commit. - Also new since 0.7.11: Docker hosts you control (
--sandbox remote-docker), regrade without rerunning the agent, reward integrity audits, a native harness for Claude Code and Codex, branching, budgets, rollout streams for trainers, task formats and embodied rollouts. Each is described in detail below. - Records you can read back.
timing.jsonnames more steps (agent install, proxy start, each hardening step, cleanup, waits). Every job and trial records the BenchFlow version that made it.cloud-metadata.json,kill-before-grading.json,locked-system-path.jsonandverifier/trusted-path.jsonsay what the sandbox did. - Install. A new install guide; every release tag carries a
constraints.txtwith the exact dependency versions it was tested with;bench doctorsays whichbenchyour shell runs, this one or the Rust engine's. - Dependency security updates. The lock, matching constraints and packaged model-proxy requirements include the refreshed dependency versions. Use the release's matching constraints for those pins. Package metadata keeps its existing minimum versions, so unconstrained installation is a separate resolution, not the locked environment.
- Patched installer trust store. Sandbox installers and CI use checksum-pinned uv 0.13.4 instead of 0.13.1, removing the affected emSign root certificate (GHSA-hq85-5cpj-mr62). The managed Python remains pinned to 3.12.15.
- Agent dependency security updates. deepagents keeps its agent version and updates certifi from 2026.7.22 to 2026.10.23. The pinned harvey-lab harness updates anyio 4.13.0 to 4.14.2, certifi 2026.2.25 to 2026.10.23, cryptography 46.0.6 to 50.0.2, idna 3.11 to 3.16, lxml 6.0.2 to 6.1.0, pillow 12.2.0 to 13.0.0, pyasn1 0.6.3 to 0.6.4, soupsieve 2.8.3 to 2.9 and urllib3 2.6.3 to 2.8.0. Both complete requirement lists retain exact versions and artifact hashes; the harness source commit is unchanged.
Speed
A pre-release review measured build 2b5c678e6 against dc509087a and the Rust engine 0.8.2 on three Terminal-Bench 2 tasks (fix-git, polyglot-c-py, pypi-server), using mini-swe-agent 2.4.6 with recorded model replies on Docker, one trial at a time. Each arm ran three measured rounds after a warm-up. Median harness time, including test.sh, was 21.69 s for this build, 25.24 s for the earlier build and 22.65 s for Rust. The paired change was −3.30 s against the earlier build (95% interval −3.55 to −3.10 s), and −1.56 s against Rust (−1.89 to +1.55 s); the Rust interval includes zero. Excluding test.sh, the medians were 13.23, 15.64 and 16.88 s. About 0.4 s of the measured reduction came from the setup-only CPU lift becoming available on this path. These are review measurements of that build, not final-release measurements.
-
Installs are built once and reused. On Docker, the in-sandbox model proxy and the pinned agent installs are built into cached image layers per task image, shared by every BenchFlow process on the machine. On Daytona, the agent and model-proxy layers are used when this process already built them, or when the plan reuses the task image (
--trialsabove 1 orBENCHFLOW_DAYTONA_LAYER_REUSE=1). One trial builds them; concurrent trials install in their sandbox instead of waiting. A single trial with no known layer installs in its sandbox. A new process does not assume Daytona retained a previous build; opt-in content snapshots have their own reuse rule.--fresh-install(orBENCHFLOW_FRESH_INSTALL=1) installs in every trial instead. uv and Python in the sandbox are pinned and checksum-verified, so images without Python work too. -
Setup CPU only. On Docker's direct path, BenchFlow can temporarily raise the CPU limit while installing the agent. It restores and reads back the task's declared limit before launching the agent; the agent and test.sh keep the task's CPU and memory limits.
-
Docker trials do less. A single-service task starts and stops through the Docker Engine API instead of a dozen
docker composecommands (BENCHFLOW_DIRECT_DOCKER=0keeps Compose). Root's setup and verifier steps go through one command channel. The verifier's preparation is one script, and independent steps before the verifier run side by side. Containers stop together, and a finished trial's sandbox is removed in the background while the next trial starts. -
Preparing the next trial. With more tasks than
--concurrency, the next trials' sandboxes are prepared while the current ones run (BENCHFLOW_WARM_POOL=offturns this off). Missing task images are pulled when the job starts. On Docker, each trial's host model proxy starts while its sandbox starts. -
More trials at once. Each Docker trial network takes a small subnet, so
--concurrencyabove about 30 works on a stock daemon. Above--concurrency 16, trial starts are paced without holding a slot. -
A warm local runtime:
bench daemon start. Opt in withBENCHFLOW_DAEMON=autoto keep BenchFlow and its model proxy loaded between non-interactive calls.--prepare Nprepares sandboxes for the next identical call. -
CLI imports. It imports only the command that runs.
-
Daytona polls for command results and sends commands of 30 s or less in one request.
-
The verifier's tests run at their own speed. coverage.py no longer traces them unasked.
BenchFlow prepares its protected test runner ($BENCHFLOW_PYTEST) only for a task whose tests folder or [verifier] env names it (see Fixed), so other tasks skip copying the image's site-packages for it. A task that names the runner only inside its image gets none (see Known limitations).
For six Terminal-Bench 2 tasks (count-dataset-tokens, password-recovery, polyglot-c-py, sanitize-git-repo, sparql-university, vulnerable-secret), a setup-only Daytona check used mini-swe with a loopback endpoint and no prompt. At build 56ca86da8, the cold single-trial median was 123.9 s using fresh installs; the first round building layers for four planned trials was 136.3 s, and a warm round was 49.6 s. The earlier build dc509087a had median 124.9 s across four rounds. These include setup, start, agent installation, connection and cleanup, not an agent turn or verification; they are provisional measurements of those builds.
Fixed
Grading.
- A repository's own
conftest.pyfiles are kept for grading; only the ones the agent added are removed. SWE-bench Verified's Sphinx, pytest, seaborn and Flask tasks are graded correctly again. matplotlib's SWE-bench tasks are graded on Daytona again (#1168). Correction: in 0.9.0 and 0.9.1 a repository's ownconftest.pyin the workspace was still removed before grading, so a correct answer whose tests need its fixtures still scored 0; fixed in 0.10.0rc1 (#1171). - test.sh runs under the interpreter its first line names, else bash. SWE-bench Verified and SWT-bench scripts whose
#!/bin/bashis not on line 1 used to run under dash. - The pytest plugin guard trusts the image's own plugins under Debian's
root:stafffolders (#1176), a plugin a freshuv initproject lacks (#1175), and every wheel of a pinned release. A test.sh that adds pytest-json-ctrf withuv addand uses--ctrfis graded again (#1170): BenchFlow suppliesctrffrom its protected verifier runtime. Other unpinned plugins installed by test.sh after the agent stops are refused; a reported refusal scores 0 with a named warning. - On images without Python, the verifier keeps the image's root-owned tool folders (cargo, mvnd) on its
PATH(#1174). Each folder it leaves out is listed inverifier/trusted-path.jsonwith the reason. - A
reward.jsonwith extra fields that hold no number scores (#1178). - Daytona Compose tasks that mount the task folder find their files there (#1181).
- An ACP agent's final reply is written to
/logs/agent/output.txt, where registry verifiers read it (#1182). OpenCode can write its temp folder (#1183). - A task with a small disk no longer runs out of space installing the proxy and the agent (#1169).
- The workspace freeze keeps the image's own symlinks, including a venv's link to its interpreter, and removes the ones the agent added.
- The oracle can use git repositories the image built inside the workspace (Terminal-Bench 2's
git-leak-recoveryscored 0). - The Python SDK's
Agent + Environmentform keeps its reward on Docker.bf.run(bf.Agent(...), bf.Environment.from_task(..., sandbox="docker"))with the task's own test script wrote the reward to one folder and read it from another, so the run came back with a missing-reward error although the verifier had scored it. This was present since 0.5. The rollout now uses the environment's own folders, andEnvironment.from_task(..., jobs_dir=...)chooses where they go. - An agent's installation no longer changes the task's environment: mini-swe no longer installs a system
python3into the task image, and codex-acp and OpenHands no longer leave aumask 077for the agent's commands. - With no reported plugin-guard problem, a run is scored from test.sh's reward, even when no pytest reached the guard. Terminal-Bench 2's
qemu-startupandqemu-alpine-sshcan therefore score 0 when test.sh fails before pytest starts. A reported guard refusal, crash, incomplete check or load failure scores 0 with a warning; aregisteredmarker cannot override it. The absence of a reported problem does not prove that the guard loaded. - When a left-out
PATHfolder is listed inverifier/trusted-path.json, its reason names the rule that left it out (/tmp/tools: under /tmp), not the folder again. - BenchFlow prepares its protected test runner (
$BENCHFLOW_PYTEST) only for a task whose tests folder or[verifier] envnames it. This holds on every backend, whatever user the agent runs as, and in a separate verifier sandbox too. Preparing it copied the image's wholesite-packagesinto the sandbox (5 to 8 GB on a torch image), and Terminal-Bench 2'smteb-retrieve,pytorch-model-recoveryandmteb-leaderboardran out of disk on Daytona. No Terminal-Bench 2 test.sh calls the runner. A task that names it only in a file inside its image gets none (see Known limitations). - A Python start-up hook that only root could have written during the agent's turn is kept for grading: owned by root, closed to group and others, in folders only root can write up to
/, outside the workspace and the other agent-writable trees, and with one name (a link count of 1). A solution that pip-installs a package as root keeps its.pthfile; Terminal-Bench 2'slargest-eigenval(eigenpy'scmeel.pth) scored 0. On Docker, wherebfxdoes the clean-up, the trial log names the first five hooks it kept and their total count; on other backends, and on Docker withoutbfx, the line goes to the step's own error output, which the trial records do not carry. The rule applies only when the agent runs as a non-root user in its own sandbox. When the agent runs as root, or in a separate verifier sandbox, the earlier rule stays: every new or changed hook is removed. - A program added to a
PATHfolder during the agent's turn whose route leads into the workspace stays on the verifier'sPATHwhen its name was not in that folder before the turn. Root records eachPATHfolder's names before the agent starts. Terminal-Bench 2'ssqlite-with-gcovsolution links/usr/local/bin/sqlite3to/app/sqlite/sqlite3. The agent's user cannot add a name to such a folder, so the name was added by root (the task's solution or a service) or, in a folder another user owns, by that user. The verifier then runs, under that name, whatever is at that workspace path after the turn, which can be the agent's file. A name the image already had is still refused, whatever it points to now, so an image cannot hand the verifier apython3orpytestthat runs the agent's files. The rule applies only when the agent runs as a non-root user in its own sandbox; with a root agent, in a separate verifier sandbox, or when the record could not be taken, such a program is left out as before. A kept program is not listed inverifier/trusted-path.json. - When BenchFlow's copy of the image's Python fails once, it is tried again, and a failed copy is not cached (
usaco/987lost its protected Python on Daytona).
Keeping the agent away from its grade and from your secrets.
- The kill before grading freezes the agent's processes first, so fork loops and processes that keep changing their pid cannot outlast it.
- An agent's launch cannot gain privileges through setuid programs such as
sudo, or take control of the firewall. An image that would hand the agent the firewall is refused before the agent starts. - Under the default open network, the agent's user cannot reach the cloud metadata endpoints. The agent, a root agent included, starts only once that block is proven; otherwise the trial stops before the agent. The one exception is a root agent on Daytona, AgentCore or Apple container, where the provider's own metadata policy applies and the trial logs a warning (
require_cloud_metadata_block = truestops it instead). A Modal sandbox has no route to the endpoints: each trial proves it with a connect to every endpoint and recordsnot_reachable. A task about the metadata service itself opts out withallow_cloud_metadata. - A task's files read a variable of your environment only when you name it (
--allow-task-env NAME). A Compose file that mounts, reads or builds from your machine is refused unless you setBENCHFLOW_ALLOW_TASK_HOST_ACCESS=1. - An agent starts with only the credentials of the route BenchFlow chose. Logins for other agents and providers stay out of its sandbox. One case is not covered yet: a task that keeps services and has more than one role (see Known limitations).
- When the agent runs as its own user (the default), BenchFlow's own root steps after the agent's turn never start the image's Python. They use a copy of the image's Python and standard library, made and sealed before the turn and always started isolated (
-I -S -B) from/. This includes the egress proxy underdenylistandallowlist(its start, its health check and its certificate lookup). The installer of the pytest plugin guard starts no interpreter at all: it reads where each interpreter looks from a record taken before the turn. An interpreter missing from that record, or changed since, gets no copy of the guard; it reaches the guard only through the verifier'sPYTHONPATH, and the trial log has abenchflow:line naming it. A separate verifier sandbox takes that record before the agent's workspace is copied in. Code that test.sh itself runs is the task's (see Known limitations). - A task that lets the agent switch to other accounts (
x-agent-identity-switch) is refused on sandboxes that cannot keep no-new-privileges on the agent's container (native Daytona, Modal, AgentCore, Apple container). - Files BenchFlow copies out of the sandbox (verifier output, artifacts, judge evidence) are published without following links or crossing mounts the agent could have planted.
- The model proxy reads no
.envfile it happens to find (see Changed). - One list of shell start-up variables for every sandbox. The loader and shell start-up variables (
LD_PRELOAD,LD_LIBRARY_PATH,BASH_ENV,ENVand 9 more) are emptied for BenchFlow's own root shells and given back only to the task's own commands, on every backend; Daytona used to refuse such images. An image or sandbox environment that sets an exported bash function (BASH_FUNC_name%%),SHELLOPTSorBASHOPTSto a value is refused at start on every backend, because bash would run it as root before any command BenchFlow sends: on Daytona an image function did run as root in root's shells. The message names the variables, never their values. An empty value is accepted. - On Daytona, a root command too large for one request is staged in a folder that is checked to be root's alone at the moment of use, and it runs whole or not at all.
- On Docker, root's steps after the agent no longer need the image's Python. BenchFlow now ships
bfx, a small static helper, in the wheel (Linux x86_64 and aarch64, pinned by hash), and mounts it read-only at/.bfxin every Docker sandbox, local, remote or Docker Desktop. The kill before grading, the workspace capture, the workspace's change of owner, clearing/logs/verifier, the start-up hook clean-up and root's command channel run on it. So the hook clean-up also runs on images without Python and after the agent removedpython3.BENCHFLOW_BFX_DELIVERY=offturns it off; a machine of another architecture runs without it. Daytona, Modal, AgentCore and Apple container keep their earlier steps. A task that mounts anything at or below/.bfxis refused. - Where
bfxis not delivered, the workspace freeze leaves a file that has a second name with its owner unchanged, asbfxdoes. A start-up hook the agent's user owns and hard-linked into the workspace stays the agent's, so the hook clean-up removes it even when the workspace'sconftest.pysweep has removed its second name first. For kept services, the close step renames root-owned copies over names that still refer to non-root files, including files with several hard links, and checks the final owners and modes.
Runs and results.
-
A root-owned link to a missing native library no longer causes a sealing refusal when only root could create its target. This fixes the loader refusal on Terminal-Bench 2's
sam-cell-seg; it does not promise its verifier passes. An unsafe library search path is refused before agent installation, with the path, owner, mode and remedy. -
Daytona guard setup records its steps and package-manager output in
daemon-guard.log; a timeout names the last step.timing.jsonrecordsdaemon_guardseparately. -
A pending model request counts as activity for up to three idle budgets; the wall deadline still applies. If a request never answers and outlives the idle budget, the result names the pending provider stall. Invalid provider responses and sandbox preparation failures have specific categories; these labels preserve the prior retry and resume decisions.
-
Run logs prefix each trial line with its task.
--quietkeeps warnings, errors and the final score. Network request chatter and routine protected-runtime messages move to DEBUG; a missing protected Python is recorded in the trial'swarningstoo. -
Argument parsing errors exit 2.
bench runalso uses 2 for an unknown agent, unsupported sandbox or empty task selection; missing credentials, missing sandbox extras and run failures exit 1. Some subcommands retain different exit rules; check the command's help and the CLI reference. -
bench hub show terminal-bench@2.0lists the 89 tasks;bench hub listdefaults to BenchFlow's dataset registry. Non-interactive trajectory commands fail with the argument to supply instead of waiting for input. -
A fresh unnamed rerun with a narrower task selection starts a new job and names the tasks left out. A named job retains its existing tasks and warns that the score includes them.
-
A stopped job records a partial summary, including
--summary-out. Result readers distinguish running, interrupted and never-started trials. A rollout stream reports a stopped job after yielding its finished rollouts. -
An unfilled task scaffold and malformed task configuration are reported before a sandbox build. A cold offline verifier cache gets a network/cache remedy; a checked warm cache still works offline.
-
total_cost_usdis null when no trial reported cost. Treat it as unknown, not a free run. Training conversion skips control runs, and reports zero rows for a control-only job. -
A closed terminal or a dropped SSH session stops a run cleanly and removes its sandboxes (exit 129).
-
After a run is killed (
kill -9, out of memory, a preempted VM), the next Docker job,bench eval resumeorbench sandbox cleanupremoves its containers and networks. -
A dataset run (
-d <dataset>) whose--includeor--excludeselects no task is refused instead of running the whole dataset. -
A
--matrixrun no longer stops after the first job with an error, and always writesmatrix-summary.json. -
A run no longer hangs when Python's garbage collection cleans up a finished sandbox while another trial stages its task.
-
Copies out of the sandbox get a deadline that grows with their size, and a failed copy is recorded instead of losing the manifest.
-
Daytona trials are scored again. On an earlier 0.8.3 candidate, a Daytona run of Terminal-Bench 2 scored none of its 238 trials: the verifier's preparation went out as one shell argument, over Linux's 128 KiB limit, and the read of the image's users lost its NUL bytes. Every backend now hands its provider at most 64 KiB per command. A larger command travels as data (on the command's input, or as a root-only staged file on Daytona) and runs whole or not at all. Folders move as one archive, so a tests folder of 2,000 files or more uploads on Daytona, and a Compose environment of 16 KB or more works on Docker-in-Docker. BenchFlow's 14-task transport check (oracle and nop) scores as expected on Docker and on Daytona. Trials lost at start under load are covered by the next entry.
-
Daytona starts hold up under load, and Daytona decides when a sandbox is abandoned. With 64 trials starting at once, the readiness check lost 13 of them before and loses none now. The run's preflight check no longer counts the Daytona SDK's import against its time limit (with many runs starting at once, that import alone could pass the limit and stop the run), and only a rejected API key stops a run. A sandbox that fails to start is retried with a fresh one only where that can help (Daytona reports
ERROR, or a build with no fallback failed); a cached install layer that fails to build is built once, then the plain task image is used. Sandboxes are created with Daytona's auto-stop at 15 minutes without activity (it was 24 hours), and BenchFlow keeps a sandbox active while a run holds it. Automatic clean-up deletes only sandboxes Daytona has already stopped, archived or failed and that no live run holds; a running one is reported, never deleted.bench sandbox cleanup --allfollows the same rule,--id IDdeletes the sandbox you name in your owner scope whatever its state (it also applies the age rule to your other idle sandboxes), and--max-agekeeps its warning. Sandboxes a run could not delete are listed in~/.local/state/benchflow/daytona-leftovers.jsonl(BENCHFLOW_DAYTONA_LEFTOVERS), whichbench sandbox cleanupreads. A trial whose sandbox Daytona stopped says so: "Daytona stopped the sandbox (SANDBOX_NOT_RUNNING). It stops a sandbox after 15 min without activity from its controller: was this process paused, asleep or offline that long?" -
Every volume a Docker trial causes is named and labelled when it is created. The image's
VOLUMEpaths and Compose's anonymous volumes become named volumes labelledio.benchflow.trial-volume=<project>, and teardown, the force-kill fallback, verifier recovery andbench sandbox cleanupremove the trial's volumes by that label, after a failed start too, locally and on remote Docker. A volume the trial did not create (yours, an external or adopted one, a--mountsource, BenchFlow'sbenchflow-bfx-*) is never removed, and nothing is removed while a container still uses it. If an image or avolumes_fromsource cannot be read, the start fails instead of guessing.
Every fix, with its reasons and examples, is listed in detail below.
Renamed
Public names that named another harness, or its reward package, now have neutral names. No old name is kept as an alias: an old CLI value is refused with the ordinary unknown-value error, which lists the values that work (--target 'x' is an unknown value; choose one of: task-toml), an old preset is refused as an unknown profile, and an old module or class no longer imports. The old runner environment variables are silently ignored; update all three below.
| Where | Before | Now |
|---|---|---|
bench export --target (and bench tasks export) | the value named after the harness whose task.toml split layout it writes | task-toml (still the default and the only value) |
export-report.json | target: that same value | target: task-toml |
bench hub list --provider | the provider named after that harness's registry | benchflow |
bench hub check --registry default | that harness's public registry.json | BenchFlow's registry: benchflow-ai/skillsbench's registry.json, then the registry packaged with BenchFlow (Terminal-Bench 2.0, 89 tasks at a pinned commit, with digests) |
bench hub check --cache-dir default | .cache/hub/ and that harness's name | .cache/hub/registry |
bench hub check output | headings naming that harness | "Registry check (<registry>)" and "check registry tasks" |
bench hub check records | a framework field naming that harness, hub_url, and an env_uid prefixed with its name | registry (the source, or benchflow), and env_uid = <dataset>/<task>@<ref> |
| task.md preset | the compatibility preset named after that harness, with its compatibility.<name> block | task-toml-compatible, with compatibility.task-toml |
| Python | the round-trip conformance report class named after that harness | TaskTomlRoundTripConformanceReport |
| Python | the round-trip mismatch class named after that harness | TaskTomlRoundTripMismatch |
| Python | the round-trip report builder named after that harness | build_task_toml_roundtrip_conformance_report |
| Python module | benchflow.hub. + that harness's name + _registry, with its task ref, load, select, check, uid and materialize functions and its registry and hub URL constants | benchflow.hub.registry: RegistryTaskRef, load_hub_registry, select_registry_tasks, check_registry_tasks, registry_env_uid, materialize_registry_task, BENCHFLOW_REGISTRY, DEFAULT_CACHE_DIR |
| Verifier strategy | the previous strategy name, in each of its spellings (one word, with a hyphen, with an underscore) | criteria-runner; every previous spelling gets the unknown-value error |
| task-package fields | the reward-package field and source value | criteria_runner (hybrid field and rubric source); criteria-runner (strategy type) |
| task-package fields | a source naming that harness; a step runner named after it | source: legacy (what bench import writes); runner: custom-step-runner |
| Python module | benchflow.adapters. + the previous harness name | benchflow.adapters.task_toml; no alias |
| Adapter class | the previous harness name in title case + Adapter | TaskTomlAdapter; no alias |
| Adapter function | from_ + the previous harness name + _dir | from_task_toml_dir; no alias |
| Adapter source | the previous harness name in InboundTask.source, compatibility.source and materialised benchflow.compat.source | task-toml; no alias |
| Runner environment | BENCHFLOW_ + the previous rewards-package name in uppercase with an underscore + _ROOT | BENCHFLOW_CRITERIA_RUNNER_ROOT; no alias, the old variable is silently ignored |
| Runner environment | the same old prefix + _CRITERIA | BENCHFLOW_CRITERIA_RUNNER_CRITERIA; no alias, the old variable is silently ignored |
| Runner environment | the same old prefix + _MANIFEST | BENCHFLOW_CRITERIA_RUNNER_MANIFEST; no alias, the old variable is silently ignored |
| Runner manifest | the previous hyphenated strategy name + -manifest.json; its version and root key | criteria-runner-manifest.json, version benchflow.criteria-runner.v1, paths.criteria_runner_root; no alias |
| Authoring profile | the profile named after the previous rewards package | criteria-runner, entrypoint verifier/criteria_runner/reward.py; no alias |
| Example task | docs/examples/task-md/ + that harness's name + -parity | docs/examples/task-md/task-toml-parity |
| Dataset id in the docs | Terminal-Bench 2 under that harness's GitHub organisation | resolve_source("terminal-bench-2"), from BenchFlow's registry |
The repository's comparison tests and integration suites against that harness are removed; they were not part of the package.
These changes came with the renames:
- Datasets come from BenchFlow's registry only.
-d name@versionreads BenchFlow's published registry and the entries packaged with BenchFlow, and nothing else.terminal-bench@2.0is packaged: 89 tasks at a pinned commit, each with a content digest that is checked. A dataset BenchFlow's registry does not have stops with "Dataset '…' not found in BenchFlow's registry" and names the next step (bench hub list --provider benchflow, or--registryfor a registry that has it). - Only job folders BenchFlow made are resumed. A job folder is resumed only when it carries a BenchFlow marker:
evaluation.json, thesummary.jsona 0.7 job writes, a trialconfig.jsonwith a 0.8 or 0.7.x trial's keys, or a trial result that is a BenchFlow rollout result (rollout_name). The job lock is not a marker. A 0.7 job stopped before any trial finished now resumes. A named run refuses any other folder with files (<folder> is a folder BenchFlow did not make; nothing in it moves); an unnamed run starts a new job beside it. bench checkreads a verifier's model use only from what the task declares ([verifier] models = true, a provider key the verifier env names, or the judge files an import recorded). A test.sh that calls a grader with none of these declared no longer gets the warning.- Integrity action records count unknown tool calls with no input in
unmapped_kinds, instead of skipping one foreign event name. - The Python export API takes only
task-toml.export_task_to_split_layout,build_compatibility_export_reportandbuild_task_toml_roundtrip_conformance_reportrefuse any other target with the CLI's unknown-value error.
Known limitations
-
Kept services can retain an open writer to an existing workspace file. Changing a single-name file's owner and mode does not revoke a descriptor a service already opened for writing. The close step replaces non-root files, including shared hard-linked files, but does not copy every root-owned file. A kept service can still change such a file while tests read it. Use a separate verifier when its inputs must not change during grading.
-
Closing shared files for kept services can consume extra disk. The root-owned copy writes sparse files in full: a sparse file with a logical size of 4 GiB can require 4 GiB of disk. Without a protected Python, the fallback file walk can also fail on paths longer than 4,096 bytes; this stops hardening rather than accepting the workspace.
-
Tasks written for a root agent can fail with the default user. The agent runs non-root while the verifier normally runs as root. It cannot install system packages or write under
/usr,/etcor/opt; packages installed in its user site may be invisible to root's tests. The historical Terminal-Bench 2.0 scan found root-only writes in 33 of 89 reference solutions; five tests read those writes directly (build-pmars,build-pov-ray,mailman,nginx-request-logging,sqlite-with-gcov). The other 28 depend on the chosen solution; this scan does not establish that every alternative solution needs root.headless-terminalalso failed when the agent installedpexpectonly for its own user.--sandbox-user noneruns the agent as root and permits those writes, but root can undo the PATH lock, protected runtime and workspace freeze and change what the verifier runs. Use it only for trusted agents; keep its scores separate from non-root runs. -
Some task images cannot pass the protected runtime's loader checks. If the native loader searches or loads libraries from a folder another user can write, BenchFlow refuses its root steps and the trial is unscored, including the oracle. Terminal-Bench 2's
make-mips-interpreteris one such task. Use an image whose library files and search folders are root-owned and closed to other writers. -
Ten of Terminal-Bench 2's 89 tasks keep services that native Daytona cannot freeze for a non-root model agent. These trials are refused:
configure-git-webserver,git-multibranch,hf-model-inference,install-windows-3.11,kv-store-grpc,mailman,nginx-request-logging,pypi-server,qemu-alpine-sshandqemu-startup. Use Docker or remote Docker. The oracle and nop do not have this model-agent refusal; their separate image and verifier limitations still apply. -
What BenchFlow does not isolate. BenchFlow treats test.sh, and everything it runs or installs, as the task author's code, as BenchFlow's Rust engine and other harnesses do. BenchFlow protects the verifier's environment, not its logic, and it does not isolate code that test.sh installs, builds or imports from files the agent wrote: a
pip install -e .of the workspace, a requirements file or wheel there, apyproject.tomlthatuv syncreads, or a module that test.sh's Python imports from the workspace, such as apytest.pythe agent left in/appwhen test.sh runscd /app && python -m pytest. Such code runs as the verifier's user, usually root, and can change the result, for example by patching pytest, standing in for it, or writing the reward file. The pytest plugin guard still refuses a plugin from such an install, but that is not isolation, and a run in which no pytest reached the guard (test.sh ended before pytest, checked without pytest, imported apytestthat is not pytest, or ran pytest where the verifier's environment does not reach, as withenv -i,sudo, ssh or another container) is scored as test.sh wrote it. -
What task authors should do. Install test tools into the image or pin them, run the tests with
$BENCHFLOW_PYTEST, do not run Python from the workspace folder, and install nothing from the agent's files. -
Where BenchFlow takes the task's word. (1) It prepares the protected runner only when the task's tests folder or
[verifier] envnames$BENCHFLOW_PYTEST, on every backend and for every agent user, a separate verifier sandbox included. The next two apply only when the agent runs as a non-root user in its own sandbox; with a root agent or in a separate verifier sandbox, the earlier rules stay. (2) It keeps a Python start-up hook that only root could have written during the agent's turn (as a root-run solution'spip installwrites). (3) It keeps on the verifier'sPATHa program in aPATHfolder whose route leads into the workspace, when that name was not in the folder before the turn: root (the task's solution or a service) added it, or, in a folder another user owns, that user did. The verifier then runs, under that name, whatever is at that workspace path after the turn, which can be the agent's file. What the image already had before the turn, and anything the agent's user could have written outside the workspace, is judged as before. Refusing or isolating these installs remains follow-up work after 0.9.0. See Sandbox security. -
Three checks of the protected runtime's native libraries remain incomplete. After the agent, BenchFlow runs its root steps with a protected copy of the image's Python. Three of its checks on native-library search paths are incomplete, and each needs a task image that already points a search path into a folder the agent can write:
- Python extension modules copied into the protected runtime keep their own library search paths (RPATH and RUNPATH).
- The check of a tool's library search path splits paths on spaces (
/opt/shared libsis checked as/opt), and on musl images it does not read RUNPATH folders. - The protected Python's own native dependency search, and plugin preparation and image start-up hooks, are not yet fully covered.
0.7.11 ran the image's own Python for these steps, so none of these is new. Their closure remains follow-up work after 0.9.0.
-
On an image with no
python3at all, the step that removes workspace links pointing outside the workspace is skipped, and the trial log says so. It is not refused, so Go, Rust and C tasks stay scored. -
On a sandbox other than Docker, an image whose only
python3is one the agent can remove (a uv-only image's alias) has its start-up hook clean-up skipped when the agent deletes it, and the trial log says so. On Docker,bfxdoes this step. -
Reconnecting while kept services retain previous-connection processes is refused. With
x-keep-services, BenchFlow cannot prove that every process from the previous role or scene stopped without also killing the services. It refuses a later connection before its probes, proxy setup or credential staging. Use a fresh rollout; the first connection and its existing session remain available. Ordinary tasks still stop the agent user's processes between connections. -
A task that keeps services cannot be graded on an image without Python when the agent runs as a non-root user. BenchFlow cannot check the kept services' workspace there, so the trial is refused with a named message and has no score. Terminal-Bench 2's
configure-git-webserveris such a task: its oracle run scores, and a run with an agent under the default non-root user (nopincluded) is unscored. -
A task image tag that someone retags during a Docker start can leave one volume behind. BenchFlow reads the image's
VOLUMEpaths when the start begins. If someone retags or pulls that tag on the same daemon in between, to an image that declares anotherVOLUMEpath, Docker gives that path an anonymous volume, and teardown does not remove it. BenchFlow's own builds use private tags, so this needs an outside retag. Remove such a volume withdocker volume rmonce its container is gone. -
A task that calls
$BENCHFLOW_PYTESTonly from a file inside its image, not from its tests folder or[verifier] env, receives a stub that prints an error and exits 4. The trial follows test.sh's reward, usually 0. Name the runner in test.sh to prepare the protected runtime. No Terminal-Bench 2 task calls it. -
On Docker, an agent can make the hook clean-up fail, so the trial is unscored and retried instead of scored. Where
bfxdoes the clean-up, its answer about the files it walked is one line of at most 256 MiB: a non-root agent that leaves about 90,000 files with start-up hook names makes it longer, and the step fails as a verifier infrastructure error. Under a Compose file that drops theDAC_OVERRIDEandDAC_READ_SEARCHcapabilities, an agent folder set to mode 000 fails the step the same way, where the shell clean-up skipped it. The shell form, used on the other backends, passes both. -
Every trial keeps a root-only copy of the workspace as it was before the agent's turn (
/testbed_verify), which the workspace checks after the turn compare against. It costs disk in proportion to the task's workspace: at most 112 MB among the 24 Terminal-Bench 2 images inspected in the historical review, but a task whose image ships a workspace of several GB can run out of space on a small sandbox, such as Daytona's 10 GB default. -
On Modal without BenchFlow's protected runtime (
--sandbox-user none, on an image where the agent can write/opt/benchflow), a folder upload can be raced so that root changes the mode of a link's target to 700. Nothing runs, but it is a mode change made as root. -
A folder made to look like a 0.7 job is resumed as one. The 0.7 trial marker checks that a trial
config.jsonhas all four 0.7.x keys, not what they hold. No other tool writes them, so only a folder built to pass does. -
A named run that refuses a folder BenchFlow did not make takes its job lock there first, so the folder's modification time changes and a lock guard file stays beside it in the jobs folder. Nothing in the folder moves. A run killed at that moment can leave
.evaluation.lockinside it. -
The packaged Terminal-Bench 2.0 entry's repository URL redirects on GitHub to the dataset's current home. Its tasks are fetched at the pinned commit and each is checked against its digest, so a different redirect target cannot change what runs.
-
An image or sandbox environment that sets an exported bash function,
SHELLOPTSorBASHOPTSis refused (see Fixed). The refusal is reported as an infrastructure error, and on Daytona it is retried twice, so three sandboxes start before the trial fails. A task's own[environment] envthat sets one is refused on Docker and Docker-in-Docker, and accepted on Daytona and Modal, where it never reaches root's shells. None of the 22 base images successfully inspected in the historical review set one; this is not a guarantee about other images. -
When the agent runs as root (
sandbox_user: null), there is no protected copy of the image's Python: root's steps and the plugin guard's installer start the image'spython3, isolated from/, as 0.7.11 did. Never run an untrusted agent as root. -
Under
denylistandallowlist, an image BenchFlow cannot copy a Python from stops before the agent with an infrastructure error: one with no root-onlypython3in/usr/local/bin,/usr/binor/bin(a conda or uv-only interpreter), or whose standard library others can write. Native Claude login on a no-web task stops the same way. -
A Python that appears or changes during the agent's turn (a solution that apt-installs
python3) gets no copy of the plugin guard. If pytest reports that it cannot load the guard, the trial scores 0 with a warning naming that Python. On an image withoutstat, every Python counts as changed. Absence of a reported failure does not establish that this interpreter loaded the guard. -
A separate verifier sandbox builds BenchFlow's protected runtime in every trial. The historical eight-task Docker measurement added roughly 1.5 to 2 s on a loaded host; that is neither total runtime setup time nor a universal cost. A verifier image that cannot hold it (no root-owned
/bin/sh, nosha256sum, or an/optothers can write) fails at setup. -
Some installs still add system packages to the task image under
allowlistanddenylistpolicies: the in-sandbox model proxy, OpenHands' tool bootstrap and the Toolathlon adapter apt-install curl, openssl and ca-certificates. -
The verifier gets the image's
LD_LIBRARY_PATH(and similar loader variables) empty. An image tool that needs its own library folder can exit 127 in the verifier. Where the backend records the service image environment, the trial warns about the recorded values it dropped. A backend or service without that record has no such warning. -
A wrapped Codex agent (
acpx/codex,acpx/codex-acp) does not receive an inline subscription login passed asCODEX_AUTH_JSON. Use the plaincodex-acpagent. -
Ignoring Python's bytecode caches in the verifier costs time. No
.pycthe agent planted can run, but every Python process in the verifier compiles what it imports. In the historicalfix-code-vulnerabilityexperiment, this added about 0.6 s per pytest process; other import workloads and machines have different costs. -
The
gpt-5.4family loses some tools under codex-acp 2.0.1 (see the known regression in Fixed, in detail).
In detail: every change since 0.7.11
Fixed
-
No image Python is started as root after the agent's turn. With a sandbox user, each connection's egress proxy under
denylistandallowliststarted three of the image's Pythons as root, after the agent's turn too: its health check, the proxy itself and the certificate fallback. An agent's/app/urllib.pyran as root in the health check. All three now run on root's protected copy of the image's Python, started-I -S -Bfrom/; a protected runtime without a Python stops the start (exit 87). The pytest plugin guard's installer no longer starts any interpreter: it installs the guard from a record, taken before the turn, of where each verifier interpreter looks (an entry must end withend, and an interpreter recorded as another file, or changed, counts as unrecorded). An unrecorded interpreter gets the guard through the verifier'sPYTHONPATHand abenchflow:line naming it. A separate verifier sandbox takes the record before the workspace is copied in, and without a kit it never starts an interpreter under the folders the agent can write. The source scan that finds root starts of an image Python now reads interpreters held in shell variables,$(command -v python...), f-strings and module constants. -
Daytona exec transport. What a backend hands its provider per command is at most 64 KiB, counted after every wrapper and checked at one place (AgentCore: 60,000 JSON bytes). A larger command, its environment included, goes as data and a fixed reader line runs it in the same shell, user and route: on the command's input on Docker, remote Docker, Apple container and Modal; on Daytona as a file in root's mode-700
/opt/benchflow/run, used only after the folder is proven root's alone and not a link, and deleted after; on Docker-in-Docker the inner command on input and an oversized VM command staged too; on AgentCore as a sealed file. A payload ends with a per-command end line, and one cut short exits 126 with nothing run. Command output that may hold NUL bytes comes back as a checked hex receipt. Directories move as one tar archive with one open file at a time (Daytona, its Docker-in-Docker VM and Modal); an image withouttarfalls back to batches of 100 files. A single environment value over 128 KiB is still refused by the kernel. -
Without a reported guard problem, the task's reward remains authoritative. A missing
registeredmarker alone no longer refuses the trial. Terminal-Bench 2'sqemu-startupandqemu-alpine-sshtherefore score the 0 that test.sh writes when setup fails before pytest starts. A signed adverse guard verdict, an incomplete check or pytest's guard-load failure message gives score 0 with a named warning, even beside aregisteredmarker. Absence of that evidence does not prove loading: code test.sh imports can suppress the guard or change its reward files. The task's test script and the code it chooses to execute remain trusted. See the threat model. -
coverage.py no longer measures the verifier's tests unasked. The verifier ran with
COVERAGE_PROCESS_STARTset to empty, meant to keep coverage.py from reading a config file, butcoverage.process_startup()starts measuring whenever the variable is defined, even empty. So a test package,sitecustomizeor.pthfile that calls the hook ran its tests under the tracer, and wrote a.coveragefile into its working folder. The verifier's command now unsetsCOVERAGE_PROCESS_STARTandCOVERAGE_PROCESS_CONFIG(which coverage.py 7.10+ reads first), so the hook returns before it reads or writes anything. -
An agent's installation no longer changes the task's environment (mini-swe, codex-acp, OpenHands). mini-swe's install apt-installed Ubuntu
python3into the task image when it had none, so the agent's commands found a Python the task never had; this changed command lookup and could make a correct solution fail on a Python-free task. mini-swe now uses a private pinned CPython under/opt/benchflow(agents #81), fetched without installing system packages, and never on the tool PATH. codex-acp and OpenHands wrote their credentials underumask 077and kept it, so the agent's files were created 0600 where the image's umask is 0022; the umask now applies only to those writes. A Python-free-image regression covers mini-swe, claude-agent-acp, codex-acp and OpenHands, fresh and cached. -
A closed terminal or a dropped SSH session stops a run cleanly, exit 129. SIGHUP killed
benchat once and left the run's sandboxes running. It now stops the run as SIGTERM does: the running trials stop, their sandboxes (the warm pool's prepared ones, and a sharded run's workers' too) are removed, and the exit is 129. A run started undernohupis unaffected. While the sandboxes are being removed, a second Ctrl-C stops at once, a second SIGTERM (or a Ctrl-C after SIGTERM) is ignored and the next one stops at once, and a SIGHUP never does (a closed terminal sends it twice). A sharded run's parent now stops its worker processes and waits for their cleanup; before, a stopped parent left them running. -
A model that never answered before the agent's wall-clock budget ran out is an integration failure in the run's own records too. The run-time integration check judged only timeouts that carry no message, and BenchFlow's own wall-clock timeout always carries one. So such a trial was written as
error_category: timeoutwith reward 0, andsummary.jsonand the end-of-run score counted it as a scored 0, whilebf.load_job,bench eval metricsand the viewer read the same trial as an unscored integration failure. A wall-clock timeout after no tool call, message, thought or output token is nowagent_integration(no_activity) when it is written: the reward is withheld,summary.jsoncounts it underintegration_failures, andagent_timeout_infostays. Like the timeout, it is not retried. An idle-watchdog timeout keeps its category while the job runs, because it is retried, and is judged when read. When a result has both, the integration failure now decideserror_category. -
An adapter's own warnings are recorded as notices, not as the agent's words. BenchFlow now declares ACP session notices (
clientCapabilities.session.notices), so codex-acp 2.x and claude-agent-acp 0.81+ send what they compose themselves (a Codex warning or model reroute, a Claude model fallback or SDK warning) asnoticeupdates; without it they wrote it into the agent's reply. On a model missing from the pinned Codex catalog (gpt-5.4, or gpt-5.4-nano), Codex warns as the turn starts, so a provider that never answered still left one agent message, and the trial endedtimed_outand scored 0 instead of an unscored integration failure. A notice is now its own trajectory event (type: "notice"withseverity,titleanddescription): never the agent's answer, never activity for the idle watchdog or the integration check (whose evidence quotes it), a notice card in the viewer, and asystemstep in ATIF; training exports leave it out. A typed-failure advisory is recorded the same way instead of as**Notice:**agent text, and the native Codex harness records the warningscodex execreports as error items as notices too (it dropped them). claude-agent-acp also stops sending its info-level SDK lines, which Claude Code itself shows only in transcript mode. -
A spent subscription fails fast with a typed error, instead of a generic, retried
acp_error. On 2026-09-30 every trial on a Claude login whose weekly window was spent endedACP error -32603: Internal error: You've hit your weekly limit · resets Oct 2, 4pm (UTC), was retried twice (three sandboxes per task), and a caller could find out only by matching that text. BenchFlow now asksclaude-agent-acp(0.81.2, checked against its source) for its typed session failures, so a spent login arrives as its own failure kind, and also reads the same text from JSON-RPC errors and codex-acp'scodexErrorInfo: "usageLimitExceeded". The trial iserror_category: usage_limitwith ausage_limit_infoblock (the login as a label such asCLAUDE_CODE_OAUTH_TOKEN (environment), never the token; the 5-hour or 7-day window; the reset), unscored as before and never retried. AnEvaluationthen starts no more trials, lets running ones finish, lists the trials not started insummary.json'susage_limitblock, and raisesbf.UsageLimitError(.login,.window,.resets_at,.result) so a caller can switch logins and resume;UsageLimitError.from_resultreads the same from abf.runresult. Every other typed failure raises the sameACPErrorthe agent raised before, so no other category or retry verdict changes; a killed Claude Code process now says it was the agent's own process. The hill-climb demo's account pool catches the error and reruns the unstarted trials on another account. -
bench tasks checkfinds pytest plugin installs in agent-writable locations. It resolves relative paths,/root/.venv,~/.venv, explicit--pythonpaths and activated environments against the task workspace, agent home and temporary/log directories. The warning names the plugin and installation location. Pin the release in[verifier] pytest_plugin_releasesor install it into the image; an unpinneduvxinstall does not fix the refusal. A reported runtime refusal scores 0. pytest-json-ctrf for--ctrfis supplied by BenchFlow's protected runtime. -
Expected errors print one message; unexpected ones a traceback and a log.
benchran the Typer app directly, so any uncaught error ended in a Rich traceback box (a task.md naming a missing environment manifest endedbench eval branchin 110 lines of one). The console scripts now runbenchflow.cli.main:main: abenchflow.errors.UserError(task-format, flag, job-state and registry errors, a missing login, a usage limit) or a missing file prints one message and its next step; anything else prints the traceback and a crash log under~/.cache/benchflow/logs(BENCHFLOW_LOG_DIR) with the command, versions and the run's last log lines. Credential values from the environment,.env,KEY=VALUEarguments, credential-named options and the Claude and Codex login files are replaced with***; redaction matches names and values, so the log is still worth reading before it is attached to a public issue. A missing login isMissingCredentialErrorwith a next step (claude setup-token,codex login), checked bybench eval run,bf.runandEvaluation.runbefore a job exists instead of logging a traceback in every trial. -
A Claude model the adapter refuses fails once and names the models it offers.
--model claude-sonnet-9made three attempts over 203 s, each an opaqueACP session/set_config_option failed ... -32603: Internal error; it is now an unscored, never retriedagent integration failure [agent_model]listing the adapter's model values, as for codex-acp. -
The Daytona PTY's messages read as a lost connection. Text-only classification read
PTY closed by the peer: ...,PTY readline timeoutand the start-marker timeout asother; they arepipe_closed, as their transport diagnostic already said. -
Two SDK errors now say what to do. A misspelt key in a budget mapping (
EvaluationConfig(budget={"max_usd": 5}), orbudget:in a YAML config) is refused with the valid keys; it used to leave the job without a budget, silently. AnEvaluationover a tasks folder that does not exist says so and what to pass, instead of a bare[Errno 2]. -
bf.regradeworks in a notebook, and the blocking entry points match their async forms.bf.regradecalledasyncio.runand failed inside a running event loop (a Jupyter cell), wherebf.run_sync,bf.run_batchandEvaluation.run_syncwork; it now blocks the same way and takesaregrade'srunner.bf.run_syncdeclaresarun's parameters instead of*args, **kwargs, sohelp()and editors show them. -
A trial whose agent errored keeps its artifacts. Artifacts (
/logs/artifactsand declaredartifacts = [...]) were collected when the verifier phase started, which an agent error skips, so a crashed trial kept nothing the agent left, such as its session log. Cleanup now collects them before the sandbox stops when the verifier never ran (bounded to 3 minutes, and skipped when the sandbox is known to be unreachable). -
Group-relative advantages stay inside one job and skip retried attempts.
bench train convert --group-advantageandbench train stream --group-sizegrouped by task, agent and model, pooling rollouts across jobs (which may have run different policy checkpoints under one model name) and across an Evaluation's retried attempts, and the stream's group size counted attempts a retry then replaced. The default key is nowtask,agent,model,job(leavejobout to pool jobs of one policy, such as--matrix --trials); a replaced attempt isexcluded: "retried"and in no baseline; the stream counts only final rollouts toward a group, emits a replaced one withretried: true, and names the attempt a rollout replaces inreplaces. -
Solve rates say when control runs were left out.
bench eval metricsover an oracle run printedScore 100.0%besideSolve rate n/a (0 scored trials)with no reason; the solve-rate caveats (CLI,summary.json, JSON export) now name the control runs (oracle, empty/nop) they leave out. -
A plain
bench eval run(orEvaluationwithout ajob_name) resumes only its own job. It resumed into the alphabetically last folder of the jobs dir, whatever it was: afterbench eval smoke, every run landed injobs/smoke(a folder of jobs) and blended its hello-world pass into another task's score, and the documented oracle-then-nop pair refused its second command. Now only a job folder is resumed (a timestamp name, a job record or trial folders; never a folder of jobs), only when it carries a BenchFlow marker (see "Only job folders carrying a BenchFlow marker are resumed" under Changed), and only when itsevaluation.jsonrecords the same agent, model and tasks folder; otherwise a new job starts, with a warning naming the one it did not resume. The refusal for an explicit mismatching job names--fresh. -
Budgets enforce USD and tokens between rollout starts, and count every attempt. USD and tokens were counted only as trials finished, and only from a trial's last attempt, while the CLI called the caps hard limits: every free slot kept starting trials until the cap was passed, then the running ones were cancelled with their spend lost, and a retried trial's earlier attempts were free. Every rollout (retries included) now counts, and once rollouts have finished a trial waits to start while the running ones, at the job's mean spend so far, would reach the cap, so a job passes a USD or token cap by about one rollout's spend (the first wave, before anything has finished, starts without an estimate). A resume counts every attempt the job folder records. The CLI help and
docs/reference/budget.mdnow say how each cap is enforced. -
results.jsonlrows say which reward convention they use. Since this release an unscored rollout's row has reward null, not 0.0, but nothing in the file said so. Each BenchFlow row now carriesinfo.schema_version2 (rows without it are version 1, where 0.0 could mean unscored), andbf.load_jobreads an unscored version-1 row (reward 0.0 with norewardinmetrics) as unscored instead of a failure. -
The committed JSON Schemas accept fields added later, as the versioning rule promises.
benchflow.trial,benchflow.job,benchflow.comparison,benchflow.run-summaryandbenchflow.rollout-stream.v1setadditionalProperties: falseat every level, so a reader validating with its copy of a v1 schema refused every document that gained an optional field, although a new optional field keeps the version. The published schemas are now open; the writers stay strict. -
Token coverage requires one logprob per sampled token id. A call whose sampled ids and logprobs had different lengths counted as complete, so
bench train token-coveragecalled the rollout training-grade whilebench train streamcould not merge it and dropped that conversation's sequence without a word. Such a call is now incomplete (logprobs:length_mismatchinunavailable), and a stream record never loses a conversation silently: the rollout is marked not training-grade with a reason. -
--source-envrefuses--fail-under,--fail-onand--summary-outinstead of ignoring them. The hosted-environment path never read them, so a CI job gated on a pass rate exited 0 and wrote no summary. vf-eval scores such a run as one mean reward, with no trials to count, so the flags now stop the run before it starts and say where the reward is. -
Repeated rollouts in a
bf.run_batchfolder are separate trials, andbench eval metricsagrees withbf.load_job.load_jobkept one trial per task and folder, which is right for the retries of an Evaluation job but collapsed a batch's independent rollouts: four rollouts with rewards 1, 0, 1, 0 (newest last) gave a solve rate of 0.0,attempts="all"gave 0.5, andbench eval metricssaid 100% (it kept the best of the four, across folders too). Attempts now collapse only inside an Evaluation job folder (evaluation.jsonorsummary.json), to the scored attempt, then the newest, the rule the job's ownsummary.jsonuses;bench eval metricscounts trials by the same rule, so itsTotal/Passed/Scorematch its solve rate, and it lists each task name once. -
Evaluation(config=cfg, budget=...)no longer changescfg. The keyword, and the task source's provenance, were written into the caller'sEvaluationConfig, so a config reused for a second Evaluation carried the first job's budget. The Evaluation keeps its own copy. -
The egress proxy no longer cuts model responses longer than 15 minutes. Under a denylist or allowlist network policy, both relays gave each socket a 900 s timeout and treated the client's silence as the end of the connection, but a streaming model response is client silence by nature: every model turn longer than 15 minutes was cut with no status, and Claude Code retried it from scratch. The 900 s is now an idle timeout for the whole connection, reset by every byte in either direction. There is no absolute cap: a connection that keeps moving bytes is in use, and the proxy lives only as long as its rollout.
-
A lost tool-call update no longer kills a working agent (#1141). The idle watchdog gave each pending tool call its own grace clock (3x the idle budget) that only that call's own updates restarted, so one call whose terminal update the ACP adapter lost ended a session that was still completing tool calls: 6.9 h of a Claude Code run on a GPU box, and two Codex runs at the 5400 s grace. A newer tool call now restarts the grace of every call already pending, since an agent that starts a new call is not waiting on the older one; a silent session still hits the grace.
BENCHFLOW_AGENT_PENDING_TOOL_GRACE_SECsets the grace in seconds, and the idle timeout message andidle_timeout_info.lost_update_tool_call_idsname the calls that got no update after later calls started ("lost update"). -
Codex no longer sends the task prompt to api.openai.com. codex-acp (1.13.1 through 2.0.1) names each session with an ephemeral thread on the hard-wired
gpt-5.6-lunawhose input is the first user message, the task prompt. It starts that thread without the session's provider, so the request went to Codex's built-inopenaiprovider at api.openai.com, which answered 401 on every rollout after the prompt had left. BenchFlow's Codex launcher now writes the run's provider into Codex's userconfig.tomlas the default for every thread (the built-inopenaiprovider points at the same endpoint), and the model gateway refuses a request for any model other than the run's with HTTP 400model_not_served, without forwarding it, and counts refusals in a warning when the rollout ends. With the real codex-acp 1.13.1 in a container with no network, the unfixed launch tried api.openai.com 17 times after the first turn; the fixed one sent the title request only to the gateway. -
claude-opus-5-5runs underclaude-agent-acp: the adapter moves to 0.81.2 and the Claude Code CLI gets its own pin (#1137). The 0.73.0 pin bundled Claude Code 2.1.257, andclaude-opus-5-5refuses every turn below 2.1.280. The sandbox now installs@anthropic-ai/claude-code@2.1.280beside the adapter and the launcher hands it to the adapter throughCLAUDE_CODE_EXECUTABLE, so the CLI the adapter's SDK bundles never runs; a model that needs a newer CLI is now a CLI bump rather than an adapter bump (the adapter stays on the 0.81 line: 0.82.0 changed the tool-call contract). The install fails when the CLI does not report its pin, the native Claude no-web gate verifies the CLI package, its binary and the launcher, andbench doctorshows and guards both pins. Atool_progressheartbeat that names an Agent call as its own parent no longer detaches the call and its subagent from the trajectory. Builds on #1139 (alistairjcbrown), which first moved the adapter to 0.81.0. -
A timed-out exec is reported as a timeout, never as an empty
verifier crashed:(#1065). Killing a child that asyncio had already reaped raisedProcessLookupError, which replaced the timeout error; it carries no text, so the verifier error readverifier crashed:with nothing after the colon. Teardown now tolerates a child that is already gone, and verifier errors name the exception class (verifier crashed: ConnectionResetError: ...) (#1081, by tulerfeng). -
A slow exec after the agent finished no longer discards the rollout (#948). Publishing the trajectory for the verifier (
mkdir -p /logs/agent) and scraping Gemini's trajectory ran a 10 s exec whose timeout threw away a completed rollout on a loaded host; they are now non-fatal and logged (#1043, by tulerfeng). The verifier's own setup commands (clearing its output directory,chmodoftest.sh, a target service's verifier directory, the criteria runner's manifest) get 60 s instead of 10 s and two more tries, 5 s and 15 s apart, beforeVerifier setup failed(verifier infrastructure). -
A multi-megabyte ACP message no longer kills the agent's transport or loses its tool call (#1138). A Claude Code
Readof a PDF returns the pages as inline base64 images, onetool_call_updateline of several megabytes: Daytona closed the PTY websocket on it (1008) on every attempt, and Docker's reader dropped any line over 10 MB, and with it the tool call's final update. When the sandbox haspython3, the agent's output now passes through a filter that rewrites any line over 1 MiB: image and audio blocks and other long base64 strings become a note of what was removed, then the longest remaining strings are cut until the line fits. The message's ids,toolCallIdandstatusare kept, so the update still closes its tool call. The host reads a Docker line over 10 MB whole instead of dropping it (up to 256 MB) and applies the same rewrite to any line still over the limit, so both backends record the same message.BENCHFLOW_ACP_LINE_LIMITsets the limit in bytes (0turns the filter off). -
A verifier command the exec layer lost is found in 30 s, retried once, then reported as infrastructure (#1136). After a long agent phase a Daytona exec session can lose the verifier's command. Only tasks with a verifier-only recovery contract asked the command for a start receipt; every other task waited out its whole verifier budget, then, finding no output, a second whole budget, and ended as
verifier timed out, whose retry replays the solver. Every script verifier's command now writes a start receipt under/run/benchflow: when it is missing 30 s after the command was issued, the command runs once more in place, and a second miss isverifier_wedge:(verifier infrastructure; a task with a recovery contract goes to fresh-sandbox recovery at the first miss, as before). A command that did start is never replayed, however quiet. Each status and log request of the Daytona command poll now has its own timeout (30 s and 120 s) and is retried, so one request on a dead connection no longer holds a finished command until the verifier budget ends. -
Reviewers get their own idle budget, and the Daytona read guard covers the pending-tool grace (#1143).
--reviewer-idle-timeout(ReviewerConfig.idle_timeout_sec, default 600,0ornonedisables) sets the reviewer's idle watchdog, which reviewers inherited from the rollout default with no way to change it. The PTY read guard is now at least 3 × idle + 60 s for solvers and reviewers: the idle watchdog lets a pending tool call stay silent for three idle budgets, and the transport used to cut it at idle + 60 s (a reviewer at 900 s). -
A closed Daytona websocket fails the agent's read at once, and a reviewer that loses it is retried (#1144). BenchFlow's Daytona PTY reader waited only for the next line, so a websocket the Daytona side closed (1006 after a reset, 1008 on an oversized message) or a network path that died without a close frame surfaced as
PTY readline timeoutonce the whole silence budget had passed, and the reviewer's transport retry skips a readline timeout by design. The read now fails as soon as the SDK's PTY handle reports its websocket gone, with the close code (PTY closed by the peer: websocket closed (close_code=1006, ...), transport diagnosispty_closed); lines sent before the close are still read. The SDK's websockets are opened with a heartbeat (a ping every 120 s;BENCHFLOW_DAYTONA_WS_HEARTBEAT_SEC,0disables), so a silently dead path closes too. A reviewer that loses its transport is retried once with a fresh sandbox, and when it loses it again the scoring block carrieserror_category: reviewer_transport. -
Concurrent jobs on one Docker daemon no longer delete each other's containers. Every
bench eval run/Evaluationon Docker randocker container pruneanddocker network prunefiltered only bybenchflow.owned=truewhen it started, before each retry and when it ended. That removed every stopped BenchFlow container and unused BenchFlow network on the daemon, including another rollout's container between Compose's create and start, its network before the container joined, and themaincontainer a branch restore was replacing: "container is marked for removal and cannot be started", "failed to set up container networking: network <project>_default not found" (retried since as a daemon race), "removal of container … is already in progress". The prune is now a sweep that removes a stopped container or unused network only once the BenchFlow process that made it (new labelbenchflow.process) is gone, or once its sandbox in this process has been torn down. New containers and networks are labelledbenchflow.owned=processinstead oftrue, so an older BenchFlow's prune on the same daemon cannot match them; the sweep leaves older versions' containers alone. Each Docker sandbox also gets its own Compose project (the rollout name plus a random suffix): branch children (n1,n2, …) and regrade's verifier had fixed names, so two of them on one daemon shared a project and tore down each other's containers. Branch restore removes the old container withdocker rm -fstraight away, which also drops a 10 sdocker stopwait from every in-place restore. -
A symlink that leaves the workspace no longer costs a trial its review or its verifier. codex-acp can leave a helper link such as
/app/apply_patchpointing outside the workspace, and every Python virtualenv linksbin/pythonto the system interpreter. Workspace capture aborted on such a link, so a rubric trial ended in a scoring error and a separate-verifier trial was unscored, both withrewards: null. Such a link, a link whose target text climbs out of the workspace (which used to pass capture and then fail extraction), and a link loop are nowsymlink_escapeexclusions that record theirlink_target, in the Python capture script and in the tar fallback for images without Python alike; no outside file is copied. (#1130) -
A regrade restores the file permissions the solver left. The frozen workspace's manifest now records each file's and directory's mode, and
bench eval regradeand verifier recovery put them back (the reviewer's copy stays 0644/0755). Before, a solver that made a key private withchmod 600passed, and a regrade with the unchanged verifier failed it. -
A regrade says when a verdict changed although the task did not. Each row carries
task_changed; a changed verdict on an unchanged task has a reason (the verifier read state a regrade does not restore, such as a package installed system-wide or a running service) and is marked in the table. -
bench eval regrade <job>finds a batch job's tasks without--tasks-dirthrough the tasks folder the job recorded, and no longer lists attempts a retry replaced as extra trials. -
bench eval list jobs/lists every job. The copy of the last summary each job writes to the jobs folder made a folder of several jobs show as one row. -
bench eval metricskeeps one result per task, agent and model. Over a folder holding an oracle job and a nop control job, every control failure was replaced by the oracle's pass. -
A rubric task with no reviewer model names
--reviewer-model. The refusal said "pass --model", which sets the solver; the rubric review docs no longer claimopencodehas a registered model. -
Resuming a job whose task changed logs one warning instead of a traceback before keeping the saved result.
-
Codex knows
gpt-6.1-sol: thecodex-acppin moves from 1.13.1 to 2.0.1. Codex takes a model's tool surface from the model catalog bundled with its@openai/codexdependency. codex 0.156.1 (behind 1.13.1) has nogpt-6.1-sol, so agpt-6.1-solrollout logged "Model metadata forgpt-6.1-solnot found. Defaulting to fallback metadata" and was offered only the fallback function tools: no code mode, noapply_patch, no multi-agent tools (the #1145 symptom again), andsession/set_modelto agpt-6.1-sol[...]id failed. codex-acp 2.0.1 depends on@openai/codex^0.159.1 (npm resolved 0.159.2 during the original pin check; both catalogs listgpt-6.1-solwith efforts low to max and ultra, code mode only, multi-agent v2, a 272k context). On a live docker rollout through the LiteLLM proxy (Azure),gpt-6.1-solat effortmaxnow gets code mode (exec,wait),clock.sleepand the collaboration tools (spawn_agent,wait_agent,send_message,followup_task,interrupt_agent,list_agents), andsession/set_modelacceptsgpt-6.1-sol[max]for direct providers. -
Codex command output stays in the trajectory under codex-acp 2.x. codex-acp 2.0 (its tool call contract) stopped putting a finished command's output in
rawOutput.formatted_outputfor a client that declares no terminal capability, which is BenchFlow: a shell command now ends with norawOutputand a read / search / list command with only{"exit_code": n}, while the output arrives as_meta.terminal_output_deltachunks and the exit code in_meta.terminal_exit.ToolCallRecord.raw_outputrebuilds the 1.x{"formatted_output", "exit_code"}shape from those chunks (an agent-sentformatted_outputstill wins), so captured trajectories keep every command's output. The trajectory viewer now shows it for shell commands too: it rendered Codex'sterminalcontent block (a terminal id) instead of the command's raw output, on codex-acp 1.x as well. -
The native harness runs the Codex core codex-acp 2.0.1 runs.
_CODEX_CLI_PACKAGEmoves from@openai/codex@0.156.1to 0.159.3, the release 2.0.1's^0.159.1dependency resolves to, so--harness nativeand the ACP harness still drive one Codex build. The native-harness samples were re-recorded on 0.159.3 against the deterministic fake model; only the SIGINT sample changed: Codex 0.159.3 does not stop on SIGINT but finishes the turn, which the native client never applies (it drops everything after a cancel and kills the CLI after its 2 s grace). The deterministic tier's Codex scenarios move fromgpt-5.4, which codex 0.158 dropped from its catalog, togpt-5.5, which both releases list without code mode, so they keep exercising Codex's full tool surface instead of its fallback tools. -
Known regression: the
gpt-5.4family falls back under codex-acp 2.0.1. The catalog comparison for this pin on 2026-10-02 covered Codex 0.156.1 and 0.159.3: the former listsgpt-5.4withoutgpt-6.1-sol, while the latter has the opposite coverage. This is a statement about those inspected versions. On 2.0.1,gpt-5.4,gpt-5.4-miniandgpt-5.4-nanolog "Model metadata … not found" and loseapply_patchandtool_search. This affects the codex-acp integration model (.github/integration/scope_defaults.yml,gpt-5.4-nano) and theopenai/gpt-5.4-miniexamples; a model the catalog knows (for examplegpt-5.5orgpt-5.6-luna) keeps the full surface. -
Codex gets its real model id, and a codex that knows it (#1145). Under a BenchFlow provider (LiteLLM proxy) the
codex-acpthread was started with the proxy alias as the model (benchflow-azure-foundry-openai-gpt-5.6-luna). Codex resolves model metadata by slug, warned "Model metadata for...not found. Defaulting to fallback metadata", and offered a reduced tool surface on every rollout: eight function tools, no nativeapply_patch, no code mode, no multi-agent tools, where nativecodex execon the same provider offers all of them.CODEX_CONFIG.modelnow names the bare slug (which the proxy already serves next to the alias), the launch-config writer accepts it when applying the reasoning effort, and thecodex-acppin moves to 1.13.1 (codex 0.156.1) because 0.148 has no metadata forgpt-6-astraeven under the bare slug. Verified on hello-world rollouts: bare slug + 0.156.1 givesgpt-6-astracode mode +apply_patch+spawn_agent/wait_agent/send_message/followup_task, andgpt-5.6-lunacode mode +apply_patch, matching the native CLI's tool lists for both. This pin is no longer current: 0.9.0 shipscodex-acp2.0.1 with Codex 0.159.3 (the two entries above).
Added
- ACP and native agents run on Modal.
ModalSandbox.live_process()returns aModalProcess: the agent's stdin and stdout are ModalSandbox.execstreams. The agent starts as on Docker: root's bash by absolute path with no startup file, the loader and start-up variables empty until the drop to the sandbox user, and the agent's environment as the first line of its stdin, so no value is on a command line, in a file or in a Modal Secret; lines over the ACP limit are shortened in the sandbox. A closed stdout raisesTransportClosedErrorwith the exit code and the redacted stderr tail. Plan validation no longer refuses ACP and native agents on Modal;denylistandallowlisttasks are still refused there. - Separate verifier sandboxes run on Modal.
verifier.sandbox_mode: separate, a declaredverifier.sandbox(its own GPUs included) and--verifier-sandbox separate/--integrity strictwere refused on Modal before the run. The verifier's sandbox starts once the agent's has stopped and been confirmed gone (as on Daytona), so a GPU trial never holds both sandboxes' GPUs at once. A file over Modal's 5 GiB per-file transfer limit cannot cross: the capture fails with that reason and the trial is unscored, never with part of the file. - Cloud metadata on Modal: proven unreachable instead of blocked. A Modal sandbox has no route to any cloud metadata endpoint (measured from a public-network sandbox: every IPv4 endpoint timed out on ports 80 and 443, every IPv6 one had no route, as root and as a non-root user). The block cannot go in there (root has no
NET_ADMIN), so every non-root agent on an open network was stopped before it started unless the run passed--allow-cloud-metadata. Each agent trial on Modal now connects to every endpoint as the agent's user and recordsnot_reachableincloud-metadata.jsonwhen none answers; any answer, or a probe that cannot run, recordsunprovenand stops the trial. Docker and Daytona keep the block. - Long Modal sandbox names. A name over Modal's 64 characters, or with characters Modal refuses, is shortened and ends in a hash of the whole name; a long task name plus
-verifierwas refused at create. - Modal trials start on Modal's current service. Modal's servers now refuse the legacy Sandbox filesystem API (
UnimplementedError: The legacy Sandbox filesystem API is no longer supported), so every Modal trial failed right after its sandbox started, on the log directories. The backend makes them, uploads and downloads withSandbox.filesystem(the legacy API'sFileNotFoundError,IsADirectoryError,NotADirectoryErrorandPermissionErrorare kept), tests paths withtest -d/test -fthrough its exec, and givesSandbox.execa whole-second timeout (a task's600.0was refused). Thesandbox-modalextra now needs Modal SDK 1.6 (modal>=1.6.2,<1.7; the lock andconstraints.txtmove from 1.4.2 to 1.6.2, with synchronicity 0.12.6): the<1.6cap only kept the legacy file API, which the service no longer serves. bfx, BenchFlow's own helper for root's steps on Docker, and a host-side record of what entered each sandbox.bfxis a static Rust binary built reproducibly frombfx/(two builds in separate folders are byte-identical); the wheel carries it for Linux x86_64 (sha2564e2b672d116399460bef89537ebbf3746a52d35640b036ed20ce6723b0a2d534) and aarch64 (dbab1ae574124d6ab8eec539a2753572bc4300ccb180e6077b4e7d846b1b6bc9), and a release fails if either is missing or differs. On Docker it is mounted read-only at/.bfx/bfx: by a bind of a copy under~/.cache/benchflow/bfx/when BenchFlow runs on Linux next to its daemon, otherwise through a volumebenchflow-bfx-<hash>that stays on the daemon for later trials (BENCHFLOW_BFX_DELIVERY=auto|bind|volume|off). It runs the kill before grading (with a receipt), the workspace capture (bfx tar-out; a workspace path with a link or mount falls back to the earlier capture), the workspace's change of owner (uses the checked fallback walk where the helper cannot serve it), clearing/logs/verifier, the start-up hook clean-up and root's command channel (bfx serve), so none of these needs a Python in the image. Without a pinned binary for the machine, or withoff, the earlier steps run unchanged. A remote daemon whose volume cannot be filled stops the start with a message namingBENCHFLOW_BFX_DELIVERY=off. A task that mounts anything at or below/.bfxis refused. Delivery code for Modal, Daytona and AgentCore exists but is off (BENCHFLOW_BFX_PROVIDERS=ondelivers the binary, and no step uses it there yet). The oracle and nop controls retain their scores. BenchFlow also records, on the host, every transfer into a sandbox and who chose its bytes (see theadmission=keyword under Changed): the ledger is private to your user (~/.local/state/benchflow/trust, orBENCHFLOW_PRIVATE_STATE), and each trial gets a read-only report copy,trust-state.json. A snapshot restored without that record counts as already touched by the agent, and--integrity strictrefuses to restore it.--integrity audit|strict: a reward-integrity verdict next to every reward (BenchShield, arXiv 2609.11028).bench eval run --integrity audit(integrity=onEvaluationConfig,RolloutConfigandTaskRuntimeConfig;integrity:in a run config) records, for every trial, what the agent did to the things its reward depends on, checks that record against the task's contract, and writesintegrity/beside the reward: the verdict (Checked,VectorExposed,AgentViolationorRejected), the contract it was checked against, the agent's actions projected from the trajectory the host recorded, hash-chained lifecycle events, the trace check and the reward's provenance. The verdict never changes the reward.strictalso runs every trial's verifier in the separate verifier sandbox (Docker, remote Docker, Daytona) and refuses, per trial, a task that sandbox cannot run.bf.load_job(...).integrity()andbf.load_trial(...).integrityread verdicts back, andbenchflow.integrity.audit_trialaudits a stored trial that ran without the option. The lifecycle model, event schema, trace checker and claim rules are ported from BenchShield's implementation, BenchGuard. The audit is off by default, never fails a trial (an audit that cannot record its own failure is logged and dropped) and runs off the event loop;--integrityon the command line overrides a--configfile, and a bad value is refused before a job folder exists. CI's Docker tier runstests/test_integrity_deterministic.py. See Reward integrity.bench eval regradeandbf.regrade: re-score stored runs with a changed verifier, without running the agent again. A run made with--freeze-workspace(rubric review and verifier recovery freeze it anyway) keeps each trial's final workspace and declared artifacts inevidence/, with every file's size and sha256 in a manifest.bench eval regrade JOB_OR_TRIAL --tasks-dir FIXED_TASKS --reason "..."(bf.regrade,bf.aregrade) builds a fresh sandbox from the current task, restores the frozen workspace after checking it byte for byte against the manifest, runs the verifier with the usual hardening, and writes the new score beside the original;result.jsonis never changed. A trial without evidence, whose evidence no longer matches its manifest, whose task folder is gone, or whose task is embodied is reported as not regradable, with the reason, and is never scored against a reconstructed or empty workspace. See Regrade.--harness native: Claude Code and Codex through their own CLIs.bench eval runandbench eval branchtake--harness native(harness="native"onEvaluationConfig,RolloutConfigandbf.branch;harness: nativein a run config) to runclaude-agent-acpasclaude -p --output-format stream-jsonandcodex-acpascodex exec --jsoninstead of through their ACP adapters, with the same agent entry, install, credentials, skills, MCP servers, model proxy, network policies, sandbox user, timeouts and trajectory; ACP stays the default. Each turn runs the CLI once, in its own process group, and resumes the CLI's session after the first; a cancel or timeout interrupts the CLI and kills every process of the turn. The CLIs' JSON events become the ACP session updates the adapters send (Claude Code's mapping is a port ofclaude-agent-acp0.81.2's), so results, trajectories, ATIF exports and usage come out the same: on the deterministic tier native Claude Code reproduces the ACP golden files, and both harnesses send the model proxy the same requests apart from a written list of expected differences. Native Codex is pinned to@openai/codex@0.159.3, the Codex corecodex-acp2.0.1 runs (it was 0.156.1 withcodex-acp1.13.1 when this harness landed), reaches models only through BenchFlow's proxy, and under an allowlist network policy opens no other host. Ask-user handlers andconfirmation_policy: humanneed the ACP harness and are refused on the native one. A Claude Code subscription login with no usage left is reported as what it is rather than as a hard task: the failed turn raises the samebf.UsageLimitErroras the ACP harness, with the window and the reset read from the CLI's machine-readablerate_limit_eventrecord, so the job stops starting trials.config.jsonrecordsharness_mode, whichbench eval compare --vary harness_modecompares. Seedocs/native-harness.md.- The end-of-run summary says why trials have no score, whose problem it is and what to do next. After the score,
bench eval runandbench eval resumeprintOutcomes: N scored (passed, failed), N unscored, N errored, then each unscored or errored group by cause with its fault (task, agent, infrastructure, setup), its tasks and a next step (bench tasks check <task>for a verifier plugin-trust refusal; switch logins or wait, thenbench eval resume <job>, for a usage limit), the trials a usage limit left unstarted, and the run's cost and time.benchflow.failures.cause_ofholds the table; When a run fails maps every message to its cause and fix. bench doctorchecks what a first run needs. Docker's daemon version and free disk in its data root (warns below 10 GiB); the Claude subscription login's 5-hour and 7-day windows, used and reset, from theanthropic-ratelimit-unified-*headers of one 8-token Haiku request with its OAuth token (a spent or refused login warns;--offlineskips it); the LiteLLM model proxy API-key runs go through and a customBENCHFLOW_PROVIDER_BASE_URL; Linux fixes on Linux and a deadDOCKER_HOSTnamed. A machine with no credential now warns instead of failing, since the oracle and nop controls need none, and the line sits with the other credential lines.bench eval smoke --agent oraclepoints atbench eval run.- A Python quickstart that runs in under a minute with no key. The Python API's Quick Start and
docs/examples/python-sdk/quickstart.pywrite a tiny task on a prebuilt image (python:3.12-slim) and run the oracle on it: no model credential and no image build. The oracle rollout takes about 20 seconds on Docker and the whole tour (a batch and the reading and comparing cells) about 45. print(job)summarises a job, and solve rates carry a 95% interval.Job.to_markdown()(whatstr(job)and a notebook show) gives the trials, tasks and agents, the solve rate with its interval, unscored trials grouped by reason, control runs left out, and the cost of every rollout (retries included, session-log estimates marked);repr(job)stays one line.SolveRates.intervalis the Wilson interval over every trial, an unscored one counting as not solved (interval_scored_onlyis the same over scored trials only), adjusted for repeated trials of a task by the design effect, so it needs no resampling; the job and comparison exports carry it (minor 1.3). AnEvaluationResultprints on one line instead of every task's result.- USD for subscription runs, from Claude Code's own session log. A Claude subscription (
CLAUDE_CODE_OAUTH_TOKEN) bypasses BenchFlow's model gateway, so its trials had tokens butcost_usdnull, and a budget'smax_cost_usdcould not count them. When the gateway has no price, cleanup now copies the sandbox user's~/.claude/projectsinto the trial'sagent/claude-sessions/(credentials redacted) and prices it: Claude Code's owntotalCostUSDwhen its lastcost-stateline covers every logged response, else the logged usage at LiteLLM's list prices. The figure is labelled an estimate:price_sourceisagent_session_logandusage_details.cost_estimategives the method (claude-code-costorusage-at-list-price) and the log's path;bench eval inspectshows it as~$…, and the trial export carries both (minor 1.3). A rollout whose sandbox holds another rollout's turns is not priced this way -- a fork's children share the parent's log, and an isolated child or a continued checkpoint starts from a snapshot that already carries the source's session file -- and a model missing from LiteLLM's table is priced only from Claude Code's own totals. - Hooks and extra artifacts for an
Evaluation.EvaluationConfig(pre_agent_hooks=[...])runs Python hooks in every trial's sandbox before its agent starts, asRolloutConfig.pre_agent_hooksdoes for one rollout (config files refuse them,evaluation.jsonrecords their names,Evaluation.resume(..., pre_agent_hooks=...)takes them again, and worker processes refuse them).config_override(--config-override) may addartifacts, collected besides each task's own and never replacing them, so a job can keep, say, the agent's session log from every trial without editing the tasks. Trial.attempts: the rollouts a retried trial took.bf.load_jobkeeps each retried task's best attempt; itsattemptsnow lists every attempt of that task in the Evaluation job, oldest first (itself included), so what a job ran or spent can be counted without walking folders. Records and thebenchflow.trial/benchflow.jobexport (minor 1.3) carry the count asattempts.bf.Budget(max_rollouts=N)andbench eval run --max-rollouts N: a cap on rollouts started, retries included. At the cap no rollout starts (a trial whose retry is refused keeps its last attempt's result) and running ones finish;summary.json'sbudget.spent.rolloutscounts them. A subscription run has no USD to cap, so this and--max-sandbox-secondsare the caps that always bind.--sandbox remote-docker: tasks on a Docker host you control. SetBENCHFLOW_REMOTE_DOCKER_HOST(orDOCKER_HOST) tossh://user@hostor totcp://host:2376withDOCKER_TLS_VERIFY=1and client certificates; plain TCP and local sockets are refused. It is the local Docker provider with the daemon elsewhere: same compose files, sandbox user and verifier hardening,no-network,allowlistanddenylistmodes, compose side services, separate verifier sandboxes and snapshots. Nothing on your machine is bind-mounted on the remote host (logs, verifier output and artifacts are copied back), the model proxy runs in the sandbox, every docker call targets the configured host regardless of your docker context, and the ssh user is redacted from messages.bench eval run,bf.Evaluationand reviewer sandboxes check the host withdocker infobefore a job exists: an unreachable host or a task asking for more CPUs or memory than the host reports is refused with the reason, and never retried. Teardown removes the project's volumes and orphans and then removes anything still labelled with the rollout's compose project (every volume the trial causes carries that label; see Known limitations for the one exception); when the host is gone, the warning names the command that cleans up later. Seedocs/remote-docker.md.bench train stream <job> --format jsonlandbf.stream_rollouts/bf.astream_rollouts: rollouts to a trainer while the job runs. Each finished rollout is emitted once, as itsresult.jsonappears, with reward (null when unscored), outcome, group id, the gateway's per-call prompt token ids, sampled token ids and logprobs, and, when the rollout is token-in/token-out, one merged sequence per conversation (prompt ids, completion ids, completion mask, completion logprobs).--group-size Nemits groups with a GRPO advantage. The stream waits for a job folder that does not exist yet and ends with the job (exit 0), its dead process (exit 1) or--timeout(exit 3). Record schemabenchflow.rollout-stream.v1(docs/reference/rollout-stream.md, JSON Schema committed);Evaluation.job_diris known before the run.bench train token-coveragenames the capture path and checks token-in/token-out per conversation. Each rollout shows the route of its captured calls (vllm,sglang, …) and its conversations (threads). Fix: Claude Code's tool-less helper calls (short separate prompts) counted as prefix breaks of the agent loop, so a real agent run on a self-hosted policy server was never training-grade; calls are now grouped by tool set and each tool-less call is its own conversation.sglang/...model route with token capture. A SGLang OpenAI-compatible server is a registered provider likevllm(--model sglang/<name> --agent-env BENCHFLOW_PROVIDER_BASE_URL=...). WithBENCHFLOW_CAPTURE_TOKEN_LOGPROBS=1the gateway asks it for prompt and sampled token ids through SGLang'ssglextextension, which works on streamed calls too, and records them inbenchflow.token_capture.v1. Fix: a streamed Anthropic Messages call (Claude Code) to a server that sends SGLang's finalsglextchunk no longer breaks LiteLLM's stream adapter and loses the call record.bench train token-coverage <job or rollout> [--json]reports whether each rollout's gateway token capture is training-grade: prompt token ids, sampled token ids and logprobs on every call, and each prompt extending the previous call's prompt and sampled tokens (token-in/token-out). Rollouts that bypassed the gateway, such as subscription-auth runs, are named asno_gateway_captureinstead of looking empty. Python:benchflow.trajectories.token_capture.summarize_rollout_token_capture/summarize_token_capture.- Separate verifier sandboxes. task.toml tasks with
[verifier] environment_mode = "separate"or[verifier.environment](BenchFlow:sandbox_mode,[verifier.sandbox]) now run on Docker and Daytona instead of being refused. The verifier runs in a fresh sandbox built from the verifier'sdocker_image, the task'sdocker_image,tests/Dockerfileor (BenchFlow only)environment/Dockerfile, and receives only the frozen workspace, the declared artifacts and/logs/artifacts, each byte checked against its capture manifest. Anything else the agent planted never reaches it. The workspace replaces the image's copy of a dedicated directory such as/appor/testbed(so files the agent deleted stay deleted), with the permission bits the solver left. A failed transfer or verifier-image build leaves the trial unscored (verifier_error"separate verifier …",verifier_infra), never 0, unless the solution's own files caused it: a workspace or/logs/artifactsover the capture limits, a declared artifact left as a symlink, or a/logs/artifactsfile taking a declared artifact's place, where the same paths, measured before the agent ran, were fine. That scores 0 with no verifier error, andverifier-sandbox.jsonsays why (statusrefused).timing.jsongainsverifier_sandbox_setup,verifier_transfer,verifier_sandbox_teardownandverifier_sandbox_total,verifier-sandbox/verifier-sandbox.jsonrecords the image source, sandbox id, transfer inventory and sandbox-seconds, and--max-sandbox-secondscounts both sandboxes. Workspace and artifact capture also work in images without Python (tar in the sandbox, rules applied on the host). Seedocs/separate-verifier.md. --source-envruns verifiers v1 environments. verifiers' v1vf-eval(0.3.2 dev line) rejected every flag BenchFlow passed. BenchFlow now reads which CLI the installed verifiers ships and speaks it: v1 runs get the v1 flags and--no-push(so nothing is uploaded to Prime), and their reward, errored episodes and tokens come fromtraces.jsonl. New--source-env-verifiers-versionpins verifiers;--source-env-base-url/--source-env-api-key-varchoose the model endpoint.- Canary strings.
bench tasks initwrites a canary line with a fresh GUID per task into thetask.mdfrontmatter,verifier/test.sh,oracle/solve.shandenvironment/Dockerfile(files the agent does not see), andbench tasks checkwarns, without failing, when a task has nocanary GUID <uuid>line (benchflow.task.canary). - Hard per-job budget.
bench eval run --max-cost-usd/--max-sandbox-seconds/--max-tokensandEvaluation(budget=bf.Budget(...))(alsoEvaluationConfig.budget, YAMLbudget:) stop a job at a cap: no new trials start and running ones are cancelled (their sandboxes cleaned up, noresult.json).summary.jsonandEvaluationResult.budgetreport the caps, what was spent, the reason, and the cancelled and not-started trials, which are never counted as failures; a resume runs them and counts what was already spent. USD counts only trials that report it; sandbox-seconds include running trials. Refused with--worker-concurrencyand--source-env. Seedocs/reference/budget.md. - Lenient-but-safe task.toml on the run path. A legacy
task.tomlwith a genuinely unknown key outside the decision-bearing tables now runs: the key is ignored with a warning (task.config.ignored_keys) andbench tasks checkprints it as a warning without failing. But leniency no longer fails open: a probable typo of a known key (a close edit distance to a sibling, e.g.allow_internett,timout_sec, a mistyped[verifer]table) and any unknown key inside a table that decides isolation, network, timeouts, resources or grading ([sandbox],[agent],[verifier],[steps]) are refused with aTaskConfigKeyErrorand a did-you-mean, so a typo can never silently fall back to a default (a public network, the default timeout);mainrefused the same file. A real newer task.toml key BenchFlow chooses to ignore can be added tobenchflow.task.imports.KNOWN_IGNORABLE_KEYS, andBENCHFLOW_TASK_TOML_ALLOW_UNKNOWN_KEYS=1restores the old drop-everything leniency for corpora you do not control.bench tasks checkreports the same refusals as errors, so the check and the run agree. Keys whose semantics BenchFlow cannot honour stay refused before launch by the runtime capability check;[[verifier.collect]]joins that list.TaskConfig.model_validate_tomlandtask.mdstay strict. The task.toml docs now say what is read, ignored and refused, and that the run path reads task files directly. - pass@k, pass^k and partial-credit solve rates.
summary.json,matrix-summary.json(per model entry, pooling itstrial-NNfolders),bench eval metrics,bench eval compare,Job.solve_rates()andbf.comparereport solve rate and the unbiased pass@k/pass^k estimators. Default statistics count every recorded trial, treating an unscored trial as not solved. Only the explicitly named*_scored_onlyvariants exclude unscored trials. Control runs are excluded; each estimator omits a task when its selected population has fewer than k trials and reports the n ≥ k caveat.--solve-threshold/solve_threshold=counts reward ≥ threshold as solved for non-binary rewards.bench eval metrics --jsonno longer breaks long strings. Seedocs/reference/pass-at-k.md. - CI, results and JSON contracts. CI:
bench eval run --fresh,--job-name,--fail-under,--fail-on timeout,error,verifier-errorand--summary-out(abenchflow.run-summarydocument);bench eval resume JOB_DIR; a resumed run says so and names--fresh; SIGTERM cancels the run, deletes its sandboxes and exits 143; an unknown one-word agent and a rejectedDAYTONA_API_KEYstop the run before a job exists;bench sandbox listshows only your sandboxes (owner label, full IDs,--json) andbench sandbox cleanup --alldeletes them regardless of age, with a reason for every skip and exit 1 on backend failure;BENCHFLOW_LOG_LEVEL/BENCHFLOW_LOG_FORMAT=time; the heartbeat stays on for a one-task job at any concurrency. A built-innopagent is the empty control run. Reading results:Job.denominators_by(by=...),compare(..., by=("agent", "model"))with a mixed-model caveat, paired-task headline rates (a_paired/b_paired),bench eval inspect --by/--include-controls/--csvandcompare --by; the best attempt is kept per agent and model, not per task; records carry cache tokens, execution, assessment and run settings;Trial.agent/model/duration_sec; compactTrial/Jobreprs;results.jsonlwithout task names andsummary.jsonpassed as a job are refused with a clear message;agent_env_keyssurvive a second config save. JSON contracts 1.1 (schema_minor):benchflow.trialcarries every rubric review in full (rubric_reviews: scoring revisions frombench eval run/bench eval scoreandbench reviewaudits, each with the rubric's criteria, weights and scales, reviewer agent/model/effort, per-criterion verdicts with cited evidence files, and the weighted reward arithmetic),usage.cost_status,sandboxandverifier.reward_details;benchflow.jobaddsgroups,interrupted,error_categoriesandtiming_totals;benchflow.comparisonaddsby,rows[].groupand the paired denominators. An unscored trial'sresults.jsonlrow has reward null, not 0.0. New page: Analysing runs. - Branching: training rows v2, snapshot reuse, costs. Branch-tree training rows are version 2: OpenAI chat-template shape (the agent's tool names,
function.argumentsJSON strings, merged assistant turns, atoolscolumn),sessioncopied from the fork, the source conversation prepended for--from-checkpointtrials (prefix_source,prefix_complete), oracle children skipped, DPO-ready same-request pairs (prompt/chosen/rejected), and--min-reward/--expected-rows/--manifestapplied;bench train validate --format branch-treechecks them and JSON Schemas are indocs/reference/schemas/; SFT conversion of a branch run points at--format branch-tree. A fork reuses the checkpoint already taken at the fork point instead of snapshotting again (Rollout.branch(reuse_snapshot=)), and a--from-checkpointtrial starts straight from the kept snapshot on Daytona. The branch view (1.1, withkind/schema_version) andbench eval branchesshow input/output/cache tokens, a list-price USD estimate and cost per scored child, discarded parents (unscored_by_design) and reused snapshots;bench eval branchesexits 2 on a missing path. Retries recordtool_calls/no_work,--retry-promptexpands@instructionand@verifier_feedback, and the run summary prints a retries line.bf.branchgainson_event,child.advantage,checkpoint=and defaults from the checkpoint's trial, with errors naming Python keywords. Each fork folder haslabels.json;timing.jsonitemizescheckpoint_snapshot. - Branch views, sturdier and resumable branch children.
benchflow.branch-view/1is one versioned JSON document per branched trial (per fork and child: request, status, reward, advantage, tokens, USD, sandbox-seconds, timings, snapshot reuse, lineage), frombf.load_trial(...).branch_view,bf.load_job(...).branch_views()orbench eval branches PATH [--json], documented indocs/reference/branch-view.mdwith a pure builder a viewer can copy. A branch child that failed before its agent did anything (a provider hiccup) is retried once and siblings of a failed child keep running inbench eval branch/bf.branch(--child-retries,--stop-on-child-failure; opt-in onRollout.branch). Automatic checkpoints record the agent session id, so--from-checkpoint … --resume-sessionand--retry-from-checkpoint … --retry-resume-sessioncontinue the conversation. Forks recordvalue_stderr. A sandbox created from a branch snapshot no longer re-uploads the task files. - Versioned JSON of results, and
bench eval inspect/bench eval compare.Trial,JobandComparisongainto_json_dict()/to_json()producingbenchflow.trial/benchflow.job/benchflow.comparisondocuments (schema_version 1) built from pydantic models that also generate the committed JSON Schemas indocs/reference/schemas/(seedocs/reference/json-export.md).bf.load_trialreads thebench reviewrubric verdict (trial.review),bf.load_jobreads folders that only haveresults.jsonlrows, andbf.comparechecks that both sides ran with comparable settings (task digest, model, harness, dataset, reasoning effort, sandbox, sandbox user, timeout, agent variables, prompts): an undeclared difference warns, or refuses withon_mismatch="raise";vary=declares the intended ones.bench eval inspect PATH [--json]andbench eval compare A B [--json]run the same code. - Faster parallel branch children, branch cost accounting, retry from a checkpoint, and a Branching guide. A sandbox created from a branch snapshot (isolated children,
--from-checkpoint) now checks what it already holds and reuses it: the agent binary (verified present), and the verifier's pre-agent baseline, which was previously re-captured from the checkpoint state (fix: a build file tampered with before the fork could have been restored as legitimate); tasksetup_commandsare no longer re-run on a checkpoint._snapshot_build_configruns in one sandbox command instead of 13 at the start of every trial. With more children than--concurrency, the next ones are prepared while earlier ones run. Each child, fork and trial recordscost(tokens, USD when the provider reports it, sandbox-seconds);bench eval branchprints a per-fork cost table andbf.branchresults carry it.bench eval run --checkpoints … --retry-from-checkpoint on-failure|on-timeout [--retry-prompt]forks one retry from a failed trial's last checkpoint, verified the same way; the trial's reward is kept and the retry's is reported next to it. Newdocs/branching.md. - Read, compare and save from Python.
bf.load_job/bf.load_trialread finished job and trial folders (current and older layouts) into typedJob/Trialobjects with trajectories, verifier output, costs, checkpoints and branch lineage;Job.denominators()andbf.compare(job_a, job_b)count like the viewer (attempted, scored, verifier errors and unscored apart; control runs left out).Evaluation.to_yaml/to_dict/from_dictandRolloutConfig.to_yaml/to_dict/from_yaml/from_dictround-trip configs withbench eval run --config(agent_env values only on request).docs/reference/cli-python-parity.mdmaps everybench eval run/branchflag to Python, and a test fails on drift.docs/examples/python-sdk/quickstart.pyis a tour in# %%cells, and the folder's README is the examples gallery. bf.branch/await bf.abranch: branch a run from Python in one call. It builds and validates the same plan asbench eval branch, runs the same driver, writes the same job folder and returns a typedBranchResult(V(checkpoint), each child's label, status, reward and reward source, the parent'sRolloutResult,to_records/to_csv/to_jsonl). Children are alabel -> promptmapping orChildSpecitems for nested forks; every CLI option is a keyword.- Parallel, nested and resumable branch children; automatic checkpoints; branch-tree training export.
bench eval branch --concurrency K(branch(isolate_children=True, concurrency=K)) runs a fork's children as sub-rollouts in their own sandboxes created from the snapshot (Daytona starts them straight from it), at most K at once, each a full trial folder underbranches/<fork>/children/<node>.--child label=…,parent=LABELforks again from a child's state (nested forks in onetree.json).--resume-session(resume_session=True) makes children resume the parent's agent conversation with ACPsession/load(Claude Code);tree.jsonrecordssnapshot.agent_session.--checkpoints every-prompt|prompt:N,Mwith--checkpoint-keeponbench eval runandbench eval branchkeeps sandbox snapshots after chosen prompts (checkpoints.json), so--from-checkpoint <trial> --checkpoint prompt:Nworks on normal runs;bench sandbox cleanupalso removes stale Dockerbf-snap-*images.bench train convert --format branch-tree [--pairs PATH]exports one row per child (shared prefix, continuation, reward, value, advantage) and sibling preference pairs. Forks recordtiming_sec.children. - Sync, async and batch entry points in Python.
bf.arun(async;bf.runstays the same function) andbf.run_sync(blocking, also inside a running loop such as Jupyter);bf.as_completed,bf.arun_batchandbf.run_batchrun a list ofRolloutConfigs with bounded concurrency, sharing one job directory, and return aResultslist withn_passed,mean_rewardand pandas-freeto_records()/to_csv()/to_jsonl()(also onEvaluationResult, viaRolloutResult.to_record()).Evaluation.stream()yields results as tasks finish,Evaluation.run_sync()blocks, andEvaluation.resume(job_dir)finishes an interrupted job from theevaluation.jsonevery job now writes (agent_env key names only, never values). A job holds<job_dir>/.evaluation.lockwhile it runs, so a second run of a live job is refused. Every public function and method is annotated (a test enforces it) and the SDK modules' doctests run in the suite. bench eval branch: fork an agent run from the command line. Runs each task up to--checkpoint-after-prompt N, snapshots the sandbox (or declared environment state), runs every--child label=NAME[,prompt=…]from that checkpoint with a fresh agent session scored by the task verifier, then continues or discards the parent (--parent).--agent oraclechildren runsolve.sh;--retain-snapshotskeeps the checkpoint and--from-checkpointbranches again from a kept sandbox snapshot. The job folder keeps the normal layout plustree.jsonper trial and a children-countingsummary.json.Rollout.branch()gainsrestore_parent=False(skip the parent restore after the last child; the rollout is then marked discarded and refuses to continue) andchild_requests(what each child's runner does, recorded asintervention.requested); each fork recordstiming_sec. Branch lineage now reachesresult.json(per-childnodes,parent_node,parent_restore),results.jsonl(info.branches) and each child'sobservation.json(lineage).- Typed results for Python callers.
RolloutResultgainsreward,passedandrollout_dir, andRolloutResult.load(path)reads a finished rollout back from itsresult.jsonand trajectory.Evaluation.run()returnsresults(task name toRolloutResult, resumed tasks included) andjob_dir.EnvironmentManifest,load_manifestandresolve_sourceare importable frombenchflow. The Python API reference documents results, batches, manifests and deprecations, anddocs/examples/python-sdk/has runnable scripts for each. - Branch runners learn which child they run, and branched results count their children. A child runner that declares a keyword-only
childparameter receives aBranchChild(index, label, node, fork id);result.jsongains abranchesblock with the children's summed native token usage and phase time;docs/examples/branch-agent-run.pyanddocs/composed-checkpoints.mdshow how to branch a real agent run. bench doctorandbench eval smoke.bench doctorchecks Python/uv, the Docker daemon and Colima VM, the Daytona SDK and key, each agent's credential (names, sources and expiry, never values; an expired~/.claude/.credentials.jsonis flagged), sandbox agent pins and model-endpoint egress, with a fix per problem,--jsonoutput and exit 1 on a required failure.bench eval smokethen runs the bundled hello-world task once per credentialed agent, one at a time on Docker or Daytona, and prints reward, time and trajectory path. Seedocs/getting-started.md#run-everything-doctor-smoke-eval.- Embodied rollouts (
benchflow.embodied). BenchFlow now owns the low-level layer for robot and simulator tasks: an embodiment spec (sensors with privileged vs visible state, named action groups with units, limits and control modes, typed skills, budgets) that covers arms, bimanual rigs, dexterous hands, mobile manipulators, quadrupeds, humanoids, drones and skill-only robots; the agent-facingrobocommand and wire protocol 2 (robo act GROUP=V1,V2 ...,robo skill NAME ARGS, with every protocol-1 command kept); the trusted episode server (budgets, video, judging, oracle token, vision mode, roles, a per-step state/action/reward trace); built-in arm and gripper skills; the simulator-sidecar task format base classEmbodiedTaskFormat; and the physical verifier. Trials with an episode record also gettrainer/embodied_steps.jsonlandtrainer/embodied_episode.json(per-step transitions, video index).bench eval run --seeds 0-4runs each task once per seed (formats that implementmaterialize_variant) and adds pass@k, mean, std and reset reproducibility per task tosummary.json;bench embodied report|export|check-specwork on any job. First user: robouse. Seedocs/embodied.md. - Embodied tasks refuse branching, checkpoint restores and replay unless they declare a restorable world. One declaration,
metadata.embodied(everyEmbodiedTaskFormatpackage has it), says what software may do to the task's world:mode(sim,realorhil-mock),world_restoreandaction_replay, both false by default; a real or hardware-in-the-loop embodiment cannot declare either. A container snapshot holds neither a simulator's process state nor a robot's arm and scene, sobench eval branchandbf.branch(with or without--from-checkpoint) refuse such a task before any sandbox starts,Rollout.branch()refuses before anything is quiesced or checkpointed,--retry-from-checkpointrecordsstatus: refused(counted asrefusedincheckpoint_retries),bench eval regradereports its trials as not regradable, andbenchflow continuerefuses to replay it. A finished trial's own episode record also marks it embodied, and wins over the task's declaration when it names a real or hardware-in-the-loop embodiment.bench eval branchandbf.branchalso take task-format folders, asbench eval rundoes. Seedocs/embodied.md. - Task formats: foreign task folders run natively. A package can register a task format through the
benchflow.task_formatsentry point (orbenchflow.task.formats.register_task_format). When BenchFlow loads a folder the format claims, the format materializes it as an ordinary native package under~/.cache/benchflow/task-formats/<format>(override withBENCHFLOW_TASK_FORMAT_CACHE), and BenchFlow runs that package. This applies tobench eval run --tasks-dir,bench tasks checkandRolloutConfig/Evaluationin the Python API. The first user is robouse:bench eval run --tasks-dir <robouse>/tasks/metaworld --agent codex --sandbox dockerruns robouse's source tasks, with the simulator as a trusted compose sidecar that runs the verifier. Seedocs/task-formats.md. - task.md draft 2 packages run natively (
benchflow.taskmd).bench eval run --tasks-dir,bench tasks check,bf.EvaluationandRolloutConfigtake a task.md draft 2 folder (the instruction, then atoml taskconfig block;sandbox/,verifier/,oracle/) through a built-in task format,taskmd, that never claims a nativetask.md, which opens with YAML frontmatter. BenchFlow parses with task-md's own reference parser, vendored verbatim at a pinned commit, and refuses by name, before anything runs, every run-affecting field it cannot honor yet; a batch skips a refused package with a warning. The verifier decides test criteria from the script's CTRF report and scores the rubric (gates, points, levels, continuous scores, headline), andllmandagentcriteria runjudge-loop@1with the byte-exactjudge-prompt@1prompt over the Anthropic Messages API (an API key or a Claude Code OAuth token;BENCHFLOW_TASKMD_JUDGE_MODELsubstitutes a model), with an agent judge's commands in the separate verifier sandbox. Families run per seed (--seeds), their generator in a fresh container of the task's image; chained stages become turns of the run's agent;reward.jsonrecordstimed_out.bench tasks checkprints the reference checker's report beside BenchFlow's field-by-field decisions. Automatic rubric review leaves a task.md-schemarubric.jsonto its package's verifier. Seedocs/task-authoring-taskmd-v2.md, which lists what is honored, refused, or recorded, field by field, and where the judges depart from the spec. - Native Google Antigravity CLI agent (
antigravity, aliasagy). The Antigravity CLI replaced the hosted Gemini CLI in mid-2026 but ships no ACP mode, so BenchFlow bundles an ACP shim over its headlessstream-jsonprotocol. The registry entry pins the nativeagyrelease (SHA-512 verified, no Node.js), runs in Gemini API-key mode, streams tool calls with raw input/output and per-turn token usage, honors--modeland--reasoning-effortover ACP (agy requires an effort; the default ishigh), discovers skills from~/.gemini/config/skillsand<workspace>/.agents/skills(aSKILL.mdread is recorded as the skill invocation), delivers task MCP servers through~/.gemini/config/mcp_config.json, routes API-key runs through the LiteLLM Gemini pass-through like thegeminiagent, and enforces the no-web and denylist policies withPreToolUsedeny hooks. - Verifier recovery from preserved solver evidence. Tasks that declare
workspace_recovery(Docker, or Daytona with a digest-pinned image) orsubmission_filescan re-run a failed verification against the saved workspace or declared outputs without replaying the solver;bench eval scoreresumes pending recoveries. Their verifier writes its start receipt under/run/benchflowrather than/logs/verifier; every script verifier's command now writes one (#1136, under Fixed above). Tasks that declare neither are not recovered this way. Seedocs/verifier-recovery.md. - Composed checkpoints for rollout branching.
Rollout.branch()can checkpoint environment and sandbox layers together, restores the parent after every child (including failed and cancelled ones), and records fork lineage intree.json. Adapted from #1046 by JeremyJC67. Seedocs/composed-checkpoints.md. - Structured diagnostics in
results.jsonl. Each row carriesinfo.diagnostics(schema version 1) with usage, error and provider categories, preserved through export and scoring resume. Adapted from #1038 by Ziao Yang. - Claude subagent attribution in trajectories. ACP events carry
parent_tool_call_id; ATIF exports put each subagent insubagent_trajectorieslinked from the spawning tool call, andbench train convertkeeps subagent calls out of parent rows (--subagent-rowsemits them separately). - Event timing in trajectories. Captured events record host receipt times and the ISO-8601
ts,started_atandfinished_atfields the trajectory viewer reads. - Native Claude OAuth on no-web tasks. An in-sandbox proxy admits only model requests to the Anthropic API and refuses requests that declare server-side tools,
mcp_serversor URL sources, so a Claude CodeadvisorModelsetting is refused on no-web runs. Seedocs/native-oauth-no-web.md. - Opt-in token capture at the LiteLLM gateway.
BENCHFLOW_CAPTURE_TOKEN_LOGPROBS=1records prompt and completion token ids and logprobs per call where the provider returns them, with an explicit reason where it does not (schemabenchflow.token_capture.v1, seedocs/reference/token-capture.md). --codex-apps-policy {disabled,inherit}controls whether account Apps stay enabled for managed Codex runs.- Trajectory viewer: subagents, execution vs assessment, lineage.
bench eval viewnests Claude subagent steps under the tool call that spawned them, reports execution and assessment separately (a verifier failure after a clean run reads "completed but unscored") with verifier-recovery evidence in the Verifier tab, and adds a Lineage tab for branched rollouts (tree.json) that opens each child's trajectory.
Changed
- One start-up name list for every backend, and bash's own start-up names refused. The 13 loader and shell start-up names (
LD_PRELOAD,LD_LIBRARY_PATH,LD_AUDIT,LD_DEBUG_OUTPUT,LD_PROFILE,LD_PROFILE_OUTPUT,GCONV_PATH,LOCPATH,NLSPATH,HOSTALIASES,MALLOC_TRACE,BASH_ENV,ENV) are emptied at create on every backend and carried aside as_BF_IMAGE_<name>for the task's own command; Daytona used to refuse an image that set one. An exported bash function (BASH_FUNC_<name>%%),SHELLOPTSorBASHOPTSwith a value, in the image'sENVor the sandbox's environment, is refused at start on Docker, remote Docker, Docker-in-Docker, Daytona (create and checkpoint), Modal, Apple container and AgentCore (at image build):benchflow: <where> sets bash start-up names (<names>); bash would run them as root before any command BenchFlow sends, and they cannot be given back to the task's own command; remove them from the image or the sandbox's environment. On Daytona an image's exported function ran as uid 0 in root's shells, andSHELLOPTS=xtraceran root's commands traced. - Neutral names for the task.toml layout, the dataset hub, the round-trip check and a verifier strategy, with no alias. Each name below says what it is. An old value gets the ordinary unknown-value error, which lists the values there are (
--target 'x' is an unknown value; choose one of: task-toml).bench tasks export --target(andbench export): the previous export target is nowtask-toml, the default and only value.compatibility/export-report.jsonrecords"target": "task-toml". The Python export API (export_task_to_split_layout,build_compatibility_export_report,build_task_toml_roundtrip_conformance_report) refuses any other target with the same error.bench hub list --provider: the previous benchmark-registry provider is nowbenchflow, which lists BenchFlow's dataset registry (title "BenchFlow Registry").bench hub check:--registrydefaults to BenchFlow's registry instead of the previous outside registry URL, and--cache-dirto.cache/hub/registryinstead of the previous cache folder. Each record names theregistryit read and anenv_uidof<dataset>/<task>@<ref>, in place of the previousframeworkandhub_urlfields and the prefixed UID. The summary line readsRegistry check (<registry>): ....- The task.md preset that marks a task exportable to task.toml is now
task-toml-compatible(its category too; itsbenchflow.compatibilitykey istask-toml). benchflow.task: the previous round-trip conformance names are nowTaskTomlRoundTripConformanceReport,TaskTomlRoundTripMismatchandbuild_task_toml_roundtrip_conformance_report.benchflow.hub: the module isbenchflow.hub.registry, withRegistryTaskRef,load_hub_registry,select_registry_tasks,check_registry_tasks,registry_env_uid,materialize_registry_taskandBENCHFLOW_REGISTRY. The previous module, its names and its default-URL constants are gone.benchflow.adapters: the inbound adapter for task.toml folders isbenchflow.adapters.task_toml, withTaskTomlAdapterandfrom_task_toml_dir. TheInboundTask.sourceit returns, and thebenchflow.compat.sourcea materialised task.md records, readtask-toml. The previous module and its names are gone. Its missing-file and unknown-format errors, and the task.toml spelling errors, describe the format as task.toml.- The verifier strategy that runs a Python criteria runner from a root folder in
verifier/is nowcriteria-runner(type: criteria-runnerinverifier/verifier.md, and thecriteria-runnerstrategy key in a hybrid task.md'sbenchflow.verifier.implementation.strategies;criteria_runneris accepted there as before for the-/_spelling). The previous value gets the ordinary unknown-value error, which lists the strategies there are (verifier.strategies.<name>.type '<value>' is an unknown value; choose one of: agent-judge, criteria-runner, llm-judge, ors-episode, script, taskmd). With it: the runner's environment variables areBENCHFLOW_CRITERIA_RUNNER_ROOT,BENCHFLOW_CRITERIA_RUNNER_CRITERIAandBENCHFLOW_CRITERIA_RUNNER_MANIFEST; the manifest is/logs/verifier/criteria-runner-manifest.json, with"version": "benchflow.criteria-runner.v1"andcriteria_runner_root; the task.md authoring profile iscriteria-runner(entrypointverifier/criteria_runner/reward.py); the module isbenchflow.task.verifier_criteria_runner. A hybrid verifier strategy named with the previous one-word spelling of the rewards package stays unrecognised. - Upgrade note: a task folder that declares the previous strategy value now fails to load with that unknown-value error, before any sandbox starts. Change its
type:tocriteria-runnerinverifier/verifier.md(and the strategy key in a hybrid task.md). A runner that reads the previous environment variable names or manifest file name must read the new ones. The folder that holds the runner can keep its name;root:names it. - Docs: the schema example folder is
docs/examples/task-md/task-toml-parity; the task-package examples showsource: legacy(whatbench importwrites),runner: custom-step-runnerand acriteria_runnerstrategy.
-d name@versionreads BenchFlow's registry and nothing else. BenchFlow's registry is its published list (registry.jsonin the skillsbench repo) plus entries the package now ships for datasets that list lacks:terminal-bench@2.0, all 89 tasks pinned to commit69671fbawith a content digest each, so they are digest-checked. A dataset BenchFlow's registry does not have stops withDataset '<name>@<version>' not found in BenchFlow's registry, and the next step says to pick onebench hub list --provider benchflowlists or to name a registry with--registry <URL or file>; no outside registry is read on its own.bf.resolve_sourcealso takes BenchFlow's dataset IDs:bf.resolve_source("terminal-bench-2", path="log-summary-date-ranges").- Only job folders carrying a BenchFlow marker are resumed. A job folder is BenchFlow's when it has a marker BenchFlow writes as the job starts or later:
evaluation.json(written as a 0.8 job starts), a BenchFlow 0.7summary.json, or a trial BenchFlow started. A trial is BenchFlow's when itsconfig.json(written as the trial starts) recordsbenchflow_versionorharness_mode(a 0.8 trial,bf.run(job_name=...)too) or has every key a 0.7.x trial's has (acp_transport,sandbox_locked_paths,sandbox_setup_timeout,agent_idle_timeout_sec), or when itsresult.json(or solver checkpoint) is a BenchFlow rollout result (rollout_name). So a 0.7 job stopped before any trial finished is resumed too. Any other folder with files is a folder BenchFlow did not make: an unnamed run starts a new job beside it, a named run refuses it (<folder> is a folder BenchFlow did not make; nothing in it moves), and nothing in it moves. A folder whose only contents are empty folders counts as empty. Before, only folders with two particular foreign file markers were treated that way. bench checkreads a verifier's model use from what the task declares.[verifier] models = true, a provider key its verifier env names, or the judge files an import recorded. It no longer guesses from the command names test.sh runs, so a script calling a model now needs one of those declarations to get the warning.- Integrity action records no longer skip one foreign event name. A tool call of an unknown kind is handled by what it carries: with a path it is witnessed as a read; with no input, or any other input, it gives no record and is counted in
unmapped_kinds, since a tool with no arguments can still act. This replaces a list entry keyed on one foreign event name; calls of that kind are now counted like any other unknown kind. - A sandbox's transfer primitives take a required
admission=keyword.upload_file,upload_dir,live_processandscript_runner_uploadof every built-in backend now refuse a call without theAdmissionthatbenchflow.trust.state.admit()returns for that sandbox, service and start, raisingAdmissionRequiredbefore any byte moves; BenchFlow records each agent-derived ingress this way. Code that calls a built-in backend's primitive directly must admit first (see the Sandbox protocol indocs/reference/python-api.md), and a backend of your own must accept the keyword. The SDK'sEnvironmentmethods admit for you and are unchanged. - BenchFlow's LiteLLM model proxy reads no
.envfile. LiteLLM callsload_dotenv()when it is imported, so every host proxy also loaded a.envthat python-dotenv found by itself. A real proxy imports LiteLLM from BenchFlow's start-up hook, before its script has a file, so python-dotenv searched from the proxy's working folder: its runtime folder, then$TMPDIR(usually/tmp), then/. A/tmp/.envor/.envleft by anyone could add provider settings (*_PROXY,AZURE_*,LITELLM_*) that the user never passed to BenchFlow, and a proxy forked by the local runtime searched elsewhere. Every BenchFlow proxy (the host proxy, cold or frombench daemon, and the in-sandbox proxy) now runs withPYTHON_DOTENV_DISABLED=1, and BenchFlow passes everything the proxy needs in its environment. If you relied on a stray/tmp/.envor/.envreaching the proxy, export those variables, or put them in the.envin the folder you runbenchfrom, which BenchFlow itself reads. The proxy also no longer puts folders it finds from its working folder on its import path (LiteLLM's proxy modules add the folder and its../..when they are imported); it imports from its runtime folder and installed packages only. - OpenClaw loads from the agents repo, and a release pins that repo to a commit (#1093). OpenClaw's ACP shim now lives in
benchflow-ai/agents(acp/openclaw) and loads on first use, like the other catalog agents. A one-word agent name that neither BenchFlow nor the catalog knows now fails before any sandbox starts instead of running as a command; commands with arguments or a path still run. The default catalog is a commit,benchflow-ai/agents@9aaec336b68426058db1bd83fd2c463be2935ce7, the agentsmainafter #71 to #81 merged (the OpenClaw shim moved by agents #72, with #73's prompt streaming, #80's pinned mini-swe install and #81's private mini-swe Python), not themainbranch itself, so a release keeps loading the manifests it was tested with.BENCHFLOW_AGENTS_SOURCE=benchflow-ai/agents@mainfollowsmain;BENCHFLOW_AGENTS_DIRpoints at a local checkout. - Every
bf.runform uses the task's own agent timeout unless you pass one.RuntimeConfig.timeoutdefaulted to 900 s, which thebf.run(bf.Agent(...), bf.Environment...)/Runtime.execute()form always applied, overriding the task's[agent] timeout_sec; it now defaults toNone. When that form runs a task whose timeout is not 900 s without an explicittimeout, it emits aFutureWarningnaming both values. PassRuntimeConfig(timeout=900)to keep the old limit. - The
Agent + Environmentform returnsRolloutResult, like every otherbf.runform, so it now carries tokens, cost, error categories andscore_outcome.RuntimeResultis deprecated: constructing it warns, andresult.verified,result.messagesandresult.snapshotskeep working onRolloutResultwith aDeprecationWarning(messagesandsnapshotswere always empty). Code that checksisinstance(result, RuntimeResult)must checkRolloutResultinstead. - Codex Apps are disabled by default for scored
codex-acptasks. The native Codex version must fall inside the range the pinnedcodex-acpadapter declares, and scored runs as root or with--sandbox-user nullare refused; pass--codex-apps-policy inheritto keep the previous behaviour. Seedocs/codex-apps-policy.md. - Hardened verifiers check pytest plugin provenance. The guard loads plugins from BenchFlow's protected verifier runtime, the running pytest or unchanged image files; discovery ignores workspace metadata. A reported refusal, guard crash, incomplete check or load failure during verification gives score 0 with a named warning. Initial hardening failures before a test command starts remain setup errors; unrelated controller, transport and provider failures remain unscored. The absence of a reported problem does not prove loading, and a
registeredmarker cannot cancel an adverse signal. The guard directory joins the verifierPYTHONPATHwhere needed; task preflights still reject a task-suppliedPYTHONPATH. Plugin discovery comments adapted from #1117 by tulerfeng. Seedocs/sandbox-hardening.md. - Verification waits for sandbox-user processes to stop before scoring, using a POSIX-shell check that needs neither procps nor Python.
- A hard deadline that cuts the agent phase is reported as
timeoutand the rollout is still verified. Only an overrun of the verification grace period remainsINFRA_ERROR. Bare-timeout reclassification adapted from #1131 by AdnanElAssadi. - The automatic reviewer's deadline scales with setup-command timeouts and the size of the evidence it uploads.
- Results awaiting assessment stay unscored. A result that declares an
assessmentofpendingorunassessable(or a legacystatus: awaiting_assessment) has no reward in score summaries, rescoring or the viewer, whatever reward sits beside it, and resume never re-runs it; score summaries gain anunscoredcount. - Removed 129 unused re-exports from
benchflow.task.verifier,benchflow._utils.task_authoring,benchflow.task.acceptance_liveandbenchflow.rollout; import those names from their defining modules. --source-repowith--source-pathdownloads only that path.bench eval run --source-repo benchflow-ai/skillsbench --source-path tasks/citation-checkcloned the whole repository to run one task: a 1.1 GB checkout (447 MB pack) plus a 644 MB snapshot worktree, about 21 s before any sandbox started. A source with a path now makes a blob-less sparse clone (--depth 1 --filter=blob:none --sparse) and checks out that path alone, so the task costs about 3 MB and under 2 s; later paths join the same cache, and the per-commit snapshot holds only the requested paths (it is created with--no-checkout, because git before 2.36 would fetch everything into a new worktree). A source with no path, a path through a symlink and a folder that holds no BenchFlow task (a foreign benchmark whose source adapter reads other files of the repository) still get the whole repository, and caches made by earlier releases are reused as they are. A missing--source-pathlists the folders of its deepest existing parent and suggests a close match, instead of listing the repository root. Git older than 2.26, which has nosparse-checkout add, still clones the whole repository.- CLI help points a first-time user at the next step.
bench --helpended with no route into the docs andbench eval --helpdescribed the group as "Evaluation commands.".bench --helpnow ends withNew here? Run bench doctor, then follow <getting-started>, andbench eval --helpsays what the group does. Onbench eval run, the flags a first run needs read "Agent name", "Model", "Subpath within the repo" and "Output directory"; they now name the values they take, including the built-inoracleandnopagents, that--source-pathis the only folder downloaded, and that a plain run resumes its latest job. (The--agentand--jobs-dirof sibling commands keep their own shorter help.)bench tasks initwrites a scaffold that failsbench tasks checkuntil its[REPLACE: ...]placeholders are filled in, and said nothing about that: it now ends by printing the two commands that come next,bench tasks check <dir>and an oraclebench eval run, and a link to the task-authoring guide. That run command carries its own--jobs-dir, so it cannot resume another experiment's job, and--fresh, so that re-running it after an edit runs the task again instead of reporting the cached result of the code the author just changed.
Deprecated
benchflow.SDK(usebf.run(bf.RolloutConfig(...)), which takes the same keyword arguments), thebf.snapshot/bf.restore/bf.list_snapshotsaliases (use theworkspace_*names) and the unusedRuntimeConfig.max_rounds,snapshot_policyandreward_streamfields each emit aDeprecationWarningand keep working.
Fixed (continued)
- task.toml multi-step tasks are refused on
steps. A multi-step task has no rootinstruction.md, sobench eval runandbench tasks checkfailed on the missing file instead of the documented refusal, and in a batch the trial left no folder (summary.json counted it,inspectandbf.load_jobdid not). - The oracle runs as the task's
[agent].userand in its[environment].workdir, a correct solution to a task that declares either previously scored 0. With neither declared the oracle still runs as root in the image's default directory. Oracle trials also recordagent_executiontime. - Sandbox user setup works on busybox images (Alpine) through
adduser, and fails with a clear setup error when no user can be created, instead of failing later as a verifier hardening identity error. - A Daytona sandbox left by a failed create that names no sandbox is deleted. A gateway 502 while the SDK waited for a new sandbox to start left it running; each create attempt now carries a unique label used for the cleanup.
- summary.json counts a fresh run's integration failures by their recorded cause (it said
unknown), and reports a null cost when no trial reported one (it said0.0for subscription-login runs;bench eval metricstoo). bench eval viewshows automatic rubric reviews stored in the trial'sscoring/report, andbench train token-coverageno longer counts a trial's reviewer run as a rollout.- A task that relies on symlinks is refused before it runs. Sandbox uploads skip symlinks on purpose (#411), so a task whose
verifier/linked to another task's reached the sandbox with an empty/tests, failed asVerifier setup failed: chmod exited with rc=1(a retried verifier-infrastructure error) and ran the agent again on every retry for nothing.bench tasks checknow reports such a task (Task uses symlinks, which are not uploaded into the sandbox: …), a batch skips it with a warning naming it (legacytask.tomllayouts too), and the bundledcitation-check-networkexample holds copies instead of symlinks. - A task with symlinks no longer aborts a batch. Source provenance for a local task in a git checkout is inferred on a best-effort basis by hashing its files, and a symlink (such as
docs/examples/task-md/real-skillsbench/citation-check-network, which links itsenvironment/,oracle/andverifier/tocitation-check) raised there: the task failed as an unexpected exception, and building its result raised again after every task had finished, so the job exited 1 withoutsummary.jsonorresults.jsonl. Such a task now gets no inferred provenance, with a warning. - A Codex model the login does not offer fails once, with the list.
--agent codex-acp --model <name>for a model missing from the session's advertised models sent the name anyway, got an opaqueACP error -32603: Internal error, and was retried twice with new sandboxes before failing the same way; the run now stops at once withagent integration failure [agent_model]: codex-acp does not offer model '<name>' for this login; it offers: …. The trial is an unscored agent-integration failure (error_categoryagent_integration,integration_failure_info.causeagent_model), is not retried, and a repeat trips the batch breaker. bench eval comparelabels each side by its own folder. Comparing a timestamped job folder with a named one (jobs/batch-oracle) labelled the named side with its parent folder (jobs); only a timestamped side now falls back to its parent.- A transposed short agent name is refused by the Python SDK too.
bf.run_sync(RolloutConfig(agent="oracel", ...))passed the pre-run check (the name scored just under its similarity cutoff), started a sandbox and failed minutes later as an ACP initialize timeout; two swapped neighbouring letters in a registered name (oracel,gemnii) now get the "did you mean" error before anything starts, asbench eval runalready did. - Token capture asked for on a run that skips the gateway now warns.
BENCHFLOW_CAPTURE_TOKEN_LOGPROBS=1on a subscription-auth run (or an agent the gateway cannot route) was ignored without a word, because capture happens in the gateway; the run now logs that no token ids or logprobs will be recorded. bench train convertno longer converts aresults.jsonlrow whose capture ended inside a Claude subagent as the parent's conversation. The results writer keeps only calls a later request consumed, so when a run stopped inside a subagent the parent's spawning call was dropped, the row's steps were the subagent's alone, and the spawn-prompt check read back from the row found nothing: one row of subagent calls was written as a parent row. When the capture shows subagent activity, the writer now tags each step'sextraswith the owner it attributes from the whole capture (agent_role=parent,subagentwithparent_tool_call_id,helperorunattributed), and conversion refuses a row with a non-parent step or one that does not end on a parent call. Rows written before this change keep the old check.- A reviewer that loses its transport before a verdict is retried once (#1144). A Daytona PTY reset during the review stage (websocket close 1006/1008, a closed peer) ended the trial with
reviewer did not produce a readable review-result.json, although the verifier output was already on disk. The review now runs once more in a fresh reviewer sandbox; the failed attempt's folder is kept undertransport-retry-<n>beside it and the trial's notes name the lost attempt. A readline timeout is not retried, since the reviewer was silent past its budget. - A Daytona PTY or AgentCore read no longer kills a stage its idle budget still allows (#1143). The transport's readline guard was a fixed 900 s unless
BENCHFLOW_DAYTONA_PTY_READLINE_TIMEOUTwas exported, so--agent-idle-timeout 7200,--agent-idle-timeout noneor a reviewer with a raised idle budget died asPTY readline timeout (900s)after 15 silent minutes. Each prompt now raises the guard to its idle budget (or, with no idle watchdog, its wall budget) plus 60 s; an explicitly setBENCHFLOW_DAYTONA_PTY_READLINE_TIMEOUT/BENCHFLOW_AGENTCORE_READLINE_TIMEOUTstill wins. - The pre-verifier
conftest.pysweep ran nowhere with GNU find. It paired-prunewith-delete, which GNU find (Debian, Ubuntu,python:*-slim) refuses with exit 1, and its error went to/dev/null, so agent-plantedconftest.pyfiles outside/testssurvived hardening. The sweep now uses-exec rm -f -- {} +; GNU find onpython:3.12-slimreproduces the old failure. bf.Evaluation(...).run()from Python makes the same pre-run checks asbf.runandbench eval run(misspelt agent or sandbox, Docker, an expired Claude login file);Evaluation(..., preflight=False)skips them, and the CLI passes it because it checks first.- Blocking SDK calls no longer log
Unclosed client session:bf.run_sync,bf.run_batch,Evaluation.run_syncandbf.branchclose the Daytona client their private event loop created. bench eval run --retry-attemptshelp no longer calls the option reserved; it sets the retries per task.- The Python SDK makes
bench eval run's pre-run checks.bf.arun/run_syncand the batch functions refuse a missing task directory, an unknown sandbox and a misspelt agent name before a sandbox starts (a misspelt agent used to fail 60 s later as an ACP initialize timeout), raise withbench doctor's fix when Docker is not ready, and warn when Claude would fall back to an expired login file (it used to fail after the sandbox started).BENCHFLOW_SKIP_PREFLIGHT=1skips the host checks. - Environment-plane manifest services start on Daytona. The sandbox env-file wrapper joined the caller's command with
&&, so a detached service start (nohup CMD ... &) backgrounded the whole list and held the Daytona session open; every manifest rollout failed withCommand timed out after 15 seconds. bf.run("agent", task_path=..., config=RuntimeConfig(...))honoursrollout_nameand a changedtimeout, as theAgent + Environmentform already did, and rejects anEnvironmentobject passed asenvinstead of building a second sandbox.- Branch snapshots are deleted, owner-scoped and credential-free.
Rollout.branch()deletes each fork's container snapshot when the fork finishes, including after a failed or cancelled child (retain_snapshots=Truekeeps it); Daytona snapshot names carryBENCHFLOW_DAYTONA_OWNERandbench sandbox cleanupplus the eval-start auto-reap delete that owner's stale ones; agent credential files are removed before capture and written back after capture and after every restore. - Branch scoring is honest for custom runners. A custom runner's verifier reward is recorded as
reward_source: "verifier", a missing verifier reward or aNonereturn leaves the child unscored instead of 0, andexecute()inside a child fills the pending node withoutnode=. - A plain task's branch error names
snapshot_layers={"sandbox"}, andDaytonaClientManager.get_client()loads the SDK itself and creates a new client under a new event loop. - The host deadline honours config overlays for agent and build timeouts. (#1134)
- Bedrock keeps prompt caching for Claude Code conversations. LiteLLM moved mid-conversation system messages and their cache breakpoint into the top-level system prompt, so conversation turns were never read from cache. (#1135)
- Claude 5-family reasoning effort reaches the provider when LiteLLM falls back to its bundled model list.
- No-web agent runs get the capabilities their firewall needs on Docker and Daytona DinD, DinD uploads the capability overlay it references, and images without python3 or openssl fail early with a clear error.
- Rollout snapshots keep distinct databases that share a filename, and Docker restore replays the container's host configuration (runtime, init, groups, tmpfs, labels) or refuses to restore.
- A refused review keeps a timed-out solver's verifier result, and one failing resumed review no longer cancels the others.
- Oversized integer rewards no longer crash aggregates or exports.
- ACP session updates are kept when the agent omits
sessionId, and unchanged tool polls no longer rewrite the trajectory file. - A failed verifier start probe is treated as unknown, so a transient exec error no longer cancels a running verifier as wedged.
- ACP prompts return as soon as they finish. The idle watchdog slept a full poll between checks, so agent execution time was rounded up to the next 30 s and the sandbox stayed up for the difference.
- Daytona deletes a sandbox that fails to build or start instead of leaving it for the reaper, and the error reports the real number of attempts and the sandbox id.
- A
task.tomlthat fails to parse is named. A batch skips the task with a warning and a single-task run raisesMalformedTaskErrornaming the file, instead of scoring a bareTOMLDecodeErroras 0. bench doctorno longer passes a Gemini Google login. Gemini runs go through the LiteLLM proxy, which needsGEMINI_API_KEY, so the login is a warning andbench eval smokeskips Gemini instead of failing on it.- An unrunnable task directory says why. Without
--include/--exclude, an empty selection names thebench tasks checkissues of the task (or counts the unrunnable task directories) instead of blaming the filters. --sandbox daytonawithout the extra fails before the job starts, and every missing-extra message gives the sameuv tool install/uv sync --extra <extra> --extra dev --lockedhint.- A trial whose pytest the verifier guard stopped by refusing a plugin is labelled in the run summary (
pytest did not run: the verifier guard refused pytest plugin …). bench eval runandbench eval smokepoint atbench eval view; the viewer flushes its URL when stdout is a pipe, and names the localhost URL when it refuses another host.- A scoped Daytona run no longer warns about other operators' unscoped sandboxes as possible orphans.
bench eval run --sandbox dockerchecks Docker before it creates a job. It reusesbench doctor's Docker check, so a missingdockerCLI or a stopped daemon stops the command with doctor's fix line instead of a traceback and a published 0/1 job (BENCHFLOW_SKIP_PREFLIGHT=1skips the check). A rollout whose compose build cannot reach the daemon is recorded as a sandbox startup failure and is not retried.- An expired
~/.claude/.credentials.jsonis flagged before the image build. When a Claude run would fall back to that file and its access token has expired,bench eval runprints doctor's warning and theclaude setup-tokenfix up front; the README gives the macOS token route. - A sandbox build whose context is missing a path is not retried (Daytona's
Path does not exist, BuildKit'sfailed to calculate checksum … not found); other startup failures still retry. - Oracle runs no longer fetch remote agent manifests, and
--source-repono longer prints git's worktree progress. bench doctorreports a missing buildx plugin, drops the HTTP status from reachable network lines, and keeps its columns aligned for long host names;bench --helpandbench eval --helplist doctor, smoke and run first.- Trajectory viewer: a subagent-spawning call is badged
agent, provisional edit titles show the file, the Lineage tab explains value and runner return, branched runs are marked in the run list, a branch child's back button opens its parent, and counts of one read "1 run". - Daytona retries a sandbox delete that the API gateway failed. A delete that gets HTTP 502, 503 or 504 is tried up to four times (about 7 s of backoff); a sandbox already gone on a retry counts as deleted. If every attempt fails, the error names the sandbox id, shows an HTML error page as its title, and points at
bench sandbox cleanup. Timeouts, connection and rate-limit errors keep the two-attempt budget. - Benign teardown and mount lines are quiet. On a Mac, a job directory outside
$HOME(which Colima does not share) no longer warns that the verifier-log bind mount is not visible; elsewhere the warning names the path and points atDOCKER_HOST. A Docker rollout that could not start because Docker was unavailable no longer warns thatcompose downfailed. Daytona no longer warns "Sandbox not found. Please build the environment first." when there is nothing to delete, and warns only when an unfinished create may have left a sandbox behind. - ACP trajectories record a tool call's final title (
Write /app/hello.txt, notPreparing file…) when a latertool_call_updateretitles it; the live progress line shows it too. - The verifier's uv and pip state leaves an agent-writable
$HOME. Under aWORKDIR /rootimage, verifier caches, tool environments, managed Pythons and configuration move into a new root-owned directory after the agent stops. An unpinned pytest plugin that test.sh installs there, including throughuvx, is still refused; a reported refusal scores 0 with a warning. Pin it in[verifier] pytest_plugin_releasesor install it into the image. citation-check and other tasks using--ctrfreceive pytest-json-ctrf from BenchFlow's protected verifier runtime. Seedocs/sandboxes/threat-model.md#pytest-plugin-guard. - The verifier's commands run under umask 022 on every backend. BenchFlow set no mask for test.sh, and
docker execon Docker-in-Docker (Docker 29.8.1, runc 1.5.1) runs with 0000, so the uv cache test.sh filled came out group- and world-writable, the pytest plugin guard refused the verifier's ownpytest-json-ctrf, and every task that passes--ctrfended unscored there, the correct oracle included (89 of 89 in Terminal-Bench 2). The mask is now set in the command string for test.sh, script strategies, hybrid strategy runners and the separate verifier's unpack, so Docker, remote Docker, Daytona and Modal all get 0644 files and 0755 directories. - The verifier's uv and pip state always gets a fresh directory. The move to
/_benchflow_verifier_<hex>used to happen only when the agent could write$HOME, so with any otherWORKDIR(Terminal-Bench 2's/app) uv's cache stayed in/root/.cache/uv. Every uv and pip cache, uv tool environment and uv-managed Python now moves there after the agent stops. uv always reads a pinned configuration: a safeUV_CONFIG_FILEalready set, else root's own~/.config/uv/uv.tomlwhen the agent could not write it, else the image's first safe systemuv.toml, else an empty one; the workspace'suv.tomland[tool.uv]are not read from there (test.sh's ownuv pip installanduv runin the workspace still read them; setUV_NO_CONFIG=1). A warm uv cache baked into the image is no longer used. The plugin guard does not trust what test.sh installs in this directory. - A separate verifier sandbox distrusts only what came from the agent. It passed
sandbox_user=Nonebut kept the plugin guard's rule for the agent's own sandbox, blocking/tmp,/var/tmpand/testbedand requiring every plugin file to be root-owned and not group- or world-writable, although no agent process ever ran there. The transfer now reports the roots it writes (the workspace, each declared artifact's bundle root,/logs/artifacts), and in that sandbox discovery and the guard block those and/logsalone and accept the verifier image's own files without the ownership rule. Plugins still come only from the protected verifier runtime, the running pytest or the image: one left in the workspace, or one test.sh installs after the transfer, is refused. - A separate verifier sandbox always empties
BASH_ENV, with a warning. The activation files it would name are transferred from the agent's sandbox, so they have no pre-agent provenance. - A plugin guard refusal says what it could not trust and why. The diagnostic identifies the refused plugin, path and reason, including ownership, write permissions, duplicate registrations or a missing installation. A reported refusal gives score 0 with a warning. The plugin-file inventory remains diagnostic evidence; it is not authority to exclude a trial or establish who caused the failure.
- Plugin-guard markers are private and signed. Markers live in the verifier user's mode-0700 guard directory and carry an HMAC-SHA256 under a per-verification key that is not put in the environment. Invalid signatures are ignored. A signed adverse marker or pytest's load-failure message gives score 0; a
registeredmarker cannot override that evidence. The verifier user can read the key, suppress failure evidence and rewrite reward files, so the signature does not authenticate that user's code. The task script and what it imports remain trusted. Markers no longer depend on downloading/logs/verifier. bench tasks checkwarns about pytest plugins installed into the workspace. It recognizes common venv, uv and pip installation forms in the working directory and temporary folders. The warning names the script, plugins and commands. Pin plugin releases in[verifier] pytest_plugin_releasesor install them into the image; unpinneduvxinstalls are refused too. A reported runtime refusal scores 0. Scripts that clearPYTEST_ADDOPTSare told that the guard does not check that code. The historical 413-script scan flagged mailman, archive/mailman, powerlifting-coef-calc, fix-build-google-auto and fix-build-agentops; that scan is not a final-release runtime result.- A task generated from the
multi-agentprofile gets a reviewer that can run. The profile paired its reviewer role'sagent: claude-agent-acpwithmodel: claude-sonnet-4-6, an id the pinned adapter (@agentclientprotocol/claude-agent-acp@0.81.2with Claude Code 2.1.280) refuses atsession/set_config_optionwith-32603 Internal error, so the review scene of every such task failed on each run, after the whole retry budget, as an opaqueacp_error. The reviewer now usesclaude-sonnet-5. The model API still serves the refused id; only the ACP adapter rejects it, which is why this surfaced as a protocol error rather than an unknown model.
0.7.11 (2026-10-16)
Added
--separate-verifier-include PATHcarries outputs outside the workspace into a separate verifier sandbox. Only the workspace (sandbox.workdir, else the image'sWORKDIR) crossed, so a task whose agent writes graded files elsewhere scored 0 with no error, though it passed in shared mode. Each include path (repeatable; SDK and configseparate_verifier_include) is captured beside the workspace intoevidence/artifacts/, hash-checked, and laid over the verifier image's copy at the same path; pseudo filesystems and roots such as/,/usrand/homeare refused before any sandbox starts.config.jsonrecordsverifier_include, and the transfer inventory lists each path as captured, missing or already in the workspace.- A separate verifier lists files the agent left outside everything that
crosses. When the workspace is frozen, the regular files written during
the agent phase outside the workspace and include paths (skipping system,
scratch and provider trees, package caches and the agent's home) are logged
as a warning with a count per directory and examples, and recorded under
outside_workspace_writesinverifier-sandbox/verifier-sandbox.json. Advisory: it never fails a rollout.
Fixed
- The verifier ignores pip's and git's per-user config under
/root. The verifier runs as root withHOME=/root, the agent's workspace in many tasks, so~/.config/pip/pip.confor~/.gitconfigthere were whatever the agent left, in both modes (a separate verifier receives the/rootworkspace too).VERIFIER_ENVnow setsPIP_CONFIG_FILE=/dev/nullandGIT_CONFIG_GLOBAL=/dev/null(git 2.32+;/etc/gitconfigstill applies), and dropsPIP_NO_USER_CONFIG, which is not a pip option and did nothing. A task that needs either config back sets it in its verifierenv. - Package-manager caches stay out of workspace evidence.
.cache/pip,.cache/uvand npm's_cacache,_npxand_logsbelow a captured root are recorded aspackage_cacheexclusions instead of captured. With a/rootworkspace the harness's own installs fill them as root before the agent starts, and they can be most of what the separate verifier receives and of the rubric review's upload. mcp1.30.0 inuv.lockfor CVE-2026-59951 (was 1.28.1), which failed thepip-auditjob and, through it,main's release gate.
0.7.10 (2026-10-16)
Added
- Separate verifier sandboxes run on Docker and Daytona. A task that sets
[verifier] sandbox_mode = "separate"(task.tomlenvironment_mode) or declares[verifier.sandbox]was refused with "separate verifier sandboxes are parsed but not executed". It now runs: after the agent stops, the workspace is frozen intoevidence/, and a fresh sandbox built from the verifier image ([verifier.sandbox].docker_image,[sandbox] .docker_image,tests/Dockerfile, then the task's ownenvironment/Dockerfile) receives only those hash-checked bytes before the normal hardening andtest.shrun there. Nothing else from the agent's sandbox reaches the verifier.bench eval run --separate-verifier(SDK and configseparate_verifier) forces the mode for every task of a run without editing them, so task digests do not change. A transfer or sandbox failure is a verifier infrastructure error, never a score;verifier-sandbox/verifier-sandbox.jsonrecords the image source, sandbox id, transfer inventory and timing, andconfig.jsonrecordsverifier_sandbox_mode. Ported from release/0.8's separate verifier, without its declared-artifact and/logs/artifactstransfer, which main does not have.
Fixed
- Root helpers no longer import modules from the agent's workspace. The
helpers BenchFlow runs as root after the agent stops (disk reclaim, the
verifier PATH check, pytest-plugin discovery, the external-symlink purge,
the workspace-restore fallback, workspace evidence capture and its
reviewer-side admission) ran plain
python3 -cfrom the sandbox's default working directory, which is the agent's workspace, so ajson.pyorglob.pyleft there was imported as root before the verifier. They now runpython3 -I; the.pth/sitecustomize.pypurge keeps its environment but runs from/. Pytest-plugin discovery therefore scans only the interpreter's site-packages: declare a plugin installed elsewhere inverifier.pytest_plugins.
0.7.9 (2026-10-14)
Fixed
- Codex knows
gpt-6.1-sol: thecodex-acppin moves from 1.13.1 to 2.0.1. Codex takes a model's tool surface from the model catalog bundled with its@openai/codexdependency. codex 0.156.1 (behind 1.13.1) has nogpt-6.1-sol, so agpt-6.1-solrollout logged "Model metadata forgpt-6.1-solnot found. Defaulting to fallback metadata" and was offered only the fallback function tools: no code mode, noapply_patch, no multi-agent tools (the #1145 symptom again), andsession/set_modelto agpt-6.1-sol[...]id failed. codex-acp 2.0.1 depends on@openai/codex^0.159.1 (npm resolves 0.159.2 today; both catalogs listgpt-6.1-solwith efforts low to max and ultra, code mode only, multi-agent v2, a 272k context). On a live docker rollout through the LiteLLM proxy (Azure),gpt-6.1-solat effortmaxnow gets code mode (exec,wait),clock.sleepand the collaboration tools (spawn_agent,wait_agent,send_message,followup_task,interrupt_agent,list_agents), andsession/set_modelacceptsgpt-6.1-sol[max]for direct providers. - Codex command output stays in the trajectory under codex-acp 2.x.
codex-acp 2.0 (its tool call contract) stopped putting a finished
command's output in
rawOutput.formatted_outputfor a client that declares no terminal capability, which is BenchFlow: a shell command now ends with norawOutputand a read / search / list command with only{"exit_code": n}, while the output arrives as_meta.terminal_output_deltachunks and the exit code in_meta.terminal_exit.ToolCallRecord.raw_outputrebuilds the 1.x{"formatted_output", "exit_code"}shape from those chunks (an agent-sentformatted_outputstill wins), so captured trajectories keep every command's output. The trajectory viewer now shows it for shell commands too: it rendered Codex'sterminalcontent block (a terminal id) instead of the command's raw output, on codex-acp 1.x as well. - Known regression: the
gpt-5.4family falls back under codex-acp 2.0.1. codex 0.158 droppedgpt-5.4from its catalog, and no codex release lists bothgpt-5.4andgpt-6.1-sol. On 2.0.1,gpt-5.4,gpt-5.4-miniandgpt-5.4-nanolog "Model metadata … not found" and loseapply_patchandtool_search. This affects the codex-acp integration model (.github/integration/scope_defaults.yml,gpt-5.4-nano) and theopenai/gpt-5.4-miniexamples; a model the catalog knows (for examplegpt-5.5orgpt-5.6-luna) keeps the full surface. - Codex gets its real model id, and a codex that knows it (#1145). Under
a BenchFlow provider (LiteLLM proxy) the
codex-acpthread was started with the proxy alias as the model (benchflow-azure-foundry-openai-gpt-5.6-luna). Codex resolves model metadata by slug, warned "Model metadata for...not found. Defaulting to fallback metadata", and offered a reduced tool surface on every rollout: eight function tools, no nativeapply_patch, no code mode, no multi-agent tools, where nativecodex execon the same provider offers all of them.CODEX_CONFIG.modelnow names the bare slug (which the proxy already serves next to the alias), the launch-config writer accepts it when applying the reasoning effort, and thecodex-acppin moves to 1.13.1 (codex 0.156.1) because 0.148 has no metadata forgpt-6-astraeven under the bare slug. Verified on hello-world rollouts: bare slug + 0.156.1 givesgpt-6-astracode mode +apply_patch+spawn_agent/wait_agent/send_message/followup_task, andgpt-5.6-lunacode mode +apply_patch, matching the native CLI's tool lists for both.
Added
bench skills validate: static skill checks with NVIDIA SkillEvaluator 0.5.0 Tier 1 and NVIDIA SkillSpector 2.12.0. Takes skill directories or any directory to search forSKILL.md, runs both external CLIs with--no-llm, and prints a per-skill table (Tier 1 status, quality grade and score, SkillSpector risk and verdict) or--json. It exits 1 on a schema, security or dependency finding (including known vulnerabilities pip-audit finds in pinned packages) at or above--fail-on(defaulthigh), when a checker fails to run, or when a given path holds noSKILL.md;--no-securityskips SkillSpector. The tools are not on PyPI, so they stay external installs (uv tool installfrom their release tags, seedocs/skill-eval.md); a missing tool gets an install hint and an older one a version warning.- Native Google Antigravity CLI agent (
antigravity, aliasagy). The Antigravity CLI replaced the hosted Gemini CLI in mid-2026 but ships no ACP mode, so BenchFlow bundles an ACP shim over its headlessstream-jsonprotocol. The registry entry pins the nativeagyrelease (SHA-512 verified, no Node.js), runs in Gemini API-key mode, streams tool calls with raw input/output and per-turn token usage, honors--modeland--reasoning-effortover ACP (agy requires an effort; the default ishigh), discovers skills from~/.gemini/config/skillsand<workspace>/.agents/skills(aSKILL.mdread is recorded as the skill invocation), delivers task MCP servers through~/.gemini/config/mcp_config.json, routes API-key runs through the LiteLLM Gemini pass-through like thegeminiagent, and enforces the no-web and denylist policies withPreToolUsedeny hooks.
0.7.8 (2026-09-14)
Added
- Automatic rubric review and scoring. Tasks with a weighted
rubric.jsonrun a separate reviewer after verification. Passing requires all required tests and rubric blockers to pass; the final reward is the weighted quality score. Solver evidence and reviewer runs are retained, andbench eval scorecan retry an interrupted review without rerunning the solver. (#1126) - Rubric results in the trajectory viewer. The Rubric tab displays criterion judgments, explanations, blocker outcomes, and weighted scores. (#1102)
- Viewer themes and syntax highlighting. Trajectory pages support a dark theme and highlighted tool content. (#1098)
Fixed
- Automatic rubric review works on Daytona when the task workspace is
/root. Daytona's daemon keeps its session tree in/root/.daytona(the entrypoint's FIFOs plus every session command's script, log and exit code), and evidence capture aborted on the first FIFO, so the rollout ended in a scoring error withrewards: nullalthough its verifier had run. Capture now leaves that tree out assandbox_runtimeand records any other socket, FIFO or device node as aspecial_fileexclusion instead of aborting. Capture limits, escaping symlinks and concurrent changes still fail closed. (#1128) - Automatic review supports shell-only task images. Required Python capture tools are provisioned before the solver starts, so a successful shell task does not lose its review to a missing interpreter. (#1127)
- Denylist enforcement closes CONNECT and HTTP Host bypasses. TLS identity and HTTP authority are checked against the allowed destination on Docker and Daytona. (#1122)
- IPv6 egress rules are installed when procfs reports zero-size files. Sandbox firewall setup reads the kernel data instead of relying on the reported file size. (#1124)
- Buffered ACP prompts respect their deadlines. Timeout handling no longer repeats captured provider history. (#1125)
- Tool captures retain their content and raw input/output. ACP trajectories preserve observations needed for later review. (#1100)
0.7.7 (2026-09-09)
Added
network_mode: denylistblocks a list of URLs and hosts for the agent on Docker and Daytona. The task keeps internet access;blocked_urlsandblocked_hostsare enforced by a root-owned loopback proxy behind the sandbox-user firewall, hosted search tools are switched off per harness, and every refused request lands intrajectory/egress_denylist.jsonl. Other backends refuse the mode at preflight. (#1113)
Fixed
- Denylist egress no longer cuts the agent off from its own model. The
controller registers the running LiteLLM gateway's exact
127.0.0.1:<port>endpoint with the proxy, so clients that ignoreNO_PROXY(Gemini's UndiciProxyAgenttunnels even plain HTTP) still reach it; the exception comes from the live gateway, never task metadata or agent-supplied environment, and every other private destination stays blocked. Reconnects re-register the current port. (#1118) - The denylist is a shared sandbox policy, not a per-harness one. Every supported ACP harness (custom registrations included) gets the same proxy, certificates, and UID firewall on primary connections and later roles alike, independent of harness name, model id, or provider. (#1118)
codex-acpsessions are configured through native ACP settings. Hosted search is switched off viaCODEX_CONFIG.web_search(the CLI ignores-coverrides), and Codex runs inagent-full-accesssession mode when BenchFlow has already selected a non-root sandbox user, so its bubblewrap sandbox is not nested inside Docker or Daytona; BenchFlow's own user, filesystem restrictions, proxy, and firewall still apply. (#1118)cryptography>=44is a core dependency. Denylist egress mints TLS certificates on both Docker and Daytona, so the pin moved out of thesandbox-agentcoreextra. (#1118)
0.7.6 (2026-09-04)
Added
- ACP rollout directories render as an interactive reviewer page.
bench eval view <rollout>now assembles a JSON payload (normalized events + result/timing/verifier metadata) and renders it client-side in a self-contained template (no build step, zero network requests): full event stream with collapse instead of truncation, harness/model/skills identity row, reward badge andresult.jsonfailure-diagnostic banners, Verifier and Metrics tabs, Focus/Full modes, per-kind filters and hues, text search, and per-event#e42anchors. Trajectory content is treated as untrusted end to end (data-only embedding,textContentrendering). Raw session JSONL files and legacyturn*.txtruns remain supported, and the--confirmapprove/reject contract remains compatible. - Directories of rollouts serve a run catalog. Pointing
bench eval viewat a job directory (or a wholejobs/tree) serves a browsable index: corpus counts, grouping by task or model + harness with per-group pass/fail aggregates and pass rates, sorting by name/reward/duration/cost, text filtering, collapsible groups with incremental pagination, and URL-preserved state; selecting a run opens the detail page and the back control restores the exact catalog view. Traces load dynamically from/api/rollout?id=…(ids resolve only by exact membership in a fresh directory scan, so crafted ids cannot reach the filesystem) and?run=<id>deep-links a run.--confirmon a multi-run directory errors out: a confirmation needs exactly one trajectory. hf://sources browse HuggingFace trajectory datasets directly.bench eval view hf://<org>/<dataset>[@revision][/subpath]fetches the viewer-relevant slice of a trajectory dataset into the sharedhuggingface_hubcache and serves it through browse mode, making the community ground-truth uploads reviewable with one command. The download allowlist is exact; trajectories, result/timing/prompts, and the four verifier sidecars the viewer renders; with no wildcards, so large run artifacts andllm_trajectory/trainerexports are never fetched. The CLI passes the spec as a string into a typedLocalPathSource | HfDatasetSourceparser (never throughpathlib.Path, whose normalization would manglehf://intohf:/), and dataset subpaths are validated.- The viewer renders per-event timelines the moment captures provide
timestamps. Steps gain a
+m:ssoffset chip and tool calls a duration chip whenever events carryts/started_at/finished_atfields; with today's captures (which carry none) nothing changes visually. - Z.ai Coding Plan routing. Coding Plan subscriptions route through the provider layer, with validated generation parameters forwarded and explicit provider endpoints preserved by the LiteLLM proxy. (#1074)
bench eval runpublishes job artifacts and eval results.--publish-bucketsyncs a job directory to a HuggingFace storage bucket (an alternative to--publish-hf's dataset-repo upload), and--eval-results-model/-dataset/-taskopens a community eval-results PR (.eval_results/*.yaml) on a model's HF repo, scored from the run's mean reward. (#1035)
Fixed
claude-fable-5-1can run: theclaude-agent-acppin moves 0.40.0 to 0.73.0. The old pin bundled@anthropic-ai/claude-agent-sdk0.3.160, and the model rejects Claude Code older than 2.1.251 withclaude_code_version_too_old(HTTP 400); so every rollout failed on its first API call. The new pin bundles sdk 0.3.257, and theset_config_option"model"/"effort"wiring is re-verified against it bytests/test_acp_pinned_protocol_guard.py. (#1086)codex-acpdispatch modernized. The shim pin moves to 1.6.0 and reasoning effort travels inside the model id (modelId[effort]) throughsession/set_model, rather than a config option the shim rejects. (#1044)- ACP usage and terminal evidence survive an agent timeout, instead of being dropped with the timed-out turn. (#1080)
- A pending tool call can no longer defer the idle watchdog without bound: the deferral is capped. (#1066)
bench evalresume re-runs infra-retryable verifier-errored tasks rather than treating the infrastructure failure as a settled result. (#1063)- Sandbox hardening execs draw on the verifier-setup budget, so hardening no longer competes with the agent's own time. (#1062)
--trials > 1without--matrixis rejected up front. (#1064)- Healthy native ACP subscription results are preserved. (#1049)
- The agent judge parses native edit targets. (#1069)
- Non-dict rewards no longer break completed-outcome classification. (#1054)
- Codex honors proxy-owned model selection. (#1076)
- Gemini routing corrections. ACP model ids fixed and wrapped ids normalized, Gemma routing supported with pass-through usage captured, every Google key alias routed through the proxy, Google Gemini gateway routes normalized, and headless runs trust the sandbox workspace.
- LiteLLM provisions the Vertex sandbox runtime. (#985)
- OpenClaw caps supported model output tokens.
- RestrictedPython updated to 8.3.
0.7.5 (2026-08-19)
Added
- Weighted rubric-review contract (v0.2).
bench reviewnow accepts the versionlessrubric.jsonshape, where every criterion adds strictblocker(0or1) andweight(1through10) fields. Binary blockers gate publication; non-blockers receive weighted0/1/2scores with raw and gated quality plus publication bands in the report. Blocker weights are excluded from quality, and the wrapper reward remains a structural-validity signal. Existing three-field v0.1 rubrics keep theirpass/fail/not_applicablebehavior unchanged. Docker runs now probe whether verifier-log bind mounts are genuinely visible to the container and fall back to explicit copy-out when path translation is unavailable.
Fixed
- Linked-worktree
.gitpointer files stay out of workspace attachments. Workspace capture now excludes.gitfiles as well as directories, preventing local absolute worktree metadata from entering uploaded archives. (#1032)
0.7.4 (2026-08-16)
Added
- Uploads are confirmed all the way into cloud storage. After the
progress bar finishes,
bench traj uploadnow polls the contribution service's newGET /v1/uploads/{digest}capture-status endpoint (the validation ledger) until the validator's verdict:✓ Verified in cloud storageonce the capture is promoted tosources/community/<digest>/, a concise exit-1 error with the fixable detail if the validator rejects it, and abench traj status sha256:<digest>handoff line if validation is still running when the budget (default 240 s,BENCHFLOW_TRAJ_WAIT_SECONDSoverride,--no-waitopt-out) runs out. A handshake 409 ("already submitted") prints the verified line immediately, and a deployed broker that predates the endpoint (404) keeps today's behavior unchanged. The newbench traj status DIGESTcommand runs one check on demand. Status polls consume a separate, higher rate-limit budget (TRAJ_STATUS_RATE_LIMIT, default 720/hour/IP) and reveal only the ledger state, the bounded rejection detail, and the public promotion prefix; never contributor identity or quarantine internals. The broker must be redeployed (deploy-trajectory-uploadworkflow orscripts/deploy.sh) before the endpoint answers in production; the CLI degrades gracefully until then. - The
bench trajfamily shares one polished terminal design language. A new presentation-only kit (cli/_traj_tui.py) givestraj setup,traj upload, andtraj statusa coherent look: a◆ benchflow · <command>banner, styled◇input prompts, an arrow-key recent-session picker (↑/↓, 1-9 jump, esc to fall back to typing a path) on real terminals, colored step kinds in the report preview matching the browser viewer's palette, and rounded panels. Every interactive affordance degrades to the exact previous prompt-driven flow off-TTY (agents, pipes, CI, Windows), and all machine-read lines (Masked for you:,Digest:,Repo:) stay plain. - Uploads can carry the session's workspace folder as a zip attachment.
bench traj uploadreads the session's recorded working directory (the same Claudecwd/ Codexsession_metaprovenance as repo tagging) and archives it into the capture asworkspace/<name>.zip, printingWorkspace: <path> (from session cwd; use --no-workspace to omit)and aWorkspace attached:line with size, file count, and exclusion count. VCS internals, dependency trees, caches, symlinks, and secret-shaped filenames (.env*,*.pem,*.key,id_rsa*,.netrc, …) never enter the archive; everything else is archived as-is without content redaction, and the attach line says so. Workspaces over 1 GiB (measured before compression, so the zip is never created), over 50,000 files, missing, or empty are skipped with a printed reason instead of failing the upload. When detection fails on a real terminal, one optional prompt accepts a folder or skips on Enter;--workspace-diroverrides detection and--no-workspaceopts out. The archive is staged in the upload's temporary directory and always deleted afterwards. Server side, the contribution service accepts the newworkspace/*.zipnamespace with a 1 GiB per-archive cap (trajectory JSONL keeps 128 MiB), allows at most one archive per capture and never an archive alone, verifies the zip container format instead of JSONL strictness, promotes it with anapplication/zipcontent type, and scopes trajectory-report cross-checks to trajectory artifacts so an attachment cannot fail report equality.
0.7.3 (2026-08-16)
Added
bench traj uploadwaits out short rate-limit responses instead of failing. When the contribution service answers 429 with a shortRetry-After, the handshake now sleeps it out with jitter and retries up to three times (two-minute cap per wait), so a crowd of simultaneous contributors self-heals instead of surfacing errors. Longer waits still fail fast with the actionable retry-after message. (#1027)
Changed
- The contribution service rate-limits per contributor, not per venue. Upload budgets are token buckets keyed on contributor identity with a wide per-IP abuse backstop, refill continuously, and answer 429 with seconds-until-next-token instead of the remainder of the clock hour, so many contributors behind one NAT no longer starve each other. Contended bucket updates back off with jitter rather than shedding simultaneous crowds. (#1027, #1028)
0.7.2 (2026-08-16)
Added
- The upload preview itemizes what redaction masked, by kind. The
redactor now categorizes every replacement by the rule that fired; API
keys, bearer tokens, private key blocks, passwords, URL credentials, and
credential-bearing field values; and the terminal trajectory report shows
a
Masked for you: 2 API keys, 1 bearer token; originals never leave this machinebreakdown under the masked-count row, plus a reassurance that redaction ran locally and the server independently rescans staged artifacts (orNo secrets or personal identifiers detected; nothing needed masking.when nothing matched).bench traj upload --dry-runprints the same breakdown as a plainMasked for you:line, andbench eval view --confirmgains a display-only--redaction-summaryflag that renders it in the confirm bar next to the Approve button; thebenchflow-traj-uploadskill stages a dry run first and passes the line through. The totalredaction_replacementscount and the manifesttrajectory_reportcontract are unchanged (the server validates the report with a closed schema and exact recompute equality, so per-category counts stay display-only). - The trajectory viewer can collect the eval-prize confirmation in the
browser.
bench eval view PATH --confirmrenders the normal page plus a sticky site-styled bottom bar ("Submit this trajectory to the BenchFlow eval prize?") with Approve & submit / Not this one buttons. A click POSTs to/decision; the server prints a machine-readableDECISION: approvedorDECISION: rejectedline to stdout, shuts down, and the CLI exits0on approve and3on reject (non-1/2 so rejection never collides with error or usage exits). Without the flag, behavior is unchanged (no bar, no endpoint, Ctrl+C to stop). Thebenchflow-traj-uploadskill now prefers the button flow and falls back to chat confirmation on CLIs older than 0.7.2. - Trajectory uploads are tagged with the session's repository by
default. Unless
--source-idis given,bench traj uploadreads the session's recorded working directory (Claudecwdevents, Codexsession_meta), resolves its gitoriginremote, and storesrepo/<owner>/<name>as the manifest source id, printingRepo: owner/name (from session cwd /path; use --no-repo to omit)(the local path is terminal output only, never uploaded).--no-repoopts out ; thebenchflow-traj-uploadskill now surfaces the detected tag during the confirm step so contributors can decline it for private repos; and undetectable repos fall back silently to the path-derived source id.
Fixed
- The repo tag derives only from the session's own recorded cwd. The
initial repo-tagging implementation (#1015) fell back to the upload
invocation directory's git remote when the session cwd yielded nothing; a
collector-side audit showed this mis-attributes provenance; a session
recorded in a non-repo directory, uploaded from the benchflow checkout,
was tagged
repo/benchflow-ai/benchflow(two community-dataset entries carry the mis-tag). The fallback is removed: no session cwd, a missing directory, or no GitHub remote now mean no repo tag, exactly like--no-repo. - The trajectory viewer header no longer shows
?badges on real Claude Code sessions.bench eval viewon a~/.claudesession JSONL renderedmodel: ?,session: ?...,claude code: ?, andtotal cost: $0.0000because the header only read atype: systemevent that real session files don't contain. The header now derives its metadata from what the file actually carries (first assistant event'smessage.model, per-eventversion/sessionId, the filename stem as a session fallback) and hides any badge whose value is unknown; including the cost badge when no event carries cost data. Presentation only. - Upload progress no longer claims the broker is waking up. A warm
retry printed
Uploading… the first request can take a minute while the service wakes upeven when the service was already up. The line is nowUploading… this can take up to a minute; retries are safe. bench traj setup --listno longer wraps session paths mid-token. Each hit prints index/source/time, then the path on its own line, then the snippet, via plainprintso Rich does not split a long JSONL path and break copy-paste.
Changed
- Trajectory viewer tool calls are color-coded and backgrounds are light.
Each tool kind now gets a muted GitHub-label-style accent on its name pill
and a left border strip on the card: shell/exec to amber, write/edit to blue,
read to teal, agent/task/skill to purple, web/search/fetch to cyan, everything
else to neutral gray. Tool arguments and tool outputs render on light
surfaces (
#f5f5f5/ white with dark ink) instead of near-black blocks; the dark#141414terminal treatment is reserved for shell-command output only, and the ink-black result card stays as the deliberate bento-ink accent. Presentation only (CSS classes + a tool-name to accent mapping); content strings and behavior are unchanged. Applies to all three viewer templates, which share one stylesheet since #1019. - The contributor prompt now tells the agent to upgrade BenchFlow first.
CONTRIBUTOR_PROMPT(kept in sync inREADME.md,docs/traj-upload.md, and thebenchflow-traj-uploadskill evals) is a three-line block that says to runuv tool install --python 3.12 --upgrade --force benchflowbefore reading the skill, so agents that skim the skill or hit a stale copy still install the latest CLI. The prompt also names OpenCode and Cursor sessions and the re:Agent hackathon 72-hour window; README/docs render it as a blockquote (soft-wraps on GitHub) behind an explicit "send this to your coding agent" framing, andbench traj setupprints the same framing. The skill's Discover step gains best-effort Cursor (~/.cursor/projects/*/agent-transcripts/) and OpenCode (~/.local/share/opencode/opencode.db,opencode db path) locations plus a prefer-recent-matching-sessions note. Follow-up to the version precondition from #1013/#1014. - Trajectory viewer restyled to match www.benchflow.ai. All three
bench eval viewpages (stream-json/JSONL, ACP events, multi-turn trial) now share one inline stylesheet with the site's design language: light monochrome palette (#fafafapage, white cards,#0a0a0aink, dark#141414code blocks), Satoshi/Google Sans Code font stacks with system-safe fallbacks, mono pill badges, and a small BenchFlow wordmark header with the inline SVG logo. Pages remain fully offline (no external font or CDN requests) and content/structure semantics are unchanged; follows the contributor paste-line flow from #1013.
0.7.1 (2026-08-16)
Added
bench traj setup/bench traj uploadprint an upgrade hint when outdated. Both commands start with a lightweight PyPI latest-version check (2 s timeout, completely silent on any network or parse failure) and print a one-lineuv tool install --python 3.12 --upgrade --force benchflowhint when the installed version is older than the latest release; dev/prereleases of a newer-or-equal base are not outdated.BENCHFLOW_SKIP_UPDATE_CHECK=1disables the check. Thebenchflow-traj-uploadskill and docs now tell contributors to upgrade to the latest BenchFlow (0.7.1+) before using the trajectory-upload flow.
Changed
- Trajectory contribution is a copy-paste line, plus optional setup.
Contributors paste one line into their agent; the agent reads
benchflow-traj-upload, opens the viewer, then uploads. Optional setup isnpx skills add benchflow-ai/benchflow --skill benchflow-traj-uploadorbench traj setup.bench eval viewaccepts a raw session JSONL file and does not writetrajectory.htmlnext to it. The CLI infers GitHub username and email fromgh/gitbefore prompting, printsSubmitted/Already submittedplus a digest for public uploads (not a private Azure inbox URL), waits up to 90s for broker cold start, and treats Azure403 UnauthorizedBlobOverwriteas an idempotent skip. (#1008's interactive report, local secret masking to<XXX-benchflow-key-values-XXX>, schema-1.2 manifest report binding, and byte progress are included; the PR #1008 operator manual now lives atbenchflow-traj-upload-opsso the public skill name stays contributor-facing.)
0.6.9 (2026-08-15)
Changed
- Trajectory uploads require contributor provenance. The single
bench traj uploadcommand now requires--github-idand--email; both values are validated locally and server-side and stored inmanifest.jsonunder the structuredcontributorfield. Existing 0.6.8 manifests remain readable by the validator during the client transition.
0.6.8 (2026-08-15)
Added
- Public trajectory contribution.
bench traj upload PATHvalidates and structurally redacts trajectory JSONL, creates a content-addressed manifest, and uploads through the built-in public broker. Azure Blob quarantine, versioning, short-lived create-only user-delegation SAS grants, and an event-driven fail-closed validator keep untrusted captures out of the community namespace until hashes, sizes, strict JSONL (including duplicate-key and non-finite-number rejection), and artifact/manifest secret scans pass. Replaying an ingested digest is a no-op; trusted operators can opt into direct Azure upload with theazureextra and--direct.
Changed
- BREAKING (task.md): the
environment:frontmatter key is renamed tosandbox:. The native task-config surface now accepts onlysandbox:(plusverifier.sandbox:for the verifier's separate sandbox spec andverifier.sandbox_mode:for shared/separate selection);environment:,verifier.environment:, andverifier.environment_mode:no longer validate and fail with an actionable message naming the rename. The one-line fix for existing task.md files is renaming the key. Legacytask.tomlimports are unaffected: the toml loader converts[environment],[verifier.environment], andenvironment_modeto thesandboxspellings (declaring both spellings in one file is an error), andbench tasks exportemits the inverse ; an[environment]-spelledtask.toml. All native emitters ;model_dump_toml,bench tasks migrate, task scaffolding, skill-eval/trace/adapter task generation, rubric-review wrappers; now writesandbox. Python API: the compat propertyTaskConfig.environmentis removed (useTaskConfig.sandbox),VerifierConfig.environment/environment_modebecamesandbox/sandbox_mode, and theVerifierEnvironmentModeenum is nowVerifierSandboxMode. The Environment plane (--environment-manifest,benchflow.environment.manifest, the eval-configenvironment:docker/daytona selector) is a different subsystem and is unchanged.
Fixed
- Coherent integration coverage for code-and-fixture changes. The credential-free fixture job now tests pull-request source together with its task fixtures, while the secret-bearing smoke job retains trusted fixtures; the release gate requires both results. (#969)
- Retryable Daytona transport failures stay retryable. Transient SDK transport errors are stamped while their vendor type is still available, empty exception messages retain useful type information, and permanent errors remain outside the retry policy. (#970)
- Live token counters no longer freeze behind callback-log reads. Larger, bounded ranged reads distinguish EOF from read failure, expose lag, and keep terminal phase labels from moving backwards. (#971)
- Fresh OpenClaw installs and GPT-5.4 calls use compatible limits. OpenClaw is pinned to a Node-compatible runtime, and raw plus ACP-alias GPT-5.4 model IDs clamp output tokens to the provider's 128,000-token limit. (#977, #986)
- Anthropic Vertex routes include their required runtime. The gateway now installs the Google Cloud Vertex AI SDK used by LiteLLM's Anthropic Vertex path. (#978)
- ACP subprocess diagnostics survive normal teardown. A bounded, redacted stderr tail is retained even when stdout remains open; stderr drain failures cannot mask the structured transport diagnosis, and repeated process close calls are idempotent. (#980)
0.6.7 (2026-08-09)
Added
-
Built-in environment registry. The committed env-axis pins (
env0@prod,env0@outage) moved frombenchmarks/_environments/into the package (benchflow/environment/_registry/) and ship inside the wheel, so--environment-manifest env0@prodresolves on a barepip install benchflowwith no checkout and no env vars.$BENCHFLOW_ENV_REGISTRY, when set, still wins entirely; resolution stays content-addressed (sha256 logged), and unknown names now error listing the available specs. (#961) -
Console progress heartbeat. Single-concurrency eval runs print a throttled progress line (
… 6.2min, 12 tool calls (last: …)) about every 45 seconds while the agent works, so a long prompt is distinguishable from a hang. The heartbeat is auto-gated off for multi-concurrency jobs;bench eval run --quietsuppresses it, andBENCHFLOW_PROGRESS=on/offoverrides the auto-gate. (#951) -
Live per-task activity in the eval dashboard. Under a TTY the running-now table gains an activity column ("38 calls · last: file_editor", plus tokens once a usage snapshot exists), polled from the ACP session's existing heartbeat counters. The agents-manifest autoload also clones quietly; one "Cloning …" summary line instead of raw git progress. (#956)
-
Phase labels and per-task failure reasons. The activity cell is never blank while a row exists: sandbox create, agent install, and verify show dim phase labels ("creating sandbox…", "installing agent…", "verifying…"). The final block prints one dim
✗ task: reasonline per failed task; verifier error first, else a compact reward/metric breakdown, else the reward; capped at 5 lines.--quietnow silences the dashboard as well as the heartbeat. (#957) -
Failure reasons mined from verifier artifacts. When a displayed failure would read as a bare "reward X", the CLI reads a small bounded artifact from the rollout's own verifier dir; the CTRF report (first failed test plus its assertion line) or a tail of
test-stdout.txt: and a dim "(details: …/verifier)" pointer names the artifact directory. Artifacts resolve by the recorded rollout name, never by glob. (#959) -
Fractional rewards in console summaries. Per-task lines carry the scored reward (
✗ task (reward=0.30, tools=47)), the Score line renders ", mean reward 0.30" alongside the binarized counts, andEvaluationResult/summary.jsongain amean_rewardfield (mean over scored rollouts; errors excluded, not zeroed). (#960) -
Full failure counts on per-task console lines. A CTRF report with more than one failed test rolls up as " (+N more failure(s); P/T checks passed)" after the first failure; the count suffix is never truncated away; and parametrized test names keep their
[param]ids whenever the report carries them. (#962) -
Live token usage in the dashboard footer. The footer sums completed tasks' trusted telemetry plus every running rollout's live ACP session usage, so spend is visible while the run executes (usage lands per completed prompt; cost stays scoring-time from the gateway log). Single-tool agents' activity cell drops the redundant "last:" suffix in favor of "38 calls · 412.0k tok" once tokens are available. (#963)
-
env0-shaped failure breakdowns. The failure-reason tiers understand metrics nested one level under
metrics/details, pair<name>_foundwith<name>_totalinto fractions ("deadlines 1/5", lowest-signal first), probeverifier/reward.jsonon disk between the CTRF and stdout tiers, and every failure block with on-disk verifier artifacts gets one "(details: …)" pointer. (#964) -
Mid-prompt live token usage via the gateway's live capture. The #963 live tokens stepped forward only when an ACP prompt completed, so a single-prompt rollout showed "; tokens" for its whole agent phase. The proxy runtime's existing live-capture loop (which already tails the sandbox gateway's callback log every second) now also accumulates provider tokens into an O(1) counter, and the dashboard reconciles the two non-decreasing live signals as max(ACP, gateway); display-only, still replaced by the trusted scoring import at completion, and any gateway-side failure degrades to the ACP-only behavior. (#965)
-
Compact flow-style arrays in emitted task.md frontmatter. Short scalar-only lists render on one line (
tags: [parsing, nlp]) instead of multiline bullets, so hand-written flow arrays survivebench tasks migrate/ normalize round-trips. The style predicate measures each list with the real YAML emitter (so it can never disagree with PyYAML's own quoting) and falls back to block style for long, nested, or multiline items. Parsing is unchanged; both styles were always accepted. (#967)
Changed
- Migrated the clawsbench
archive-amazon-shippingtask to the nativetask.mdpackage layout (bench tasks migrate --remove-legacy), and a clawsbench run launched without--environment-manifestnow fails with an actionable verifier message naming the exact flag to pass instead of an opaque connection error. (#952)
Fixed
- Isolated pull-request and post-test integration concurrency groups so a credential-free workflow completion cannot cancel an active PR rollout.
- Host-side hard rollout deadline. An await wedged below the phase-level
watchdogs (e.g. a Daytona PTY teardown on a dead websocket) could freeze an
entire eval job. Every rollout attempt now runs under a host-side hard
deadline derived from the task's own phase budgets plus a fixed margin; a
trip returns a retryable infra-error result and abandons the sandbox after
a bounded cleanup grace. Override with
BENCHFLOW_ROLLOUT_HARD_DEADLINE(seconds;off/none/0disables). Verifier timeouts that produced zero output are retried once; that signature is an exec-layer wedge, not a slow verifier. Agent-sent reasoning parameters are also forwarded through the LLM gateway, and DeepSeek routes natively withreasoning_effortpassed verbatim. (#949) - Quieter Daytona teardown: successful runs end at the score line; the
atexit client cleanup no longer prints a cancelled-reader traceback after
✓ Score, and benign engineio PTY-disconnect errors are silenced. (#951) - Accept the task.toml schema 1.3
[task] versionfield in task configs. It is informational and stored verbatim; the strict schema previously rejected it, which made every schema-1.3 curated task unloadable. (#953) - Synced the committed
env0@prod/env0@outageenvironment pins with the upstreambenchflow-ai/env0manifest (the pins had drifted to stale service CLI names, a phantom service, and shifted ports), and a manifest environment now fails loud when it declares services but none are startable in the image, instead of passing a vacuous readiness gate and dying at the verifier. (#954) - Pointed the built-in
env0@prod/env0@outagepins at the org-ownedghcr.io/benchflow-ai/env0:0.2.0base image. The wheel-shipped pins named a personal Docker Hub image while their own comments and the environment docs declared the ghcr image authoritative;base_imageis recorded as rollout provenance, so every env0 result carried the wrong base. Verified on Daytona (env0auth-least-privilege-summary, 8/8 services ready, reward 1.0). - Restored the
env0@outageperturbation (gmail and slack removed relative toenv0@prod) that the #954 upstream sync had erased by mirroring the full service list into both pins, and corrected the pin header's stale slack port. (#955) - ACP protocol JSON glued to PTY shell noise now decodes. On PTY
transports, an agent's initialize response could arrive on the same line
as the shell prompt (ANSI/OSC-prefixed), fail the strict whole-line JSON
parse, and get filed as noise; the handshake then "timed out" at any
window with the answer sitting in the agent log. The PTY-facing transport
now retries from the first
{and once more after an ANSI scrub, but only until the first successfully decoded protocol message per connection; afterwards the strict contract rules, so log-echoed envelopes cannot impersonate protocol traffic mid-session. The pre-prompt handshake window is also env-configurable viaBENCHFLOW_ACP_HANDSHAKE_TIMEOUT(seconds; default 60). (#958)
0.6.6 (2026-08-04)
Added
- Apple Container and Amazon Bedrock AgentCore sandboxes. Apple Silicon
users can run supported single-container arm64 tasks through Apple's
Virtualization.framework, while
--sandbox agentcorebuilds task-specific AWS runtimes with lease-aware cleanup for supported public-network, single-container arm64 tasks. (#936, #937) - Rubric review (
bench review). Detached agentic grading of finished rollouts against arubric.json: rubric contract v0.1: an object with acriterialist, each entry carrying aname(structured-output field), adescription(author documentation, never shown to the reviewer), andguidance(the grading contract). The document carries no in-file version key. One reviewer agent per rollout runs as an ordinary rollout of a throwaway wrapper task on a digest-pinned multi-architecture base image and no task-authored Dockerfile (AgentCore still builds a derived runtime image), reads a read-only evidence copy of the rollout and, when admitted from a trusted--tasks-rootwith a verified digest, its task, and answers every criterion withpass/fail/not_applicableplus an explanation. The wrapper's own reward means only "the reviewer produced a structurally valid result"; graded outcomes land inreview_report.json. Reviews never modify a reviewed rollout's rewards orresult.json. Rubric resolution:-r> the task's ownverifier/rubric.json> a built-in default (reward_hacking,task_specification).--passing/--failingfilter the rollouts under review; a job-level prose summary aggregates multi-rollout runs. The default reviewer harness isopencode(pinned to1.18.11). Evidence mounts at/evidenceoutside the agent workdir (root-owned, unwritable by the reviewer), symlinks are dropped rather than dereferenced, task skills, shipped rubrics, and cumulative provider-history trajectories are excluded while the canonical ACP trajectory is retained; dropped ACP tool observations and generic tool titles are repaired from exact-ID events in the trusted provider capture, reviewer egress is gateway-scoped via the sandbox lockdown flag, artifact consumption is pinned to each invocation's unique runtime leaf, and the job summary is a deterministic aggregation. - Sealed AgentCore uploads. Every AgentCore upload and staged
environment is now encrypted end-to-end: the sandbox generates a
keypair, only the public key appears in command output, payloads travel
as AES-256-CTR ciphertext with an HMAC-SHA256 tag over IV and
ciphertext (verified before decryption), and the decrypted key never
appears in command text. Fixes provider credentials from
launch_config.jsonand command environments being recoverable from the runtime's CloudWatch command log; the generated wrapper image installsopensslwhen missing. Downloads are not sealed: they return file contents as base64 through command output, so they must only carry non-secret run artifacts. RolloutConfig.uploads. Generic post-start host to sandbox uploads (directory or file to absolute sandbox path), used by rubric review to deliver evidence into prebuilt-image sandboxes and available to any caller whose task data is not baked into the image.- Native TRL tool-calling SFT export.
bench train convert --format trl-sftemits conversational prompt/completion rows with atoolscolumn, excludes OpenCode title/summary helper calls, and accepts rollout trees, canonicalresults.jsonl, or existing TRL JSONL.bench train validate --format trl-sftcan render rows with a pinned tokenizer and fail closed on missing assistant masks or overlength samples. Tokenizer-aware--context-policy message-windowkeeps the harness/task prefix and the longest complete recent assistant/tool suffix when exact rows exceed the student context window. (#925) - LLM call-purpose provenance. Captured LLM exchanges retain provider/model
metadata and classify agent, title, summary, compaction, and helper calls;
results.jsonlcarries the metadata on each trajectory step. (#925) - Live trajectory streaming and token logprobs. Redacted LLM trajectory
snapshots are written during active rollouts, and training runs can opt into
sampled-token logprobs with
BENCHFLOW_CAPTURE_TOKEN_LOGPROBS=1. (#922, #926)
Changed
- OpenHands supports requested reasoning effort through its LiteLLM request body and now fails closed if completed provider exchanges are missing from trainer trajectories. The built-in OpenCode harness is version-pinned for reproducibility. (#921, #931)
Fixed
- Restored the documented
testto liveintegration-lightto internal-preview publication chain for successfulmainpushes, pinned to the exact tested commit and fail-closed against untrusted workflow sources. - Fixed OpenHands startup outside root-owned workdirs, OpenCode gateway setup in non-Python task images, Qwen3.5 TRL SFT tokenization, and incomplete agent skill-path validation. (#919, #924, #929, #932)
- Moved recursive review evidence work and blocking platform probes off the async event loop so concurrent reviews and rollouts retain responsive scheduling and timeouts.
- Remove staged Docker and Apple Container credential files when live process launch fails before the agent can source and unlink them.
- Corrected release-facing authentication, sandbox, trajectory-artifact, branching, and provider documentation to match the shipped interfaces.
0.6.5 (2026-07-10)
Added
- Reproducible evaluation artifact workflows.
bench eval runcan emit task/run manifests, trajectory-health summaries, canonical one-rollout-per-task selections, materialized trainer inputs, repeated model matrices, and Hugging Face dataset uploads with source provenance. (#844, #907) - Prime-RL supervised fine-tuning integration.
bench train validateandbench train run sft --backend prime-rladd fail-closed trainer-row checks, local/Hugging Face dataset staging, publishing hooks, and compatibility controls for reproducing the Mobile300 PR828 training recipe. (#842 to #865) - Native external benchmark adapters. BenchFlow can materialize MCP Atlas and Toolathlon sources, credential-backed task packages, and the service sidecars required by their hosted runtimes. (#878, #885, #889)
- Registry-driven agent extension. Agent packages can autoload through
benchflow.agentsentry points or declarative manifests, including namespace shorthand for externally registered agents. (#873, #877) - BenchFlow-native GRPO/TRL pipeline. Adds reusable
TaskRuntime, the optionalBenchFlowSpecTRL adapter, selective Hugging Face task snapshots, pairedbench eval compare-liftreporting, and an end-to-end GRPO runbook. (#901 to #907)
Changed
- BenchFlow CLI and SDK installations now explicitly require Python 3.12 or
newer, with
uv tool install --python 3.12as the recommended CLI path. (#899)
Fixed
- Hardened LiteLLM and provider routing across Responses/chat bridges, Gemini custom endpoints, Claude OAuth, Harvey LAB, OpenHands Azure reasoning effort, diagnostics, and parallel ACP shim startup. (#868, #871, #879 to #881, #886, #888, #911)
- Stabilized Daytona execution for Toolathlon and ACP agents, including DinD retries, PTY handling, orphan-free long-command heartbeats, and Gemini's SSH transport fallback. (#890, #892 to #896, #910, #912)
- Tightened trajectory and Prime-SFT artifact integrity with secret redaction, tool-call validation, repaired results conversion, and reproducible compatibility controls. (#849 to #865)
Removed
BENCHFLOW_SKILL_NUDGEskill prompt nudge. The optional prompt injection that prepended mounted-skill names/descriptions/bodies to the task instruction (#207) is gone. Setting the environment variable now has no effect: prompt resolution never reads skill directories, and mounted skills reach agents only through their native skill paths. This keeps prompts identical across skill modes and closes off accidental prompt-level skill leakage. (#908)
0.6.4 (2026-06-27)
Added
- Environment and config as run-time axes on
bench eval run.--statebinds the environment (S-axis) per run; inline JSON, a registryname@versionresolved through the environment registry, or a manifest path (takes precedence over--environment-manifest).--config-overrideoverlays the task config (C-axis); inline JSON/YAML/TOML or@file, deep-merged into each task's resolved config.--configalso gains a--run-configalias. (#790) - Content-addressed environment binding. Registry environment resolution is
content-addressed (
env_hash = sha256(manifest)) so aname@versionresolves to an exact, pinned environment that is recorded for replay; the C-axis--config-overrideis likewise persisted with its content hash and the applied patch. Every rollout is attributable to the precise world and config it ran against. (#790) - MLE-bench adapter. Adds an MLE-bench benchmark adapter, parity fixture, and task plumbing for running and auditing MLE-bench through BenchFlow. (#792)
- Agent adapter skill. Adds the canonical adapter skill under
.agents/skillsfor harness-side adapter work. (#793) - Prime-RL SFT export. Adds
bench train convert prime-sftsupport for exporting BenchFlow trajectories into Prime SFT-ready JSONL artifacts. (#828)
Changed
bench continueis nowbench eval continue. The command (and itscontinue-batchcompanion) moved under theevalgroup, where it is now discoverable inbench eval --helpalongsiderun/adopt. The original top-levelbench continue/bench continue-batchremain as hidden, deprecated aliases (they print a deprecation notice) so existing scripts keep working. (#800)- Routable agents always go through the LiteLLM usage proxy. OpenCode-family and pi-acp model calls now stay on the proxy path so token usage, cost, and trajectory capture are preserved consistently. (#797, #803, #820)
- Agent manifest loading is now the additive decoupling path. The core agent manifest loader and Omnigent/session-factory seam are gated in while preserving existing ACP manifests and byte-identical parity coverage. (#825, #836, #837)
Fixed
- Resolved the sharded and run-config paths so the S-axis environment and C-axis
config overlay are applied consistently in
bench eval run. (#804) - Added
bench eval run --context-rootplumbing and early validation for missing paths. (#816) - Fixed verifier-error resume logging and streaming
claude-agent-acptrajectory emission so failed or streamed runs retain the expected evidence. (#819, #839) - Resolved bare model IDs to their provider, avoided pi-acp context-window retry storms, and kept provider failure causes visible while preserving redaction. (#805, #831, #834, #835)
- Preserved Codex subscription-auth behavior and auth-file permissions in the launcher path. (#825)
- Rejected
.gitandfile://source paths with clear errors. (#822) - Hardened experiment-review and integration gates around missing trajectories, summaryless roots, file-editor false positives, and L3 review calibration. (#802, #806, #807, #808, #809, #810, #811, #812, #814, #817, #821, #823, #824)
0.6.3 (2026-06-16)
Changed
bench eval createrenamed tobench eval run. The verb now matches what the command does (it runs an evaluation, single task or batch).bench eval createstays as a deprecated alias that prints a deprecation notice on use, so existing scripts, YAML configs, and downstream repos (e.g.benchflow-ai/skillsbench) keep working unchanged. Switch tobench eval run.task.mdis now the sole task authoring format.bench tasks initscaffolds a nativetask.mdpackage (task.md+environment/+oracle/+verifier/).bench tasks init --format legacyis retired and now exits with an error pointing atbench tasks migrate <dir> --remove-legacy. Existing split-layout packages remain readable, andbench tasks migrate/bench tasks exportcontinue to cover the migration and compatibility paths. Authoring docs now lead withtask.md; the split layout is documented only as a migration/export target.
Fixed
bench skills evalnow exits non-zero when any eval case errors (e.g. missing credentials), matchingbench eval run. A 100%-error run printed0/1but exited0, so CI/scripts read a total failure as success.- The "task.md already exists" migrate error now names both surfaces
(
--overwritefor the CLI,overwrite=Truefor the Python API) instead of only the API kwarg. bench eval view <job-dir>no longer shows a blank "No trajectory files found" when given a job directory (the natural value fromeval run's "Artifacts:" line); it now indexes the rollout subdirectories to drill into.bench hub env listprints a footer (Showing N…) with how to refine (--search/--owner/--limit/--json), so a small page of a large catalog no longer reads as "the provider only has N environments".bench tasks check --level publication-gradeerrors for a missing verifier package now include a remediation hint (authorverifier/verifier.md; note thatbench tasks migratedoes not generate it), instead of a dead-end.
0.6.0 (2026-06-13)
Added
- The
task.mdtask standard: a single-file unified task format (parser, verifier planes, prompt sidecars, round-trip export with a machine-readable loss report) plus the authoring CLI:bench tasks init / check / migrate / export, with a layeredcheck --levelladder up to a leaderboard-grade acceptance gate. Seedocs/task-standard.mdand the native authoring guide. bench eval adoptbenchmark-adoption router:initscaffolds a benchmark conversion perbenchmarks/CONVERT.md,convertdrives the hostcodexCLI through the conversion workflow, andverifyruns the parity gate (deterministic per-criterion conversion parity plus the agent-scale reward-distribution layer) and emits a confidence verdict, with a drafted support issue on divergence.bench eval adopt verify --rerunindependently re-executes the benchmark'sparity_test.pyand scores its fresh output (instead of trusting the recordedparity_experiment.json), failing closed if the output is not scoreable;bench eval adopt convert -c key=valuepasses codex config overrides through to the host codex driver (e.g. to work around~/.codexdrift).bench tasks digestrecognizes nativetask.mdtasks as well as legacytask.toml.- ATIF and ADP trajectory artifacts: every scored rollout now emits
trainer/atif.jsonandtrainer/adp.jsonl(alongside the existingverifiers.jsonl), with job-level ADP aggregation. One canonical raw trajectory, multiple ecosystem formats out of the box. - OpenReward (ORS) reward-format interop: export BenchFlow rewards in the
Open Reward Standard shape (
benchflow.adapters.ors) and theors-episodeverifier strategy is recognized. (The hosted-environment episode runner that executes ORS environments end-to-end is in progress, not in this release.) - Daytona sandbox auto-reap: orphaned sandboxes are cleaned at eval start
(TTL-tiered; failure states reaped sooner; an idle-activity guard protects
live runs), gated by
BENCHFLOW_DAYTONA_AUTO_REAP(any of0/false/no/off, case-insensitive, disables it). - Registry-pinned dataset runs:
bench eval create -d name@version(e.g.-d skillsbench@1.1) resolves a dataset from a git-backedregistry.json(see skillsbenchdocs/dataset-versioning.md): tasks are cloned at their pinnedgit_commit_idinto.cache/datasetsand every task directory is verified against its sha256 content digest before anything runs; the entry'sbench_versionrange is checked against the installed benchflow.--registryoverrides the default (skillsbench) registry.result.json/config.jsonare stamped withdataset_name,dataset_version, and a per-tasktask_digest(summary.jsoncarries the name/version);--tasks-dirdev runs carry no dataset fields but still stamp a live-computedtask_digest, so every trajectory stays attributable to exact task content.bench tasks digest <dir>prints the digest for task authoring, andcheck_results.pyaudits the stamps. Seedocs/running-benchmarks.md. (#689, #690, #691;packagingpromoted to a core dependency for thebench_versioncheck.) benchflow continue <run-folder>: resume a previous, unfinished (timed-out)openhandsrun to completion. A standalone tool (it does not touch the normal run path) that reconstructs the run's exact workspace and agent memory from the recordedllm_trajectory.jsonlvia record-replay, then continues with the live model (no injected prompt) and writes a new HF-compatible folder withcontinued_fromprovenance. Seedocs/continue-runs.md.
Changed
bench metricstobench eval metricsandbench viewtobench eval view(the deprecated hidden top-level forms are gone; use theevalsubgroup).- Quickstart and CLI reference now match observed run behavior; the real jobs
directory layout and artifact map, the
<PROVIDER>_API_KEY/<PROVIDER>_BASE_URLconvention, and exit-code semantics. - Document the public vs internal preview install/upgrade command matrix,
including
uv toolexact pins, internal preview upgrades, and the--forcepath for replacing stale entrypoint scripts.
Renamed (aliased; old names removed in 0.7)
- Benchmark adoption is now
bench eval adopt {init,convert,verify}. It lives underevalbecauseevalis the universal benchmark entry point (eval createruns a benchmark;eval adoptmakes a foreign one runnable). Two prior spellings remain as hidden deprecated aliases, each printing a one-line stderr notice pointing atbench eval adopt: the originalbench agent create|run|verify, and the 0.6-dev intermediate top-levelbench adopt.bench agentnow means agent management only (list/show). - The overloaded
bench environmentgroup was split and is now a hidden deprecated alias group (removed in 0.7): the local sandbox lifecycle moved tobench sandbox {create,list,cleanup}, and hosted-provider browsing tobench hub env {list,show,inspect}. The oldbench environment create|list|cleanup|show|inspect(pluslist --provider/--hub) still work, each printing a one-line stderr deprecation notice. The hosted run path stays onbench eval create --source-env.
Removed
- Removed the unwired
OTelCollector(benchflow.OTelCollector/benchflow.trajectories.OTelCollector) and itstrajectories/otel.pymodule. It was a designed-but-never-wired OTLP receiver from the v2 rewrite; never instantiated, never tested, and not part of any run path (BenchFlow captures trajectories via ACP session events and the LiteLLM callback path instead). This drops it from the public__all__; re-add it (with a test + real wiring) if OpenTelemetry-based capture is revived. - Removed two unimplemented stub methods (
read_file,write_file) from the@runtime_checkableSandboxProtocol. No backend implemented them (backends expose theupload_file/download_filefamily) and there were no call sites, so they were a latentisinstancetrap on the contract surface. - Dead-code purge, round 3 (no public-API impact; each symbol re-verified
zero-reference with class context): removed
TaskMetrics.audit_outcome,OTelCollector.endpoint,ReplayRouter.cursor,RuntimeResult.to_run_result(legacy SDK-compat converter, unused), the never-read dataclass fieldsToolCall.outputandJudgeConfig.{reference, prompt_template}, the write-onlyReplayProxy._host, the inertAgentProtocolError.codeannotation, and an unusedretry_if_exception_typeimport + fallback insandbox/daytona.py. - Dead-code purge, round 2 (no public-API impact; each symbol adversarially
verified zero-reference with class context): removed seven unused
*_path@propertys fromTaskPaths/RolloutPaths(readme_path,gitignore_path,verifier_document_path,artifacts_manifest_path,result_path,exception_message_path,log_path), the vestigialModalSandbox.supports_gpus/can_disable_internetcapability properties (not on the Sandbox Protocol), an unused module-levelloggerincli/continue_cmd.py, and the orphanedmcp_service_hooks_from_confighelper. - Dead-code purge (no public-API impact unless noted): removed the unused
job_config_from_yamlhelper, the nominalTASK_REPOSback-compat dict (useTASK_ALIASES), the_looks_like_verifier_dep_install_errorshim (usecontains_verifier_dep_install_marker), the unusedparse_binary_verdictreward helper (useparse_verdict), the deadSandboxBackendtype alias, an unusedStdioTransport._read_bufferfield, and 12 redundantrolloutpackage re-export aliases (submodule definitions unchanged). - Removed the deprecated, hidden
benchflow skills installCLI command. The SDK functionbenchflow.skills.install_skillis unchanged. - Retired the deprecated top-level legacy CLI (
cli/legacy.py). The dead 0.3-erajob/agents/evalcommands are removed;metricsandvieware promoted to first-classbench eval metrics/bench eval view; and the redundantcleanupcommand is dropped in favor of the existingbench environment cleanup. - Removed the
experiments/research/dev tooling tree (never shipped in the wheel) and its 6 dependent test modules, completing the dev-tree cleanup alongside the earlierdashboard/removal andlabs/todocs/labsmigration. Benchmark result files were preserved out-of-tree, not deleted.
Fixed
- CLI errors now go to stderr.
print_error(the single CLI error sink) wrote to stdout, so abench … --json | jqpipeline could get a non-JSON error line on the JSON channel. All CLI errors (and the dataset bench-version remediation hint) now route to stderr; exit codes are unchanged, so failures stay detectable. bench hub env list --jsonnow emits valid JSON at any width. The raw payload was printed through Rich's console, which soft-wrapped long strings and injected literal newlines mid-value (unparseable JSON when piped). It is now written verbatim.- No more raw tracebacks on bad input. Hardened the unguarded front doors a
stress sweep surfaced:
eval create --source-repoclone failures and--tasks-dir <file>;eval viewon corrupt/partial trajectory artifacts (prompts.json, a badacp_trajectory.jsonlline,result.json, a nullsession_id);sandbox createwith an unknown--sandboxbackend or a missing optional sandbox dependency;tasks digeston an unreadable file (single = clean error, batch = warn-and-skip); andhub checkwith a malformed/missing--registry(now a user-meaningful message, not a rawJSONDecodeError/OSError). - Markup-safe output. User/author-controlled strings that look like Rich markup
no longer crash or silently garble output:
eval listjob names,eval metricstitle,skills listcells, andtasks init's reported path are now escaped. skills evalschema errors no longer leak pydantic internals (private model name,[type=…]tags, the pydantic.dev URL); just the actionable per-field text.bench environmentdeprecation notice now fires exactly once (one line, once per process) instead of doubling up with Typer's genericDeprecationWarning, and its aliased verbs are hidden from--help, matching theagent/eval adoptalias families.benchmarks/CONVERT.mdnow references the canonicalbench eval adopt verify(was the deprecatedbench agent verify) in the conversion prompt.bench tasks migrateemits minimal, canonical (schema_version) front matter instead of a full defaults dump.- Verifier
timeout_secis validated as a positive, finite budget (fail-closed at parse time; omission inherits the documented default). - Docker
compose upretries on the daemon network create/attach race. - Console error messages truncate at word boundaries instead of mid-token.
- Recorded sandbox-setup timeouts and trajectory artifacts are consistent across the Docker and Daytona backends.
- The
task.mdinit scaffold is agent-neutral, so--agent oracleworks on a freshly scaffolded task. gemini/-prefixed judge/simulated-user models now resolve to the Google backend instead of passing the slashed name through and 404-ing.- Model-backed judges raise a clear error naming the provider and pointing at
pip install benchflow[judge]when the judge SDK is missing, instead of the misleading "Missing OPENAI_API_KEY" fall-through. bench tasks checkrecognizes a rubric-backedllm-judgeverifier as a valid entrypoint and no longer demands atest.sh.- Pre-verifier disk reclaim is workspace-aware and symlink-safe: it rejects
symlinked cache candidates and realpath-guards every deletion against the
workspace and
/logs, so an agent-planted~/.cachesymlink cannot steer the reclaim into workspace or output state (#601). - Bedrock Claude 4.8+ routes fail closed when LiteLLM's adaptive-thinking patch is inactive, instead of silently sending a request the proxy cannot satisfy (#602).
0.5.2 (2026-06-05)
Changed
- PyPI project README badge: replace the dynamic PyPI version badge with a stable package badge so the rendered project description cannot show a stale external version image after a public release.
- Release documentation refresh: update public install snippets,
release-channel docs, examples, and citation metadata to
0.5.2.
0.5.1 (2026-06-05)
Added
- Daytona usage telemetry by default: Daytona runs now start a sandbox-local provider usage proxy so token/cost telemetry works without an external tunnel; use
--usage-tracking offto bypass proxying when needed. - Azure AI Foundry providers: new
azure-foundry-openai/andazure-foundry-anthropic/prefixes routing through Foundry's unified resource. ExportAZURE_API_KEYplusAZURE_API_ENDPOINT(e.g.https://<resource>.openai.azure.com/); benchflow derives the resource name from the endpoint host, builds the per-surface base URL, and maps the key onto the agent-native auth env automatically. Missing/unrecognized endpoints and unsupported agent/provider protocol pairings fail fast with clear errors instead of falling through to the wrong endpoint. - Azure Foundry auth guidance: agent discovery output and docs now call out that provider-prefixed models can use provider-specific credentials instead of the agent's native/default API key.
Changed
- PyPI project documentation refresh: the public package README, install snippets, release-channel docs, examples, and citation metadata now point at
0.5.1.
Fixed
- Inherit
BENCHFLOW_PROVIDER_BASE_URL/BENCHFLOW_PROVIDER_API_KEYfrom the host environment so self-hosted / OpenAI-compatible endpoints route correctly instead of falling back toapi.openai.com; empty or whitespace-only host values are skipped so they cannot shadow the resolved provider URL (benchflow-ai/skillsbench#817).
0.5.0 (2026-06-04)
Added
- Public/internal preview release channels: tag-driven public releases publish stable PyPI packages and GitHub Releases; merges to
mainpublish internal preview.devNpackages after CI passes. - v0.5 integration evidence: release validation docs now cover urgent blocker closure, SkillsBench infra-fix validation, adapter evidence, trace-to-task evidence, hosted env compatibility, and diagnostic fields.
- Release automation guardrails: public release tags must point at commits contained in
main, version tags must matchpyproject.toml, and PyPI publishing uses Trusted Publishing/OIDC instead of stored tokens.
Changed
mainnow tracks the next public version as0.5.1.dev0; the published public SDK is0.5.0, and internal previews are emitted as0.5.1.dev<N>.- Documentation now directs downstream users to depend on public PyPI releases by default and use prerelease-enabled internal previews only for validation before the next public cut.
Fixed
- Closed the v0.5 release blocker set covering structured sandbox/verifier diagnostics, Daytona startup/export retries, verifier dependency classification, CTRF path consistency, and SkillsBench task compatibility evidence.
0.3.3 (2026-05-15)
Added
- Harvey LAB benchmark: converter, agent shim, and parity validation for 1,251 legal AI tasks (#239).
- Harvey LAB Claude Sonnet judge: switched verifier from Gemini to
claude-sonnet-4-6, matching the original benchmark default (#264). - ProgramBench integration: new benchmark adapter; TB2 removed;
.ref/migrated tobenchmarks/(#237). - CLI progress output:
bench eval create/bench runnow show progress messages by default (#264). - Skill nudge: optional prompt injection for skill-enhanced agent runs (#207).
- Self-generated skill mode for Codex agent (#233).
- Integration test suite, and the
OPENAI_BASE_URLinheritance fix (#255). - Modal backend support: Dockerfile compatibility for Modal environments.
- CITATION.cff (#246).
AGENTS.md: canonical contributor guide;CLAUDE.mddeprecated (#258).
Changed
- Two-field source pattern for dataset sourcing (#252).
- Docs overhaul: synced from www.benchflow.ai; Mintlify config added then orphaned config removed (#259, #257, #226).
uv syncfor package management (#232).
Fixed
- Prevent
TypeErrorinmetrics.collect_metricswhen reward isNone(#243). - Copy eval
requirements.txtinto Docker build context (#245). - Resolve agent aliases in
bench agent showand display aliases inbench agent list(#251). - Guard ACP transports against JSON scalar logs (#236).
- Agent timeout reward fallback for Codex (#234).
- Isolate JS agent runtime installs (#231).
- Route Codex ACP through responses API (#224).
- Deploy skills and forward
solution.envfor oracle runs (#223). - Honor no-internet tasks for agent runs; disable web tools without prompt mutation (#215).
- Propagate
OPENAI_API_KEYfor vllm provider (#215). - Preserve arrival order of thought/message within flush windows (#215).
- Record user messages and per-turn agent text in ACP trajectory (#215).
- Chown skill-link parent dirs so sandbox user can write into them.
- Dynamic
--rootdirinPYTEST_ADDOPTSbased on task workspace. - Unique env-file path in
DaytonaPtyProcessto avoid race conditions (#200).
0.2.3 (2026-04-15)
Added
benchmarks/tb2_multiturn-claude-haiku45.yaml: shipped config for the README's TB2 multi-turn Claude result.- Daytona resource clamping via
BENCHFLOW_DAYTONA_MAX_CPUS/MAX_MEMORY_MB.
Changed
- Renamed
skillsbench-claude-glm5.yamltoskillsbench-claude-glm51.yamlto match the model ID. codex --logincorrection indocs/getting-started.md.- Restricted sdist build to
src/,tests/, and metadata.
Fixed
- Verifier sandbox hardening follow-ups across several base-image and tooling edge cases.
- Preserve trusted verifier path entries and workspace answer files.
- Redirect oracle output to container log.
- Align YAML path resolution to config file location.
0.2.2 (2026-04-13)
Added
- Sandbox hardening tiers 1 to 3: layered defense (env scrubbing, path lockdown, workspace freeze, wider snapshot, oracle privilege drop) blocking F1 to F6 red-team findings.
labs/reward-hack-matrix: per-trial timeout support and 0.2.2 sweep handoff scripts.
Fixed
- Multiple sandbox bypass vectors identified in red-team testing.
0.2.1 (2026-04-12)
Added
- Sandbox hardening on by default:
sandbox_usernow defaults to"agent"(wasNone/root). Blocks conftest-hook and answer-lookup exploit patterns. - Path lockdown: new
sandbox_locked_pathsparameter makes/solutionand/testsread-only before the verifier runs, blocking.pth-injection and similar pre-verify tampering. - Verifier failure isolation: agent errors and verifier errors are now stored separately; a crashing verifier no longer masks the agent result.
labs/benchjack-sandbox-hardening: cookbook demonstrating three exploit patterns (P1 conftest-hook, P2 answer-lookup, P7.pth-injection) and their defenses.
Fixed
- Oracle runs as
sandbox_user: oracle agent now respects path lockdown instead of running as root and bypassing it. - Multi-endpoint provider routing: providers with multiple endpoints now route by the agent's native API protocol.
- Stale API key shadowing subscription auth: emits a warning when
ANTHROPIC_API_KEYenv var is present alongsideclaude logincredentials. - pytest
ini-injection bypass: closed a verifier hardening edge case.
Changed
- Version is now single-sourced via
importlib.metadata; no more duplicate version string in__init__.py. - User-facing docs: new
docs/directory with getting-started guide, CLI reference, architecture overview, task-authoring guide, and labs index. README trimmed; detailed content moved todocs/.
0.2.0 (2026-04-09)
First public release. A near-complete rearchitecture from the 0.1.x era. API surface has changed; assume breaking changes. Future releases will maintain compatibility within the 0.2.x line. 0.1.x users should treat this as a fresh install; see .dev-docs/sdk-reference.md for the new SDK.
Added
- Multi-agent, multi-provider, multi-auth matrix: one YAML config, any supported agent × model × provider × auth combination.
- Subscription auth support: use
claude login,codex --login,geminiOAuth credentials directly. No API keys required for host-based agent workflows. - Vertex AI support: ADC auth for
google-vertex/,anthropic-vertex/,vertex-zai/prefixed models. - Provider registry: add a new LLM endpoint via a dict entry in
providers.py, no code changes. benchmarks/directory with reusable YAML configs and runner scripts for TB2 and SkillsBench.- Auto task download: YAML configs reference datasets as
org/repo/path(e.g.benchflow-ai/skillsbench/tasks). Repos are cloned on first use and cached under.cache/datasets/. benchflow tasks init: scaffold new tasks.benchflow tasks check: validate task structure.benchflow cleanup: delete old sandboxes with--max-agefiltering (default 24h).- Oracle agent support: run
solution/solve.shdirectly for task validation. - Hello-world-task example for sanity-testing the agent pipeline.
- Model generation params via env vars (
BENCHFLOW_TEMPERATURE,BENCHFLOW_TOP_P,BENCHFLOW_MAX_TOKENS). - OpenClaw ACP shim with trajectory parsing and skills support.
- ACP trajectory capture: full multi-turn agent trajectories via ACP protocol.
Changed
- Skill loading: agent-targeted with proper precedence; auto-distributed from
task.tomlskills_dir. openclaw-geminimerged intoopenclaw: provider mode selected at runtime viaBENCHFLOW_PROVIDER_NAME.
Fixed
- API keys leaking in
ps aux: env vars now written inside the container instead of passed via Docker exec-e. - Subscription auth skipped without
-m:benchflow runwithout--modelnow checks correctly. - ADC credentials break with
sandbox_user(#111); credentials written to sandbox user's home instead of/root/. - Daytona sandboxes not cleaned up (#102); auto-delete after max age.
benchflow cleanupignoring--max-age: was deleting everything regardless of age.- readline buffer overflow crashes trial (#98).
- OpenClaw ACP shim loses tool command text (#96).
- OpenClaw ACP shim hardcodes
anthropic/prefix (#95); now routes correctly for Gemini/GLM models. - Oracle agent
PermissionErrorwritingagent/oracle.txt(#91). - Oracle path skips
pre_agent_hooks(#92); services now start before oracle runs. - Richer trial data (#90); richer
result.json, agent logs, per-phase timing. SDK.run()PermissionError:jobs_dirsubdirectories created as root (#88).- Partial trajectory lost on timeout: saved before timeout raises.
- Redundant
--versionbinary check removed; was wasting 30s per trial. - Trajectory fallback: scrapes agent-native files when ACP
session/updateis empty (#94). litellmupgraded to 1.83.0 for CVE-2026-35030; transitive dep security alerts resolved (13 Dependabot alerts closed).
Deprecated
BaseAgentre-export; planned removal in 0.3.0Trialre-export; planned removal in 0.3.0