Repository navigation
bench: preflight for the SWE-bench run (subscription-only login, CLI pin, caps, usage meter) [PROTECTED: owner merges] - #756
Conversation
…pin, caps, usage meter First of three stacked PRs for #748 Phase 0 (the gated-merge benchmark). The owner changed the auth model on 2026-10-18: no API key. The bench runs as its own OS user (default wl-bench) with its own claude, signed in once by the owner through Anthropic's own flow. So before anything runs, the bench checks: - it runs as that user; - claude --version is the pin (2.1.303), and every claude it starts gets DISABLE_UPDATES=1; - claude auth status --json reports authMethod claude.ai or oauth_token, on any plan. The plan is recorded. An API key, a helper, Bedrock, Vertex, Foundry, or a signed-out or unreadable status is refused. It never reads the login's files. The caps are the owner's approval, in usage-equivalent dollars (the CLI's own total_cost_usd): - --spend is required: $40 per smoke round, $300 for the baseline; - $15 per task; - a smoke mean over $8 per task needs the owner's re-approval. The meter caps each run at what is left of its task and the run. At the first rate_limited result it stops: no retry, and every later run is refused unstarted. Containers, limits and the agent sandbox come in the next PR; the driver, grading and aggregates in the one after. Test (fails without): test/bench.test.ts; the module is new, so every test fails on main (no module). Guard-off runs: the caps line was measured on the combined branch. The usage-limit and subscription lines are run by the owner. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
|
Owner's fail-without results (measured by the owner, 2026-10-18), on 9d73baa4 in a scratch worktree, now removed:
Both guards fail without their check. The PR body is updated. |
Evaluator verdict: CHANGES NEEDED (round 1/2)Reviewed head SummaryThe structure is sound, and most of it does what the owner asked. Three guards can be bypassed (all measured below):
The PR body's "What this can never do" section claims all three, so they are required changes. Acceptance criteria
Auth field names (as asked)
So
The real Bedrock shape is Fail-without
Protection
Scope
Hygiene
CIRed, and not because of this PR (measured from
Required changes
Not required (for the owner)
|
…ight Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…ny 429, own-key caps, real bench root Review round 1 on the preflight (#748), with the owner's decisions of 2026-10-18: - caps: --tasks constructor, toString or __proto__ matched `in` on Object.prototype and got no ceiling at all. The mode is now checked with Object.hasOwn (isMode). - login: only authMethod claude.ai starts a run. oauth_token is refused: the real 2.1.303 reports it for CLAUDE_CODE_OAUTH_TOKEN and for federation credentials alike, with no plan. apiProvider must be firstParty, and the status's configDirectory must be inside the bench user's home (a CLAUDE_CONFIG_DIR pointing at another login is refused). Login-switching variables are refused by prefix (every ANTHROPIC_*, CLAUDE_CODE_USE_/OAUTH_/ API_KEY/PROVIDER_/CLIENT_*, CLAUDE_CODE_SKIP_*_AUTH, *_FILE_DESCRIPTOR, *_BASE_URL, AWS_*, AZURE_*, Google and Vertex/Foundry/Bedrock/Mantle names), so a variable a later CLI adds is refused too. - usage meter: a 429 the engine classes as transient (a failed run with no result line) used to be retried inside the meter and never stopped the bench. The meter now stops on any result classifyTransient calls a rate limit, and aborts any transient-error wait a runner starts, so no second session starts. - bench root: resolved with realpath (the longest existing prefix), on both sides of each overlap check, so a ~/bench that links into ~/.claude or a checkout is refused. Test (fails without): bench.test.ts "a 429 the engine would retry as transient stops the bench too", the caps prototype keys, the claude.ai-only shapes and prefix variables, and "the bench root is checked where it really is". Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 1 fixes (head f122bfd; main merged at 858f627)Each point from the verdict and the owner's decisions of 2026-10-18. Labels: measured = a command I ran, inferred = reasoned, not run.
How the fail-without was run (measured): f122bfd's test file against the PR's own pre-fix modules ( Local, on f122bfd (measured): run with a temp HOME and state dirs, and a refusing
Body: "What this can never do" and "Compromised component" now match the code. That includes a new inferred limit: a settings-file CI: running at the time of this comment (dco, denylist, scan, scope, new-deps and shard 2/4 on Node 22 passed; the rest pending). Main includes #753, so any red is this PR's to explain. |
Evaluator verdict: CHANGES NEEDED (round 2/2)I reviewed head SummaryEvery round-1 required change and every owner decision for #756 is met. I checked each one with my own probes against the built head (measured, details below). Two narrow claims in "What this can never do" are still not true in edge cases (measured):
Neither leads to a non- Round-1 required changes and owner decisions
Fail-without (measured)
"What this can never do" and "Compromised component": are they true now?
Protection
Scope and hygiene
CIGreen (measured,
Required changes
Not required (notes for the owner)
|
…ight Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…through a dangling link is refused Round 3 on #756, the owner's decisions of 2026-10-18. The bench user's home now comes from the user database (os.userInfo().homedir), not from $HOME, in requireSubscription (the config-dir check), benchRoot and the credential places derived from the home. A $HOME that isn't that home is refused, not ignored, because claude itself follows $HOME to its login: with HOME moved to another folder holding a claude.ai login, requireSubscription accepted it, and benchRoot built its checks on that folder. realPath now refuses a path whose component exists but doesn't resolve (a dangling link or a loop). Before, it fell back to the unresolved path, so ~/bench -> ~/.claude/not-yet was accepted. The places the bench must never touch are followed through readlink instead, so a root where a dangling ~/.ssh points is refused too. Test (fails without): "the bench user's home is its passwd entry's, never $HOME" and "a bench root through a link that doesn't resolve is refused" in test/bench.test.ts. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 3 (owner-granted): what changedHead 571f3c0. origin/main (3519839, with #751 and #761) is merged in first with a signed-off merge commit (measured: the merge was clean, with no conflicts). 1. The home comes from the user database, never
2. A
Local results (measured): Body: the paths and auth bullets, the test count, verification, "What this can never do" (a new bench-root bullet with its one inferred limit) and the bench-user-shell entry are updated to match. Owner rule A (inferred from the code): the default-path test resolves the real passwd home's path with |
… issue-748-bench-driver #757's round 3 (d57acd8), with #756's round 3 and main under it. Conflicts resolved in scripts/bench/container.ts (the async exec kept, TaskRunner's base now required ahead of the signal) and test/bench.test.ts (both sides' tests kept, the new tar-stream test made async). What the merge needed to build on #757's API: BenchOptions.baseTree, required, gives each task's base tree to its TaskRunner (the driver passed undefined before); the test machine passes dataHome and a base tree; the confirmation test counts the probe's two new sockets. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: PASSI reviewed head Both round-2 required changes are fixed, following the owner's round-3 decision. Every earlier pass still holds. I ran every probe and test under Round-3 changes
Earlier passes, re-run on this head (round 2's probe, unchanged)
The default-path test touches nothing under the real home or
|
Intent: This is the first of three stacked PRs for #748 Phase 0, the gated-merge benchmark on SWE-bench Verified. It adds the bench's preflight under
scripts/bench/:wl-bench), with its home taken from the user database (the passwd entry), never from$HOME.claude.ailogin only (any plan, which is recorded), on Anthropic's own API, in a config dir inside the bench user's home. There is no API key and nothing reads the login's files.2.1.303), withDISABLE_UPDATES=1on every claude the bench starts.--spendrequired, $40 per smoke round, $300 baseline, $15 per task, and re-approval over $8 per task.total_cost_usdand tokens, and stops cleanly at the first usage or rate limit of any kind, with no retry.Nothing here runs a model, Docker or a download.
Risk class: protected (every path is under
scripts/**, the L3 review point the issue names).Part of #748. Stacked: this PR (preflight) → #757 (containers and limits) → #759 (driver, grading, aggregates, README).
What's in it
scripts/bench/paths.ts:benchHome: the bench user's home fromos.userInfo().homedir(the passwd entry). When$HOMEis set and isn't that home (compared on real paths), it refuses; it doesn't silently ignore$HOME, because claude itself follows$HOMEto its login.~/benchin that home;WORKLANE_BENCH_ROOToverrides it. It is resolved withrealpath(the longest existing prefix), and so is each place it is checked against.readlink, so a root where a dangling~/.sshpoints is refused too.~/.claude,~/.ssh, ...), the real engine state dir, or a held-out store the environment names. A~/benchthat is a symlink into one of those is refused too, whether or not the link resolves.scripts/bench/auth.ts:requireBenchUser;requireSubscription:authMethodclaude.aiwithapiProviderfirstParty, and aconfigDirectoryinside the bench user's home (recorded asconfig_directory, with the plan). That home isbenchHome's, never$HOME;oauth_token,api_key,api_key_helper,third_party, signed-out, and a status that isn't a JSON object;LOGIN_SWITCH_VAR(a prefix rule, not a list: next section), and a$HOMEthat isn't the bench user's home;requireClaudePin;BenchAgentRunner, which addsDISABLE_UPDATES=1to every run.scripts/bench/caps.ts:capsFromArgs, built on the existing spend gate (src/spend-gate.ts). The mode is checked withObject.hasOwn(isMode), soconstructor,toStringand__proto__are refused.scripts/bench/usage.ts:UsageMeterandTaskMeter.classifyTransientcalls a rate limit. That coversrate_limitedand afailedrun on a 429 with no result line.onTransientRetry. If a runner starts to wait out a transient error, the meter aborts the wait: a rate limit stops the bench, and anything else ends that run.benchClaude,budgetMs: 0).tsconfig.json: compilesscripts/bench/**/*.ts. The output isdist/scripts/bench/, which the package'sfileslist doesn't include (below).test/bench.test.ts: ten tests. The stand-inclaudeanswers with the real CLI's status shapes (next section), the usage-limit result (rate_limit_eventrejected, then anis_errorresult), and an exit 1 onAPI Error: 429with no result line.The login variables (checked against code.claude.com/docs/en/env-vars, 2026-10-18)
LOGIN_SWITCH_VARrefuses these:ANTHROPIC_*. That covers keys, tokens, base URLs, federation (ANTHROPIC_FEDERATION_RULE_ID+ANTHROPIC_ORGANIZATION_ID),ANTHROPIC_PROFILE,ANTHROPIC_CUSTOM_HEADERS(anAuthorizationheader) and the model overrides. None is exempt: the bench needs none, and a model override would change what is benchmarked (inferred);CLAUDE_CODE_USE_*,CLAUDE_CODE_OAUTH_*,CLAUDE_CODE_SKIP_*_AUTH,CLAUDE_CODE_API_KEY*,CLAUDE_CODE_PROVIDER_*(PROVIDER_MANAGED_BY_HOST) andCLAUDE_CODE_CLIENT_*(a gateway's mTLS identity);*_FILE_DESCRIPTOR(CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTORandCLAUDE_CODE_API_KEY_FILE_DESCRIPTOR, which the reviewer found in the binary; they aren't on the docs page) and any*_BASE_URL;AWS_*,AZURE_*,GOOGLE_APPLICATION_CREDENTIALS,CLOUD_ML_*, and any name containingBEDROCK,VERTEX,FOUNDRYorMANTLE.measured: every variable the reviewer listed matches the rule (the test asserts each one, plus a made-up
ANTHROPIC_A_VARIABLE_A_LATER_CLI_ADDSand an emptyANTHROPIC_BASE_URL). Unrelated variables such asDISABLE_TELEMETRYandCLAUDE_CODE_MAX_OUTPUT_TOKENSstill pass.The real status shapes
measured: the real 2.1.303
claude auth status --json, run with a throwaway HOME andCLAUDE_CONFIG_DIR, no login and no model call:loggedInauthMethodapiProvideranalyticsDisabled,projectsDirectory,configDirectoryANTHROPIC_FEDERATION_RULE_ID+ANTHROPIC_ORGANIZATION_IDCLAUDE_CODE_OAUTH_TOKENANTHROPIC_API_KEYapiKeySourceCLAUDE_CODE_USE_BEDROCKANTHROPIC_BASE_URLorAWS_BEARER_TOKEN_BEDROCKaloneclaude.aishape addsemail,orgId,orgNameandsubscriptionTypeto the same fields. That is inferred from the CLI's status code (the reviewer's static read), not run, since no real login is used.claude.ai+bedrockshape is replaced. Aclaude.ai+vertexcase remains for the provider check, labelled synthetic in the test.Still ahead in #748 Phase 0, after this stack: the PR-mode task runner on
src/harness-evals/replay.tswith stub CI, the agent's test relay into the container, task selection, the pinned-download scripts, the grading run, the B0, B1 andclaude -pbaselines, and the CLI entry point.How it was verified
node --import ./dist/test/isolate.js --test dist/test/bench.test.jswith a temp HOME, state and held-out dirs and a refusingclaude,dockerandsrtfirst on PATH:# pass 10,# fail 0.npm run typecheck(both tsconfigs) exits 0.npm run check:denylist:denylist: 1278 files clean.npm run check:dco -- origin/main HEAD:DCO: all commits signed off.npm pack --dry-run --ignore-scriptslists no file underscripts/benchordist/scripts.scripts/bench/auth.tsandpaths.tschecked out from its parent (the code before this round). No guard was edited.# pass 8,# fail 2:not ok 3(the$HOMEtest: the first assertion got the old config-dir message, not aHOMErefusal) andnot ok 10,Missing expected exception: ~/bench -> ~/.claude/not-yet (dangling).claude.ailogin in$HOME/.claude). Old code, then new:requireSubscriptionwith no home given andHOMEat a folder holding aclaude.ailogin that the user database doesn't name: ACCEPTED, then REFUSED (HOME is …, not this user's home … from the user database);benchRootwithHOMEat another folder, home given, and with no home given: both ACCEPTED, then both REFUSED;benchRootwith~/bench→~/.claude/not-yet(dangling), a root under a dangling link, and a root under a link loop: all ACCEPTED, then all REFUSED (… is a link that doesn't resolve);~/.sshpoints: ACCEPTED, then REFUSED (overlaps);HOMEequal to the bench home, a plain root): ACCEPTED on both.git checkout 0a45e717 -- scripts/bench/{auth,caps,paths,usage}.ts, the PR's own commit before these guards existed; then restored withgit checkout HEAD --). No guard was edited.# pass 4,# fail 4.Missing expected exception: __proto__.Missing expected exception: an oauth_token (CLAUDE_CODE_OAUTH_TOKEN or federation: the real shape).retries on: exactly one claude session,expected: 1,actual: 8.Missing expected exception: ~/bench is a link to ~/.claude.acceptance: the driver refuses to start without a spend capfailed:The input did not match the regular expression /spend cap|over the owner's approval/ ... "TypeError: Cannot read properties of undefined (reading 'spendUsd')".driver.ts:153off) gavenot ok"the driver stops cleanly at a usage limit". The subscription-only login (auth.ts:107off) gavenot ok"the driver refuses a login that is not a subscription".New dependencies
None.
What this can never do
Start a bench run on anything but a
claude.ailogin. The check readsclaude auth status --jsonand refuses:authMethodother thanclaude.ai(anoauth_token, which also covers gateway bearers, federation and Console profiles, included);apiProviderother thanfirstParty;$HOMEthat isn't that home, before claude is asked (measured);inferred limit: a user-level settings
envblock is not read by this check. For one that sets a token or key, the CLI's own status shows the changedauthMethod, and that is refused. A settings-levelANTHROPIC_BASE_URLbeside aclaude.ailogin is not detected here.Put the bench root in a credential store, the engine's state, a held-out store or a checkout, including through a symlink, a chain of symlinks, a dangling link or a link loop (measured, tests 9 and 10). inferred limit: only the root is checked; a link planted inside an existing root later (
~/bench/state→ somewhere) is not, so the sandbox in bench: task containers in rootless Docker with required limits, exec-only, no daemon for agents [PROTECTED: owner merges] #757/bench: run driver, patch export, grading inputs, aggregates and setup README [PROTECTED: owner merges] #759 must keep agents from writing to~/bench.Read, copy or relay the login. The check asks the CLI and never opens a file in
~/.claude.Retry after a usage or rate limit. The meter stops the whole bench on any result the engine classes as a rate limit, a
failed429 included. It aborts any transient wait a runner starts, so a second session never starts. Every later run is refused unstarted.Spend past the owner's caps.
--spendis required. The per-mode maxima and the $15 per task are constants a reviewed change must raise, and onlysmokeandbaseline(own keys) are modes.Compromised component
src/imports it.WORKLANE_BENCH_CLAUDE_VERSIONandWORKLANE_BENCH_USER, which change the pin and the expected user without a reviewed change (inferred; kept for the owner's later pin bumps). Moving$HOMEis refused, not honoured (measured).SUBSCRIPTION_AUTHor narrowLOGIN_SWITCH_VAR. The guard is this review: the owner mergesscripts/**.