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:
Configtrait +#[derive(Config)]macroConfigManagerwith ordered source chain- Five
ConfigSourceimplementations:EnvSource— readsHARMONY_CONFIG_{KEY}env varsLocalFileSource— reads/writes{key}.jsonfiles from a directorySqliteSource— NEW reads/writes to SQLite databasePromptSource— returnsNone/ no-op on set (placeholder for TUI integration)StoreSource<S: SecretStore>— wraps anyharmony_secret::SecretStorebackend
- 24 unit tests (mock source, env, local file, sqlite, prompt, integration)
- Global
CONFIG_MANAGERstatic withinit(),get(),get_or_prompt(),set() - Two examples:
basicandpromptinginharmony_config/examples/ - Zero workspace consumers — nothing calls
harmony_configyet
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
sqlxwith SQLite runtime SqliteSource::open(path)- opens/creates database at given pathSqliteSource::default()- uses default Harmony data directory
Files:
harmony_config/src/source/sqlite.rs- new fileharmony_config/Cargo.toml- addedsqlx = { workspace = true, features = ["runtime-tokio", "sqlite"] }Cargo.toml- addedanyhow = "1.0"to workspace dependencies
Tests (all passing):
test_sqlite_set_and_get— round-trip aTestConfigstructtest_sqlite_get_returns_none_when_missing— key not in DBtest_sqlite_overwrites_on_set— set twice, get returns latesttest_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:
-
harmony_config/examples/basic.rs- demonstrates:- Zero-setup SQLite backend (auto-creates directory)
- Using the
#[derive(Config)]macro - Environment variable override (
HARMONY_CONFIG_TestConfigoverrides SQLite) - Direct set/get operations
- Persistence verification
-
harmony_config/examples/prompting.rs- demonstrates:- Config with no defaults (requires user input via
inquire) get()flow: env > sqlite > prompt fallbackget_or_prompt()for interactive configuration- Full resolution chain
- Persistence of prompted values
- Config with no defaults (requires user input via
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()returnsfalse- shouldn't persist prompted values to env varsPromptSource::should_persist()returnsfalse- doesn't persist anywayget_or_prompt()now skips sources whereshould_persist()isfalse
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_nonetest_prompt_source_set_is_nooptest_prompt_source_does_not_persisttest_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 sqlitetest_full_resolution_chain_env_overrides_sqlite— env set, sqlite has value, get() returns envtest_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 postgresqlwith no env vars → prompts for nothing- An example that uses
SecretManagertoday (e.g.,brocade_snmp_server) → when migrated toharmony_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
SqliteSourceimplementation with tests- Functional
PromptSourcewithshould_persist()design - Fix
get_or_promptto persist to first writable source (viashould_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
-
SQLite path:
~/.local/share/harmony/config/config.db(not~/.local/share/harmony/config.db) -
Auto-create directory:
SqliteSource::open()creates parent directories if they don't exist -
Default path:
SqliteSource::default()usesdirectories::ProjectDirsto find the correct data directory -
Env var precedence: Environment variables always take precedence over SQLite in the resolution chain
-
Testing: All tests use
tempfile::NamedTempFilefor temporary database paths, ensuring test isolation