Skip to content

feat(get): resolve leased credentials from fnox get#338

Merged
jdx merged 40 commits intomainfrom
feat/get-lease-resolution
Mar 9, 2026
Merged

feat(get): resolve leased credentials from fnox get#338
jdx merged 40 commits intomainfrom
feat/get-lease-resolution

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 8, 2026

Summary

  • Adds produced_env_vars() to the LeaseBackend trait so each backend declares which env var keys it produces
  • fnox get now checks lease backends first — if the requested key matches a lease backend's output, it resolves the lease (with caching) before falling back to provider secrets
  • Extracts resolve_lease from exec.rs into lease.rs as a shared function for both fnox exec and fnox get

Test plan

  • fnox get AWS_ACCESS_KEY_ID returns the leased credential (not the master secret) when an AWS STS lease is configured
  • fnox get SOME_PROVIDER_SECRET still works for non-lease keys
  • fnox exec still resolves leases correctly (regression check)
  • Cached leases are reused by fnox get

🤖 Generated with Claude Code


Note

Medium Risk
Changes touch credential leasing, caching/encryption, and environment injection paths used by fnox exec/get, so regressions could affect secret retrieval or lease reuse behaviour, though the scope is localized and mostly refactoring/extension.

Overview
fnox get now detects when the requested key is produced by a configured lease backend and resolves that lease (reusing cached credentials when possible) before falling back to normal secret/provider resolution, including honoring as_file output.

Lease handling is refactored: the lease resolution/caching logic previously embedded in exec is moved into lease.rs as a shared resolve_lease that uses short-lived ledger locks (read → unlock → async work → write) to reduce cross-process contention, and fnox get injects only the backend/encryption-provider consumed secrets into the environment when needed.

Backends now declare which env vars they may consume (and AWS STS which it produces), and a new FnoxError::LeaseContractViolation surfaces when a backend claims to produce a key but does not return it at runtime; housekeeping updates include hk.pkl linter tweaks.

Written by Cursor Bugbot for commit f6736ad. This will update automatically on new commits. Configure here.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the fnox tool by enabling the get command to retrieve leased credentials. It introduces a mechanism for lease backends to declare their produced environment variables and refactors the lease resolution logic into a shared module, improving code reusability and consistency across fnox exec and fnox get. This change allows users to fetch temporary, time-bound credentials directly, aligning fnox get with the secure credential management capabilities previously available only through fnox exec.

Highlights

  • Lease Backend Trait Enhancement: The LeaseBackend trait now includes a produced_env_vars() method, allowing each backend to declare the environment variable keys it generates.
  • fnox get Lease Resolution: The fnox get command has been updated to prioritize resolving credentials from lease backends, with caching, before falling back to traditional provider secrets.
  • Shared Lease Resolution Logic: The resolve_lease function has been extracted from exec.rs into lease.rs, making it a shared utility for both fnox exec and fnox get commands.
Changelog
  • src/commands/exec.rs
    • Removed unused imports LeaseBackendConfig and IndexMap.
    • Updated the call to resolve_lease to use the newly extracted lease::resolve_lease function.
    • Removed the local resolve_lease async function, as it was moved to src/lease.rs.
  • src/commands/get.rs
    • Added imports for lease and LeaseLedger.
    • Introduced a new asynchronous method resolve_from_lease to handle lease resolution for the get command.
    • Integrated the resolve_from_lease logic into the run method, allowing fnox get to check for and resolve leased credentials.
  • src/lease.rs
    • Added a new public asynchronous function resolve_lease, which encapsulates the logic for resolving lease backends and reusing cached credentials. This function was previously part of src/commands/exec.rs.
  • src/lease_backends/aws_sts.rs
    • Implemented the produced_env_vars method for AwsStsBackend, returning AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN.
  • src/lease_backends/azure_token.rs
    • Implemented the produced_env_vars method for AzureTokenBackend, returning the configured environment variable.
  • src/lease_backends/command.rs
    • Implemented the produced_env_vars method for CommandBackend, returning an empty vector as its output keys are dynamic.
  • src/lease_backends/gcp_iam.rs
    • Implemented the produced_env_vars method for GcpIamBackend, returning the configured environment variable.
  • src/lease_backends/mod.rs
    • Added a new trait method produced_env_vars to the LeaseBackend trait, requiring all implementations to declare the environment variable keys they produce.
  • src/lease_backends/vault.rs
    • Implemented the produced_env_vars method for VaultBackend, returning the values from its env_map.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Comment thread src/commands/get.rs Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 8, 2026

