feat(relay): validate required env vars at boot (#80)#84
Conversation
Introduce CheckEnvConfig + envContracts registry and wire into main.go ahead of CheckInsecureListenInProduction so that a typo like PYRYCODE_RELAY_PRODUCTION=true is caught before IsProductionMode's silent-non-production fallback can be reached. ErrInvalidConfig carries the offending Key and Reason; branchable via ErrInvalidConfigSentinel; matched via errors.As for structured logging. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code Review: #80Decision: PASS Findings
SummaryAll five ACs are satisfied. |
Add feature doc and per-ticket codebase note for the boot-time env-var validator. Cross-link from production-mode.md so readers know the malformed-value cases listed there are now caught by CheckEnvConfig before IsProductionMode is consulted. Update INDEX.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
What
Adds a boot-time, structured env-var validator that runs over an explicit contract table and aborts with a per-key reason on the first missing/malformed value.
internal/relay/env_config.go:ErrInvalidConfigSentinelpackage-level sentinel (branchable viaerrors.Is).*ErrInvalidConfig{Key, Reason}struct error with a customIssoerrors.Is(err, ErrInvalidConfigSentinel)returns true without double-printing the prefix; field extraction viaerrors.As.envContractsregistry — single source of truth for "what env vars the relay reads". Today: one row,PYRYCODE_RELAY_PRODUCTION(optional, exact-"1"or-unset).CheckEnvConfig(lookup func(string) (string, bool)) error—os.LookupEnvshape so missing-but-required is distinguishable from present-but-empty.checkEnvConfigWithseam so tests can exercise therequired: truebranch without a production registry entry.cmd/pyrycode-relay/main.go: wired between the--domain/--insecure-listenflag guard andCheckInsecureListenInProduction. Logsenv_var+reasonviaerrors.As, exits 2 on failure. Ordering is load-bearing — running this beforeCheckInsecureListenInProductionpreventsIsProductionMode's silent-non-production fallback from being reached with an unvalidated env. Novaluefield in the log line (per security review's forward-looking caveat).Issue
Closes #80. Architecture:
docs/specs/architecture/80-validate-env-vars-at-boot.md.Testing
internal/relay/env_config_test.go:TestCheckEnvConfig_ValidEnvReturnsNil— unset and"1"both pass.TestCheckEnvConfig_MalformedValueReturnsStructuredError— covers"true","0","yes"," 1","1 ","PRODUCTION",""; assertserrors.Issentinel match,errors.Asextraction,KeyandReasonshape (mirrorsIsProductionModevalue matrix so a future loosening of the"1"-exact rule breaks both at once).TestCheckEnvConfig_MissingRequiredKey— exercises therequired: truebranch viacheckEnvConfigWithwith a synthetic contract table (no production row is required today).TestCheckEnvConfig_IgnoresUnregisteredKeys— locks in "validator polices its declared contract, not arbitrary process env".TestErrInvalidConfigSentinel_IsBranchable—errors.Is+errors.Asdual contract.Verification:
All tests are
t.Parallel()and never touch process env (injectedfakeLookup).Architecture compliance
ErrInvalidConfigper the AC's literal wording; sentinel namedErrInvalidConfigSentinelto distinguish.os.LookupEnvshape (func(string) (string, bool)) per the AC — required to distinguish missing-but-required from present-but-empty.IsProductionModekeeps itsfunc(string) stringgetter; the two seams coexist.cmd/pyrycode-relay/main.go: env-config check →CheckInsecureListenInProduction→CheckCapabilities.err,env_var,reason); reason string forPYRYCODE_RELAY_PRODUCTIONis safe to echo because the legitimate value is the literal"1".🤖 Generated with Claude Code