Skip to content

feat(age): support provider-backed identities#515

Open
k35o wants to merge 3 commits into
jdx:mainfrom
k35o:fix/age-provider-backed-identity
Open

feat(age): support provider-backed identities#515
k35o wants to merge 3 commits into
jdx:mainfrom
k35o:fix/age-provider-backed-identity

Conversation

@k35o
Copy link
Copy Markdown

@k35o k35o commented Jun 3, 2026

Summary

The keychain provider docs added in v1.25.1 recommend storing a single age identity in the OS keychain and using the age provider for bulk secret storage:

[providers]
keychain = { type = "keychain", service = "fnox" }
age = { type = "age", recipients = ["age1..."], identity = { provider = "keychain", value = "age-key" } }

With the documented config, fnox currently fails while parsing the age provider:

unknown field `identity`, expected one of `recipients`, `key_file`, `auth_command`

This implements provider-backed age identities so the documented bootstrap pattern works.

What changed

  • Add identity = { provider = "...", value = "..." } support to the age provider config.
  • Add a provider/value reference type for provider config fields.
  • Resolve age identities lazily during decryption, so encryption-only commands like fnox set do not require access to the backing identity provider.
  • Preserve config context for nested provider-backed age identities.
  • Update provider add age, JSON schema, docs, and tests.

Test plan

  • cargo check
  • cargo build
  • cargo test -p fnox-core providers::secret_ref
  • test/bats/bin/bats test/age.bats
  • cargo fmt --check

Summary by CodeRabbit

  • New Features

    • Age encryption provider can source its decryption identity from another configured provider (provider-backed identity); CLI template updated to initialise this field.
  • Bug Fixes

    • Identity-resolution now detects circular dependencies and surfaces clearer errors when referenced providers are missing or require interactive auth.
  • Documentation

    • Added docs showing how to supply Age identity via a keychain-backed provider.
  • Tests

    • Added integration tests covering provider-backed identities, nested resolution, and cycle detection.

Allow age providers to resolve an identity from another provider, matching the keychain bootstrap configuration documented in v1.25.1.

The identity reference is kept lazy so encryption-only operations do not read the private identity provider. Decryption keeps config context so nested provider-backed age identities can resolve correctly.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 884f529f-ba00-4863-9027-474250332d75

📥 Commits

Reviewing files that changed from the base of the PR and between 0208c18 and 504ce11.

📒 Files selected for processing (4)
  • crates/fnox-core/src/providers/age.rs
  • crates/fnox-core/src/providers/mod.rs
  • crates/fnox-core/src/providers/resolver.rs
  • test/age.bats
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/fnox-core/src/providers/mod.rs
  • crates/fnox-core/src/providers/resolver.rs

📝 Walkthrough

Walkthrough

This PR introduces provider-backed secret references, allowing the age encryption provider to obtain its decryption identity from another configured provider. The change adds a new ProviderSecretRef type, extends code generation to handle this field type, refactors the age provider to support identity resolution, and implements recursive provider resolution with cycle detection during decryption.

Changes

Provider-backed Secret References and Age Identity Resolution

Layer / File(s) Summary
Provider-backed secret reference types
crates/fnox-core/src/providers/secret_ref.rs
ProviderSecretRef and OptionProviderSecretRef types defined with serde/JSON-schema support, including custom serialization for absent optional references and test coverage for TOML round-trip behavior.
Code generation for provider_ref field type
crates/fnox-core/build/generate_providers.rs
Provider code generator extended to handle provider_ref field type across config/resolved variants, conversion logic, and resolver fields; imports updated for both generated modules; age wizard initialization includes identity field.
Age provider schema and implementation
crates/fnox-core/providers/age.toml, crates/fnox-core/src/providers/age.rs
Age provider TOML schema adds [fields.identity] as provider_ref type; AgeEncryptionProvider struct extended with identity reference, shared config context, and metadata; constructors updated to accept identity; decryption-time identity resolution via resolve_provider_identity() with cycle detection.
Provider instantiation with context
crates/fnox-core/src/providers/mod.rs
get_provider_from_resolved_with_context helper added to pass config, profile, and provider-name context during age provider construction; public re-export of OptionProviderSecretRef added.
Provider reference resolution
crates/fnox-core/src/providers/resolver.rs
resolve_provider_ref function added to resolve provider-backed references through provider lookup, error suggestions, recursive config resolution with cycle detection, and asynchronous secret fetching; resolver updated to use context-aware provider construction.
Schema, CLI, and documentation
docs/public/schema.json, src/commands/provider/add.rs, docs/providers/age.md
JSON schema definitions for ProviderSecretRef and OptionProviderSecretRef added; age provider schema includes identity property; CLI add command initializes age provider with identity field; documentation example shows OS keychain-backed identity configuration.
Integration tests
test/age.bats
Five new Bats tests covering provider-backed age identity resolution, error cases, nested resolution, and cycle detection.

Sequence Diagram

