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/thin_layer.rs

//! **The console is a thin layer** (owner, 2026-10-04): every read a person
//! makes is asked of gunnar AS THAT PERSON, and gunnar decides.
//!
//! Tests only, and its own module for the reason [`crate::account_model`] is:
//! what is asserted is a property of the whole path — session → bound
//! principal → the person's own key → gunnar's grant table — and not of one
//! handler.
//!
//! # The failure this is here to catch
//!
//! Until this module the viewer-floor GETs below were answered with the
//! console's OWN key, which holds a server-wide `Admin`. gunnar cannot refuse a
//! caller it cannot see, so a paying customer, an importer or an employee read
//! every principal, key, grant, lockout note, deleted repository, orphan store,
//! storage row and box counter — and another account's private pull requests,
//! issues, reviews and branch rules — by spelling a path. A test that asserted
//! only "the owner sees their pull request" is green on that hole. So every
//! row here asks as a SECOND tenant and asserts two things:
//!
//! 1. the answer is a refusal and carries nothing of the other account's, and
//! 2. **what gunnar heard**: the call reached the fake signed by the second
//!    tenant's own key, never by the console's. A console that refused after
//!    asking with its own key would pass (1) and fail (2).
//!
//! # Why the fake authorises by fingerprint
//!
//! Each tenant's key is enrolled through `Access.AddKey` (the console's
//! [`crate::tenant::TenantKeys`]); the fake records fingerprint → principal and
//! answers every later call by the principal that signed it, with gunnar's
//! scopes: the identity table, lockouts, the deleted and orphan listings, a
//! whole-box storage report, `Stats`, `Health` and `Capacity` need a
//! server-wide grant; a repository's pull requests, issues, reviews, branch
//! rules and maintenance need Read on that repository.

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

use tonic::{Request, Response, Status};
use tower::ServiceExt as _;

use crate::api::mirror_gate::Imports;
use crate::pb;
use crate::role::Role;

const CONSOLE_SEED: [u8; 32] = [61u8; 32];
const GUEST_SEED: [u8; 32] = [62u8; 32];

/// Strings that may only ever reach a body read by the box's administrator or
/// by acme. Distinctive, so a `contains` over a whole body means something.
const SECRET_PR: &str = "SECRET-acme-pull-merger-plans";
const SECRET_ISSUE: &str = "SECRET-acme-issue-layoffs";
const SECRET_RULE: &str = "refs/heads/SECRET-acme-release";
const SECRET_GATE: &str = "SECRET-acme-gate";
const SECRET_PERSON: &str = "SECRET-person";
const SECRET_NOTE: &str = "SECRET-lockout-note";
const SECRET_DELETED: &str = "SECRET-deleted-repo";
const SECRET_ORPHAN: &str = "SECRET-orphan-store";
/// The box's size: no tenant's number.
const SECRET_BOX_TOTAL: u64 = 987_654_321_987;
/// What gunnar says is left of beta's room: not cap − used, on purpose.
const GUNNARS_REMAINING: u64 = 42;

fn signer_of(seed: [u8; 32]) -> crate::signer::Signer {
    crate::signer::Signer::new(ssh_key::PrivateKey::from(
        ssh_key::private::Ed25519Keypair::from_seed(&seed),
    ))
    .expect("a fixed seed makes a signer")
}

fn fingerprint_of<T>(request: &Request<T>) -> String {
    request
        .metadata()
        .get(crate::signer::CREDENTIAL_METADATA_KEY)
        .and_then(|value| value.to_str().ok())
        .and_then(|line| line.split_whitespace().nth(1))
        .unwrap_or("(unsigned)")
        .to_owned()
}

// ───────────────────────────────────────────────────────────────────────────
// The fake
// ───────────────────────────────────────────────────────────────────────────

/// What a call is about, in gunnar's terms.
enum Scope {
    Server,
    Repo(String, String),
}

#[derive(Clone, Default)]
struct Fake {
    /// Every call, as `<Method> by <who>` — `console`, a principal id, or
    /// `stranger`.
    heard: Arc<Mutex<Vec<String>>>,
    /// tenant-key fingerprint → principal.
    keys: Arc<Mutex<BTreeMap<String, String>>>,
    /// (principal, namespace, repo, level).
    #[allow(clippy::type_complexity)]
    grants: Arc<Mutex<Vec<(String, String, String, i32)>>>,
    /// Every `Repos.List`'s `(query, mirrored_only)`.
    searched: Arc<Mutex<Vec<(String, bool)>>>,
}

impl Fake {
    fn grant(&self, principal: &str, ns: &str, level: pb::GrantLevel) {
        self.grants
            .lock()
            .expect("grants")
            .push((principal.into(), ns.into(), String::new(), level as i32));
    }

    fn who<T>(&self, request: &Request<T>) -> String {
        let fingerprint = fingerprint_of(request);
        if fingerprint == signer_of(CONSOLE_SEED).fingerprint() {
            return "console".to_owned();
        }
        self.keys
            .lock()
            .expect("keys")
            .get(&fingerprint)
            .cloned()
            .unwrap_or_else(|| "stranger".to_owned())
    }

    /// Note the call and decide it the way gunnar does: the console's key
    /// holds a server-wide Admin; a tenant holds what the grant table says.
    fn require<T>(&self, request: &Request<T>, method: &str, scope: Scope) -> Result<(), Status> {
        let who = self.who(request);
        self.heard
            .lock()
            .expect("heard")
            .push(format!("{method} by {who}"));
        if who == "console" {
            return Ok(());
        }
        let allowed = self.grants.lock().expect("grants").iter().any(|(p, ns, repo, level)| {
            *p == who
                && *level >= pb::GrantLevel::Read as i32
                && match &scope {
                    Scope::Server => ns.is_empty(),
                    Scope::Repo(n, r) => ns.is_empty() || (ns == n && (repo.is_empty() || repo == r)),
                }
        });
        if allowed {
            Ok(())
        } else {
            Err(Status::permission_denied(format!(
                "{who} may not {method}"
            )))
        }
    }

