Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-server/src/api/account.rs

//! **The keys a person may add to their OWN name, and nothing else.**
//!
//! # Why this module exists at all, when `api::access` already adds keys
//!
//! `/api/keys` is on `role_floor`'s `ADMIN_SURFACES` list and belongs there: a
//! POST to it names any `principal_id` on the box, so it hands out SSH access
//! to somebody else's namespace. That floor is right and it is untouched.
//!
//! What it left behind was a hole with a person in it. A tenant who has just
//! registered a passkey through a paid signup is a **viewer**. They can read
//! the console and they can do nothing with git — gunnar authenticates the KEY
//! over SSH and has never heard of them — and the one action that would fix
//! that was behind an admin door. The console's own first-run nudge pointed at
//! a form that would refuse them.
//!
//! So: a second surface, for the one act that is nobody's business but the
//! actor's.
//!
//! # What makes a viewer's floor safe here, and it is not the floor
//!
//! **The target is never a parameter.** Every handler in this file takes
//! [`Me`], which reads the session's own user name out of the extension
//! `require_auth` inserted, and there is no path segment, query parameter or
//! body field that can change it — [`OwnKey`] REFUSES a body that carries
//! `user` or `principal_id` rather than ignoring it, so a client that tries is
//! told, and the test that proves it cannot pass by accident.
//!
//! That is the whole argument. A viewer may write here because there is
//! nothing to escalate TO: the most a caller can do is register a key against
//! the name they already log in as, which is what "add my own key" means.
//!
//! Two consequences follow and both are enforced below:
//!
//! * **A name with no principal is not created.** If gunnar has no principal
//!   named like the session, this refuses and says so. Minting one would be
//!   `Access.AddPrincipal` — creating an identity and its namespace — from a
//!   viewer's floor, which is the escalation this design is built to not have.
//! * **Every call is signed AS THE PERSON** (thin layer, owner 2026-10-04).
//!   gunnar's self-scope rule decides: a principal may list, add and revoke
//!   its OWN keys as itself, and anything else — somebody else's
//!   fingerprint — is refused there. Until then this console checked
//!   membership itself and signed with its own server-wide key.
//!
//! # Not to be confused with `api::accounts`
//!
//! `api::accounts` is the ADMIN surface — `Accounts.Gate` locks somebody out
//! of everything and `Accounts.Purge` empties their account. `/api/accounts`
//! is on the admin list; `/api/account` (singular) is this, and it is a
//! viewer's. The two paths are one letter apart, so
//! `the_own_account_floor_does_not_reach_the_admin_accounts_surface` asserts
//! the ladder tells them apart rather than trusting the reader to.

use axum::extract::{Query, State};
use axum::Json;
use serde::Deserialize;

use crate::api::access::{render_key, Key};
use crate::api::{ApiError, ApiResult, Page};
use crate::app::AppState;
use crate::auth::AUDIT_TARGET;
use crate::grpc::Fail;
use crate::pb;

/// **Whose keys this request is about, decided before a handler exists to
/// forget.**
///
/// An extractor and not a helper, for the reason [`crate::api::Reader`] and
/// `buy_site::GunnarSite::operator` (was `increase::Ordering`) are: a helper is something a handler can forget to
/// call, and forgetting here means a viewer writing to somebody else's
/// identity. A handler either has this name in its signature or it has no name
/// to act for.
///
/// Three callers, three answers:
///
/// * a passkey session — its user name, which is the only interesting case;
/// * the **break-glass admin token** — refused. It is a machine credential
///   held by whoever runs the process, not a person, and there is no principal
///   named after it. An admin with a token that wants to add somebody a key
///   has `/api/keys`, which is what that door is for.
/// * **no session at all** — refused, including in the open-console posture
///   (`trunk serve` on loopback), where every request runs as admin and still
///   has no name. `buy_site::GunnarSite::operator` (was `increase::Ordering`) calls that caller "the open console";
///   here there is no principal by that name either, so the honest answer is
///   the same refusal.
#[derive(Debug, Clone)]
pub struct Me(pub String);

impl axum::extract::FromRequestParts<AppState> for Me {
    type Rejection = ApiError;

    async fn from_request_parts(
        parts: &mut http::request::Parts,
        _state: &AppState,
    ) -> Result<Me, ApiError> {
        match parts.extensions.get::<crate::auth::middleware::Caller>() {
            Some(crate::auth::middleware::Caller::Operator(session)) => {
                Ok(Me(session.user.clone()))
            }
            Some(crate::auth::middleware::Caller::AdminToken) => Err(refuse(
                "permissiondenied",
                "the break-glass token is not a person and has no keys of its own; add a key \
                 for somebody through /api/keys",
            )),
            None => Err(refuse(
                "permissiondenied",
                "this needs a session that can be named: these are the keys of whoever is \
                 logged in, and nobody is",
            )),
        }
    }
}

fn refuse(code: &str, message: &str) -> ApiError {
    ApiError(Fail {
        code: code.to_owned(),
        message: message.to_owned(),
    })
}

/// One key, as this surface accepts it.
///
/// **`user` and `principal_id` are here to be REFUSED.** They are not ignored:
/// serde drops an unknown field silently, and a client that sent
/// `{"user":"someone-else", …}` would get a 200 and a key on the wrong name
/// with nothing to tell it otherwise. Naming them makes the attempt a 400 that
/// says what happened, and makes
/// `a_body_that_names_somebody_else_is_refused_and_lands_on_nobody` a test
/// that cannot pass by accident.
#[derive(Debug, Clone, Deserialize)]
pub struct OwnKey {
    /// One line in OpenSSH `authorized_keys` format. gunnar parses it and
    /// derives the fingerprint itself — a fingerprint the client chose is not
    /// one the SSH listener could ever match.
    pub openssh_public_key: String,
    #[serde(default)]
    pub comment: String,
    #[serde(default)]
    pub principal_id: Option<String>,
    #[serde(default)]
    pub user: Option<String>,
}

/// Which key to revoke.
///
/// In the QUERY string and not a path segment, deliberately: a fingerprint is
/// `SHA256:` followed by base64, which contains `/`, and axum's `{param}`
/// matches one segment. `api::policy::delete` takes its ref pattern the same
/// way and for the same reason. (`/api/keys/{fingerprint}` predates this and
/// is left alone; it is reached by an admin with a percent-encoded value.)
#[derive(Debug, Clone, Deserialize)]
pub struct Which {
    pub fingerprint: String,
}

/// **The principal this console user is**: gunnar's answer when it verified
/// their passkey (the identity core's session names the principal),
/// carried by their session key (thin layer L). Never a name looked up: the
/// session's own key says who they are.
///
/// # Errors
///
/// The session holds no key at gunnar any more (log in again).
pub(crate) async fn principal_of(state: &AppState, user: &str) -> Result<String, ApiError> {
    state
        .webauthn
        .as_ref()
        .and_then(|webauthn| webauthn.principal_of(user))
        .ok_or_else(|| {
            refuse(
                "failedprecondition",
                "this sign-in holds no key at Gunnar any more, so this console cannot say which \
                 account it is; log in again",
            )
        })
}

/// Every key gunnar holds for `principal`.
///
/// Filtered here as well as asked for by id. gunnar narrows `ListKeys` by the
/// `principal_id` in the request, so the second pass can only ever remove rows
/// — the same "one `==` against a deployment mistake" argument
/// `routes::public_repos` makes, and the same reason it is acceptable there:
/// it cannot add a row, and what it defends against is this console asking the
/// wrong question rather than gunnar answering one wrongly.
async fn keys_of(
    state: &AppState,
    gunnar: &crate::grpc::Gunnar,
    user: &str,
    principal: &str,
) -> Result<Vec<Key>, ApiError> {
    let listed = gunnar
        .access()
        .list_keys(pb::ListKeysRequest {
            principal_id: principal.to_owned(),
        })
        .await
        .map_err(ApiError::from)?
        .into_inner();
    let consoles = console_key_of(state, user).await;
    Ok(listed
        .keys
        .into_iter()
        .filter(|k| k.principal_id == principal)
        .filter(|k| !consoles.contains(&k.fingerprint))
        .map(render_key)
        .collect())
}

/// **The fingerprints of the session keys THIS CONSOLE signs with on `user`'s
/// principal** — gunnar enrolled them at their passkey logins (thin layer L).
///
/// They are registered under their principal, so `Access.ListKeys` returns
/// them and this page would otherwise show them as the person's own — keys
/// they never made, with a delete button beside them that ends the session
/// they are reading with. So they are filtered out of the listing and refused
/// at the revocation; logging out is the lever that retires them.
async fn console_key_of(state: &AppState, user: &str) -> Vec<String> {
    // A person's identity-core session holds no key at gunnar (IDENTITY-HOST):
    // only a test's planted custody key is the console's on them.
    let mut held = Vec::new();
    if let Ok(Some(signing)) = crate::api::person_identity(state, user).await {
        if !signing.fingerprint.is_empty() {
            held.push(signing.fingerprint);
        }
    }
    held
}

/// **The person's own credential** — their session key, which gunnar
/// enrolled on their principal when it verified their passkey. gunnar's
/// self-scope rule lets a principal list, add and revoke its OWN keys as
/// itself — no grant, and no attestation: the request's signature by one of
/// that principal's keys IS the consent — and refuses anything else in its
/// own words.
async fn as_the_person(
    state: &AppState,
    user: &str,
    _principal: &str,
) -> Result<crate::grpc::Gunnar, ApiError> {
    match crate::api::person_identity(state, user).await {
        Ok(Some(identity)) => Ok(identity.gunnar),
        Ok(None) => Err(refuse(
            "failedprecondition",
            "this sign-in holds no key at Gunnar any more; log in again",
        )),
        Err(why) => Err(tenant_refusal(why)),
    }
}

/// `GET /api/account/keys` — the keys registered against the session's name,
/// asked of gunnar AS THE PERSON.
pub async fn list(Me(user): Me, State(state): State<AppState>) -> ApiResult<Page<Key>> {
    let principal = principal_of(&state, &user).await?;
    let gunnar = as_the_person(&state, &user, &principal).await?;
    let mine = keys_of(&state, &gunnar, &user, &principal).await?;
    Ok(Json(Page::of(mine, usize::MAX)))
}

/// `POST /api/account/keys` — register one, against the session's name.
pub async fn add(
    Me(user): Me,
    State(state): State<AppState>,
    Json(new): Json<OwnKey>,
) -> ApiResult<Key> {
    if new.principal_id.is_some() || new.user.is_some() {
        return Err(refuse(
            "invalidargument",
            "this route registers a key against the session's own name and takes no other; use \
             /api/keys, which needs an admin, to add a key for somebody else",
        ));
    }
    let line = new.openssh_public_key.trim();
    if line.is_empty() {
        return Err(refuse("invalidargument", "no key was pasted"));
    }
    let principal = principal_of(&state, &user).await?;
    // **Signed AS THE PERSON** (thin layer, owner 2026-10-04). gunnar's
    // self-scope rule: a principal adding a key to ITSELF needs no grant and
    // owes no attestation — the call's own signature, by a key the principal
    // already holds, is its word. Until this line the console signed the
    // attestation itself with that key and sent the AddKey with its own
    // server-wide one, which is a BFF vouching for a person to gunnar.
    let gunnar = as_the_person(&state, &user, &principal).await?;
    let added = gunnar
        .access()
        .add_key(pb::AddKeyRequest {
            principal_id: principal,
            openssh_public_key: line.to_owned(),
            comment: new.comment.trim().to_owned(),
            // Not offered here. An expiry a person sets on their own key is a
            // reminder they cannot lift without an admin, and the console has
            // nowhere to renew it from.
            expires_at_unix_ms: None,
            // Owed by nobody: the caller IS the target.
            attestation: None,
        })
        .await
        // **gunnar's own refusal, by name.** A key that already belongs to
        // another principal is refused there, with its words, and this console
        // is not a second opinion about why.
        .map_err(ApiError::from)?
        .into_inner();
    state.caches.invalidate_access().await;
    let key = render_key(added.key.unwrap_or_default());
    tracing::info!(
        target: AUDIT_TARGET,
        event = "key.add",
        by = %user,
        user = %user,
        fingerprint = %key.fingerprint,
        "somebody registered a key against their own name"
    );
    Ok(Json(key))
}

/// `DELETE /api/account/keys?fingerprint=SHA256:…` — remove one of your own.
///
/// **gunnar decides whose key it is** (thin layer, owner 2026-10-04):
/// `Access.RevokeKey` is signed AS THE PERSON, and gunnar's self-scope rule
/// lets a principal revoke only a key it holds — anybody else's fingerprint is
/// refused there, in the same words whether it exists or not. Until this line
/// the console checked membership itself and then revoked with its own
/// server-wide key.
///
/// The one refusal kept here is not a permission: the console's OWN key on
/// this principal is what the person's session reads with, and revoking it
/// would blank their console with nothing saying why.
pub async fn revoke(
    Me(user): Me,
    State(state): State<AppState>,
    Query(which): Query<Which>,
) -> ApiResult<crate::api::repos::Done> {
    let principal = principal_of(&state, &user).await?;
    if console_key_of(&state, &user).await.contains(&which.fingerprint) {
        return Err(refuse(
            "permissiondenied",
            "that key is this console's own, registered on your principal so your session can \
             read as you; revoking it would blank your own console. Revoke the passkey instead \
             and this key goes with it",
        ));
    }
    let gunnar = as_the_person(&state, &user, &principal).await?;
    gunnar
        .access()
        .revoke_key(pb::RevokeKeyRequest {
            fingerprint: which.fingerprint.clone(),
        })
        .await
        .map_err(ApiError::from)?;
    state.caches.invalidate_access().await;
    tracing::warn!(
        target: AUDIT_TARGET,
        event = "key.revoke",
        by = %user,
        user = %user,
        fingerprint = %which.fingerprint,
        "somebody revoked one of their own keys"
    );
    Ok(Json(crate::api::repos::Done { ok: true }))
}

// ── the console's own key on this person's principal ─────────────────────────

/// **What the console's credential on this account is doing**, and what the
/// person has to do about it if it is stuck.
///
/// See [`crate::tenant`] for the mechanism. In short: the console's reads are
/// answered with a key of the tenant's own, and gunnar will only accept a
/// SECOND key on a principal if the principal vouches for it. Everyone who
/// added an SSH key before their first console read is in exactly that state,
/// and until 2026-09-18 they had no way out and no sentence telling them so —
/// their reads silently fell back to the grantless guest and showed the public
/// subset of a box they pay for.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ConsoleKey {
    /// `enrolled`, `awaiting_attestation`, or `absent`.
    ///
    /// A word and not a boolean because there are three states and the third —
    /// no principal behind this session at all — is not a degenerate case of
    /// either other one.
    pub state: String,
    /// The principal this session acts for, when there is one.
    pub principal_id: String,
    /// `SHA256:…` of the console's key: the enrolled one, or the one waiting.
    pub fingerprint: String,
    /// The OpenSSH line to attest. Empty unless something is waiting — a
    /// public key on screen with nothing to do about it is noise.
    pub openssh_public_key: String,
    /// **The exact command to run on the appliance**, composed from the two
    /// values above so nobody has to assemble it from a log line. Empty unless
    /// something is waiting.
    pub command: String,
    /// **gunnar's own sentence**, when it refused. Verbatim: the appliance
    /// knows why and this console does not, and a reworded refusal leaves a
    /// reader comparing two explanations with no way to tell which is true.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub refusal: String,
}

/// The attestation, as `gunnar control user attest-key` printed it.
///
/// The CLI prints `gunnar_core::SignedKeyAttestation` as JSON with the inner
/// attestation FLATTENED (`#[serde(flatten)]`), so these five fields are one
/// object. Mirrored here rather than shared, because this crate vendors
/// gunnar's protos and not its core; `the_attestation_shape_is_gunnars` pins
/// the field names against the vendored proto so the mirror cannot drift
/// quietly.
///
/// Unknown fields are ALLOWED, unlike gunnar's own struct: a newer appliance
/// that adds a field would otherwise make every paste a local parse error on a
/// console that is merely relaying it.
#[derive(Debug, Clone, Deserialize)]
pub struct Attestation {
    pub principal_id: String,
    pub fingerprint: String,
    pub at_unix_ms: i64,
    pub key_fingerprint: String,
    pub signature: String,
}

/// What the browser posts: the CLI's output, pasted whole.
#[derive(Debug, Clone, Deserialize)]
pub struct Attested {
    /// The JSON text `gunnar control user attest-key` printed. Taken as TEXT
    /// rather than as a parsed object so the person pastes exactly what they
    /// were given — a form that asked for five fields separately is a form
    /// somebody fills in wrong at 2am.
    pub attestation: String,
}

