Skip to content

feat(configmanager): SeiConfigManager v2 body — validate-passthrough (PLT-775 PR2) - #3678

Open
bdchatham wants to merge 35 commits into
mainfrom
feat/configmanager-v2-body-plt775
Open

feat(configmanager): SeiConfigManager v2 body — validate-passthrough (PLT-775 PR2)#3678
bdchatham wants to merge 35 commits into
mainfrom
feat/configmanager-v2-body-plt775

Conversation

@bdchatham

@bdchatham bdchatham commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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.New pulls keys back out of, are still produced entirely by legacy code. What v2 adds is the validation pass.

Apply resolves 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 seid config.toml parses.

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.New reads through appOpts.Get and 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-cosmos fork. The diff is confined to cmd/seid/cmd/configmanager/, the comparison tests, and go.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/v2 from 2.4.0 to 2.5.0 and moves BurntSushi/toml off its pinned pseudo-version to v1.5.0. mapstructure backs rootViper.Unmarshal inside the legacy interception handler, so it changes decoding for every node, including one with SEI_CONFIG_MANAGER unset. 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 are sei-tendermint/test/e2e tooling rather than the node's config path.

Deferred Work

Three pieces wait for the generate path rather than this boot path:

  1. The SEI_ environment-prefix collision audit.
  2. Render fidelity from the model back to the legacy files, which is the quoted-primitive and pruning write-side round trip.
  3. Fatal validation. sei-config validates the files alone while the node resolves from four layers, so it reports a value absent that a cobra flag default supplies. Making validation fatal on that view would refuse boot on a healthy node, so it waits until the pass validates the resolved configuration.

Lineage

  • Issue: PLT-775.
  • Design: bdchatham-designs, designs/config-manager/DESIGN.md.
  • Depends on sei-config#36, merged as v0.0.21.

🤖 Generated with Claude Code

@cursor

cursor Bot commented Jun 30, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches every node’s config load path when v2 is enabled and pulls dependency bumps (mapstructure/toml) that affect legacy decoding for all builds; mitigated by advisory-only validation and extensive differential tests.

Overview
With SEI_CONFIG_MANAGER=v2, SeiConfigManager is no longer a stub: it runs an advisory read/validate via sei-config, then still calls the legacy InterceptConfigsPreRunHandler on the operator’s on-disk files. Boot channels (serverCtx.Config / Viper) stay legacy-owned; validation never writes configs or blocks startup.

Apply order is validate → legacy handler → log advisories (after the handler sets log level). Failures, diagnostics, and panics in the pass are recovered and logged only; corrupt configs still fail with the same error as legacy.

Adds resolveHomeDir (mirrors legacy viper home resolution for the advisory read), capped diagnostic logging, and package doc.go. go.mod adds github.com/sei-protocol/sei-config v0.0.21 and bumps BurntSushi/toml / mapstructure/v2 transitively.

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 PersistentPreRunE fuzz expectations now require v2 to boot successfully.

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

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJul 30, 2026, 10:18 PM

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.75000% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.42%. Comparing base (ff942ff) to head (285be96).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
cmd/seid/cmd/configmanager/configmanager.go 78.75% 12 Missing and 5 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            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     
Flag Coverage Δ
sei-chain-pr 49.52% <78.75%> (?)
sei-db 70.41% <ø> (ø)
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/configmanager/configmanager.go 81.11% <78.75%> (-18.89%) ⬇️

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

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

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 tidy is 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.

Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
@bdchatham

Copy link
Copy Markdown
Contributor Author

Review-gate summary (clean)

Coral /xreview slate — all RATIFY:

  • idiomatic-reviewer: RATIFY (reads native).
  • sei-network-specialist: COMPATIBLE — and verified advisory-not-fatal is the correct MVP choice (a default node fails sei-config validation today via the pruning template-omission gap, so fatal would refuse every node).
  • systems-engineer (assigned dissenter): RATIFY, no must-fix — verified resolveHomeDir fidelity, passthrough inertness, and the os.ErrNotExist chain.

Findings (all [should]/[nit]) resolved: package-comment doc-drift fixed; validation-error advisory made louder; on-disk-fidelity note added; resolveHomeDir test added. Env-precedence + mixed-home differential rows are deferred to the generate-path PR (the boot differential already proves passthrough parity by construction).

Bots: Cursor Bugbot ✅ SUCCESS; seidroid ✅ APPROVED (0 blocking).

Expected, not a regression