    fn take_heard(&self) -> Vec<String> {
        std::mem::take(&mut *self.heard.lock().expect("heard"))
    }
}

fn repo_scope(path: Option<pb::RepoPath>) -> Scope {
    let path = path.unwrap_or_default();
    Scope::Repo(path.namespace, path.repo)
}

fn repo_scope_of(slash: &str) -> Scope {
    let (ns, repo) = slash.split_once('/').unwrap_or((slash, ""));
    Scope::Repo(ns.to_owned(), repo.to_owned())
}

#[tonic::async_trait]
impl pb::access_server::Access for Fake {
    async fn who_am_i(
        &self,
        request: Request<pb::WhoAmIRequest>,
    ) -> Result<Response<pb::WhoAmIResponse>, Status> {
        let who = self.who(&request);
        self.heard.lock().expect("heard").push(format!("Access.WhoAmI by {who}"));
        let server_level = self
            .grants
            .lock()
            .expect("grants")
            .iter()
            .filter(|(p, ns, _, _)| *p == who && ns.is_empty())
            .map(|(_, _, _, level)| *level)
            .max()
            .unwrap_or(pb::GrantLevel::Unspecified as i32);
        Ok(Response::new(pb::WhoAmIResponse {
            principal: Some(pb::Principal {
                id: who.clone(),
                name: who,
                kind: pb::PrincipalKind::User as i32,
                created_at_unix_ms: 1,
            }),
            fingerprint: fingerprint_of(&request),
            server_level,
            ..Default::default()
        }))
    }
    async fn add_key(
        &self,
        request: Request<pb::AddKeyRequest>,
    ) -> Result<Response<pb::AddKeyResponse>, Status> {
        if self.who(&request) != "console" {
            return Err(Status::permission_denied("only the console enrols here"));
        }
        let asked = request.into_inner();
        let parsed = ssh_key::PublicKey::from_openssh(&asked.openssh_public_key)
            .map_err(|e| Status::invalid_argument(format!("not an OpenSSH public key: {e}")))?;
        let fingerprint = parsed.fingerprint(ssh_key::HashAlg::Sha256).to_string();
        self.keys
            .lock()
            .expect("keys")
            .insert(fingerprint.clone(), asked.principal_id.clone());
        Ok(Response::new(pb::AddKeyResponse {
            key: Some(pb::PublicKey {
                fingerprint,
                algo: "ssh-ed25519".to_owned(),
                principal_id: asked.principal_id,
                comment: asked.comment,
                added_at_unix_ms: 3,
                last_used_unix_ms: None,
                expires_at_unix_ms: None,
            }),
        }))
    }
    async fn list_principals(
        &self,
        request: Request<pb::ListPrincipalsRequest>,
    ) -> Result<Response<pb::ListPrincipalsResponse>, Status> {
        self.require(&request, "Access.ListPrincipals", Scope::Server)?;
        Ok(Response::new(pb::ListPrincipalsResponse {
            principals: ["acme", "beta", SECRET_PERSON]
                .into_iter()
                .map(|id| pb::Principal {
                    id: id.to_owned(),
                    name: id.to_owned(),
                    kind: pb::PrincipalKind::User as i32,
                    created_at_unix_ms: 1,
                })
                .collect(),
            next_page_token: String::new(),
            ..Default::default()
        }))
    }
    async fn list_keys(
        &self,
        request: Request<pb::ListKeysRequest>,
    ) -> Result<Response<pb::ListKeysResponse>, Status> {
        self.require(&request, "Access.ListKeys", Scope::Server)?;
        Ok(Response::new(pb::ListKeysResponse {
            keys: vec![pb::PublicKey {
                fingerprint: "SHA256:secret".to_owned(),
                algo: "ssh-ed25519".to_owned(),
                principal_id: SECRET_PERSON.to_owned(),
                ..Default::default()
            }],
        }))
    }
    async fn list_grants(
        &self,
        request: Request<pb::ListGrantsRequest>,
    ) -> Result<Response<pb::ListGrantsResponse>, Status> {
        self.require(&request, "Access.ListGrants", Scope::Server)?;
        Ok(Response::new(pb::ListGrantsResponse {
            grants: vec![pb::Grant {
                principal_id: SECRET_PERSON.to_owned(),
                namespace: "acme".to_owned(),
                repo: String::new(),
                level: pb::GrantLevel::Admin as i32,
                granted_at_unix_ms: 1,
            }],
        }))
    }
}

#[tonic::async_trait]
impl pb::accounts_server::Accounts for Fake {
    async fn list_account_gates(
        &self,
        request: Request<pb::ListAccountGatesRequest>,
    ) -> Result<Response<pb::ListAccountGatesResponse>, Status> {
        self.require(&request, "Accounts.ListAccountGates", Scope::Server)?;
        Ok(Response::new(pb::ListAccountGatesResponse {
            gates: vec![pb::AccountGate {
                account: SECRET_PERSON.to_owned(),
                reason: pb::GateReason::Administrative as i32,
                note: SECRET_NOTE.to_owned(),
                ..Default::default()
            }],
        }))
    }
    async fn purge_status(
        &self,
        request: Request<pb::PurgeAccountStatusRequest>,
    ) -> Result<Response<pb::PurgeAccountStatusResponse>, Status> {
        self.require(&request, "Accounts.PurgeStatus", Scope::Server)?;
        Ok(Response::new(pb::PurgeAccountStatusResponse {
            job: None,
            gate: Some(pb::AccountGate {
                account: SECRET_PERSON.to_owned(),
                reason: pb::GateReason::Administrative as i32,
                note: SECRET_NOTE.to_owned(),
                ..Default::default()
            }),
        }))
    }
}

