Skip to content

[ConfigManager] Install resolved configuration into the boot source - #3973

Open
bdchatham wants to merge 2 commits into
mainfrom
plt-775-appopts
Open

[ConfigManager] Install resolved configuration into the boot source#3973
bdchatham wants to merge 2 commits into
mainfrom
plt-775-appopts

Conversation

@bdchatham

Copy link
Copy Markdown
Contributor

A node's configuration is answered today by viper, reading a file, the environment and flags. The registry declares keys and resolves a value for each. This is the piece that puts one into the other: Install writes every declared key into the source a booting node reads, at override precedence, and leaves every other key alone.

Start with config/appopts/appopts.go — the package doc explains why it layers rather than replaces, which is the decision the rest follows from.

Properties

  • A declared key reads the registry over everything else. It is written at override precedence, so no resolution runs when the node reads it.
  • Nothing undeclared is disturbed. A value can only be resolved ahead of the read if its name is known, and a name is known only once a section declares it. An environment cannot be enumerated for a prefix, so a value delivered that way under an undeclared key is readable and unlistable at the same time: building a fresh source from an enumeration would drop exactly those values and replace an operator's setting with a code default. Leaving them alone cannot, because the code that answers them is unchanged.
  • The key space only grows. Installing adds declared keys and removes none, so a key a node read before it reads after.
  • Two declared keys where one names a prefix of the other are refused. A source holds a value at a path, so writing a.b turns a into a table and destroys what a held, and writing a afterwards destroys the table. No order installs both. This is a property of how the source stores a key rather than of whether the key space is coherent, which is why it is checked here and not at registration.
  • A refused install changes nothing. The refusal happens before the first write.
  • The report is taken before anything is written. Installing makes every declared key enumerable, and the report names the set that was not, so taken afterwards it is always empty.

Report names the three populations: the declared keys written, the keys that still read as they always have, and the declared keys the source did not carry. The second is the migration that remains, and it shrinks as sections are declared.

Scope

This is the package alone. Nothing here calls it: the boot path that installs on startup is a separate change.

Three things it does not carry, each for want of a caller today. A one-line summary of the report, and the mapping from a node mode to the mode Tendermint runs, both belong with the command that prints them. Reconciling the mode sei.toml records against the one config.toml runs is a diagnostic, and it arrives with the diagnostic that asks for it.

Verified

build, vet, gofmt -s, goimports, golangci-lint clean. 10 cases under -race, 100% of statements.

Clobbering an undeclared key fails three tests, taking the report after installing fails two, and dropping the collision refusal fails two. A fold to lower case on what the source enumerates was removed rather than tested: a source lower-cases a key on the way in, so the fold could not fire.

… boots with

A key a section declares is written at override precedence, so the registry's
answer wins over the file, the environment and any flag. Every other key is left
exactly as it was.

Layering rather than replacing is what makes the migration possible. A value can
only be resolved ahead of the read if its name is known, and a name is known only
once a section declares it. An environment cannot be enumerated for a prefix, so
a value delivered that way under a key nothing declares is readable and
unlistable at the same time: building a fresh source from an enumeration would
drop exactly those values and replace an operator's setting with a code default.
Leaving them alone cannot, because the code answering them is unchanged.

Two declared keys where one names a prefix of the other are refused. A source
holds a value at a path, so writing a.b turns a into a table and destroys what a
held, and writing a afterwards destroys the table; no order installs both. That
belongs here rather than at registration, because it is a property of how this
source stores a key and not of whether the key space is coherent.

The report is taken before anything is written. Installing makes every declared
key enumerable, and the set that was not enumerable is what the report names, so
taken afterwards it is always empty.

Three things this package does not carry yet, each for want of a caller. A
one-line summary of the report, and the mapping from a node mode to the mode
Tendermint runs, both belong with the command that prints them. Reconciling the
mode this file records against the one Tendermint runs is a diagnostic, and it
arrives with the diagnostic that asks for it.

10 cases, 100% of statements, race clean, 0 lint issues.

