Files
harmony/ROADMAP/01-config-crate.md
Jean-Gabriel Gill-Couture 6a57361356
Some checks failed
Run Check Script / check (pull_request) Failing after 12s
chore: Update config roadmap
2026-03-22 19:04:16 -04:00

6.7 KiB

Phase 1: Harden harmony_config, Validate UX, Zero-Setup Starting Point

Goal

Make harmony_config production-ready with a seamless first-run experience: clone, run, get prompted, values persist locally. Then progressively add team-scale backends (OpenBao, Zitadel SSO) without changing any calling code.

Current State

harmony_config now has:

  • Config trait + #[derive(Config)] macro
  • ConfigManager with ordered source chain
  • Five ConfigSource implementations:
    • EnvSource — reads HARMONY_CONFIG_{KEY} env vars
    • LocalFileSource — reads/writes {key}.json files from a directory
    • SqliteSourceNEW reads/writes to SQLite database
    • PromptSource — returns None / no-op on set (placeholder for TUI integration)
    • StoreSource<S: SecretStore> — wraps any harmony_secret::SecretStore backend
  • 24 unit tests (mock source, env, local file, sqlite, prompt, integration)
  • Global CONFIG_MANAGER static with init(), get(), get_or_prompt(), set()
  • Two examples: basic and prompting in harmony_config/examples/
  • Zero workspace consumers — nothing calls harmony_config yet

Tasks

1.1 Add SqliteSource as the default zero-setup backend

Status: Implemented

Implementation Details:

  • Database location: ~/.local/share/harmony/config/config.db (directory is auto-created)
  • Schema: config(key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))
  • Uses sqlx with SQLite runtime
  • SqliteSource::open(path) - opens/creates database at given path
  • SqliteSource::default() - uses default Harmony data directory

Files:

  • harmony_config/src/source/sqlite.rs - new file
  • harmony_config/Cargo.toml - added sqlx = { workspace = true, features = ["runtime-tokio", "sqlite"] }
  • Cargo.toml - added anyhow = "1.0" to workspace dependencies

Tests (all passing):

  • test_sqlite_set_and_get — round-trip a TestConfig struct
  • test_sqlite_get_returns_none_when_missing — key not in DB
  • test_sqlite_overwrites_on_set — set twice, get returns latest
  • test_sqlite_concurrent_access — two tasks writing different keys simultaneously

1.1.1 Add Config example to show exact DX and confirm functionality

Status: Implemented

Examples created:

  1. harmony_config/examples/basic.rs - demonstrates:

    • Zero-setup SQLite backend (auto-creates directory)
    • Using the #[derive(Config)] macro
    • Environment variable override (HARMONY_CONFIG_TestConfig overrides SQLite)
    • Direct set/get operations
    • Persistence verification
  2. harmony_config/examples/prompting.rs - demonstrates:

    • Config with no defaults (requires user input via inquire)
    • get() flow: env > sqlite > prompt fallback
    • get_or_prompt() for interactive configuration
    • Full resolution chain
    • Persistence of prompted values

1.2 Make PromptSource functional

Status: Implemented with design improvement

Key Finding - Bug Fixed During Implementation:

The original design had a critical bug in get_or_prompt():

// OLD (BUGGY) - breaks on first source where set() returns Ok(())
for source in &self.sources {
    if source.set(T::KEY, &value).await.is_ok() {
        break;
    }
}

Since EnvSource.set() returns Ok(()) (successfully sets env var), the loop would break immediately and never write to SqliteSource. Prompted values were never persisted!

Solution - Added should_persist() method to ConfigSource trait:

#[async_trait]
pub trait ConfigSource: Send + Sync {
    async fn get(&self, key: &str) -> Result<Option<serde_json::Value>, ConfigError>;
    async fn set(&self, key: &str, value: &serde_json::Value) -> Result<(), ConfigError>;
    fn should_persist(&self) -> bool {
        true
    }
}
  • EnvSource::should_persist() returns false - shouldn't persist prompted values to env vars
  • PromptSource::should_persist() returns false - doesn't persist anyway
  • get_or_prompt() now skips sources where should_persist() is false

Updated get_or_prompt():

for source in &self.sources {
    if !source.should_persist() {
        continue;
    }
    if source.set(T::KEY, &value).await.is_ok() {
        break;
    }
}

Tests:

  • test_prompt_source_always_returns_none
  • test_prompt_source_set_is_noop
  • test_prompt_source_does_not_persist
  • test_full_chain_with_prompt_source_falls_through_to_prompt

1.3 Integration test: full resolution chain

Status: Implemented

Tests:

  • test_full_resolution_chain_sqlite_fallback — env not set, sqlite has value, get() returns sqlite
  • test_full_resolution_chain_env_overrides_sqlite — env set, sqlite has value, get() returns env
  • test_branch_switching_scenario_deserialization_error — old struct shape in sqlite returns Deserialization error

1.4 Validate Zitadel + OpenBao integration path

Status: Not yet implemented

Remaining work:

  • Validate that ConfigManager::new(vec![EnvSource, SqliteSource, StoreSource<Openbao>]) compiles
  • When OpenBao is unreachable, chain falls through to SQLite gracefully
  • Document target Zitadel OIDC flow as ADR

1.5 UX validation checklist

Status: Partially complete - manual verification needed

  • cargo run --example postgresql with no env vars → prompts for nothing
  • An example that uses SecretManager today (e.g., brocade_snmp_server) → when migrated to harmony_config, first run prompts, second run reads from SQLite
  • Setting HARMONY_CONFIG_BrocadeSwitchAuth='{"host":"...","user":"...","password":"..."}' → skips prompt, uses env value
  • Deleting ~/.local/share/harmony/config/ directory → re-prompts on next run

Deliverables

  • SqliteSource implementation with tests
  • Functional PromptSource with should_persist() design
  • Fix get_or_prompt to persist to first writable source (via should_persist()), not all sources
  • Integration tests for full resolution chain
  • Branch-switching deserialization failure test
  • StoreSource<OpenbaoSecretStore> integration validated (compiles, graceful fallback)
  • ADR for Zitadel OIDC target architecture
  • Update docs to reflect final implementation and behavior

Key Implementation Notes

  1. SQLite path: ~/.local/share/harmony/config/config.db (not ~/.local/share/harmony/config.db)

  2. Auto-create directory: SqliteSource::open() creates parent directories if they don't exist

  3. Default path: SqliteSource::default() uses directories::ProjectDirs to find the correct data directory

  4. Env var precedence: Environment variables always take precedence over SQLite in the resolution chain

  5. Testing: All tests use tempfile::NamedTempFile for temporary database paths, ensuring test isolation