Repository navigation
fix(config): reject keys the schema does not declare - #439
Merged
Merged
Conversation
config.toml is the sole source of model prices, and every structure accepted any key it did not know: none of the fourteen carried deny_unknown_fields. toml discarded the unknown key, the field stayed at its serde(default) value, and the process loaded, validated, started and settled money at a price the operator never declared. The value a typo leaves behind is 0.0, which is a legal price meaning "free", so validate() could not tell "the operator chose zero" from "the operator misspelled the key" -- it checks signs, not presence. Measured on the shipped example config: adding cache_write_input_per_mm = 3.5 under [[models]] loaded clean, left cache_write_input_per_m at 0.0, and settled 60,000 cache-write tokens at 0.000000 instead of the 0.210000 the operator wrote. All fourteen structures now refuse an undeclared key at load time, and the error names it. Both in-tree literals still parse unchanged -- the embedded DEFAULT_CONFIG that a fresh install boots from, and catalog_gate's copy of config.example.toml -- as does the shipped example itself, and Rollup's documented legacy retain_days remains a declared field, so old configs that use it keep working. Closes #438
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.
Summary
config.tomlis the sole source of model prices, and every structure accepted any key it did not know: none of the fourteen insrc/config.rscarried#[serde(deny_unknown_fields)].tomldiscarded the unknown key, the field stayed at its#[serde(default)]value, and the process loaded, validated, started and settled money at a price the operator never declared. No error, no log line.The value a typo leaves behind is
0.0, which is a legal price meaning "free", sovalidate()could not tell "the operator chose zero" from "the operator misspelled the key" — it checks signs, not presence. That is what made this silent rather than loud.Measured on the shipped
config/config.example.tomlbefore the fix, with one edit per row:cache_write_input_per_mm = 3.5under[[models]]LOADED OKcache_write_input_per_m = 0.0— 60,000 cache-write tokens settled at 0.000000 instead of 0.210000from_tokens = 512001under[[models]]instead of[[models.tiers]]LOADED OKtiers = 0— believed tiered, was notcontext_windowforcontext_lengthLOADED OKcontext_length = 0Related Issue
Closes #438
Changes
#[serde(deny_unknown_fields)]on all fourteen structures insrc/config.rs—Config,Server,Mail,Log,Archive,UsageRetention,Rollup,Billing,Points,Provider,Plan,Endpoint,ModelTier,Model. Not narrowed to the price tables: a misspelling in[log]or[archive]is equally silent, and a subset invites the reader to guess which half is guarded.unknown_config_keys_are_rejected_and_named— the three defect classes above, each of which must now fail to load and whose error must name the offending key. A valid baseline config is asserted to load first, so a failure cannot be blamed on the fixture.config_load_rejects_the_typo_and_the_shipped_example_still_loads— the same rejection through the real entry pointConfig::load(file →toml::from_str→validate), because that is the gate a deployer actually meets at startup, plus an assertion thatconfig.example.tomlstill loads.Naming the key is part of the fix, not a nicety: a rejection that says "unknown field" and stops there is harder to act on than the defect it replaces.
tomldoes name it, and both tests pin that.Tests
cargo test— 586 passed, 0 failed (baseline 584, plus the two new tests).cargo fmt --check— clean.cargo clippy --all-targets -- -D warnings— clean (the CI invocation,.github/workflows/ci.yml:30).Checked by mutation: stripping the attribute from
Modelalone turns exactly the two new tests red — with the message this test writes (Chinese in the source, translated here): "the misspelled price key (cache_write_input_per_m → _mm): must fail to load, yet it was accepted" — while the other 24 tests in the module stay green. Restored: 586 / 0.The three in-tree literals were verified to still parse, since each one is a config the project ships or embeds:
DEFAULT_CONFIGinsrc/main.rs, which a fresh standalone install boots from —embedded_default_config_is_completestill green.CONFIG_TOMLinsrc/catalog_gate.rs, the catalogue gate's copy ofconfig.example.toml— still green.Rollup's documented legacy keyretain_daysis a declared field (Option<i64>), so configs that still use it keep loading —rollup_legacy_retain_days_still_selects_the_windowstill green. The hardening does not break the one compatibility path the file deliberately keeps.Billing impact, and the upgrade note
For a correctly configured deployment no stored row changes and no bill changes: this changes what is accepted, not how anything is priced. Correct configs parse to exactly the same values as before, which the existing suites demonstrate.
What does change is that a config carrying a stray or outdated key now fails to start, naming the key. That is the intent — a config that says something the runtime ignores should not look healthy — but it makes the upgrade fail-closed rather than fail-silent.
Deployers should diff their config against
config/config.example.tomlbefore upgrading, or grep for the keys they actually use. Any key that survives the diff is one the schema declares; anything else was being silently ignored until now, and the fix will tell them which.Deliberate boundaries
tokenssemantics,seed_modelsbehaviour or schema change.