Mutation-checked: clobbering an undeclared key fails three tests, taking the
report after installing fails two, and dropping the collision refusal fails two.
A fold to lower case on what the source enumerates was removed rather than
tested, because a source lower-cases on the way in and the fold could not fire.
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 20, 2026, 7:20 PM

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.50000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.61%. Comparing base (01f3c6a) to head (28004bf).

Files with missing lines Patch % Lines
config/appopts/appopts.go 97.50% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3973      +/-   ##
==========================================
- Coverage   58.69%   57.61%   -1.08%     
==========================================
  Files        2326     2229      -97     
  Lines      199122   187916   -11206     
==========================================
- Hits       116877   108275    -8602     
+ Misses      71477    69758    -1719     
+ Partials    10768     9883     -885     
Flag Coverage Δ
sei-chain-pr 97.50% <97.50%> (?)
sei-db 70.02% <ø> (+0.21%) ⬆️
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
config/appopts/appopts.go 97.50% <97.50%> (ø)

... and 136 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bdchatham
bdchatham marked this pull request as ready for review August 20, 2026 18:37
@cursor

cursor Bot commented Aug 20, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches how node configuration is composed (viper override layer vs operator file/env values). The package is unused on the boot path yet, but incorrect layering or collision checks would drop or override operator settings once wired.

Overview
Adds config/appopts.Install, which layers registry-resolved values into the existing viper source a node already reads, instead of replacing that source.

Declared keys are written at override precedence so the registry wins over file, env, and flags. Undeclared keys are left untouched, including env-only values that viper can Get but not enumerate. A Report lists installed, passthrough (still-to-migrate), and newly added keys, taken before any write.

Install refuses colliding declared prefixes and any write that would silently shadow an undeclared value (including env-only ancestors). Refusals happen before the first Set, so a rejected install leaves the source unchanged. Nothing in this PR calls Install; boot wiring is left for a follow-up.

Reviewed by Cursor Bugbot for commit 28004bf. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c80c82c. Configure here.

Comment thread config/appopts/appopts.go

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New config/appopts package is well-documented and thoroughly tested, but its central guarantee — "nothing undeclared is disturbed" — is only enforced between declared keys, leaving the reachable case (a declared key that prefixes, or is prefixed by, an existing undeclared key in the target) unguarded.

Findings: 1 blocking | 0 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • None at the file/PR level.

Comment thread config/appopts/appopts.go
…ared

The package promises that migrating one section does not change what any other
key answers. It checked only declared keys against each other, and that pair
barely arises. The pair that does arise is a declared key against an undeclared
one, and it was unchecked in both directions. Measured:

  declared giga_executor over an operator's giga_executor.enabled
    before true, after false
  declared a.b.c over an operator's a.b = 42
    before 42, after map[c:x], and GetInt reads 0

Neither reports anything. The report went further and named the lost key as one
that still reads as it always has.

The stake is not hypothetical. giga_executor.enabled is set on two production
nodes, one of them the shadow replayer whose whole job is finding an AppHash
divergence between the two executors. With the executor silently off it compares
one implementation against itself, finds nothing, and reports a release safe.

A source holds one value per path, so the two keys cannot both occupy theirs.
That makes this a refusal rather than a report: skipping the declared key leaves
a section half migrated and a key whose answer depends on which reader a caller
asked, where refusing names both keys and stops before the first write.

The collision walk is replaced rather than repaired. It read a sorted key list
and stopped at the first key that was not a child, which assumes the children
follow directly. A hyphen sorts before a dot, so a hyphenated sibling sits
between a key and its children and ends the walk. This key space separates words
with hyphens, and grpc-web.address already sits between grpc and grpc.enable. The
paths a key nests under are now derived from the key, which assumes nothing about
order and answers the target-side question too.

The source is asked whether it holds a value, not whether it lists one, because a
value delivered through the environment is answerable and unlistable. It is asked
with IsSet rather than a nil read, because a bound flag nobody set still answers
with its default and refusing over one would be a false alarm.

Five mutations each fail a named test: restoring the sorted scan, dropping the
shadowing refusal, removing the IsSet gate, dropping the table arm that keeps
every nested section installable, and swapping IsSet for a nil read.

24 cases, 98.5% of statements, race clean, 0 lint issues.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant