Skip to content

[ConfigManager] Check a Node's sei.toml - #4045

Merged
bdchatham merged 13 commits into
mainfrom
plt-775-install-check
Sep 3, 2026
Merged

[ConfigManager] Check a Node's sei.toml#4045
bdchatham merged 13 commits into
mainfrom
plt-775-install-check

Conversation

@bdchatham

@bdchatham bdchatham commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

A command that answers what a node's sei.toml reaches, without starting the node. Second of two.

seid config check

It reports which declared keys the file states, which names in it reach nothing, and which written
values a delivery would decline. Its exit status is the answer.

What it answers

A file that will not parse, records a schema version this binary does not know, or names no node
kind exits non-zero. A node with no file has nothing that could be wrong, and stays quiet.

It names a disagreement about what kind of node this is. Two files record that under different
names, and a node whose sei.toml says validator while its own file says full resolves a
validator's answers and serves queries.

It says whether the gate would make a boot read the file at all, so a pass is not read as "in use
and correct". The prediction comes from the same code that would decline the value at boot.

Where it lives

Under the existing config command, which reads and writes client.toml. Neither shadows the
other. Its own pre-run hook stops a question about one file generating two others.

Verified

Formatters, go vet and golangci-lint clean. -race -count=2 -shuffle clean. Driven against a
built binary for the new command and for seid config <key> [value] beside it.

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.74074% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.27%. Comparing base (f77f645) to head (a446747).

Files with missing lines Patch % Lines
cmd/seid/cmd/configmanager/check.go 90.56% 8 Missing and 7 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4045      +/-   ##
==========================================
- Coverage   61.29%   60.27%   -1.02%     
==========================================
  Files        2183     2076     -107     
  Lines      191331   178944   -12387     
==========================================
- Hits       117268   107866    -9402     
+ Misses      62978    61040    -1938     
+ Partials    11085    10038    -1047     
Flag Coverage Δ
sei-chain-pr 64.23% <90.74%> (?)
sei-db 69.80% <ø> (ø)
sei-db-state-db ?

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

Files with missing lines Coverage Δ
cmd/seid/cmd/root.go 69.00% <100.00%> (+0.23%) ⬆️
cmd/seid/cmd/configmanager/check.go 90.56% <90.56%> (ø)

... and 137 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.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 3, 2026, 5:01 PM

@bdchatham

Copy link
Copy Markdown
Contributor Author

@seidroid review

@bdchatham
bdchatham force-pushed the plt-775-install-decode branch from f70df38 to ae60b14 Compare August 27, 2026 22:09
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from e9161f4 to 0dc6b00 Compare August 27, 2026 22:09
@bdchatham
bdchatham force-pushed the plt-775-install-decode branch from ae60b14 to 93d7b68 Compare August 31, 2026 14:29
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 0dc6b00 to 724ba6e Compare August 31, 2026 14:29
@bdchatham
bdchatham marked this pull request as ready for review August 31, 2026 14:29
@cursor

cursor Bot commented Aug 31, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes operator-facing node configuration validation and deploy runbook exit codes, but the command is read-only and reuses existing resolution/delivery logic rather than altering boot behavior.

Overview
Adds seid config check under the existing config command so operators can validate a node’s sei.toml without starting the node. The command resolves configuration the same way a boot would (registry, env, flags), prints notes and problems, and exits non-zero when anything would fail or mislead at runtime.

The check covers the config gate (SEI_CONFIG_MANAGER), unreadable or invalid files, undeclared keys, node mode mismatches between sei.toml and config.toml, resolution defects, and section-level decode/delivery refusals aligned with boot behavior. It uses a no-op PersistentPreRunE so the root hook does not generate config.toml/app.toml or treat CLI flags as file-written keys.

configmanager package docs describe the new workflow. Unit and integration tests exercise the command directly and via the real root (config check --home), including pass/fail cases and parity with delivery refusals.

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

seidroid[bot]
seidroid Bot previously requested changes Aug 31, 2026

@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.

Adds a seid sei-config check preflight command plus its own command group with a no-op pre-run hook, and loosens modesDisagree to treat an empty running mode as agreement. The command's exit status contradicts its own report when the SEI_CONFIG_MANAGER gate is invalid, and its node-kind comparison silently disagrees with what a boot would compute on a node with no config.toml.

Findings: 1 blocking | 5 non-blocking | 2 posted inline

Blockers

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

