Repository navigation
Spec: Custom-runner Arms for process Experiments - #2203
Merged
Merged
Conversation
Freeze Custom runner Arms and run process Trials
Support Custom runner Arms in Console Experiments
Enclose Custom runner Trials in declared multi-provider sandboxes
Adds regressions for two Spec review findings (expected to fail before the fix): a timed-out runner with provider traffic must stay failed, and the Trial-wide web-search scan must skip events another Arm's build wrote. Also covers the provider-traffic and crossed-Harness accounting gap rules directly, documents native web-search Voiding, refreshes code navigation pointers and names test modules for their selectors. Co-Authored-By: Claude <noreply@anthropic.com>
… scans Accounting gaps now judge only an outcome the runner reported in a valid result.json, so timeouts, runners that could not start and broken runner protocol keep their failed classification (outer supervision keeps precedence, #1468). The Trial-wide web-search scan skips events this build cannot decode, as the other Session-trace readers do, instead of aborting the Experiment on a native Arm's newer trace. Co-Authored-By: Claude <noreply@anthropic.com>
Sandboxed code reaches the host's record socket, and the host process that runs concurrent Trials sits outside the Trial memory cap. Cap unfinished frame bytes and open connections, release closed connections' descriptors, and return each poll to supervision within 50 ms so the Trial timeout and quota pauses still apply.
Record Custom runner sessions and enforce Trial accounting
Static checks freeze the Custom runner manifest, reporting each malformed field, missing source, invalid target, unpinned Harness, missing login or [providers] key and unpriced role Model. --suite/--task runs one Trial through the Experiment execution in a private directory, outside Experiment storage. /api/v1/evals/check-runner returns the CLI's --json report. Refs #2192
…check-runner Refs #2192
Address review: the dry run and Experiments share the review-suite refusal and nonempty Trial input checks, both Console twins run the CLI through one helper, Check names come from one list, execute() no longer reports a failed check as success, and code navigation points at the Custom runner check. Refs #2192
A run-scope caller could name any local .toml file; its TOML parse error echoed the offending line, secrets included, in the report. The API now checks Custom runners by name, as ADR 0026 keeps caller-selected local paths CLI-only.
Add evals check-runner, its Console API twin, and Custom runner documentation
/api/v1/evals/check-runner refused caller-selected .toml manifests, but /api/v1/start forwarded baseline_runner and candidate_runner paths to the CLI unchecked. Both endpoints now share one refusal, and ADR 0026 lists it.
…ecord The Command record took the Candidate Setup's Choice, whose missing Harness fell back to claude, so an Experiment with a Custom-runner Candidate claimed a Harness it never ran. Such a record now names "custom runner".
Pairwise blinding replaced every substring of a runner's name, digest or role metadata, so a runner named review turned reviewer into [withheld]er, and it dropped every added table with a Role column. It now withholds whole words only and drops a role table only when it names the runner's identity.
The runner sandbox re-encoded the native login table and had dropped Muse's optional auth.json.lock. Both now read one MUSE_LOGINS list.
- Share the web-search Void between native and Custom-runner Trials. - Name check-runner's CLI-twin budget and what it covers. - Reuse the Console's short reference in Arm identity. - Index setup.rs, cli_twin and named_runner in code navigation. - Check the architecture page positively for the Custom runner sandbox. - Skip Custom runner sandbox tests without bwrap namespaces or socat, through one shared guard. - Fail loudly when a DELIVER edit no longer applies, give each refusal its expected message, and poll for queued record frames instead of sleeping.
The Custom runner sandbox bound the whole Trial attempt read-write. After the runner ended, the host wrote stdout, stderr, exit-status and uncommitted.txt into that attempt, following any link the runner had planted there, so a sandboxed runner could overwrite host files outside the sandbox with its own stdout. Bind only the repository, fresh home and output read-write; the rest of the attempt stays host-only.
After the runner ended, the host ran `git status` in the runner's repository, whose config the sandboxed runner can write. A runner-set core.fsmonitor or clean filter then ran as a host command outside the sandbox. Status now reads the working tree through a host-owned Git directory whose index holds the runner's HEAD, with the frozen git_exclude patterns and submodules ignored.
The host read output/result.json with a plain read after the runner ended. The runner owns that directory, so a FIFO there held the Experiment forever, outside the Trial timeout, and a link or sparse file made the host read whatever it named. result.json now goes through the same no-follow, nonblocking, regular-file reader as sessions.json, bounded at 1 MiB. The three new Custom runner sandbox tests share one fixture that runs a quiet runner in real bwrap with a socat stand-in.
JacobStephens2
marked this pull request as ready for review
November 15, 2026 04:34
# Conflicts: # GLOSSARY.md # docs/adr/0032-custom-runners-run-inside-one-trial-sandbox.md # site/index.html # site/tests/pages.test.cjs # src/evals/experiment.rs # src/sandbox.rs # tests/console/start.rs
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Custom runner Arms to
processExperiments (ADR 0032). Either Arm, or both, can be an outside orchestrator run from a frozen.tomlmanifest instead of a thirdshift build. The runner works in one Trial sandbox and returns a commit and Session traces, and both Arms are graded by the same code Graders, Grader panel and blind pairwise review.Closes #1468
Tickets
Summary
How a Custom-runner Trial runs:
Where the code lives:
Changes from the final review, on top of the Tickets:
Tests sit at the Spec's agreed seams:
thirdshift experiment … --json,evals prepare-armsandevals check-runner --json, driven through the Experiment fixture with fake runners, fake Harnesses and a loopback provider/api/v1/start,/api/v1/evals/check-runner,/experiments/<id>, Trial Peek and pairwise pagescustom::collect,pairwiseblinding and the recorderEvidence
Execution evidence
These are the review fixes. Each test was run red before its fix and green after.
cargo test --bin thirdshift console::start::experiment::testscargo test --test experiments custom_runner_experiment_command_record_names_no_fabricated_harnesscargo test --bin thirdshift evals::experiment::pairwise::tests::runner_blindingcargo test --bin thirdshift evals::experiment::pairwisenode --test tests/experiments.test.cjs, run fromconsole/, gives# pass 15,# fail 0. This includes the existing "Custom runner identity in retained pair evidence stays blind" and "preserves ordinary source evidence" cases.npm run docs:check --prefix siteexits 0 after the manual was regenerated. The architecture-page test insite/tests/pages.test.cjspasses.Wider runs, and their limits on this host:
cargo test --no-fail-fast --test '*'gave 2677 passed and 27 failed. All 44custom_runner::andcheck_runner::tests passed.sandbox failed to start: socat is not on PATH. The proxy relay needs socat, which this machine lacks.a_trial_over_its_cap_dies_alone_and_is_void_while_others_carry_onandreview_a_cap_killed_tool_cannot_leave_a_successful_trial, are flaky. Run alone at the pre-review commit ef1ab4d, they failed once in four runs.scripts/affected-tests main: it stopped atcargo testafter--bin thirdshiftgave 1871 passed and 20 failed. All 20 aresandbox::teststhat need socat.Visual evidence
Experiment page for an Experiment whose Arms are both Custom runners, with "What was compared" opened.
Desktop, 1440×900:
Mobile, 390×844:
On
main, the Console cannot read a format-13 Setup with a nullcommit, so the page says "Experiment is unavailable." On this branch, the page shows each Custom runner's name, digest, Git provenance and declared roles.Merge Danger
Door: two-way
The Experiment JSON format moves to 13, with nullable
commitandharnesson Setup. Older thirdshift builds cannot read Custom-runner Experiments. Native Experiments keep their identity, so they resume unchanged. Credentials gain an optional, Trials-only[providers]table.Blast Radius: Experiments
Some changes reach native Experiments too:
fireworks-aifamily. Legacy Bars keep their original family classification when they resume.Factory Commands are unaffected.
Unaddressed findings
Standards
Options instead of a sum type (Primitive Obsession).custom runner.commit/harnessplusrunner/runner_digest("Setup makes commit and harness nullable"). An internal enum would change about 60 branches across 15 files at the end of a six-Ticket Spec. That is an Architecture review's job, not a review fix.Execution::runmixes native and runner flows (shallow module). Declined for the same reason.execution/custom.rsalready isolates the runner contract. Folding the recorder, sandbox and classification into one interface is a refactor with no behaviour change.MUSE_LOGINSlist. Merging the native and runner login tables is declined, because they differ on purpose: native Trials bind user rules and settings files, while runner homes require only declared logins (src/sandbox/custom.rslogin).proxy::Providerand Kimi host resolution duplication;Stringfamilies. Declined as a refactor. Families stay strings because the frozenfamiliesandprovider_familyfields serialize them as strings (docs/experiment-results.md).model_grader.rscustom_runner.rst.aftercleanups inconsole/tests/experiments.test.cjssuite.tomlliteral, the unusedrunner()return value, and thesessions/tests.rstable row. None changes what the tests prove.sessions/tests.rsinvalid-evidence cases check onlyis_err(). Declined: each case builds one specific invalid input, and its name says which. The refusal and conflict assertions incustom_runner.rsnow check their exact messages.Spec
$0failed Trial.crashed_runner_after_unrecorded_provider_traffic_is_an_accounting_gap ... FAILED.failed) keeps precedence" and that runner crashes "without outer Void evidence record that Trial asfailed". The code (src/evals/experiment/execution/custom.rscollect) and its testtimed_out_runner_after_provider_traffic_stays_failedfollow that precedence over theaccounting_gaprule.docs/trial.mddocuments it.docs/experiment-results.mddefines a seat's family as its provider ("the Harness's own, the provider an OpenCode Model names, or the resolved provider of a Kimi alias"). OpenCode Models already counted by serving provider.src/evals/experiment/panel.rscheck_stand_in.sessions.jsonstream voids a finished Trial. Declined.usage_limit(docs/trial.md:215-216). Imported streams "also retain quota stops" (docs/eval-tasks.md:563, testraw_harness_sessions_retain_normalized_web_search_and_quota_evidence).Security
None. The security review found three boundary breaks this change introduced, and fixed each one. A proof-of-concept test for each runs a runner inside real bwrap. Each test failed before its fix, passes after it, and stays as a regression test in
tests/experiments/custom_runner.rs.host_attempt_writes_do_not_follow_links_planted_by_the_custom_runner.git statusin the runner's repository, so acore.fsmonitoror clean filter the runner set there ran on the host. Status now reads the working tree through a host-owned Git directory. Test:host_git_in_the_runner_repository_ignores_runner_configured_commands.result.json(c0580f7). A FIFO stalled the Experiment beyond the Trial timeout.result.jsonnow goes through the same no-follow, nonblocking, regular-file reader assessions.json, capped at 1 MiB. Test:a_fifo_result_fails_its_trial_instead_of_holding_the_host.Review coverage
site/docs/index.html. Its source,docs/guide.md, was reviewed.npm run docs:check --prefix siteexits 0, and the output was spot-checked for rendering and escaping.src/logs.rsandsrc/logs/recording.rs: reviewers read them only as supporting files, before the fix.docs/adr/0026-each-console-serves-an-api-that-can-do-what-the-cli-can.md: the Standards reviewer read it before the one-line addition.CI notes
Two failures in run 43682288379 come from tests this branch does not touch:
Working identifies live Review and Fix targets locally and across the Fleet(console/tests/working-targets.test.cjs:83,1440px remote review-explicit: visible PR target): intermittent. The same assertion failed on main in run 43466605947 (4d24f79d) and onissue-2098in run 43293144147. The branch changes neither this test nor the Working or Fleet rendering, and the test passes 3 of 3 times locally.release::native_release_waits_for_leak_decisions_before_any_push(tests/experiments/release.rs:218,Experiment is already running or being rechecked): intermittent lock race. The test dismisses a leak as soon asexperiments --jsonshowswaiting_on_leaks. Butexecuteinsrc/evals/experiment.rssaves that Verdict and keeps holding the Experiment lock while it runsretention::repositoriesandprint_results, so the dismissal'stry_locksometimes loses. The branch leaves that code and this test unchanged from main, and the test passes 2 of 2 times locally.testandrust-x86 / completefailed only as aggregates of these jobs.Built with claude 2.1.322 · claude-opus-5-5 · high
Security review outcome
No unaddressed introduced findings.