# Secrets `0.2.0` Upgrade + Clustered Topology Adoption — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Move all four family apps from `ZB.MOM.WW.Secrets` `0.1.2` to `0.2.0`, and adopt a **clustered secrets topology** in the two Akka-clustered apps (OtOpcUa, ScadaBridge) so a secret written on one node is resolvable on every node that needs it. **Architecture:** No library construction — `0.2.0` is built, published, and restore-verified on the Gitea feed (5 packages). Two distinct workstreams: (A) a **pure version bump** for all four apps, which is behaviour-preserving and independently valuable (it picks up the G-8 KEK-rotation surface and a **high-severity transitive security fix**); (B) **topology adoption** for the two clustered apps, which is *configuration + one DI call*, no product-code change. The topology choice is **not the same for both apps** — see §2, this is the load-bearing design point. **Tech Stack:** .NET 10, C#, `ZB.MOM.WW.Secrets{,.Abstractions,.Ui}` @ `0.2.0`, plus `ZB.MOM.WW.Secrets.Replicator.AkkaDotNet` (OtOpcUa) / `ZB.MOM.WW.Secrets.Replicator.SqlServer` (ScadaBridge), all from the `dohertj2-gitea` feed. Akka.NET **1.5.62** in both clustered apps — an exact match with the replicator package's Akka dependency. **Predecessors:** [`2026-07-16-secrets-adoption-design.md`](2026-07-16-secrets-adoption-design.md) (the adoption template — §5 rewritten by this work), the three executed per-app adoption plans, and [`2026-07-17-secrets-g7-clustered-replication-design.md`](2026-07-17-secrets-g7-clustered-replication-design.md) (the fork analysis, now a *deployment* decision rather than a build one). --- ## 1. Why bump at all > **⚠️ CORRECTED TWICE during execution, 2026-07-18 — read this before citing the bump's value.** > The original text claimed the bump "closes a high-severity advisory in all four apps." A first > correction narrowed that to "two apps." **Both were wrong.** Measured by A/B against the `0.1.2` > baseline in every repo: **all four already resolved the patched `SQLitePCLRaw.lib.e_sqlite3 > 2.1.12` before this bump.** The Secrets `0.2.0` upgrade delivers **no security change to any > app.** The lesson — the same one G-7's post-build review taught — is that a plausible mechanism > is not evidence. Both wrong claims came from reasoning about what `0.2.0`'s nuspec *should* > deliver instead of measuring what each repo *already resolved*. `0.2.0` does declare a direct dependency on the patched `SQLitePCLRaw.lib.e_sqlite3 2.1.12` (GHSA-2m69-gcr7-jv3q, high). That is real — it is simply **redundant everywhere**, because each repo had already arrived at ≥`2.1.12` by another route: | App | How it already resolved ≥ 2.1.12 before the bump | Security delta from this bump | |---|---|---| | HistorianGateway | Direct pin `SQLitePCLRaw.lib.e_sqlite3 3.50.3` (cites a *different* CVE, CVE-2025-6965) | **None** | | OtOpcUa | `SQLitePCLRaw.bundle_e_sqlite3` pinned `2.1.12` in `Directory.Packages.props` | **None** | | MxAccessGateway | Transitively from the pre-existing `ZB.MOM.WW.Auth.ApiKeys 0.1.5` reference | **None** (A/B-verified on the 0.1.2 baseline) | | ScadaBridge | Transitively from `ZB.MOM.WW.Auth.ApiKeys 0.1.5` (commit `50d79ed1`) | **None** (A/B-verified) | **The honest justification for Tasks 1–4 is therefore version hygiene plus a precondition:** the replication packages in Tasks 5–9 are `0.2.0` artifacts and cannot be consumed on `0.1.2`, and `0.2.0` exposes the G-8 KEK-rotation surface (`secret rewrap-all`) the runbooks document but no app can currently run. That is a legitimate reason to land them — but it is **weaker than originally stated**, and it means Tasks 1–4 are *not* independently valuable in the way this plan first claimed. If the clustering work is abandoned, the bumps buy only currency and rotation access. ### The real SQLite finding — and it was a live vulnerability Chasing the suppressions turned up something the version bump had nothing to do with. **Fixed 2026-07-18**, in its own commits, separate from the Secrets bump: **ScadaBridge was shipping a live high-severity vulnerability, masked by its own suppression.** The earlier A/B only checked the *Host* project, which resolved a patched `2.1.12` transitively via `Auth.ApiKeys`. Every *other* SQLite consumer — `AuditLog`, `SiteRuntime`, `StoreAndForward`, `SiteEventLogging` and 11 test projects — still resolved the vulnerable **`2.1.11`**, and the `NuGetAuditSuppress` hid NU1903 for all of them. Proven by removing the suppression and forcing a restore: 12+ projects immediately reported NU1903. Both suppressions rested on false premises: mxaccessgw's said *"no patched e_sqlite3 release exists yet"*; ScadaBridge's said *"the only patched native lib is the SQLitePCLRaw 3.x line."* **`2.1.12` patches this advisory within the 2.1.x line** — verified empirically (suppression removed + pin in place → forced restore reports no NU1903). HistorianGateway's separate `3.50.3` pin cites **CVE-2025-6965**, a *different* advisory; that is the likely source of the "only 3.x" confusion. **Fix applied:** explicit `PackageReference` to `SQLitePCLRaw.lib.e_sqlite3 2.1.12` in every SQLite-consuming project, suppressions deleted in both repos (ScadaBridge `2a0faeab`, mxaccessgw `2f0cfe3`). HistorianGateway and OtOpcUa needed nothing — genuinely fixed already, no suppressions. **Rejected alternative:** `CentralPackageTransitivePinningEnabled`, which clears it in one line but makes every central version a ceiling for transitive resolution too — immediately demanding bumps to `Google.Protobuf`, `Grpc.Net.Client`, `Microsoft.Data.SqlClient` and `Newtonsoft.Json`. That is a gRPC/data-access change to a production SCADA platform and belongs in its own reviewed commit, not smuggled in under a security fix. **Recorded as a genuine follow-up** — it is the better long-term posture. Verified: ScadaBridge 55 projects build 0 warn / 0 error, **1108 tests pass across the SQLite layer**; mxaccessgw 0 warn / 0 error with test results identical to baseline. Tasks 1–4 remain four independent, single-file, behaviour-preserving edits in four separate repos — **fully parallelizable**. ### Build/test expectations — corrected against reality The original text expected "0 warnings" everywhere. Measured, on the `0.1.2` baseline: - **OtOpcUa** emits **861** pre-existing warnings (`OTOPCUA0001` / xUnit analyzers). Identical count before and after the bump. Do not expect 0. - **HistorianGateway**'s full-solution restore **fails** on an AngleSharp `NU1902` advisory promoted to an error by `TreatWarningsAsErrors` — pre-existing, reproduced on the unmodified base branch. - **MxAccessGateway** has no `src/MxGateway.sln`; the solutions are `src/ZB.MOM.WW.MxGateway.slnx` and `src/ZB.MOM.WW.MxGateway.NonWindows.slnx` (the macOS-safe subset omitting the x86 net48 worker). **`CLAUDE.md`'s recorded build command is stale** and should be corrected. - Several suites have pre-existing environment-gated failures on macOS (live-historian DNS, Unix-domain-socket path length, shared Docker-fixture contention). **A/B against the baseline before attributing any failure to the bump.** ## 2. The topology fork — and why the two clustered apps resolve it differently Verified against both repos on 2026-07-18. This is the single most important section; getting it wrong produces a cluster that looks healthy and silently fails to converge. ### OtOpcUa → **Akka peer-to-peer** (recommended) - OtOpcUa is **one Akka cluster**. Every node shares a single `seed-nodes` list, so `DistributedPubSub` reaches every member. The app **already uses `DistributedPubSub` heavily** in product code (`DriverHostActor.cs:419` caches the mediator; driver-health, alarm, and deployment-ack topics all ride it), so the transport is proven in-app, not newly introduced. - OtOpcUa registers its `ActorSystem` **in DI via `Akka.Hosting`** — which is exactly what `SecretReplicationActorProvider` requires. - **Driver-role nodes resolve Layer-B driver secrets** (Galaxy API key, OpcUaClient passwords) at driver-session-open. Those nodes should keep starting drivers when central SQL is unreachable; peer-to-peer replication serves reads from local state, so a partitioned driver node keeps working on last-known-good and re-converges on heal. - *Best-practice fit:* matches the app's existing cluster-communication idiom, adds no new infrastructure dependency, and preserves partition tolerance for the node role that most needs it. - **Alternative:** shared SQL store against the existing ConfigDb (`AddZbSecretsSqlServerStore`) — simpler (zero distributed failure modes, one `rewrap-all`), but couples every driver node's secret resolution to ConfigDb reachability. Prefer this **only** if you decide driver nodes should hard-fail rather than run on cached secrets during a ConfigDb outage. ### ScadaBridge → **SQL-Server hub mode** (recommended) — Akka *cannot* do the job here - ScadaBridge is **hub-and-spoke: one central cluster + N site clusters, each a SEPARATE Akka cluster** with its own `seed-nodes` list (`AkkaHostedService.BuildHocon`, `seed-nodes = [...]` per node). Central↔site traffic goes through `CentralCommunicationActor`, not cluster membership. - **Therefore `DistributedPubSub` is cluster-scoped and does not cross the central↔site boundary.** An Akka replicator would converge the central pair among themselves and each site pair among themselves, producing **independent secret islands that never reconcile** — while every node reports healthy. This is the failure mode to avoid, and it is why "integrate with Akka" is the wrong answer for this app. - Hub mode (`AddZbSecretsSqlServerReplication`) fits the topology exactly: each node keeps a **local** store and syncs bidirectionally with a shared central SQL hub. Site nodes keep resolving secrets **through a WAN outage** — which is the entire point of the hub-and-spoke design — and re-converge when the link returns. - *Best-practice fit:* mirrors how ScadaBridge already treats central SQL (config, audit forwarding) — authoritative when reachable, locally buffered when not. - **Alternative:** shared SQL store (`AddZbSecretsSqlServerStore`) — one store, no reconciliation, but a site that loses the WAN loses secret resolution past the resolver's short TTL cache. **Not recommended**: it contradicts the site-autonomy premise of the architecture. ### HistorianGateway, MxAccessGateway → **version bump only** Both are single-box, non-clustered. No replication package, no topology. Tasks 1–2. ## 3. Constraints that apply to every topology 1. **Same KEK on every node — non-negotiable.** Rows are ciphertext-only; a node whose master key differs stores replicated rows fine and then **fails closed on every resolve** with a `kek_id` mismatch. It reads like data corruption but is a deployment error. Verify with the digest check in [`clustered-secrets.md`](../../ZB.MOM.WW.Secrets/docs/operations/clustered-secrets.md) §0 **before** enabling replication. 2. **KEK rotation does not replicate.** A re-wrap deliberately leaves `revision`/`updated_utc` untouched so it stays invisible to LWW. `rewrap-all` runs **once per independent store** — once for a shared store; once per node *and* against the hub in hub mode; once per node in Akka mode. 3. **`nuget.config` needs no change.** Both clustered repos already map `ZB.MOM.WW.Secrets.*` to `dohertj2-gitea` (OtOpcUa `NuGet.config:29`, ScadaBridge `nuget.config:29`), and that pattern already covers `ZB.MOM.WW.Secrets.Replicator.SqlServer` / `.Replicator.AkkaDotNet`. **Verified** — do not add redundant patterns. 4. **Do not reconfigure Data Protection.** Unchanged from the original adoption: secrets envelope crypto is independent of DP, and touching DP risks invalidating cookies/hub tokens. --- ## Task 1 — HistorianGateway: bump `0.1.2` → `0.2.0` **Classification:** trivial **Estimated implement time:** ~2 min **Parallelizable with:** Task 2, Task 3, Task 4 (different repos entirely) **Files:** - Modify: `~/Desktop/HistorianGateway/src/ZB.MOM.WW.HistorianGateway.Server/ZB.MOM.WW.HistorianGateway.Server.csproj:37-38` **Steps:** 1. This repo does **not** use CPM for these — versions are inline on the `PackageReference`. Change both: ```xml ``` 2. Restore + build: ```bash cd ~/Desktop/HistorianGateway && dotnet build ZB.MOM.WW.HistorianGateway.slnx ``` **Expected:** succeeds, 0 warnings. `0.2.0` is API-compatible with `0.1.2` for everything this app uses. 3. Confirm the security fix actually landed — this is the *point* of the task, so verify it rather than assuming: ```bash dotnet list ZB.MOM.WW.HistorianGateway.slnx package --include-transitive | grep -i sqlitepclraw ``` **Expected:** `SQLitePCLRaw.lib.e_sqlite3` resolves to `2.1.12` (not `2.1.11`). 4. Run the offline suite: `dotnet test ZB.MOM.WW.HistorianGateway.slnx` **Expected:** green. **Known pre-existing:** an AngleSharp `NU1902` advisory warning in the test project — unrelated to this change; do not chase it, but confirm it is the *only* warning and that it predates the bump (`git stash` + rebuild to compare if unsure). 5. Commit: ```bash git add -A && git commit -m "chore(secrets): bump ZB.MOM.WW.Secrets 0.1.2 -> 0.2.0 (SQLitePCLRaw advisory fix + KEK rotation)" ``` --- ## Task 2 — MxAccessGateway: bump `0.1.2` → `0.2.0` **Classification:** trivial **Estimated implement time:** ~2 min **Parallelizable with:** Task 1, Task 3, Task 4 **Files:** - Modify: `~/Desktop/MxAccessGateway/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj:20-22` **Steps:** 1. Set all three inline versions to `0.2.0` (`ZB.MOM.WW.Secrets`, `.Abstractions`, `.Ui`). 2. Build: `cd ~/Desktop/MxAccessGateway && dotnet build src/MxGateway.sln` → **Expected:** 0 warnings. 3. Verify the transitive pin as in Task 1 step 3. 4. Test: `dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj` → **Expected:** green (no MXAccess needed — fake worker). 5. Commit with the same message shape as Task 1. --- ## Task 3 — OtOpcUa: bump `0.1.2` → `0.2.0` (CPM) **Classification:** trivial **Estimated implement time:** ~2 min **Parallelizable with:** Task 1, Task 2, Task 4 **Files:** - Modify: `~/Desktop/OtOpcUa/Directory.Packages.props:135-137` **Steps:** 1. OtOpcUa **is** on Central Package Management — edit the three `` rows to `0.2.0`. Consuming csprojs carry bare `` with no `Version`; **do not** add versions there. 2. Build: `cd ~/Desktop/OtOpcUa && dotnet build ZB.MOM.WW.OtOpcUa.slnx` → **Expected:** 0 warnings (repo builds under `TreatWarningsAsErrors`). 3. Verify the transitive pin as in Task 1 step 3. 4. Test: `dotnet test ZB.MOM.WW.OtOpcUa.slnx` → **Expected:** green. 5. Commit. Branch: `feat/secrets-0.2.0` off `origin/master` (**OtOpcUa's default is `master`**). --- ## Task 4 — ScadaBridge: bump `0.1.2` → `0.2.0` (CPM) **Classification:** trivial **Estimated implement time:** ~2 min **Parallelizable with:** Task 1, Task 2, Task 3 **Files:** - Modify: `~/Desktop/ScadaBridge/Directory.Packages.props:89-91` **Steps:** 1. Edit the three `` rows to `0.2.0`. 2. Build: `cd ~/Desktop/ScadaBridge && dotnet build ZB.MOM.WW.ScadaBridge.slnx` → **Expected:** 0 warnings. 3. Verify the transitive pin as in Task 1 step 3. 4. Test: `dotnet test ZB.MOM.WW.ScadaBridge.slnx` **Expected:** green except the **known pre-existing** AngleSharp `NU1902` break in `CentralUI.Tests`. Confirm it predates the bump; do not fix it here. 5. Commit. Branch: `feat/secrets-0.2.0` off `origin/main`. --- > **Checkpoint — review after Tasks 1–4.** All four apps are on `0.2.0` and the advisory is closed family-wide. This is a coherent, independently shippable slice: FF-merge and push each repo before starting Task 5. If the topology work is deferred, the plan still delivered its security value. --- ## Task 5 — OtOpcUa: add the Akka replicator package **Classification:** small **Estimated implement time:** ~3 min **Parallelizable with:** Task 7 (different repo) **Depends on:** Task 3 **Files:** - Modify: `~/Desktop/OtOpcUa/Directory.Packages.props` (ZB.MOM block, beside the rows edited in Task 3) - Modify: `~/Desktop/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj` **Steps:** 1. Add the CPM version row: ```xml ``` 2. Add a bare reference in the **Host** csproj only (the Host owns the actor system and the secrets registration; driver projects keep referencing only `.Abstractions`): ```xml ``` 3. Restore: `dotnet restore ZB.MOM.WW.OtOpcUa.slnx` **Expected:** resolves from `dohertj2-gitea` via the existing `ZB.MOM.WW.Secrets.*` mapping. It pulls `Akka.Cluster.Tools 1.5.62` — **the exact version OtOpcUa already pins**, so expect no downgrade/conflict. If NU1109 appears, stop: that means the Akka pins have drifted and the plan's premise needs rechecking. 4. Build → **Expected:** 0 warnings, nothing consumes the package yet. 5. Commit. --- ## Task 6 — OtOpcUa: wire Akka replication + shared-KEK config **Classification:** high-risk **Estimated implement time:** ~5 min **Parallelizable with:** none **Depends on:** Task 5 > **High-risk because** it changes which `ISecretStore` implementation the container resolves on **every** node, including driver-role nodes that have no auth/AdminUI. A mistake here surfaces as drivers failing to open sessions, not as a test failure. **Files:** - Modify: `~/Desktop/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs` (the existing unconditional `AddZbSecrets` call site added by the G-2 adoption) - Modify: `~/Desktop/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json` - Test: `~/Desktop/OtOpcUa/tests/.../Host.Tests/` — DI-resolution test (see step 4) **Steps:** 1. **Replace** the runtime registration — `AddZbSecretsAkkaReplication` calls `AddZbSecrets` internally, so calling both would double-register: ```csharp // was: builder.Services.AddZbSecrets(builder.Configuration, "Secrets"); builder.Services.AddZbSecretsAkkaReplication(builder.Configuration, "Secrets", "Secrets:Replication"); ``` Using: `ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.DependencyInjection`. **Leave the pre-host expander untouched** — it builds a throwaway container that must stay a plain local SQLite store. Replication is a *runtime* concern; the bootstrap provider runs before the actor system exists. 2. Merge the serializer HOCON into the actor-system config so replication messages use the package's `SerializerWithStringManifest` rather than falling back to a default serializer: ```csharp // where the Akka HOCON is assembled: config = config.WithFallback(AkkaSecretsReplication.SerializationConfig); ``` 3. Add the config block to `appsettings.json` beside the existing `Secrets` section: ```json "Secrets": { "Replication": { "AnnounceInterval": "00:00:30", "ActorName": "zb-secret-replication" } } ``` 4. **Write a DI-resolution test before wiring** (this is the class of defect that shipped in the library itself — every unit test passed because nothing ever built a container): ```csharp [Fact] public void Secrets_store_resolves_as_the_replicating_decorator() { // Build the Host's service collection, then: var store = provider.GetRequiredService(); Assert.IsType(store); // And the undecorated concrete store must still resolve — the decorator depends on it: Assert.NotNull(provider.GetRequiredService()); } ``` Run it first and confirm it **fails** before step 1's edit, passes after. 5. Build + full suite → **Expected:** 0 warnings, green. 6. **Verify the same KEK is on every node** before any multi-node run — per §3.1, use the digest check in `clustered-secrets.md` §0. Do not skip: a mismatch is silent until first resolve. 7. Commit. --- ## Task 7 — ScadaBridge: add the SQL-Server replicator package + hub schema **Classification:** standard **Estimated implement time:** ~4 min **Parallelizable with:** Task 5 (different repo) **Depends on:** Task 4 **Files:** - Modify: `~/Desktop/ScadaBridge/Directory.Packages.props` - Modify: `~/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Host/ZB.MOM.WW.ScadaBridge.Host.csproj` **Steps:** 1. Add CPM row + bare reference for `ZB.MOM.WW.Secrets.Replicator.SqlServer` @ `0.2.0`. 2. Restore + build → **Expected:** resolves via the existing mapping; pulls `Microsoft.Data.SqlClient 6.0.2`. ScadaBridge already uses SqlClient — **if NU1109 appears, the repo pins an older SqlClient and you must reconcile the pin before continuing.** Report rather than force. 3. **Do not hand-provision the hub schema.** `AddZbSecretsSqlServerReplication` registers `SqlServerHubMigrationHostedService`, which creates it idempotently at startup. The DB and a login with `CREATE TABLE`/`CREATE SCHEMA` rights must exist — see `clustered-secrets.md` for the provisioning SQL. 4. Commit. --- ## Task 8 — ScadaBridge: wire hub replication + per-node config **Classification:** high-risk **Estimated implement time:** ~5 min **Parallelizable with:** none **Depends on:** Task 7 > **High-risk because** it introduces a background sweep that writes to a shared central database from every site node, and because a wrong `Secrets:SqlServer:ConnectionString` on a site node fails at startup (eager `Validate()`), taking the node down. **Files:** - Modify: `~/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs` (existing `AddZbSecrets` call site) - Modify: `~/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Host/appsettings.json` - Modify: the per-node docker appsettings under `~/Desktop/ScadaBridge/docker/` **Steps:** 1. Replace the runtime registration (it calls `AddZbSecrets` internally — do not call both): ```csharp builder.Services.AddZbSecretsSqlServerReplication( builder.Configuration, "Secrets", "Secrets:SqlServer"); ``` 2. Config block — the connection string is itself a secret, so deliver it the way every other ScadaBridge connstr is delivered (a `${secret:}` token resolved by the pre-host expander, which runs *before* this registration binds): ```json "Secrets": { "SqlServer": { "ConnectionString": "${secret:sql/scadabridge/secrets-hub}", "SchemaName": "zbsecrets", "SyncInterval": "00:00:30", "SyncOnStartup": true } } ``` **Ordering gotcha:** seed `sql/scadabridge/secrets-hub` in the **local** store on every node first. The hub connstr cannot itself come from the hub — that is a bootstrap cycle. This is the one config value that must stay locally seeded. 3. Add the same DI-resolution test as Task 6 step 4, asserting `ReplicatingSecretStore`. 4. Build + suite → **Expected:** 0 warnings; green modulo the known AngleSharp break. 5. Verify the shared KEK across central + site nodes (§3.1). 6. Commit. --- ## Task 9 — Live convergence gate (both clustered apps) **Classification:** high-risk **Estimated implement time:** ~5 min (excluding rig setup) **Parallelizable with:** none **Depends on:** Task 6, Task 8 > This is the **G-2-class live gate** the G-7 design called for. The library's offline suite proves the algorithms (2-node in-process Akka cluster, 15 live SQL-Server tests); it cannot prove *this app's* wiring. Do not mark the topology adopted without it. **Steps — run per app:** 1. Stand up 2 nodes (OtOpcUa: docker-dev rig; ScadaBridge: central pair, then a site node against the central hub). 2. **Write on node A**, via the `/admin/secrets` UI or the `secret` CLI. 3. **Resolve on node B.** Expected: the value appears within one sweep/announce interval (~30 s). 4. **Delete on A → confirm the tombstone propagates to B** and B no longer resolves it. (Resurrection is the classic anti-entropy bug; this is the case that catches it.) 5. **Partition test** — the reason each topology was chosen: - OtOpcUa: stop node B's cluster link; confirm B still resolves its Layer-B driver secrets from local state; heal; confirm re-convergence. - ScadaBridge: stop the site node's route to the central hub; confirm it still resolves; restore; confirm the sweep re-converges both directions. 6. **Negative KEK control** (proves fail-closed rather than silent-wrong): point one node at a *different* KEK, confirm it fails closed with a `kek_id` mismatch rather than returning a wrong plaintext. Revert. 7. Record results in [`components/secrets/GAPS.md`](../../components/secrets/GAPS.md) — including anything that did **not** pass. --- ## Task 10 — Docs reconcile **Classification:** small **Estimated implement time:** ~3 min **Parallelizable with:** none **Depends on:** Task 9 **Files:** - Modify: `components/secrets/GAPS.md` (close the two Open items: version bump; topology adoption) - Modify: `CLAUDE.md` (Secrets row: consumer pins `0.1.2` → `0.2.0`; record the adopted topology per app) - Modify: `components/secrets/current-state/*/CURRENT-STATE.md` (per-app store/topology) **Steps:** State what was **proven live** vs what was only built. If Task 9's partition or KEK-negative steps were skipped for lack of a rig, say so explicitly — an unproven claim in these docs is the exact failure mode [`component-status-claims-are-optimistic`](../../components/secrets/GAPS.md) already warns about. --- ## Rollback Every task is a version/config change with a clean revert: - Tasks 1–4: revert the version rows; `0.1.2` is still on the feed. - Tasks 5–8: revert the `AddZbSecrets*Replication` call to plain `AddZbSecrets(config, "Secrets")` and drop the package reference. Local SQLite stores are untouched by replication — rows written while replication was on remain readable, since the KEK and schema are unchanged.