Skip to content

fix(tui): skip providers requiring interactive auth#333

Merged
jdx merged 5 commits intomainfrom
fix/tui-skip-interactive-providers
Mar 8, 2026
Merged

fix(tui): skip providers requiring interactive auth#333
jdx merged 5 commits intomainfrom
fix/tui-skip-interactive-providers

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 8, 2026

Summary

  • Add requires_interactive_auth() method to the Provider trait (defaults to false)
  • FIDO2 and YubiKey providers return true since they need physical key touch/PIN prompts
  • TUI sets a non_interactive flag on startup
  • During secret resolution, providers requiring interactive auth are skipped with an error message directing users to fnox exec instead
  • Prevents TUI display corruption from provider stderr output and demand::Input prompts

Test plan

  • All 107 cargo tests pass
  • Manual: run fnox tui with a FIDO2/YubiKey provider configured — should show error instead of hanging/corrupting display
  • Manual: fnox exec with the same provider should still work normally

🤖 Generated with Claude Code


Note

Medium Risk
Touches core secret/provider resolution paths and changes runtime behavior in TUI/non-interactive contexts, which could cause new “missing secret/provider” outcomes if the flag is set unexpectedly.

Overview
Prevents fnox tui from invoking providers that may prompt for input or write to stderr by introducing a global NON_INTERACTIVE flag and setting it on TUI startup.

Extends provider TOML and codegen to carry a requires_interactive_auth property (exposed as ProviderConfig::requires_interactive_auth()), marks fido2/yubikey as interactive-only, and updates both provider-config resolution and single/batch secret resolution to error/skip these providers in non-interactive mode while honoring per-secret if_missing handling.

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

FIDO2 and YubiKey providers require physical key interaction (touch,
PIN prompt) which would corrupt the TUI display. Add a
`requires_interactive_auth()` method to the Provider trait and check
it during secret resolution when running in non-interactive mode.

Affected providers show an error directing users to `fnox exec` instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@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 improves the stability and user experience of the fnox Text User Interface (TUI) by preventing conflicts with authentication providers that demand interactive user input. It establishes a clear mechanism for providers to declare their interactive requirements and ensures that such providers are gracefully bypassed when the application is running in a non-interactive TUI environment. This change avoids display corruption and hangs, guiding users to the appropriate fnox exec command for operations requiring physical interaction.

Highlights

  • Provider Trait Enhancement: Introduced a new requires_interactive_auth() method to the Provider trait, which defaults to false.
  • Interactive Provider Identification: Configured FIDO2 and YubiKey providers to return true for requires_interactive_auth(), indicating their need for physical user interaction.
  • Non-Interactive Context Flag: Implemented a non_interactive flag that is set to true upon TUI startup, marking the environment as non-interactive.
  • Secret Resolution Logic: Modified the secret resolution process to skip providers that require interactive authentication when operating in a non-interactive TUI context, providing an informative error message.
  • TUI Stability Improvement: Addressed potential TUI display corruption and application hangs that could occur due to interactive authentication prompts from providers.
Changelog
  • src/commands/tui.rs
    • Set the global non-interactive flag to true at the start of the TUI command execution.
  • src/env.rs
    • Added an AtomicBool named NON_INTERACTIVE to manage the application's interactive state.
    • Provided public functions set_non_interactive and is_non_interactive for controlling and querying this flag.
  • src/providers/fido2.rs
    • Implemented the requires_interactive_auth method for Fido2Provider to return true.
  • src/providers/mod.rs
    • Defined a new requires_interactive_auth method within the Provider trait, with a default implementation returning false.
  • src/providers/yubikey.rs
    • Implemented the requires_interactive_auth method for YubikeyProvider to return true.
  • src/secret_resolver.rs
    • Added a conditional check within try_get_secrets_batch to detect if the environment is non-interactive and the provider requires interactive authentication, returning an error in such cases.
Activity
  • All 107 cargo tests passed successfully.
  • Manual testing is planned to verify that fnox tui displays an error instead of hanging or corrupting the display when a FIDO2/YubiKey provider is configured.
  • Manual testing is planned to confirm that fnox exec continues to function normally with FIDO2/YubiKey providers.
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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 8, 2026

Greptile Summary

This PR introduces a non_interactive flag for the TUI to prevent providers requiring physical interaction (FIDO2, YubiKey) from hanging or corrupting the display. The implementation is solid:

  • Atomic ordering (Release/Acquire): Correctly implemented for cross-thread visibility.
  • Code generation approach: Exhaustively generates a compile-time-checked match for requires_interactive_auth() from provider TOML files—avoids magic strings and ensures all variants are covered.
  • Batch handling: Correctly applies per-secret if_missing policy (mirroring the ProviderNotConfigured pattern), preventing one provider's failure from silencing other providers at the same resolution level.
  • Non-interactive checks placed correctly: Guards in both single-secret (try_get_secret) and provider-resolution (resolve_secret_ref) paths happen before provider instantiation, avoiding unnecessary work.