#[tonic::async_trait]
impl pb::admin_server::Admin for Fake {
    async fn health(
        &self,
        request: Request<pb::HealthRequest>,
    ) -> Result<Response<pb::HealthResponse>, Status> {
        self.require(&request, "Admin.Health", Scope::Server)?;
        Ok(Response::new(pb::HealthResponse {
            serving: true,
            agent: format!("gunnar/{SECRET_PERSON}"),
            ..Default::default()
        }))
    }
    async fn stats(
        &self,
        request: Request<pb::StatsRequest>,
    ) -> Result<Response<pb::StatsResponse>, Status> {
        self.require(&request, "Admin.Stats", Scope::Server)?;
        Ok(Response::new(pb::StatsResponse {
            principals: SECRET_BOX_TOTAL,
            ..Default::default()
        }))
    }
    async fn storage_info(
        &self,
        request: Request<pb::StorageInfoRequest>,
    ) -> Result<Response<pb::StorageInfoResponse>, Status> {
        let scope = match request.get_ref().repo.clone() {
            Some(path) => Scope::Repo(path.namespace, path.repo),
            None => Scope::Server,
        };
        self.require(&request, "Admin.StorageInfo", scope)?;
        Ok(Response::new(pb::StorageInfoResponse {
            total_hot_bytes: SECRET_BOX_TOTAL,
            repos: vec![pb::RepoStorage {
                repo: Some(pb::RepoPath {
                    namespace: "acme".to_owned(),
                    repo: SECRET_DELETED.to_owned(),
                }),
                hot_bytes: 7,
                ..Default::default()
            }],
            ..Default::default()
        }))
    }
    async fn capacity(
        &self,
        request: Request<pb::CapacityRequest>,
    ) -> Result<Response<pb::CapacityResponse>, Status> {
        self.require(&request, "Admin.Capacity", Scope::Server)?;
        Ok(Response::new(pb::CapacityResponse {
            total_bytes: SECRET_BOX_TOTAL,
            free_bytes: SECRET_BOX_TOTAL / 2,
            servable_bytes: SECRET_BOX_TOTAL / 2,
            ..Default::default()
        }))
    }
}

#[tonic::async_trait]
impl pb::repos_server::Repos for Fake {
    // Encrypted repositories (gunnar 2026-10-05): not modelled by this fake.
    async fn set_encrypted(
        &self,
        _: tonic::Request<pb::SetEncryptedRequest>,
    ) -> Result<tonic::Response<pb::SetEncryptedResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("Repos.SetEncrypted"))
    }
    async fn recipient_keys(
        &self,
        _: tonic::Request<pb::RecipientKeysRequest>,
    ) -> Result<tonic::Response<pb::RecipientKeysResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("Repos.RecipientKeys"))
    }

    async fn list(
        &self,
        request: Request<pb::ListReposRequest>,
    ) -> Result<Response<pb::ListReposResponse>, Status> {
        let asked = request.get_ref().clone();
        self.searched
            .lock()
            .expect("searched")
            .push((asked.query.clone(), asked.mirrored_only));
        let who = self.who(&request);
        // gunnar's query and narrowing, applied by gunnar.
        let repos = (0..300)
            .map(|n| format!("repo-{n:03}"))
            .chain(["the-needle".to_owned()])
            .filter(|name| asked.query.is_empty() || name.contains(&asked.query))
            .map(|name| pb::Repo {
                path: Some(pb::RepoPath {
                    namespace: who.clone(),
                    repo: name,
                }),
                visibility: pb::Visibility::Private as i32,
                ..Default::default()
            })
            .collect();
        Ok(Response::new(pb::ListReposResponse {
            repos,
            next_page_token: String::new(),
        }))
    }
    async fn get(
        &self,
        request: Request<pb::GetRepoRequest>,
    ) -> Result<Response<pb::GetRepoResponse>, Status> {
        let scope = repo_scope(request.get_ref().path.clone());
        self.require(&request, "Repos.Get", scope)?;
        Ok(Response::new(pb::GetRepoResponse {
            repo: Some(pb::Repo {
                path: request.into_inner().path,
                hash_kind: pb::HashKind::Sha256 as i32,
                ..Default::default()
            }),
        }))
    }
    async fn list_namespaces(
        &self,
        request: Request<pb::ListNamespacesRequest>,
    ) -> Result<Response<pb::ListNamespacesResponse>, Status> {
        let _ = self.who(&request);
        Ok(Response::new(pb::ListNamespacesResponse {
            namespaces: vec![
                pb::Namespace {
                    name: "beta".to_owned(),
                    owner_kind: pb::PrincipalKind::User as i32,
                    ..Default::default()
                },
                pb::Namespace {
                    name: "gunnar-twin".to_owned(),
                    owner_kind: pb::PrincipalKind::Machine as i32,
                    public: true,
                    ..Default::default()
                },
            ],
            next_page_token: String::new(),
        }))
    }
    async fn list_deleted(
        &self,
        request: Request<pb::ListDeletedRequest>,
    ) -> Result<Response<pb::ListDeletedResponse>, Status> {
        self.require(&request, "Repos.ListDeleted", Scope::Server)?;
        Ok(Response::new(pb::ListDeletedResponse {
            repos: vec![pb::DeletedRepo {
                repo: Some(pb::RepoPath {
                    namespace: "acme".to_owned(),
                    repo: SECRET_DELETED.to_owned(),
                }),
                principal_id: SECRET_PERSON.to_owned(),
                bytes: 11,
                ..Default::default()
            }],
            total_bytes: 11,
            default_retention_days: 30,
        }))
    }
    async fn list_orphans(
        &self,
        request: Request<pb::ListOrphansRequest>,
    ) -> Result<Response<pb::ListOrphansResponse>, Status> {
        self.require(&request, "Repos.ListOrphans", Scope::Server)?;
        Ok(Response::new(pb::ListOrphansResponse {
            orphans: vec![pb::OrphanStore {
                repo: Some(pb::RepoPath {
                    namespace: "acme".to_owned(),
                    repo: SECRET_ORPHAN.to_owned(),
                }),
                bytes: 13,
                ..Default::default()
            }],
            total_bytes: 13,
        }))
    }
    async fn gc_status(
        &self,
        request: Request<pb::GcStatusRequest>,
    ) -> Result<Response<pb::GcStatusResponse>, Status> {
        let scope = repo_scope(request.get_ref().path.clone());
        self.require(&request, "Repos.GcStatus", scope)?;
        Ok(Response::new(pb::GcStatusResponse { job: None }))
    }
}

#[tonic::async_trait]
impl pb::pull_requests_server::PullRequests for Fake {
    async fn list(
        &self,
        request: Request<pb::ListPullRequestsRequest>,
    ) -> Result<Response<pb::ListPullRequestsResponse>, Status> {
        let scope = repo_scope(request.get_ref().repo.clone());
        self.require(&request, "PullRequests.List", scope)?;
        Ok(Response::new(pb::ListPullRequestsResponse {
            pulls: vec![secret_pull()],
        }))
    }
    async fn get(
        &self,
        request: Request<pb::GetPullRequestRequest>,
    ) -> Result<Response<pb::GetPullRequestResponse>, Status> {
        let scope = repo_scope(request.get_ref().repo.clone());
        self.require(&request, "PullRequests.Get", scope)?;
        Ok(Response::new(pb::GetPullRequestResponse {
            pull: Some(secret_pull()),
        }))
    }
}

fn secret_pull() -> pb::PullRequest {
    pb::PullRequest {
        repo: Some(pb::RepoPath {
            namespace: "acme".to_owned(),
            repo: "app".to_owned(),
        }),
        number: 1,
        title: SECRET_PR.to_owned(),
        source_ref: "refs/heads/topic".to_owned(),
        target_ref: "refs/heads/main".to_owned(),
        state: pb::PullRequestState::Open as i32,
        author: "acme".to_owned(),
        ..Default::default()
    }
}

#[tonic::async_trait]
impl pb::refs_server::Refs for Fake {
    async fn diff(
        &self,
        request: Request<pb::DiffRequest>,
    ) -> Result<Response<pb::DiffResponse>, Status> {
        let scope = repo_scope(request.get_ref().repo.clone());
        self.require(&request, "Refs.Diff", scope)?;
        // gunnar's structure and counts: 70 files, more than the console draws
        // hunks for, every one counted.
        let changes = (0..70)
            .map(|n| pb::FileChange {
                kind: pb::ChangeKind::Added as i32,
                path: format!("f{n:02}.txt"),
                new_oid: format!("{n:064x}"),
                insertions: 2,
                ..Default::default()
            })
            .collect();
        Ok(Response::new(pb::DiffResponse { changes }))
    }
    async fn read_object(
        &self,
        request: Request<pb::ReadObjectRequest>,
    ) -> Result<Response<pb::ReadObjectResponse>, Status> {
        let scope = repo_scope(request.get_ref().repo.clone());
        self.require(&request, "Refs.ReadObject", scope)?;
        Ok(Response::new(pb::ReadObjectResponse {
            kind: pb::ObjectKind::Blob as i32,
            data: b"one\ntwo\n".to_vec(),
            ..Default::default()
        }))
    }
    async fn list(
        &self,
        request: Request<pb::ListRefsRequest>,
    ) -> Result<Response<pb::ListRefsResponse>, Status> {
        let scope = repo_scope(request.get_ref().repo.clone());
        self.require(&request, "Refs.List", scope)?;
        Ok(Response::new(pb::ListRefsResponse::default()))
    }
}

#[tonic::async_trait]
impl pb::forge_server::Forge for Fake {
    async fn append(
        &self,
        request: Request<pb::AppendEventRequest>,
    ) -> Result<Response<pb::AppendEventResponse>, Status> {
        let repo = request
            .get_ref()
            .event
            .as_ref()
            .and_then(|e| e.event.as_ref())
            .map(|e| e.repo.clone())
            .unwrap_or_default();
        let actor = request
            .get_ref()
            .event
            .as_ref()
            .and_then(|e| e.event.as_ref())
            .map(|e| e.actor.clone())
            .unwrap_or_default();
        self.require(&request, "Forge.Append", repo_scope_of(&repo))?;
        self.heard
            .lock()
            .expect("heard")
            .push(format!("event actor {actor}"));
        Ok(Response::new(pb::AppendEventResponse::default()))
    }
    async fn get_review(
        &self,
        request: Request<pb::GetReviewRequest>,
    ) -> Result<Response<pb::GetReviewResponse>, Status> {
        let scope = repo_scope_of(&request.get_ref().repo);
        self.require(&request, "Forge.GetReview", scope)?;
        Ok(Response::new(pb::GetReviewResponse {
            state: Some(pb::ReviewState {
                repo: "acme/app".to_owned(),
                pull: 1,
                ..Default::default()
            }),
            events: Vec::new(),
        }))
    }
    async fn list_issues(
        &self,
        request: Request<pb::ListIssuesRequest>,
    ) -> Result<Response<pb::ListIssuesResponse>, Status> {
        let scope = repo_scope_of(&request.get_ref().repo);
        self.require(&request, "Forge.ListIssues", scope)?;
        Ok(Response::new(pb::ListIssuesResponse {
            issues: vec![secret_issue()],
        }))
    }
    async fn get_issue(
        &self,
        request: Request<pb::GetIssueRequest>,
    ) -> Result<Response<pb::GetIssueResponse>, Status> {
        let scope = repo_scope_of(&request.get_ref().repo);
        self.require(&request, "Forge.GetIssue", scope)?;
        Ok(Response::new(pb::GetIssueResponse {
            state: Some(secret_issue()),
            events: Vec::new(),
        }))
    }
}