/// `GET /api/account/console-key`
///
/// **The key this console signs with for the person: their session key**,
/// which gunnar enrolled on their principal when it verified their passkey
/// (thin layer L). Always `enrolled` for a live session — nothing waits on the
/// person's word any more, because the verified passkey IS their consent.
pub async fn console_key(Me(user): Me, State(state): State<AppState>) -> ApiResult<ConsoleKey> {
    match crate::api::person_identity(&state, &user)
        .await
        .map_err(tenant_refusal)?
    {
        Some(identity) => Ok(Json(ConsoleKey {
            state: "enrolled".into(),
            principal_id: identity.principal_id,
            fingerprint: identity.fingerprint,
            openssh_public_key: String::new(),
            command: String::new(),
            refusal: String::new(),
        })),
        None => Ok(Json(ConsoleKey {
            state: "absent".into(),
            principal_id: String::new(),
            fingerprint: String::new(),
            openssh_public_key: String::new(),
            command: String::new(),
            refusal: "this sign-in holds no key at Gunnar any more; log in again".into(),
        })),
    }
}

/// `POST /api/account/console-key/attestation`
///
/// **Nothing waits on an attestation any more** (thin layer L): the session's
/// key is enrolled by gunnar when it verifies the person's passkey. Kept as a
/// route so an old page gets a sentence rather than a 404.
pub async fn attest_console_key(
    Me(_user): Me,
    State(_state): State<AppState>,
    Json(_body): Json<Attested>,
) -> ApiResult<ConsoleKey> {
    Err(refuse(
        "failedprecondition",
        "no console key waits on an attestation: Gunnar enrols your session's key when it \
         verifies your passkey",
    ))
}

/// gunnar's refusal, or this console's, in the shape the console renders.
///
/// [`crate::tenant::TenantError`] already carries gunnar's sentence verbatim;
/// this only gives it a code, and never reworks the words.
fn tenant_refusal(why: crate::tenant::TenantError) -> ApiError {
    refuse("failedprecondition", &why.to_string())
}

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

    use std::sync::{Arc, Mutex};

    use tower::ServiceExt as _;

    use crate::pb::access_server::{Access as AccessService, AccessServer};

    /// The two people on this fixture's box. `anna` is the tenant who just
    /// signed up and holds no key; `olof` is somebody else, and every test that
    /// matters here is about anna not being able to touch his.
    const ANNA: &str = "p-anna";
    const OLOF: &str = "p-olof";
    /// Olof's key, and the fingerprint anna can read off `GET /api/keys` — which
    /// is exactly why `Access.RevokeKey` taking a fingerprint and nothing else
    /// has to be checked on this side.
    const OLOFS_KEY: &str = "SHA256:olofolofolofolof";

    /// Percent-encode a fingerprint for the query string, the way the console
    /// does (`gunnar_ui_web::api::segment`): everything outside RFC 3986's
    /// unreserved set. A raw `+` in a query value decodes as a SPACE, and a
    /// real base64 fingerprint has them.
    fn encoded(value: &str) -> String {
        value
            .bytes()
            .map(|b| match b {
                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                    (b as char).to_string()
                }
                other => format!("%{other:02X}"),
            })
            .collect()
    }

    fn unimplemented<T>(what: &str) -> Result<T, tonic::Status> {
        Err(tonic::Status::unimplemented(format!(
            "not this fixture's: {what}"
        )))
    }

    /// A gunnar with a principal table and a key table, and a log of the calls
    /// it was asked to make.
    #[derive(Clone)]
    struct FakeGunnar {
        keys: Arc<Mutex<Vec<pb::PublicKey>>>,
        calls: Arc<Mutex<Vec<String>>>,
        /// What `AddKey` refuses with, when it is set. gunnar refuses a key
        /// that already belongs to another principal, and the console must pass
        /// that sentence on rather than invent one.
        refuse_add: Option<String>,
        /// **gunnar's attestation rule, as this fixture keeps it**: a SECOND
        /// key on a principal needs the principal's own word. Off by default so
        /// every older row here is unaffected; on for the rows about the
        /// deadlock it caused.
        require_attestation: bool,
        /// Every attestation `AddKey` was handed, so a test can assert the
        /// console relayed one rather than merely stopped failing.
        attestations: Arc<Mutex<Vec<pb::SignedKeyAttestation>>>,
        /// Every key call, as `<Method> by <who>` — `console`, the principal
        /// whose key signed it, or `stranger` (thin layer, 2026-10-04).
        by: Arc<Mutex<Vec<String>>>,
    }

    impl Default for FakeGunnar {
        fn default() -> FakeGunnar {
            FakeGunnar {
                keys: Arc::new(Mutex::new(vec![pb::PublicKey {
                    fingerprint: OLOFS_KEY.to_owned(),
                    algo: "ssh-ed25519".to_owned(),
                    principal_id: OLOF.to_owned(),
                    comment: "olof's laptop".to_owned(),
                    added_at_unix_ms: 1,
                    last_used_unix_ms: None,
                    expires_at_unix_ms: None,
                }])),
                calls: Arc::new(Mutex::new(Vec::new())),
                refuse_add: None,
                require_attestation: false,
                attestations: Arc::new(Mutex::new(Vec::new())),
                by: Arc::new(Mutex::new(Vec::new())),
            }
        }
    }

    /// The console's own signing seed in this fixture (see [`console`]).
    const CONSOLE_SEED: [u8; 32] = [21u8; 32];

    impl FakeGunnar {
        fn record(&self, what: String) {
            self.calls.lock().expect("the call log").push(what);
        }

        fn calls(&self) -> Vec<String> {
            self.calls.lock().expect("the call log").clone()
        }

        /// Whose key signed this call, by gunnar's key table. A key with no
        /// row here is this fixture's administrator — the console's own key,
        /// or one of the admin clients the tenant-key rows build by seed.
        fn who<T>(&self, request: &tonic::Request<T>) -> String {
            let fingerprint = request
                .metadata()
                .get(crate::signer::CREDENTIAL_METADATA_KEY)
                .and_then(|v| v.to_str().ok())
                .and_then(|v| v.split_whitespace().nth(1).map(str::to_owned))
                .unwrap_or_default();
            let console = crate::signer::Signer::new(ssh_key::PrivateKey::from(
                ssh_key::private::Ed25519Keypair::from_seed(&CONSOLE_SEED),
            ))
            .expect("the console's signer")
            .fingerprint()
            .to_owned();
            if fingerprint == console {
                return "console".to_owned();
            }
            self.keys
                .lock()
                .expect("the key table")
                .iter()
                .find(|k| k.fingerprint == fingerprint)
                .map(|k| k.principal_id.clone())
                .unwrap_or_else(|| "console".to_owned())
        }

        fn signed(&self, method: &str, who: &str) {
            self.by.lock().expect("the by log").push(format!("{method} by {who}"));
        }

        fn by(&self) -> Vec<String> {
            self.by.lock().expect("the by log").clone()
        }

        fn fingerprints_of(&self, principal: &str) -> Vec<String> {
            self.keys
                .lock()
                .expect("the key table")
                .iter()
                .filter(|k| k.principal_id == principal)
                .map(|k| k.fingerprint.clone())
                .collect()
        }
    }

    #[tonic::async_trait]
    impl AccessService for FakeGunnar {
        async fn list_principals(
            &self,
            _request: tonic::Request<pb::ListPrincipalsRequest>,
        ) -> Result<tonic::Response<pb::ListPrincipalsResponse>, tonic::Status> {
            self.record("Access.ListPrincipals".to_owned());
            Ok(tonic::Response::new(pb::ListPrincipalsResponse {
                principals: vec![
                    pb::Principal {
                        id: ANNA.to_owned(),
                        name: "anna".to_owned(),
                        kind: pb::PrincipalKind::User as i32,
                        created_at_unix_ms: 1,
                    },
                    pb::Principal {
                        id: OLOF.to_owned(),
                        name: "olof".to_owned(),
                        kind: pb::PrincipalKind::User as i32,
                        created_at_unix_ms: 1,
                    },
                ],
                next_page_token: String::new(),
                ..Default::default()
            }))
        }

        async fn list_keys(
            &self,
            request: tonic::Request<pb::ListKeysRequest>,
        ) -> Result<tonic::Response<pb::ListKeysResponse>, tonic::Status> {
            let who = self.who(&request);
            let asked = request.into_inner();
            self.record(format!("Access.ListKeys {}", asked.principal_id));
            self.signed("Access.ListKeys", &who);
            // gunnar's rule: the server's admin lists any; a principal its
            // OWN (self-scope); anybody else is refused.
            if who != "console" && (asked.principal_id.is_empty() || asked.principal_id != who) {
                return Err(tonic::Status::permission_denied(format!(
                    "principal {who} has no admin grant on server"
                )));
            }
            let held = self.keys.lock().expect("the key table");
            let keys = held
                .iter()
                .filter(|k| asked.principal_id.is_empty() || k.principal_id == asked.principal_id)
                .cloned()
                .collect();
            Ok(tonic::Response::new(pb::ListKeysResponse { keys }))
        }

        async fn add_key(
            &self,
            request: tonic::Request<pb::AddKeyRequest>,
        ) -> Result<tonic::Response<pb::AddKeyResponse>, tonic::Status> {
            let who = self.who(&request);
            let asked = request.into_inner();
            self.record(format!("Access.AddKey {}", asked.principal_id));
            self.signed("Access.AddKey", &who);
            if let Some(refusal) = &self.refuse_add {
                return Err(tonic::Status::already_exists(refusal.clone()));
            }
            // gunnar's self-scope: a principal adding a key to ITSELF needs no
            // grant and owes no attestation. Anybody else needs the server's
            // Admin (the console's key, here).
            let own = who == asked.principal_id;
            if !own && who != "console" {
                return Err(tonic::Status::permission_denied(format!(
                    "principal {who} has no admin grant on server"
                )));
            }
            // gunnar's own precondition, in its own words: a principal that
            // already holds a signing key has somebody to ask, so a caller who
            // is not the target must bring their answer.
            if self.require_attestation && !own {
                // **gunnar's rule exactly**: a principal holding NO key has
                // nobody to ask, so the first enrolment is free. Refusing that
                // one too would make the fixture describe a gunnar that cannot
                // bootstrap anybody, and the console's whole first-enrolment
                // path would look broken in tests and work in production.
                let held = self.fingerprints_of(&asked.principal_id).len();
                match &asked.attestation {
                    None if held > 0 => {
                        return Err(tonic::Status::failed_precondition(format!(
                            "principal {} already holds {held} key(s), so adding another \
                             needs their own attestation",
                            asked.principal_id
                        )));
                    }
                    None => {}
                    Some(signed) => self
                        .attestations
                        .lock()
                        .expect("the attestation log")
                        .push(signed.clone()),
                }
            }
            // **gunnar derives the fingerprint from the line, and so does
            // this.** It used to be a synthetic function of the string, which
            // was enough while nothing compared a fingerprint across the wire
            // — and stopped being enough the day `TenantKeys` began checking
            // whether gunnar still holds the key it thinks it enrolled: that
            // check compares the REAL fingerprint against this table, and a
            // stand-in made every credential look forgotten.
            //
            // A line that is not a key keeps the synthetic form, because
            // several rows here paste deliberate rubbish and want gunnar's
            // later refusal rather than a parse error at this seam.
            let key = pb::PublicKey {
                fingerprint: ssh_key::PublicKey::from_openssh(&asked.openssh_public_key)
                    .map(|k| k.fingerprint(ssh_key::HashAlg::Sha256).to_string())
                    .unwrap_or_else(|_| {
                        format!(
                            "SHA256:{}",
                            asked
                                .openssh_public_key
                                .replace(|c: char| !c.is_ascii_alphanumeric(), "")
                        )
                    }),
                algo: "ssh-ed25519".to_owned(),
                principal_id: asked.principal_id,
                comment: asked.comment,
                added_at_unix_ms: 2,
                last_used_unix_ms: None,
                expires_at_unix_ms: None,
            };
            self.keys.lock().expect("the key table").push(key.clone());
            Ok(tonic::Response::new(pb::AddKeyResponse { key: Some(key) }))
        }

        async fn revoke_key(
            &self,
            request: tonic::Request<pb::RevokeKeyRequest>,
        ) -> Result<tonic::Response<pb::RevokeKeyResponse>, tonic::Status> {
            let who = self.who(&request);
            let asked = request.into_inner();
            self.record(format!("Access.RevokeKey {}", asked.fingerprint));
            self.signed("Access.RevokeKey", &who);
            // gunnar's self-scope: a principal revokes only a key it holds;
            // any other fingerprint is the server admin's — the same sentence
            // whether it exists or not.
            let own = self
                .keys
                .lock()
                .expect("the key table")
                .iter()
                .any(|k| k.fingerprint == asked.fingerprint && k.principal_id == who);
            if who != "console" && !own {
                return Err(tonic::Status::permission_denied(format!(
                    "principal {who} has no admin grant on server"
                )));
            }
            self.keys
                .lock()
                .expect("the key table")
                .retain(|k| k.fingerprint != asked.fingerprint);
            Ok(tonic::Response::new(pb::RevokeKeyResponse {}))
        }

        async fn add_principal(
            &self,
            _r: tonic::Request<pb::AddPrincipalRequest>,
        ) -> Result<tonic::Response<pb::AddPrincipalResponse>, tonic::Status> {
            // Recorded rather than merely refused: `a_session_with_no_principal
            // _is_refused_rather_than_given_one` asserts this was never reached,
            // and an `unimplemented!()` would have made the same test pass by
            // panicking the server task instead.
            self.record("Access.AddPrincipal".to_owned());
            unimplemented("AddPrincipal")
        }
        async fn rename_principal(
            &self,
            _r: tonic::Request<pb::RenamePrincipalRequest>,
        ) -> Result<tonic::Response<pb::RenamePrincipalResponse>, tonic::Status> {
            unimplemented("RenamePrincipal")
        }
        async fn remove_principal(
            &self,
            _r: tonic::Request<pb::RemovePrincipalRequest>,
        ) -> Result<tonic::Response<pb::RemovePrincipalResponse>, tonic::Status> {
            unimplemented("RemovePrincipal")
        }
        async fn grant(
            &self,
            _r: tonic::Request<pb::GrantRequest>,
        ) -> Result<tonic::Response<pb::GrantResponse>, tonic::Status> {
            unimplemented("Grant")
        }
        async fn revoke(
            &self,
            _r: tonic::Request<pb::RevokeRequest>,
        ) -> Result<tonic::Response<pb::RevokeResponse>, tonic::Status> {
            unimplemented("Revoke")
        }
        async fn list_grants(
            &self,
            _r: tonic::Request<pb::ListGrantsRequest>,
        ) -> Result<tonic::Response<pb::ListGrantsResponse>, tonic::Status> {
            unimplemented("ListGrants")
        }
        async fn add_group_member(
            &self,
            _r: tonic::Request<pb::AddGroupMemberRequest>,
        ) -> Result<tonic::Response<pb::AddGroupMemberResponse>, tonic::Status> {
            unimplemented("AddGroupMember")
        }
        async fn remove_group_member(
            &self,
            _r: tonic::Request<pb::RemoveGroupMemberRequest>,
        ) -> Result<tonic::Response<pb::RemoveGroupMemberResponse>, tonic::Status> {
            unimplemented("RemoveGroupMember")
        }
        async fn list_group_members(
            &self,
            _r: tonic::Request<pb::ListGroupMembersRequest>,
        ) -> Result<tonic::Response<pb::ListGroupMembersResponse>, tonic::Status> {
            unimplemented("ListGroupMembers")
        }
        async fn list_group_membership_events(
            &self,
            _r: tonic::Request<pb::ListGroupMembershipEventsRequest>,
        ) -> Result<tonic::Response<pb::ListGroupMembershipEventsResponse>, tonic::Status> {
            unimplemented("ListGroupMembershipEvents")
        }
    }

    /// A console with a REAL passkey subsystem, so `require_auth` runs the role
    /// ladder rather than the open-on-loopback bypass and sessions can be
    /// minted for it. Without that every one of the refusals below would pass
    /// vacuously.
    async fn console(fake: FakeGunnar) -> (axum::Router, Arc<crate::auth::Webauthn>, FakeGunnar) {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
            .await
            .expect("a loopback port");
        let addr = listener.local_addr().expect("its address");
        let serving = fake.clone();
        tokio::spawn(async move {
            tonic::transport::Server::builder()
                .add_service(AccessServer::new(serving))
                .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
                .await
        });

        let dir = tempfile::tempdir().expect("a temp dir").keep();
        let store = crate::state::StateStore::open(dir.join("state.redb")).expect("a store");
        let webauthn = Arc::new(
            crate::auth::Webauthn::new(
                crate::auth::RelyingParty {
                    rp_id: "console.example".to_owned(),
                    origin: "https://console.example".to_owned(),
                    rp_name: "gunnar".to_owned(),
                },
                store.clone(),
            )
            .expect("a subsystem"),
        );
        let signer = Arc::new(
            crate::signer::Signer::new(ssh_key::PrivateKey::from(
                ssh_key::private::Ed25519Keypair::from_seed(&CONSOLE_SEED),
            ))
            .expect("a fixed seed makes a signer"),
        );
        let endpoint = format!("http://{addr}");
        let router = crate::serve::router(Arc::new(crate::app::App {
            webauthn: Some(webauthn.clone()),
            // Set, so `the_admin_token_is_not_a_person` has a token to offer.
            admin_token: Some("break-glass".to_owned()),
            #[cfg(feature = "metered")]
            monetize: None,
            events: crate::sse::EventHub::detached(),
            gunnar: crate::grpc::Gunnar::connect_lazy(&endpoint, signer).expect("a handle"),
            twin: None,
            guest: None,
            reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
                endpoint.clone(),
                store.clone(),
            )),
            tenants: Arc::new(crate::tenant::TenantKeys::new(endpoint, store)),
            forges: Default::default(),
            imports: Default::default(),
            forge_apps: Default::default(),
            ui_dir: None,
            caches: crate::api::Caches::default(),
            objects: Default::default(),
            readmes: Default::default(),
        }));
        (router, webauthn, fake)
    }

    /// The same box, plus the two things a RESTART has to be modelled with: the
    /// state store (which survives one) and gunnar's endpoint (which a fresh
    /// `TenantKeys` is built over). Nothing else can express "the process came
    /// back and read its own disk", and that is the case
    /// `a_key_gunnar_has_forgotten_is_dropped_and_asked_for_again` is about.
    async fn console_and_its_disk(
        fake: FakeGunnar,
    ) -> (Arc<crate::auth::Webauthn>, FakeGunnar, crate::state::StateStore, String) {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
            .await
            .expect("a loopback port");
        let addr = listener.local_addr().expect("its address");
        let serving = fake.clone();
        tokio::spawn(async move {
            tonic::transport::Server::builder()
                .add_service(AccessServer::new(serving))
                .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
                .await
        });
        let dir = tempfile::tempdir().expect("a temp dir").keep();
        let store = crate::state::StateStore::open(dir.join("state.redb")).expect("a store");
        let webauthn = Arc::new(
            crate::auth::Webauthn::new(
                crate::auth::RelyingParty {
                    rp_id: "console.example".to_owned(),
                    origin: "https://console.example".to_owned(),
                    rp_name: "gunnar".to_owned(),
                },
                store.clone(),
            )
            .expect("a subsystem"),
        );
        (webauthn, fake, store, format!("http://{addr}"))
    }

    /// One request, optionally carrying a session cookie or a bearer token.
    async fn send(
        router: &axum::Router,
        method: http::Method,
        uri: &str,
        cookie: Option<&str>,
        token: Option<&str>,
        body: &str,
    ) -> (http::StatusCode, String) {
        let mut builder = http::Request::builder()
            .method(method)
            .uri(uri)
            .header(http::header::CONTENT_TYPE, "application/json")
            // The CSRF guard reads the origin on a mutation; this is what a
            // real browser on this console sends.
            .header(http::header::ORIGIN, "https://console.example");
        if let Some(cookie) = cookie {
            builder = builder.header(
                http::header::COOKIE,
                format!("{}={cookie}", crate::auth::SESSION_COOKIE),
            );
        }
        if let Some(token) = token {
            builder = builder.header(http::header::AUTHORIZATION, format!("Bearer {token}"));
        }
        let response = router
            .clone()
            .oneshot(
                builder
                    .body(axum::body::Body::from(body.to_owned()))
                    .expect("a request"),
            )
            .await
            .expect("the console answers");
        let status = response.status();
        let bytes = axum::body::to_bytes(response.into_body(), 1 << 20)
            .await
            .expect("a body");
        (status, String::from_utf8_lossy(&bytes).into_owned())
    }

    /// ★ **A VIEWER adds, lists and removes a key on their own name.**
    ///
    /// The whole point of the surface. Red before it existed: `/api/keys` is
    /// admin-floored, so a tenant who had just registered a passkey through a
    /// paid signup could read the console and never push to it.
    #[tokio::test]
    async fn a_viewer_adds_lists_and_removes_a_key_on_their_own_name() {
        let (router, webauthn, fake) = console(FakeGunnar::default()).await;
        webauthn.plant_bound_credential_for_tests("anna", crate::role::Role::Viewer, ANNA);
        let anna = webauthn.make_session("anna", crate::role::Role::Viewer, true);

        let (added, body) = send(
            &router,
            http::Method::POST,
            "/api/account/keys",
            Some(&anna),
            None,
            r#"{"openssh_public_key":"ssh-ed25519 AAAAannakey anna@laptop","comment":"laptop"}"#,
        )
        .await;
        assert_eq!(added, http::StatusCode::OK, "a viewer was refused: {body}");

        let (listed, mine) = send(
            &router,
            http::Method::GET,
            "/api/account/keys",
            Some(&anna),
            None,
            "",
        )
        .await;
        assert_eq!(listed, http::StatusCode::OK, "{mine}");
        assert!(mine.contains(ANNA), "{mine}");
        assert!(
            !mine.contains(OLOFS_KEY),
            "somebody else's key came back from a route that is about your own: {mine}"
        );

        // Hers is the one the page lists; the console's own key on her
        // principal (what her session reads with) is not shown.
        let listed: serde_json::Value = serde_json::from_str(&mine).expect("json");
        let items = listed["items"].as_array().expect("items");
        assert_eq!(items.len(), 1, "{mine}");
        let fingerprint = items[0]["fingerprint"].as_str().expect("a fingerprint").to_owned();
        let (removed, gone) = send(
            &router,
            http::Method::DELETE,
            &format!("/api/account/keys?fingerprint={}", encoded(&fingerprint)),
            Some(&anna),
            None,
            "",
        )
        .await;

        assert_eq!(removed, http::StatusCode::OK, "{gone}");
        assert!(!fake.fingerprints_of(ANNA).contains(&fingerprint));
        // ★ Every key call was signed AS anna (thin layer, owner 2026-10-04):
        // gunnar's self-scope decided, the console's key only enrolled her
        // session's own credential.
        let by = fake.by();
        for method in ["Access.ListKeys", "Access.RevokeKey"] {
            assert!(
                by.iter().filter(|c| c.starts_with(method)).all(|c| c.ends_with(&format!("by {ANNA}"))),
                "{method} was not signed by anna: {by:?}"
            );
        }
        assert_eq!(
            by.iter().filter(|c| c.starts_with("Access.AddKey")).cloned().collect::<Vec<_>>(),
            vec!["Access.AddKey by console".to_owned(), format!("Access.AddKey by {ANNA}")],
            "the console enrols her session key; her own key is added by her"
        );
        assert_eq!(
            fake.fingerprints_of(OLOF),
            vec![OLOFS_KEY.to_owned()],
            "removing your own key touched somebody else's"
        );
    }

    /// **And the admin door stays shut to them.** The floor on `/api/keys` is
    /// what this route exists to avoid lowering, so it is asserted here rather
    /// than assumed: if a later edit widened it, this surface would have been
    /// built for nothing and the widening would be the actual change.
    #[tokio::test]
    async fn a_viewer_cannot_reach_the_admin_key_surface() {
        let (router, webauthn, fake) = console(FakeGunnar::default()).await;
        let anna = webauthn.make_session("anna", crate::role::Role::Viewer, true);

        let (status, body) = send(
            &router,
            http::Method::POST,
            "/api/keys",
            Some(&anna),
            None,
            r#"{"principal_id":"p-olof","openssh_public_key":"ssh-ed25519 AAAAsneak x"}"#,
        )
        .await;

        assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
        assert!(
            body.contains("admin"),
            "the refusal must name the role: {body}"
        );
        assert!(
            !fake.calls().iter().any(|c| c.starts_with("Access.AddKey")),
            "gunnar was told about a key this console had already refused: {:?}",
            fake.calls()
        );
    }

    /// ★ **A body that names somebody else is refused, and lands on nobody.**
    ///
    /// Refused rather than ignored: serde drops an unknown field silently, so a
    /// client sending `{"user":"olof", …}` would have got a 200 and a key on the
    /// right name with nothing to tell it the field meant nothing. Both halves
    /// are asserted — the refusal, and that neither principal grew a key.
    #[tokio::test]
    async fn a_body_that_names_somebody_else_is_refused_and_lands_on_nobody() {
        for body in [
            r#"{"openssh_public_key":"ssh-ed25519 AAAAsneak x","user":"olof"}"#,
            r#"{"openssh_public_key":"ssh-ed25519 AAAAsneak x","principal_id":"p-olof"}"#,
        ] {
            let (router, webauthn, fake) = console(FakeGunnar::default()).await;
            let anna = webauthn.make_session("anna", crate::role::Role::Viewer, true);

            let (status, answer) = send(
                &router,
                http::Method::POST,
                "/api/account/keys",
                Some(&anna),
                None,
                body,
            )
            .await;

            assert_eq!(
                status,
                http::StatusCode::BAD_REQUEST,
                "{body} was accepted: {answer}"
            );
            assert_eq!(
                fake.fingerprints_of(OLOF),
                vec![OLOFS_KEY.to_owned()],
                "the key landed on the name in the BODY: {body}"
            );
            assert!(
                fake.fingerprints_of(ANNA).is_empty(),
                "a refused request still registered a key: {body}"
            );
            assert!(
                !fake.calls().iter().any(|c| c.starts_with("Access.AddKey")),
                "gunnar heard about a body this console refuses: {:?}",
                fake.calls()
            );
        }
    }

    /// ★ **Somebody else's key cannot be revoked — gunnar refuses the person's
    /// own call.**
    ///
    /// `Access.RevokeKey` takes a fingerprint and nothing else. Since
    /// 2026-10-04 it is signed AS THE PERSON and gunnar's self-scope rule
    /// scopes it: a principal revokes only a key it holds.
    #[tokio::test]
    async fn a_key_that_is_not_yours_cannot_be_revoked() {
        let (router, webauthn, fake) = console(FakeGunnar::default()).await;
        webauthn.plant_bound_credential_for_tests("anna", crate::role::Role::Viewer, ANNA);
        let anna = webauthn.make_session("anna", crate::role::Role::Viewer, true);

        let (status, body) = send(
            &router,
            http::Method::DELETE,
            &format!("/api/account/keys?fingerprint={}", encoded(OLOFS_KEY)),
            Some(&anna),
            None,
            "",
        )
        .await;

        assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
        assert!(body.contains("anna"), "{body}");
        assert_eq!(
            fake.fingerprints_of(OLOF),
            vec![OLOFS_KEY.to_owned()],
            "a viewer revoked somebody else's key"
        );
        // ★ The refusal is gunnar's, of anna's OWN call — never a revoke the
        // console's server-wide key could have made for her.
        assert_eq!(
            fake.by().iter().filter(|c| c.starts_with("Access.RevokeKey")).cloned().collect::<Vec<_>>(),
            vec![format!("Access.RevokeKey by {ANNA}")],
        );
    }

    /// **A session with no principal is refused, never given one.**
    ///
    /// Minting one is `Access.AddPrincipal`, which creates an identity AND its
    /// namespace. Doing that from a viewer's floor is the escalation this whole
    /// module is shaped to not have, so the empty call log is the assertion
    /// that matters.
    #[tokio::test]
    async fn a_session_with_no_principal_is_refused_rather_than_given_one() {
        let (router, webauthn, fake) = console(FakeGunnar::default()).await;
        let stranger = webauthn.make_session("nobody", crate::role::Role::Viewer, true);

        let (status, body) = send(
            &router,
            http::Method::POST,
            "/api/account/keys",
            Some(&stranger),
            None,
            r#"{"openssh_public_key":"ssh-ed25519 AAAAnobody x"}"#,
        )
        .await;

        // Since passkeys live at gunnar (thin layer L) the session's principal
        // is gunnar's answer at the login; a session carrying none is told to
        // log in again, and nothing is created for it.
        assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
        assert!(body.contains("log in again"), "{body}");
        assert!(
            !fake.calls().contains(&"Access.AddPrincipal".to_owned()),
            "the console created an identity for a viewer: {:?}",
            fake.calls()
        );
    }

    /// **The break-glass token is not a person.** It is held by whoever runs
    /// the process and there is no principal named after it; an admin adding
    /// somebody a key has `/api/keys`.
    #[tokio::test]
    async fn the_admin_token_is_not_a_person_and_has_no_keys_of_its_own() {
        let (router, _, fake) = console(FakeGunnar::default()).await;

        let (status, body) = send(
            &router,
            http::Method::GET,
            "/api/account/keys",
            None,
            Some("break-glass"),
            "",
        )
        .await;

        assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
        assert!(fake.calls().is_empty(), "{:?}", fake.calls());
    }

    /// **gunnar's refusal, by name.** A key that already belongs to another
    /// principal is refused there; this console is not a second opinion about
    /// why, and rewording it would leave an operator comparing two sentences
    /// with no way to know which to believe.
    #[tokio::test]
    async fn gunnars_own_refusal_is_passed_through_unreworded() {
        let refusal = "that key is already registered to principal p-olof";
        let (router, webauthn, _) = console(FakeGunnar {
            refuse_add: Some(refusal.to_owned()),
            ..FakeGunnar::default()
        })
        .await;
        webauthn.plant_bound_credential_for_tests("anna", crate::role::Role::Viewer, ANNA);
        let anna = webauthn.make_session("anna", crate::role::Role::Viewer, true);

        let (status, body) = send(
            &router,
            http::Method::POST,
            "/api/account/keys",
            Some(&anna),
            None,
            r#"{"openssh_public_key":"ssh-ed25519 AAAAolofkey x"}"#,
        )
        .await;

        assert_ne!(status, http::StatusCode::OK, "{body}");
        assert!(body.contains(refusal), "the refusal was reworded: {body}");
    }

    // ── the console's own key, and the deadlock it used to sit in ────────────

    async fn console_key_of(router: &axum::Router, session: &str) -> serde_json::Value {
        let (status, body) = send(
            router,
            http::Method::GET,
            "/api/account/console-key",
            Some(session),
            None,
            "",
        )
        .await;
        assert_eq!(status, http::StatusCode::OK, "{body}");
        serde_json::from_str(&body).expect("the console key answer is json")
    }

    /// ★ **The defect that made every paid account unpushable, closed.**
    ///
    /// gunnar allows ONE unattested key per principal. The console spends it on
    /// itself — the tenant credential, enrolled the first time a bound person
    /// reads anything — so when the customer pasted their OWN key it was the
    /// second, and gunnar refused, telling them to run `attest-key` with a key
    /// whose private half never leaves this process. `git push` therefore never
    /// worked for any paid account.
    ///
    /// This drives the real order through the real router: the console's key
    /// enrols first (free, the principal holds none), then the person's own key
    /// goes in behind an attestation the console makes with that credential.
    /// The assertion is on what reached GUNNAR, because "the request stopped
    /// failing" would also pass if the attestation were empty.
    #[tokio::test]
    async fn a_person_can_add_their_own_key_after_the_console_has_taken_the_free_slot() {
        let (router, webauthn, fake) = console(FakeGunnar {
            require_attestation: true,
            ..FakeGunnar::default()
        })
        .await;
        webauthn.plant_bound_credential_for_tests("anna", crate::role::Role::Viewer, ANNA);
        let anna = webauthn.make_session("anna", crate::role::Role::Viewer, true);

        // The console takes the free slot, exactly as a first read would.
        let state = console_key_of(&router, &anna).await;
        assert_eq!(state["state"], "enrolled", "the console's own key did not enrol: {state}");
        assert_eq!(fake.fingerprints_of(ANNA).len(), 1, "anna holds no console key");

        // Now anna's own key — the second on her principal, and the one that
        // used to be impossible.
        let hers = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::from_seed(&[9u8; 32]))
            .public_key()
            .to_openssh()
            .expect("a public key line");
        let want = ssh_key::PublicKey::from_openssh(&hers)
            .expect("parses")
            .fingerprint(ssh_key::HashAlg::Sha256)
            .to_string();

        let body = serde_json::json!({ "openssh_public_key": hers }).to_string();
        let (status, answer) = send(
            &router,
            http::Method::POST,
            "/api/account/keys",
            Some(&anna),
            None,
            &body,
        )
        .await;
        assert_eq!(status, http::StatusCode::OK, "{answer}");

        // **What gunnar was actually handed** (thin layer, owner 2026-10-04):
        // her AddKey, signed by HER — so gunnar's self-scope rule admits it
        // and no attestation is owed. The console vouches for nobody.
        assert!(
            fake.attestations.lock().expect("the log").is_empty(),
            "the console signed a consent on anna's behalf"
        );
        assert_eq!(
            fake.by().last().cloned(),
            Some(format!("Access.AddKey by {ANNA}")),
            "her key was not added as her"
        );
        assert!(fake.fingerprints_of(ANNA).contains(&want), "her key did not land");
        assert_eq!(fake.fingerprints_of(ANNA).len(), 2, "her key did not land");
    }

    /// **The admin surface does NOT get this.** `/api/keys` names any principal
    /// on the box, and there the console has no business vouching for anybody:
    /// gunnar's refusal stands and reaches the caller in gunnar's own words.
    /// The whole safety of the row above is that the target is the session's
    /// own name, so the distinction is worth a test rather than a comment.
    #[tokio::test]
    async fn the_admin_key_surface_still_cannot_vouch_for_anybody() {
        let (router, webauthn, fake) = console(FakeGunnar {
            require_attestation: true,
            ..FakeGunnar::default()
        })
        .await;
        let boss = webauthn.make_session("boss", crate::role::Role::Admin, true);

        // olof already holds a key in this fixture, so an admin adding another
        // to HIM is exactly the case gunnar guards.
        let body = serde_json::json!({
            "principal_id": OLOF,
            "openssh_public_key": "ssh-ed25519 AAAAsomebodyelseskey x",
        })
        .to_string();
        let (status, answer) = send(&router, http::Method::POST, "/api/keys", Some(&boss), None, &body).await;

        assert_ne!(status, http::StatusCode::OK, "an admin minted a key as another person: {answer}");
        assert!(
            answer.contains("attestation"),
            "gunnar's reason did not reach the caller: {answer}"
        );
        assert!(
            fake.attestations.lock().expect("the log").is_empty(),
            "the console vouched on the ADMIN surface"
        );
    }

    /// ★ **A re-imaged appliance does not leave a tenant permanently mute.**
    ///
    /// The two halves of this credential live in different places: the private
    /// half in the console's state store, the public half in gunnar's key
    /// table. A re-image resets gunnar's and leaves the console's untouched, so
    /// a console that trusted its own disk would go on signing with a key the
    /// appliance has never heard of — `Unauthenticated` on every read, for
    /// ever, from a stored record that looks perfectly healthy on this side.
    ///
    /// Modelled the only way it can be: the same store, a FRESH `TenantKeys`
    /// over it, which is what a restart is. A warm credential is still believed
    /// without asking — that is the whole point of the split — so a test that
    /// reused the old handle would prove nothing.
    #[tokio::test]
    async fn a_key_gunnar_has_forgotten_is_dropped_and_asked_for_again() {
        let (_webauthn, fake, store, endpoint) = console_and_its_disk(FakeGunnar::default()).await;
        let signer = Arc::new(
            crate::signer::Signer::new(ssh_key::PrivateKey::from(
                ssh_key::private::Ed25519Keypair::from_seed(&[21u8; 32]),
            ))
            .expect("a fixed seed makes a signer"),
        );
        let admin = crate::grpc::Gunnar::connect_lazy(&endpoint, signer).expect("a handle");

        let before = crate::tenant::TenantKeys::new(endpoint.clone(), store.clone());
        let first = before.ensure("anna", ANNA, &admin).await.expect("a first enrolment");
        assert!(
            fake.fingerprints_of(ANNA).contains(&first.fingerprint),
            "gunnar did not take the first key"
        );

        // The re-image: gunnar's key table loses everything, the console's disk
        // keeps the private half.
        fake.keys.lock().expect("the key table").clear();

        // The restart.
        let after = crate::tenant::TenantKeys::new(endpoint, store);
        let second = after.ensure("anna", ANNA, &admin).await.expect("a re-provision");

        assert_ne!(
            second.fingerprint, first.fingerprint,
            "the console went on using a key gunnar has forgotten"
        );
        assert!(
            fake.fingerprints_of(ANNA).contains(&second.fingerprint),
            "the credential handed back is not one gunnar holds: {:?}",
            fake.fingerprints_of(ANNA)
        );
    }

    /// A key table row as `pair` leaves one.
    fn key_row(principal: &str, fingerprint: &str) -> pb::PublicKey {
        pb::PublicKey {
            fingerprint: fingerprint.to_owned(),
            algo: "ssh-ed25519".to_owned(),
            principal_id: principal.to_owned(),
            comment: String::new(),
            added_at_unix_ms: 1,
            last_used_unix_ms: None,
            expires_at_unix_ms: None,
        }
    }

    fn admin_over(endpoint: &str, seed: u8) -> crate::grpc::Gunnar {
        let signer = Arc::new(
            crate::signer::Signer::new(ssh_key::PrivateKey::from(
                ssh_key::private::Ed25519Keypair::from_seed(&[seed; 32]),
            ))
            .expect("a fixed seed makes a signer"),
        );
        crate::grpc::Gunnar::connect_lazy(endpoint, signer).expect("a handle")
    }

    /// **The ladder's seed is adopted: no key minted, nothing asked of gunnar
    /// but "do you hold it".** `cargo xtask estate rebuild` lays
    /// `<GUNNAR_UI_TENANT_SEEDS>/<principal>` on this box at `pair`, enrols it
    /// second on the operator's principal and has the operator's own key vouch
    /// for it. This is the console's half: the first read after the operator's
    /// first login is answered with THAT key. Inverted in the same test — the
    /// same appliance, no seeds directory — the console mints one, gunnar
    /// refuses it (the operator's key spent the free slot), and the paste form
    /// is back; so a door that did nothing would fail here.
    #[tokio::test]
    async fn a_seed_the_ladder_enrolled_is_adopted_without_minting_or_asking() {
        let fake = FakeGunnar {
            require_attestation: true,
            ..Default::default()
        };
        let (_webauthn, fake, store, endpoint) = console_and_its_disk(fake).await;
        let admin = admin_over(&endpoint, 23);

        let seeds = tempfile::tempdir().expect("a seeds dir");
        let seed = crate::reviewer::mint_ed25519().expect("a seed");
        std::fs::write(seeds.path().join(ANNA), &seed).expect("the seed laid");
        let seed_fp = crate::signer::Signer::from_openssh(&seed)
            .expect("the seed parses")
            .fingerprint()
            .to_owned();
        // The appliance as `pair` left it: the operator's key first, the seed second.
        fake.keys.lock().expect("the key table").extend([
            key_row(ANNA, "SHA256:annasLaptopAnnasLaptopAnnasLaptopAnnasLapt"),
            key_row(ANNA, &seed_fp),
        ]);

        let keys = crate::tenant::TenantKeys::new(endpoint.clone(), store.clone())
            .with_seeds(Some(seeds.path().to_path_buf()));
        let identity = keys.ensure("anna", ANNA, &admin).await.expect("the seed is adopted");
        assert_eq!(identity.fingerprint, seed_fp, "the console reads with something other than the seed");
        assert_eq!(identity.signer.fingerprint(), seed_fp, "the signer is not the seed's private half");
        assert!(
            !fake.calls().iter().any(|c| c.starts_with("Access.AddKey")),
            "the console asked gunnar for a key it already had: {:?}",
            fake.calls()
        );
        assert_eq!(fake.fingerprints_of(ANNA).len(), 2, "a key was minted beside the seed");

        // Inverted: the same appliance, a console with no seeds directory, a
        // second console user bound to the same principal. This is the defect.
        let blind = crate::tenant::TenantKeys::new(endpoint, store);
        let refused = blind.ensure("anna-on-another-browser", ANNA, &admin).await;
        let why = refused.err().map(|e| e.to_string()).unwrap_or_default();
        assert!(why.contains("already holds"), "without the door gunnar should have refused a third key: {why}");
    }

    /// **A seed gunnar does not hold is the key ASKED FOR — never a fresh mint
    /// beside it.** An appliance re-imaged after the ladder paired it forgets
    /// the seed; the console then enrols the seed itself as the principal's
    /// first key, so the fingerprint on the box is the one the ladder will find
    /// held when it re-pairs, and the one in any refusal is the seed's.
    #[tokio::test]
    async fn a_seed_gunnar_has_forgotten_is_the_key_asked_for_not_a_fresh_one() {
        let (_webauthn, fake, store, endpoint) = console_and_its_disk(FakeGunnar::default()).await;
        let admin = admin_over(&endpoint, 24);
        let seeds = tempfile::tempdir().expect("a seeds dir");
        let seed = crate::reviewer::mint_ed25519().expect("a seed");
        std::fs::write(seeds.path().join(ANNA), &seed).expect("the seed laid");
        let seed_fp = crate::signer::Signer::from_openssh(&seed)
            .expect("the seed parses")
            .fingerprint()
            .to_owned();
        assert!(fake.fingerprints_of(ANNA).is_empty(), "the fixture holds a key for anna already");

        let keys = crate::tenant::TenantKeys::new(endpoint, store)
            .with_seeds(Some(seeds.path().to_path_buf()));
        let identity = keys.ensure("anna", ANNA, &admin).await.expect("enrolled as the first key");
        assert_eq!(identity.fingerprint, seed_fp);
        assert_eq!(fake.fingerprints_of(ANNA), vec![seed_fp], "gunnar was handed something other than the seed");
        assert_eq!(
            fake.calls().iter().filter(|c| c.starts_with("Access.AddKey")).count(),
            1,
            "{:?}",
            fake.calls()
        );
    }

    /// **A principal id is a value off the wire, and a seed is a path.** An id
    /// that cannot be a plain file name is never joined to the seeds directory;
    /// the console mints as before and the file that WOULD have been reached is
    /// untouched. And a seed that exists but cannot be read is a refusal, not a
    /// quiet fresh mint reporting health.
    #[tokio::test]
    async fn a_seed_is_reached_by_a_plain_name_only_and_an_unreadable_one_refuses() {
        let (_webauthn, fake, store, endpoint) = console_and_its_disk(FakeGunnar::default()).await;
        let admin = admin_over(&endpoint, 25);
        let seeds = tempfile::tempdir().expect("a seeds dir");
        std::fs::create_dir_all(seeds.path().join("a")).expect("a nested dir");
        let planted = crate::reviewer::mint_ed25519().expect("a planted key");
        std::fs::write(seeds.path().join("a").join("b"), &planted).expect("planted");
        let planted_fp = crate::signer::Signer::from_openssh(&planted)
            .expect("parses")
            .fingerprint()
            .to_owned();

        let keys = crate::tenant::TenantKeys::new(endpoint.clone(), store.clone())
            .with_seeds(Some(seeds.path().to_path_buf()));
        let identity = keys.ensure("eve", "a/b", &admin).await.expect("minted as before");
        assert_ne!(identity.fingerprint, planted_fp, "a principal id was joined to the seeds directory as a path");

        // Unreadable: a directory where the seed should be.
        std::fs::create_dir_all(seeds.path().join(ANNA)).expect("a directory in the seed's place");
        let refused = keys.ensure("anna", ANNA, &admin).await;
        let why = refused.err().map(|e| e.to_string()).unwrap_or_default();
        assert!(why.contains("could not be read"), "an unreadable seed did not refuse by name: {why}");
        assert!(fake.fingerprints_of(ANNA).is_empty(), "a key was minted for anna past an unreadable seed");
    }

    /// **A credential discarded because the passkey was re-pinned is a NAMED
    /// refusal, once, and the next read provisions.** Until 2026-09-20
    /// `existing` answered `Ok(None)` here, which is the same answer as "this
    /// user has no credential yet"; a key the estate ladder had enrolled and
    /// had the operator vouch for could be given up with only a log line. The
    /// refusal names the console user, the principal the record was on, its
    /// fingerprint and the pinned principal. It is not fatal: the record is
    /// removed BEFORE it is returned, so the second call is `Ok(None)`.
    #[tokio::test]
    async fn a_credential_discarded_for_a_re_pinned_passkey_is_refused_by_name_once() {
        let (_webauthn, _fake, store, endpoint) = console_and_its_disk(FakeGunnar::default()).await;
        let admin = admin_over(&endpoint, 26);
        let keys = crate::tenant::TenantKeys::new(endpoint, store);
        let on_anna = keys.ensure("anna", ANNA, &admin).await.expect("the first enrolment");

        let why = keys
            .existing("anna", Some("bosse"))
            .await
            .err()
            .map(|e| e.to_string())
            .unwrap_or_default();
        assert!(why.contains("discarded"), "the drop was not answered by name: {why}");
        assert!(why.contains(ANNA) && why.contains("bosse"), "the refusal names neither principal: {why}");
        assert!(
            why.contains(&on_anna.fingerprint),
            "the refusal does not name the fingerprint it gave up: {why}"
        );
        assert!(!why.contains("PRIVATE KEY"), "the refusal carries key material: {why}");

        // Already gone: the same question a second time is the ordinary "no
        // credential yet", so nothing is wedged.
        assert!(
            keys.existing("anna", Some("bosse")).await.expect("no record left").is_none(),
            "the discarded record is still being answered"
        );
    }

    /// And the other half of the same split: a credential this process has
    /// already used is NOT re-checked, or every tenant read would carry an
    /// `Access.ListKeys` in front of it.
    #[tokio::test]
    async fn a_warm_credential_is_not_re_asked_about_on_every_read() {
        let (_webauthn, fake, store, endpoint) = console_and_its_disk(FakeGunnar::default()).await;
        let signer = Arc::new(
            crate::signer::Signer::new(ssh_key::PrivateKey::from(
                ssh_key::private::Ed25519Keypair::from_seed(&[22u8; 32]),
            ))
            .expect("a fixed seed makes a signer"),
        );
        let admin = crate::grpc::Gunnar::connect_lazy(&endpoint, signer).expect("a handle");
        let keys = crate::tenant::TenantKeys::new(endpoint, store);
        keys.ensure("anna", ANNA, &admin).await.expect("a first enrolment");

        let after_first = fake.calls().len();
        for _ in 0..5 {
            keys.ensure("anna", ANNA, &admin).await.expect("a warm read");
        }
        assert_eq!(
            fake.calls().len(),
            after_first,
            "a warm credential cost gunnar a call: {:?}",
            fake.calls()
        );
    }

    /// **The paste shape is gunnar's**, and this pins it. The CLI prints
    /// `gunnar_core::SignedKeyAttestation`, whose inner attestation is
    /// `#[serde(flatten)]` — so five fields in ONE object, not a nested one.
    /// Getting that wrong makes every paste a parse error, and the person has
    /// no way to tell whether they or the console are at fault.
    #[test]
    fn the_attestation_shape_is_gunnars() {
        let printed = r#"{
            "principal_id": "p-anna",
            "fingerprint": "SHA256:thenewkey",
            "at_unix_ms": 1788000000000,
            "key_fingerprint": "SHA256:theoldkey",
            "signature": "-----BEGIN SSH SIGNATURE-----\nx\n-----END SSH SIGNATURE-----"
        }"#;
        let parsed: Attestation = serde_json::from_str(printed).expect("the CLI's own shape");
        assert_eq!(parsed.principal_id, "p-anna");
        assert_eq!(parsed.fingerprint, "SHA256:thenewkey");
        assert_eq!(parsed.key_fingerprint, "SHA256:theoldkey");
        assert_eq!(parsed.at_unix_ms, 1_788_000_000_000);

        // A newer appliance may print more; relaying must not become a local
        // parse error over a field this console never reads.
        let with_more = r#"{"principal_id":"p","fingerprint":"f","at_unix_ms":1,
            "key_fingerprint":"k","signature":"s","something_new":true}"#;
        serde_json::from_str::<Attestation>(with_more).expect("unknown fields are relayed, not refused");

        // The proto side, pinned by construction: these are the three fields
        // `KeyAttestation` carries and the two `SignedKeyAttestation` adds.
        let _ = pb::SignedKeyAttestation {
            attestation: Some(pb::KeyAttestation {
                principal_id: parsed.principal_id,
                fingerprint: parsed.fingerprint,
                at_unix_ms: parsed.at_unix_ms,
            }),
            key_fingerprint: parsed.key_fingerprint,
            signature: parsed.signature,
        };
    }

}