Skip to content

feat(catalogue): add OSWorld2 with OSWorld-V2's own agents, its services on the machine and a null control - #134

Draft
sirotaoleg wants to merge 31 commits into
ThinkFlowLab:mainfrom
sirotaoleg:feat/osworld2
Draft

sirotaoleg wants to merge 31 commits into
ThinkFlowLab:mainfrom
sirotaoleg:feat/osworld2

Conversation

@sirotaoleg

@sirotaoleg sirotaoleg commented Nov 15, 2026 •

Copy link
Copy Markdown

Stacked on #133, itself on #124: this pull request's own commits are the last five. Until they are merged, the diff shows their files too. It can be merged after both.

What

OSWorld-V2's 108 tasks become a bundled benchmark, osworld2, on the Nullius standard, at the upstream's release osworld-v2.1. Each task is a unit in an Ubuntu VM of its own on the upstream's docker provider, set up by the task's own class and scored from 0 to 1 by its own evaluate(). The tasks and their assets are gated on Hugging Face. Nullius does not fetch them, and the entry is gated (#133): a unit's failure from a model endpoint says its status alone, and the driver names an upstream exception by a known class's name at most, never by its words. The Original setup runs one of OSWorld-V2's own agents, M3 or Muse Spark, chosen by the Agent option, from its published script, with every model call sent to the relay. The Original's models are pinned to their providers on OpenRouter: M3 to MiniMax, Muse Spark to Meta. Claude Sonnet 4.6 is pinned to Anthropic; it is the LLM grader of 21 tasks and the simulated user of 6. Only M3's runs count as comparable with the paper's row. Its Review row and its generated page name the setup "OSWorld-V2 agents". A null control runs one DONE action, then the task's evaluate() on the untouched VM. The entry declares agent.computer/v1. No agent from the Agents page runs it yet. Its texts are one Good to know line and 25 limitations. Settings lists GitLab's root token under Custom keys, for the two GitLab tasks.

The mock websites of 40 tasks are OSWorld-web at the same tag. The guide's steps build them from source and serve them on the machine the units run on. A unit that needs them is refused before its VM starts when they do not answer. For 026 and 041, the driver starts GitLab anew from its snapshot, one task at a time, and Overleaf's 2 tasks get Overleaf started anew. Each of the 8 tasks that ask for a proxy gets a proxy of its own. The options are Steps (100, 300 or 500), Thinking for M3, the website host suffix, the GitLab host and its write share, and disk write limits for each VM. By default a run takes 100 steps with thinking off, as the upstream's M3 script does; the paper ran M3 at 500 steps with thinking on. Muse Spark runs as published at this release, while its published score came from newer upstream code; a limitation says what differs. There are six datasets: All tasks (108, the upstream's test_v2.json), Offline (82), and four by the services their tasks need. Tasks 103 and 104 are not supported: their evaluate() needs FreeCAD on the driver's machine. Review and the run's start ask for Docker and for the scoring programs (ImageMagick, ffmpeg, tesseract) on the driver's machine. As for OSWorld1, the Where step greys out the container card under the Original, whose docker provider needs Docker on the machine. A unit that ends rate-limited is not run again by itself (limits.run_again: false): an episode is long and paid. A guide, docs/benchmarks/osworld2-machine.md, prepares a machine, runs the null control on one task as its last setup step, and says what a run takes in time and money.

Touches

