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 |
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.
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.