Non-blocking

  • [suggestion] check_test.go's runCheck, runCheckWithNodeFile and TestCheckReportsAFileItCannotRead skip configtest.Isolate(t), unlike the sibling tests in the same file, yet they resolve through os.LookupEnv and read os.Getenv(EnvVar). A stray SEID_* or SEI_CONFIG_MANAGER in a developer's or runner's environment can change a resolved value and flip the pass/fail assertions.
  • [suggestion] runCheckThroughRoot re-implements svrcmd.Execute (the client/server context values, the two logging persistent flags, PrepareBaseCmd) rather than calling it, so its stated claim to use "the same wiring the binary uses" drifts silently the moment Execute changes. root.SetArgs(...) followed by svrcmd.Execute(root, home) pins it to the real wiring, and there is no import cycle — nothing under sei-cosmos imports cmd/seid/cmd.
  • [suggestion] cmd/seid/cmd/configmanager/doc.go still describes the package as only the boot-time manager selected behind SEI_CONFIG_MANAGER; the new operator-facing sei-config check command it now also owns is not mentioned anywhere in the package doc.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.
  • 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] cmd/seid/cmd/configmanager/tendermint_copy.go:200 — the doc comment opens with refuseWhatDecodesToSomethingElse while the function it documents is whatDecodesToSomethingElse, so the godoc does not name its subject.

Comment thread cmd/seid/cmd/configmanager/check.go Outdated
Comment thread cmd/seid/cmd/configmanager/check.go Outdated
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 724ba6e to 1f912df Compare August 31, 2026 14:45
@bdchatham
bdchatham force-pushed the plt-775-install-decode branch from 0b3492e to f1ef6f1 Compare August 31, 2026 15:15
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 1f912df to b58c89b Compare August 31, 2026 15:15
@bdchatham
bdchatham force-pushed the plt-775-install-decode branch from f1ef6f1 to a697060 Compare August 31, 2026 16:02
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from b58c89b to 327f551 Compare August 31, 2026 16:02
Comment thread cmd/seid/cmd/configmanager/check.go
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 327f551 to 24c78ae Compare August 31, 2026 16:15
@bdchatham
bdchatham force-pushed the plt-775-install-decode branch from a697060 to 48812ba Compare August 31, 2026 16:15
Comment thread cmd/seid/cmd/configmanager/check.go Outdated
@bdchatham

Copy link
Copy Markdown
Contributor Author

@seidroid review

seidroid[bot]
seidroid Bot previously requested changes Aug 31, 2026

@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.

The seid sei-config check command is well-tested and the two previous inline findings are genuinely fixed, but the new config.toml reader treats an unparseable or unreadable file as an absent one, so the check passes a node whose boot fails outright — the same "false pass in the dangerous direction" class as the last blocker, and a violation of this PR's own "a file it cannot read is a problem, not an absence" doctrine.

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

Blockers

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

Non-blocking

  • [suggestion] whatADecodeWouldRefuse (check.go:201) rehearses each section against tmcfg.DefaultConfig() while deliverOneSection rehearses against the node's real ctx.Config. The doc acknowledges the weaker target, but the command already opens config.toml for the node kind — decoding that file into the tmcfg.Config used as the rehearsal base would close most of the gap for the cost of the read the fix above already needs, and would make ValidateBasic answer for the configuration the node actually holds.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread cmd/seid/cmd/configmanager/check.go Outdated
Comment thread cmd/seid/cmd/configmanager/check.go
Comment thread cmd/seid/cmd/configmanager/check.go
@bdchatham
bdchatham force-pushed the plt-775-install-decode branch from 48812ba to 9e58ca0 Compare August 31, 2026 17:49
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 384cd38 to 6fc3394 Compare August 31, 2026 17:49
Comment thread cmd/seid/cmd/configmanager/check.go Outdated
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 6fc3394 to b9aa629 Compare August 31, 2026 17:58
@bdchatham
bdchatham force-pushed the plt-775-install-decode branch from 9802cfe to f60169a Compare August 31, 2026 18:03
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from b9aa629 to 1e9837e Compare August 31, 2026 18:03
Comment thread cmd/seid/cmd/configmanager/check.go Outdated
@bdchatham
bdchatham force-pushed the plt-775-install-decode branch from f60169a to df86468 Compare August 31, 2026 19:55
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 5d84309 to 1d9e345 Compare September 2, 2026 19:11
Comment thread cmd/seid/cmd/configmanager/check.go
@bdchatham
bdchatham force-pushed the plt-775-install-check branch 3 times, most recently from 45781b9 to 766b285 Compare September 2, 2026 21:12

@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 766b285. Configure here.

Comment thread cmd/seid/cmd/configmanager/check.go
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 766b285 to 0244d06 Compare September 3, 2026 14:26
@bdchatham bdchatham changed the title [ConfigManager] Check a Node's sei.toml 2/2 [ConfigManager] Check a Node's sei.toml Sep 3, 2026
Base automatically changed from plt-775-install-app to main September 3, 2026 15:13
bdchatham and others added 12 commits September 3, 2026 09:20
An operator running this before a restart is asking whether their file is right. It
answers without starting a node: which declared keys the file supplies, which names
reach nothing, and which written values a delivery would refuse.