catalogue · templates and static (the mark) · docs and site (docs/benchmarks/, the generated pages, AGENTS.md) · routes (tests only) · packaging (the entry's .yml, .Dockerfile and startup/*.py as package data in pyproject.toml)

Before and after

Before After
Benchmarks page before
The Benchmarks page before this change
Benchmarks page after
OSWorld2 listed, with its mark
Benchmark step before
The builder's Benchmark step under Computer use: OSWorld1, beside the selected GAIA
Benchmark step after
OSWorld1 and OSWorld2
Agents page before
The Agents page: "n/7 benchmarks it runs"
Agents page after
"n/8": OSWorld2 counts, and no agent runs it
Custom keys before
Settings, Custom keys: "No custom keys yet."
Custom keys after
OSWorld2's GITLAB_PRIVATE_TOKEN, not set

Taken at 1400 px, light theme, on a fresh profile; the last three pairs are the top 1000 px of their page.

Verified

  • At 57851a44, on Python 3.12: backend suite (python3 app/tests/parallel.py), 2489 tests in 136 modules, OK; route suite with Node on PATH, 433 tests in 41 modules, OK.
  • OSWorld2's 15 test modules, 371 tests, OK; they need the entry, so none passes without it. 10 are skipped here: they run only when the environment names the upstream with its gated task classes, OSWorld-web or GitLab. Three route test modules and three backend test modules changed for the new entry: the benchmark lists and counts, the Where step's container card, Settings' custom keys, and OSWorld2's cases of gated tasks and pinned routes (test_standard_gated.py, its tests in the class Definition of test_standard_relay_pin.py).
  • In a headless Chromium, light and dark, 1400 and 390 px: the Benchmarks page, OSWorld2's page, the Agents page, Settings and the builder, no page error or cut text. /ui-sweep --only osworld2 at fc0bdb1f, this pull request's third commit: 6 screenshots, none with a page error or cut text.
  • PYTHONPATH=app python3 -m nullius standard check: ok. python3 app/tests/catalogue_docs.py --write, then the check: up to date. python3 app/tests/field_contract.py: ok. app/site/readme_media.py --check: current. The docs check reports the same two findings in AGENTS.md as on feat(standard): choose the serving providers that may answer a model, in the builder and on the command line #124's branch.
  • Where the runs below ran. The commits named here are of the port's working branches, not in this repository:
    • c3637ffd is this pull request's tree with main's two later commits merged in;
    • f74d1a00 and 58c2b586 add one test commit: two JSON files, no code. It gives OSWorld2's Original one more model, MiniMax M3.1 Flash, the name it is served as, its pin to MiniMax and --max_tokens 32768. That model is a test stand, not in this pull request;
    • 58c2b586 is this entry on the core before feat(standard): choose the serving providers that may answer a model, in the builder and on the command line #124;
    • 34e35370 and 8e2e381b are of the port before its move to this repository's history.
  • The controls, recorded in standard.json's proven:
    • Offline, no services, on 2026-11-04, on an earlier commit: the 36 ids are the same twice, and the null control scores 0 on 4 tasks;
    • All tasks, on 2026-11-15, on fc0bdb1f, this pull request's third commit; the fourth only writes this record, and the fifth changes three tests. The null control ran on all 108 tasks, one launch with each task a shard of its own, at 16 MB/s and 160 IOPS a VM, in six lanes with at most seven VMs at a time; it took 1 h 32 min. 108 and 093 failed their setup under that load and passed alone at 24 MB/s and 240 IOPS, in 24 more minutes. The grader cost $0.14. nullius standard prove osworld2 --dataset test_v2 --job nop=<merged job> passed: the 108 ids are the same twice, 0 of 108 at full score, a mean of 0.0021 against a bound of at most 0.01. Five tasks score above 0 on the untouched VM, as a limitation says;
    • every task is in line with the same control on all 108 tasks on 34e35370, which had the same mean; 077's LLM grader gave 0.054 both times.
  • An agent's score on every kind of task, each above the same task's null control (0.0 in each):
    • Seven kinds, on 58c2b586, by the test stand's model in OSWorld-V2's M3 agent, from the command line:

      • desktop apps, 045: 1.0;
      • mock websites, 052: 1.0;
      • Overleaf, 072: 0.775;
      • GitLab, 026: 0.5, a partial score;
      • a proxy, 075: 1.0 (recorded 0.9999999999999999), with the unit's proxy carrying new outbound connections while the agent acted;
      • the public internet, 027: 1.0;
      • the LLM grader, 003: 1.0, with both grader calls answered 200.

      Each of the seven gives its recorded score again at c3637ffd when its outcome is computed from its saved records, with no VM and no evaluate().

    • The eighth, the simulated user: 007, 0.8, by Muse Spark on 8e2e381b; the simulated user answered 7 times. It was not run on this base.

  • Live on this base, against OpenRouter:
    • on c3637ffd, the null control on 087, a graded task: 0.0. The grader's call went to Anthropic by the definition's pin, and standard.served reads held: true;
    • on f74d1a00, the test stand's model on 052: 1.0. All 68 calls went to MiniMax by the test commit's pin and named it, held: true, with no error.
  • This pull request's own agents have no positive score on this base. M3 ran one task here, 017, on the core before feat(standard): choose the serving providers that may answer a model, in the builder and on the command line #124 (90fde73e), and scored 0.0: the agent declared the task failed at its 120th step. Muse Spark ran none. Their scores before the move, on 34e35370 and earlier:
    • M3 on 9 tasks: 070 1.0 twice, 030 1.0, 045 1.0, 013 1.0 then 0.0, 031 0.714, 017 0.429, 010 0.0 three times and once an environment error; 026 and 098 left no score (a full context window, a spend guard);
    • Muse Spark on 3 tasks: 045 1.0, 007 0.8; 052 left no score (a launch error of ours, then a spend guard).
  • Every attempt before the move, two test stands' models included, was recorded and triaged: 61 attempts on 33 tasks. 43 were scored (21 full, 11 partial, 11 zero), 10 ended in an environment error and 8 were stopped by a guard or by hand. None was an agent error. Each of their full and partial scores was checked for the false-success patterns the port's audits found: none.
  • Not run: a positive score of M3 or Muse Spark on this base; the simulated user on this base; task 041 with an agent; a parity run of the paper's 108 tasks; the 10 skipped tests with the upstream named; the docker tier; the site build; a wheel's check.

Risk

  • OSWorld2's driver and OSWorld-V2's scripts run only when someone runs OSWorld2. The Agents page counts eight agent benchmarks, and the generated agent pages gain an OSWorld2 column. No agent there declares agent.computer/v1. Settings shows OSWorld2's GitLab token under Custom keys to every user.
  • Scores. A run compares with the paper's row by its model alone: Docker instead of AWS, no US residential proxy, release osworld-v2.1, the grader through OpenRouter, and by default 100 steps with thinking off. A wrong error kind would move a unit between "environment error" (left out of the adjusted rate) and the agent's error (counted).
  • The machine.
    • VMs, the mock websites, GitLab and the proxies run on the machine the units run on, outside a run's constraints in Settings; -j bounds them.
    • GitLab's Docker in Docker and the websites' image build run as privileged containers.
    • In each of the driver's processes, startup/sitecustomize.py wraps the creation of the VM's container: its network, its ports on 127.0.0.1, its disk and its write limits. The VMs need a free Docker subnet.
    • The VM's clock is UTC+8: a dated task set up before 16:00 UTC and acted on after can score 0.
  • Secrets. GitLab's root token is a licence of the benchmark, handed to the driver only. Model keys stay with the relay. Three tasks' mail profiles lose their saved sign-ins before the VM starts.
  • Stacked on feat(standard): let a benchmark keep its tasks gated, pin its own providers, set its own limits and prove a merged null control #133 and feat(standard): choose the serving providers that may answer a model, in the builder and on the command line #124, with no conflict. Once they are merged, this branch is rebased on main.
  • Undone by reverting the commits; the catalogue entry goes with them.

pierreleca and others added 30 commits November 13, 2026 14:54
On OpenRouter one model is served by several providers, and a run could
neither say which may answer nor see which one did. `--served-by
[MODEL=]SLUG[,SLUG...]`, repeatable, names them for a model (the run's
when MODEL= is left out). Every relay of the run, a container's sidecar
included, puts `provider: {order: [...], allow_fallbacks: false}` on each
request it sends as that model, on any route and in the three API shapes,
in place of the caller's own routing field. A pinned Chat Completions
request asks for OpenRouter's routing facts, where its reply names the
provider that served it. Without the option a request goes on byte for
byte as before.

The option is refused before any unit when the run's provider routes no
model to several (only openrouter does), when it names a model the run
does not call, or when a slug is malformed or given twice.

Each call's record keeps the routing sent (and the caller's own when it
was replaced), the serving provider the reply names, how the reply ended
and an error the reply holds under a 200, which now makes the call a
failed one. The job's config records the pin by model; each trial and
the job say the answered calls by serving provider and, for a pinned
model, whether every one was served by a pinned provider. merge refuses
shards whose pins differ, unless --force.
The run page gains the option and its rules: the slugs, what the relay
sends, the refusals, the record and how a pin reads as held. The standard
page names the record's new fields and the pin among merge's checks, and
the providers page says that a run may choose OpenRouter's serving
providers.
…ge no outcome without a pin

