Repository navigation
Store host profile model + --check-settings verb + --validate-config registry fix (#4214) - #4271
Conversation
…gistry fix (#4214) Part 1 of #4214: the cross-platform host/store profile, per-setting source attribution and verdicts (matches / stale-after-hardware-change / operator-override / not-managed), the --check-settings CLI verb, and the --validate-config fix from the issue comment (probe the store's registry when reachable, not darling.json's seed list; a file-only server is now a warning, not exit 1). Reuses DeriveMemorySettings/DeriveWorkerSettings/DeriveWalSettings and the Windows GlobalMemoryStatusEx read rather than duplicating them. Source attribution reads the managed data directory's postgresql.conf/.auto.conf from disk instead of pg_settings.sourcefile, which needs escalated privileges the mcp role does not have. Checkpoint commit: tests, startup logging, README and the full suite run still need to land before this is ready for review. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
#4214) RAM/cgroup parsing (every branch: meminfo present/missing/unparseable, v2 max vs a real limit, v1's near-long.MaxValue sentinel), containerization, managed-block line detection, the four-way verdict classification, and pg_settings unit normalization -- all pure, all Windows-runnable per ruling 10. Plus CLI verb recognition for --check-settings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…dicts Add DarlingStoreHostProfileLiveTests: builds the profile against a real managed data directory and a real running server, proving Matches, StaleAfterHardwareChange, OperatorOverride and NotManaged all resolve correctly from real conf files and real pg_settings, not just the pure classification the existing pin tests cover. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Add DarlingCliCommandsHostCheckTests: an exit-code test per --check-settings outcome (config parse error, unreachable store, a bring-your-own store with nothing stale, a managed store with a stale setting, text and --json), plus --validate-config's three registry paths (seed-only server warns and exits 0, a registry server that fails still fails, an unreachable store falls back to darling.json's own list and says so). Proved live that the stale-settings test fails if its exit-code branch is broken, then restored it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…avior Add a README section for --check-settings (verdicts, exit codes) next to --validate-config, and document that --validate-config probes the store's own registry once reachable, falling back to darling.json's seed list only when the store cannot be reached. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
DarlingCliCommandsHostCheckTests reaches the shared DARLING_TEST_PG store directly and through ScratchPostgres but was not serialized against the other live-postgres classes; add the collection attribute. TsqlConventionGuardTests' member-range census now sees DarlingStoreHostProfile.ComputeEffectiveMemoryLimitBytes stop short (an expression-bodied one-liner, the same shape as the other entries in the list) - add it to KnownTruncatedRanges per the test's own instructions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…profile log SerialLoopStoreSizeSourceTests failed on this branch because DarlingStoreHostProfile.StoreSizeSql adds a fourth pg_database_size call site the pin's census did not know about. It is a real fourth regime, not a false positive: --check-settings's own CliStoreReadSeconds budget, one-shot, never on the serial loop. Documented and added to the pin's allow-list rather than weakened. Also builds the startup logging item (ruling 9): DarlingStoreHostProfile.GatherStartupProfileAsync gathers only the cheap parts (host facts, pg_settings, conf files) and never reaches GatherStoreFactsAsync, so the store-size/chunk-total reads stay out of the collection loop's startup path entirely. DarlingWorker.LogStoreHostProfileAsync calls it once, ahead of the collection loop's while, under its own short deadline (ServiceCommandDeadlines.StartupHostProfileSeconds), logs the host/settings profile, warns once per stale-after-hardware-change setting, and never throws. Pinned with StartupHostProfileLogTests.cs (log-line shape via a fixture whose Store is deliberately filled with sentinel values that must never print; the failure path and the store-facts exclusion via source scans of DarlingWorker.cs and DarlingStoreHostProfile.cs; the call site's position ahead of the collection loop) and a new live test proving GatherStartupProfileAsync never gathers store facts even against a real running server. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
LiveCleanupConversionRatchetTests: 6 finally blocks in DarlingCliCommandsHostCheckTests used root.Delete(recursive: true) (a DirectoryInfo instance call), which contains neither File. nor Directory. and was flagged as store-teardown. Switch all 7 instances to Directory.Delete(root.FullName, recursive: true) so the static call carries the Directory. exclusion token the ratchet requires. DarlingMissingCredentialMessageTests: PR #4271 added a new verb that calls MissingStoreCredentialMessage, raising the count from 6 to 7. Update the Assert.Equal count to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ
Fix reportTwo CI failures fixed in commit 9f6f98e, pushed to Failure 1: LiveCleanupConversionRatchetTests (6 offenders)Diagnosis confirmed. Fix: changed all 7 instances to Result: Failure 2: DarlingMissingCredentialMessageTests — Assert.Equal(6) → 7Diagnosis confirmed. The test Fix: updated the expected value from 6 to 7 at Result: Build
|
* Add get_store_host MCP read for #4214 part 2 (checkpoint) get_store_host reuses part 1's DarlingStoreHostProfile.GatherAsync (#4271) to serve the host/store/settings profile over MCP and /api/read, with its own enclosing deadline (store-size reads scale with the store, #3199) and a fixed #4271 CLI test that assumed a bootstrapped rig's own postgresql.conf and "darling" database rather than building its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214: bump README/llms.txt Darling tool census 159 -> 160 get_store_host is the 160th Darling MCP tool; DarlingMcpInstructions.cs already carried this bump but README.md and llms.txt did not, failing CrossAppMcpToolInventoryPinTests.RootReadmeDarlingToolCensus_MatchesTheScannedInventory and LlmsTxtToolCensus_SpansTheTwoCurrentEditions in CI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214: live test proves get_store_host under the mcp/viewer least-privilege roles DarlingMcpStoreHostToolsLiveTests calls DarlingMcpStoreHostTools.GetStoreHost through the SAME least-privilege roles it runs under in production (mcp for the MCP host, viewer for the web /api/read row), not the rig superuser every other #4214 part 2 test uses. Proves all four verdicts (matches, stale_after_hardware_change, operator_override, not_managed) are reachable under each role: - work_mem: a per-role ALTER ROLE ... SET default applied before the role's first connection, matched against this host's own DeriveMemorySettings figure -> matches (shared_buffers/max_connections are PGC_POSTMASTER and the live rig would need a restart to move them, so work_mem, PGC_USERSET, is the one setting a live value can be set for without touching the rig's real conf or going through postgresql.auto.conf, which would misattribute to operator_override). - max_connections: a managed-block assignment of 100 against the TargetMaxConnections=200 constant -> stale_after_hardware_change. - maintenance_work_mem: left unassigned in the fake managed conf -> operator_override (ClassifyVerdict's default arm). - A second gather with Managed=false -> not_managed for every setting. No permission gap found: pg_extension/pg_stat_database/pg_settings carry no Darling-authored grant anywhere in DarlingManagedRoles, and this proves PostgreSQL's own PUBLIC defaults are what let mcp/viewer read them; the one grant that matters (SELECT on collect, for timescaledb_information.chunks visibility) already exists in production provisioning and is mirrored here. Ran twice against the rig-th rig (port 55983): 1 total, 0 failed both times; confirmed host_mcp_test/host_viewer_test roles are dropped after each run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214: get_store_host tools/list + response-size budget pins McpToolsListBudget/DarlingMcpStoreHostTools.txt pins the new tool's served block (510 chars) and McpToolsListBudgetTests.TotalCeilingBytes moves 173,157 -> 173,773 (+616, the tool's own JSON scaffolding included) to match: get_store_host has no parameters, so this is the tool's whole entry. DarlingMcpStoreHostBudgetLiveTests proves the response-size half: get_store_host's payload is a fixed shape (host/store facts plus one row per sizing-relevant setting, 8 today), not row-scaling like the other #4198 tools, so one live call in the not_managed shape (the longer of the two source strings) is the whole proof. Passed at well under McpResponseBudget.DefaultBytes (32 KB) against the rig-th rig. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214: web Store host panel on the Sweeps page Adds a "Store host" section to the Fleet Sweeps page, below Watch items: reads get_store_host through /api/read (readTool, the page's existing read pattern - no raw fetch, no second copy of the verdict math #4214 part 1 already owns) and renders host facts (platform, CPUs, RAM, data volume, managed flag, PostgreSQL/TimescaleDB facts) plus a per-setting table (current/derived/source/verdict), with every stale-* verdict highlighted using the page's existing sev-Warning/ sev-Healthy vocabulary. Read-only, like the rest of this page (no apiSend call) - the CLI --check-settings verb and the companion sizing issue own any write. Uses the page's own local table()/cellText() helpers (no new table implementation) and four new util.js imports (fmtMb, fmtPct, fmtBool, fmtText) already used elsewhere in the app. FleetSweepWebFeedTests gains a source pin (no JS runner in this pipeline) proving the section title, the get_store_host read call, and the startsWith("stale") highlight predicate. Verified sev-Warning/ sev-Healthy are real table.data td rules in app.css, and node -c confirms the edited file parses. FleetSweepWebFeedTests (59) and DarlingWebAssetsTests all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214: fix two full-suite regressions from the get_store_host change Full suite (13,992 tests) surfaced two defects caused by this branch: - DarlingMcpStoreHostBudgetLiveTests reached the shared DARLING_TEST_PG store without [Collection("live-postgres")], failing LivePostgresCollectionHygieneTests. Added the attribute, matching its sibling DarlingMcpStoreHostToolsLiveTests. - DarlingMcpFleetSweepToolsTests.TheMirrorsLoggerSeat_... pinned the old BuildReadDispatch(logger) call-site text; BuildReadDispatch now also threads postgresConfig (get_store_host's config seat) per this PR's earlier commit, so the literal moved to BuildReadDispatch(logger, postgresConfig) - the same kind of pin update already made for FleetSweepWebFeedTests/SharedBaselineCacheTests, missed for this third site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 3: trim PostgresConfig at the MCP/web DI seat Round-1 review Low 3: the full PostgresConfig (owner connection string included) was injectable via DI at both the MCP and web host wiring, even though get_store_host's GatherAsync only ever reads Managed/DataDirectory off it. Register a trimmed copy at both call sites instead, so a future [McpServerTool] that takes a PostgresConfig parameter cannot receive the owner secret through this seat. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 7: get_store_host description tail fixes Round-1 review: three fixes to the description tail (strictly after <<GUIDE>>, so the served tools/list head is unchanged - confirmed by McpToolsListBudgetTests, 5/5 green with no fixture edit needed). - data_volume_note -> data_volume.note (the payload key is nested under data_volume, not a flat data_volume_note). - Add the missing "ready" field to the data_volume parenthetical list. - Document gathered_at (UTC) and the 5-minute shared cache ahead of item 2 landing the field/cache itself, per the brief's item ordering. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 1: redact conf file paths in get_store_host's settings.source Round-1 review Medium 1: get_store_host's settings[].source printed a full local filesystem path for a managed block or operator override, fine on the CLI's own shell but not fine handed to a remote MCP/web caller. - HostSettingProfile gains SourceFile/SourceLine (default null/0), set only by the managed-attribution branch of GatherSettingProfilesAsync. SourceDescription itself is untouched, so --check-settings and the startup log line still print the full path as before. - New DarlingStoreHostProfile.FormatSourceForMcp: passes SourceDescription through verbatim when there is no SourceFile (not-managed/unreadable - already a kind word, never a path), otherwise renders "<kind> (<path>: <line>)" with the path redacted to a data-directory-relative path when inside the managed data directory, or the bare file name only when outside it (never a leading ../ that would still leak the parent tree). - get_store_host resolves the managed data directory once and reuses it for every settings row via the new formatter, in place of the raw SourceDescription. Tests: 3 new pure-function tests on FormatSourceForMcp (inside data directory, outside it, no SourceFile) plus the existing DarlingStoreHost- ProfileTests/StartupHostProfileLogTests/McpPayloadContractCensusTests/ McpToolsListBudgetTests classes, 124/124 passing. Revert-proved: gutting FormatSourceForMcp to return SourceDescription unconditionally failed the two new sanitization tests; restored and re-verified green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 4: DI-seat census + UNC refusal for the managed data directory Round-1 review Low 4, two independent halves. Census: a new McpServiceParameterDiSeatCensusTests pins that every DI-service-typed [McpServerTool] parameter (NpgsqlDataSource, PostgresConfig, DarlingAnalysisService, ILogger today) has an AddSingleton<T>/AddSingleton(new T seat in DarlingMcpHostService.cs's own source text. Without one, the SDK would serve that parameter as a client argument instead of resolving it from DI - for PostgresConfig, a remote caller could then set managed:true with a UNC dataDirectory. Coupled to item 3 by design: before item 3 trimmed the registration, AddSingleton(config.Postgres) did not name PostgresConfig in source text, so this test only started passing once item 3 landed (confirmed by reverting item 3's line, below). UNC refusal: new DarlingStoreHostProfile.TryResolveProfileDataDirectory wraps DarlingManagedPostgres.ResolveDataDirectory and refuses (returns null) a UNC-resolved path, mirroring DarlingManagedPostgres.TryResolveConfPath's own UNC refusal for an include directive - without it, a UNC-configured managed store would make the service open a conf file, or stat a volume, on a remote share as its own account (an NTLM-relay vector). Does not change ResolveDataDirectory itself, shared by the writer/provisioning path. Wired at both call sites: GatherSettingProfilesAsync's dataDirectory local (falls through to the same not-managed path a bring-your-own store already takes - never opens a file), and ResolveVolumeAnchor's managed arm (falls back to AppContext.BaseDirectory, same as its existing BYO branch). Tests: McpServiceParameterDiSeatCensusTests (new) plus 3 new pure-function tests on TryResolveProfileDataDirectory/ResolveVolumeAnchor. 135/135 passing (4 skipped, live-gated, no rig this pass). Revert-proved twice: reverting item 3's AddSingleton line failed the new census test; separately, gutting TryResolveProfileDataDirectory's UNC check failed both new UNC tests. Both restored and re-verified green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 6: random per-run password for the live test roles Round-1 review Low 6: DarlingMcpStoreHostToolsLiveTests.cs's two LOGIN test roles (each granted SELECT on every table in collect) used a hardcoded password that is public in this repository. Cleanup runs in finally, but a cleanup failure after a failed body is swallowed, and a killed run skips cleanup outright, so a role that survives on a reachable rig would be a known, reusable credential. Generate the password fresh per run instead (Convert.ToHexString(RandomNumberGenerator.GetBytes(16)) - hex output needs no escaping in the DDL string). Scope is this file only; DarlingSecuritySplitLiveTests uses the same pattern but is out of scope here. Build only (this is a live-gated test class, no rig this pass): 0 warnings, 0 errors; the class's own live Fact still skips cleanly without DARLING_TEST_PG set. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 1 follow-up: mcpDataDirectory agrees with the UNC refusal GetStoreHost's mcpDataDirectory local called the raw DarlingManagedPostgres.ResolveDataDirectory, while GatherSettingProfilesAsync (Low 4) now refuses a UNC-resolved data directory via TryResolveProfileDataDirectory. The doc comment on mcpDataDirectory claimed it was "the SAME data directory GatherSettingProfilesAsync resolved" - no longer true under a UNC-configured managed store. Harmless today (every setting's SourceFile is null in that case, so FormatSourceForMcp never reaches the sanitizer), but switches mcpDataDirectory to the same TryResolveProfileDataDirectory call so the comment is accurate again and there is no divergent path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 2: get_store_host's 5-minute shared cache New StoreHostProfileCache (Darling/PerformanceMonitor.Darling.Service/StoreHostProfileCache.cs): a single immutable CacheEntry behind one volatile reference field (not a bare tuple field, which can tear under concurrent reads), a SemaphoreSlim gate for single-flight gather, and an injected clock. A cache hit never calls the gather delegate at all, so it never opens a connection. A failed gather is never cached: the finally always releases the gate, and _entry is only assigned after gather returns successfully. Wiring: GetStoreHost takes a third StoreHostProfileCache parameter; postgres.OpenConnectionAsync moved inside the gather delegate; the response gains gathered_at (naive-UTC, matching every other "_at" timestamp on an MCP payload in this codebase per DarlingFleetReader/DarlingAgReader/ DarlingMcpConfigHistoryTools/DarlingMcpTrendTools - not the DateTimeKind.Utc the original plan named). DarlingMcpHostService.cs registers the process-wide StoreHostProfileCache.Shared singleton via the typed-generic AddSingleton<T> overload McpServiceParameterDiSeatCensusTests requires. DarlingWebEndpoints.cs's direct-call dispatch passes the same Shared instance. StoreHostProfileCache is public, not internal as the handoff's draft had it: GetStoreHost is a public [McpServerTool] method, and a parameter type may never be less accessible than the method (CS0051) - matches the existing PostgresConfig/DarlingAnalysisService precedent. GetOrGatherAsync itself stays internal, since its signature carries the internal HostProfile type. Test call sites (DarlingMcpStoreHostBudgetLiveTests.cs, DarlingMcpStoreHostToolsLiveTests.cs) each get a fresh StoreHostProfileCache instance, never the production Shared singleton. The two calls in DarlingMcpStoreHostToolsLiveTests.cs's role loop (managed config then BYO config against the same data source) each need their OWN fresh cache: the cache holds one un-keyed entry, so sharing one across those two calls would serve the first call's cached profile back for the second regardless of which PostgresConfig was passed. New Darling/Darling.Tests/StoreHostProfileCacheTests.cs, 4 tests: two concurrent callers gather once (proven deterministically via a two-TCS choreography, no Task.Delay), a call inside 5 minutes gathers nothing, a call after 5 minutes gathers again, a throwing gather is not cached. Revert-proved: gutted TryGetFresh to always return null, confirmed both TwoConcurrentCalls_GatherOnce and ACallInsideFiveMinutes_GathersNothing fail, restored, rebuilt 0/0, reverified all green. McpServiceParameterDiSeatCensusTests' pinned service-parameter-type list grows from four names to five (StoreHostProfileCache added), with the reason in the test's own comment. Build: 0 Warning(s), 0 Error(s). Targeted run (StoreHostProfileCacheTests, McpServiceParameterDiSeatCensusTests, McpPayloadContractCensusTests, McpToolsListBudgetTests, DarlingStoreHostProfileTests, StartupHostProfileLogTests, DarlingCliCommandsHostCheckTests, DarlingMcpStoreHostToolsLiveTests, DarlingMcpStoreHostBudgetLiveTests, FleetSweepWebFeedTests, DarlingWebAssetsTests, SerialLoopStoreSizeSourceTests): 220 total, 0 failed, 6 skipped (live-gated, no rig this pass). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 2 follow-up: StoreHostProfileCache implements IDisposable (CA1001) A non-incremental rebuild surfaced CA1001: the class owns a disposable SemaphoreSlim field (_gate) but was not itself IDisposable. Mirrors DarlingWebOidcClient's own gate disposal. Safe for the Shared singleton despite two hosts (DarlingMcpHostService, DarlingWebEndpoints.cs's direct-call dispatch) both holding a reference to it: both register it with the DI INSTANCE overload (AddSingleton<StoreHostProfileCache>(Shared)), and the built-in container never disposes an instance it did not itself construct, so no container shutdown disposes Shared out from under the other host. Build: 0 Warning(s), 0 Error(s) on a --no-incremental rebuild. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 3: sweeps.js's store host card skips re-fetching on a poll tick app.js's route() now forwards opts (the 60s poll's { poll: true } marker) to renderSweeps, which forwards it to renderStoreHost. renderStoreHost is split into itself (fetch/cache) and a new renderStoreHostPayload(box, p) (pure render), with a module-level lastStoreHostPayload. A poll tick with an already-fetched payload replays it through renderStoreHostPayload instead of calling readTool("get_store_host", {}) again; every non-poll call (first paint, hashchange, the span control) always fetches fresh. The profile this card reports (host RAM/CPUs/disk, PostgreSQL/ TimescaleDB versions, sizing verdicts) changes on a hardware or version change, never per-tick, so a 60-second poll paying a fresh network round trip for it was pure waste - the tool's own 5-minute server-side cache (item 2) already made repeat calls cheap for the STORE, but not for the browser's own network cost. FleetSweepWebFeedTests.TheSweepPage_IsWiredIntoTheShellAndTheRouter pinned the literal "renderSweeps(main)" call text; updated to "renderSweeps(main, opts)" to match. Build: 0 Warning(s), 0 Error(s) (--no-incremental). Targeted run (StoreHostProfileCacheTests, McpServiceParameterDiSeatCensusTests, McpPayloadContractCensusTests, McpToolsListBudgetTests, FleetSweepWebFeedTests, DarlingWebAssetsTests, SerialLoopStoreSizeSourceTests, DarlingCliCommandsHostCheckTests): 164 total, 0 failed, 4 skipped (live-gated, no rig this pass). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 * #4214 round-1 fix item 5: store-fact assertions in the least-privilege live test GetStoreHost_AsMcpAndViewerRoles_SurfacesAllFourVerdicts_Live gains three assertions per role, against the managed-config response: store.size_bytes and store.timescale_version are not null (both come off reads PUBLIC can run with no Darling-authored GRANT, so a regression there is a silent "unavailable" null rather than a thrown exception), and store.uncompressed_chunk_count matches a fresh owner-side read of the exact same query (DarlingStoreHostProfile. UncompressedChunkSizeSql, reused rather than duplicated) taken immediately after the tool call. Written per the handoff; not run live in this pass (no rig). Build: 0 Warning(s), 0 Error(s). The class still skips cleanly without DARLING_TEST_PG. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Part of #4214.
Why
The managed store already sizes PostgreSQL from the host. None of that was visible from the product. Answering "is this store sized right" needed a remote shell,
pg_settings.sourceline, and hand-counting conf blocks. This lane builds part 1: the host/store profile model, per-setting source attribution and verdicts, and the--check-settingsCLI verb. It leaves theget_store_hostMCP read, the web Store/Host panel, and cloud identity via IMDS to a later lane, per the brief.What changes
New file
Darling/PerformanceMonitor.Darling.Service/DarlingStoreHostProfile.cs:Environment.ProcessorCount), RAM, and the data volume (size, free space, filesystem, all viaDriveInfo).GlobalMemoryStatusExreadDarlingManagedPostgressizes itself from. It is extracted toTryReadWindowsPhysicalMemoryBytes, so there is no second P/Invoke of the same API. RAM on Linux reads/proc/meminfoMemTotal, plus cgroup v2memory.maxwith a fallback to v1memory.limit_in_bytes. Both reads go through pure functions over already-read file text, so a Windows test covers every branch: meminfo missing, meminfo unparseable, v2'smax, and v1's near-long.MaxValuesentinel.timescaledb_information.chunksreports as not yet compressed, summed withpg_total_relation_size.shared_buffers,work_mem,effective_cache_size,maintenance_work_mem,timescaledb.max_background_workers,max_worker_processes,max_connections,max_wal_size. Each reuses the product's ownDeriveMemorySettings,DeriveWorkerSettings, orDeriveWalSettingsfor "the value this host would derive right now," so no formula is duplicated. Mid-lane I found thatmax_wal_sizeis not the fixed 4GB literal the v4 block suggests: the v12 block supersedes it with a value derived from free disk space. The check followsDeriveWalSettings, not a constant.postgresql.conf's managed blocks andpostgresql.auto.confdirectly from disk, reusingDarlingManagedPostgres.ReadConfAssignments, which already follows PostgreSQL's own include and precedence order. It does not readpg_settings.sourcefile, because that column needs superuser orpg_read_all_settings, a right the least-privilegemcprole does not have (seeDarlingStoreMetricsReader.JobExecutionLoggingSql's remarks, which this lane's code comments point back to). A generic line-vs-managed-block scan walks every marker in a newDarlingManagedPostgres.AllManagedConfMarkerslist, so a future version block needs no update here. A bring-your-own store getsnot-managedfor every setting, pluspg_settings.source.matches,stale-after-hardware-change,operator-override,not-managed.--check-settingsCLI verb (DarlingCliCommands.CheckSettingsAsync, wired inProgram.cs). It prints the profile and the per-setting table;--jsonselects the automation form. It returns three distinct exit codes, kept separate per the ruling so a caller can tell a sizing problem from a connectivity problem: 1 for a config error, 2 for an unreachable store, 3 for one or morestale-after-hardware-changeverdicts.--validate-configfix, from the issue's comment. It now probes the store's registry (config_monitored_servers, read via the newly-internalStoreConfigProvider.ReadMonitoredServersAsync) when the store is reachable. A darling.json server absent from the registry now gets a warning instead of failing the run. When the store cannot be reached, it falls back to darling.json's own list and says so on stdout. This is a behavior change: before this PR,--validate-configand--test-connectionalways probed the file's list and exited 1 on a stale entry. That is the exact false failure the issue's comment reported on a 42-server production store.Small in-lane fixes, both mechanical and in files this PR already touches:
DarlingManagedPostgres.TargetMaxConnections = 200, so the v4 block and the new check share one source instead of the literal200living in two places.FindHardwareSizingBlockEndandReadConfAssignmentsreachable (internal), so the new file can reuse them instead of re-implementing them.DarlingStoreHostProfile.ComputeEffectiveMemoryLimitBytestoTsqlConventionGuardTests.KnownTruncatedRanges(the member-range census correctly flagged it as a new expression-bodied one-liner, the same shape as the other entries already in that list).This lane (H1b) added, on top of H1's core:
Darling/Darling.Tests/DarlingStoreHostProfileLiveTests.cs: one live test that builds the profile against a real managed data directory and a real running server, proving all four verdicts resolve correctly end to end — not just the pure classification.Matchescomes from a real v8-marker block pluspg_reload_conf()for three sighup/user-context settings;stale-after-hardware-changeneeds no live change at all, because PostgreSQL's untouchedmax_connectionsdefault (100) already disagrees with today's derivation (200) once a managed block claims it;operator-overridecomes from a realALTER SYSTEM;not-managedcomes from a bring-your-own config against the same connection. Restores the conf files (and reloads) infinallyviaLiveStoreCleanup.Darling/Darling.Tests/DarlingCliCommandsHostCheckTests.cs: an exit-code test per--check-settingsoutcome (config parse error, unreachable store, a bring-your-own store where nothing is ever stale in both text and--json, a managed store with one stale setting) and per--validate-configregistry path (a seed-only server warns and exits 0, a registry server that fails still fails, an unreachable store falls back to the file's list and says so). Proved live that the stale-settings exit-code test fails if its branch is broken (flipped theAny(... StaleAfterHardwareChange)check toMatches, watched it fail, reverted) — no diff remains from that step.--validate-config's registry-vs-file behavior.LivePostgresCollectionHygieneTestsflagging the new CLI test class for reaching the shared store without[Collection("live-postgres")].EXPLAIN (ANALYZE, BUFFERS) on the uncompressed-chunks read (
UncompressedChunkSizeSql), seeded on the rig'sprobedatabase: 3 hypertables, 10 daily chunks each (30 chunks total), 13 compressed and 17 left uncompressed.All 508 buffer hits were cache hits (no disk reads). The catalog view itself (
timescaledb_information.chunks) costs 28 of those; the rest comes frompg_total_relation_size()resolving each of the 17 uncompressed chunks' size — relation metadata (extent/fork sizes), never a scan of chunk row data. 20ms total for 30 chunks matches the code's own justification for paying this live rather than reading the hourly-sweep figure: a one-shot CLI/once-per-start cost, not a hot path. No bound was added; the read does not scale into chunk contents, only chunk count.CHANGELOG entry
SECTION: Added
ENTRY:
--check-settingsreports whether a managed store's sizing still matches this host ([Store host profile model + --check-settings verb + --validate-config registry fix (#4214) #4271]) - a new CLI verb prints the host and store facts (RAM, CPUs, PostgreSQL/TimescaleDB versions, store size, buffer hit ratio, uncompressed TimescaleDB chunk bytes against RAM) and a verdict for each of the eight sizing-relevant settings: matches, stale after a hardware change, operator override, or not managed.--jsonprints the same facts for scripting. Separate exit codes for a config error, an unreachable store, and a stale setting let an install or upgrade script gate on the outcome that matters to it.SECTION: Fixed
ENTRY:
--validate-config(--test-connection) no longer fails a healthy store over a stale entry in darling.json ([Store host profile model + --check-settings verb + --validate-config registry fix (#4214) #4271]) - once the store is reachable, this now probes its own registry of monitored servers instead of the config file's first-bootstrap seed list. A server that is only in the file (never registered, or since removed from the registry) is now a warning, not a failure. A store that cannot be reached still falls back to the file's list, and says so on the console. This closes the false failure reported on a 42-server production store whose darling.json still listed two long-removed servers.REF:
[Store host profile model + --check-settings verb + --validate-config registry fix (#4214) #4271]: Store host profile model + --check-settings verb + --validate-config registry fix (#4214) #4271
Test plan
dotnet build Darling/PerformanceMonitor.Darling.Service/PerformanceMonitor.Darling.Service.csproj: 0 Warning(s), 0 Error(s).dotnet build Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s).DarlingStoreHostProfileTests, 43 tests, all passing (pure classification, every RAM/cgroup parse branch, verdict classification,pg_settingsunit normalization, CLI verb recognition).DarlingStoreHostProfileLiveTests(new, gated onDARLING_TEST_PG+DARLING_TEST_PGRUNTIME): 1 test, all four verdicts proved end to end against the rig. Passing.DarlingCliCommandsHostCheckTests(new): 7 tests, all--check-settingsand--validate-configexit-code paths. Passing. Proved live that the stale-settings test fails against a broken branch, then reverted.probe— numbers above; no bound needed.--check-settingssection,--validate-configregistry behavior); plain-English checker run once, the one genuinely long new sentence split.git merge origin/dev(clean, no conflicts),darlingtestdropped/recreated, full Darling suite run once: 13944 total, 0 errors, 7 failed, 29 skipped. Two failures were this branch's: a hygiene-collection miss and a census list needing the new pure function added (both fixed, files changed 0 net diff besides the additions; re-ran the affected classes clean, 76/76 passing). The other 5 (SerialLoopStoreSizeSourceTests,EventWindowedReadsAreBoundedLivePostgresTests,CaptureDownChunkOrderTests,ServerListAndSummaryPlanShapeTests,LiveCleanupBatchTests) were called plan-shape/chunk-count assertions in files this PR never touches. That call was wrong for the first of the five; see below.SerialLoopStoreSizeSourceTestsis this branch's own defect, not pre-existing. Re-ran it alone on a freshly createddarlingtest.PgDatabaseSizeRunsOnlyWhereItsCostHasABudgetfailed becauseDarlingStoreHostProfile.StoreSizeSql(pg_database_size) is a real fourth call site the pin's census did not know about yet, not a false positive. It is a genuine fourth regime:--check-settings's own one-shot, user-invoked read underCliStoreReadSeconds(10s), never the serial loop. It was documented and added to the pin's allow-list with that reasoning, mirroring the existing three entries' style, not weakened. 20/20 passing after the fix.DarlingStoreHostProfile.GatherStartupProfileAsyncgathers only the cheap parts: host facts, the livepg_settingsread, the managed conf files. It never callsGatherStoreFactsAsync, so the store-size/chunk-total reads that scale with the store stay out of the startup path entirely.DarlingWorker.LogStoreHostProfileAsynccalls it once, ahead of the collection loop'swhile(pinned by source order). It runs under its own new deadline,ServiceCommandDeadlines.StartupHostProfileSeconds(CliStoreReadSeconds+ 5 = 15s, documented as its own regime rather than reusingBootstrapSeconds), logs the profile at info, warns once perstale-after-hardware-changesetting, and never throws (pinned: nothrowanywhere in the method's source). New fileStartupHostProfileLogTests.cshas 4 tests: the log-line shape against a fixture whoseStoreis filled with sentinel values that must never print, the store-facts exclusion, the never-throws failure path, and the call-site ordering. Plus 1 new live test inDarlingStoreHostProfileLiveTests.csproving the exclusion holds against a real running server. All passing.StartupCommandTimeoutTests,StorageCommandTimeoutTests,LivePostgresCollectionHygieneTests,TsqlConventionGuardTests,DocCommentHygiene,DarlingStoreHostProfileTests,DarlingCliCommandsHostCheckTests. 185 total, 0 failed.SerialLoopStoreSizeSourceTests(20),StartupHostProfileLogTests(4) andDarlingStoreHostProfileLiveTests(2) ran separately above, all passing.git merge origin/devagain for this lane's own work (clean, no conflicts; dev had since touchedEventWindowedReadsAreBoundedLivePostgresTests.csandPlanChunkScans.cs),darlingtestdropped/recreated. Then ran H1b's 4 other remaining-failure classes alone, skippingEventWindowedReadsAreBoundedLivePostgresTests(another lane owns it):SerialLoopStoreSizeSourceTests,CaptureDownChunkOrderTests,ServerListAndSummaryPlanShapeTests,LiveCleanupBatchTests. 29 total, 0 failed. None of H1b's other 4 reproduced after this merge, so there was nothing left to check against dev's CI for them.What the coordinator should double-check
SerialLoopStoreSizeSourceTests's new fourth allow-list entry (DarlingStoreHostProfile.cs, in the test file). Please confirm the reasoning holds:--check-settingsis genuinely this call site's only caller, and startup deliberately never reaches it.C:\GitHub\worktrees\rig-th, port 55983) is stopped. It now also carries adarlingdatabase (managed-mode connections always target that fixed name) and aprobedatabase with 3 seeded/partly-compressed hypertables, left in place for reuse. Both are throwaway rig state, not customer data.AppContext.BaseDirectory, not a store data directory. A BYO store's data directory is not something this process can know, and the store may be remote. Documented inDarlingStoreHostProfile.ResolveVolumeAnchor's comment as the smallest reasonable call, flagged for revisiting once part 2 builds the web panel.ServiceCommandDeadlines.StartupHostProfileSecondsis a new constant (15s =CliStoreReadSeconds+ 5). Please confirm the derivation in its doc comment reads right: it backstopsGatherSettingProfilesAsync's ownCliStoreReadSeconds-bound query rather than setting an independent number, the same relationshipCliBudgetBackstopSecondsalready has toCliStoreReadSeconds.No monitored or production server was touched.
Installer.Testswas not run.🤖 Generated with Claude Code
https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3