On a default node, v2 logs a benign advisory storage.pruning validation error — sei-config reads the on-disk app.toml directly, and the template omits the pruning key that cosmos defaults in code. Validation is advisory (never refuses boot); promoting to fatal is the un-defer once sei-config's read replays template-omitted defaults. Boot is identical to legacy by construction.

Merge prerequisites (separate from the review-gate)

Standard CI (Test / Lint / Race / etc.) + the category label, same as the seam PR.

bdchatham and others added 3 commits July 1, 2026 09:42
…(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>
@bdchatham
bdchatham force-pushed the feat/configmanager-v2-body-plt775 branch from c18b062 to 13b3a44 Compare July 1, 2026 16:42

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

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>

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

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.

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

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: resolveHomeDirseiconfig.ReadConfigFromDirValidate → 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.

Comment thread cmd/seid/cmd/configmanager/configmanager_test.go Outdated
…_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>

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

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>

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

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.Validate diagnostics 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>

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

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/toml v1.4.1-dev → v1.5.0 and go-viper/mapstructure/v2 v2.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>

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

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.md and REVIEW_GUIDELINES.md were 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.
  • resolveHomeDir duplicates the legacy handler's home-resolution logic (SetEnvPrefix/AutomaticEnv/replacer) by hand rather than sharing it; this is a deliberate mirror and is guarded by TestConfigManagerLegacyVsV2Differential_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.

Comment thread cmd/seid/cmd/configmanager_differential_test.go Outdated
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>

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

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/toml 1.4.1-pre → 1.5.0 and github.com/go-viper/mapstructure/v2 2.4.0 → 2.5.0. These are repo-wide TOML/decoding dependencies, so the change is not truly confined to configmanager/ 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.md was blank and codex-review.md reported 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.md in 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 "" to ReadConfigFromDir, 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.

bdchatham and others added 2 commits July 1, 2026 14:50
….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>

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

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.

Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
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>

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

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 test could 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.

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

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/v2 v2.4.0 → v2.5.0 and BurntSushi/toml v1.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 the testutil/configtest characterization suite is green on this branch, since that's the instrument that would catch legacy-path drift.
  • cmd/seid/cmd/root.go:103-106 still carries the PR1 comment "When PR2 adds the seid 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: logAdvisory runs before InterceptConfigsPreRunHandler, which is where seilog.SetDefaultLevel applies the operator's configured level — so advisory warnings bypass the level the operator set in config.toml. And because Apply is wired into PersistentPreRunE for every non-init subcommand, SEI_CONFIG_MANAGER=v2 makes seid 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.

Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go
Comment thread cmd/seid/cmd/configmanager_differential_test.go
Comment thread cmd/seid/cmd/configmanager/configmanager.go
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.
Comment thread cmd/seid/cmd/configmanager/configmanager.go
Comment thread cmd/seid/cmd/configmanager_differential_test.go
… 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.

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

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 inside validateAdvisory — 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-level readConfig/validate func vars, or a logAdvisory indirection) would make both recovers testable.
  • cmd/seid/cmd/root.go:104 now carries a stale comment: "When PR2 adds the seid 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 v2 the advisory pass runs on every non-init command that goes through PersistentPreRunE (tx, query, keys, ...), not just start, 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 InterceptConfigsPreRunHandler resolves 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 in doc.go alongside the "fresh node isn't validated until its second boot" note already there.
  • TestValidateAdvisoryReportsWhatItFound asserts on the literal diagnostic substring chain.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.

Comment thread cmd/seid/cmd/configmanager/configmanager.go
Comment thread cmd/seid/cmd/configmanager/configmanager.go
Comment thread go.mod
Comment thread cmd/seid/cmd/configmanager_differential_test.go Outdated

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

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.
Comment thread cmd/seid/cmd/configmanager_differential_test.go Outdated

@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 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.md is empty), so this review merges only Claude + Codex findings.
  • The advisory logs are emitted before InterceptConfigsPreRunHandler runs, and that handler is what calls seilog.SetDefaultLevel from 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's stageRead branch logs stage + error but not Home, 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.Home to the stopped-early warn.
  • The dependency bumps (go-viper/mapstructure/v2 2.4.0→2.5.0, BurntSushi/toml → v1.5.0) change rootViper.Unmarshal inside interceptConfigs for every node, including with SEI_CONFIG_MANAGER unset — 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 ConfigManager interface 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.
  • TestValidateAdvisoryReportsWhatItFound asserts 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.
  • TestConfigManagerV2WritesNothing snapshots around the whole Apply, 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 reading sei-cosmos/server/util.go, which does confirm resolveHomeDir reproduces the handler's BindPFlagsSetEnvPrefix(path.Base(exe))SetEnvKeyReplacerAutomaticEnvGetString(FlagHome) sequence exactly.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated

@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 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.md is 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 execute go build/go test/gofmt in this environment, so formatting/green-test status is unverified from my side.
  • runManager in configmanager_differential_test.go overwrites StartCmd's own PreRunE, which in the real boot does serverCtx.Viper.BindPFlags(cmd.Flags()) plus GetPruningOptionsFromFlags. Parity still holds (both sides skip it symmetrically), but the AllSettings() 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 original PreRunE rather 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-110 says "When PR2 adds the seid 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=v2 from legacy through root.go (the v2 branch of FuzzPreRunManagerSelection asserts only "boots and populates channels", which legacy also satisfies, and TestPreRunReadsTheManagerGatePerInvocation switched to v3). TestSelect's require.IsType still covers the dispatch table directly, so the gap is narrow — but a regression that mapped "v2" to LegacyConfigManager inside root.go would 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.

Comment thread cmd/seid/cmd/configmanager_differential_test.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go
Comment thread go.mod
@bdchatham

Copy link
Copy Markdown
Contributor Author

Addressed both suggestions in 16ee3e9:

  1. reportAdvisory recover handler — guarded the handler's logger.Error with an inner defer func() { _ = recover() }(). Since the logger is the most plausible cause of a panic reaching this recover (validateAdvisory has its own recover and returns normally), this makes the never-refuse-boot guarantee unconditional: even a logger broken independent of its arguments can't produce a second panic that escapes Apply into PersistentPreRunE.
  2. validateAdvisory re-entry-ordering comment — corrected the rationale. The ordering defers the pruning-gap warning by one boot rather than avoiding it (from boot Create .gitpod.yml #2 on, the pre-handler pass reads the same seid-generated config and logs it until the gap closes), and I noted that closing the pruning gap is a prerequisite for wider v2 rollout, not just for making validation fatal. The ordering itself is unchanged.

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

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.md is empty — that pass produced no output, so this review rests on the Claude and Codex passes only.
  • I verified resolveHomeDir against sei-cosmos/server/util.go:141-165 (bind order, path.Base(os.Executable()) prefix, ./-_ replacer, AutomaticEnv) and against interceptConfigs, which reads FlagHome before ReadInConfig — the home-resolution parity claim holds. The duplication of that block in configmanager.go rather than a shared helper is a reasonable call given the no-edits-to-sei-cosmos scope, and TestResolveHomeDirAgreesWithTheLegacyHandler is the right guard against drift.
  • Rollout note (already acknowledged in the validateAdvisory doc 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 InterceptConfigsPreRunHandler applies 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 configured log_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 test in this environment (command approval denied), so compilation and test-pass status are unverified here — this review is from source reading. The configtest helpers the new tests depend on (Isolate, NewHome, ServerEnvPrefix, ServerEnvKey, EnvValueIsSettable, Home.Read/Write*) do all exist on the base branch with matching signatures, and server.StartCmd's three-arg signature matches the call sites.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go
Comment thread cmd/seid/cmd/configmanager/configmanager.go
Comment thread go.mod
Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager_differential_test.go Outdated
@bdchatham

Copy link
Copy Markdown
Contributor Author

Another cycle — all four:

  1. differential_test.go fuzz guard (467): widened as suggested (3259f24). The guard now skips a fuzzed key whose derived env var collides with any bound StartCmd flag (walking Flags()/PersistentFlags()), not just --home. That covers SEID_LOG_LEVEL — the sharp one, where the legacy handler parses the value and errors on an unparseable level, tripping execConfigManager's require.NoError on a -fuzz run — and any future consumed flag. Since require.Equal(value, legacyGot) also fails on a not-live probe, generalizing beyond --home is the robust fix.
  2. viper duplication (245): added a TODO (230292d) to extract an exported server.ResolveHomeDir(cmd) and call it from both sides once the resolver lands, noting TestResolveHomeDirAgreesWithTheLegacyHandler guards the drift until then.
  3. advisory log level (51): keeping the current early emission by design — the advisory reads and reports before re-entry, so a config warning is visible during the pre-boot phase regardless of the operator's not-yet-applied level, which for an advisory is the safer default (you don't want a config problem silenced by a high configured level). The split you describe (capture before, log after re-entry with the recover moved with it) is a clean alternative if the team would rather the diagnostics follow the configured level; flagging it as a deliberate call, not an oversight.
  4. ungated dep bump (go.mod:191): agreed, no code change — mapstructure 2.4.0→2.5.0 backs rootViper.Unmarshal and so affects every node, and the differential structurally can't see it (both sides link the same version). The PR description already flags it with the 23-package -race characterization; surfacing it in the release notes as an all-nodes change is the right home.

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

  • resolveHomeDir was verified line-for-line against server.InterceptConfigsPreRunHandler (sei-cosmos/server/util.go:141-165): same BindPFlags(cmd.Flags()) then BindPFlags(cmd.PersistentFlags()) order, same path.Base(os.Executable()) prefix, same replacer, same fresh viper.New() base (NewDefaultContext sets no defaults). The duplication is correct today and TestResolveHomeDirAgreesWithTheLegacyHandler guards it — no change requested, just confirming the TODO is the right disposition.
  • Idempotency of the differential fixtures checks out: interceptConfigs only writes config.toml/app.toml when they are absent, so the seed-then-compare pattern in seedDefaultConfig + the corpus tests is stable across the two runs and TestConfigManagerV2WritesNothing is a meaningful assertion rather than a vacuous one.
  • Stale comment outside this diff: cmd/seid/cmd/root.go:104 still says "When PR2 adds the seid 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/configtest characterization 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.

Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager_differential_test.go
Comment thread go.mod
@bdchatham

Copy link
Copy Markdown
Contributor Author

seidroid xreview

Comment thread cmd/seid/cmd/configmanager_differential_test.go
@bdchatham

Copy link
Copy Markdown
Contributor Author

Final focused pass:

  1. recover guarantee untested (:64): added TestReportAdvisoryNeverEscapesWhenTheLoggerPanics — swaps logger for a *slog.Logger whose handler panics on every record, then asserts reportAdvisory returns normally over a config that produces a diagnostic. That drives both the reporting logger.Warn and the recover handler's own logger.Error into a panic, exercising the outer recover and the nested guard together.
  2. clean pass logged nothing (:222/:227): added an Info line when the pass completes with no diagnostics, so a v2 operator can tell a clean run from the quiet fresh-node skip.
  3. stageRead not driven by a real path (:182): resolving with rationale — what stageRead affects is the log rendering, which TestLogAdvisoryHandlesEveryOutcome covers via a hand-built outcome; driving validateAdvisory itself there needs a present-but-unreadable config, env-fiddly for little added coverage.
  4. go.mod:191 (ungated bump): acknowledged, no code change — flagged in the PR description with the 23-package -race characterization; the all-nodes framing belongs in the release notes.
  5. advisory log level (:52): the early emission is deliberate (a config warning is visible pre-boot regardless of the not-yet-applied level); the capture-before/log-after split is a clean alternative if the team prefers diagnostics follow the configured level.
  6. fuzz guard re-flag: superseded — the guard now walks every bound flag's derived env var, not just --home, so SEID_LOG_LEVEL and any other consumed flag are covered.

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

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

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.md is 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. TestLogAdvisoryHandlesEveryOutcome feeds a synthetic Panic outcome and TestReportAdvisoryNeverEscapesWhenTheLoggerPanics covers a panicking logger, but nothing forces seiconfig.ReadConfigFromDir/Validate to actually panic, so the recover in validateAdvisory (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 assert SeiConfigManager{}.Apply still 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 logAdvisory additionally 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.
  • resolveHomeDir duplicates ~15 lines of the legacy handler's viper bootstrap. The TODO and TestResolveHomeDirAgreesWithTheLegacyHandler are the right mitigation for now; just make sure the extraction to an exported server.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.

Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread cmd/seid/cmd/configmanager/configmanager.go Outdated
Comment thread go.mod
Comment thread cmd/seid/cmd/configmanager_differential_test.go
…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.

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

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.md is empty (no output from the Cursor pass) and codex-review.md reports "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_MANAGER unset: go-viper/mapstructure/v2 2.4.0→2.5.0 backs rootViper.Unmarshal(conf) and rootViper.Unmarshal(&customConfig) in sei-cosmos/server/util.go:290,300, i.e. every node's tendermint + app config decode. I verified BurntSushi/toml's only in-tree importers are sei-tendermint/test/e2e/* (not the node config path), and there are no replace directives 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).Diagnostics into a log line. Worth confirming Diagnostic.String() renders field paths (and not raw values) for keys that can hold credentials — e.g. an RPC endpoint with embedded auth in app.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 the logger.Warn path 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:105 still carries the PR1 comment "When PR2 adds the seid 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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.

3 participants