Greptile Summary

This PR wires fnox get into the lease resolution pipeline so that fnox get AWS_ACCESS_KEY_ID (and similar lease-produced keys) returns the live leased credential rather than the underlying master secret. It also extracts the previously exec.rs-local resolve_lease into a shared lease.rs function consumed by both commands, and adds produces_env_var / consumed_env_vars declarations to every backend so the routing and secret-injection filtering can be done without instantiating backends or making network calls.

Key changes:

  • src/commands/get.rs: New resolve_from_lease method with a tiered lookup: plaintext fast-path (no env injection, no network), encrypted-cache retry (env injection first, then decrypt), and fresh-lease creation. Uses rfind to match fnox exec's last-wins semantics.
  • src/lease.rs: resolve_lease shared function with minimal-scope ledger locks; record_lease helper de-duplicates the add+save pattern; find_cached_entry / resolve_cached_entry / try_decrypt_cached helpers for clean separation of sync and async cache work.
  • Backend files: All seven backends now declare CONSUMED_ENV_VARS (including comprehensive AWS, Vault TLS, and cloud-provider aliases) and AWS STS adds PRODUCED_ENV_VARS. produces_env_var in mod.rs is zero-allocation and covers all variants including Cloudflare and GithubApp.
  • src/error.rs: New LeaseContractViolation error with backend-aware help text.
  • hk.pkl: Removes redundant cargo-check and tightens clippy to -D warnings.

The one remaining concern is that skip_cache=true is passed to resolve_lease unconditionally from get.rs, including on the cold-start path. On a cold-start the TOCTOU check inside resolve_lease is a cheap disk read with no network call, so using skip_cache=true here saves nothing while preventing staggered concurrent callers from reusing a cache entry written by an earlier concurrent call.

Confidence Score: 3/5

  • Safe to merge with awareness of the skip_cache=true cold-start behaviour; no data loss or security issues, but concurrent fnox get callers on a cold cache will each create a fresh lease instead of sharing.
  • The PR is a well-executed refactor that successfully addresses a large number of previously flagged issues (label prefix, as_file, base64-decode, encryption-provider deps, lock scope, consumed/produced var completeness, TOCTOU comments). The implementation is complex but the logic is clearly reasoned and documented. The one remaining logic issue — skip_cache=true applied unconditionally, including on the cold-start path where the TOCTOU guard would be a free disk read — is not a correctness bug but can cause unnecessary API calls under concurrent invocations (shell prompt hooks, direnv). Core credential retrieval and caching paths are otherwise correct.
  • Pay closest attention to src/commands/get.rs (the skip_cache flag in resolve_from_lease) and src/lease.rs (resolve_lease with skip_cache semantics).

Important Files Changed

Filename Overview
src/commands/get.rs Core of this PR: adds resolve_from_lease with plaintext fast-path, encrypted-cache retry after env injection, consumed/produced var routing, and correct rfind last-wins semantics. The skip_cache=true flag is applied unconditionally including the cold-start path, where it loses TOCTOU protection with no performance benefit (cold-start TOCTOU check is a cheap disk read, not a network call).
src/lease.rs Well-structured refactor: resolve_lease is extracted with minimal-scope ledger locks, record_lease helper de-duplicates add+save, and new find_cached_entry/resolve_cached_entry/try_decrypt_cached helpers cleanly separate concerns. #[allow(clippy::too_many_arguments)] placement is now correct on all three functions.
src/commands/exec.rs Correctly delegates to shared lease::resolve_lease with skip_cache=false and label_prefix="exec", preserving TOCTOU semantics. Per-lease ledger lock scope is shortened relative to the old shared lock; this changes from a single-snapshot to independent-load semantics, but a comment explains this is intentional.
src/lease_backends/mod.rs Adds produces_env_var (zero-allocation, using rfind-compatible design) and consumed_env_vars methods to LeaseBackendConfig. Cloudflare and GithubApp are correctly wired in produces_env_var, addressing a gap from earlier review iterations.
src/error.rs Adds LeaseContractViolation error with appropriate diagnostic code. Help text distinguishes Vault (path misconfiguration) from other backends (code bug), which was previously flagged as an issue.