A review found the pin's record could copy credentials, miss calls and
change what a run without the option records and decides. Now:

- The facts of a reply (who served it, how it ended, an error under a
  200) are read by reply_facts, apart from read_reply, which is as
  before: the text, tool calls and usage a record held are read as they
  were, and no fact can raise into the delivery of a reply. Every choice
  is read for its end and its error, by index; a top-level error is read
  whatever else the reply holds; a completed Responses event's routing
  facts name who served it; a fact of the wrong type is left out.
- An error under a 200 is recorded beside the reply and nothing else
  follows from it: the call keeps its status and failure reads it as
  before.
- The recorded routing objects and facts are cleaned: a value under a
  credential's name, the relay's own key and key-shaped strings are
  removed. What is sent is unchanged.
- The routing-facts header goes to OpenRouter alone, as enabled whatever
  the caller sent.
- One check of a pin, shared by the run, its command line and the relay's
  own command; a control is refused a pin on a model it never calls.
- Who served a unit counts its judge's and simulated user's routes, and
  the job the benchmark's own relay's calls; the tokens and the cost stay
  the agent's. A trial whose answered calls name no provider keeps its
  count. A name reads as a slug by its case and spaces alone.
- merge compares pins as mappings, and a forced merge of different pins
  reads held unknown for such a model.
