Skip to content

Show where a credential resolves from

Two pieces combine into a "where do my credentials come from" command: CredentialBearing enumerates the credentials a config carries, and Resolver::probe reports which source would win without returning the secret.

Enumerate your config's credentials

Implement CredentialBearing on the struct that owns them:

use rtb_credentials::{CredentialBearing, CredentialRef};

struct Config {
    anthropic: Section,
    github: Section,
}
struct Section { cred: CredentialRef }

impl CredentialBearing for Config {
    fn credentials(&self) -> Vec<(&'static str, &CredentialRef)> {
        vec![
            ("anthropic", &self.anthropic.cred),
            ("github", &self.github.cred),
        ]
    }
}

The &'static str is the operator-facing name for the credential — the thing you print, and the thing you would accept as a command argument.

There is no derive macro. The list is written by hand, which means a credential added to the config and not added here is silently absent from every listing.

Probe each one

use rtb_credentials::{ResolutionOutcome, ResolutionSource, Resolver};

for (name, cref) in config.credentials() {
    let status = match resolver.probe(cref).await? {
        ResolutionOutcome::Resolved(ResolutionSource::Env) => "environment variable",
        ResolutionOutcome::Resolved(ResolutionSource::Keychain) => "OS keychain",
        ResolutionOutcome::Resolved(ResolutionSource::Literal) => "literal in config",
        ResolutionOutcome::Resolved(ResolutionSource::FallbackEnv) => "fallback variable",
        ResolutionOutcome::LiteralRefusedInCi => "literal — refused under CI",
        ResolutionOutcome::Missing => "not configured",
        _ => "unknown",
    };
    println!("{name}: {status}");
}

Both ResolutionOutcome and ResolutionSource are #[non_exhaustive], so the wildcard arm is required and new variants will not break the build.

Know what probe costs

probe is not a cheap inspection. When keychain is configured it performs a real store.get, reads the secret and drops it. Against a platform keychain that is a genuine round-trip per credential, and on macOS it can raise an unlock prompt.

What it avoids is handing the secret back to your code, so the value cannot reach a terminal or a log by accident.

Treat LiteralRefusedInCi as a state, not an error

probe returns LiteralRefusedInCi as an Ok outcome, where resolve returns it as an Err. That split exists precisely so a status command can display the condition instead of failing on it.

Showing it is worth doing: it is the case where the credential is configured and readable but will not be used, which is otherwise very hard for an operator to diagnose.

Use () when a tool has no typed config yet

CredentialBearing has a blanket implementation for () returning an empty Vec, so code generic over the trait compiles before a tool has declared any credentials.

let nothing: Box<dyn CredentialBearing> = Box::new(());
assert!(nothing.credentials().is_empty());

The trait is object-safe, so Box<dyn CredentialBearing> and &dyn CredentialBearing both work.

What this does not tell you

A Resolved outcome means a source produced a value, not that the value is a working credential. Nothing validates length, format or authenticity, and a source holding an empty string reports as Resolved.

A status command built this way answers "where would this come from", not "does this work".