One remaining issue: prompt_and_run_auth in src/auth_prompt.rs has no is_non_interactive() guard. If any non-FIDO2 provider (e.g., 1Password) fails with an auth error in the TUI, it will still call eprintln!() and render demand::Confirm, corrupting the display—the exact problem this PR aims to solve.

Confidence Score: 3/5

  • Safe for FIDO2/YubiKey skipping, but TUI can still be corrupted by auth errors from other providers.
  • The core logic for skipping interactive providers in batch and single-secret paths is correct, with proper per-secret if_missing handling. Atomic ordering and code generation are sound. However, prompt_and_run_auth lacks an is_non_interactive() guard, meaning non-FIDO2 providers that fail with auth errors can still write to stderr and render prompts, corrupting the TUI display—the exact problem this PR aims to solve. This is a concrete bug but isolated to one function with a straightforward fix.
  • src/auth_prompt.rs — needs is_non_interactive() guard in prompt_and_run_auth to prevent TUI corruption from any auth-error-throwing provider.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[TUI starts\nset_non_interactive true] --> B[resolve_secrets called]
    B --> C{Provider-backed\nor no-provider?}
    C -->|Provider-backed| D[resolve_provider_batch]
    C -->|No-provider| E[resolve_secret\ntry_get_secret]

    D --> F{is_non_interactive\n&& requires_interactive_auth?}
    F -->|Yes| G{if_missing policy\nper secret}
    G -->|ignore / warn| H[return Ok with None\nother providers unaffected]
    G -->|error| I[return Err\nfail fast]
    F -->|No| J[try_batch_with_auth_retry]
    J --> K[try_get_secrets_batch]
    K --> L{Batch succeeds?}
    L -->|Yes| M[return results]
    L -->|No auth error| N[prompt_and_run_auth\n⚠️ NOT guarded by is_non_interactive]
    N -->|is_auth_error? No| O[return Err]
    N -->|is_auth_error? Yes\n& is TTY| P[eprintln! + demand::Confirm\n💥 TUI corruption risk]

    E --> Q{is_non_interactive\n&& requires_interactive_auth?}
    Q -->|Yes| R[return Err\nbypasses if_missing]
    Q -->|No| S[get_provider_resolved\nthen get_secret]
Loading

Comments Outside Diff (1)

  1. src/auth_prompt.rs, line 17-45 (link)

    prompt_and_run_auth not guarded by is_non_interactive()

    This PR correctly skips FIDO2/YubiKey providers in the TUI, but prompt_and_run_auth has no awareness of the non-interactive flag. If any other provider (e.g., 1Password) fails with an auth error while the TUI is running, the following will happen:

    1. error.is_auth_error()true
    2. config.should_prompt_auth()true (the TUI process still has a TTY on stdin)
    3. eprintln!(...) is called — writes raw text over the TUI frame
    4. demand::Confirm::new(...).run() is called — renders a terminal prompt directly, corrupting the display

    The PR description states: "Prevents TUI display corruption from provider stderr output and demand::Input prompts", but this case is left unguarded.

    The simplest fix is to add an early return:

    if crate::env::is_non_interactive() {
        return Ok(false);
    }

    Add this check after the error.is_auth_error() guard and before config.should_prompt_auth().

Fix All in Claude Code

Last reviewed commit: 5c790c1

Comment thread src/env.rs Outdated
Comment thread src/env.rs Outdated
Comment thread src/secret_resolver.rs Outdated
Comment thread src/secret_resolver.rs Outdated
Comment thread src/secret_resolver.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 introduces a mechanism to handle providers that require interactive authentication in non-interactive environments like the TUI. It adds a requires_interactive_auth method to the Provider trait and a global non_interactive flag. The changes are well-structured and address the problem described. I have a couple of suggestions to improve the robustness and maintainability of the implementation.

Comment thread src/env.rs Outdated
Comment on lines +12 to +16
NON_INTERACTIVE.store(value, Ordering::Relaxed);
}

pub fn is_non_interactive() -> bool {
NON_INTERACTIVE.load(Ordering::Relaxed)
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

While Ordering::Relaxed might be sufficient here since this flag is set once at startup, using Ordering::SeqCst would be safer and make the code easier to reason about in the future if the usage of this flag becomes more complex. SeqCst provides the strongest guarantees (sequential consistency) and prevents potential reordering issues with other memory operations across threads. The performance difference is likely negligible in this context.

Suggested change
NON_INTERACTIVE.store(value, Ordering::Relaxed);
}
pub fn is_non_interactive() -> bool {
NON_INTERACTIVE.load(Ordering::Relaxed)
NON_INTERACTIVE.store(value, Ordering::SeqCst);
}
pub fn is_non_interactive() -> bool {
NON_INTERACTIVE.load(Ordering::SeqCst)
}

Comment thread src/secret_resolver.rs Outdated
Comment on lines +751 to +754
return Err(FnoxError::Provider(format!(
"Provider '{}' requires interactive authentication (e.g. physical key touch) and cannot be used in the TUI. Use 'fnox exec' instead.",
provider_name
)));
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 error message is specific to the TUI, but the check is_non_interactive() is generic. If other non-interactive contexts are added in the future, this error message would be inaccurate. It's better to make the message more generic to reflect the nature of the check.

Suggested change
return Err(FnoxError::Provider(format!(
"Provider '{}' requires interactive authentication (e.g. physical key touch) and cannot be used in the TUI. Use 'fnox exec' instead.",
provider_name
)));
return Err(FnoxError::Provider(format!(
"Provider '{}' requires interactive authentication (e.g. physical key touch) and cannot be used in non-interactive mode. Use 'fnox exec' instead.",
provider_name
)));

- Use Release/Acquire ordering for NON_INTERACTIVE AtomicBool
- Add non-interactive check to try_get_secret (single-secret path)
- Add non-interactive check in resolve_secret_ref to prevent bypass
  when provider config resolution triggers interactive providers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/secret_resolver.rs Outdated
Consistent with the same check in providers/resolver.rs and more
future-proof if set_non_interactive is used outside the TUI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/providers/resolver.rs Outdated
Comment thread src/secret_resolver.rs Outdated
Comment thread src/secret_resolver.rs Outdated
Move the non-interactive guard to ProviderConfig (static, based on
provider type) so it fires before async resolution, avoiding unnecessary
network calls or secret-ref resolution for interactive providers.

Also move the batch-path check to resolve_provider_batch so the error
is always surfaced regardless of per-secret if_missing policy.

Remove the now-unused requires_interactive_auth() from the Provider
trait and its fido2/yubikey implementations.

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/secret_resolver.rs
Comment thread src/secret_resolver.rs
Comment thread src/providers/mod.rs Outdated
The non-interactive check in resolve_provider_batch was returning a hard
Err that propagated through resolve_level's ? operator, aborting
resolution of ALL providers at the same dependency level — not just the
interactive one. Now follows the same pattern as ProviderNotConfigured:
apply per-secret if_missing policy and return Ok(results).

Also moves requires_interactive_auth() from a hardcoded string match to
a code-generated method driven by provider TOML definitions, so future
interactive providers get compile-time enforcement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx enabled auto-merge (squash) March 8, 2026 22:05
@jdx jdx merged commit a255282 into main Mar 8, 2026
16 checks passed
@jdx jdx deleted the fix/tui-skip-interactive-providers branch March 8, 2026 22:10
jdx pushed a commit that referenced this pull request Mar 9, 2026
### 🚀 Features

- **(cloudflare)** add Cloudflare API token lease backend by
[@jdx](https://github.com/jdx) in
[#335](#335)
- **(fido2)** bump demand to v2, mask PIN during typing by
[@jdx](https://github.com/jdx) in
[#334](#334)
- **(init)** add -f as short alias for --force by
[@jdx](https://github.com/jdx) in
[#329](#329)
- **(lease)** add --all flag, default to creating all leases by
[@jdx](https://github.com/jdx) in
[#337](#337)
- **(lease)** add GitHub App installation token lease backend by
[@jdx](https://github.com/jdx) in
[#342](#342)

### 🐛 Bug Fixes

- **(config)** fix directory locations to follow XDG spec by
[@jdx](https://github.com/jdx) in
[#336](#336)
- **(exec)** use unix exec and exit silently on subprocess failure by
[@jdx](https://github.com/jdx) in
[#339](#339)
- **(fido2)** remove duplicate touch prompt by
[@jdx](https://github.com/jdx) in
[#332](#332)
- **(set)** write to lowest-priority existing config file by
[@jdx](https://github.com/jdx) in
[#331](#331)
- **(tui)** skip providers requiring interactive auth by
[@jdx](https://github.com/jdx) in
[#333](#333)

### 🛡️ Security

- **(ci)** retry lint step to handle transient pkl fetch failures by
[@jdx](https://github.com/jdx) in
[#341](#341)
- **(mcp)** add MCP server for secret-gated AI agent access by
[@jdx](https://github.com/jdx) in
[#343](#343)
- add guide for fnox sync by [@jdx](https://github.com/jdx) in
[#328](#328)

### 🔍 Other Changes

- share Rust cache across CI jobs by [@jdx](https://github.com/jdx) in
[#340](#340)
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