sequenceDiagram
  participant User
  participant AgeProvider as AgeEncryptionProvider
  participant Resolver as resolve_provider_identity()
  participant ResolutionContext as ResolutionContext
  participant OtherProvider as ReferencedProvider
  participant SecretFetch as get_secret() on ReferencedProvider

  User->>AgeProvider: request decryption (Age provider with identity ref)
  AgeProvider->>Resolver: resolve_provider_identity() (if env var absent)
  Resolver->>ResolutionContext: check/track resolution path (cycle guard)
  ResolutionContext-->>Resolver: ok / not in cycle
  Resolver->>OtherProvider: construct resolved provider with context
  OtherProvider->>SecretFetch: fetch referenced secret value
  SecretFetch-->>OtherProvider: return secret string
  OtherProvider-->>Resolver: secret string
  Resolver-->>AgeProvider: identity string
  AgeProvider->>AgeProvider: use identity to decrypt
  AgeProvider-->>User: decrypted secret
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops with identity in hand,
Providers linked across the land,
Age unlocks secrets nested deep,
References resolve while cycles we keep,
Decryption sings and rabbits leap!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(age): support provider-backed identities' clearly summarizes the main change—adding support for provider-backed age identities as a new feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 3, 2026

Greptile Summary

This PR implements identity = { provider = "...", value = "..." } support for the age encryption provider, enabling the documented keychain-bootstrap pattern where a single age identity key is stored in the OS keychain and looked up lazily at decrypt time. Config resolution is unchanged — the identity field is carried opaquely through to ResolvedProviderConfig and resolved only when get_secret is called.

  • Lazy identity resolution: resolve_provider_identity is only invoked from get_secret, so encryption-only commands (fnox set) work without touching the backing identity provider.
  • Cycle detection via AgeIdentityCycleGuard: A shared Arc<Mutex<Vec<String>>> is threaded through every hop of the identity chain; enter() detects a revisit and returns ProviderConfigCycle, while the RAII AgeIdentityCycleEntry pops the entry on drop, keeping the guard clean across both success and error paths.
  • Code-generation coverage: All four codegen paths are updated for the new provider_ref field type, and the existing generated get_provider_from_resolved is bypassed for age providers by get_provider_from_resolved_with_context_and_identity_cycle_guard.

Confidence Score: 5/5

Safe to merge — the new identity resolution path is correctly isolated from the encryption path and the cycle-detection machinery handles both direct and multi-hop cycles.

The AgeIdentityCycleGuard correctly shares state across nested resolution hops via Arc::clone and the RAII drop guard ensures cleanup on both success and error. All call sites that were using the no-context get_provider_from_resolved have been updated. The bats tests exercise the full happy path, the lazy-skip-on-encrypt property, nested two-hop resolution, and mutual cycle detection.

No files require special attention.

Important Files Changed

Filename Overview
crates/fnox-core/src/providers/age.rs Adds identity field, new_with_config, AgeIdentityCycleGuard/AgeIdentityCycleEntry for lazy provider-backed identity resolution with cycle detection; logic is correct and drop semantics are sound.
crates/fnox-core/src/providers/resolver.rs Adds resolve_provider_ref and resolve_provider_ref_with_identity_cycle_guard; updates resolve_secret_ref to use context-aware provider instantiation; interacts correctly with the new cycle guard machinery.
crates/fnox-core/src/providers/mod.rs Adds get_provider_from_resolved_with_context and get_provider_from_resolved_with_context_and_identity_cycle_guard; routes age provider instantiation through new_with_config for all call paths that go via get_provider_resolved.
crates/fnox-core/src/providers/secret_ref.rs Adds ProviderSecretRef and OptionProviderSecretRef with correct serde/JsonSchema implementations and unit tests covering both serialisation directions.
crates/fnox-core/build/generate_providers.rs Wires provider_ref type through all codegen paths and adds identity: OptionProviderSecretRef::none() to the wizard template.
test/age.bats Adds five integration tests covering single-hop identity, multi-secret exec, encryption-only lazy skip, nested two-hop identity, and mutual cycle detection.

Reviews (3): Last reviewed commit: "Merge branch 'main' into fix/age-provide..." | Re-trigger Greptile

Comment thread crates/fnox-core/src/providers/age.rs
Comment thread crates/fnox-core/src/providers/age.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fnox-core/src/providers/age.rs`:
- Around line 56-82: resolve_provider_identity in AgeEncryptionProvider creates
a fresh ResolutionContext and only rejects direct self-reference, so transitive
provider identity cycles (age-A → age-B → age-A) are not detected; to fix,
change resolve_provider_identity to accept and propagate a shared cycle guard
(e.g., pass a &mut ResolutionContext or a &mut HashSet<String> of in-progress
provider names) into the resolve_provider_ref call instead of creating a new
ctx, add an early check that inserts the current provider_name into the guard
before delegating and removes it after, and ensure resolve_provider_ref will
return an error if it finds the provider_name already in the guard (mirroring
resolve_provider_config_with_context behavior); update the test suite (the bats
test for nested provider-backed age identity) to include a new test asserting
that mutual age↔age references produce the expected
ProviderConfigCycle/FnoxError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1db9a6d-8d67-468b-bfa0-78874d458ee1

📥 Commits

Reviewing files that changed from the base of the PR and between 31699a5 and 0208c18.

📒 Files selected for processing (10)
  • crates/fnox-core/build/generate_providers.rs
  • crates/fnox-core/providers/age.toml
  • crates/fnox-core/src/providers/age.rs
  • crates/fnox-core/src/providers/mod.rs
  • crates/fnox-core/src/providers/resolver.rs
  • crates/fnox-core/src/providers/secret_ref.rs
  • docs/providers/age.md
  • docs/public/schema.json
  • src/commands/provider/add.rs
  • test/age.bats

Comment thread crates/fnox-core/src/providers/age.rs
@jdx jdx enabled auto-merge (squash) June 4, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants