gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! ★ **The generic server settings, over the REAL plane and stores** (owner decision
//! 2026-10-26, item 5: "stored as a generic server setting, audited").
//!
//! `Admin.GetSetting` / `Admin.SetSetting`: bytes under a name the client chooses. Server Admin
//! only; every write audited (who, the name, old → new); bounded, each refusal naming its
//! bound; kept across a restart of the server. And the server learns nothing of what a client
//! keeps there: no product feature is named in the server crates (the grep below). Its own test
//! binary: `tracing` caches a callsite's interest per process (see gunnar-grpc's
//! `machine_enrolment_audit.rs`).

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

use gunnar_grpc::{
    AdminService, Caller, ControlPlane, EventBus, ServerConfig, ServiceContext, AUDIT_TARGET,
};
use gunnar_proto::admin_server::Admin as _;
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use tonic::{Code, Request};
use tracing_subscriber::layer::SubscriberExt as _;

#[derive(Clone, Default)]
struct AuditLog(Arc<Mutex<Vec<String>>>);

struct AuditLayer(AuditLog);

impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for AuditLayer {
    fn on_event(
        &self,
        event: &tracing::Event<'_>,
        _ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        if event.metadata().target() != AUDIT_TARGET {
            return;
        }
        struct Visit(String);
        impl tracing::field::Visit for Visit {
            fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
                self.0.push_str(&format!("{}={:?} ", field.name(), value));
            }
        }
        let mut visit = Visit(String::new());
        event.record(&mut visit);
        self.0 .0.lock().unwrap().push(visit.0);
    }
}

fn caller(id: &str, machine: bool) -> Caller {
    Caller {
        principal_id: id.to_owned(),
        fingerprint: format!("SHA256:{id}"),
        machine,
        authenticated: true,
    }
}

fn authed<T>(caller: &Caller, body: T) -> Request<T> {
    let mut r = Request::new(body);
    r.extensions_mut().insert(caller.clone());
    r
}

fn admin_at(dir: &Path) -> AdminService {
    let stores = Arc::new(ServerStores::open(dir).expect("stores"));
    let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
    if plane
        .add_principal("gunnar-ui", "gunnar-ui", PrincipalKind::Machine)
        .is_ok()
    {
        plane
            .grant("gunnar-ui", "", "", GrantLevel::Admin)
            .expect("the console is a server admin");
        plane
            .add_principal("acme", "acme", PrincipalKind::User)
            .expect("an account");
        plane
            .grant("acme", "acme", "", GrantLevel::Admin)
            .expect("acme administers its own account only");
    }
    let control: Arc<dyn ControlPlane> = plane;
    AdminService::new(ServiceContext::new(
        control,
        Arc::new(EventBus::new(64)),
        ServerConfig::default(),
    ))
}

#[test]
fn a_setting_is_server_admin_only_audited_bounded_and_kept() {
    let log = AuditLog::default();
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("runtime");
    let subscriber = tracing_subscriber::registry().with(AuditLayer(log.clone()));
    tracing::subscriber::with_default(subscriber, || runtime.block_on(inner(log.clone())));
}

