a73x

docs/superpowers/plans/2026-08-18-delegate-certificates.md

Ref:   Size: 51.0 KiB   History

# Delegate Certificates Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** SSH certificates authenticate as short-lived delegates of an enrolled person, able to write `refs/collab/*` and nothing else.

**Architecture:** `settings.git` grows `cadir/` (per-person CA enrolment, same basename convention as `keydir/`). A new russh `auth_openssh_certificate` handler validates certs against `cadir/` + `keydir/`; the session carries a delegate marker; the existing update hook enforces the hard-coded `refs/collab/` write ceiling via a new env var. No config surface, no client-side change.

**Tech Stack:** Rust 2021, russh 0.62 (`auth_openssh_certificate`), `ssh_key::Certificate::validate_at`, git2, existing test harness (`tests/common/mod.rs`).

**Spec:** `docs/superpowers/specs/2026-08-18-delegate-certificates-design.md`

## Global Constraints

- The server code is its own crate rooted at `src/server/` (modules are `crate::governance`, `crate::ssh`, `crate::repos`).
- Test expectations for keys/certs come from OpenSSH tooling (`ssh-keygen`), never from our own code path (external-oracle rule; see `src/server/governance/keydir.rs` test comments for the pattern).
- No new dependencies. `russh::keys::ssh_key` re-exports `Certificate`, `Fingerprint`, `Error`.
- `cargo test` green after every task; `cargo clippy --all-targets` clean; run `rustfmt` only on files you created or edited (repo has pre-existing fmt drift — do not reformat other files).
- Commit after every task with a message in the repo's style (imperative sentence about the behavior, why-not-how body), trailer `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`.
- Delegates on ungoverned servers: reject at auth. Certificates only mean something under governance.

---

### Task 1: `CaDir` — the per-person CA roster

**Files:**
- Create: `src/server/governance/cadir.rs`
- Modify: `src/server/governance/mod.rs` (add `pub mod cadir;` beside `pub mod keydir;`)

**Interfaces:**
- Consumes: `keydir::name_for_path(path: &str) -> Option<&str>`, `keydir::validate_name(name: &str) -> Result<(), String>` (both already exist; `validate_name` is `pub(crate)`).
- Produces:
  - `pub struct CaDir` with `pub fn new() -> Self`, `pub fn insert(&mut self, path: &str, content: &str) -> Result<(), CaDirError>`, `pub fn fingerprints_for(&self, name: &str) -> &[russh::keys::ssh_key::Fingerprint]`, `pub fn is_empty(&self) -> bool`.
  - `pub enum CaDirError` mirroring `KeyDirError`'s `Malformed`/`BadName` variants (no `Ambiguous` — see below).

- [ ] **Step 1: Write the failing tests**

Create `src/server/governance/cadir.rs` with the module doc, an empty `CaDir` skeleton that panics `todo!()` in `insert`, and these tests. Reuse the exact throwaway keys from `keydir.rs`'s tests (copy the constants — the files are read independently):

