feat(fnox): library-mode FnoxClient behind fnox-library cargo feature#45
Closed
bglusman wants to merge 3 commits intointegration/super-combinedfrom
Closed
feat(fnox): library-mode FnoxClient behind fnox-library cargo feature#45bglusman wants to merge 3 commits intointegration/super-combinedfrom
bglusman wants to merge 3 commits intointegration/super-combinedfrom
Conversation
The user pushed back on the original "subprocess-only" design — they were right that wrapping the binary's config-loading dance in our client and using the fnox library directly is feasible AND better. Earlier conclusion that "fnox needs git dep, not on crates.io" was also wrong — it's on crates.io as 1.21.0 (matches git tip). This commit lands library mode behind a feature flag so we can validate the design without imposing the deps cost on consumers that don't need fnox at all. Surface: - New `FnoxLibrary` type with same get/list semantics as the subprocess `FnoxClient` - `discover()` walks up looking for fnox.toml (matches binary) - `with_profile`/`with_timeout` builders - `set` deliberately not implemented in this cut — fnox's SetCommand::run is ~100 LOC of provider/encryption logic that's the bulk of what should land as the upstream PR (see fnox_library.rs module doc). Subprocess FnoxClient still services set. Build cost when feature on: ~30 transitive crates (AWS SDK, GCP SDK, keyring, age) + 1m 39s cold workspace build. When feature off: zero deps cost, FnoxLibrary is shimmed to a NotInstalled error so callers can compile-test the type without enabling fnox. Smoke tested: - `cargo run --features fnox-library --example fnox_library_smoke` successfully loads fnox.toml + lists declared secrets in the default profile, sharing state with the subprocess CLI. Next: upstream PR/issue to fnox proposing a `Fnox::discover()` → `fnox.get/set/list` convenience API, so downstream consumers can stop replicating the binary's orchestration.
Owner
Author
|
Upstream PR open: jdx/fnox#442 (jdx approved the design in discussion #441). Once it lands, the body of |
The convenience API our discussion #441 / PR #442 proposed has landed on our fork branch. Switch the dep to the fork and refactor FnoxLibrary from "replicate the binary's orchestration" to "thin error-coercion shim over fnox::Fnox". Cargo.toml: fnox dep is now git = bglusman/fnox branch = feat/library-convenience-api. One-line edit when jdx merges + tags 1.22+ — back to fnox = "1.22" from crates.io. Implementation shrinks from ~150 LOC of CLI orchestration to ~50: - discover walk → fnox::Fnox::discover() (full merge chain) - profile resolution → fnox::Config::get_profile(None) - secret config lookup → fnox::Fnox::get(key) (no IndexMap clone) - decl list → fnox::Fnox::list() (sync) - error → FnoxError::SecretNotFound (matches binary contract) API surface kept minimal: - new() — discover from CWD - with_root(dir) — pin a specific dir for tests/daemons - with_profile(p) — override (None defers to FNOX_PROFILE) - get(name) → Result<String, FnoxError> - list() → Result<Vec<String>, FnoxError> set still NOT implemented — follows the same pattern as upstream PR #442 which deferred set to a follow-up. Subprocess FnoxClient services set in the meantime. Smoke tested: cargo run --features fnox-library --example fnox_library_smoke loads fnox.toml + lists declared secrets in the active profile.
Picks up upstream PR v3: - Doc example fix (fnox.list()?) - get() docstring covers both ignore + warn None cases - SecretNotFound.suggestion populated via crate::suggest Smoke test (fnox_library_smoke example) still passes.
Owner
Author
jdx
pushed a commit
to jdx/fnox
that referenced
this pull request
Apr 26, 2026
#442) Per [discussion #441](#441) — "sounds fine" 👍 ## What Adds a top-level `Fnox` struct in a new `src/library.rs` module so downstream library consumers can write three lines instead of replicating `GetCommand::run`'s boilerplate. ```rust use fnox::Fnox; let fnox = Fnox::discover()?; // walks up + merges parent + local + global let value = fnox.get("MY_KEY").await?; let names = fnox.list()?; ``` ## Final API surface (after v3 fixes) - `Fnox::discover()` — `Config::load_smart(CONFIG_FILENAME)` (full upward-search + parent-merge + local-override + global merge chain) - `Fnox::open(path)` — explicit config path - `Fnox::with_profile(profile)` — builder for non-default profile - `Fnox::profile()` / `Fnox::config()` — accessors - `Fnox::get(key) -> Option<String>` — resolves via `secret_resolver::resolve_secret`. Errors with `FnoxError::SecretNotFound { key, profile, config_path, suggestion }` (suggestion populated via `crate::suggest` → "Did you mean DATABASE_URL?") - `Fnox::list() -> Vec<String>` — declared secret names, declaration order, **sync** `Config` is held behind `Arc` — `Fnox: Clone` is cheap. Re-exported as `fnox::Fnox` from `lib.rs`. ## Removed from earlier drafts - `Fnox::discover_from(start)` — couldn't be implemented safely without a process-global `set_current_dir` violating AGENTS.md. Removed in v2. ## What's NOT here `set()` — `SetCommand::run` is ~100 LOC of provider/encryption/remote-storage branching. Substantial enough to warrant its own design pass and PR. Happy to follow up if the API shape here lands cleanly. ## Tests 8 in `library::tests`: - `open_loads_explicit_path` / `open_errors_when_path_missing` - `list_returns_declared_secrets_in_declaration_order` - `get_returns_default_value_when_no_provider` - `get_errors_with_secret_not_found_when_key_undeclared` - `get_secret_not_found_carries_did_you_mean_suggestion` (regression for the suggestion field) - `with_profile_routes_list_to_named_profile` - `clone_does_not_deep_copy_config` (asserts Arc sharing, not deep copy) Full `cargo test --lib`: **143 passed** (135 existing + 8 new), 0 failed. ## Review fixes (v1 → v2 → v3) v2 (commit 79f1a81) — addressed v1 review: - HIGH: `discover_from` was bypassing load_smart's merge chain. Removed. - MED: `list()` made sync; `Fnox` holds `Arc<Config>`; `get()` uses `Config::get_secret` not full clone; profile via `Config::get_profile(None)` (honors `FNOX_PROFILE`); error is `SecretNotFound` not `Config(...)`. v3 (commit c17f24a) — addressed v2 review: - P1: doc example used `fnox.list()` without `?` — fixed. - MED: `get()` docstring tightened to mention both `if_missing = ignore | warn`. - MED: `SecretNotFound`'s `suggestion` field now populated via `crate::suggest::find_similar` + `format_suggestions`. New regression test. ## Downstream POC [bglusman/calciforge#45](bglusman/calciforge#45) is now using this fork branch via git dep, with the wrapper collapsed from ~150 LOC of replicated orchestration to ~50 LOC of error coercion. --------- Co-authored-by: admin <[email protected]>
NorthIsUp
pushed a commit
to NorthIsUp/fnox
that referenced
this pull request
Apr 28, 2026
jdx#442) Per [discussion jdx#441](jdx#441) — "sounds fine" 👍 ## What Adds a top-level `Fnox` struct in a new `src/library.rs` module so downstream library consumers can write three lines instead of replicating `GetCommand::run`'s boilerplate. ```rust use fnox::Fnox; let fnox = Fnox::discover()?; // walks up + merges parent + local + global let value = fnox.get("MY_KEY").await?; let names = fnox.list()?; ``` ## Final API surface (after v3 fixes) - `Fnox::discover()` — `Config::load_smart(CONFIG_FILENAME)` (full upward-search + parent-merge + local-override + global merge chain) - `Fnox::open(path)` — explicit config path - `Fnox::with_profile(profile)` — builder for non-default profile - `Fnox::profile()` / `Fnox::config()` — accessors - `Fnox::get(key) -> Option<String>` — resolves via `secret_resolver::resolve_secret`. Errors with `FnoxError::SecretNotFound { key, profile, config_path, suggestion }` (suggestion populated via `crate::suggest` → "Did you mean DATABASE_URL?") - `Fnox::list() -> Vec<String>` — declared secret names, declaration order, **sync** `Config` is held behind `Arc` — `Fnox: Clone` is cheap. Re-exported as `fnox::Fnox` from `lib.rs`. ## Removed from earlier drafts - `Fnox::discover_from(start)` — couldn't be implemented safely without a process-global `set_current_dir` violating AGENTS.md. Removed in v2. ## What's NOT here `set()` — `SetCommand::run` is ~100 LOC of provider/encryption/remote-storage branching. Substantial enough to warrant its own design pass and PR. Happy to follow up if the API shape here lands cleanly. ## Tests 8 in `library::tests`: - `open_loads_explicit_path` / `open_errors_when_path_missing` - `list_returns_declared_secrets_in_declaration_order` - `get_returns_default_value_when_no_provider` - `get_errors_with_secret_not_found_when_key_undeclared` - `get_secret_not_found_carries_did_you_mean_suggestion` (regression for the suggestion field) - `with_profile_routes_list_to_named_profile` - `clone_does_not_deep_copy_config` (asserts Arc sharing, not deep copy) Full `cargo test --lib`: **143 passed** (135 existing + 8 new), 0 failed. ## Review fixes (v1 → v2 → v3) v2 (commit 79f1a81) — addressed v1 review: - HIGH: `discover_from` was bypassing load_smart's merge chain. Removed. - MED: `list()` made sync; `Fnox` holds `Arc<Config>`; `get()` uses `Config::get_secret` not full clone; profile via `Config::get_profile(None)` (honors `FNOX_PROFILE`); error is `SecretNotFound` not `Config(...)`. v3 (commit c17f24a) — addressed v2 review: - P1: doc example used `fnox.list()` without `?` — fixed. - MED: `get()` docstring tightened to mention both `if_missing = ignore | warn`. - MED: `SecretNotFound`'s `suggestion` field now populated via `crate::suggest::find_similar` + `format_suggestions`. New regression test. ## Downstream POC [bglusman/calciforge#45](bglusman/calciforge#45) is now using this fork branch via git dep, with the wrapper collapsed from ~150 LOC of replicated orchestration to ~50 LOC of error coercion. --------- Co-authored-by: admin <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Library-mode `FnoxClient` (called `FnoxLibrary`) calling the fnox crate directly, behind the `fnox-library` cargo feature. Subprocess `FnoxClient` remains the default.
Motivation
What's in this PR
What's deliberately NOT in this PR
Build cost when enabled
~30 transitive crates (AWS SDK, GCP SDK, keyring, age, etc.) and ~1m 39s added to a cold workspace build. Default-off keeps consumers that don't need fnox unaffected.
Validated
`cargo run --features fnox-library --example fnox_library_smoke` loads the local fnox.toml and lists declared secret names successfully.
Follow-up
Upstream issue/PR to fnox proposing:
```rust
let fnox = Fnox::discover()?;
let v = fnox.get("MY_KEY").await?;
fnox.set("MY_KEY", "value").await?;
fnox.list().await?;
```
…so downstream library consumers stop having to reimplement the binary's orchestration.
🤖 Generated with Claude Code