fn secret_issue() -> pb::IssueState {
    pb::IssueState {
        repo: "acme/app".to_owned(),
        number: 1,
        title: SECRET_ISSUE.to_owned(),
        author: "acme".to_owned(),
        open: true,
        ..Default::default()
    }
}

#[tonic::async_trait]
impl pb::policy_server::Policy for Fake {
    async fn get_protection(
        &self,
        request: Request<pb::GetProtectionRequest>,
    ) -> Result<Response<pb::GetProtectionResponse>, Status> {
        let scope = repo_scope(request.get_ref().repo.clone());
        self.require(&request, "Policy.GetProtection", scope)?;
        Ok(Response::new(pb::GetProtectionResponse {
            protections: vec![pb::BranchProtection {
                ref_pattern: SECRET_RULE.to_owned(),
                ..Default::default()
            }],
        }))
    }
    async fn get_ref_name_policy(
        &self,
        request: Request<pb::GetRefNamePolicyRequest>,
    ) -> Result<Response<pb::GetRefNamePolicyResponse>, Status> {
        let scope = repo_scope(request.get_ref().repo.clone());
        self.require(&request, "Policy.GetRefNamePolicy", scope)?;
        Ok(Response::new(pb::GetRefNamePolicyResponse {
            policy: Some(pb::RefNamePolicy {
                allow_patterns: vec![SECRET_RULE.to_owned()],
                deny_patterns: Vec::new(),
            }),
        }))
    }
    async fn list_gates(
        &self,
        request: Request<pb::ListGatesRequest>,
    ) -> Result<Response<pb::ListGatesResponse>, Status> {
        let scope = repo_scope(request.get_ref().repo.clone());
        self.require(&request, "Policy.ListGates", scope)?;
        Ok(Response::new(pb::ListGatesResponse {
            gates: vec![pb::GateInfo {
                name: SECRET_GATE.to_owned(),
                description: String::new(),
                enabled: true,
            }],
        }))
    }
}

#[tonic::async_trait]
impl pb::usage_server::Usage for Fake {
    async fn read(
        &self,
        request: Request<pb::ReadUsageRequest>,
    ) -> Result<Response<pb::ReadUsageResponse>, Status> {
        let namespace = request.get_ref().namespace.clone();
        self.require(&request, "Usage.Read", Scope::Repo(namespace.clone(), String::new()))?;
        Ok(Response::new(pb::ReadUsageResponse {
            usage: Some(pb::NamespaceUsage {
                namespace: namespace.clone(),
                pack_bytes: 5,
                ..Default::default()
            }),
            allowance: Some(pb::Allowance {
                pack_bytes: Some(100),
                ..Default::default()
            }),
            // gunnar's own `remaining` — deliberately NOT 100 − 5, so a console
            // that computed its own would be caught.
            remaining: Some(pb::Remaining {
                pack_bytes: Some(GUNNARS_REMAINING),
                ..Default::default()
            }),
            repos: vec![pb::RepoStored {
                repo: Some(pb::RepoPath {
                    namespace,
                    repo: "only-in-usage-read".to_owned(),
                }),
                stored_bytes: 5,
                ..Default::default()
            }],
        }))
    }
}

// ───────────────────────────────────────────────────────────────────────────
// The console
// ───────────────────────────────────────────────────────────────────────────

struct Rig {
    router: axum::Router,
    webauthn: Arc<crate::auth::Webauthn>,
    fake: Fake,
    state: crate::app::AppState,
}

impl Rig {
    /// A passkey session for `user` at `role`, bound to the principal of the
    /// same name.
    fn session(&self, user: &str, role: Role) -> String {
        self.webauthn
            .plant_bound_credential_for_tests(user, role, user);
        self.webauthn.make_session(user, role, true)
    }

    async fn post(&self, uri: &str, cookie: &str, body: serde_json::Value) -> (http::StatusCode, String) {
        let request = http::Request::builder()
            .method("POST")
            .uri(uri)
            .header(http::header::CONTENT_TYPE, "application/json")
            .header(http::header::ORIGIN, "https://console.example")
            .header(
                http::header::COOKIE,
                format!("{}={cookie}", crate::auth::SESSION_COOKIE),
            )
            .body(axum::body::Body::from(body.to_string()))
            .expect("a request");
        let response = self
            .router
            .clone()
            .oneshot(request)
            .await
            .expect("the console answers");
        let status = response.status();
        let bytes = axum::body::to_bytes(response.into_body(), 4 << 20)
            .await
            .expect("a body");
        (status, String::from_utf8_lossy(&bytes).into_owned())
    }

    async fn get(&self, uri: &str, cookie: &str) -> (http::StatusCode, String) {
        let request = http::Request::builder()
            .method("GET")
            .uri(uri)
            .header(http::header::ORIGIN, "https://console.example")
            .header(
                http::header::COOKIE,
                format!("{}={cookie}", crate::auth::SESSION_COOKIE),
            )
            .body(axum::body::Body::empty())
            .expect("a request");
        let response = self
            .router
            .clone()
            .oneshot(request)
            .await
            .expect("the console answers");
        let status = response.status();
        let bytes = axum::body::to_bytes(response.into_body(), 4 << 20)
            .await
            .expect("a body");
        (status, String::from_utf8_lossy(&bytes).into_owned())
    }
}

async fn console() -> Rig {
    let fake = Fake::default();
    // acme owns its namespace; beta owns its own and nothing of acme's.
    fake.grant("acme", "acme", pb::GrantLevel::Admin);
    fake.grant("beta", "beta", pb::GrantLevel::Admin);
    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(pb::access_server::AccessServer::new(serving.clone()))
            .add_service(pb::accounts_server::AccountsServer::new(serving.clone()))
            .add_service(pb::admin_server::AdminServer::new(serving.clone()))
            .add_service(pb::repos_server::ReposServer::new(serving.clone()))
            .add_service(pb::pull_requests_server::PullRequestsServer::new(serving.clone()))
            .add_service(pb::refs_server::RefsServer::new(serving.clone()))
            .add_service(pb::forge_server::ForgeServer::new(serving.clone()))
            .add_service(pb::policy_server::PolicyServer::new(serving.clone()))
            .add_service(pb::usage_server::UsageServer::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 endpoint = format!("http://{addr}");
    let state: crate::app::AppState = Arc::new(crate::app::App {
        webauthn: Some(webauthn.clone()),
        admin_token: Some("break-glass".to_owned()),
        #[cfg(feature = "metered")]
        monetize: None,
        events: crate::sse::EventHub::detached(),
        gunnar: crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(signer_of(CONSOLE_SEED)))
            .expect("a handle"),
        twin: None,
        guest: Some(
            crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(signer_of(GUEST_SEED)))
                .expect("a guest handle"),
        ),
        reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
            endpoint.clone(),
            store.clone(),
        )),
        tenants: Arc::new(crate::tenant::TenantKeys::new(endpoint, store)),
        forges: Default::default(),
        imports: Arc::new(Imports::default()),
        forge_apps: Arc::default(),
        ui_dir: None,
        caches: crate::api::Caches::default(),
        objects: Default::default(),
        readmes: Default::default(),
    });
    let router = crate::serve::router(state.clone());
    Rig {
        router,
        webauthn,
        fake,
        state,
    }
}

/// The secrets a body for `beta` must not carry.
fn leaks(body: &str) -> Vec<&'static str> {
    [
        SECRET_PR,
        SECRET_ISSUE,
        SECRET_RULE,
        SECRET_GATE,
        SECRET_PERSON,
        SECRET_NOTE,
        SECRET_DELETED,
        SECRET_ORPHAN,
    ]
    .into_iter()
    .filter(|s| body.contains(s))
    .chain(body.contains(&SECRET_BOX_TOTAL.to_string()).then_some("the box total"))
    .collect()
}

/// Every call gunnar heard for one request was signed by `who`, and at least
/// one was made — a refusal the console raised on its own is not "gunnar
/// decided".
fn all_by(heard: &[String], who: &str, uri: &str) {
    assert!(!heard.is_empty(), "{uri}: gunnar was never asked");
    for line in heard {
        assert!(
            line.ends_with(&format!(" by {who}")),
            "{uri}: gunnar heard {line:?}, not a call signed by {who} — every call was {heard:?}"
        );
    }
}

// ───────────────────────────────────────────────────────────────────────────
// The rows
// ───────────────────────────────────────────────────────────────────────────

/// The reads of ONE repository: its pull requests, the review and inline
/// comments, its issues, its branch rules, ref-name policy, gates and
/// maintenance.
const REPO_READS: &[&str] = &[
    "/api/repos/acme/app/pulls",
    "/api/repos/acme/app/pulls/1",
    "/api/repos/acme/app/pulls/1/review",
    "/api/repos/acme/app/pulls/1/comments",
    "/api/repos/acme/app/issues",
    "/api/repos/acme/app/issues/1",
    "/api/repos/acme/app/protection",
    "/api/repos/acme/app/ref-names",
    "/api/repos/acme/app/gates",
    "/api/repos/acme/app/gc",
];

/// ★ **A second tenant reads nothing of another account's repository, and it
/// is GUNNAR that says no — on the second tenant's own key.** Red before this
/// module: each of these was a `State`-only handler signed with the console's
/// server-wide admin key, so beta read acme's private pull requests, issues
/// and branch rules by spelling the path.
#[tokio::test]
async fn a_second_tenant_is_refused_another_accounts_repository_by_gunnar_on_their_own_key() {
    let rig = console().await;
    let beta = rig.session("beta", Role::Viewer);
    for uri in REPO_READS {
        rig.fake.take_heard();
        let (status, body) = rig.get(uri, &beta).await;
        assert!(
            matches!(status, http::StatusCode::FORBIDDEN | http::StatusCode::NOT_FOUND),
            "{uri}: beta was answered {status}: {body}"
        );
        assert!(leaks(&body).is_empty(), "{uri}: {:?} reached beta: {body}", leaks(&body));
        all_by(&rig.fake.take_heard(), "beta", uri);
    }
}

/// The same reads as the OWNER: answered, on acme's own key — so the rows
/// above are gunnar's grant table and not a wall the console put up.
#[tokio::test]
async fn the_owner_reads_their_own_repository_on_their_own_key() {
    let rig = console().await;
    let acme = rig.session("acme", Role::Viewer);
    for uri in REPO_READS {
        rig.fake.take_heard();
        let (status, body) = rig.get(uri, &acme).await;
        assert_eq!(status, http::StatusCode::OK, "{uri}: {body}");
        all_by(&rig.fake.take_heard(), "acme", uri);
    }
    let (_, body) = rig.get("/api/repos/acme/app/pulls", &acme).await;
    assert!(body.contains(SECRET_PR), "{body}");
    let (_, body) = rig.get("/api/repos/acme/app/issues", &acme).await;
    assert!(body.contains(SECRET_ISSUE), "{body}");
}

