rtb-credentials 0.6.5

OS keychain credential storage. Part of the phpboyscout Rust toolkit.
Documentation
# AGENTS.md

This file provides guidance to AI coding agents (Claude Code, agy, codex, etc.) when working with code in this repository.

Ways of working are deliberately not repeated here. They live in the phpboyscout
skills, and naming a skill tends to age better than restating it, since the
restatement goes stale while the skill does not.

## What this is

`rtb-credentials` is a crate in the
[phpboyscout Rust toolkit](https://rust.phpboyscout.uk), one of the small,
framework-free crates extracted from
[rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base). It answers one
question: *where does this secret live, and can you hand it to me?*

Two halves that compose rather than overlap. `CredentialStore` (`src/store.rs:18`)
is the backend seam — `get` / `set` / `delete`, everything in `SecretString`, with
`KeyringStore`, `EnvStore`, `LiteralStore` and `MemoryStore` shipped. `Resolver`
(`src/resolver.rs:22`) walks a `CredentialRef` through the fixed chain
`env > keychain > literal > fallback_env` and returns the first hit; `probe` reports
which leg would win without handing back the value. It holds nothing about identity,
validates nothing, caches nothing and expires nothing;
`docs/explanation/limitations.md` is the full list, and the first thing to read.

**The seam exposes no enumeration.** `CredentialStore` is exactly those three
methods; nothing here asks a keychain what it holds. Listing comes from the other
direction — `CredentialBearing` (`src/bearing.rs:52`), implemented by a downstream tool
on its own typed config so `rtb-cli`'s `credentials` subtree walks what the *config*
declares. Keep that precise in writing: `keyring-core` 1.0.0, under the pinned
`keyring` 4.1.5, does ship `Store::search`; this crate simply never reaches for it,
which is narrower and far more defensible than "keychains cannot be enumerated".

If you know the Go side, the shapes genuinely correspond:
[`go/credentials`](https://gitlab.com/phpboyscout/go/credentials) makes the same env /
keychain / literal decision behind a `Backend` and likewise stores nothing about
identity. Where they part company is already in the limitations page — the Go module's
registration seam lets a build omit keychain support, and this crate has no equivalent.

## Where it has got to

Pre-1.0 (0.6.3), so a public shape can still move, and `cargo-semver-checks` runs on
the release MR to keep a move deliberate. Releases go through colophon
(`.colophon.yaml`), as on the Go side, with cicd's `rust-release` publishing the
tag; releaser-pleaser's conventions do not apply here.

Five crates depend on this one, all by published crates.io version rather than path,
all currently requiring `0.6.0`: `rtb-app` (`crates/rtb-app/Cargo.toml:23`),
`rtb-cli` (`crates/rtb-cli/Cargo.toml:24`) and `rtb-update`
(`crates/rtb-update/Cargo.toml:22`) unconditionally; `rtb-forge` (`rust/forge`
`Cargo.toml:121`) behind its default-on `git` feature; `rtb` (`rust-tool-base`
`crates/rtb/Cargo.toml:60`) behind its default-on `credentials` feature. `rtb-forge`
is the interesting one — its `git` feature exists so `Repo::clone` / `fetch` /
`push` can call `Resolver::resolve`, so a change to `CredentialRef` or `Resolver`
changes how every forge backend authenticates.

## The traps

**Nothing here reaches a sibling until it is released.** Dependents resolve this crate
from crates.io, not by path. "`main` does it" and "the crate does it" are different
claims — check the tag before telling a caller a capability exists.

**`unsafe_code` is `deny` here, not `forbid`, and the difference is load-bearing.**
`rust-tool-base` states `#![forbid(unsafe_code)]` as the production guarantee, and
`src/lib.rs:44` does ship exactly that. The *manifest* lint (`Cargo.toml:20`) is
`deny` on purpose, because `tests/bdd.rs:5` needs `#![allow(unsafe_code)]` for its
env-var mutation. Promote the manifest to `forbid` and the BDD binary stops
compiling — `forbid` cannot be lifted by an inner `allow`.

**`CI` is the one environment variable two tests share.** `t8_resolver_precedence`
removes it and `t9_literal_refused_in_ci` sets it, and `cargo test` runs both on
threads in one process, so both hold the `CI_VAR` lock in `tests/unit.rs` while they
touch it. Any new test that reads or writes `CI` must take that lock too. The BDD
suite is a single `#[tokio::test]` (`tests/bdd.rs`), so every scenario shares one
process whatever the runner, and cucumber 0.23.0 runs scenarios concurrently by
default; S6, the only scenario that sets `CI`, is tagged `@serial` for that reason.
`just test` runs `cargo nextest run` and nothing else: it used to fall back to
`cargo test` on failure, which hid any failure that only nextest produced.

`just ci` runs `fmt-check`, `lint`, `docs`, `test` and `audit`, and passes clean on
`main`. CI adds cross-OS runs and a 45% coverage gate — a current-floor ratchet,
not the estate standard, because keychain paths need a keychain the container lacks.

## Which skills apply here

| When | Skill |
|---|---|
| Adding or moving any check a secret passes through | `centralize-security-footguns` |
| Deciding whether `CredentialStore` earns another method | `deep-modules` |
| Before writing code for anything more than a typo | `spec-driven-development` |
| Deciding whether a change is finished (docs ship with it) | `definition-of-done` |
| Writing or restructuring this crate's `docs/` tree | `diataxis-docs`, `checkable-claims` |
| Drawing a conclusion from a checkout you did not just fetch | `verify-dont-trust` |
| Sizing a Renovate bump on `keyring`, `secrecy` or the CI components | `assess-before-bumping` |
| Releasing this crate and its dependents together | `release-train` |
| Writing a commit message or a merge request description | `conventional-commits`, `pre-1-0-release-safety` |
| Committing, branching, merging, or opening a merge request | `forge-publish-workflow` |

> Skills are a Claude Code mechanism, shipped by the
> [phpboyscout marketplace]https://gitlab.com/phpboyscout/claude-code-plugins; an
> agent without them should treat a named skill as a topic to ask about rather than
> a file it can load. There are no Rust-specific skills yet, so this is the
> language-agnostic set — Go-shaped rows are left out rather than relabelled, and
> `releaser-pleaser-releases` does not apply because releases run on colophon.

## House rules

- Linear history. Rebase and fast-forward; never squash-merge from the UI.
- Conventional Commits, and the type decides whether a release is cut. `feat`,
  `fix`, `perf` and `refactor` release. A change that repoints or removes a public
  interface is `feat`, not `refactor`, or it ships as a patch.
- No AI attribution in anything published, and never at-mention anyone.
- Never cut a release yourself. That is the maintainer's call, every time.