Predicting the refusals is why this sits on top of the deliveries rather than beside
them. The answer has to come from the same code that would refuse the value at boot, or
it is a second opinion that can disagree with the one that matters.

A file that cannot be read is reported as a problem of a file that was found, so the
command exits non-zero. It previously reported that the node had no file at all, which
is both wrong and the answer least likely to make an operator look. Its own group of
commands rather than a subcommand of the existing one, which reads and writes the files
this is about rather than the file that replaces them.

Verified by mutation: collapsing an unreadable file back to an absent one fails all
three of the cases an operator hits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tor runs it on

The command failed on a correct file. Run the way a runbook runs it, with --home,
it exited non-zero and named `home` as a key sei.toml writes:

  $ seid sei-config check --home /var/lib/sei
  home: sei.toml writes this and no section declares it, so it has no effect
  Error: 1 problem(s); a boot would apply what it could and report the rest

The file contains no such key. Every unmatched flag reached the resolution under
its own name and was counted with the file's own undeclared keys. It now reports
only the file's. A pre-flight that fails every time carries nothing, and this
command is the whole compensating control for a boot that may not refuse a file.

It also wrote the files it was asked about. The root command's hook runs the
configuration handler, which generates config.toml and app.toml when they are
absent, so asking a question about one file created two others. The same hook
copies configuration values into flags and marks them changed, which is exactly
the state that makes a flag indistinguishable from a key an operator's app.toml
holds, and this command reports on what was typed. The command group now carries a
hook of its own, which stops both.

It was silent on the question with the largest consequence. Two files record what
kind of node this is, under different names, and nothing keeps them in step. A node
whose sei.toml says validator while its own file says full resolves a validator's
answers and serves queries, and every report about it reads correctly. The boot
reports that at its loudest level and the check did not ask. A node that has no
configuration file of its own has nothing to disagree with, and that case is now
answered in the one function both callers pass through rather than at each of them.

It said nothing about whether a boot would read the file at all. Until the gate is
switched a boot reads none of it, so a passing check read as "in use and correct"
on every node, which invites trusting a file nothing reads. It now says so, and it
names a gate value this binary would refuse outright.

The refusal messages spliced two sentences into one, and the tests could not see any
of this: they executed the command with no parent, so no hook ran, no flag was
marked changed, and no file was generated. The new tests run it through the real
root command with the wiring the binary uses.

Not rehearsed: the install into the source a node builds, because that source does
not exist until a boot builds it. A key can be refused there for a reason nothing
here can see. Stated in the command's own documentation, along with the fact that
this reads the environment of whoever runs it rather than the node's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A gate value this binary refuses was printed and not counted, so the command said
both "a boot would refuse before reaching this file" and "every value this file
supplies is one this binary can use", and exited zero. A runbook gating on the status
read a pass for a node that cannot start. The gate is now a problem, and it is
answered before anything about the file, because a refused gate stops the node
whether or not there is a file to check.

A node with no configuration file of its own is compared against what a boot would
compute rather than against nothing. A boot starts from this binary's defaults and
writes that file itself, so the running kind is never empty on that path. Answering
empty here passed a node whose sei.toml named a different kind, which then reported
the disagreement at its loudest level on the next start: a pass in the case with the
largest consequence. The branch in modesDisagree that allowed for an empty kind is
gone, because neither caller can produce one now.

The through-the-root test calls the binary's own entry point instead of a copy of its
wiring. The copy would drift the moment the real one changed, and the claim the test
rests on is that the command runs the way an operator runs it.

Three test helpers now isolate the environment they resolve through, the two homes
they build record what kind of node they are, and the package documentation names the
command this package owns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ry does

The rehearsal took the decode and stopped there, so a value the node's own rules
reject passed the check and the boot then dropped its section. The check exists to
prevent that divergence rather than produce it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t the resolution already knows

An unreadable or unparseable config.toml was answered with this binary's default node
kind, so it was indistinguishable from a node that has none. A boot does not start at
all on one of those, so the common case, a node whose sei.toml says full, got a clean
pass and exit zero from a command whose whole contract is that the exit status is the
answer. Only an absent file routes to the default now; every other failure is reported
as a file that cannot be read, which is the rule this command already applied to
sei.toml.

Two problems the resolution already carries are now reported. A refused registration
matters most for what it does to the report beside it: the section's keys are absent
from the declared set, so an operator's valid key for one of them lands among the keys
nothing declares, and this command told them their file was wrong about a key it was
right about. Reported before that, so whoever reads in order meets the cause first.
The other is a variable set for a key the environment cannot carry, which the boot
warns about and this command was silent on.