```rust
//! `cadir/` — who may act as you.
//!
//! `keydir/` answers "which keys ARE this person"; `cadir/` answers "which
//! CAs may mint delegates OF this person". Same convention: the basename is
//! the name, directories are ignored, one trailing `.pub` is stripped.
//!
//! One deliberate rule difference from `keydir/`: the same CA key enrolled
//! under two names is ALLOWED here, not an error. In `keydir/` that is an
//! authorization coin-flip, because the connection presents only a
//! fingerprint and the server must pick a name. A certificate *names its
//! principal*, so the lookup runs the other way — "is this CA enrolled for
//! the name the cert claims" — and the same key under two names is simply a
//! shared CA that two people have each explicitly opted into.

use std::collections::HashMap;

use russh::keys::ssh_key::Fingerprint;
use russh::keys::{HashAlg, PublicKey};

use super::keydir::{name_for_path, validate_name};

#[derive(Debug, thiserror::Error)]
pub enum CaDirError {
    #[error("{path}: not a well-formed OpenSSH public key: {source}")]
    Malformed {
        path: String,
        #[source]
        source: russh::keys::ssh_key::Error,
    },
    #[error("{path}: {reason}")]
    BadName { path: String, reason: String },
}

/// Name-to-CA-fingerprints mapping built from `cadir/`.
#[derive(Debug, Default)]
pub struct CaDir {
    by_name: HashMap<String, Vec<Fingerprint>>,
}

impl CaDir {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn insert(&mut self, path: &str, content: &str) -> Result<(), CaDirError> {
        todo!()
    }

    /// The CA fingerprints enrolled for `name`. Empty means no CA may mint
    /// delegates of this person.
    pub fn fingerprints_for(&self, name: &str) -> &[Fingerprint] {
        self.by_name.get(name).map(Vec::as_slice).unwrap_or(&[])
    }

    pub fn is_empty(&self) -> bool {
        self.by_name.is_empty()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Throwaway PUBLIC keys, same provenance discipline as keydir.rs's:
    /// expected fingerprints are `ssh-keygen -lf`'s answers, not ours.
    const KEY_A: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2PlPIF/fKLCQvCHhIUX2FKpQRflVl6CNoQ8aFjIxdG governance-test-a@git-collab";
    const FP_A: &str = "SHA256:h9V15zrr/EYDfNMPefKR+Gf2PpXdfw8M7Fvu9zLjjqY";
    const KEY_B: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDaUXZkX8MTYd4ztPAb11azBoq42VDJowOS3Zuj9jZlg governance-test-b@git-collab";
    const FP_B: &str = "SHA256:GauEGK/qjDwFjqhK9/tOqTzcjmf48HCMgBUX6sddJg4";

    fn fp(s: &str) -> Fingerprint {
        s.parse().unwrap()
    }

    #[test]
    fn a_ca_at_the_top_level_authorizes_its_basename() {
        let mut cas = CaDir::new();
        cas.insert("cadir/alex.pub", KEY_A).unwrap();
        assert_eq!(cas.fingerprints_for("alex"), &[fp(FP_A)]);
        assert!(cas.fingerprints_for("mallory").is_empty());
    }

    #[test]
    fn directories_are_ignored() {
        let mut cas = CaDir::new();
        cas.insert("cadir/openbao/alex.pub", KEY_A).unwrap();
        assert_eq!(cas.fingerprints_for("alex"), &[fp(FP_A)]);
    }

    /// The deliberate difference from keydir/: a shared CA is two explicit
    /// opt-ins, not an ambiguity.
    #[test]
    fn the_same_ca_under_two_names_is_allowed() {
        let mut cas = CaDir::new();
        cas.insert("cadir/shared/alex.pub", KEY_A).unwrap();
        cas.insert("cadir/shared/bob.pub", KEY_A).unwrap();
        assert_eq!(cas.fingerprints_for("alex"), &[fp(FP_A)]);
        assert_eq!(cas.fingerprints_for("bob"), &[fp(FP_A)]);
    }

    #[test]
    fn two_cas_for_one_name_are_both_kept() {
        let mut cas = CaDir::new();
        cas.insert("cadir/openbao/alex.pub", KEY_A).unwrap();
        cas.insert("cadir/laptop/alex.pub", KEY_B).unwrap();
        let fps = cas.fingerprints_for("alex");
        assert!(fps.contains(&fp(FP_A)) && fps.contains(&fp(FP_B)));
    }

    #[test]
    fn the_same_file_twice_is_idempotent() {
        let mut cas = CaDir::new();
        cas.insert("cadir/alex.pub", KEY_A).unwrap();
        cas.insert("cadir/mirror/alex.pub", KEY_A).unwrap();
        assert_eq!(cas.fingerprints_for("alex").len(), 1);
    }

    #[test]
    fn a_malformed_key_is_rejected_naming_its_path() {
        let mut cas = CaDir::new();
        let err = cas.insert("cadir/alex.pub", "not a key").unwrap_err();
        assert!(err.to_string().contains("cadir/alex.pub"), "got {err}");
    }

    #[test]
    fn non_pub_files_are_ignored() {
        let mut cas = CaDir::new();
        cas.insert("cadir/README", "not a key").unwrap();
        assert!(cas.is_empty());
    }

    #[test]
    fn reserved_and_malformed_names_are_rejected() {
        let mut cas = CaDir::new();
        assert!(cas.insert("cadir/CREATOR.pub", KEY_A).is_err());
        assert!(cas.insert("cadir/@admins.pub", KEY_A).is_err());
        assert!(cas.insert("cadir/has space.pub", KEY_A).is_err());
    }
}
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `cargo test --lib cadir 2>&1 | tail -20` (adjust: if `src/server` is a separate binary crate, `cargo test -p <server-crate> cadir` — check `Cargo.toml` for the crate layout; `cargo test cadir` from the workspace root also works)
Expected: FAIL — panics on `todo!()`.

- [ ] **Step 3: Implement `insert`**

```rust
    pub fn insert(&mut self, path: &str, content: &str) -> Result<(), CaDirError> {
        let Some(name) = name_for_path(path) else {
            // Not a .pub file; a README in cadir/ is not a CA.
            return Ok(());
        };
        validate_name(name).map_err(|reason| CaDirError::BadName {
            path: path.to_string(),
            reason,
        })?;

        let key =
            PublicKey::from_openssh(content.trim()).map_err(|source| CaDirError::Malformed {
                path: path.to_string(),
                source,
            })?;
        let fingerprint = key.fingerprint(HashAlg::Sha256);

        let entry = self.by_name.entry(name.to_string()).or_default();
        if !entry.contains(&fingerprint) {
            entry.push(fingerprint);
        }
        Ok(())
    }
```

Replace the `todo!()`. Add `pub mod cadir;` to `src/server/governance/mod.rs` next to `pub mod keydir;`. `validate_name` in `keydir.rs` is `pub(crate)` — that is already visible from a sibling module in the same crate.

- [ ] **Step 4: Run tests to verify they pass**

Run: `cargo test cadir 2>&1 | tail -5`
Expected: all pass. Also run `cargo clippy --all-targets 2>&1 | grep -E "^(warning|error)"` — expect nothing new.

- [ ] **Step 5: Commit**

```bash
git add src/server/governance/cadir.rs src/server/governance/mod.rs
git commit -m "cadir/: which CAs may mint delegates of each person"
```

---

### Task 2: Governance loads `cadir/`, and the roster rule covers it

**Files:**
- Modify: `src/server/governance/mod.rs` — `Governance` struct (~line 97), `read_tree` (~line 195), `read_keydir`'s neighborhood (~line 213), `check_roster_has_rules` (~line 305)

**Interfaces:**
- Consumes: `CaDir` from Task 1.
- Produces: `Governance` gains `pub cas: cadir::CaDir`. `read_tree` populates it from a `cadir/` tree entry (absent → empty). `check_roster_has_rules` treats `cadir/` as roster.

- [ ] **Step 1: Write the failing tests**

Add to `mod.rs`'s `#[cfg(test)] mod tests` (it exists, ~line 420; it builds settings trees with `git2::Repository::init_bare` into a `TempDir` — follow the pattern of the existing `roster` test at ~line 467, which commits a tree via `repo.commit(Some("HEAD"), ...)`). Reuse its helper for writing a tree; the existing tests show how blobs are inserted with `TreeBuilder`. Write:

```rust
    /// KEY_A / its ssh-keygen fingerprint, same constants as cadir.rs tests.
    const CA_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2PlPIF/fKLCQvCHhIUX2FKpQRflVl6CNoQ8aFjIxdG governance-test-a@git-collab";

    #[test]
    fn read_tree_loads_cadir() {
        // Build a tree: conf/access.conf + keydir/alex.pub + cadir/mint/alex.pub
        // using the same tree-building helper the existing tests use.
        // Assert: read_tree returns Some(governance) and
        // governance.cas.fingerprints_for("alex").len() == 1
        // and governance.cas.fingerprints_for("bob").is_empty().
    }

    #[test]
    fn cadir_without_rules_is_an_unruled_roster() {
        // Tree with ONLY cadir/mint/alex.pub (no conf/, no keydir/).
        // Assert: check_roster_has_rules(&tree).is_err()
    }

    #[test]
    fn a_malformed_cadir_file_fails_validation() {
        // Tree with valid conf + keydir, plus cadir/alex.pub containing "junk".
        // Assert: validate_settings_tree(...).unwrap_err() names "cadir/alex.pub".
    }
```

