Condense the documentation and thin the server's comments

The markdown had accumulated a lot that was stale rather than wrong.
PLAN.md still described pi as the llama.cpp harness, a refcounted
LlamaServerManager, and a providers-by-hosts cross-product, all of which
were superseded or never built; it also carried a second copy of the HTTP
table that routes.rs owns. EXPLORER.md and TRANSCRIPT_CACHE.md held
implementation checklists for work that has since landed. AGENTS.md
restated most of PLAN.md's design instead of being the working-notes
layer it says it is. 3225 lines of markdown to 2180, with the stale
sections gone rather than reworded.

On the server, comments explaining what the code already says are out and
the ones recording a constraint, a measurement or an incident are kept but
cut to a few lines each: 5504 comment lines to 4586.

Four doc comments in session/mod.rs, and one each in process.rs and
usage.rs, had drifted onto the item above the one they describe --
functions were reordered without them, so `stop_session`'s doc sat on
`set_session_cwd`, `stat_of`'s on `struct Stat`, and `UsageMonitor`'s on
`type Cached`. Each is back on its own item.

routes.rs's module table also claimed later phases would add `/hosts`,
which setups replaced.

cargo test (127 passed), clippy --all-targets and fmt are clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-04 15:45:43 -04:00
1 parent e3e02d55f7
commit 79682f03a7
24 files changed
+4572 -6821

No files matched your search

