fix: reject param-file keys that look like spawn settings (#526) - #532
Merged
Conversation
scttfrdmn
force-pushed
the
fix/525-sweep-drops-cli-spend-controls
branch
from
August 19, 2026 06:37
7f13678 to
cfbabe3
Compare
scttfrdmn
force-pushed
the
fix/526-reject-dangerous-param-keys
branch
from
August 19, 2026 06:46
8ac2bef to
546c298
Compare
scttfrdmn
changed the base branch from
fix/525-sweep-drops-cli-spend-controls
to
main
August 19, 2026 06:46
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Every unrecognised key fell through to the PARAM_* passthrough arm, so a misspelled or invented spawn setting was indistinguishable from a workload parameter: `ttl_hours: 4` and `on-complete: terminate` launched instances with no bound and no complaint. Passthrough stays — it is the feature. A key is now rejected when it cannot become an env var at all (the bootstrap writes `export PARAM_<key>="<value>"` into /etc/profile.d, so a hyphen is a shell error), when it is a recognised key misspelled by hyphen or case, or when it is on a curated list of CLI-only flags and near-misses. Each error names the right spelling; all bad keys are reported in one pass. An explicit `param:<name>:` prefix is the escape hatch, so a reserved name is a redirect rather than a dead end for a workload that really does have a `budget` or `time_limit` parameter. Ambiguous words are off the list: `timeout` is documented step vocabulary and TestBuildLaunchConfigFromParams_WorkflowStep caught the first draft denying it. The recognised-key registry is a duplicate of the switch it mirrors, so a go/ast test requires the two to be equal in both directions — drift in the dangerous direction would otherwise be silent.
scttfrdmn
force-pushed
the
fix/526-reject-dangerous-param-keys
branch
from
August 19, 2026 07:08
546c298 to
709d751
Compare
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.
Closes #526.
Stacked on #529 (which is stacked on #528). Merge order: #528 → #529 → this.
The bug
cmd/sweep.go'sdefault:arm sent every unrecognised param-file key toconfig.Parameters, i.e. aspawn:param:<key>tag and aPARAM_<key>env var. That passthrough is the feature — it is how a sweep hands values to a workload — but it also meant the parser could never say "that isn't a thing". A key the user meant as a spawn setting and a key they meant for their own program were indistinguishable:ttl_hours: 4PARAM_ttl_hours=4, no TTLon-complete: terminatePARAM_on-complete, instance never terminatesbudget: 50PARAM_budget=50, nothingmax_concurrent: 3PARAM_max_concurrent=3, nothing (CLI-only)The first two leave a running instance. That is the version of this that costs money.
The fix
Passthrough is unchanged for real parameters. A key is rejected — before anything is launched or priced — under three rules, checked in this order:
B. A recognised key, misspelled. Hyphens or wrong case:
on-complete,instance-type,cost-limit,TTL,Instance_Type. The error names the exact spelling.C. A curated denylist of CLI-only flags and near-misses:
ttl_hours,walltime,max_runtime,max_cost,budget,max_concurrent,launch_delay,instance_types,image_id,spot_price, … Each entry carries the correct spelling, or says the control is CLI-only.A. Not a valid shell identifier. This one is mechanical rather than a matter of taste, and
pkg/launcher/bootstrap.go:534is why:export PARAM_on-complete="terminate"is a "not a valid identifier" error in every login shell. A hyphenated key was never going to reach the workload no matter what it meant.All offending keys are reported in one error rather than one per run — a 30-row sweep with three typos should be one round trip. The same check also runs at the
buildLaunchConfigFromParamsseam, whichspawn resumeand the quota preflight reach without going throughlaunchParameterSweep, so resume cannot rebuild an instance from a file the launch path refused.Option (3) from the issue is in too: the
PARAM_*variables a sweep will set are listed in the sweep header, so an unintended passthrough has somewhere to be noticed.The escape hatch, and why the denylist would otherwise be a bug
A reserved name with no way through is not a fix, it is a hard block on a legitimate parameter.
budgetis an optimiser's evaluation budget as often as it is a dollar figure;time_limitis a standard solver option (Gurobi, CPLEX). Rejecting those with no alternative would break the feature this check exists to protect.So
param:<name>:passes any name straight through asPARAM_<name>, and every rejection message names it:Rule A still applies to whatever follows the prefix, since that is what becomes the variable name.
What is deliberately not on the denylist
timeoutis the load-bearing example. The first draft rejected it andTestBuildLaunchConfigFromParams_WorkflowStepfailed — correctly:examples/workflow-ci-pipeline.yamlsetstimeout: 10mper step andpkg/queue.JobConfighas aTimeoutfield. It is documented vocabulary, not a near-miss.Also excluded, as plausible sweep parameters:
image(a container image),type,count,steps(next to spawn'sstep),instance(an optimisation problem instance),runtime(a container runtime),idle,instances. When in doubt the key passes through. A denylist that swallows legitimate parameters is a worse bug than the one it fixes, andTestClassifyRowKeyAllowsWorkloadParamspins all of them.Tests
cmd/sweep_keys_test.go:TestRecognizedRowKeysMatchesSwitch— the registry of recognised keys is a hand-written duplicate of the switch's case labels, so this test parsescmd/sweep.gowithgo/ast, extracts the labels, and requires equality in both directions. A missing map entry means a real spawn key gets treated as a near-miss and a working file breaks; a missing case label means a key spawn ignores is advertised as recognised, which is this bug wearing a different hat. It also fails if the AST walk finds zero labels, because a broken walk would make every other assertion in it pass vacuously.TestClassifyRowKeyRejects— 19 cases from the issue's table, asserting on the guidance text, not just that an error happened.TestClassifyRowKeyAllowsWorkloadParams— 25 legitimate parameter names, including the ambiguous ones above.TestNoRecognizedKeyIsAlsoReserved/TestReservedKeysAreNormalized— two ways a denylist entry could become unreachable dead guidance that still reads as active.TestValidateSweepParamKeysReportsEveryProblem(all three typos named, the legitimatealpharow not named).test/e2e/tier0_sweep_param_keys_test.go(Substrate, zero spend), 4 tests:ttl_hours:rejected with nothing launched,on-complete:rejected with nothing launched, workload params + an escapedparam:budgetreaching the instance asspawn:param:*tags, and the header listingPARAM_isa.The rejection tests pass a real
--ttlon the command line so the launch would otherwise succeed — without it a non-zero exit would prove only that the sweep was unbounded, not that the key was caught. All 4 verified failing at7f13678and passing here.Cross-refs: #524/#528, #525/#529 — same launch path, same shape. Two further findings from this work are filed separately: #530 (unknown keys at the top level are dropped entirely, including in spawn's own examples) and #531 (param values are interpolated unquoted into
/etc/profile.d, so a$or a quote silently changes what the workload sees). This PR fixes the key side of thatexportline; #531 is the value side.