Fill the bodies concretely from the neighboring tests' tree-building code — the pattern is `repo.treebuilder(None)`, insert blobs with `repo.blob(...)`, nest subtrees, `builder.write()`. Do not invent a new helper if one already exists in that test module; extend it to take extra `(path, content)` pairs if needed.

- [ ] **Step 2: Run to verify failure**

Run: `cargo test --lib governance 2>&1 | tail -15`
Expected: `read_tree_loads_cadir` fails to compile (no `cas` field) — that counts as the RED step for a struct change; the other two fail at assert.

- [ ] **Step 3: Implement**

In `mod.rs`:

```rust
const CADIR: &str = "cadir";
```

`Governance` gains the field:

```rust
pub struct Governance {
    pub conf: AccessConf,
    pub keys: KeyDir,
    pub cas: cadir::CaDir,
}
```

`read_tree`'s tail becomes:

```rust
    let keys = read_keydir(repo, tree)?;
    let cas = read_cadir(repo, tree)?;
    Ok(Some(Governance { conf: access, keys, cas }))
```

`read_cadir` mirrors `read_keydir` exactly (collect blobs, sort for deterministic error reporting, insert), with `CADIR` in place of `KEYDIR` and `cadir::CaDir` in place of `KeyDir`. Copy the structure — including the "Collect first, insert after" comment rationale — rather than abstracting the two into one generic walker; two small readers beat one clever one here.

`check_roster_has_rules`:

```rust
    let has_rules = tree.get_path(Path::new(ACCESS_CONF_PATH)).is_ok();
    let has_roster = tree.get_path(Path::new(KEYDIR)).is_ok()
        || tree.get_path(Path::new(CADIR)).is_ok();
```

and extend its error text to mention both directories (`{KEYDIR}/ or {CADIR}/ without {ACCESS_CONF_PATH}: ...`).

Fix every construction site of `Governance { .. }` — `read_tree` is the only one in production code; the compiler will find any test sites.

- [ ] **Step 4: Run the full suite**

Run: `cargo test 2>&1 | grep -E "test result: FAILED|^error\[" ; echo exit=$?`
Expected: no FAILED lines. The governance behavioral tests exercise settings pushes end to end and must still pass untouched.

- [ ] **Step 5: Commit**

```bash
git add src/server/governance/mod.rs
git commit -m "Governance loads cadir/ and the roster rule covers it"
```

---

### Task 3: `delegate::validate` — the whole cert policy in one function

**Files:**
- Create: `src/server/governance/delegate.rs`
- Modify: `src/server/governance/mod.rs` (add `pub mod delegate;`)

**Interfaces:**
- Consumes: `Governance` (fields `keys`, `cas` — both pub), `KeyDir::names()`, `CaDir::fingerprints_for`.
- Produces:
  - `pub struct Delegate { pub person: String, pub key_id: String }`
  - `pub fn validate(cert: &russh::keys::ssh_key::Certificate, governance: &super::Governance, unix_now: u64) -> Result<Delegate, String>`
  - The `Err` string is a log/debug reason, never sent to the client (auth failures are silent rejects).

- [ ] **Step 1: Write the failing tests**