+69 -92
View File
@@ -1,14 +1,12 @@
//! A phone interface to AI coding sessions -- the backend. See PLAN.md for
//! the whole picture; this is the entry point: config + session registry,
//! token bootstrap, and the one TLS listener.
//! A phone interface to AI coding sessions -- the backend. See PLAN.md for the
//! whole picture; this is the entry point: config + session registry, token
//! bootstrap, and the one TLS listener.
//!
//! The listener binds the WireGuard interface's address only, and fails
//! closed -- if `wg0` is down the server refuses to start rather than
//! falling back to `0.0.0.0`, because this API *is* remote code execution
//! and the tunnel is what keeps its pre-auth surface (TLS handshake, HTTP
//! parsing, auth middleware) off the open internet. `--bind` overrides
//! explicitly for development; that is a deliberate, logged choice, never a
//! fallback.
//! The listener binds the WireGuard interface's address only, and fails closed
//! -- if `wg0` is down the server refuses to start rather than falling back to
//! `0.0.0.0`, because this API *is* remote code execution and the tunnel is
//! what keeps its pre-auth surface off the open internet. `--bind` overrides
//! explicitly for development; a deliberate, logged choice, never a fallback.
//!
//! There is no plaintext listener at all, so the bearer token can't travel
//! unencrypted by misconfiguration -- even inside the tunnel.
@@ -50,10 +48,9 @@ struct Args {
#[arg(long, default_value_t = DEFAULT_PORT)]
port: u16,
/// Address to bind instead of the wg0 interface's -- a development
/// override (e.g. 127.0.0.1 for curl, or a LAN address for a phone
/// before the tunnel exists). Production runs without it and fails
/// closed when wg0 is absent.
/// Address to bind instead of the wg0 interface's -- a development override
/// (127.0.0.1 for curl, or a LAN address for a phone before the tunnel
/// exists). Production runs without it and fails closed when wg0 is absent.
#[arg(long)]
bind: Option<IpAddr>,
@@ -83,44 +80,34 @@ struct Args {
rotate_token: bool,
/// Enroll one more device without touching the running server: mint a
/// token, print its enrollment link (one line, stdout, nothing else)
/// and exit. The server adopts the token the first time that device
/// uses it. For a tool -- Dev Updater -- that opens the link on the
/// phone, where a QR printed here cannot be scanned.
/// token, print its enrollment link (one line, stdout, nothing else) and
/// exit. The server adopts the token the first time that device uses it.
/// For a tool that opens the link on the phone, where a QR printed here
/// cannot be scanned.
#[arg(long)]
enroll_link: bool,
/// Hold every response back by this many milliseconds.
///
/// A development aid, and a specific one: over the tunnel a phone's
/// requests take tens to hundreds of milliseconds, and several faults
/// live entirely in what the app does *while* one is outstanding --
/// a page of history landing mid-fling, a screen drawn before its
/// first answer arrives. On a loopback server every response is back
/// within a millisecond or two, so those windows close before
/// anything can be observed and the bug looks like it is not there.
/// This reopens them on demand rather than by unplugging something.
/// A development aid, and a specific one: over the tunnel a phone's requests
/// take tens to hundreds of milliseconds, and several faults live entirely
/// in what the app does *while* one is outstanding. On a loopback server
/// those windows close before anything can be observed and the bug looks
/// like it is not there.
#[arg(long, default_value_t = 0, value_name = "MS")]
delay: u64,
/// Mark every session spawned here as throwaway: its process is
/// stopped when this server exits, instead of being left running for
/// the next start to adopt. On by default in a debug build.
/// Mark every session spawned here as throwaway: its process is stopped
/// when this server exits, instead of being left running for the next start
/// to adopt. On by default in a debug build.
///
/// Sessions outlive the backend on purpose, which is right for the
/// ones somebody is using and wrong for the ones a test made: a
/// session spawned to check something leaves a `claude` behind that
/// every later server adopts, and they accumulate silently -- twelve
/// of them on this machine in a day, each holding a conversation open.
/// So a development build cleans up after itself unless told not to
/// (`--throwaway-sessions=false`), and a release build never does
/// unless asked.
/// Sessions outlive the backend on purpose, which is right for the ones
/// somebody is using and wrong for the ones a test made -- twelve of those
/// accumulated on this machine in a day, each holding a conversation open.
///
/// The flag decides only what *new* sessions are marked as. What
/// happens on the way out is decided by the mark, which is written
/// into the session and outlives the server that made it -- so
/// sessions spawned without it keep running, whichever server is up
/// when one exits.
/// The flag decides only what *new* sessions are marked as. What happens on
/// the way out is decided by the mark, which outlives the server that made
/// it.
#[arg(
long,
default_value_t = cfg!(debug_assertions),
@@ -134,18 +121,16 @@ struct Args {
#[tokio::main]
async fn main() -> Result<()> {
// Both rustls crypto providers are in the dependency graph (ureq
// brings ring, axum-server brings aws-lc-rs), so rustls refuses to
// pick one itself; choose before anything touches TLS.
// Both rustls crypto providers are in the dependency graph (ureq brings
// ring, axum-server brings aws-lc-rs), so rustls refuses to pick one itself.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.expect("no other TLS crypto provider is installed before main");
// `info` unless RUST_LOG says otherwise. Written as a *fallback* rather than as the filter,
// because `with_env_filter("info")` is a fixed directive that never reads the environment --
// so the per-request diagnostics that AGENTS.md tells you to turn on with
// `RUST_LOG=ai_server=debug` printed nothing, and the switch looked like the code it was
// meant to instrument being wrong.
// `info` unless RUST_LOG says otherwise. Written as a *fallback* rather than
// as the filter, because `with_env_filter("info")` is a fixed directive that
// never reads the environment -- so `RUST_LOG=ai_server=debug` printed
// nothing, and the switch looked like the code it was meant to instrument.
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
@@ -157,11 +142,10 @@ async fn main() -> Result<()> {
let config_path = args
.config
.unwrap_or_else(|| config_home("ai-app").join("config.ron"));
// Before the manager exists, on purpose: constructing it and seeding
// setups touches sessions and subprocesses this invocation has no
// business with while another instance is serving. Only the hash
// reaches disk, in the spool `auth.rs` reads; the link itself goes to
// stdout alone, because the caller opens whatever this prints.
// Before the manager exists, on purpose: constructing it and seeding setups
// touches sessions and subprocesses this invocation has no business with
// while another instance is serving. Only the hash reaches disk, in the
// spool `auth.rs` reads; the link goes to stdout alone.
if args.enroll_link {
let bind_ip = match args.bind {
Some(ip) => ip,
@@ -182,9 +166,9 @@ async fn main() -> Result<()> {
let data_dir = args
.data_dir
.unwrap_or_else(|| data_home("ai-app").join("sessions"));
// Beside the session data rather than under it: models outlive every
// session and are shared by all of them, so deleting a session must
// never take a multi-gigabyte download with it.
// Beside the session data rather than under it: models outlive every session
// and are shared by all of them, so deleting a session must never take a
// multi-gigabyte download with it.
let models_dir = args
.models_dir
.unwrap_or_else(|| data_home("ai-app").join("models"));
@@ -200,9 +184,9 @@ async fn main() -> Result<()> {
server exits rather than left running (--throwaway-sessions=false to keep them)"
);
}
// After construction rather than inside it: seeding asks this machine
// what it has, which is I/O, and a constructor that quietly runs a
// subprocess is a surprise to every caller including the tests.
// After construction rather than inside it: seeding asks this machine what
// it has, and a constructor that quietly runs a subprocess is a surprise to
// every caller including the tests.
manager.seed_setup().await?;
tracing::info!("config: {}", config_path.display());
@@ -210,9 +194,9 @@ async fn main() -> Result<()> {
for setup in manager.setups() {
match &setup.ssh {
Some(ssh) => tracing::info!(" setup \"{}\" -> {}", setup.name, ssh.address),
// No parenthetical naming the local machine: the default
// setup is *called* "this machine", and the line read
// "setup this machine (this machine)".
// No parenthetical naming the local machine: the default setup is
// *called* "this machine", and the line read "setup this machine
// (this machine)".
None => tracing::info!(" setup \"{}\" runs here", setup.name),
}
for provider in &setup.providers {
@@ -228,10 +212,9 @@ async fn main() -> Result<()> {
);
}
// Before the interface check below, deliberately: the certificates are
// also what the phone app embeds at build time, so they need to be
// obtainable on a machine whose tunnel isn't up yet. The leaf is
// reissued on every start, so once wg0 exists the next start covers it.
// Before the interface check below, deliberately: the certificates are also
// what the phone app embeds at build time, so they need to be obtainable on
// a machine whose tunnel isn't up yet. The leaf is reissued on every start.
let certs_dir = args
.certs
.unwrap_or_else(|| config_home("ai-app").join("certs"));
@@ -256,9 +239,8 @@ async fn main() -> Result<()> {
None => netif::wg_address("ai-server")?,
};
// Token bootstrap: first run generates one; --rotate-token replaces
// whatever exists. Either way the plaintext appears exactly once, in
// the QR printed here.
// Token bootstrap: first run generates one; --rotate-token replaces whatever
// exists. Either way the plaintext appears exactly once, in the QR.
if args.rotate_token || manager.tokens().is_empty() {
let rotating = args.rotate_token && !manager.tokens().is_empty();
let token = enroll::generate_token();
@@ -279,16 +261,13 @@ async fn main() -> Result<()> {
.await
.context("failed to load TLS cert/key")?;
// No providers listed here any more: which machines can be asked, and
// about what, comes from the setups at the moment the screen is opened
// -- so a machine added from the phone reports its limits without a
// restart, and the backend's own account stops standing in for every
// machine's.
// No providers listed here any more: which machines can be asked, and about
// what, comes from the setups at the moment the screen is opened -- so a
// machine added from the phone reports its limits without a restart.
let monitor = Arc::new(usage::UsageMonitor::new());
// The bearer-token middleware wraps the entire router -- routes and
// fallback alike -- here and only here, so a new route can't forget
// auth. Zero unauthenticated endpoints.
// The bearer-token middleware wraps the entire router -- routes and fallback
// alike -- here and only here, so a new route can't forget auth.
let app = routes::router(Arc::clone(&manager))
.merge(routes::usage_router(monitor, Arc::clone(&manager)))
.merge(routes::models_router(Arc::clone(&models)))
@@ -297,9 +276,9 @@ async fn main() -> Result<()> {
auth::require_token,
));
// Outside the auth layer, so an unauthenticated request is refused at
// the speed it always was: this is here to slow the app down, not to
// widen the window on anything guessing at tokens.
// Outside the auth layer, so an unauthenticated request is refused at the
// speed it always was: this is here to slow the app down, not to widen the
// window on anything guessing at tokens.
let app = match args.delay {
0 => app,
ms => {
@@ -316,13 +295,11 @@ async fn main() -> Result<()> {
let addr = SocketAddr::new(bind_ip, args.port);
tracing::info!("serving https://{addr}");
// Let go of the sessions on the way out rather than stopping them:
// their processes are meant to outlive this one, so restarting the
// backend does not end a turn somebody is waiting on. Each is recorded
// in its session directory and adopted again on the way back up (see
// `session::process`). The exception is the sessions marked throwaway,
// which are stopped first -- see `--throwaway-sessions`. Both signals,
// because systemd and OpenRC send TERM while a terminal sends INT.
// Let go of the sessions on the way out rather than stopping them: their
// processes are meant to outlive this one. Each is recorded in its session
// directory and adopted again on the way back up. The exception is the
// sessions marked throwaway, which are stopped first. Both signals, because
// systemd and OpenRC send TERM while a terminal sends INT.
let serving = axum_server::bind_rustls(addr, tls_config)
.serve(app.into_make_service_with_connect_info::<SocketAddr>());
let mut terminate = signal(SignalKind::terminate()).context("listening for SIGTERM")?;
@@ -331,9 +308,9 @@ async fn main() -> Result<()> {
_ = terminate.recv() => tracing::info!("SIGTERM -- letting go of sessions"),
_ = tokio::signal::ctrl_c() => tracing::info!("interrupted -- letting go of sessions"),
}
// Stopped before the rest are let go of, and on every way out of the
// select above: a throwaway session is one nobody meant to keep, and
// the whole point is that nothing has to remember to clean it up.
// Stopped before the rest are let go of, and on every way out of the select
// above: a throwaway session is one nobody meant to keep, and the whole point
// is that nothing has to remember to clean it up.
manager.stop_throwaway_sessions();
manager.detach_all();