Sequence Diagram

sequenceDiagram
    participant User
    participant get.rs as fnox get
    participant lease.rs as lease.rs
    participant Ledger as LeaseLedger
    participant Provider as Encryption Provider
    participant Backend as Lease Backend API

    User->>get.rs: fnox get AWS_ACCESS_KEY_ID

    get.rs->>get.rs: produces_env_var() config lookup
    Note right of get.rs: Pure in-memory check,<br/>no network I/O

    get.rs->>Ledger: lock + load (short lock)
    Ledger-->>get.rs: CachedEntry (or None)
    Note right of get.rs: Lock released immediately

    alt Plaintext cached entry
        get.rs-->>User: return credential (fast path, no network)
    else Encrypted cached entry
        get.rs->>get.rs: consumed_env_vars filter
        get.rs->>get.rs: resolve_secrets_batch (needed secrets only)
        get.rs->>get.rs: set_secrets_as_env
        get.rs->>Provider: find_encryption_provider + decrypt
        alt Decryption succeeds
            Provider-->>get.rs: decrypted credentials
            get.rs-->>User: return credential
        else Decryption fails / No cached entry
            get.rs->>get.rs: check_prerequisites
            get.rs->>lease.rs: resolve_lease(skip_cache=true)
            lease.rs->>Backend: backend.create_lease()
            Backend-->>lease.rs: Lease result
            lease.rs->>Provider: cache_credentials (encrypt)
            Provider-->>lease.rs: encrypted creds (or None on failure)
            lease.rs->>Ledger: lock + load + record_lease + save (short lock)
            lease.rs-->>get.rs: credentials
            get.rs-->>User: return credential
        end
    end
Loading

Comments Outside Diff (1)

  1. src/commands/get.rs, line 407-421 (link)

    skip_cache=true on cold-start path loses TOCTOU protection with no benefit

    The comment justifies skip_cache=true as avoiding "a redundant network round-trip to the encryption provider on cache-miss." However, this is only true on the encrypted-cache-decryption-failure path (when cached_entry.is_some()). On a cold-start (no cache entry at all, cached_entry.is_none()), resolve_lease's TOCTOU check performs only a cheap disk read — find_cached_entry returns None immediately and resolve_cached_entry is never called. No network round-trip to the encryption provider occurs.

    Using skip_cache=true unconditionally on the cold-start path means staggered concurrent callers (e.g., shell prompt hooks, parallel CI steps, direnv) that all miss the initial cache check will all create fresh leases instead of the later ones reusing the first one's result. With skip_cache=false, a caller that reaches the TOCTOU check after an earlier concurrent caller has already written to the ledger would find and reuse that entry at the cost of only one disk read.

    Consider passing skip_cache based on whether a decryption attempt was already made:

    // Only skip the cache if we already tried (and failed) decryption —
    // that's the case where the TOCTOU check would re-contact the encryption
    // provider unnecessarily. On a cold-start (no cached_entry) the TOCTOU
    // check is a cheap disk read and preserves the ability to reuse a
    // concurrently-written cache entry.
    let had_encrypted_cache_miss = cached_entry.is_some(); // plaintext already returned above
    let creds = lease::resolve_lease(
        name,
        lease_config,
        config,
        profile,
        &project_dir,
        prereq_missing.as_deref(),
        "get",
        had_encrypted_cache_miss, // skip_cache
    )
    .await?;

Fix All in Claude Code

Last reviewed commit: f6736ad

Comment thread src/lease.rs Outdated
Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs Outdated
Comment thread src/lease_backends/command.rs Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively adds support for resolving leased credentials via fnox get, improving code reuse by extracting resolve_lease and enabling new functionality with produced_env_vars in the LeaseBackend trait. However, it introduces serious security risks due to the lack of a trust mechanism for project-local configuration files, which could lead to arbitrary code execution via the Command lease backend or theft of Vault tokens. It is strongly recommended to implement a trust mechanism (e.g., fnox trust) that requires explicit user approval of configuration files. Additionally, I suggest refactoring the produced_env_vars logic to LeaseBackendConfig for better code structure and error handling, and improving documentation around a limitation of this feature with the command backend.

