Drop the in-repo state fallback

Nothing has run against the host's backend yet, so there is no old config
or transcript to keep working -- the XDG paths are simply where state
lives. Removing the fallback takes repo_root() with it, since finding the
repo from the running executable existed only to locate that legacy state.

AGENTS.md gets the arrangement that replaced it: the host builds and runs
from its own clone outside the shared mount, and code reaches it by push
to gitea, which this VM's key is not authorized for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Fable 5 committed 2026-08-25 04:01:37 -04:00
1 parent d2d2832ec8
commit ce349af414
3 files changed
+19 -106

No files matched your search

+4 -92
View File
@@ -21,7 +21,7 @@ mod ssh;
mod usage;
use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result, bail};
@@ -55,59 +55,6 @@ fn xdg_dir(var: &str, fallback: &str) -> PathBuf {
.join("ai-app")
}
/// Picks `preferred`, unless only the older in-repo `legacy` path exists --
/// in which case it is used, loudly.
///
/// State used to live in the repo, which is wrong for two reasons: a
/// `git clean -xdf` would take the enrollment and every transcript with
/// it, and this repo is a virtiofs mount shared with a machine that is not
/// trusted with them (see PLAN.md's security section). Moving is left to a
/// person rather than done silently, because relocating a live config out
/// from under a running backend is not the kind of thing a program should
/// decide to do on its own.
fn prefer_xdg(preferred: PathBuf, legacy: PathBuf, what: &str) -> PathBuf {
if !preferred.exists() && legacy.exists() {
tracing::warn!(
"using {} from {} -- move it to {} when convenient; the repo is shared with the VM \
and is not a good home for {what}",
what,
legacy.display(),
preferred.display(),
);
return legacy;
}
preferred
}
/// The repo root, one level above this crate. Only used now to find the
/// legacy in-repo state above; everything the server reads by default
/// resolves from the XDG directories instead.
///
/// Found from the running executable first, and only then from the path
/// compiled in. This repo is shared between a VM and its host over
/// virtiofs at *different* absolute paths (`~/host/repos/ai-app` vs
/// `~/stuff/vm/ai/repos/ai-app`), and `target/` is shared along with it --
/// so a binary built on one side and run on the other would otherwise look
/// for its config under a path that doesn't exist there, which is exactly
/// what happened once. Where both agree (the ordinary case) the answer is
/// identical either way; the flags below override it regardless.
fn repo_root() -> &'static Path {
static ROOT: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
ROOT.get_or_init(|| {
// target/{debug,release}/ai-server -> four levels up is the root.
let from_exe = std::env::current_exe().ok().and_then(|exe| {
let root = exe.ancestors().nth(4)?.to_path_buf();
root.join("server/Cargo.toml").is_file().then_some(root)
});
from_exe.unwrap_or_else(|| {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("CARGO_MANIFEST_DIR has a repo-root parent")
.to_path_buf()
})
})
}
/// Serves AI coding sessions (Claude Code, llama.cpp) to the phone app.
#[derive(Parser)]
struct Args {
@@ -193,16 +140,8 @@ async fn main() -> Result<()> {
tracing_subscriber::fmt().with_env_filter("info").init();
let args = Args::parse();
let config_path = args.config.unwrap_or_else(|| {
prefer_xdg(
config_home().join("config.json"),
repo_root().join("config.json"),
"the config",
)
});
let data_dir = args.data_dir.unwrap_or_else(|| {
prefer_xdg(data_home().join("sessions"), repo_root().join("sessions"), "transcripts")
});
let config_path = args.config.unwrap_or_else(|| config_home().join("config.json"));
let data_dir = args.data_dir.unwrap_or_else(|| data_home().join("sessions"));
let manager = Arc::new(
SessionManager::new(config_path.clone(), data_dir)
.with_context(|| format!("failed to load {}", config_path.display()))?,
@@ -244,9 +183,7 @@ async fn main() -> Result<()> {
print_enrollment(bind_ip, args.port, &token)?;
}
let certs_dir = args
.certs
.unwrap_or_else(|| prefer_xdg(config_home().join("certs"), repo_root().join("certs"), "certificates"));
let certs_dir = args.certs.unwrap_or_else(|| config_home().join("certs"));
let leaf_cert = certs_dir.join("leaf.pem");
let leaf_key = certs_dir.join("leaf-key.pem");
if !leaf_cert.is_file() || !leaf_key.is_file() {
@@ -292,31 +229,6 @@ async fn main() -> Result<()> {
mod tests {
use super::*;
/// Choosing the wrong file here is silent -- the server would come up
/// happily on the wrong tokens and the wrong session list -- so the
/// precedence is worth pinning down.
#[test]
fn state_prefers_xdg_and_falls_back_to_the_repo_only_when_it_must() {
let dir = tempfile::tempdir().expect("tempdir");
let xdg = dir.path().join("xdg/config.json");
let legacy = dir.path().join("repo/config.json");
std::fs::create_dir_all(xdg.parent().unwrap()).expect("mkdir");
std::fs::create_dir_all(legacy.parent().unwrap()).expect("mkdir");
// Neither exists: the XDG path, which is where a first run writes.
assert_eq!(prefer_xdg(xdg.clone(), legacy.clone(), "the config"), xdg);
// Only the old in-repo one exists: keep using it rather than
// silently starting empty and losing the enrollment.
std::fs::write(&legacy, "{}").expect("write");
assert_eq!(prefer_xdg(xdg.clone(), legacy.clone(), "the config"), legacy);
// Both exist: the moved one wins, so finishing a migration takes
// effect even if the old file was left behind.
std::fs::write(&xdg, "{}").expect("write");
assert_eq!(prefer_xdg(xdg.clone(), legacy, "the config"), xdg);
}
#[test]
fn xdg_dirs_respect_the_environment_and_are_namespaced() {
// Relative values are ignored per the spec, rather than resolving