Skip to content

CredentialRef configuration keys

CredentialRef is the shape a tool author embeds in a typed config struct. It is what an operator actually writes in a config file, so its four keys are the crate's real configuration surface.

use rtb_credentials::CredentialRef;

#[derive(serde::Deserialize)]
struct MyConfig {
    anthropic: CredentialRef,
}

The four keys, in resolution order

Every key is optional and defaults to absent. Each carries #[serde(default)], so omitting it is always valid.

Key Type Default Resolution position
env string — the name of an environment variable absent 1st
keychain table — service and account absent 2nd
literal string — the secret itself absent 3rd
fallback_env string — the name of an environment variable absent 4th

A fully populated reference in YAML:

anthropic:
  env: MYTOOL_ANTHROPIC_KEY
  keychain:
    service: mytool
    account: anthropic
  literal: sk-ant-...
  fallback_env: ANTHROPIC_API_KEY

Nothing requires all four. A reference with only fallback_env set is valid and common; so is an entirely empty one, which always fails to resolve.

env and fallback_env hold variable names, not values

Both keys take the name of an environment variable. Writing the secret itself into env does not work — the resolver calls std::env::var(name) with whatever string is supplied and treats a missing variable as a miss.

The practical failure this produces: a config with env: sk-ant-abc123 silently resolves nothing, because no variable called sk-ant-abc123 exists. The chain then continues to the next leg and usually ends in NotFound. The secret has also now been written into a config file in plain text. The key for an inline secret is literal.

The two keys behave identically at resolution time. The distinction is one of intent: env is the variable this tool defines for itself, and fallback_env is an ecosystem-wide name such as ANTHROPIC_API_KEY or GITHUB_TOKEN that the tool did not invent and shares with other software.

keychain requires both service and account

The keychain key deserialises into a KeychainRef, which has two fields and neither is optional:

Field Type Required
service string yes
account string yes
keychain:
  service: mytool
  account: anthropic

Omitting either field is a deserialisation error, not a resolution-time miss — the config fails to load. KeychainRef carries #[serde(deny_unknown_fields)], so a misspelled subkey such as accont is also a load-time error rather than a silently ignored entry.

The two strings are passed straight through to the platform keychain as the service and account identifiers. service is conventionally the tool's name or a provider identifier; account is conventionally a user login or a fixed label such as default. Neither is interpreted by this crate.

Unknown keys are a load-time error

CredentialRef carries #[serde(deny_unknown_fields)]. A key that is not one of the four documented above fails deserialisation.

anthropic:
  environment: MYTOOL_KEY   # error: unknown field `environment`

This is deliberate: a mistyped key on a credential would otherwise mean the credential silently resolves from a lower-precedence source, or not at all, with no signal that the config said something the tool did not understand.

The trade-off is that config files cannot carry extra annotations — comments describing a credential must be file comments, not extra keys.

CredentialRef cannot be serialised back to config

CredentialRef derives Debug, Clone, Default and Deserialize. It deliberately does not derive Serialize.

A tool that loads config, changes an unrelated setting and writes the whole struct back cannot compile if that struct contains a CredentialRef. This is the intended barrier: it makes "resolved secret accidentally written to disk in plain text" a compile error rather than a runtime accident. Writing a credential back has to be a deliberate, separate code path.

The underlying reason is secrecy. SecretString is SecretBox<str>, and secrecy only provides impl Serialize for SecretBox<T> where T: SerializableSecret + Sized — an opt-in marker trait. str is unsized and does not implement the marker, so SecretString has no Serialize impl and neither can any struct containing one.

SecretString does implement Deserialize, which is why literal can be read from config in the first place. The direction is deliberately asymmetric: in, but not out.

KeychainRef is serialisable

Unlike its parent, KeychainRef derives Serialize as well as Deserialize:

#[derive(Debug, Clone, Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct KeychainRef { /* service, account */ }

This is consistent rather than contradictory — a KeychainRef holds no secret, only the two identifiers used to look one up. A tool can safely serialise a KeychainRef into config, a status report or a log line.

The asymmetry is worth knowing because a struct holding a KeychainRef can derive Serialize, while one holding a whole CredentialRef cannot.

Default gives an empty reference that never resolves

CredentialRef::default() sets all four keys to None. Resolving it returns CredentialError::NotFound with the name <unnamed credential>.

This matters when a config struct derives Default: a credential the operator never configured is not an error at load time, only at the point something tries to use it.