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 } = ®ime {
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.