feat(configmanager): SeiConfigManager v2 body — validate-passthrough (PLT-775 PR2) - #3678
feat(configmanager): SeiConfigManager v2 body — validate-passthrough (PLT-775 PR2)#3678bdchatham wants to merge 35 commits into
Conversation
PR SummaryMedium Risk Overview
Adds Tests cover home resolution lockstep with legacy, advisory behavior, legacy-vs-v2 differential/fuzz parity (including env-only keys and “v2 writes nothing”), and root Reviewed by Cursor Bugbot for commit 285be96. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3678 +/- ##
==========================================
- Coverage 61.27% 58.42% -2.85%
==========================================
Files 2351 2185 -166
Lines 197250 178086 -19164
==========================================
- Hits 120864 104055 -16809
+ Misses 65533 64789 -744
+ Partials 10853 9242 -1611
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Small, well-documented, well-tested change that fills in the v2 SeiConfigManager as a validate-passthrough behind SEI_CONFIG_MANAGER=v2; resolveHomeDir faithfully mirrors the legacy home resolution and all validation is advisory, so there are no correctness or boot-safety blockers. Only a stale doc-comment inconsistency and dependency-bump verification are worth noting.
Findings: 0 blocking | 4 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Cursor's second-opinion review (cursor-review.md) was empty — that pass produced no output. Codex (codex-review.md) reported no material findings but noted it could not run targeted tests due to a Go version mismatch (sandbox 1.24.13 vs required 1.25.6), so its pass was static-only.
- go.mod/go.sum bump three deps (add sei-config v0.0.21, BurntSushi/toml 1.4.1-pseudo → 1.5.0, mapstructure v2.4.0 → v2.5.0). The toml and mapstructure bumps appear to be transitive pulls from sei-config; worth confirming
go mod tidyis clean and that the toml 1.5.0 bump doesn't alter behavior of any existing config rendering/parsing in the repo, since CI here could not be run against Go 1.25.6. - Consider a follow-up note/tracking item so the advisory validation messages printed to stderr on every v2 boot (e.g. false-positive validation issues while sei-config read fidelity is still being hardened) don't become persistent log noise before validation is promoted to fatal.
- 1 suggestion(s)/nit(s) flagged inline on specific lines.
Review-gate summary (clean)Coral /xreview slate — all RATIFY:
Findings (all [should]/[nit]) resolved: package-comment doc-drift fixed; validation-error advisory made louder; on-disk-fidelity note added; Bots: Cursor Bugbot ✅ SUCCESS; seidroid ✅ APPROVED (0 blocking). Expected, not a regressionOn a default node, v2 logs a benign advisory Merge prerequisites (separate from the review-gate)Standard CI (Test / Lint / Race / etc.) + the category label, same as the seam PR. |
…(PLT-775 PR2) Fills in the v2 ConfigManager behind SEI_CONFIG_MANAGER=v2 (seam landed in #3671). v2 boot reads the config through the sei-config unified model to VALIDATE it, then re-enters the unchanged legacy reader on the operator's ORIGINAL files — it does not rewrite, migrate, or author sei.toml at boot. The two consumed channels are produced identically to legacy by construction; v2's boot value-add is the validation pass. - SeiConfigManager.Apply: resolveHomeDir (mirrors the legacy handler's flag>SEID_env>default resolution exactly) -> ReadConfigFromDir -> Validate -> re-enter. Advisory: read/validate problems are logged, never refuse boot. - Pin sei-config v0.0.21 (merged lenient-decode fix) so a real seid config.toml can be read for validation. - Legacy-vs-v2 differential test: both managers read the same home (v2 is passthrough); serverCtx.Config + Viper.AllSettings() must match, incl. after the start.go chain-id mutation. Design captured in bdchatham-designs designs/config-manager/DESIGN.md (validate-passthrough; advisory validation; sei.toml-as-source at the generate path; explicit migration). Fatal validation is the un-defer once sei-config read fidelity is hardened (a pruning read-mapping gap is surfaced advisory). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y, home test) PR2 xreview slate (all RATIFY) follow-ups: - doc: package comment said v2 "re-authors" — stale from the old write-then- re-enter model; v2 reads to validate, does not rewrite (idiomatic-reviewer + seidroid, convergent). - advisory: distinguish validation ERRORS with a louder lead-in — they are the same SeverityError class (sc-write-mode) legacy panics on later at app.New(); surfaced earlier, not enforced (systems-engineer dissent F2). - doc: note validation runs against the on-disk files only, not the merged AppOptions, so a default node's pruning advisory is expected/benign — the template omits the key cosmos defaults in code (sei-network-specialist). - test: TestResolveHomeDir_Flag covers the --home resolution. Deferred (dissent F4, sanctioned cut): env-precedence + mixed-home rows ride the generate-path PR — the boot differential already proves passthrough parity by construction. Design MVP-scope reconciled separately (bdchatham-designs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tic findings) Addresses agentic reviewer findings on PR #3678: - lint (errcheck, red CI): the three advisory fmt.Fprintf(cmd.ErrOrStderr()) calls didn't check the return -> `_, _ =` (golangci-lint clean locally). - claude[bot] (correctness): the advisory branch fired only on HasErrors() and printed only Errors(), silently dropping SeverityWarning diagnostics. Now logs the full res.Diagnostics (each carries its [ERROR]/[WARNING]/[INFO] severity) so warnings are surfaced too; errors stay distinguished. - claude[bot] (doc): drop the stale "(forthcoming)" from the package comment — this PR lands the manager. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c18b062 to
13b3a44
Compare
There was a problem hiding this comment.
PR2 fills in SeiConfigManager.Apply as a validate-passthrough behind SEI_CONFIG_MANAGER=v2: it reads/validates config advisorily, then re-enters the unchanged legacy handler on the operator's original files, backed by a legacy-vs-v2 differential test. The change is well-scoped, correct, and low-risk; only non-blocking notes remain.
Findings: 0 blocking | 4 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Both second-opinion passes produced no material findings: Codex reported no findings (and could not run tests — local Go 1.24.13 vs required 1.25.6), and the Cursor review file was empty. REVIEW_GUIDELINES.md was also empty, so no repo-specific standards were applied.
- Dependency blast radius: go.mod bumps github.com/BurntSushi/toml (1.4.1-pre → 1.5.0) and go-viper/mapstructure (2.4.0 → 2.5.0), which affect TOML parsing / decoding repo-wide (e.g. tendermint config read/write), not just configmanager. The differential test only covers configmanager parity; worth confirming the broader CI suite passes on Go 1.25.6 given these are transitively-consumed libraries.
- resolveHomeDir duplicates the legacy handler's home-resolution logic (BindPFlags + SEID_ env prefix + AutomaticEnv). This is a documented, deliberate tradeoff, but it can silently drift from InterceptConfigsPreRunHandler if the legacy resolution ever changes; since divergence only affects which dir gets validated (advisory), impact is limited, but a comment cross-reference or shared helper would harden it.
- The env-precedence branch of resolveHomeDir is acknowledged as untested (only the --home flag path is covered in configmanager_test.go). Low risk since it mirrors legacy viper behavior, but the env path remains unexercised until the generate-path PR.
The flag-driven differential never exercised resolveHomeDir's SetEnvPrefix/AutomaticEnv/replacer mirror of the legacy handler. Add an env-driven twin harness + TestConfigManagerLegacyVsV2Differential_EnvHome: drive home through the environment (no --home) and assert legacy and v2 resolve the SAME home, with a non-vacuous guard that the env var actually drove resolution. Closes the silent-drift seam the advisory design cannot otherwise surface (systems-engineer review). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Clean, tightly-scoped PR2 that implements the v2 SeiConfigManager as an advisory validate-passthrough re-entering the unchanged legacy config reader; parity is guaranteed by construction and well-covered by flag- and env-driven differential tests. No blocking issues found; only minor non-blocking observations.
Findings: 0 blocking | 4 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Second-opinion inputs were empty: cursor-review.md produced no output and REVIEW_GUIDELINES.md is empty, so this synthesis rests only on the Claude pass (Codex explicitly reported no findings, noting it could not run Go tests due to a 1.24 vs 1.25.6 toolchain mismatch).
- At boot under v2 the config is read twice (once by seiconfig.ReadConfigFromDir for validation, once by the re-entered legacy handler). This is negligible — one-time at startup and only when the experimental gate is enabled — but worth noting.
- resolveHomeDir duplicates the legacy handler's home-resolution logic rather than sharing it; the differential env-home test pins them together, but if the legacy resolution ever changes, the two could silently diverge (only the validation target would be wrong, not boot correctness, since validation is advisory). A shared helper would be more robust if/when the sei-cosmos fork can be touched.
- Diagnostics are printed to stderr via fmt.Fprintf rather than the node's structured logger; acceptable for an advisory MVP but a follow-up could route them through the logger for consistent formatting/filtering.
There was a problem hiding this comment.
Prior substantive concerns (warning-drop, package doc-drift) are all fixed; only a nit test-comment doc-drift remains and is already posted inline.
Extended reasoning...
Overview
This PR fills in the v2 SeiConfigManager body behind SEI_CONFIG_MANAGER=v2 (PLT-775 PR2). Scope is confined to cmd/seid/cmd/configmanager/ + one differential test + go.mod/go.sum (adds sei-config v0.0.21, bumps BurntSushi/toml and go-viper/mapstructure). The v2 body is validate-passthrough: resolveHomeDir → seiconfig.ReadConfigFromDir → Validate → advisory-log diagnostics → re-enter the unchanged InterceptConfigsPreRunHandler on the operator's original files.
Security risks
Low. Both channels the app consumes (serverCtx.Config and Viper) are produced by the same legacy reader as before; v2 writes nothing to disk and never refuses boot. resolveHomeDir mirrors the legacy handler's home resolution exactly (BindPFlags + SetEnvPrefix(path.Base(exe)) + AutomaticEnv), so validation and re-entry read the same directory. The os.ErrNotExist gate correctly avoids noise on fresh homes. No auth/crypto/permission surface is touched.
Level of scrutiny
Moderate — the code sits on the boot config resolution path, but the passthrough-by-construction design confines behavioral divergence from legacy to (a) an advisory stderr line and (b) the sei-config Validate side-effect (pure over an in-memory struct). The differential tests (TestConfigManagerLegacyVsV2Differential + the env-home twin added in commit 9756ba4) pin serverCtx.Config + Viper.AllSettings() parity across both home-resolution paths, including after the start.go chain-id mutation, which is exactly the property that matters for correctness. The v2 gate is opt-in (SEI_CONFIG_MANAGER=v2), so blast radius on default operators is nil.
Other factors
My prior review's substantive findings (SeverityWarning diagnostics being silently dropped by HasErrors()/Errors(), and stale package-level doc) have been directly addressed — the current code now gates on len(diags) > 0 and prints the full Diagnostics slice, and the top-of-file doc no longer says "(forthcoming)"/"re-authors". The only remaining finding is a nit that a test comment claims the env-precedence case "rides the generate-path PR" when commit 9756ba4 (DWC-1, this PR) added TestConfigManagerLegacyVsV2Differential_EnvHome covering exactly that case; posted inline. The review-gate summary shows all cross-reviewer RATIFY with no blocking findings, and codecov/cursor/seidroid are green.
…_Flag The DWC-1 commit (9756ba4) added TestConfigManagerLegacyVsV2Differential_EnvHome covering the env-driven home resolution, but left the TestResolveHomeDir_Flag comment claiming the env case 'rides the generate-path PR' — now self- contradictory. Point it at the env-home differential test instead (claude[bot]). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
PR2 fills in the SeiConfigManager v2 body as validate-passthrough: it reads/validates config advisorily via sei-config, then re-enters the unchanged legacy reader on the operator's original files, so the two consumed channels are identical to legacy by construction. The change is small, safe, and well-tested; no blocking issues found.
Findings: 0 blocking | 3 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Cursor's second-opinion pass (cursor-review.md) was empty — no findings from that tool. Codex reported no material findings, matching this review. REVIEW_GUIDELINES.md was also empty, so no repo-specific standards were applied.
- Parity between v2 and legacy is guaranteed by re-entering the same legacy handler, but resolveHomeDir duplicates the legacy handler's home-resolution logic rather than sharing it. If the vendored sei-cosmos handler ever changes its env-prefix/flag-binding scheme, this copy could silently drift; TestConfigManagerLegacyVsV2Differential_EnvHome guards against that, which is good — worth keeping that test in mind as the load-bearing pin.
- Advisory validation writes diagnostics to stderr via fmt.Fprintf rather than the node's logger. This is intentional for the MVP (documented), but consider routing through the structured logger when validation is promoted to fatal so operators get consistent, filterable output.
… form The in-file documentation was overbearing. Move the package narrative into a dedicated doc.go (single home for the why), cut each exported symbol's go-doc to its point, and drop the inline block comments in Apply so the code reads on its own. doc.go leads with the safety invariant (never rewrites, never refuses boot) per the /lingua R2 pass (prose-steward). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
PR2 fills in SeiConfigManager v2 as a validate-passthrough: it resolves home the same way the legacy handler does, reads/validates the config advisorily, then re-enters the unchanged legacy reader on the operator's original files. The change is small, self-contained, and its home-resolution faithfully mirrors the legacy handler; no blocking issues found, with only minor non-blocking notes.
Findings: 0 blocking | 4 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Test coverage gap (as Codex noted): the differential tests prove legacy/v2 channel parity but never assert that the advisory validation path actually emits diagnostics to stderr on a bad/invalid config. Consider a test that feeds a config triggering
seiconfig.Validatediagnostics and asserts the advisory message is written (and boot still proceeds). - The v2 advisory path silently swallows a resolveHomeDir/read mismatch by design: if resolveHomeDir ever diverged from the legacy handler's resolution, v2 would validate one directory while legacy boots another, with no error surfaced. TestConfigManagerLegacyVsV2Differential_EnvHome pins this today, but it's worth keeping in mind that the safety net is entirely test-based since the seam duplicates (rather than reuses) the legacy resolution logic.
- Dependency bumps (BurntSushi/toml 1.4.1-pre→1.5.0, go-viper/mapstructure 2.4.0→2.5.0, new sei-config v0.0.21) are pulled in transitively for sei-config; these libraries are used broadly (viper/toml config decoding across the tree), so ensure the full test suite passes on Go 1.25.6 since Codex could not run targeted tests locally.
- Second-opinion reviews produced no findings: cursor-review.md is empty and codex-review.md reports no material correctness/security findings (only the test-coverage residual-risk note above).
…e, fuzz Widen the legacy-vs-v2 proof beyond the single default fixture, all unit-tier: - Corpus differential (table-driven): default, leading comments/blanks, unknown section, quoted scalar (#36 lenient-decode) — parity holds for any on-disk shape, including ones that exercise sei-config's own reader. - Advisory-invariance: v2 never refuses boot on a valid config, and on an unreadable config it returns the SAME error legacy does (not masked, not invented) after logging the advisory read failure. - FuzzConfigManagerLegacyVsV2Parity: arbitrary app.toml suffix -> legacy and v2 reach the same outcome (identical channels, or the identical error). Runs the seed corpus deterministically in CI; open to -fuzz locally. Harness: runManager now captures stderr + the Apply error so the advisory paths are observable; execConfigManager keeps the happy-path assertion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Fills in the v2 SeiConfigManager as a validate-passthrough: it advisorily validates config via the sei-config library, then re-enters the unchanged legacy reader on the operator's original files, so consumed channels match legacy by construction. The change is tightly scoped, correct, and unusually well-tested; only minor non-blocking notes remain.
Findings: 0 blocking | 5 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Dependency bumps have blast radius beyond configmanager:
github.com/BurntSushi/tomlv1.4.1-dev → v1.5.0 andgo-viper/mapstructure/v2v2.4.0 → v2.5.0 are direct/indirect deps used across the tree (TOML parsing of app.toml/config.toml everywhere, not just here). Worth a quick confirmation that existing TOML/mapstructure decoding behavior is unchanged, since this PR's differential tests only cover the configmanager path. - resolveHomeDir divergence would be silent: if v2's home resolution ever diverged from the legacy handler's, v2 would advisorily validate one directory while the re-entered legacy reader boots on another. The consumed-channel parity assertions can't catch this (re-entry does its own resolution), and by design the mismatch produces no error/diagnostic. TestResolveHomeDir_Flag and the EnvHome differential mitigate this today, but the seam relies on the legacy env-resolution logic never changing — consider a follow-up that asserts resolveHomeDir's result equals the legacy handler's resolved home directly.
- The
errors.Is(err, os.ErrNotExist)suppression in Apply assumes sei-config's ReadConfigFromDir wraps os.ErrNotExist for a missing file; if it returns a differently-typed 'not found' error, operators would see a spurious 'could not read config for validation' advisory line. Non-blocking since it's advisory-only and never affects boot. - Second-opinion passes produced no material output: Codex reported no findings (could not run tests — workspace has Go 1.24.13 and Go 1.25.6 download was network-blocked), and the Cursor review file was empty. Combined findings therefore rest on this pass.
- No detected prompt-injection attempts in the diff or PR description.
Adopt the systems-engineer's extensible pattern for the workstream: - Apply is now two named steps (validateAdvisory -> re-enter legacy), so the generate path can add its authoring/render step as a sibling instead of rewriting Apply. Advisory pass is a void helper; boot outcome is unchanged. - Shared configCorpus() is the single source of interesting on-disk shapes, consumed by BOTH the table-driven differential and the fuzz target (fuzz now crosses every shape with an arbitrary suffix). Kills the corpus/seed drift. - Add the cosmos-only-write-mode case: the deprecated state-commit.sc-write-mode version-skew class. sei-config still accepts it, so v2 raises no diagnostic today; the case proves read-parity now and becomes a caught case once fatal validation + a sei-config deprecation rule land. - doc.go states the reduction lemma: the node boots on the two channels, not the model, so channel-equality is the whole correctness argument. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
A well-scoped, well-documented change that fills in the v2 SeiConfigManager as a pure advisory validate-passthrough: it validates via sei-config, logs diagnostics, and re-enters the unchanged legacy reader on the operator's original files, so boot parity with legacy holds by construction and is proven by strong differential + fuzz tests. Only a minor test-only integer-overflow issue in the fuzz target is worth noting; no production blockers.
Findings: 0 blocking | 3 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
cursor-review.mdandREVIEW_GUIDELINES.mdwere both empty — the Cursor pass produced no output and no repo-specific review guidelines were available, so those inputs did not contribute to this review.resolveHomeDirduplicates the legacy handler's home-resolution logic (SetEnvPrefix/AutomaticEnv/replacer) by hand rather than sharing it; this is a deliberate mirror and is guarded byTestConfigManagerLegacyVsV2Differential_EnvHome, but it remains a drift risk if the legacy resolution changes — worth a code comment pointer or a shared helper in a future PR.- 1 suggestion(s)/nit(s) flagged inline on specific lines.
idiomatic-reviewer nits on the fold-in: - Fuzz index is now uint, dropping the sign guard and the math.MinInt negation edge (negating MinInt stays negative -> corpus[neg] panic; a fuzzer would hit it). Validated: 15s / 4354 execs, no panic, no divergence. - replaceInFile params renamed oldStr/newStr to stop shadowing builtin new. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
PR2 fills in the v2 SeiConfigManager as a strictly advisory validate-then-passthrough: it reads/validates the config via sei-config, logs any diagnostics, and re-enters the unchanged legacy handler on the operator's original files. The design is sound, the home-resolution mirror is faithful to the legacy handler, and the differential/fuzz test coverage is unusually thorough. A couple of non-blocking notes on the repo-wide dependency bumps and the empty second-opinion inputs.
Findings: 0 blocking | 4 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- go.mod bumps
github.com/BurntSushi/toml1.4.1-pre → 1.5.0 andgithub.com/go-viper/mapstructure/v22.4.0 → 2.5.0. These are repo-wide TOML/decoding dependencies, so the change is not truly confined toconfigmanager/as the PR description states — it affects the legacy config path and any other TOML/mapstructure consumers. The differential test (which reads a legacy-created config) gives partial assurance of parity, but consider confirming no behavior change in the broader config-parsing surface before merge. - Both second-opinion inputs were effectively empty:
cursor-review.mdwas blank andcodex-review.mdreported no findings but noted it could not run the Go test suite (local toolchain Go 1.24.13 vs required 1.25.6, and sandboxed network blocked the toolchain download). So neither external pass verified the tests actually pass — worth confirming CI is green on Go 1.25.6. REVIEW_GUIDELINES.mdin the working tree is empty, so no repo-specific review standards were applied.- No prompt-injection or malicious content was detected in the PR diff, title, or description.
Comments that couldn't be anchored to the diff
cmd/seid/cmd/configmanager/configmanager.go:132-- [nit] Minor robustness/observability gap: when home resolves to an empty string (neither --home nor the env var set), this passes "" toReadConfigFromDir, which will read relative to the process CWD rather than the node home the legacy handler would use. It's harmless here because the whole pass is advisory and swallowed, but it means the advisory validation could silently validate the wrong directory without any diagnostic. Consider logging when home is empty, since the whole point of the env-home differential test is to guard against exactly this kind of home-resolution drift going unsurfaced.
….Fprintf Advisory validation diagnostics now go through seilog (the seid-wide logger, already used in root.go) at Warn, instead of raw fmt.Fprintf to cmd stderr. seilog is a package-global slog logger available in PersistentPreRunE — it does not depend on the node/server-context logger (wired later inside the re-entered handler) — so logging is now structured, level-controlled, and consistent with the rest of seid. Tests: the advisory paths' behavior (I2 — v2 returns the same error as legacy, boots identically) is unchanged and still asserted. seilog fixes its output sink at package init (no per-test hook), so the log-TEXT assertion is dropped — the invariant, not the log text, is what the test protects. runManager simplified to (ctx, err). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
idiomatic-reviewer follow-ups on the seilog switch:
- Log diagnostics as a []string of Diagnostic.String() instead of
fmt.Sprintf("%v", diags): structured/queryable under the JSON handler and
still readable under text, where a bare []Diagnostic would struct-dump.
- Use the "error" attribute key to match the sibling logger calls (ethreplay.go).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
PR2 fills in the v2 SeiConfigManager as a validate-passthrough: it advisorily validates config via sei-config, then re-enters the unchanged legacy reader on the operator's original files. The change is tightly scoped, the home-resolution mirror matches the legacy handler exactly, and parity is proven by differential + fuzz tests. No blockers; a couple of non-blocking robustness/dependency notes.
Findings: 0 blocking | 4 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Dependency bumps ride in transitively via sei-config: BurntSushi/toml 1.4.1-pre → 1.5.0 and go-viper/mapstructure/v2 2.4.0 → 2.5.0. These libraries are used repo-wide for TOML/mapstructure decoding, not just by the new package. The differential/fuzz tests only prove legacy-vs-v2 parity within configmanager; they don't cover behavior changes these bumps could introduce for other config/serialization consumers. Worth a sanity check that the broader test suite is green on 1.25.6 (Codex could not run tests due to a Go 1.24 vs 1.25.6 toolchain mismatch, so no independent test signal here).
- Minor: on the v2 path the on-disk config is read twice at boot (once by sei-config for validation, once by the re-entered legacy reader). This is a one-time startup cost and acceptable, but worth noting as the generate path evolves.
- Process notes: Cursor produced no output (cursor-review.md is empty) and REVIEW_GUIDELINES.md is empty/missing, so no repo-specific standards were applied. Codex reported no material findings but explicitly could not execute Go tests.
- 1 suggestion(s)/nit(s) flagged inline on specific lines.
The advisory pass's contract is 'never refuses boot,' but a panic in seiconfig.ReadConfigFromDir/Validate (read fidelity still being hardened) would propagate out of Apply and crash boot — violating advisory-invariance (I2). Guard validateAdvisory with a recover that logs the panic and lets boot proceed via the re-entered legacy reader, so the pass truly cannot change the boot outcome (seidroid). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
A well-scoped, thoroughly-tested PR that fills in the v2 SeiConfigManager as an advisory validate-then-passthrough over the unchanged legacy config handler; v2's consumed channels are identical to legacy by construction and the differential/fuzz suite proves it. No blockers found; only informational notes about the second-opinion passes.
Findings: 0 blocking | 4 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Cursor's review file (cursor-review.md) is empty — that second-opinion pass produced no output.
- Codex reported no material findings but noted its focused
go testcould not run because the sandbox blocked module downloads, so its pass was effectively static-analysis-only. - REVIEW_GUIDELINES.md (from the base branch) is empty, so no repo-specific review standards could be applied.
- validateAdvisory logs every sei-config diagnostic at Warn regardless of the diagnostic's own severity. Fine for the advisory MVP, but when validation later becomes fatal it would be worth mapping diagnostic severity to log level (and to boot-refusal) rather than treating all diagnostics uniformly.
There was a problem hiding this comment.
Solid, well-scoped PR: v2 is a read-and-validate passthrough that re-enters the unchanged legacy handler, so boot-channel parity is true by construction and is backed by a strong differential/fuzz suite. No blocking issues found; a handful of robustness and observability suggestions in the advisory path, plus a latent false-positive in one fuzz target.
Findings: 0 blocking | 10 non-blocking | 5 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Second-opinion passes: Codex reported no material issues (tests could not run — Go 1.25.6 toolchain download network-blocked). The Cursor review file (
cursor-review.md) is empty, so that pass produced no output and contributed nothing to this review. - Dependency bumps ride along with sei-config v0.0.21:
go-viper/mapstructure/v2v2.4.0 → v2.5.0 andBurntSushi/tomlv1.4.1-0.20240526… → v1.5.0. mapstructure is on the real config decode path via viper, so it can affect the legacy path too — and the legacy-vs-v2 differential can't see that, since both sides run the new version. In-tree BurntSushi/toml usage is only sei-tendermint e2e tooling, so that one is low risk. Worth confirming thetestutil/configtestcharacterization suite is green on this branch, since that's the instrument that would catch legacy-path drift. cmd/seid/cmd/root.go:103-106still carries the PR1 comment "When PR2 adds theseid config ...subtree (which skips PersistentPreRunE), it must own single-read discipline". This is PR2 and it doesn't add that subtree — the comment should be updated or re-pointed at the follow-up PR so the single-read obligation isn't mistakenly assumed to have been discharged. (Not in the diff, hence not inline.)- Ordering/scope of advisory logs:
logAdvisoryruns beforeInterceptConfigsPreRunHandler, which is whereseilog.SetDefaultLevelapplies the operator's configured level — so advisory warnings bypass the level the operator set in config.toml. And becauseApplyis wired intoPersistentPreRunEfor every non-initsubcommand,SEI_CONFIG_MANAGER=v2makesseid keys list,seid tx …, etc. each do a config read + validation and potentially print diagnostics. Both are acceptable behind the opt-in gate, but worth an explicit note before v2 becomes the default. - Diagnostic strings come from sei-config's
Diagnostic.String()and are emitted verbatim into node logs. If any rule ever renders the offending value (rather than just the field path) for a sensitive field, that value lands in logs at Warn. Worth confirming sei-config redacts values, or rendering only field paths on this side. - 5 suggestion(s)/nit(s) flagged inline on specific lines.
From an independent cross-review. The security lens found that the refactor which made the advisory pass observable moved the logging out from under the recover that used to cover it: every logger call used to sit inside validateAdvisory, and Apply now called logAdvisory after it returned. A panic in the reporting path therefore propagated out of Apply into PersistentPreRunE and refused the boot, which is the one thing this manager promises it cannot do. It was a regression introduced while closing an earlier review comment, and no test could have caught it, since the existing one only asserts the reporter does not panic on the shapes it is handed. reportAdvisory now contains the pass and the reporting under one recover, with a deliberately minimal handler that does not touch the value that may have caused the panic. The pass's stage discriminator is a typed constant instead of a string, so a mistyped stage is a compile error rather than a switch case that silently drops an operator warning. The fixture home in the package test now uses configtest.NewHome rather than hand-rolling the same directory, which is what the suite's own rails ask for. The differential's header no longer describes itself as a parity proof. While v2 is a passthrough both sides of every comparison run the same legacy reader, so those rows detect a side effect and cannot fail on their own; the rows that can are the ones asserting something other than equality between two runs of one reader. Saying so is more useful than the claim, and the claim becomes true when the resolver lands.
… skip flag keys Three review follow-ups, one of them a defect in a test added earlier in this PR. The stage switch had a case per stage, so a stage added later without a matching case would have logged nothing and turned a failure into silence. It now reports any stage that is not the completed one, and stage carries a String so the attribute is legible. The resolved home is now in the diagnostics line. The reason the lockstep test exists is that a drifted resolveHomeDir would have these diagnostics describe a directory the node is not booting on, and without the path in the line there was no way to tell that from a log. FuzzConfigManagerEnvOnlyKeyParity asserted that its probe value arrived through the environment, but nothing stopped the fuzzer choosing a key the harness also sets as a flag. startCmdForHome marks --home Changed, and a Changed pflag outranks AutomaticEnv, so that key would have read back the fixture root and failed as a not-live probe with no divergence present. The precedence is already asserted by TestResolveHomeDirEnvAndPrecedence in this package, which is what makes the collision real rather than theoretical. Reachable by -fuzz rather than by CI, since only the seed corpus runs under plain go test.
There was a problem hiding this comment.
PR2 fills in the SEI_CONFIG_MANAGER=v2 body as a validate-then-passthrough manager: it reads/validates via sei-config, logs advisory diagnostics, and re-enters the unchanged legacy handler, with a large differential/fuzz test suite backing the "identical channels, never refuses boot, writes nothing" claims. I found no blocking correctness or security issues — only observability, test-seam, and dependency-hygiene notes.
Findings: 0 blocking | 11 non-blocking | 4 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Second-opinion passes: Codex reported no material issues; the Cursor review file (
cursor-review.md) is empty, so that pass produced no output and contributed nothing to this synthesis. - No test exercises
reportAdvisory's own recover, nor a panic originating insidevalidateAdvisory— sei-config can't be made to panic on demand, so the invariant the comments defend most strongly ("cannot refuse a boot the legacy path would have allowed") is argued rather than asserted. A small injection seam (package-levelreadConfig/validatefunc vars, or alogAdvisoryindirection) would make both recovers testable. cmd/seid/cmd/root.go:104now carries a stale comment: "When PR2 adds theseid config ...subtree (which skips PersistentPreRunE)...". This is PR2 and it does not add that subtree — worth retargeting to PR3 so the single-read-discipline note stays actionable.- Under
v2the advisory pass runs on every non-initcommand that goes throughPersistentPreRunE(tx,query,keys, ...), not juststart, so every CLI invocation pays a second config read and may repeat the same diagnostics. Acceptable for an opt-in gate, but consider scoping it to node-starting commands before the gate widens. - Advisory diagnostics are emitted before
InterceptConfigsPreRunHandlerresolves and applies the config-driven seilog level, so they are governed by the pre-config default rather than the operator's configured level. That follows necessarily from the deliberate ordering, but it's worth stating indoc.goalongside the "fresh node isn't validated until its second boot" note already there. TestValidateAdvisoryReportsWhatItFoundasserts on the literal diagnostic substringchain.min_gas_prices, so a sei-config bump that renames the field path breaks it. The comment justifies naming a field over counting, which I agree with — just flagging it as expected maintenance at each version bump.- Diagnostic strings are logged verbatim. If sei-config ever renders offending values into a diagnostic, config material (e.g. endpoints, credentials embedded in URLs) would land in node logs; worth a look before validation goes fatal and this path gets more traffic.
- 4 suggestion(s)/nit(s) flagged inline on specific lines.
There was a problem hiding this comment.
No new bugs found on this pass. Every issue raised earlier in this thread — the env-only fuzz probe colliding with --home, the advisory reporter's panic escaping its own recover, the stage switch silently dropping unknown stages, and Home never being logged — has since been fixed by the author (22620e2 / 4bd8330) and matches what I see in the current diff.
Given the scope here (a new v2 boot path, hand-rolled home-dir resolution that has to stay in lockstep with the legacy handler, and a transitive mapstructure bump that touches the legacy decode path too), I'd still like a human to take a pass before this merges.
Extended reasoning...
Overview
This PR fills in the v2 SeiConfigManager body behind SEI_CONFIG_MANAGER=v2: it resolves the node home directory (mirroring the legacy flag>env>default precedence), reads and validates the config through the sei-config library, logs findings advisory-only, and then re-enters the unchanged legacy reader so the two boot channels (serverCtx.Config, serverCtx.Viper) are produced identically to legacy. It also bumps sei-config to v0.0.21 and picks up transitive bumps to mapstructure and BurntSushi/toml, and adds substantial differential/fuzz test coverage comparing legacy vs v2 output.
Security risks
No auth/crypto/permission surface here. The main risk class is silent divergence between the home directory v2 validates and the one the node actually boots on (resolveHomeDir drifting from the legacy handler), and the mapstructure/BurntSushi bump altering the legacy decode path even though the PR claims 'legacy untouched'. Both were raised and addressed during review: a direct lockstep test was added for the former, and the author ran the full characterization/golden suite under -race to confirm the latter is behavior-preserving.
Level of scrutiny
This warrants a human look rather than a shadow-approve. It's not mechanical — it introduces new home-resolution logic with real precedence subtleties (viper AutomaticEnv vs Changed pflags), a panic-recovery boundary around an advisory pass that must never fail boot, and a dependency bump with an indirect effect on the legacy path. The PR is gated behind an opt-in env var and defaults to unchanged legacy behavior, which lowers blast radius, but the logic itself is intricate enough that I'd rather a person sign off.
Other factors
The review history on this PR is unusually thorough: two rounds of bot review (seidroid and this reviewer) surfaced real defects — a home/env fuzz-key collision, a panic escaping the advisory recover, a silently-dropped unknown stage, and an unlogged home field — and the author fixed all of them with test coverage added for each, walking through the reasoning in detail each time. Nothing outstanding is unaddressed in the visible thread. That track record, plus the size and subtlety of the change, is why I'm deferring rather than approving outright.
…env name Two of these close holes in fixes from the previous round. The unresolved-home skip was silent. Skipped was set and logAdvisory had no case for it, so an operator who opted into v2 with no home to resolve could not tell a declined pass from a pass that ran and found nothing. It now reports at Info. Home is what separates the two skips, since it is empty only when the home never resolved, so the ordinary fresh-node case of a missing config file stays quiet. The catch-all stage arm still carried the read-specific message, so a stage added between resolve and read would have been reported as a read failure. The phrasing is neutral now, for the same reason the arm is a catch-all. The env-only fuzz guard compared the key rather than the variable it derives. ServerEnvKey upper-cases and rewrites separators, so "HOME" and "Home" derive the same variable as --home while passing an equality test against the flag name, and that flag is Changed, which outranks AutomaticEnv. Comparing the derived name closes every spelling rather than the one. Verified: all three spellings derive one variable.
There was a problem hiding this comment.
The v2 body is a genuine passthrough — resolveHomeDir mirrors InterceptConfigsPreRunHandler's flag/env resolution exactly, the advisory pass is fully recovered and never returns an error, and the test suite is unusually careful about non-vacuity. No blockers; the notes below are hardening, doc-accuracy, and the out-of-gate dependency bump.
Findings: 0 blocking | 10 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Cursor's second-opinion pass produced no output (
cursor-review.mdis empty), so this review merges only Claude + Codex findings. - The advisory logs are emitted before
InterceptConfigsPreRunHandlerruns, and that handler is what callsseilog.SetDefaultLevelfrom the config/flag. So v2's diagnostics render at seilog's process default level and format rather than the operator's configured ones — worth a line in the operator-facing notes for the gate. logAdvisory'sstageReadbranch logsstage+errorbut notHome, while the diagnostics branch does log it. Since the same drift argument applies (a read failure against the wrong directory looks identical to one against the right directory), consider adding"home", out.Hometo the stopped-early warn.- The dependency bumps (
go-viper/mapstructure/v22.4.0→2.5.0,BurntSushi/toml→ v1.5.0) changerootViper.UnmarshalinsideinterceptConfigsfor every node, including withSEI_CONFIG_MANAGERunset — i.e. outside the gate the rest of the PR is scoped by. The PR body's argument (unchanged characterization goldens across 23 packages) is the right instrument and I'd accept it, but this is the one part of the change with no feature flag in front of it, so it deserves an explicit call-out in the release notes rather than living only in the PR description. - The
ConfigManagerinterface doc lost the "all-or-nothing with respect to on-disk state / write BOTH files atomically and then re-enter the legacy read tail" clause. It is moot for a passthrough v2, but it is exactly the constraint the deferred generate path has to satisfy; dropping it from the interface means the next author has to rediscover it from the design doc. TestValidateAdvisoryReportsWhatItFoundasserts on sei-config's literal diagnostic text (chain.min_gas_prices). That coupling is deliberate (it is the only non-vacuity guard on the pass), but it will break on sei-config bumps that rename the field path — fine, just expect it.TestConfigManagerV2WritesNothingsnapshots around the wholeApply, so it measures advisory-pass + legacy-re-entry writes together rather than isolating a v2-side write. That is sound today only because the legacy handler is a no-op on a pre-seeded home; if that ever stops holding the test silently weakens.- I could not run
go build/go vet/ the package tests in this environment (sandbox denied the commands), so this review is static. The channel-parity claims rest on readingsei-cosmos/server/util.go, which does confirmresolveHomeDirreproduces the handler'sBindPFlags→SetEnvPrefix(path.Base(exe))→SetEnvKeyReplacer→AutomaticEnv→GetString(FlagHome)sequence exactly. - 2 suggestion(s)/nit(s) flagged inline on specific lines.
There was a problem hiding this comment.
The v2 manager is a well-scoped, genuinely advisory validation pass that re-enters the unchanged legacy reader, and the home-resolution parity, writes-nothing, and never-refuses-boot properties are each pinned by a test that can actually fail. No blockers; the notes are a fuzz-target false-positive guard, log-ordering, and the ungated dependency bumps.
Findings: 0 blocking | 8 non-blocking | 4 posted inline
Blockers
- None at the file/PR level.
Non-blocking
cursor-review.mdis empty, so the Cursor pass produced no output for this PR; Codex reported no material issues but noted it could not run targeted tests (sandbox blocked the Go toolchain). I also could not executego build/go test/gofmtin this environment, so formatting/green-test status is unverified from my side.runManagerinconfigmanager_differential_test.gooverwritesStartCmd's ownPreRunE, which in the real boot doesserverCtx.Viper.BindPFlags(cmd.Flags())plusGetPruningOptionsFromFlags. Parity still holds (both sides skip it symmetrically), but theAllSettings()comparison runs over a viper that has not had StartCmd's flag binding applied, so it covers a slightly smaller surface than the real boot — worth chaining to the originalPreRunErather than replacing it when the real resolver lands and flag-vs-env precedence starts to matter.- The stale comment at
cmd/seid/cmd/root.go:108-110says "When PR2 adds theseid config ...subtree" — this is PR2 and it does not add that subtree. Not in the diff, so not actionable here, but worth refreshing in the next PR. - End-to-end, nothing observable now distinguishes
SEI_CONFIG_MANAGER=v2from legacy throughroot.go(the v2 branch ofFuzzPreRunManagerSelectionasserts only "boots and populates channels", which legacy also satisfies, andTestPreRunReadsTheManagerGatePerInvocationswitched tov3).TestSelect'srequire.IsTypestill covers the dispatch table directly, so the gap is narrow — but a regression that mapped "v2" toLegacyConfigManagerinsideroot.gowould now pass every test. A cheap fix is to have the v2 root-level case assert a v2-only observable (e.g. an advisory log line or an injected manager). - 4 suggestion(s)/nit(s) flagged inline on specific lines.
… the re-entry-ordering rationale
|
Addressed both suggestions in
|
There was a problem hiding this comment.
Solid, well-scoped PR: the v2 manager is a pure advisory validation pass that re-enters the unchanged legacy reader, and the differential/fuzz suite plus the direct resolveHomeDir-vs-handler assertion cover the properties that matter. No blockers found; the notes below are test-coverage gaps and one dependency-bump blast-radius flag.
Findings: 0 blocking | 9 non-blocking | 4 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Second-opinion coverage: Codex reported no material issues, but
cursor-review.mdis empty — that pass produced no output, so this review rests on the Claude and Codex passes only. - I verified
resolveHomeDiragainstsei-cosmos/server/util.go:141-165(bind order,path.Base(os.Executable())prefix,./-→_replacer,AutomaticEnv) and againstinterceptConfigs, which readsFlagHomebeforeReadInConfig— the home-resolution parity claim holds. The duplication of that block inconfigmanager.gorather than a shared helper is a reasonable call given the no-edits-to-sei-cosmos scope, andTestResolveHomeDirAgreesWithTheLegacyHandleris the right guard against drift. - Rollout note (already acknowledged in the
validateAdvisorydoc comment, repeating for visibility): because the pass runs before the legacy handler, a v2 node whose config seid itself generated will log the pruning-gap diagnostic on every start from boot #2 onward. Operators learning to ignore a permanent warning erodes the pass's signal value, so closing that gap should gate any wider v2 enablement, not just the later switch to fatal validation. - Advisory logs are emitted before
InterceptConfigsPreRunHandlerapplies the config-file log level (sei-cosmos/server/util.go:187-217), so these lines are evaluated against seilog's pre-config default rather than the operator's configuredlog_level. Benign today, but worth knowing if a diagnostic ever goes missing for an operator who set a level in config.toml. - I could not run
go vet/go testin this environment (command approval denied), so compilation and test-pass status are unverified here — this review is from source reading. Theconfigtesthelpers the new tests depend on (Isolate,NewHome,ServerEnvPrefix,ServerEnvKey,EnvValueIsSettable,Home.Read/Write*) do all exist on the base branch with matching signatures, andserver.StartCmd's three-arg signature matches the call sites. - 4 suggestion(s)/nit(s) flagged inline on specific lines.
…v var (not just --home)
…licated viper bootstrap
|
Another cycle — all four:
|
There was a problem hiding this comment.
The v2 manager is a genuinely advisory passthrough: Apply runs a recovered validation pass and then re-enters the unchanged legacy handler, and the tests back the claims that matter (writes-nothing, home-resolution agreement with the real handler, error parity). No blocking correctness or security issues found; the notes below are observability, test-fidelity, and dependency-risk suggestions.
Findings: 0 blocking | 8 non-blocking | 3 posted inline
Blockers
- None at the file/PR level.
Non-blocking
resolveHomeDirwas verified line-for-line againstserver.InterceptConfigsPreRunHandler(sei-cosmos/server/util.go:141-165): sameBindPFlags(cmd.Flags())thenBindPFlags(cmd.PersistentFlags())order, samepath.Base(os.Executable())prefix, same replacer, same freshviper.New()base (NewDefaultContextsets no defaults). The duplication is correct today andTestResolveHomeDirAgreesWithTheLegacyHandlerguards it — no change requested, just confirming the TODO is the right disposition.- Idempotency of the differential fixtures checks out:
interceptConfigsonly writesconfig.toml/app.tomlwhen they are absent, so the seed-then-compare pattern inseedDefaultConfig+ the corpus tests is stable across the two runs andTestConfigManagerV2WritesNothingis a meaningful assertion rather than a vacuous one. - Stale comment outside this diff:
cmd/seid/cmd/root.go:104still says "When PR2 adds theseid config ...subtree (which skips PersistentPreRunE), it must own single-read discipline". This is PR2 and it does not add that subtree — worth retargeting the note to the actual follow-up PR so the single-read obligation does not get lost. - The Cursor second-opinion file (
cursor-review.md) is empty — that pass produced no output, so this review reflects only the Codex pass ("no material issues found") merged with my own. - Please confirm CI on this branch actually runs the full
testutil/configtestcharacterization suite (all 23 manifest/golden packages) rather than only the sharded default set — the PR body's evidence for the mapstructure bump being behavior-preserving rests entirely on that suite being green here. - 3 suggestion(s)/nit(s) flagged inline on specific lines.
|
seidroid xreview |
…n (operator can see it ran)
|
Final focused pass:
|
…uard The widened guard walked StartCmd's own flags to skip a fuzzed key that collides with a handler-consumed flag, on the assumption the collision hazard is always a bound flag that outranks AutomaticEnv (true for --home). log_level and log_format are a different hazard: the handler reads them from viper under AutomaticEnv whether or not any flag is bound, and they are registered on the root command, not on the bare StartCmd the harness builds, so a flag walk can never see them. A -fuzz run hitting SEID_LOG_LEVEL=<garbage> would fail InterceptConfigsPreRunHandler's log-level parse and persist a spurious crasher. Name the two AutomaticEnv-read flags explicitly alongside the walk.
There was a problem hiding this comment.
Well-scoped, gated change: v2 adds an advisory sei-config validation pass in front of an unchanged legacy re-entry, with unusually strong test framing (side-effect detectors, non-vacuity guards, a "writes nothing" home snapshot, and a direct home-resolution lockstep assertion against the real handler). No blockers; the notes are about log-level ordering, an untested panic-injection path, and the fact that the dependency bumps escape the feature gate.
Findings: 0 blocking | 8 non-blocking | 3 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Cursor's second-opinion pass produced no output (
cursor-review.mdis empty); Codex reported "No material issues found". So this review is effectively a single independent pass plus Codex's null result. - Test-coverage gap on the headline promise: "a panic inside sei-config cannot refuse boot" is never exercised end-to-end.
TestLogAdvisoryHandlesEveryOutcomefeeds a syntheticPanicoutcome andTestReportAdvisoryNeverEscapesWhenTheLoggerPanicscovers a panicking logger, but nothing forcesseiconfig.ReadConfigFromDir/Validateto actually panic, so therecoverinvalidateAdvisory(configmanager.go:153-157) is untested against a real fault. Consider hoisting those two calls into package-level func vars so a test can inject a panic and assertSeiConfigManager{}.Applystill returns nil. - The code itself documents (configmanager.go:142-151) that a v2 node which never touches its config will log the pruning-gap diagnostic on every start, and
logAdvisoryadditionally emits an Info "config validation passed" line every boot. Acceptable behind an opt-in gate, but worth demoting the clean-pass line to Debug and closing the pruning gap before any wider rollout — as the comment already says. resolveHomeDirduplicates ~15 lines of the legacy handler's viper bootstrap. The TODO andTestResolveHomeDirAgreesWithTheLegacyHandlerare the right mitigation for now; just make sure the extraction to an exportedserver.ResolveHomeDir(cmd)actually happens when the resolver lands, since this becomes load-bearing then.- Per REVIEW_GUIDELINES.md §1/§2 there is nothing to flag here — this PR has no version-gated constants or upgrade-handler references.
- 3 suggestion(s)/nit(s) flagged inline on specific lines.
…e operator's log level The advisory pass emitted its lines before InterceptConfigsPreRunHandler applies the configured log level (seilog.SetDefaultLevel), so a node with log_level = "error" still got the advisory output and a higher default would have dropped the diagnostics silently, which defeats a pass whose only output is operator-facing. Split Apply: validate before re-entry (still reading the operator's authored files), report after (now governed by the applied level), and report even when the handler returns an error. The reporter's recover records the recovered panic value and stack rather than discarding them, matching what the pass already captures for a panic in validateAdvisory.
…al harness runManager's injected PreRunE returns a non-nil sentinel (errStopPreRun) on the happy path, which cobra's ExecuteC treats as a command error. SetErr(io.Discard) only covers the "Error:" line (the err writer); the ~50-line start usage block goes to the out writer and was dumping to the real process stderr on every call, multiplying under -fuzz. Set SilenceUsage and SilenceErrors so the harness swallows both, matching the comment's intent.
There was a problem hiding this comment.
Solid, well-scoped PR: the v2 manager is a pure advisory-validation pass that re-enters the unchanged legacy reader, and the parity/never-refuses-boot invariants are tested on the existing configtest harness. No blocking correctness or security issues found; the main gap is that nothing asserts Apply actually invokes the validation pass, and the dependency bumps reach every node regardless of the feature gate.
Findings: 0 blocking | 7 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
cursor-review.mdis empty (no output from the Cursor pass) andcodex-review.mdreports "No material issues found" — so this review is effectively a single-reviewer pass plus my own analysis. Nothing in the diff, commit messages, or PR body attempts prompt injection.- Dependency bumps are the only part of this PR that reaches nodes with
SEI_CONFIG_MANAGERunset:go-viper/mapstructure/v22.4.0→2.5.0 backsrootViper.Unmarshal(conf)androotViper.Unmarshal(&customConfig)insei-cosmos/server/util.go:290,300, i.e. every node's tendermint + app config decode. I verifiedBurntSushi/toml's only in-tree importers aresei-tendermint/test/e2e/*(not the node config path), and there are noreplacedirectives pinning either module, so the version moves are clean. The description's evidence (23 characterization packages green) is the right instrument — worth confirming those packages actually run in a CI shard on this branch rather than only locally, since that's what makes the evidence durable. - Advisory diagnostics are rendered straight from
seiconfig.Validate(cfg).Diagnosticsinto a log line. Worth confirmingDiagnostic.String()renders field paths (and not raw values) for keys that can hold credentials — e.g. an RPC endpoint with embedded auth inapp.toml— before v2 is rolled out anywhere with shared log sinks. validateAdvisory's own doc (configmanager.go:150-159) states that from the second boot onward, a v2 node that never touches its config logs the pruning-gap diagnostic on every start. That's known and gated, but it means thelogger.Warnpath is the default steady state for v2 today rather than the exception — the pruning gap is effectively a prerequisite for enabling the gate anywhere, which matches what the comment says. No change requested; flagging so it stays tracked.cmd/seid/cmd/root.go:105still carries the PR1 comment "When PR2 adds theseid config ...subtree (which skips PersistentPreRunE), it must own single-read discipline". This is PR2 and it doesn't add that subtree; the comment is now misleading about what has shipped. Not in this diff, so just a cleanup note.- 2 suggestion(s)/nit(s) flagged inline on specific lines.
| // reported even when the handler errors, so a boot that fails still gets the advisory. | ||
| // Nothing in either step refuses a boot the legacy path would have allowed. | ||
| func (SeiConfigManager) Apply(cmd *cobra.Command, customAppConfigTemplate string, customAppConfig any) error { | ||
| out := validateAdvisory(cmd) |
There was a problem hiding this comment.
[suggestion] Nothing asserts this wiring. Deleting line 58 and line 60 — making SeiConfigManager.Apply byte-identical to LegacyConfigManager.Apply — leaves every test in this PR green: the differential/parity rows compare two runs of the same reader and would still match, TestConfigManagerV2AdvisoryNeverRefusesBoot and FuzzPreRunManagerSelection's v2 case only assert a successful boot, and TestValidateAdvisoryReportsWhatItFound / TestReportAdvisoryNeverEscapesWhenTheLoggerPanics call validateAdvisory and reportAdvisory directly rather than through Apply.
Given the PR's framing — "what v2 adds is the validation pass" — this is the one claim about v2 that is currently unpinned, and it's the same class of vacuity the file comments are careful about everywhere else. TestReportAdvisoryNeverEscapesWhenTheLoggerPanics already establishes that logger is swappable, so a test that points Apply at a home missing minimum-gas-prices with a capturing slog.Handler and asserts a record naming chain.min_gas_prices was emitted would close it directly.
| ) | ||
|
|
||
| // EnvVar is the experimental gate that selects the configuration manager. | ||
| var logger = seilog.NewLogger("cmd", "seid", "configmanager") |
There was a problem hiding this comment.
[nit] Package-level mutable logger that a test reassigns (configmanager_test.go, logger = slog.New(panickingHandler{})). Safe today — configtest.Isolate calls t.Setenv, so the testing package rejects t.Parallel() in those tests or any ancestor — but the safety is incidental rather than structural: a future test in this package that doesn't call Isolate and does call t.Parallel() would race with the swap under -race. Threading the logger through reportAdvisory/logAdvisory as a parameter (they're already pure-ish and separately tested) would remove the global mutation entirely.
Overview
PR2 of the ConfigManager work (PLT-775). PR1 (#3671) landed the selection point that chooses between the two configuration managers. This one fills in the v2 manager behind
SEI_CONFIG_MANAGER=v2.v2 does not resolve configuration yet, and that is deliberate. It reads the node's config through the sei-config model and validates it, then re-enters the unchanged legacy reader on the operator's own files. It never writes, migrates, or authors anything at boot. The two places the rest of the boot reads, a Tendermint config struct and the flat key-value map that
app.Newpulls keys back out of, are still produced entirely by legacy code. What v2 adds is the validation pass.Applyresolves the home directory the same way the legacy handler does, flag over environment variable over default, reads the config from that directory, validates it, and re-enters. It pins sei-config v0.0.21, which carries the lenient-decode fix from sei-config#36 so that a real seidconfig.tomlparses.Advisory Validation
Every outcome of the pass is advisory. A resolve failure, a read failure, a validation finding, and even a panic inside sei-config are all captured and logged, and none of them can refuse a boot the legacy path would have allowed. That is the manager's one promise, so the recover covers the reporting as well as the pass, and a panic in the reporter cannot escape into the command's pre-run hook.
The pass reports what it saw rather than logging from inside itself, which is what makes it observable. Without that, a channel-parity or never-refuses-boot assertion holds just as well when the read always fails or when validation has quietly become a no-op, so a test points the pass at a config missing a required field and asserts it names that field.
Two cases stay quiet on purpose. A fresh node has no config yet, so the pass declines and the legacy handler creates the files. An unresolved home reports at info instead, because that is closer to a misconfiguration than to a default, and reading a directory that resolved to nothing would validate whatever node happens to live under the process working directory.
Equality Testing
The point of the manager split is that the legacy reader stays frozen as the contract and v2 is proven to resolve every key the same way. This PR builds that comparison on the characterization harness that landed with #3816, so the equality proof and the recorded contract it compares against rest on the same foundation.
Be precise about what it proves today. While v2 re-enters the legacy reader, both sides of every comparison run the same code, so those rows detect a side effect rather than showing that two independent implementations agree. They are the scaffolding the real comparison will run on, and they become a parity proof when the resolver lands. The rows that can fail on their own today are the ones asserting something other than equality between two runs of one reader: that v2 writes nothing at boot, that the validation pass actually produced a finding, and that v2's own home resolution agrees with the handler it re-enters.
That last one matters more than it looks. v2's channels come entirely from the legacy handler, so a home resolution that drifted would only make the advisory read skip or warn, changing no channel and failing no comparison, while v2 reported diagnostics about a directory the node is not booting on. It is asserted directly against the real handler.
One comparison reaches a class the settings map cannot. That map enumerates only what viper knows structurally, so a value carried solely by an environment variable for a key absent from the file appears in neither the map nor its key list. It still reaches running code, because
app.Newreads throughappOpts.Getand viper resolves environment variables at read time. Since environment resolution is exactly what the resolver will change, that comparison asserts the resolved value per key rather than diffing the map.Scope
No edits to the vendored
sei-cosmosfork. The diff is confined tocmd/seid/cmd/configmanager/, the comparison tests, andgo.mod/go.sum.The dependency bumps reach further than the feature gate does, and that is verified rather than assumed. Adding sei-config pulls
go-viper/mapstructure/v2from 2.4.0 to 2.5.0 and movesBurntSushi/tomloff its pinned pseudo-version to v1.5.0. mapstructure backsrootViper.Unmarshalinside the legacy interception handler, so it changes decoding for every node, including one withSEI_CONFIG_MANAGERunset. Legacy untouched holds for code and not for dependencies, and the legacy-versus-v2 comparison cannot see this class because both sides link the same version.The characterization suite is the instrument that can see it, and it is green on this branch. All 23 packages that carry a config manifest or a recorded-defaults golden pass under
-race. Those goldens pin exact decoded defaults, so an unchanged golden set is direct evidence the bump preserves behavior for everything pinned.BurntSushi/toml's in-tree importers aresei-tendermint/test/e2etooling rather than the node's config path.Deferred Work
Three pieces wait for the generate path rather than this boot path:
SEI_environment-prefix collision audit.Lineage
bdchatham-designs,designs/config-manager/DESIGN.md.🤖 Generated with Claude Code