/// The box-wide reads: the identity table, lockouts, an account's purge, the
/// deleted and orphan listings, the whole-box storage report, the counters and
/// gunnar's health.
const BOX_READS: &[&str] = &[
    "/api/principals",
    "/api/keys",
    "/api/grants",
    "/api/accounts/gates",
    "/api/accounts/SECRET-person/purge",
    "/api/repos/deleted",
    "/api/orphans",
    "/api/storage",
    "/api/stats",
    "/api/health",
];

/// ★ **A tenant is refused the box's identity table and its administrator's
/// reads — by gunnar, on the tenant's key.** Red before this module: every one
/// was answered with the console's key to any viewer, so a paying stranger read
/// every principal, key, grant, lockout note, deleted repository and orphan
/// store on the box. The administrator still reads them, and a tenant asking
/// AFTER the administrator is not handed the administrator's cached answer.
#[tokio::test]
async fn a_tenant_is_refused_the_boxs_identity_table_and_admin_reads_by_gunnar() {
    let rig = console().await;
    let beta = rig.session("beta", Role::Viewer);
    // The administrator: a server-wide Admin grant at gunnar, reading as
    // themselves (console roles are grants, thin layer 2026-10-04).
    rig.fake.grant("boss", "", pb::GrantLevel::Admin);
    let boss = rig.session("boss", Role::Admin);
    for round in ["before the administrator", "after the administrator"] {
        for uri in BOX_READS {
            rig.fake.take_heard();
            let (status, body) = rig.get(uri, &beta).await;
            assert!(
                matches!(status, http::StatusCode::FORBIDDEN | http::StatusCode::NOT_FOUND),
                "{uri} ({round}): beta was answered {status}: {body}"
            );
            assert!(leaks(&body).is_empty(), "{uri} ({round}): {:?} reached beta: {body}", leaks(&body));
            all_by(&rig.fake.take_heard(), "beta", uri);
        }
        if round == "before the administrator" {
            for uri in BOX_READS {
                let (status, body) = rig.get(uri, &boss).await;
                assert_eq!(status, http::StatusCode::OK, "{uri}: the administrator: {body}");
            }
        }
    }
}

/// ★ **A tenant's usage carries no box.** The namespace's own meters are
/// theirs; the appliance's capacity is not, and gunnar refuses it on the
/// tenant's key. Red before this module: `Admin.Capacity` was read with the
/// console's key and every customer saw the machine's total, free and
/// servable bytes beside their own.
#[tokio::test]
async fn a_tenants_usage_carries_their_meters_and_not_the_box() {
    let rig = console().await;
    let beta = rig.session("beta", Role::Viewer);
    rig.fake.take_heard();
    let (status, body) = rig.get("/api/namespaces/beta/usage", &beta).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let json: serde_json::Value = serde_json::from_str(&body).expect("json");
    assert_eq!(json["pack_bytes"], 5, "{body}");
    assert!(
        json.get("box").is_some_and(serde_json::Value::is_null),
        "the box reached a tenant: {body}"
    );
    assert!(
        !json["box_unreadable"].as_str().unwrap_or_default().is_empty(),
        "the page is told why there is no box: {body}"
    );
    assert!(leaks(&body).is_empty(), "{:?}: {body}", leaks(&body));
    all_by(&rig.fake.take_heard(), "beta", "/api/namespaces/beta/usage");

    // …and another account's meters are refused, by gunnar.
    let (status, body) = rig.get("/api/namespaces/acme/usage", &beta).await;
    assert!(
        matches!(status, http::StatusCode::FORBIDDEN | http::StatusCode::NOT_FOUND),
        "{body}"
    );
}

// ── gunnar's answers, taken as given (thin layer C-group, 2026-10-04) ──────

/// ★ **"My room" and the usage page are gunnar's answer, asked as the
/// person**: the per-repository table is `Usage.Read`'s own breakdown and
/// "free"/`remaining` is gunnar's `remaining` — not cap − used computed here,
/// and never a whole-box `Admin.StorageInfo` read with the console's key.
#[tokio::test]
async fn my_room_and_my_remaining_are_gunnars_answer_asked_as_me() {
    let rig = console().await;
    let beta = rig.session("beta", Role::Viewer);

    rig.fake.take_heard();
    let (status, body) = rig.get("/api/account/room", &beta).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let room: serde_json::Value = serde_json::from_str(&body).expect("json");
    assert_eq!(room["free_bytes"], GUNNARS_REMAINING, "free is not gunnar's remaining: {body}");
    assert_eq!(room["repos"][0]["repo"], "only-in-usage-read", "{body}");
    let heard = rig.fake.take_heard();
    assert!(
        !heard.iter().any(|h| h.starts_with("Admin.StorageInfo")),
        "My room read the box's storage report: {heard:?}"
    );
    all_by(&heard, "beta", "/api/account/room");

    let (status, body) = rig.get("/api/namespaces/beta/usage", &beta).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let usage: serde_json::Value = serde_json::from_str(&body).expect("json");
    assert_eq!(usage["remaining"]["pack_bytes"], GUNNARS_REMAINING, "{body}");
}

/// ★ **A search finds the 301st repository: gunnar applies it before
/// paging**, and "Imported only" is gunnar's `mirrored_only`. Red before: the
/// console asked for the whole catalogue and the browser filtered the first
/// 250 rows it was handed.
#[tokio::test]
async fn a_search_is_gunnars_and_finds_what_is_past_the_first_page() {
    let rig = console().await;
    let beta = rig.session("beta", Role::Viewer);
    let (status, body) = rig.get("/api/repos?q=needle&imported=true", &beta).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    assert!(body.contains("the-needle"), "{body}");
    assert!(
        rig.fake
            .searched
            .lock()
            .expect("searched")
            .contains(&("needle".to_owned(), true)),
        "the query was not handed to gunnar: {:?}",
        rig.fake.searched.lock().expect("searched")
    );
}