…held cannot tell

The pages now say that an error under a 200 is recorded and changes
nothing else, that the record's copies are cleaned of credentials, that
who served a run counts every route, how a name reads as a slug, and that
held names a provider, never its endpoint variant.
A run started from the builder could not pin its serving providers. On
OpenRouter, the Model step now shows Served by under each model field
(the model, the user simulator's and the judge's): the providers that
may answer that model, tried in their order and no other, typed as slugs
or added from the model's providers. Empty, the default, pins nothing.

The recipe holds them as served_by, by field. The server checks them as
--served-by is checked, refuses them for another provider or for a model
no job calls, and compiles them into --served-by for each job that calls
that model, by the name the run sends it as. Review shows them in its
Model row. A saved recipe keeps them, and choosing another provider drops
them. A recipe with no pin compiles to the command it did.

The model's providers come from a new route, POST /new-run/endpoints,
which asks OpenRouter's endpoint list as /new-run/models asks the model
list: local and form-token guarded, the key kept on this machine and sent
when set, the answer cached by the page for five minutes, and a fixed
message when the list cannot be had, the field then taking a typed slug.
…del it was set for

A review of the pin found records and summaries that could read held
where another provider answered, credentials that could reach a record,
and pins that the builder dropped or moved to another model. Now:

- The provider recorded is the one of the reply's end: a stream's latest
  event that names one, and the last attempt of OpenRouter's routing
  facts; an earlier, failed attempt is never taken, and a last attempt
  that names none gives none.
- An error keeps its code, type and message alone, of those kinds; the
  rest of the object is not kept. A recorded routing object's keys are
  cleaned as its values are.
- A merged job's summary is made of each shard's whole one, its
  benchmark relay's calls and an unknown included.
- A recipe's pins are resolved by model, then given to every job that
  calls that model, a control's judge job included. The compile route
  returns each field's model as the run sends it, and Served by lists
  that model's providers.
- A control is refused a pin on its simulated user's model, which it
  never calls. relay serve refuses a pin unless its base is OpenRouter's
  (or --routing is given) and the model is one it sends; a container's
  relay is given the pins of the models it sends.
