# 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.<Environment>.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.