Usage is silenced. A mistyped value is not a usage error, and nothing in the production
wiring silences it, so cobra followed the error with the whole usage block into the
same stdout the report had just been written to. Both test harnesses set it themselves,
which is why the shape was not visible from either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A refused registration leaves its section's keys out of the declared set, so an
operator's valid key for one of them reads as a key nothing declares. Printed after
that line, the defect explains something the reader has already concluded was their
own typo. It now comes first, which is the order the boot already uses and the order
the helper's own comment described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A present but empty mode was answered with this binary's default. A boot unmarshals
that empty string over its default and runs with no kind at all, then reports the
disagreement, so the check passed a node the boot goes on to complain about. The case
that separates the two answers is a sei.toml naming the default kind, which is what
the test drives.

The rehearsal reads the decoded sections through the accessor the boot uses, since the
narrower projection is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oes not state

Every declared key sei.toml leaves out takes the value this binary declares, so a
sparse file is not a small change to a node that has been running: it is most of its
configuration. An operator running this before a restart is owed the count, because it
is the difference between moving one setting and replacing everything around it.

    this file states 1 of 274 declared keys; the other 273 take the value this binary
    declares for a validator, whatever app.toml and config.toml currently say

A note rather than a problem, because it is the design working and there is no file for
which it is absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`seid sei-config check` becomes `seid config check`. The node's configuration is
what this reads, and "sei" adds nothing to that name inside the sei binary.

One namespace holds both commands. The existing one reads and writes client.toml.
check reads the node's sei.toml and writes nothing. Neither shadows the other: no
client.toml key is named check, and cobra resolves a subcommand before an argument.

The no-op pre-run hook moves to check itself, from the group that used to wrap it.
Cobra runs the closest hook it finds, so `seid config <key> [value]` keeps running
the root hook and behaves exactly as before. Verified against a built binary for
query-all, query-one and set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ode's own configuration

The count told an operator a binary default applies where it does not. It counted
every declared key absent from sei.toml, and a key answered by an environment
variable or a flag is absent from the file while taking that source's value. The
three sets are now counted apart, and a test holds them to summing to the whole.
Nothing covered that arithmetic, and the sentence reads correctly whatever the
numbers are.

The godoc for the node-kind comparison claimed neither caller passes an empty
running mode. Both can. A node's own file can state the key with nothing after it,
a boot unmarshals that over its default, and the comparison is what reports it,
which a test already required. As written the comment invited restoring an early
return that would hide it.

The gate note asked the environment which value means v2. Select owns that mapping
and was already called on the line above, so the note now asks the manager it
returned. A value added to Select later cannot make this say a boot reads none of
the file on a node where it does.

The rehearsal now runs against the node's own configuration rather than a fresh
one, which is the target the delivery uses. Every declared key of a section is
delivered, so the values under test are the same either way today; sharing the
target is what keeps the two answers together as the declared set changes. The
node's file is read once and decoded, so the node kind and the rehearsal base come
from the same read, and a file that will not decode is an error rather than a
default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check rehearsed a section and then held the whole configuration to its rules.
Those stop at the first failing section, so a failure standing anywhere in the
node's own config.toml was reported as this section's written values being refused.
Measured on a node whose state sync is armed with no servers listed, which its own
rules reject and a boot never validates on an existing file: the check named nine
sections refused and the delivery refused none. The one section at fault was the
only one not named, because its own declared values correct the failure before the
whole-configuration sweep runs.

Both sides now ask the same function. A boot and this command answer alike for the
same pair of files, which is the only reason to predict a refusal here rather than
form a second opinion.

There is no gap in the other direction today. The whole configuration's rules call
every section type that states any, and the two registered sections it skips state
none, so its answer was a superset. That stops being true the moment either of
those states a rule, and asking the section removes the surface rather than
leaving it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every path here joins the home with config/sei.toml. An empty home left that
relative, so the read landed wherever the command was run from and reported on a
file belonging to another node. Without the guard the check answers confidently:
it named a mode disagreement and counted the declared keys the file leaves out,
all about a file the node it was asked about does not have.

An operator runs this to decide whether to restart, so a confident answer about
the wrong node is worse than declining. The boot declines the same case. Refused
rather than reported, because the exit status is this command's answer and there
is nothing here to have an opinion about.

The message names the variable the home resolves from, derived from the running
binary the same way the resolver derives it, so it cannot drift from the name that
works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bdchatham
bdchatham force-pushed the plt-775-install-check branch from 0244d06 to 92fba02 Compare September 3, 2026 16:25
@bdchatham
bdchatham enabled auto-merge September 3, 2026 16:31
@bdchatham
bdchatham disabled auto-merge September 3, 2026 16:37
@bdchatham
bdchatham enabled auto-merge September 3, 2026 17:02
@bdchatham
bdchatham added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit f9471b4 Sep 3, 2026
72 checks passed
@bdchatham
bdchatham deleted the plt-775-install-check branch September 3, 2026 17:41
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.

2 participants