Skip to content

fix(config): let a UI read config from a newer daemon - #413

Draft
hyperb1iss wants to merge 3 commits into
mainfrom
nova/tolerant-config-wire
Draft

hyperb1iss wants to merge 3 commits into
mainfrom
nova/tolerant-config-wire

Conversation

@hyperb1iss

Copy link
Copy Markdown
Owner

Held as a draft until 0.7.0 ships. It is ready for review now.

A web UI that is older than the daemon it talks to loses its whole Settings surface. The UI decodes GET /api/v1/config into HypercolorConfig, and every config section type carried #[serde(deny_unknown_fields)], so the moment a daemon gains a config key (daemon.state_history_generations most recently) the older UI's fetch fails with unknown field and nothing in Settings loads. That happens to anyone running a remote UI against another computer's daemon, and it happened on the owner's own dev stack, where a stale UI build logged the failure on every socket open. After this PR, clients decode the daemon's config tolerantly, and every authoritative source (the config file, startup overlays, REST writes) still refuses a key it does not model, now naming it by its full path.

What this changes

  • The deny_unknown_fields attribute is gone from the 13 config section types and SessionConfig, so the plain Deserialize impl that clients use skips fields it does not know.
  • Two new strict decoders, HypercolorConfig::deserialize_strict and deserialize_section_strict (in the new hypercolor-types/src/config/strict.rs), decode through serde_ignored, collect every key the derive skipped, and refuse the document with a message such as unknown config keys `daemon.prot`, `rendering.servo_gpu_import.mdoe` .
  • Every authoritative path goes through them: ConfigManager::parse_toml (the only path from hypercolor.toml, used by load, reload, and the daemon's startup parse), dotted overlays in config/sources.rs, both decodes in the daemon's REST config writes, and the config registry's section validator.
  • REST config writes (PUT and PATCH /api/v1/config/keys/{key}) still answer 422 validation_error, now with the key path.
  • The root extensions map and driver settings maps were already open and still accept arbitrary keys.

Why

The repo's agent guide (CLAUDE.md) asks clients to consume tolerant subsets of daemon payloads. The strictness exists for a reason: a typo in hypercolor.toml should fail loudly instead of vanishing on the next rewrite. That rule belongs to the loaders, so this PR moves it there. A strict-config cargo feature gating deny_unknown_fields was considered and rejected: serde behavior that depends on a feature can change silently when Cargo unifies features across crates.

Found while rehearsing the demo film against a live rig.

Verification

  • Added or updated tests
  • Added or updated docs (README, AGENTS.md, relevant spec, or guide)
  • just verify passes locally (Rust fmt + lint + test)
  • just deny passes (required for dependency or license changes)
  • just ui-test and just ui-build pass (required for crates/hypercolor-ui/)
  • just sdk-lint, just sdk-check, and just sdk-build pass (required for sdk/)
  • just python-verify passes (required for python/)
  • just compat-check passes (required for data/drivers/vendors/*.toml)
  • just docs-build passes (required for docs or README changes)
  • cd docs && zola check passes (required for docs link/content changes)
  • Packaging scripts were syntax-checked (required for scripts/ or packaging/)
  • just e2e-build passes with the normal Servo stack (required for daemon/UI/effect integration changes)
  • just e2e-build-cpu passes when validating the CPU smoke fallback
  • just e2e passes against the Servo stack (required for end-to-end behavior changes; starts daemon/browser)
  • Tested on real hardware, simulator, or e2e harness (describe below)

The full just verify gate runs the daemon suite in parallel, which deadlocks on GPU tests on this machine, so the gates ran per crate instead:

Check Result
cargo test -p hypercolor-types 668 passed, including the new client-tolerance and strict-path tests
config_tests, config_sources_tests, config_persistence_tests (core) pass
daemon api_tests filtered to config_, single-threaded 38 passed, including PUT and PATCH rejection with the key path and no write to disk
clippy -D warnings on types, core, daemon (--all-targets) pass
cargo check --workspace and the UI --lib check for wasm32-unknown-unknown pass
cargo deny check, cargo fmt --check pass

The UI's own code did not change (its decode becomes tolerant through the shared types), so just ui-test and just ui-build did not run; only its lockfile gained serde_ignored. The client regression test decodes a config payload carrying invented future keys in the daemon, audio, rendering, and session sections and checks the known values survive. An independent review audited every Rust file that references the config types for a remaining lenient authoritative path and found none; the one regression it raised (the registry validator) is fixed in the last commit.

Notes for reviewers

  • The error text changes from serde's "unknown field x, expected one of ..." to the dotted key path, so the list of valid fields no longer appears. Nothing in the repo matched the old text.
  • The serde_ignored crate cannot see keys inside #[serde(flatten)] or untagged and internally tagged content. The config graph only flattens open maps today; a future closed struct placed behind one would lose strictness without a test failing.
  • A decoder outside this repo that wants strictness must call the strict helpers.
  • Not changed here: PATCH /api/v1/config/keys/session?live=false with {"not_a_real_key": null} still returns 200, because the merge treats null as "remove this member" and the unchanged-value check returns before any decode. The API response envelope types (ApiResponse, ResponseMeta) still deny unknown fields too, which is the same version-skew bug one level up. Both are tracked separately.

🤖 Generated with Claude Code

hyperb1iss and others added 3 commits October 12, 2026 16:12
Add HypercolorConfig::deserialize_strict, which wraps any deserializer
in serde_ignored, collects every key the derive skipped, and refuses
the document naming each one by its dotted path. Root extension
sections and driver entries are open maps, so their keys are captured
rather than skipped and keep loading.

Route every decode that reads an authoritative source through it: the
TOML file loader (ConfigManager::parse_toml, the only path that turns
hypercolor.toml into a config), the env and CLI overlay pipeline, and
both decodes in the REST config write handler. The section types still
deny unknown fields here, so behavior is unchanged; this moves the
rule to the decoders so the types can stop enforcing it on clients.

serde_ignored is dtolnay's MIT OR Apache-2.0 crate and depends only on
serde_core, which the lockfile already carries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The config section types and SessionConfig carried deny_unknown_fields,
so any UI older than the daemon it talks to failed the whole GET
/api/v1/config decode on the first new key (state_history_generations
was the one that surfaced it) and lost the entire Settings surface.
That bites in production, not only in dev: the remote UI embeds this
crate and connects to daemons of other versions.

Drop the attribute everywhere in the config schema. Strictness now
lives in HypercolorConfig::deserialize_strict, which the file loader,
the startup overlays, and REST config writes already use, so a typo in
hypercolor.toml, an unknown overlay key, or a PUT or PATCH naming a key
the daemon does not model is still refused, now with the full dotted
key path instead of a bare field name.

The typo-rejection tests decode through the strict path, and new tests
cover a client decoding an envelope that carries unknown keys in
several sections, the loader naming the file and every key path, an
unknown env overlay key failing the load, and the REST write handler
refusing unknown keys on PUT and PATCH without touching the config.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The capture registry validator decoded its section with plain
serde_json, so once the section types stopped denying unknown fields
it silently accepted the retired monitor key it used to refuse.

Move the serde_ignored decode into a config::strict module and expose
a section-scoped twin, deserialize_section_strict, that names unknown
keys under the section root (capture.monitor rather than monitor).
HypercolorConfig::deserialize_strict and the capture validator share
it, so both report through the same message. Client decodes are
untouched and stay tolerant.

Capture is the only section row with a validator; giving other rows
one would flip their published has_validator flag, which promises
checks beyond the type. The new tests cover the capture validator and
the shared helper across audio, capture, and a nested rendering key,
each asserting the plain decode of the same value still succeeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant