---
title: Resolver — resolve and probe
description: Resolver constructors, the exact resolution chain, which errors abort the walk, and how probe differs from resolve.
tags:
- reference
- resolver
---
# `Resolver` — `resolve` and `probe`
`Resolver` walks a [`CredentialRef`](credential-ref.md) through a fixed
chain of sources and returns the first hit.
## Constructing a `Resolver`
| `Resolver::new(store: Arc<dyn CredentialStore>)` | the store supplied |
| `Resolver::with_platform_default()` | `KeyringStore::new()` |
| `Resolver::default()` | identical to `with_platform_default()` |
All three are infallible and do no I/O. The injected store is used for
the keychain leg only — the environment and literal legs do not consult
it.
```rust
use rtb_credentials::{MemoryStore, Resolver};
use std::sync::Arc;
let resolver = Resolver::new(Arc::new(MemoryStore::new()));
```
## The resolution chain
`resolve` returns the first source that produces a value:
1. **`cref.env`** — `std::env::var(name)`. A set variable is a hit, even
if its value is the empty string.
2. **`cref.keychain`** — `store.get(service, account)` on the injected
store.
3. **`cref.literal`** — the embedded secret, unless `CI=true`.
4. **`cref.fallback_env`** — `std::env::var(name)`.
A key that is absent is skipped. If every configured leg misses,
`resolve` returns `CredentialError::NotFound`.
The order is fixed in code and there is no way to reorder, disable or
extend it. A tool that needs different precedence has to implement its
own walk over the four fields.
## A keychain backend error aborts the walk
The keychain leg distinguishes two kinds of failure:
| `CredentialError::NotFound` | falls through to the next leg |
| **any other error** | **returns that error immediately** |
A locked, unavailable or misconfigured keychain therefore fails the
whole resolution even when `literal` or `fallback_env` is configured and
would have produced a value. Only a clean "no such entry" continues the
chain.
This is why a machine with a broken keychain can fail to start a tool
that appears, from its config, to have a perfectly good environment-variable
fallback.
## `literal` under `CI=true` stops the chain rather than falling through
When the `literal` key is present and the process is running under
`CI=true`, `resolve` returns `CredentialError::LiteralRefusedInCi`.
It does **not** continue to `fallback_env`. A reference carrying both a
`literal` and a `fallback_env` fails in CI even when the fallback
variable is set and readable:
```yaml
anthropic:
literal: sk-ant-... # present
fallback_env: ANTHROPIC_API_KEY # set in CI, but never reached
```
Removing the `literal` key from config that runs in CI is the way to
make the fallback reachable.
## What counts as CI
The check is exact-match, case-sensitive:
```rust
fn is_ci() -> bool {
std::env::var("CI").as_deref() == Ok("true")
}
```
Only the literal string `true` triggers refusal. `CI=1`, `CI=TRUE`,
`CI=True` and `CI=yes` all leave literals permitted. See
[environment variables](environment-variables.md) for which platforms
set the value this crate expects.
## Which name appears in a `NotFound` error
When nothing resolves, the `name` field of `NotFound` is chosen by this
order — which is **not** the resolution order:
1. `fallback_env`, if set
2. otherwise `env`, if set
3. otherwise `"{service}/{account}"` from `keychain`, if set
4. otherwise the literal string `<unnamed credential>`
A reference with both `env: MYTOOL_KEY` and
`fallback_env: ANTHROPIC_API_KEY` reports
`credential not found: ANTHROPIC_API_KEY`, naming the ecosystem variable
rather than the tool's own. The message names one source, not every
source that was tried.
## `probe` reports the winning source without returning the secret
```rust
pub async fn probe(&self, cref: &CredentialRef)
-> Result<ResolutionOutcome, CredentialError>;
```
`probe` walks the same chain and reports which leg would win.
| `Resolved(ResolutionSource)` | this leg produced a readable value |
| `LiteralRefusedInCi` | `literal` is configured and `CI=true` |
| `Missing` | nothing resolved |
`ResolutionSource` is `Env`, `Keychain`, `Literal` or `FallbackEnv`.
Both enums are `#[non_exhaustive]`, so a `match` on either needs a
wildcard arm and adding a variant is not a breaking change.
`LiteralRefusedInCi` is an `Ok` outcome from `probe` and an `Err` from
`resolve`. The split is deliberate: a status command wants to display
"refused in CI" as a state, not to fail.
### `probe` does the same I/O as `resolve`
`probe` is not a cheap, side-effect-free inspection. When `keychain` is
configured it performs a real `store.get`, reading the secret and
dropping it. On a platform keychain that means a genuine round-trip per
reference, and on some systems an unlock prompt.
What `probe` avoids is returning the secret to the caller, so a status
command can list credentials without a secret reaching a terminal.
### `probe` and `resolve` can disagree on an empty keychain entry
`probe`'s environment legs test `std::env::var(name).is_ok()`, and its
keychain leg tests whether `get` returned `Ok`. These are the same
conditions `resolve` uses, so the two agree on which leg wins.
The one behaviour worth knowing is shared by both: a configured source
holding an empty string counts as a hit. `probe` reports `Resolved`, and
`resolve` returns an empty `SecretString`. Neither treats "set but
empty" as a miss, so a credential can resolve successfully to nothing
and fail later at the point of use.