[ConfigManager] Check a Node's sei.toml - #4045
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
|
@seidroid review |
f70df38 to
ae60b14
Compare
e9161f4 to
0dc6b00
Compare
ae60b14 to
93d7b68
Compare
0dc6b00 to
724ba6e
Compare
PR SummaryMedium Risk Overview The check covers the config gate (
Reviewed by Cursor Bugbot for commit a446747. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
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'srunCheck,runCheckWithNodeFileandTestCheckReportsAFileItCannotReadskipconfigtest.Isolate(t), unlike the sibling tests in the same file, yet they resolve throughos.LookupEnvand reados.Getenv(EnvVar). A straySEID_*orSEI_CONFIG_MANAGERin a developer's or runner's environment can change a resolved value and flip the pass/fail assertions. - [suggestion]
runCheckThroughRootre-implementssvrcmd.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 momentExecutechanges.root.SetArgs(...)followed bysvrcmd.Execute(root, home)pins it to the real wiring, and there is no import cycle — nothing undersei-cosmosimportscmd/seid/cmd. - [suggestion]
cmd/seid/cmd/configmanager/doc.gostill describes the package as only the boot-time manager selected behindSEI_CONFIG_MANAGER; the new operator-facingsei-config checkcommand 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 withrefuseWhatDecodesToSomethingElsewhile the function it documents iswhatDecodesToSomethingElse, so the godoc does not name its subject.
724ba6e to
1f912df
Compare
0b3492e to
f1ef6f1
Compare
1f912df to
b58c89b
Compare
f1ef6f1 to
a697060
Compare
b58c89b to
327f551
Compare
327f551 to
24c78ae
Compare
a697060 to
48812ba
Compare
|
@seidroid review |
There was a problem hiding this comment.
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 againsttmcfg.DefaultConfig()whiledeliverOneSectionrehearses against the node's realctx.Config. The doc acknowledges the weaker target, but the command already opensconfig.tomlfor the node kind — decoding that file into thetmcfg.Configused as the rehearsal base would close most of the gap for the cost of the read the fix above already needs, and would makeValidateBasicanswer for the configuration the node actually holds. - 2 suggestion(s)/nit(s) flagged inline on specific lines.
48812ba to
9e58ca0
Compare
384cd38 to
6fc3394
Compare
6fc3394 to
b9aa629
Compare
9802cfe to
f60169a
Compare
b9aa629 to
1e9837e
Compare
f60169a to
df86468
Compare
5d84309 to
1d9e345
Compare
45781b9 to
766b285
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
766b285 to
0244d06
Compare
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>
0244d06 to
92fba02
Compare

A command that answers what a node's
sei.tomlreaches, without starting the node. Second of two.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.tomlsays validator while its own file says full resolves avalidator'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
configcommand, which reads and writesclient.toml. Neither shadows theother. Its own pre-run hook stops a question about one file generating two others.
Verified
Formatters,
go vetandgolangci-lintclean.-race -count=2 -shuffleclean. Driven against abuilt binary for the new command and for
seid config <key> [value]beside it.