Comment thread src/lease.rs
Comment on lines +512 to +609
pub async fn resolve_lease(
name: &str,
lease_config: &crate::lease_backends::LeaseBackendConfig,
config: &Config,
profile: &str,
project_dir: &Path,
ledger: &mut LeaseLedger,
prereq_missing: Option<&str>,
) -> Result<IndexMap<String, String>> {
let config_hash = lease_config.config_hash();
if let Some(cached_lease) = ledger.find_reusable(name, &config_hash)
&& let Some(ref cached_creds) = cached_lease.cached_credentials
{
if let Some(ref enc_provider_name) = cached_lease.encryption_provider {
match find_encryption_provider(config, profile).await {
EncryptionProviderResult::Available(found_name, provider)
if found_name == *enc_provider_name =>
{
match decrypt_credentials(provider.as_ref(), cached_creds).await {
Ok(decrypted) => {
tracing::debug!(
"Reusing cached encrypted lease '{}' for backend '{}'",
cached_lease.lease_id,
name
);
return Ok(decrypted);
}
Err(e) => {
tracing::warn!(
"Failed to decrypt cached lease '{}': {}, creating fresh lease",
cached_lease.lease_id,
e
);
}
}
}
_ => {
tracing::warn!(
"Encryption provider '{}' not available for cached lease '{}', creating fresh lease",
enc_provider_name,
cached_lease.lease_id
);
}
}
} else {
tracing::debug!(
"Reusing cached plaintext lease '{}' for backend '{}'",
cached_lease.lease_id,
name
);
return Ok(cached_creds.clone());
}
}

if let Some(missing) = prereq_missing {
return Err(FnoxError::Config(format!(
"Lease '{}': cached credentials could not be decrypted and \
prerequisites are missing: {}\n\
Run 'fnox lease create -i {}' to set up credentials interactively.",
name, missing, name
)));
}
let backend = lease_config.create_backend()?;

let duration_str = lease_config.duration().unwrap_or(DEFAULT_LEASE_DURATION);
let duration = parse_duration(duration_str)?;

let max_duration = backend.max_lease_duration();
if duration > max_duration {
return Err(FnoxError::Config(format!(
"Lease duration '{}' for '{}' exceeds maximum {:?}",
duration_str, name, max_duration
)));
}

let label = format!("fnox-{}", name);
let result = create_and_record_lease(
backend.as_ref(),
name,
&label,
duration,
config_hash,
config,
profile,
ledger,
project_dir,
)
.await?;

tracing::debug!(
"Created lease '{}' for backend '{}' (expires {:?})",
result.lease_id,
name,
result.expires_at
);

Ok(result.credentials)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-critical critical

The resolve_lease function, which has been moved to this file and is now used by both fnox exec and fnox get, facilitates the execution of arbitrary shell commands when the Command lease backend is configured. Since fnox automatically loads and merges configuration files from the current directory tree without any trust mechanism or user confirmation, an attacker can achieve Remote Code Execution (RCE) by placing a malicious .fnox.toml file in a directory. When a user runs fnox exec or fnox get in that directory, the create_command specified in the malicious configuration will be executed.

Comment thread src/lease_backends/vault.rs Outdated
Comment on lines +271 to +273
fn produced_env_vars(&self) -> Vec<String> {
self.env_map.values().cloned().collect()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-high high

The implementation of produced_env_vars for the Vault lease backend enables fnox get to automatically resolve Vault leases when a matching environment variable is requested. If an attacker provides a malicious configuration file with a Vault lease backend pointing to an attacker-controlled server, fnox get will send the user's VAULT_TOKEN in the X-Vault-Token header to that server. This allows an attacker to steal Vault credentials from users who run fnox get in a directory containing a malicious configuration.

Comment thread src/commands/get.rs Outdated
Comment on lines +99 to +105
let matching_lease = leases.iter().find(|(_, lease_config)| {
let backend = match lease_config.create_backend() {
Ok(b) => b,
Err(_) => return false,
};
backend.produced_env_vars().contains(&self.key)
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current approach of creating a LeaseBackend instance inside the find closure has a couple of downsides:

  1. It's potentially inefficient, as create_backend() might do non-trivial work.
  2. It silently swallows errors from create_backend(), which can hide configuration issues.

A cleaner approach would be to move the logic for determining produced environment variables from the LeaseBackend trait to the LeaseBackendConfig enum. This would centralize the logic and avoid instantiating a backend just to check its produced keys.

This would involve:

  1. Adding a produced_env_vars(&self) -> Vec<String> method to the LeaseBackendConfig enum.
  2. Removing produced_env_vars() from the LeaseBackend trait and its implementations.
  3. Updating this find call to use lease_config.produced_env_vars(), which would simplify it to a single line and make it more robust:
let matching_lease = leases.iter().find(|(_, lease_config)| {
    lease_config.produced_env_vars().contains(&self.key)
});

Comment thread src/lease_backends/mod.rs Outdated
Comment on lines +40 to +41
/// Environment variable keys this backend produces
fn produced_env_vars(&self) -> Vec<String>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The command backend returns an empty Vec for produced_env_vars, which means fnox get will not be able to resolve credentials from it. This is a reasonable limitation since the keys are dynamic, but it should be clearly documented in the trait definition for produced_env_vars so that future implementers and users are aware of this behavior.

Suggested change
/// Environment variable keys this backend produces
fn produced_env_vars(&self) -> Vec<String>;
/// Environment variable keys this backend produces.
///
/// This allows `fnox get` to resolve leased credentials by key. Backends with
/// dynamic keys (like the `command` backend) should return an empty vector,
/// which means `fnox get` will not be able to resolve their credentials.
fn produced_env_vars(&self) -> Vec<String>;

Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs
Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs
Comment thread src/commands/get.rs Outdated
Comment thread src/lease_backends/mod.rs Outdated
@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Mar 8, 2026

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Comment thread src/lease_backends/aws_sts.rs
jdx and others added 9 commits March 8, 2026 19:07
Previously `fnox get` only resolved provider secrets, ignoring lease
backends entirely. If a key like CLOUDFLARE_API_TOKEN was produced by a
lease backend, `fnox get` would fall through to the provider lookup and
either return the master secret or error.

This adds `produced_env_vars()` to the `LeaseBackend` trait so each
backend declares which env var keys it produces. `fnox get` now checks
lease backends first and resolves the lease (with caching) before falling
back to provider secrets.

Also extracts `resolve_lease` from `exec.rs` into `lease.rs` so it can
be shared between `fnox exec` and `fnox get`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move produced_env_vars() from LeaseBackend trait to LeaseBackendConfig
  so key matching works without instantiating backends (fixes ordering
  bug where env vars weren't set before create_backend, and silently
  swallowed backend construction errors)
- Move set_secrets_as_env before the lease find so fnox-managed secrets
  (e.g. VAULT_ADDR) are available for prerequisite checks
- Apply --base64-decode to lease-resolved values (was silently ignored)
- Add label_prefix to resolve_lease so exec uses "fnox-exec-<name>" and
  get uses "fnox-get-<name>" for distinguishable audit logs
- Add doc comment noting command backend returns empty produced_env_vars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the matching_lease lookup before resolve_secrets_batch so non-lease
keys skip secret resolution entirely. Since produced_env_vars() is on
the config (not the backend), no env vars or network calls are needed
for the match check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of resolving all profile secrets, filter to only those matching
the backend's required_env_vars (e.g. CLOUDFLARE_API_TOKEN, VAULT_ADDR).
Avoids unnecessary provider calls and spurious failures from unrelated
unreachable secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use rfind() instead of find() to match exec's last-wins semantics
  when multiple leases produce the same key
- Error instead of silently falling through when lease claims to produce
  a key but returned credentials don't contain it
- Document command backend produced_env_vars limitation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
required_env_vars() under-enumerates credential aliases (e.g.
FNOX_VAULT_TOKEN vs VAULT_TOKEN, CF_API_TOKEN vs CLOUDFLARE_API_TOKEN),
causing silent failures when secrets are stored under alternate names.
Resolve all profile secrets like exec does for correctness.

Also fix clippy::too_many_arguments on resolve_lease.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ced_env_vars

- Add consumed_env_vars() that enumerates all env var names a backend
  may read at runtime, including aliases (FNOX_VAULT_TOKEN, CF_API_TOKEN,
  GCP_SERVICE_ACCOUNT_KEY, etc.). Use this to filter which profile
  secrets to resolve, avoiding unnecessary network calls to unrelated
  providers while still covering every alias.
- Change produced_env_vars() to return Vec<&str> instead of Vec<String>
  to avoid per-call allocations in the rfind loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rs to backends

- Check as_file on profile secret config when returning lease-resolved
  credentials, matching exec's behavior
- Move CONSUMED_ENV_VARS constants into each backend module to
  centralize env var knowledge per backend
- consumed_env_vars() now returns &'static [&'static str] (zero alloc)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sumed vars

The AWS SDK respects these env vars to locate non-default credential
files. Without them, fnox get with a lease backend would fail to find
credentials when a user stores these as profile secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx force-pushed the feat/get-lease-resolution branch from 95055b0 to 7ca203e Compare March 8, 2026 23:08
Define PRODUCED_ENV_VARS constant in aws_sts.rs rather than hardcoding
the env var names in mod.rs, consistent with how CONSUMED_ENV_VARS
is already defined per-backend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs Outdated
Comment thread src/lease_backends/mod.rs Outdated
autofix-ci Bot and others added 3 commits March 8, 2026 23:24
…c produces_env_var

Reorder lock acquisition in resolve_from_lease so the ledger lock is held
before set_secrets_as_env mutates process environment variables.

Replace produced_env_vars() (which allocated a Vec per call) with
produces_env_var() for zero-allocation key matching in the rfind loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs
Comment thread src/lease_backends/command.rs Outdated
…tion

# Conflicts:
#	src/lease_backends/aws_sts.rs
#	src/lease_backends/azure_token.rs
#	src/lease_backends/command.rs
#	src/lease_backends/gcp_iam.rs
#	src/lease_backends/vault.rs
Comment thread src/commands/get.rs Outdated
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/lease_backends/vault.rs
Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs

let mut temp_env_guard = lease::TempEnvGuard::default();
let _temp_files =
lease::set_secrets_as_env(&resolved_secrets, &needed_secrets, &mut temp_env_guard)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consumed env vars filter excludes encryption provider credentials

Medium Severity

The secret injection in resolve_from_lease filters profile secrets to only those matching the lease backend's consumed_env_vars(). However, the code's own comments (lines 152-154) state env vars are injected for two purposes: (1) the encryption provider and (2) the backend SDK. When the encryption provider differs from the lease backend (e.g., a Cloudflare lease with Vault as the encryption provider), credentials like VAULT_TOKEN won't be injected because they aren't in Cloudflare's CONSUMED_ENV_VARS. This causes resolve_cached_entry to fail to decrypt cached credentials, producing a confusing hard error. In contrast, exec.rs injects all resolved secrets, so it works correctly in the same scenario.

Additional Locations (1)

Fix in Cursor Fix in Web

…dle empty consumed set

- Add VAULT_NAMESPACE to Vault backend's CONSUMED_ENV_VARS so fnox get
  injects it when stored as a profile secret
- Fall back to unwrap_or_default when consumed_env_vars is empty, since
  backends using ambient credentials don't need profile secrets and
  shouldn't fail on transient config read errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/commands/get.rs
Comment thread src/commands/get.rs Outdated
Ensure the encryption provider is reachable or run \
'fnox lease create -i {}' to refresh credentials.",
name, name
)));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Encrypted cache failure blocks get but not exec