- The builder clears a field's pin when its model changes, a preset or a
  benchmark included; keeps a saved pin as written, for the server to
  refuse what is wrong in sight; keeps an empty part of a typed list; puts
  its help in a tooltip; and keeps the keyboard on the model field once
  the form is drawn again.
…d where held reads unknown

A variant may be a quantisation, a speed or a region. Of several
choices' errors the record keeps the top-level one, else the first
choice's. A provider whose name is not its slug once lowercased reads
unknown though it served the call; the example of a name that holds is
DeepInfra.
…facts mark as selected

OpenRouter's routing facts list the endpoints available for a request and
mark the one that served it as selected; they hold no list of attempts.
The relay now reads that entry, at the top of a reply and in a completed
Responses event's response, and asks for the facts on a pinned Responses
request too: a Responses reply names its provider there alone, so a pin
on a model an agent calls through Responses could never read as held.
… pin no provider serves gets 404, and why held may read unknown
… asks for the routing facts and where held reads unknown
main gained the OSWorld1 benchmark and the run option --units since this branch started. Seven files met: the command line, the job's record, merge, the run and three pages. Both sides are kept whole, main's first: a run takes --units and --served-by together, config.json records standard.units_file and standard.served_by, and merge compares both (different unit lists are always refused, different pins unless forced).
…s record

A run given --units and --served-by from the command line records both in config.json, with and without --from-run.
…n the field guide

The field guide names both config fields, the routing fields of a call, the serving counters and held, and says that merge refuses different unit lists always and different pins unless forced.
main gained the settings that say what Nullius may use on each machine, with the run option --constraint, and a fix of the settings page. Seven files met: the command line, the job's record, the recipe, the run, a test module and two pages. Both sides are kept whole, main's first: a run takes --constraint and --served-by together, config.json records standard.constraints and standard.served_by, and a recipe compiles each target's constraints and the pins of the models its jobs call.
Seven cases where the two meet: both records from the command line, with and without --from-run; the order of the refusals; a scope's enforcement with a valid pin; a container relay's caps beside its pins; each target's compiled command; the builder's checks; and shards whose machine limits differ.
…ving pin in the field guide

The field guide says that standard.constraints, standard.units_file and standard.served_by may stand together in config.json, and which of them a merged job keeps from its first shard.
…on top of --served-by: gated tasks, a definition's pins as its serving pins, a pinned route's refusal as the unit's, limits of their own, and prove of a merged control

A definition's pin (setups.original.pin, score.judge.pin) joins the run's --served-by pins and wins over
--served-by for its model; ApiProviderRouteError reads the pin and reply.served_by. A failed Responses
object reads reply.end and reply.error.
…eep the score of a unit a guard stopped despite a last 429

limits.run_again false: a unit that ended rate-limited is not run once more; its outcome stands. A unit whose driver scored it after a guard's stop keeps that score, marked stopped, though its last own call got a 429.
… own last 429, not a side call's or a context window's stop

The rule reads the agent's own last call, a guard's line in the stop file and a score stopped by other than the context window; a cap's refusal is still the cap's. limits.run_again takes null as unset. The rate limits row names the opt-out.
… upstream's own runners, mock websites, GitLab and proxy, proven on test_v2, and how to prepare a machine for it

OSWorld2 runs each of the 108 tasks of test_v2 in a VM of its own through the upstream's docker provider, with the mock websites its tasks need served on the machine, and GitLab and a proxy started for each unit that needs one. Its Original runs the upstream's agents with their models pinned to their providers on OpenRouter, and its null control is proven on test_v2 at 0 of 108. The tasks are gated, so a failing endpoint is kept as its status alone. Its machine page says what a machine needs, how to check it and what a run costs.
limits.run_again false: an OSWorld2 episode is paid and long; one that ended rate-limited is run again by hand.
…g programs, and fake the sweep's clock for the sweep alone

Two tests made a Run of OSWorld2, which asks for ImageMagick, ffmpeg and
tesseract on the machine: they now say the programs are there, as the
test of their refusal does. The sweep's tests replaced time.sleep for
every module, so a slow docker's wait in subprocess advanced the fake
clock and the test's own steps: the fake now replaces the sweep's time
module alone.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants