# Design: `secret` interactive console (ZB.MOM.WW.Secrets CLI, 0.3.0) **Date:** 2026-07-19 · **Status:** Approved · **Execution:** git worktree `worktree-secrets-interactive-cli` (another agent is active on `main`) ## 1. Purpose & posture Extend the existing headless `secret` CLI (`src/ZB.MOM.WW.Secrets.Cli`) with a menu-driven Spectre.Console TUI for two operator jobs: 1. **Deployment setup** — seed a deployment's secrets before first startup (apps fail closed when a `${secret:...}` reference is missing). 2. **Lockout recovery** — diagnose and fix wrong/lost-KEK and missing-secret states. No authentication: the tool only runs locally with direct file/DB access — the same trust model as the existing headless verbs. All existing verbs stay byte-identical for scripts. ## 2. Entry point `secret` with **no args in a real terminal** (`!Console.IsInputRedirected && !Console.IsOutputRedirected`) launches the TUI. Redirected I/O keeps today's usage-and-exit-2 behavior, so nothing scriptable changes. All other `Program.cs` paths are untouched. ## 3. Target selection & sessions First screen picks a **store target**: - **Recent targets** — persisted to `~/.zb-secrets/recent-targets.json` (names and paths only; NEVER key material). - **Browse** to an app's `appsettings.json`. - **Manual entry** — db path + KEK source. `TargetConfigReader` loads the app's real config the way the app would (`appsettings.json` + optional `appsettings..json` overlay + environment variables; section `Secrets` by default, overridable) and binds `SecretsOptions` (+ `SqlServerSecretsOptions` when present) — operating on exactly what the app will read at startup, so a fix cannot land in the wrong store. `SecretsSessionFactory` constructs the stack directly — `SecretsSqliteConnectionFactory` / `SqlServerSecretStore`, `MasterKeyProviderFactory`, `AesGcmEnvelopeCipher`, the matching migrator — no per-target DI host. If the configured KEK env var isn't set in the CLI's shell (common: it exists only in the service's environment), the session opens **degraded**: metadata ops work; decrypt/seal ops prompt for the key via masked input or file path, held in memory only. Dependency changes to `ZB.MOM.WW.Secrets.Cli`: add `Spectre.Console` and a project reference to `ZB.MOM.WW.Secrets.Replicator.SqlServer` (to open SQL-hub stores such as ScadaBridge central). ## 4. Menu actions | Action | Behavior | |---|---| | List / set-rotate / get / delete | Thin TUI wrappers over the same core seams `SecretCommands` uses. Masked value entry; tables for listings; **get** reveals once after confirm, never cached. | | **Reference audit + seeding** | Scan the target app's fully-composed config for `${secret:NAME}` tokens (same regex as `SecretReferenceExpander`); report each OK / missing / tombstoned / undecryptable; walk through seeding every non-OK one with masked prompts. This is the deployment-setup flow. | | **KEK doctor** | Verify the session KEK against the store: per-row KekId match + actual DEK-unwrap probe; report row classes (OK / wrong-KEK / corrupt). Guided remedies: `rewrap-all` (old key from env/file/masked paste, reusing `KekRotationService`) or re-set affected secrets when the old KEK is truly lost. This is the lockout flow. | | **Bundle export / import** | Export rows as ciphertext-only JSON (name, metadata, wrapped DEK, kekId, ciphertext — safe at rest, useless without the KEK). Import into another store; conflicts resolved by core `SecretLastWriterWins` with per-row override prompt. Cross-KEK import offers inline rewrap given the source KEK. | | Switch target / quit | Rebuild session for a new target; quit exits 0. | ## 5. Structure New `Interactive/` layer inside `ZB.MOM.WW.Secrets.Cli`: - `InteractiveShell` — menu loop; the ONLY class that touches `IAnsiConsole`. - UI-free services: `TargetConfigReader`, `SecretsSessionFactory`, `ReferenceAuditor`, `KekDoctor`, `SecretBundleCodec` + import/export service, `RecentTargetsStore`. Services never touch the console; the shell never touches the store directly. Secret values are never written to recents, bundles (plaintext), logs, or screen except the explicit reveal action. ## 6. Error handling Every action catches `SecretDecryptionException` / `ArgumentException` / IO / SQL failures and renders a red panel with a remedy hint (usually "run KEK doctor"), returning to the menu — the shell never crashes out mid-recovery. Ctrl-C at a prompt cancels back to the menu; at the menu it exits 0. ## 7. Testing - Unit tests for all services in `tests/ZB.MOM.WW.Secrets.Tests/Cli/` (temp SQLite stores, fake KEK providers — same style as existing tests). - Shell flows scripted via `Spectre.Console.Testing`'s `TestConsole`: menu navigation, masked entry, degraded-KEK path, audit→seed walk-through, doctor→rewrap. - Pin test: non-TTY no-args still prints usage / exit 2. - Suite stays 0-warning (`TreatWarningsAsErrors` posture of the repo). ## 8. Versioning & execution - `Directory.Build.props` version bumps `0.2.3` → `0.3.0` (new feature; CLI is unpacked but the 5 packages ship together — publish decision deferred to rollout). - Implemented on worktree branch `worktree-secrets-interactive-cli`; merge to `main` when the full suite is green. ## Alternatives considered - **Separate `ZB.MOM.WW.Secrets.Cli.Interactive` library** — cleaner layering if another front-end ever hosts the flows; rejected as YAGNI for a one-exe operator tool. - **Prompts inside `SecretCommands`** — least code but couples UI into the tested headless layer and breaks its TextWriter-only contract; rejected. - **REPL shell / wizards-only UX** — rejected in favor of discoverable menus (operators under lockout stress shouldn't need to remember verbs). - **Own profiles file as source of truth** — rejected: can drift from what the app actually reads; parsing the app's own config is the honest source.