async fn inner(log: AuditLog) {
    let dir = tempfile::tempdir().unwrap();
    let console = caller("gunnar-ui", true);
    let acme = caller("acme", false);
    let name = "console.a-switch";
    {
        let admin = admin_at(dir.path());
        let unset = admin
            .get_setting(authed(&console, GetSettingRequest { name: name.into() }))
            .await
            .expect("a read")
            .into_inner();
        assert_eq!(
            (unset.present, unset.value.as_slice()),
            (false, &b""[..]),
            "nothing stored yet"
        );

        // Somebody who is not a server admin reads and writes nothing.
        for refused in [
            admin
                .get_setting(authed(&acme, GetSettingRequest { name: name.into() }))
                .await
                .map(|_| ()),
            admin
                .set_setting(authed(
                    &acme,
                    SetSettingRequest {
                        name: name.into(),
                        value: b"on".to_vec(),
                    },
                ))
                .await
                .map(|_| ()),
            admin
                .set_setting(Request::new(SetSettingRequest {
                    name: name.into(),
                    value: b"on".to_vec(),
                }))
                .await
                .map(|_| ()),
        ] {
            let code = refused.expect_err("refused").code();
            assert!(
                matches!(code, Code::PermissionDenied | Code::Unauthenticated),
                "{code:?}"
            );
        }
        assert!(
            !log.0
                .lock()
                .unwrap()
                .iter()
                .any(|l| l.contains("admin.setting_set")),
            "a refused write writes and audits nothing"
        );

        let first = admin
            .set_setting(authed(
                &console,
                SetSettingRequest {
                    name: name.into(),
                    value: b"on".to_vec(),
                },
            ))
            .await
            .expect("the console writes")
            .into_inner();
        assert!(!first.had && first.previous.is_empty());
        let second = admin
            .set_setting(authed(
                &console,
                SetSettingRequest {
                    name: name.into(),
                    value: b"off".to_vec(),
                },
            ))
            .await
            .expect("again")
            .into_inner();
        assert_eq!(
            (second.had, second.previous.as_slice()),
            (true, &b"on"[..]),
            "the value it replaced"
        );

        // Bounded, and each refusal names its bound.
        for (n, v, bound) in [
            ("Console.X".to_owned(), b"1".to_vec(), "[a-z0-9._-]"),
            ("a".repeat(129), b"1".to_vec(), "128"),
            ("console.big".to_owned(), vec![b'x'; 4097], "4096"),
        ] {
            let s = admin
                .set_setting(authed(&console, SetSettingRequest { name: n, value: v }))
                .await
                .expect_err("out of bounds");
            assert_eq!(s.code(), Code::InvalidArgument, "{s:?}");
            assert!(
                s.message().contains(bound),
                "the refusal names the bound {bound}: {}",
                s.message()
            );
        }
    }
    // Kept across a restart of the server.
    let admin = admin_at(dir.path());
    let kept = admin
        .get_setting(authed(&console, GetSettingRequest { name: name.into() }))
        .await
        .expect("a read")
        .into_inner();
    assert_eq!((kept.present, kept.value.as_slice()), (true, &b"off"[..]));

    let lines: Vec<String> = log
        .0
        .lock()
        .unwrap()
        .iter()
        .filter(|l| l.contains("admin.setting_set"))
        .cloned()
        .collect();
    assert_eq!(lines.len(), 2, "one audit line per write: {lines:#?}");
    assert!(
        lines[0].contains("principal=gunnar-ui")
            && lines[0].contains(&format!("name={name}"))
            && lines[0].contains("old=(unset)")
            && lines[0].contains("new=\"on\""),
        "{}",
        lines[0]
    );
    assert!(
        lines[1].contains("old=\"on\"") && lines[1].contains("new=\"off\""),
        "{}",
        lines[1]
    );
}

/// ★ The server stays generic: no source file of the server crates names a product feature a
/// console keeps a switch for (the public site, its SEO, the MCP connector, IndexNow).
#[test]
fn the_server_crates_name_no_console_feature() {
    let crates = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
    let words = [
        "public-site",
        "public_site",
        "gunnar-mcp",
        "gunnar_mcp",
        "console.mcp",
        "indexnow",
        "llms.txt",
        "seo",
    ];
    let mut hits = Vec::new();
    for krate in [
        "gunnar-server/src",
        "gunnar-core/src",
        "gunnar-grpc/src",
        "gunnar-proto/proto",
    ] {
        walk(&crates.join(krate), &mut |path, text| {
            for (i, line) in text.lines().enumerate() {
                let lower = line.to_ascii_lowercase();
                if words.iter().any(|w| contains_word(&lower, w)) || contains_word(&lower, "mcp") {
                    hits.push(format!("{}:{}: {}", path.display(), i + 1, line.trim()));
                }
            }
        });
    }
    assert!(
        hits.is_empty(),
        "the server crates name a console feature:\n{}",
        hits.join("\n")
    );
    let mut seen = 0;
    walk(&crates.join("gunnar-core/src"), &mut |_, _| seen += 1);
    assert!(seen > 10, "the walk read the sources");
}

/// `word` in `line` with no letter or digit beside it (so `seo` is not `useo…`).
fn contains_word(line: &str, word: &str) -> bool {
    line.match_indices(word).any(|(i, _)| {
        let before = line[..i]
            .chars()
            .next_back()
            .is_none_or(|c| !c.is_ascii_alphanumeric());
        let after = line[i + word.len()..]
            .chars()
            .next()
            .is_none_or(|c| !c.is_ascii_alphanumeric());
        before && after
    })
}

fn walk(dir: &Path, seen: &mut dyn FnMut(&Path, &str)) {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            walk(&path, seen);
        } else if path.extension().is_some_and(|e| e == "rs" || e == "proto") {
            if let Ok(text) = std::fs::read_to_string(&path) {
                seen(&path, &text);
            }
        }
    }
}

#[test]
fn the_word_test_needs_the_whole_word() {
    assert!(contains_word("the seo pages", "seo") && contains_word("(mcp)", "mcp"));
    assert!(!contains_word("museo", "seo") && !contains_word("mcpx", "mcp"));
}