/// ★ **Whose account a namespace is comes from gunnar** (`owner_kind`), so
/// the page can tell the box's own namespaces from people's without reading
/// the identity table — which a person may not.
#[tokio::test]
async fn a_namespace_says_whose_kind_of_account_it_is() {
    let rig = console().await;
    let beta = rig.session("beta", Role::Viewer);
    let (status, body) = rig.get("/api/namespaces", &beta).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let page: serde_json::Value = serde_json::from_str(&body).expect("json");
    let kind = |name: &str| {
        page["items"]
            .as_array()
            .and_then(|items| items.iter().find(|n| n["name"] == name))
            .map(|n| n["owner_kind"].clone())
    };
    assert_eq!(kind("beta"), Some(serde_json::json!("user")), "{body}");
    assert_eq!(kind("gunnar-twin"), Some(serde_json::json!("machine")), "{body}");
}

/// ★ **A comparison is gunnar's `Refs.Diff`, asked as the reader**: every
/// changed file and its counts, past the 60 the console draws hunks for. Red
/// before: the console walked both trees itself, one `ReadObject` per tree,
/// believing gunnar could not diff.
#[tokio::test]
async fn a_comparison_is_gunnars_tree_diff_asked_as_the_reader() {
    let rig = console().await;
    let acme = rig.session("acme", Role::Viewer);
    // The reader acme's own channel, as the route would use it.
    let (status, _) = rig.get("/api/repos/acme/app/pulls", &acme).await;
    assert_eq!(status, http::StatusCode::OK);
    let reader = crate::api::tenant_reader(&rig.state, "acme").await.expect("acme reads as acme");
    let repo = crate::git::Repository::open(&reader.gunnar, "acme", "app", Default::default())
        .await
        .expect("opens");
    rig.fake.take_heard();
    let comparison = crate::git::diff::compare(&repo, "tree-before", "tree-after", "")
        .await
        .expect("gunnar answers the comparison");
    assert_eq!(comparison.changes.len(), 70);
    assert_eq!(comparison.counted_files, 70, "every file is counted, by gunnar");
    assert_eq!(comparison.insertions, 140);
    assert!(comparison.hunks_truncated);
    let heard = rig.fake.take_heard();
    assert!(heard.contains(&"Refs.Diff by acme".to_owned()), "{heard:?}");
}

// ── roles are gunnar's grants (thin layer L, 2026-10-04) ───────────────────

/// ★ **An administrator reads and acts as THEMSELVES**: their console role is
/// their server-wide grant at gunnar, so the console signs with their own key
/// and gunnar's audit names them. Red before: an operator's or an
/// administrator's session was answered with the console's own key.
#[tokio::test]
async fn an_administrator_reads_as_themselves_and_gunnar_names_them() {
    let rig = console().await;
    rig.fake.grant("boss", "", pb::GrantLevel::Admin);
    let boss = rig.session("boss", Role::Admin);
    rig.fake.take_heard();
    let (status, body) = rig.get("/api/principals", &boss).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    all_by(&rig.fake.take_heard(), "boss", "/api/principals");
}

/// ★ **The console role is gunnar's answer, not the passkey row's**: a
/// session whose row says admin, for a person gunnar holds no server-wide
/// grant for, is a viewer — refused an administrator's route, and told so by
/// `/auth/status` — and a grant at gunnar is what makes somebody an operator.
/// Red before: the role was the console's own ladder, read off its own row.
#[tokio::test]
async fn the_console_role_is_the_persons_server_wide_grant_at_gunnar() {
    let rig = console().await;
    rig.webauthn
        .plant_bound_credential_for_tests("mallory", Role::Admin, "mallory");
    let mallory = rig.webauthn.make_session_asking_gunnar("mallory", Role::Admin);
    let (status, body) = rig.get("/auth/status", &mallory).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let said: serde_json::Value = serde_json::from_str(&body).expect("json");
    assert_eq!(said["role"], "viewer", "gunnar holds no grant for mallory: {body}");
    let (status, body) = rig.get("/api/disks", &mallory).await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "an administrator's read: {body}");

    // A server-wide Write at gunnar is what makes somebody an operator.
    rig.fake.grant("olive", "", pb::GrantLevel::Write);
    rig.webauthn
        .plant_bound_credential_for_tests("olive", Role::Operator, "olive");
    let olive = rig.webauthn.make_session_asking_gunnar("olive", Role::Operator);
    let (_, body) = rig.get("/auth/status", &olive).await;
    let said: serde_json::Value = serde_json::from_str(&body).expect("json");
    assert_eq!(said["role"], "operator", "{body}");
}

/// ★ **An issue is filed as the person**: the forge event names their
/// principal and carries their own signature, and gunnar's Write on the
/// repository decides. Red before: every issue, comment and close the console
/// filed was the console's own, attributed to its principal.
#[tokio::test]
async fn an_issue_is_filed_as_the_person_and_gunnar_decides() {
    let rig = console().await;
    rig.fake.grant("acme", "", pb::GrantLevel::Write);
    let acme = rig.session("acme", Role::Operator);
    rig.fake.take_heard();
    let (status, body) = rig
        .post(
            "/api/repos/acme/app/issues",
            &acme,
            serde_json::json!({ "title": "a bug", "body": "it broke" }),
        )
        .await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let heard = rig.fake.take_heard();
    assert!(heard.contains(&"Forge.Append by acme".to_owned()), "{heard:?}");
    assert!(heard.contains(&"event actor acme".to_owned()), "{heard:?}");
}