fix(secrets): review round 1 — pre-host expander reads the same store the runtime serves, raw-string Replication:Enabled parse, leftover-key warning

- SecretsRegistration: UsesCentralSharedStore predicate shared by AddScadaBridgeSecrets and the
  new AddPreHostExpander, so a Central node's Layer-A ${secret:} expander composes against the
  shared SQL store whenever Secrets:Store:Provider=SqlServer does — closing the gap where a
  central migrated off the retired Grpc topology would boot-time-resolve config from per-node
  SQLite while serving the shared store.
- Program.cs: expander block now calls AddPreHostExpander + resolves the store-agnostic
  ISecretsStoreMigrator seam; stale Grpc-hub/replication comment rewritten.
- DeployedSecretsMirror.cs: stale "gRPC-hub follower half" doc comment rewritten to state the
  now-permanent local-SQLite-only invariant.
- StartupValidator: Secrets:Replication:Enabled read as a raw string (any non-empty, non-"false"
  value rejected) instead of GetValue<bool>, which threw a raw binder crash on "yes"/"1"; added a
  CollectWarnings entry for leftover Secrets:Replication/Secrets:GrpcHub child keys once Enabled
  is false/unset.
- Tests: +15 (AddPreHostExpander/UsesCentralSharedStore wiring, Replication:Enabled edge values,
  leftover-key warnings). Full solution build and full Host.Tests green (593/593).
This commit is contained in:
Joseph Doherty
2026-09-28 20:10:17 -04:00
parent 2bac13170c
commit bc29dc412b
6 changed files with 423 additions and 30 deletions
@@ -116,13 +116,16 @@ public sealed record MirrorResult(
/// can observe a stale pre-rotation plaintext for up to the cache TTL.
/// </para>
/// <para>
/// <b>Node-local store assumption (review Minor #5).</b> This mirror assumes the site's
/// <see cref="ISecretStore"/> is node-local (plain local SQLite, or the gRPC-hub follower half,
/// both of which keep writes local). It is NOT safe to run against a site node whose store is
/// decorated by SQL-Server secrets REPLICATION (<c>Secrets:Replication:Mode=SqlServer</c>) — its
/// <c>DeleteAsync</c> publishes the tombstone to the shared hub, so a site merely un-referencing
/// a secret would delete it fleet-wide. Task 11 rejects that configuration on the Site role;
/// no code change is made here.
/// <b>Node-local store assumption (review Minor #5; Task 11 removed every replication mode).</b>
/// This mirror assumes the site's <see cref="ISecretStore"/> is always plain, node-local, local
/// SQLite — the ONLY shape a Site node's store can be after Task 11 retired both the pull-only
/// gRPC secrets hub and the SQL-Server <c>ReplicatingSecretStore</c> mode that preceded it (see
/// <c>SecretsRegistration</c>). It would NOT have been safe to run this mirror against a store
/// decorated by either retired replication mode: <c>DeleteAsync</c> would have published the
/// tombstone to every other node sharing the decorator, so a site merely un-referencing a secret
/// would have deleted it fleet-wide. <c>StartupValidator</c> now rejects
/// <c>Secrets:Replication:Enabled=true</c> outright on either role, so that configuration can no
/// longer reach this mirror at all.
/// </para>
/// <para>
/// <b>Resolve semantics and the resolver cache.</b> The trial-resolve verify step goes through