From 5ba80e49b740d50b3590cd50749bf01c32bd4fe6 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 1 Aug 2026 06:02:31 -0400 Subject: [PATCH] docs(quick-260731-upz): entropy-audit remediation backlog + tracker items - Adds F-13 (High) to the audit: the BIP-84 account PRIVATE key is imported into Bitcoin Core (bitcoin.rs:203 disable_private_keys=false, :229-231 wpkh(xprv/...)), so the spending key is persisted outside the Argon2 envelope in a wallet with an empty passphrase; the descriptors also carry no [fingerprint/derivation] key origin, so no hardware signer could ever use them. Found by tracing secret class (1) end-to-end - Fills the Remediation Backlog: R-00..R-15, prioritised severity x effort, each with the finding it closes, files, effort, and hardware gating; plus an explicit "not implemented here, and why" section - Records ARCHY-1 as APPLIED with the exact test evidence and an honest note that making the source explicit removes a future failure mode rather than repairing a past one - Wires the resulting open items into docs/UNIFIED-TASK-TRACKER.md in its existing tier/checkbox format: Tier 0 (cargo audit/deny CI, ceremony mnemonic input, a five-item hygiene batch, secrets.rs OsRng), Tier 1 (ISO fail-open first-boot secrets, Argon2 vs ADR-005, the on-node checklist), Tier 2 (the Critical unauthenticated seed RPCs, PSBT Phase 1, PSBT phases 2-7, seed-RPC transport confinement) Co-Authored-By: Claude Opus 5 (1M context) --- docs/UNIFIED-TASK-TRACKER.md | 138 ++++++++++++++++++ .../security/ENTROPY-SEED-AUDIT-2026-07-31.md | 126 +++++++++++++++- 2 files changed, 261 insertions(+), 3 deletions(-) diff --git a/docs/UNIFIED-TASK-TRACKER.md b/docs/UNIFIED-TASK-TRACKER.md index f92bf4d4..b673be70 100644 --- a/docs/UNIFIED-TASK-TRACKER.md +++ b/docs/UNIFIED-TASK-TRACKER.md @@ -62,8 +62,89 @@ those are marked ✅ below with the commit that did it, so we stop re-litigating installed on any of .116/.198/.228 to check directly; low priority until someone actually needs it installed. +- [ ] **Add `cargo audit` / `cargo deny` to CI, failing on duplicate `rand` majors** + (entropy audit R-05, finding F-07 — + `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`). `cargo-audit` is not installed + anywhere, so no RustSec check has ever run against this tree. Separately, + `cargo tree` shows **both** `rand 0.8.5` (direct, all first-party key generation) + and `rand 0.9.2` (transitive via `totp-rs` and `tungstenite 0.26.2`) resolved into + one binary. `rand 0.9.0` removed `ThreadRng` fork protection and the orchestrator + forks constantly, so a future bump must be visible rather than silent — add a + `bans` rule so the duplicate majors show up in CI, not in an incident. + +- [ ] **Harden the release signing ceremony's mnemonic input** (entropy audit R-08, + finding F-06). `ceremony gen` prints the release master mnemonic to **stdout** + (`core/archipelago/src/ceremony.rs:71-77`) and `load_release_root_key` prefers the + `RELEASE_MASTER_MNEMONIC` **environment variable** over stdin (`:157-160`) — both + leak into shell history, `/proc//environ`, tmux scrollback and terminal + recordings. This is the seed that derives the fleet release-root signing key, so a + leak means forged signed manifests fleet-wide. Make stdin/TTY the only supported + input for `sign`/`pubkey`; write `gen`'s output to a `0600` file rather than the + terminal. Small change, but schedule it deliberately — it is the signing ceremony. + +- [ ] **Small entropy-audit hygiene batch** (entropy audit R-09 – R-12, R-14 — + `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`). Five independent one-liners, + each closing a Low/Informational finding: + - Persist the CSPRNG-readiness verdict (`seed.rs:85-91`) as a durable structured + event, so any node can answer post-hoc "was the entropy pool ready when this seed + was born?" — the question Coldcard owners cannot answer today. + - Add a test asserting the `getrandom` crate uses the **blocking** syscall, making + `seed.rs:52-57`'s invariant mechanical instead of a comment. + - Clear `_seed_words` from `sessionStorage` on route-leave from onboarding, not only + on successful verify (`OnboardingSeedVerify.vue:251`), plus a wall-clock expiry + mirroring the server's 10-minute `MNEMONIC_TTL`. + - Replace `% charset.len()` in `totp.rs:305` with `SliceRandom::choose(&mut OsRng)`. + (No bias today — 32 divides 256 — but any future charset edit introduces one + silently. The audit refutes the research's claim that this is currently biased.) + - Comment `pickRandomIndices` (`OnboardingSeedVerify.vue:157`) to record that its + `Math.random()` picks a UX challenge, not key material, so the next auditor does + not re-derive that it is benign. + +- [ ] **Swap container `generated_secrets` to explicit `OsRng`** (entropy audit R-13, + finding F-10) — `random_hex`/`random_base64` in + `core/archipelago/src/container/secrets.rs:90-102` use `thread_rng()`. Correct on + `rand 0.8.5`, but the same implicit-source pattern as [ARCHY-1]. Two-line change. + **Currently blocked only on tree hygiene** — that file had another agent's + uncommitted work when the audit ran. + ## Tier 1 — Medium effort, unblocked +- [ ] **Fix the fail-open first-boot secret regeneration in the ISO** (entropy audit + R-02 + R-03, finding F-03 — `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`). + The installed rootfs is a **cached container export shared by every node** + (`image-recipe/_archived/build-auto-installer-iso.sh:717-726`, extracted at + `:2303`), and it bakes SSH host keys (via the `openssh-server` install at `:345`) + and a TLS keypair (`:463-469`). `archipelago-first-boot-secrets.service` correctly + regenerates both per device — but both branches are **fail-open** (`:1647`, + `:1659`) and `touch "$MARKER"` at `:1663` runs **unconditionally**, so a single + transient failure permanently leaves that node on the image-wide shared SSH host + key and TLS private key, with the failure visible only in a log file. Fix: + (a) set the marker only when both regenerations succeeded, so it retries next + boot; (b) surface the failure in the UI/doctor, not just the log; (c) strip the + baked keys from the rootfs tar so a failure degrades to "no key" rather than + "shared key". Needs an ISO rebuild and two fresh flashes to verify. + +- [ ] **Reconcile `Argon2::default()` with ADR-005** (entropy audit R-06, finding F-05). + ADR-005 states 64 MB / 3 iterations + (`docs/adr/005-chacha20-backup-encryption.md:31`); `Argon2::default()` in + argon2 0.5.3 is Argon2id at **19 MiB / t=2 / p=1**. Used at + `core/archipelago/src/seed.rs:249` and `:285`, `backup/identity.rs:38`/`:93`, + `backup/full.rs:618`/`:650`. Either raise the parameters behind a versioned + envelope **with a migration** (an existing `master_seed.enc` was encrypted under + the old parameters and will not decrypt under new ones) or amend the ADR to state + the real numbers. Do not change them silently. + +- [ ] **Run the on-node entropy verification checklist** (entropy audit R-15, §6 of + `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`). Everything in that section is + explicitly **UNVERIFIED** — it needs real hardware this environment cannot reach. + Highest value first: **C-3** (are SSH host-key and TLS fingerprints actually + different across two nodes flashed from the same ISO?) and **C-5** (the cross-node + same-ISO seed collision test — the empirical check that would have caught the + Coldcard defect). Also C-1 (`crng init done` vs seed-generation timestamp), C-2 + (`machine-id` uniqueness), C-4 (what the rootfs tar actually contains, run on the + build host), C-6 (is `/rpc/v1` reachable unauthenticated from the LAN). Use a + disposable node — C-5 overwrites node identity. + - [x] ~~immich → Quadlet migration~~ — investigated 2026-07-01, turned out already done: immich uses the same `install_stack_via_orchestrator` primitive as netbird/btcpay (`immich_stack_app_ids()` in `stacks.rs:690`), and is confirmed running as real @@ -86,6 +167,63 @@ those are marked ✅ below with the commit that did it, so we stop re-litigating ## Tier 2 — High effort, mostly unblocked (the actual next exit criteria) +- [ ] **🔴 Gate the unauthenticated seed RPCs** (entropy audit R-01, finding **F-01, + Critical** — `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`). `seed.generate`, + `seed.verify`, `seed.restore` and `seed.save-encrypted` are in + `UNAUTHENTICATED_METHODS` (`core/archipelago/src/api/rpc/middleware.rs:24-28`), + which skips session, RBAC **and** CSRF (`api/rpc/mod.rs:263`, `:295`, `:326`). + Neither handler checks whether onboarding is already complete + (`api/rpc/seed_rpc.rs:93-159`, `:226-305`), and `NodeIdentity::from_seed` + overwrites `node_key`, `nostr_secret` and the FIPS mesh key **unconditionally** + (`identity.rs:79-114`). There is no rate limit (`rate_limit.rs:60-97` has no + `seed.*` entry). The endpoint is proxied to the LAN over plaintext HTTP + (`image-recipe/configs/nginx-archipelago.conf:11`, `:165`, `:192`) and mesh peers + can reach it too (`server.rs:2080` asserts `/rpc/v1` passes the peer path filter). + Net: **one unauthenticated POST can take over or destroy a live node's identity**, + and `seed.restore` lets the attacker choose the mnemonic. The guard already exists + and is simply never called — `NodeIdentity::key_exists` (`identity.rs:117`). + Fix: bail when a node key exists and no onboarding mnemonic is pending; prefer + also gating on `auth_manager.is_onboarding_complete()`; add rate limits at + `auth.changePassword` strictness; narrow the peer path filter. Changes an + authentication boundary on a live fleet — **needs its own `/gsd-plan-phase` with a + federation re-verify**, not an opportunistic patch. + +- [ ] **PSBT-first signing: Phase 1 — move the Bitcoin private key out of Core** + (entropy audit R-04, finding **F-13** + `docs/security/PSBT-SIGNING-ARCHITECTURE.md` + §8 Phase 1). `handle_bitcoin_init_wallet_from_seed` passes + `disable_private_keys = false` (`core/archipelago/src/api/rpc/bitcoin.rs:203`) and + imports `wpkh(xprv/0/*)` / `wpkh(xprv/1/*)` (`:229-231`), so the BIP-84 account + **private** key is persisted in Bitcoin Core's `wallet.dat` — with an empty wallet + passphrase (`:205`) — in addition to the daemon's Argon2 envelope. The descriptors + also carry **no `[fingerprint/derivation]` key origin**, so no hardware signer + could ever locate its key even if the private key were removed. Fix: watch-only + (`disable_private_keys=true`) + xpub-with-origin descriptors + a migration that + verifies balance and UTXO parity **before** removing the old wallet. This is the + single highest-value change in the PSBT spec and unblocks every later phase. + Needs a node with real UTXO history to verify. + +- [ ] **PSBT-first signing: Phases 2-7 rollout** + (`docs/security/PSBT-SIGNING-ARCHITECTURE.md` §8) — the spec is written to be + consumed directly by `/gsd-plan-phase`, with per-phase goals, dependencies, + candidate requirements and hardware gating. Sequence: PSBT construct/export → + external-signer import + finalize → air-gap transport (BC-UR v2 primary, BBQr for + Coldcard, file fallback always) → `wsh(sortedmulti)` multisig on BIP-48 → LND + remote signing → hot-wallet spend limits and cold/warm/hot tiering. Two hard rules + the spec fixes in place: a channel-funding PSBT must **never** be self-broadcast + (funds can be lost), and no UI copy may imply a routing node's Lightning channel + keys are cold — they are necessarily hot. Phases 3-6 need real hardware. + +- [ ] **Confine the seed-bearing RPCs to loopback/TLS** (entropy audit R-07, finding + F-04 / [ARCHY-4]). The 24-word master mnemonic is returned to the browser over + JSON-RPC (`core/archipelago/src/api/rpc/seed_rpc.rs:147`, `:156-158`), held in + process memory under a 10-minute TTL (`:27`) and deliberately **not** cleared at + verify time (`:205-211`, with a documented and defensible rationale about client + retries) — over a transport that is plaintext HTTP on LAN by design + (`api/rpc/mod.rs:227-241`). Anyone with LAN traffic visibility during onboarding + reads the phrase that unlocks the wallet and the node identity. Fix: force TLS or + loopback for seed methods, shrink the TTL, and clear on an acknowledged verify + with a short grace window. Touches the onboarding transport — needs a phase. + - [~] **Multinode test pass** (`docs/multinode-testing-plan.md`) — worked the preconditions on .198 2026-07-01: - ✅ cleared 2 stale failed-unit records (`archy-mempool-db.service`, diff --git a/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md b/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md index 6f677892..43e323d1 100644 --- a/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md +++ b/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md @@ -159,6 +159,7 @@ action of the "your seed may be predictable, migrate now" kind is warranted — | F-10 | **Low** | Container `generated_secrets` use `thread_rng()` rather than an explicit `OsRng` (same T1 shape as F-02, smaller blast radius) | `core/archipelago/src/container/secrets.rs:92`, `:101` | | F-11 | **Informational** | `Math.random()` inside a seed-handling view (benign — UX challenge selection only) | `neode-ui/src/views/OnboardingSeedVerify.vue:159` | | F-12 | **Informational** | Identical default OS credentials on every flashed node | `image-recipe/archipelago-scripts/install-to-disk.sh:205` | +| F-13 | **High** | BIP-84 account **private** key is imported into Bitcoin Core's wallet, duplicating the spending key outside the encrypted envelope | `core/archipelago/src/api/rpc/bitcoin.rs:203`, `:229-231` | --- @@ -524,6 +525,55 @@ in scope to name. --- +### F-13 — BIP-84 account **private** key is imported into Bitcoin Core — **High** + +**Evidence.** `core/archipelago/src/api/rpc/bitcoin.rs:161-294` +(`handle_bitcoin_init_wallet_from_seed`): + +- `:203` passes `disable_private_keys = false` to `createwallet`. +- `:188-189` derives the BIP-84 account **xprv** (`crate::seed::derive_bitcoin_xprv`, + `core/archipelago/src/seed.rs:207-224`) and stringifies it. +- `:229-231` builds `wpkh({xprv}/0/*)` and `wpkh({xprv}/1/*)`. +- `:278-281` imports those descriptors via `importdescriptors`. + +**Assessment.** The node's Bitcoin spending key is therefore persisted **twice**: once in the +daemon's Argon2 + ChaCha20-Poly1305 envelope (`core/archipelago/src/seed.rs:238-269`, `0600` via +`:318-324`), and once in Bitcoin Core's `wallet.dat`, which has neither the Argon2 passphrase +protection nor the same ownership story — it lives in the Bitcoin Core container's data volume. +The wallet is created with an **empty** encryption passphrase (`bitcoin.rs:205`), so Core's own +wallet encryption is not engaged either. + +**Not an entropy defect**, and reported here because Step B required tracing secret class (1), +the user Bitcoin/LND wallet seed, from generation to consumer — and this is where that trace +ends up. + +**Credit where due:** the in-memory handling of the xprv string is careful — it is zeroized on +both the error path (`bitcoin.rs:222`) and the success path (`:284`) — and the wallet is a +*descriptor* wallet (`:207`), which is the correct foundation. The defect is which key goes +into it. + +**Secondary defect, same lines.** The descriptors at `:230-231` carry **no key-origin +annotation** (`[fingerprint/derivation]`). Without it, no hardware signer can locate its key in +a PSBT — so the current wallet could not be converted to an external-signer setup even if the +private key were removed. + +**Exploitability.** Requires read access to the Bitcoin Core data volume. That is a lower bar +than the encrypted envelope: a container escape, a backup of the Bitcoin volume, or a +misconfigured bind mount exposes it, whereas `master_seed.enc` additionally requires the user's +password. + +**Blast radius.** The node's entire on-chain Bitcoin balance at `m/84'/0'/0'`. It does **not** +extend to the other key classes — the release-root key, node identity and FIPS keys are HKDF +siblings, not children of the BIP-84 branch, so an attacker with the account xprv cannot climb +back to the master seed. + +**Remediation.** Pass `disable_private_keys = true`; import the account **xpub** with a +key-origin annotation instead of the xprv; sign via the daemon (or an external signer) rather +than via Core. This is Phase 1 of `docs/security/PSBT-SIGNING-ARCHITECTURE.md` §8, including +the migration that verifies balance/UTXO parity before removing the private-key-bearing wallet. + +--- + ## 4. [ARCHY-1] … [ARCHY-4] adjudication ### [ARCHY-1] — **CONFIRMED** @@ -785,9 +835,41 @@ every install. --- -## 7. `ARCHY-1` remediation status +## 7. `ARCHY-1` remediation status — **APPLIED** - +The injectable-RNG-seam refactor described in F-02 was applied to +`core/archipelago/src/seed.rs`. Scope, precisely: + +- A new private helper `generate_mnemonic_with(rng: &mut R)` + calls `bip39::Mnemonic::generate_in_with(rng, Language::English, 24)` — the **injectable** + bip39 entry point — instead of the defaulting `Mnemonic::generate(24)`. +- `MasterSeed::generate()` passes `&mut rand::rngs::OsRng` explicitly. +- A doc comment at the helper pins the rationale to this audit and to T1, so a future + `rand`/`bip39` bump cannot rebind the entropy source without someone reading why it matters. +- Two tests added to the existing module. + +**Nothing else changed.** The derivation paths, the 24-word count, the empty-BIP-39-passphrase +decision, the at-rest encryption envelope, and every existing test are untouched. + +**Tests.** `cd core && CARGO_INCREMENTAL=0 cargo test -p archipelago seed::` → +**25 passed, 0 failed.** + +- `mnemonic_generation_uses_injected_rng` — drives generation from a deterministic + `CryptoRng + RngCore` test RNG and asserts (a) the result equals + `bip39::Mnemonic::from_entropy()`, which is the direct proof + that the **injected** RNG — not bip39's transitive `rand::thread_rng()` default — is the one + actually consumed; (b) a known-answer word list; (c) determinism across two identical RNG + states. **This test is impossible to write against the pre-change code**, because + `Mnemonic::generate(24)` exposes no seam through which the RNG can be observed or substituted. +- `mnemonic_generation_is_256_bit` — the production `OsRng` path yields 24 words and two + successive productions differ. + +**The residual risk this does not close.** Making the source explicit does not make the *fix* +retroactive: mnemonics generated before this change came from `rand::thread_rng()`. That was +and remains a genuine CSPRNG (F-02, "Exploitability: none today"), so no existing seed is +weakened — but the guarantee for those seeds rests on `rand 0.8.5`'s behaviour, not on this +call site. Reviewers should read this as *removing a future failure mode*, not as repairing a +past one. Everything else in this document is queued in §8, not implemented. @@ -795,7 +877,45 @@ Everything else in this document is queued in §8, not implemented. ## 8. Remediation Backlog - +Prioritised by severity × effort, most valuable per unit of work first. **Only R-00 was +implemented by this audit.** Everything else is queued here and mirrored into +`docs/UNIFIED-TASK-TRACKER.md` so it is not stranded in a document nobody re-reads. + +**Effort scale:** S = under an hour; M = half a day; L = a day or more; **PHASE** = needs its +own `/gsd-plan-phase`, not an opportunistic edit. + +| # | Closes | Change | Files | Effort | Hardware? | +|---|---|---|---|---|---| +| **R-00** | F-02 / [ARCHY-1] | **DONE in this audit.** Route mnemonic generation through an injectable-RNG helper; production passes `OsRng`; known-answer test proves the injected RNG is consumed | `core/archipelago/src/seed.rs` | S | no | +| **R-01** | **F-01 (Critical)** | Gate `seed.generate` / `seed.restore` on onboarding being incomplete; add rate limits; narrow the mesh peer path filter | `core/archipelago/src/api/rpc/seed_rpc.rs`, `.../middleware.rs`, `.../rate_limit.rs`, `server.rs` | M | yes (re-onboard + federation re-verify) | +| **R-02** | F-03 (High) | Move `touch "$MARKER"` inside a both-succeeded branch so a failed regeneration retries next boot; surface the failure beyond a log file | `image-recipe/_archived/build-auto-installer-iso.sh` | S | **yes** (ISO rebuild + fresh flash) | +| **R-03** | F-03 (High) | Strip baked SSH host keys and the TLS keypair from the rootfs tar at build time, so a regeneration failure degrades to "no key" not "shared key" | `image-recipe/_archived/build-auto-installer-iso.sh` | M | **yes** | +| **R-04** | **F-13 (High)** | `disable_private_keys=true`; import the **xpub** with a `[fingerprint/derivation]` key origin; migrate with balance/UTXO parity verification | `core/archipelago/src/api/rpc/bitcoin.rs` | **PHASE** | **yes** (node with real UTXO history) | +| **R-05** | F-07 (Medium) | Add `cargo audit` / `cargo deny check advisories bans` to CI, with a `bans` rule failing on duplicate `rand` majors | CI config | S | no | +| **R-06** | F-05 (Medium) | Reconcile `Argon2::default()` (19 MiB / t=2) with ADR-005 (64 MB / 3) — either raise the params behind a versioned envelope with a migration, or amend the ADR | `core/archipelago/src/seed.rs`, `backup/full.rs`, `backup/identity.rs`, `docs/adr/005-...` | M | no | +| **R-07** | F-04 (Medium) | Confine seed-bearing RPCs to loopback/TLS; shrink `MNEMONIC_TTL`; clear on acknowledged verify with a short grace window | `core/archipelago/src/api/rpc/seed_rpc.rs`, `.../mod.rs`, nginx config | **PHASE** | yes | +| **R-08** | F-06 (Medium) | Make stdin/TTY the only mnemonic input for `ceremony sign`/`pubkey`; stop printing the mnemonic to stdout in `ceremony gen` | `core/archipelago/src/ceremony.rs` | S | no (but schedule deliberately — it is the signing ceremony) | +| **R-09** | [ARCHY-2] hardening | Persist the CSPRNG-readiness verdict as a durable structured event, so any node can answer post-hoc "was the pool ready when this seed was born?" | `core/archipelago/src/seed.rs` | S | no | +| **R-10** | [ARCHY-2] hardening | Add a test asserting the `getrandom` crate uses the **blocking** syscall, so the invariant is mechanical rather than a comment | `core/archipelago/src/seed.rs` | S | no | +| **R-11** | F-08 (Low) | Clear `_seed_words` on route-leave from onboarding, not only on successful verify; add a wall-clock expiry mirroring `MNEMONIC_TTL` | `neode-ui/src/views/OnboardingSeedGenerate.vue`, `OnboardingSeedVerify.vue` | S | no | +| **R-12** | F-09 (Low) | Replace `% charset.len()` with `SliceRandom::choose(&mut OsRng)` and pin the uniformity property with a test | `core/archipelago/src/totp.rs` | S | no | +| **R-13** | F-10 (Low) | Swap `random_hex` / `random_base64` from `thread_rng()` to explicit `OsRng` | `core/archipelago/src/container/secrets.rs` | S | no — **blocked**: file has another agent's uncommitted work | +| **R-14** | F-11 (Informational) | One-line comment at `pickRandomIndices` recording that the `Math.random()` is a UX challenge selector, not key material | `neode-ui/src/views/OnboardingSeedVerify.vue` | S | no | +| **R-15** | §6 checklist | Run the on-node verification checklist — especially C-3 (per-node SSH/TLS keys) and C-5 (cross-node collision test) | — | M | **yes** (2+ nodes from one ISO) | + +### Explicitly NOT implemented in this task, and why + +- **R-01, R-04, R-07** need their own phase. R-01 changes an authentication boundary on a live + fleet; R-04 moves the spending key out of a wallet holding real funds; R-07 changes the + onboarding transport. Each needs a migration story and real-node verification that an + audit-and-spec task cannot provide. +- **R-02, R-03** require rebuilding the ISO and flashing at least two machines to verify. Not + reachable from this environment. +- **R-13** is blocked purely by tree hygiene: `core/archipelago/src/container/secrets.rs` had + another agent's uncommitted changes at audit time and this plan's invariant is that no commit + it authors touches their files. Trivial once that work lands. +- **The whole of `docs/security/PSBT-SIGNING-ARCHITECTURE.md`** is a rollout, not a fix. It is + queued as a spec for `/gsd-plan-phase`, not implemented anywhere. ---