Medium Severity

When an encrypted cached lease entry exists but decryption fails, resolve_from_lease returns a hard error instead of falling through to create a fresh lease. In contrast, resolve_lease (used by fnox exec) falls through from try_cached_credentials returning None and successfully creates a fresh lease. This means fnox exec succeeds while fnox get for the same key fails when the encryption provider is temporarily unavailable. The comment justifies this as avoiding a "cycle," but create_and_record_lease handles encryption failure gracefully by storing (None, None) for cached credentials, so fresh credentials are still returned successfully.

Additional Locations (1)

Fix in Cursor Fix in Web

…iour

Remove the hard error when encrypted cache decryption fails — instead
fall through to create a fresh lease, matching how resolve_lease
(used by exec) handles the same case. create_and_record_lease handles
encryption failure gracefully by storing (None, None).

Also document why check_prerequisites is called after set_secrets_as_env
(profile secrets are already injected, matching exec.rs).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/lease_backends/vault.rs
Add VAULT_CACERT, VAULT_CAPATH, VAULT_SKIP_VERIFY, VAULT_CLIENT_CERT,
VAULT_CLIENT_KEY, and VAULT_TLS_SERVER_NAME so fnox get injects them
when stored as profile secrets, matching fnox exec behaviour.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/commands/get.rs Outdated
Comment thread src/lease.rs
Clarify that a missing key in the runtime credential map is a backend
contract violation, not a static config error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/commands/get.rs
…lter