Certs in these tests are minted by real `ssh-keygen` at test time — the external oracle. Test-support helper inside the `#[cfg(test)]` module (uses `tempfile::TempDir` and `std::process::Command`, both already used by this crate's tests):

```rust
#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;
    use std::process::Command;

    /// Generate a keypair; returns (private_path, public_content).
    fn keygen(dir: &Path, name: &str) -> (std::path::PathBuf, String) {
        let key = dir.join(name);
        let out = Command::new("ssh-keygen")
            .args(["-t", "ed25519", "-N", "", "-q", "-C", name])
            .arg("-f").arg(&key)
            .output().unwrap();
        assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
        let public = std::fs::read_to_string(key.with_extension("pub")).unwrap();
        (key, public)
    }

    /// Mint a certificate with ssh-keygen. `extra` lets a test pass flags like
    /// ["-h"] (host cert) or ["-O", "force-command=/bin/true"] (critical opt).
    /// `principals`: None = valid-for-anyone (no -n flag).
    fn mint(ca: &Path, subject_pub: &Path, key_id: &str, principals: Option<&str>,
            validity: &str, extra: &[&str]) -> russh::keys::ssh_key::Certificate {
        let mut cmd = Command::new("ssh-keygen");
        cmd.arg("-s").arg(ca).args(["-I", key_id, "-V", validity]).args(extra);
        if let Some(p) = principals {
            cmd.args(["-n", p]);
        }
        cmd.arg(subject_pub);
        let out = cmd.output().unwrap();
        assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
        let cert_path = subject_pub.to_str().unwrap().replace(".pub", "-cert.pub");
        let text = std::fs::read_to_string(&cert_path).unwrap();
        std::fs::remove_file(&cert_path).unwrap(); // ssh-keygen refuses to overwrite
        text.trim().parse().unwrap()
    }

    /// A Governance where `person` is enrolled in keydir/ and `ca_pub` (if
    /// given) is enrolled for `ca_for` in cadir/.
    fn governance(person_pub: &str, person: &str, ca_pub: Option<(&str, &str)>) -> Governance {
        let conf = crate::governance::conf::AccessConf::parse(&format!(
            "repo settings\n    RW+ = {person}\n"
        )).unwrap();
        let mut keys = crate::governance::keydir::KeyDir::new();
        keys.insert(&format!("keydir/{person}.pub"), person_pub).unwrap();
        let mut cas = crate::governance::cadir::CaDir::new();
        if let Some((content, name)) = ca_pub {
            cas.insert(&format!("cadir/{name}.pub"), content).unwrap();
        }
        Governance { conf, keys, cas }
    }

    const NOW: u64 = 1_755_500_000; // fixed 'current time', 2026-08; certs minted -1m:+30m around real now also satisfy validate when tests pass real now — so mint with absolute windows instead
    // ...tests below
}
```

**Important on time:** mint with an *absolute* validity window that brackets a fixed `NOW`, so tests are deterministic: `-V 20260101000000:20270101000000` with `NOW = 1_755_500_000` (2026-08-18) inside it, and an expired window `-V 20200101000000:20210101000000` for the expiry test. Never call real wall-clock in assertions.

The tests (each ~6 lines given the helpers):

```rust
    #[test]
    fn a_cert_from_an_enrolled_ca_naming_an_enrolled_person_validates() {
        // ca + person keypairs; governance with ca enrolled for "alex";
        // mint(ca, person.pub, "claude-a", Some("alex"), VALID_WINDOW, &[]);
        // validate(...) == Ok(Delegate { person: "alex", key_id: "claude-a" })
    }

    #[test]
    fn a_cert_from_an_unenrolled_ca_is_rejected() {
        // governance has NO ca for anyone (ca_pub: None) → Err mentions the CA
    }

    #[test]
    fn a_ca_enrolled_for_a_different_name_cannot_mint_for_this_one() {
        // ca enrolled for "bob" in cadir; cert names "alex" → Err
    }

    #[test]
    fn an_unenrolled_principal_is_rejected_even_from_a_trusted_ca() {
        // keydir has "alex"; cert names "mallory"; ca enrolled for "mallory" → Err
        // (cadir cannot create identity: mallory is not in keydir/)
    }

    #[test]
    fn zero_principals_is_rejected() { /* principals: None → Err */ }

    #[test]
    fn two_principals_are_rejected() { /* Some("alex,bob") → Err */ }

    #[test]
    fn a_host_certificate_is_rejected() { /* extra: &["-h"], principals Some("alex") → Err */ }

    #[test]
    fn an_unknown_critical_option_is_rejected() {
        /* extra: &["-O", "force-command=/bin/true"] → Err */
    }

    #[test]
    fn an_expired_certificate_is_rejected() {
        /* mint with the 2020 window, NOW in 2026 → Err */
    }
```

- [ ] **Step 2: Run to verify failure**

Run: `cargo test delegate 2>&1 | tail -15`
Expected: FAIL (module skeleton `todo!()`).

- [ ] **Step 3: Implement**

```rust
//! Delegate certificates: the one policy decision, in one place.
//!
//! A certificate is a delegate of the person it names. This function is the
//! entire answer to "is this certificate a valid delegate right now" — auth
//! calls it when the connection opens, and the regime calls it again on every
//! subsequent command, which is what makes revocation (cadir/ entry removed,
//! person's keys removed, cert expired) take effect on the next command
//! rather than the next connection.

use russh::keys::ssh_key::certificate::CertType;
use russh::keys::ssh_key::Certificate;

use super::Governance;

#[derive(Debug)]
pub struct Delegate {
    pub person: String,
    pub key_id: String,
}

pub fn validate(
    cert: &Certificate,
    governance: &Governance,
    unix_now: u64,
) -> Result<Delegate, String> {
    if cert.cert_type() != CertType::User {
        return Err("not a user certificate".to_string());
    }

    // Exactly one principal: a delegate acts for one person. Zero is
    // OpenSSH's "valid for anyone", which is an anti-goal here.
    let person = match cert.valid_principals() {
        [one] => one.clone(),
        [] => return Err("certificate names no principal".to_string()),
        many => return Err(format!("certificate names {} principals", many.len())),
    };

    // cadir/ delegates identity; it never creates it. The person must exist.
    if !governance.keys.names().iter().any(|n| *n == person) {
        return Err(format!("{person} is not enrolled in keydir/"));
    }

    // Per PROTOCOL.certkeys, an implementation MUST refuse a certificate
    // carrying a critical option it does not recognize. We recognize none.
    if let Some((name, _)) = cert.critical_options().iter().next() {
        return Err(format!("unrecognized critical option {name:?}"));
    }

    // Signature verifies, signing CA is enrolled *for this person*, and the
    // timestamp is inside the validity window — all three via validate_at.
    let fingerprints = governance.cas.fingerprints_for(&person);
    if fingerprints.is_empty() {
        return Err(format!("no CA is enrolled in cadir/ for {person}"));
    }
    cert.validate_at(unix_now, fingerprints.iter())
        .map_err(|e| format!("certificate did not validate for {person}: {e}"))?;

    Ok(Delegate {
        person,
        key_id: cert.key_id().to_string(),
    })
}
```

Check the exact `CertType` import path compiles (`ssh_key::certificate::CertType`); if the re-export differs, follow the compiler. Add `pub mod delegate;` to `governance/mod.rs`.

- [ ] **Step 4: Run to verify pass**

Run: `cargo test delegate 2>&1 | tail -5` then the full `cargo test`.
Expected: all pass.

- [ ] **Step 5: Commit**

```bash
git add src/server/governance/delegate.rs src/server/governance/mod.rs
git commit -m "Validate a certificate into a delegate of an enrolled person"
```

---

### Task 4: Certificates authenticate; the regime knows a delegate

**Files:**
- Modify: `src/server/ssh/session.rs` — `SshHandler` fields (~line 44), `Regime` (~line 15), `regime()` (~line 87), `auth_publickey` (~line 525), exec handler's principal derivation (~line 631), `receive_pack_env` (~line 117), `handle_release_command`'s regime match (~line 197)
- Modify: `tests/common/mod.rs` — harness helpers for CAs, certs, and settings-with-cadir
- Create: `tests/delegate_test.rs`

**Interfaces:**
- Consumes: `governance::delegate::{validate, Delegate}` from Task 3.
- Produces:
  - `enum AuthIdentity { Key { principal: String }, Delegate { certificate: Box<Certificate> } }` stored as `authenticated: Option<AuthIdentity>` (replaces `authenticated_principal: Option<String>`).
  - `Regime::Governed` gains `delegate: Option<String>` (the cert key ID; `None` for a person's own key).
  - `fn regime(&self) -> Regime` (drops the `fingerprint` parameter — it reads `self.authenticated`).
  - Harness (Task 5 and 6 rely on these exact signatures):
    - `pub fn delegate_ca(&self, name: &str) -> PathBuf` — keypair under `<root>/cas/<name>`, created once, mirrors `named_key`.
    - `pub fn mint_cert(&self, ca: &Path, key: &Path, key_id: &str, principals: &str, validity: &str) -> PathBuf` — runs `ssh-keygen -s <ca> -I <key_id> -n <principals> -V <validity> <key>.pub`, returns the `-cert.pub` path.
    - `pub fn stage_settings_with_cas(&self, access_conf: &str, keys: &[(&str, &str)], cas: &[(&str, &str)])` and `pub fn bootstrap_settings_with_cas(...)` — `cas` maps path-within-`cadir/` → `delegate_ca` name, exactly as `keys` maps into `keydir/`.
    - `pub fn ssh_push_from_cert(&self, dir: &Path, key: &Path, cert: &Path, repo: &str, refspec: &str) -> Output` and `pub fn ssh_fetch_cert(&self, dir: &Path, key: &Path, cert: &Path, repo: &str) -> Output` — like `ssh_push_from`/`ssh_fetch` but with `-o CertificateFile=<cert>` added to `GIT_SSH_COMMAND` (add `fn ssh_command_for_cert(key: &Path, cert: &Path) -> String` beside `ssh_command_for`, ~line 1782).

- [ ] **Step 1: Harness helpers**

In `tests/common/mod.rs`, implement the five helpers above. `delegate_ca` copies `named_key`'s body (~line 1194) with directory `cas` instead of `keys`. `mint_cert`:

```rust
    /// Mint a certificate with ssh-keygen — the external oracle for what a
    /// valid OpenSSH cert looks like. Returns the `<key>-cert.pub` path.
    /// `validity` is ssh-keygen's -V syntax, e.g. "-1m:+30m".
    pub fn mint_cert(
        &self,
        ca: &Path,
        key: &Path,
        key_id: &str,
        principals: &str,
        validity: &str,
    ) -> PathBuf {
        let cert = PathBuf::from(format!("{}-cert.pub", key.display()));
        let _ = std::fs::remove_file(&cert); // ssh-keygen refuses to overwrite
        let mut cmd = Command::new("ssh-keygen");
        cmd.arg("-s").arg(ca).args(["-I", key_id, "-V", validity]);
        if !principals.is_empty() {
            cmd.args(["-n", principals]);
        }
        cmd.arg(key.with_extension("pub"));
        let output = cmd.output().expect("failed to run ssh-keygen -s");
        assert!(
            output.status.success(),
            "ssh-keygen -s failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        cert
    }
```

`stage_settings_with_cas`: copy `stage_settings`'s body (~line 1276) and append a loop writing `cadir/<rel>` from `self.delegate_ca(name).with_extension("pub")`; `bootstrap_settings_with_cas` stages then force-pushes exactly as `bootstrap_settings` does. Then refactor `stage_settings(a, k)` to call `stage_settings_with_cas(a, k, &[])` so there is one body.

`ssh_command_for_cert`:

```rust
fn ssh_command_for_cert(key: &Path, cert: &Path) -> String {
    format!("{} -o CertificateFile={}", ssh_command_for(key), cert.display())
}
```

- [ ] **Step 2: Write the failing behavioral tests**

Create `tests/delegate_test.rs`:

```rust
mod common;

use std::process::Output;

use common::ServerHarness;

fn stderr(output: &Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}

/// Rules never mention delegates: the person holds the grants, the cert
/// borrows them.
fn access_conf(repo: &str) -> String {
    format!(
        "repo settings\n    RW+ = alex\n\nrepo {repo}\n    RW+ = alex\n    RW+ = bob\n"
    )
}

/// A certificate from an enrolled CA, naming an enrolled person, can read
/// what the person reads.
#[test]
fn a_delegate_certificate_authenticates_and_fetches() {
    let harness = ServerHarness::new("delegate-fetch");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    // The delegate's own key is enrolled NOWHERE — that is the point.
    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let out = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(out.status.success(), "delegate fetch failed: {}", stderr(&out));
}

/// The same certificate on an ungoverned server is nothing: there is no
/// roster to tie its principal to.
#[test]
fn a_certificate_is_rejected_on_an_ungoverned_server() {
    let harness = ServerHarness::new("delegate-ungoverned");
    let _ = harness.ssh_client_key(); // authorized_keys exists, server ungoverned
    harness.push_head();

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let out = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(!out.status.success(), "an ungoverned server accepted a certificate");
}

/// A cert whose CA is enrolled for someone else cannot act as this person.
#[test]
fn a_ca_enrolled_for_another_name_is_rejected() {
    let harness = ServerHarness::new("delegate-wrong-ca");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex"), ("bob.pub", "bob")],
        &[("mint/bob.pub", "mint")], // mint may act for bob, NOT alex
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let out = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(!out.status.success(), "a CA enrolled for bob minted a working delegate of alex");
}
```

- [ ] **Step 3: Run to verify failure**

Run: `cargo test --test delegate_test 2>&1 | tail -15`
Expected: `a_delegate_certificate_authenticates_and_fetches` FAILS (server has no cert handler → auth rejected); the two rejection tests may pass vacuously — that is fine, they exist to pin the behavior against regressions once the handler lands.

- [ ] **Step 4: Implement in `session.rs`**

1. Identity storage:

```rust
use russh::keys::ssh_key::Certificate;

enum AuthIdentity {
    /// An enrolled key (or authorized_keys, ungoverned): the principal string
    /// is the fingerprint form ssh_key_principal produces.
    Key { principal: String },
    /// A delegate certificate. Kept whole so the regime can re-validate it
    /// per request — expiry and cadir/keydir membership are checked on every
    /// command, not once at connection open.
    Delegate { certificate: Box<Certificate> },
}
```

Replace `authenticated_principal: Option<String>` with `authenticated: Option<AuthIdentity>` (constructor too). In `auth_publickey`'s accept arm, store `AuthIdentity::Key { principal }`.

2. New handler, beside `auth_publickey`. russh calls it only after verifying the client holds the cert's private key; russh has also already checked expiry and the embedded signature, but we re-check both via `validate` because the regime path needs them anyway:

```rust
    async fn auth_openssh_certificate(
        &mut self,
        _user: &str,
        certificate: &Certificate,
    ) -> Result<Auth, Self::Error> {
        let GovernanceState::Active(governance) = governance::load(&self.config.repos_dir)
        else {
            debug!("Certificate auth rejected: server is not governed");
            return Ok(Auth::reject());
        };
        match governance::delegate::validate(certificate, &governance, unix_now()) {
            Ok(delegate) => {
                info!(
                    "Certificate auth accepted: {} (via {})",
                    delegate.person, delegate.key_id
                );
                self.authenticated = Some(AuthIdentity::Delegate {
                    certificate: Box::new(certificate.clone()),
                });
                Ok(Auth::Accept)
            }
            Err(reason) => {
                debug!("Certificate auth rejected: {reason}");
                Ok(Auth::reject())
            }
        }
    }
```

with, at module level:

```rust
fn unix_now() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}
```

(`unwrap_or(0)` fails closed: time-before-epoch validates nothing.)

3. `Regime::Governed` gains the marker:

```rust
    Governed {
        governance: Box<Governance>,
        name: String,
        /// `Some(key_id)` when this session is a certificate, acting for
        /// `name` under the hard-coded collab-refs ceiling.
        delegate: Option<String>,
    },
```

4. `regime()` becomes parameterless and dispatches on identity. The existing body is the `Key` arm (with `delegate: None` added to its `Governed` construction); the new arm:

```rust
    fn regime(&self) -> Regime {
        let Some(identity) = self.authenticated.as_ref() else {
            return Regime::Closed;
        };
        match identity {
            AuthIdentity::Key { principal } => { /* existing body, fingerprint = principal */ }
            AuthIdentity::Delegate { certificate } => {
                match governance::load(&self.config.repos_dir) {
                    GovernanceState::Active(governance) => {
                        match governance::delegate::validate(certificate, &governance, unix_now())
                        {
                            Ok(d) => Regime::Governed {
                                governance,
                                name: d.person,
                                delegate: Some(d.key_id),
                            },
                            Err(reason) => {
                                warn!("Delegate no longer valid, closing: {reason}");
                                Regime::Closed
                            }
                        }
                    }
                    GovernanceState::Unreadable(reason) => {
                        error!("Settings repository is unreadable, closing everything: {reason}");
                        Regime::Closed
                    }
                    // Governance turned off since auth: the delegate's whole
                    // basis is gone.
                    GovernanceState::Absent => Regime::Closed,
                }
            }
        }
    }
```

5. Exec handler (~line 631): the `principal` local is used for logging and for ungoverned `server.toml` matching. Derive it from the identity:

```rust
        let principal = match self.authenticated.as_ref() {
            Some(AuthIdentity::Key { principal }) => principal.clone(),
            Some(AuthIdentity::Delegate { certificate }) => {
                format!("delegate:{}", certificate.key_id())
            }
            None => { /* existing not-authenticated rejection */ }
        };
        // ...
        let regime = self.regime();
```

A delegate identity can never reach `Regime::Ungoverned`, so the `delegate:` label never hits `server.toml` policy matching — it only appears in logs.

6. Fix every `Regime::Governed { .. }` construction/match site the compiler reports: `receive_pack_env` (~line 148, add `..` for now — Task 5 uses `delegate` there), the exec `authorized` match (~line 693, bind `delegate` but ignore for now), the create-permission block (~line 715), `record_creator` block (~line 737), and `handle_release_command` (~line 197). This task changes no authorization outcomes for delegates beyond what `regime()` yields; the ceiling lands in Task 5.

- [ ] **Step 5: Run to verify pass, then the whole suite**

Run: `cargo test --test delegate_test 2>&1 | tail -8`, then `cargo test 2>&1 | grep -cE "test result: FAILED"` (expect `0`), then clippy.
Expected: all three delegate tests pass; every existing governance/server test unchanged.

- [ ] **Step 6: Commit**

```bash
git add src/server/ssh/session.rs tests/common/mod.rs tests/delegate_test.rs
git commit -m "Certificates authenticate as delegates of the person they name"
```

---

### Task 5: The ceiling — collab refs only, nothing else

**Files:**
- Modify: `src/server/governance/hook.rs` — env consts (~line 28), `run()` (~line 115)
- Modify: `src/server/ssh/session.rs` — `receive_pack_env` (~line 117), exec create block (~line 715), `handle_release_command` (~line 193)
- Modify: `tests/delegate_test.rs`

**Interfaces:**
- Consumes: `Regime::Governed { delegate, .. }` from Task 4; hook env plumbing from `receive_pack_env`.
- Produces: `pub const ENV_DELEGATE: &str = "GIT_COLLAB_DELEGATE";` in `hook.rs`. Hook refuses any ref outside `refs/collab/` when it is set. Exec denies `Access::Create` and release mutation to delegates.

- [ ] **Step 1: Write the failing behavioral tests**

Append to `tests/delegate_test.rs`:

```rust
/// The ceiling: a delegate writes collab refs with the person's authority —
/// and cannot move a branch the person holds RW+ on.
#[test]
fn a_delegate_writes_collab_refs_and_may_not_write_branches() {
    let harness = ServerHarness::new("delegate-ceiling");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    harness.work_repo().issue_open("Filed by a delegate");
    let push = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        "refs/collab/*:refs/collab/*",
    );
    assert!(push.status.success(), "delegate collab push failed: {}", stderr(&push));

    harness.work_repo().commit_file("d.txt", "delegate", "delegate commit");
    let push = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        "main:main",
    );
    assert!(
        !push.status.success(),
        "a delegate moved a branch; alex holds RW+ but the cert must not inherit it"
    );
    assert!(
        stderr(&push).contains("refs/collab"),
        "the refusal should name the ceiling, got: {}",
        stderr(&push)
    );
}

/// The person's own key is untouched by the ceiling.
#[test]
fn the_person_still_writes_branches_directly() {
    let harness = ServerHarness::new("delegate-person-unaffected");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );
    harness.work_repo().commit_file("p.txt", "person", "person commit");
    let push = harness.ssh_push(&harness.named_key("alex"), "main:main");
    assert!(push.status.success(), "the person's own push failed: {}", stderr(&push));
}

/// Creation is a permission delegates never hold, so CREATOR can never
/// resolve to one.
#[test]
fn a_delegate_may_not_create_a_repository_its_person_could() {
    let harness = ServerHarness::new("delegate-create");
    harness.push_head();
    let conf = format!(
        "repo settings\n    RW+ = alex\n\nrepo {}\n    RW+ = alex\n\nrepo agents/[a-z-]+\n    C = alex\n    RW+ = CREATOR\n",
        harness.repo_name()
    );
    harness.bootstrap_settings_with_cas(
        &conf,
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    harness.work_repo().commit_file("n.txt", "new", "seed");
    let push = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        "agents/claude-a",
        "main:main",
    );
    assert!(!push.status.success(), "a delegate created a repository");
    assert!(
        !harness.repos_dir().join("agents/claude-a.git").exists(),
        "the repository must not exist after a refused create"
    );
}
```

- [ ] **Step 2: Run to verify failure**

Run: `cargo test --test delegate_test 2>&1 | tail -15`
Expected: `a_delegate_writes_collab_refs_and_may_not_write_branches` fails at the branch-push assert (delegate currently inherits full RW+); `a_delegate_may_not_create_a_repository_its_person_could` fails (create allowed via person's `C`). The person test passes.

- [ ] **Step 3: Implement**

`hook.rs` — new const with the others:

```rust
/// Set only when the pushing session is a delegate certificate; its value is
/// the cert's key ID. Presence is what puts the collab-refs ceiling in force.
pub const ENV_DELEGATE: &str = "GIT_COLLAB_DELEGATE";
```

In `run()`, inside the `GovernanceState::Active` block, *before* the rule check (the ceiling is not a rule — it precedes them all):

```rust
        // The delegate ceiling. Hard-coded rather than configured: no line in
        // access.conf can widen what a certificate may write.
        let delegate = std::env::var(ENV_DELEGATE).ok().filter(|v| !v.is_empty());
        if let Some(key_id) = &delegate {
            if !refname.starts_with("refs/collab/") {
                return Err(format!(
                    "delegate {key_id} of {principal} may only write refs/collab/*, \
                     not {refname}"
                ));
            }
        }
```

`session.rs` `receive_pack_env` — extend the `Governed` arm:

```rust
        if let Regime::Governed { name, delegate, .. } = regime {
            env.push((governance::hook::ENV_PRINCIPAL.to_string(), name.clone()));
            if let Some(key_id) = delegate {
                env.push((governance::hook::ENV_DELEGATE.to_string(), key_id.clone()));
            }
        }
```

Exec create block (~line 715) — deny before consulting the person's rules:

```rust
            if let Regime::Governed { governance, name, delegate } = &regime {
                let allowed = delegate.is_none()
                    && repo_key.as_deref().is_some_and(|key| {
                        governance
                            .conf
                            .allows_repo(key, &Subject::new(name), Access::Create)
                    });
                if !allowed { /* existing rejection */ }
            }
```

`handle_release_command` (~line 193): where `needed` is computed, add after it:

```rust
        // Artifacts are not collab refs. A delegate may list what its person
        // may see; publishing and deleting are outside the ceiling.
        if let Regime::Governed { delegate: Some(key_id), .. } = regime {
            if needed != Access::Read {
                warn!("Rejected release command from delegate {key_id}");
                return reply_and_close(session, channel, NOT_FOUND, 1);
            }
        }
```

- [ ] **Step 4: Run to verify pass, then the whole suite and clippy**

Run: `cargo test --test delegate_test 2>&1 | tail -8`; `cargo test 2>&1 | grep -cE "test result: FAILED"` → `0`.

- [ ] **Step 5: Commit**

```bash
git add src/server/governance/hook.rs src/server/ssh/session.rs tests/delegate_test.rs
git commit -m "Delegates write refs/collab/* and nothing else"
```

---

### Task 6: Revocation cascades and push validation, end to end

**Files:**
- Modify: `tests/delegate_test.rs`
- Modify (only if a test exposes a gap): `src/server/governance/mod.rs`

**Interfaces:**
- Consumes: everything above. No new production surface expected — these tests pin the properties the spec promises fall out of per-request re-reads and Task 2's validation.

- [ ] **Step 1: Write the tests**

Append to `tests/delegate_test.rs`:

```rust
/// Removing the CA enrolment kills the delegates it minted, on their next
/// command — no restart, no KRL, and the cert itself is still inside its
/// validity window.
#[test]
fn removing_the_cadir_entry_cuts_the_delegate_off() {
    let harness = ServerHarness::new("delegate-revoke-ca");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let before = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(), &agent_key, &cert, harness.repo_name());
    assert!(before.status.success(), "delegate should work before revocation");

    // Re-bootstrap with the cadir entry gone: same conf, same keys, no CAs.
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[],
    );

    let after = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(), &agent_key, &cert, harness.repo_name());
    assert!(!after.status.success(), "the delegate outlived its CA enrolment");
}

/// Removing the person kills the person's delegates: cadir/ lends identity,
/// keydir/ is what makes it exist.
#[test]
fn removing_the_person_kills_their_delegates() {
    let harness = ServerHarness::new("delegate-revoke-person");
    harness.push_head();
    // Two people, so removing bob leaves a valid config (alex retains RW+ on
    // settings — the lockout check requires someone does).
    let conf = format!(
        "repo settings\n    RW+ = alex\n\nrepo {}\n    RW+ = alex\n    RW+ = bob\n",
        harness.repo_name()
    );
    harness.bootstrap_settings_with_cas(
        &conf,
        &[("alex.pub", "alex"), ("bob.pub", "bob")],
        &[("mint/bob.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "bob-agent", "bob", "-1m:+30m");

    let before = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(), &agent_key, &cert, harness.repo_name());
    assert!(before.status.success(), "bob's delegate should work while bob exists");

    // bob leaves; his cadir entry remains — and must grant nothing.
    harness.bootstrap_settings_with_cas(
        &conf,
        &[("alex.pub", "alex")],
        &[("mint/bob.pub", "mint")],
    );

    let after = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(), &agent_key, &cert, harness.repo_name());
    assert!(!after.status.success(), "a delegate outlived its person");
}

/// An expired certificate is rejected at the door.
#[test]
fn an_expired_certificate_does_not_authenticate() {
    let harness = ServerHarness::new("delegate-expired");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-30m:-1m");

    let out = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(), &agent_key, &cert, harness.repo_name());
    assert!(!out.status.success(), "an expired certificate authenticated");
}

/// A settings push carrying a malformed cadir/ file is refused whole; the
/// previous config keeps governing.
#[test]
fn a_malformed_cadir_file_rejects_the_settings_push() {
    let harness = ServerHarness::new("delegate-bad-cadir");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    // Stage a broken CA file in the settings work tree and push over SSH.
    let work = harness.settings_work_dir();
    std::fs::write(work.join("cadir").join("junk.pub"), "not a key").unwrap();
    common::git_cmd(&work, &["add", "-A"]);
    common::git_cmd(&work, &["commit", "-q", "-m", "break cadir"]);

    let push = harness.push_settings_over_ssh(&harness.named_key("alex"));
    assert!(!push.status.success(), "a malformed cadir file was accepted");
    assert!(
        stderr(&push).contains("junk.pub"),
        "the refusal should name the file, got: {}",
        stderr(&push)
    );
}
```

`settings_work_dir` does not exist yet: `settings_work()` on the harness is private. Make it `pub fn settings_work_dir(&self) -> PathBuf { self.settings_work() }` in `tests/common/mod.rs` (or just make `settings_work` pub — match the file's existing style of small pub accessors).

- [ ] **Step 2: Run**

Run: `cargo test --test delegate_test 2>&1 | tail -20`
Expected: all four SHOULD pass already — they pin behavior Tasks 2–5 built. If any fails, that is a real gap: debug it (systematic-debugging skill), fix in the named production file, and note in the commit message which property had not actually held.

- [ ] **Step 3: Full suite + clippy + fmt on touched files**

Run: `cargo test 2>&1 | grep -cE "test result: FAILED"` → `0`; `cargo clippy --all-targets` clean; `rustfmt --edition 2021 tests/delegate_test.rs tests/common/mod.rs src/server/governance/cadir.rs src/server/governance/delegate.rs`.

- [ ] **Step 4: Commit**

```bash
git add tests/delegate_test.rs tests/common/mod.rs
git commit -m "Pin the delegate revocation cascade and cadir push validation"
```

---

### Task 7: Document the model

**Files:**
- Modify: `README.md` — inside the Governance section, after the `keydir/` explanation (~line 445–460)

- [ ] **Step 1: Write the subsection**

Insert after the paragraph explaining that a principal's name is the basename (~line 452), keeping the README's voice (present tense, mechanism-first, one idea per paragraph):

```markdown
#### Delegates

A key in `keydir/` is a person. A certificate is a **delegate** of the person
it names, and may write `refs/collab/*` and nothing else.

`cadir/` mirrors `keydir/`, but answers the other question — not "which keys
are this person" but "which CAs may mint delegates of them":

​```text
settings.git
├── keydir/xps14/alex.pub        who you are
└── cadir/mint/alex.pub          who may act as you
​```

Any OpenSSH CA works. Mint a short-lived credential and hand it to an agent:

​```console
$ ssh-keygen -s mint -I claude-a -n alex -V +10m agent_key.pub
​```

The cert's principal must name an enrolled person and its CA must be enrolled
*for that name* — `cadir/` lends identity, it never creates it. `access.conf`
is never consulted about delegates and cannot widen them: no rule grants a
certificate a branch, a release, or a repository creation. The same CA key
enrolled under two names is allowed (unlike `keydir/`, where one key under two
names is an authorization coin-flip): a certificate names its principal, so
the lookup runs the other way, and a shared CA is two explicit opt-ins.

Revocation is the roster: remove `cadir/mint/alex.pub` and the delegates it
minted die on their next command; remove the person's keys and their
delegates die with them. The cert's own expiry does the rest — there is no
revocation list to maintain.
```

(The ` ```text `/` ```console ` fences above are shown escaped; write them as normal fences.)

- [ ] **Step 2: Verify the claims against the tests**

Every sentence in the subsection must be pinned by a test from Tasks 4–6. Re-read both; if a claim has no test, either add the test or cut the claim.

- [ ] **Step 3: Commit**

```bash
git add README.md
git commit -m "Document delegates: certificates under the collab-refs ceiling"
```

---

## Self-review notes (already applied)

- Spec coverage: trust layout → T1/T2; authentication → T3/T4; authorization ceiling incl. releases and create → T5; attribution (log lines) → T4 step 4.2; revocation → T6; push validation → T2/T6; testing-as-oracle → every cert minted by `ssh-keygen`; non-goals need no tasks.
- The spec's "certificate presented to an ungoverned server" and "wrong-name CA" behavioral cases live in T4; zero/two-principal, host-cert, critical-option, unenrolled-principal edge cases live in T3 as unit tests against `validate` — the OpenSSH *client* cannot be relied on to transmit all of those shapes, so the function boundary is where they are testable deterministically.
- russh already rejects expired certs and bad embedded signatures before our handler runs; `validate` re-checks anyway because `regime()` calls it per request long after auth.
- Type consistency: `Regime::Governed { governance, name, delegate }` — T4 defines, T5 consumes; `ENV_DELEGATE` — T5 defines and consumes; harness helpers defined in T4 step 1, consumed T4–T6 with matching signatures.