Files
nanny/doc/rotation.md
rob thijssen 991ab992da docs: record the rotation seam and the exclusion evidence
Adds doc/rotation.md: what is already in place for optional rotation
(RotationHint, Status::Rotated, fingerprint-as-unit-of-work), what it would
take, and the four constraints a future executor does not get to relax —
chiefly that nanny holding provisioning credentials is the real cost of the
feature and must be opt-in per service.

Broadens the noise exclusions after a first sweep of a real ~/git produced
1030 working-tree findings, 273 of them from one crate's vendored
target/container/cargo/registry. Excluding all of target/ rather than just
debug/ and release/, plus vendored dependencies, build output, tool caches
and minified assets. .git/config stays readable: a password in a remote URL
is a genuine spill.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqHWfdMAsYu1o36tgeima
2026-09-02 15:13:30 +03:00

4.7 KiB

Rotation

Status: design note. Not implemented. nanny records and alerts; the operator decides what to do. This document exists so that when optional rotation is built, it lands on a seam that is already in the right place rather than being retrofitted through the middle of the detection path.

Why it is not built yet

The expensive mistake would be to build rotation before the detection is trusted. A tool that revokes a credential on a false positive costs more than one that misses a real leak, because the false positive breaks something that was working and the operator learns to distrust the tool. Detection quality has to be demonstrated on a real corpus first — and the first sweep of a real workstation produced 1538 findings, most of them fixtures in vendored dependencies, which is exactly the state in which automated revocation would have been a disaster.

So the order is: detect, tune until the findings are believable, then automate the response.

What is already in place

RotationHint, carried by a rule rather than by a finding, because what you do about a leaked forge token does not depend on which transcript it turned up in:

pub struct RotationHint {
    pub service: String,          // "anthropic", "gitea", "aws" — the dispatch key
    pub instructions: String,     // what a human does, right now
    pub docs: Option<String>,
    pub rotation_suffices: bool,  // false ⇒ incident response, not rotation
}

rotation_suffices is the field that will matter most. A leaked API key is undone by revoking it. A leaked private key is not: the key has to be replaced and the old public key removed from every trust store it reached, and no amount of automation makes that a one-button operation. Rules where this is false must never be handed to an automatic executor.

Status::Rotated, distinct from Status::Acknowledged. Acknowledged means the operator has seen it and decided; rotated means the leaked value is now worthless. Only the second closes the exposure, and the UI keeps them apart so that a triaged backlog is never mistaken for a clean one. An executor sets Rotated; nothing else should.

The Fingerprint. Because the same secret in five places shares one fingerprint, a single rotation can resolve every finding carrying it. That is the unit of work an executor should operate on — a secret, not a location.

What it would take

An Remediator port in nanny-core, alongside Alerter, dispatching on RotationHint::service to per-service implementations. Sketch:

#[async_trait]
pub trait Remediator: Send + Sync {
    fn service(&self) -> &str;
    /// What this would do, without doing it.
    async fn plan(&self, finding: &Finding) -> Result<RemediationPlan>;
    /// Do it. Only ever reached after explicit operator confirmation.
    async fn execute(&self, plan: &RemediationPlan) -> Result<Remediated>;
}

Four constraints that follow from the rest of the design, and that a future implementation does not get to relax:

  1. An executor cannot read the leaked value, because nothing stores it. It therefore cannot revoke "the credential that leaked" by presenting it. It revokes by identity at the service — the key with this prefix, the token named for this consumer — which is why agent-credentials.md §5's practice of naming issued credentials after their consumer matters here.
  2. Rotation needs credentials, and nanny currently has none. This is the real cost of the feature: nanny becomes a process holding provisioning credentials, which is the thing the patterns-only detection design exists to avoid. The mitigation is the mint-don't-share pattern of agent-credentials.md §2 — a narrow provisioning credential per service that can revoke and reissue and nothing else — and it should be opt-in per service, off by default, never a blanket capability.
  3. Never automatic. plan then confirm then execute. A monitor that revokes credentials on its own judgement is a denial-of-service with good intentions.
  4. Still never edits a harness's files. Rotating makes the leaked copy inert, which is the correct remedy. Rewriting a live session transcript to scrub it risks breaking the harness that is writing it, and leaves the operator believing a copy is gone when backups, the model provider's logs and file-history/ still hold it.

The thing rotation does not fix

A credential that reached a model provider's servers is gone, whatever happens locally afterwards. Rotation is the only remedy that works, which is the whole argument for detecting the spill in the first place — and for nanny scan exiting non-zero, so it can gate something before the transcript is uploaded rather than after.