Add LeaseContractViolation error variant for backend contract violations
instead of reusing FnoxError::Config. When a cached lease entry uses an
encryption provider, include that provider's env_dependencies in the
consumed set so credentials like VAULT_TOKEN get resolved and injected
before decryption is attempted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/lease_backends/github_app.rs
Comment thread src/lease.rs
…firmed needed

When no consumed var is present as a profile secret (e.g. GitHub App
with private_key_file, Azure with az CLI auth), a transient secrets-config
read failure no longer aborts the command. Also add doc note explaining
resolve_cached_entry plaintext branch reachability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/commands/get.rs Outdated
Comment thread src/commands/get.rs Outdated
Look up the default encryption provider's env_dependencies
unconditionally (not gated on cached_entry existence) so that
create_and_record_lease can encrypt credentials on a cold start.
Previously, encryption provider credentials like VAULT_TOKEN were only
injected when an encrypted cached entry already existed, causing a
permanent cache-miss cycle.

Also simplify get_secrets error handling: use consumed.is_empty() to
decide between unwrap_or_default and hard ?, avoiding a double-call
pattern that could silently swallow read errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/error.rs
Distinguish Vault/configurable-path backends (where a missing key is
likely a server/config mismatch) from static backends (where it may
indicate a code bug).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/commands/get.rs Outdated
Replace the single resolve_lease call (which held the ledger lock across
async decryption and network calls) with inlined logic that acquires
short-lived locks only for synchronous ledger reads and writes:

