feat(defs): throw on malformed BSL arguments instead of coercing (audit theme 6) - #139
Open
passcod wants to merge 4 commits into
Open
feat(defs): throw on malformed BSL arguments instead of coercing (audit theme 6)#139passcod wants to merge 4 commits into
passcod wants to merge 4 commits into
Conversation
The defs layer had two styles for consuming a script value. The checked one
converts, names the argument and the actual type, and throws at the script
line that is wrong. The coercing one — into_string().unwrap_or_default(),
filter_map(try_cast), bare as casts — turns a type error into a different
meaning: select(#{ types: ResourceType.Service }) with the brackets forgotten
dropped the criterion and matched every resource in the app, so a following
rt.stop stopped every workload; pids_limit(4294967297) wrapped to 1;
command(["nginx", 8080]) produced an argv containing "".
crates/core/src/defs/take.rs is the checked style extracted, and the ~17
coercing sites in selector, container, ingress, install and app now use it.
Selector::from_map is fallible and rejects unknown criteria, since the spec
says all possible keys are defined in it. Two adjacent validation gaps ride
along: an inverted scale(5..2) range and a digest hex check that accepted
g-z. A CI grep keeps the idiom out of the layer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtxQdsF6YhLzz9RrDmHEDv
Tracey requires every rule to carry an impl annotation; the take module had only verify references from the tests.
Contributor
Code Coverage OverviewLanguages: TypeScript, Rust TypeScript / code-coverage/vitestThe overall coverage in commit 246b0b4 in the Rust / code-coverage/rustThe overall coverage in commit 246b0b4 in the Show a code coverage summary of the most impacted files.
Updated |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtxQdsF6YhLzz9RrDmHEDv
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Aug 2, 2026
Both add a guard step to the lint job, so the two land side by side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtxQdsF6YhLzz9RrDmHEDv
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 cross-cutting theme 6 from the logic bug audit: silent coercion in the BSL defs layer.
The class
The defs layer had two styles for consuming a
rhai::Dynamic. The checked one —take_retries,Port::new,validate_scale— converts, names the argument and the actual type, and throws, so rhai reports the script line that is wrong. The coercing one —into_string().unwrap_or_default(),filter_map(try_cast), bareascasts — produces not a failure but a different meaning, surfacing far from the cause at reconcile or container-start time:select(#{ types: ResourceType.Service })— array brackets forgotten — dropped the criterion, and aselectwith no surviving criteria matches every resource in the app, so a followingrt.stopstopped every workload.pids_limit(4294967297)wrapped to1; the container cannot start a workload.command(["nginx", 8080])produced an argv containing"".kindfell back to the default, quietly installing apasswordparam as plain text.The coercing style was never asserted by a test or licensed by the spec.
The change
crates/core/src/defs/take.rsis the checked style extracted —take_string,take_bool,take_map,take_array,take_string_array,take_array_of,take_int_in_range— keeping the conventions of the sites it came from: backtick-quote the argument as the script author wrote it, state what was expected, report what arrived, and give the index for array elements. The ~17 coercing sites acrosscollection/selector.rs,container.rs,ingress.rs,app/install.rsandapp.rsnow use it, so the path of least resistance for the next builder istake_string(...)?rather than a silent default.Selector::from_mapbecomes fallible and additionally rejects unknown criteria —l[collection.select]says "All possible keys are defined in this spec", so a typo'd key is a criterion that constrains nothing.Collection::selectandApp::selectreturnResult, which rhai supports directly.Two adjacent s2 findings ride along, since they are validation gaps in the same functions: the inverted
scale(5..2)range (H20) and a digest hex check usingis_ascii_lowercase, which acceptsg–z. The three remaining hand-rolled range checks (validate_scale,validate_scale_lower,take_retries) fold intotake_int_in_range, so no unchecked narrowing cast is left in the layer.Findings closed
selectcriteria silently invert to select-everything (or nothing)pids_limitandstop_timeoutsilently truncate i64 → u32scale(5..2)range acceptedkindand non-map entries in param schemas silently acceptedg–zBehaviour change — needs a release note
Scripts that "worked" via coercion now throw at evaluation. That is the intended outcome — any script relying on it was already misbehaving, matching everything, running with empty argv elements, or storing a password param in plain text — but an
/apps/updatethat previously succeeded can now fail.The ordering constraint this created has been discharged: until
AppRegistry::reloadstopped publishing a partially-evaluated definition (#138), a newly-throwing script during/apps/updatewould have triggered the destructive post-reload path — volume hold, scaling wipe, forward teardown. That fix is onmain, so this is safe to land.Enforcement
l[bsl.args.strict]inlanguage.md— malformed input is never coerced, defaulted, or silently ignored, and the error names the argument, the expectation and what was supplied. Explicitly carves out spec-defined coercions such ascol.l[ingress.redirect]now pins the accepted code range at 300–399, which was previously unstated and therefore unenforceable.run_test_script_errcase per converted builder, asserting both that it throws and that the message names the argument —selectwith a scalartypes, a non-ResourceTypeelement, a non-stringnameselement, an unknown criterion,pids_limit/stop_timeoutpastu32,command/argwith a non-string element, a non-mapenventry, a non-hex digest,scale(5..2),redirect(80, 200).etc/ci/check-defs-coercion.shfails on the idiom insidecrates/core/src/defs/. Genuine type dispatch that ends in a throw —col()'s successive casts, the string-vs-array dispatch intake_command_cmd— is exempt via a// take: dispatchmarker; neither needs one today.Not in scope
Value-level gaps where the type is right and the domain check is missing are only fixed where they sit in a touched function. The s2 semantic findings — frozen-reference semantics for
external_service, thecol(action)leftover branch, thesecret(false)tri-state — are model bugs, not coercion. And nothing here catches a type-valid mistake such asnames: ["sevrice-a"]selecting nothing.