1. TOCTOU cache re-check: sync read under lock, release, async decrypt
2. Lease creation: async network call with no lock held
3. Credential encryption: async, no lock held
4. Ledger write: sync add+save under short lock

This prevents concurrent fnox get/exec calls in the same project from
serializing on a single lock for the duration of network I/O.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread src/commands/get.rs Outdated
}
}

self.extract_key_from_creds(name, result.credentials, all_secrets)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicated lease creation logic diverges from shared function

Medium Severity

The resolve_from_lease method in get.rs duplicates the lease creation, duration validation, credential encryption, and ledger recording logic that already exists in create_and_record_lease and resolve_lease in lease.rs. The resolve_lease docstring claims it is "Shared between fnox exec and fnox get", but get.rs never calls it. Any future bug fix or behavior change to lease creation logic needs to be applied in both places, creating a maintenance risk and divergence hazard.

Additional Locations (1)

Fix in Cursor Fix in Web

…lication

Refactor resolve_lease to manage its own ledger locks with minimal scope
instead of requiring callers to hold a lock across async operations:

1. TOCTOU cache check: short lock for sync read, release, async decrypt
2. Lease creation: async network call with no lock held
3. Credential encryption: async, no lock held
4. Ledger write: short lock for sync add+save

This removes the duplicated lease creation logic from get.rs (now calls
resolve_lease again) and fixes exec.rs which also held a lock across all
resolve_lease calls in its loop. Remove now-unused try_cached_credentials.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/lease.rs
Comment thread src/commands/get.rs
Comment thread src/commands/exec.rs
…ve_lease

Address three review items:

1. Extract record_lease() helper for the shared add+save+warn-on-error
   pattern, used by both create_and_record_lease and resolve_lease.
   Eliminates the duplicated LeaseRecord construction and save logic.

2. Add skip_cache parameter to resolve_lease. When true (used by get.rs),
   the TOCTOU cache check is skipped since the caller already performed
   the full cache lookup and decryption attempt with encryption-provider
   credentials injected. Avoids a redundant network round-trip to the
   encryption provider on cache-miss.

3. Add comment in exec.rs noting the intentional per-lease lock scope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx merged commit 6ae2c40 into main Mar 9, 2026
16 checks passed
@jdx jdx deleted the feat/get-lease-resolution branch March 9, 2026 04:34
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.

1 participant