Prune commentary and stale Rust port notes

This commit is contained in:
iris committed 2026-09-10 00:44:13 -04:00
1 parent 5428cd75c9
commit 25370731d0
193 files changed
+693 -16219

No files matched your search

-207
View File
@@ -1,18 +1,3 @@
//! The server's persistent state: the enrolled token hashes and the sessions
//! that exist.
//!
//! Written whole and atomically (temp file + rename) rather than appended to:
//! it is small, and a half-written config would take the server down on next
//! start with no obvious way to recover from a phone. Every mutation funnels
//! through `SessionManager`, so in-memory and on-disk state can't come apart.
//!
//! The file is RON, in the shape [`wg_app_link::format`] describes -- the same
//! two house rules as dev-updater's config, because both are read and written
//! by hand.
//!
//! Transcripts do NOT live here: each session's events are an append-only JSONL
//! file in its own directory.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
@@ -24,14 +9,9 @@ use wg_app_link::format;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Config {
/// Enrolled device tokens, hashes only -- a leaked config doesn't leak the
/// credential. A list (of one, today) so per-device tokens with individual
/// revocation are a config entry later, not a migration.
pub tokens: Vec<TokenEntry>,
pub setups: Vec<SetupConfig>,
pub sessions: Vec<SessionConfig>,
/// What a new session's thinking level is when nothing chose one.
///
/// Here rather than on a provider because providers are *discovered*: a
/// default written onto one would be erased by the next rediscovery, which
/// is the kind of setting that looks like it stuck until the day it did
@@ -44,26 +24,13 @@ pub struct Config {
pub default_effort: Option<String>,
}
/// A machine, and the things it can run.
///
/// This is the unit a session is spawned against. Grouping providers under the
/// machine they exist on is what stops the spawn screen offering combinations
/// that cannot work; the previous model let any provider be paired with any
/// host and offered the whole cross-product.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetupConfig {
/// Stable identifier, minted when the setup is added and never
/// changed. Sessions reference this rather than the label, so
/// renaming a machine on the phone does not orphan its sessions --
/// which is the whole reason the two are separate fields.
pub id: String,
pub name: String,
/// How to reach it, absent for this machine.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ssh: Option<SshConfig>,
/// What can be spawned here. Names are unique within a setup, and only
/// within it: two machines may each have a `claude-cli`, which is the point.
#[serde(default)]
pub providers: Vec<ProviderConfig>,
}
@@ -74,25 +41,18 @@ impl SetupConfig {
}
}
/// One thing a setup can run: which driver, and how to invoke it.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderConfig {
/// Shown on the spawn screen and stored by sessions that use it.
pub name: String,
pub kind: DriverKind,
/// Override for the executable, for an install that isn't on PATH.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
/// Models offered on the spawn screen. Free text is always allowed
/// too; this is a shortcut list, not a restriction.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub models: Vec<String>,
}
impl ProviderConfig {
/// The executable to run for this provider: its override, or its kind's
/// default.
pub fn program(&self) -> &str {
self.command
.as_deref()
@@ -100,28 +60,16 @@ impl ProviderConfig {
}
}
/// How to reach a setup that isn't this machine, with the system `ssh` client
/// -- so `~/.ssh/config`, agents and jump hosts all keep working, and there is
/// one place to configure connections. A remote session is the identical
/// command with `ssh host …` in front, and nothing downstream knows.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SshConfig {
/// `user@host`, or a `Host` alias from `~/.ssh/config`.
pub address: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity_file: Option<PathBuf>,
/// Extra `-o` settings, each written as `Key=value`.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<String>,
/// Where this machine keeps the GGUF models it can serve, absent for
/// the same default this backend uses (`~/.local/share/ai-app/models`
/// -- `$XDG_DATA_HOME` is not read on the far side, since it is this
/// machine's environment that would answer). A `~` prefix is the
/// remote home.
///
/// Here rather than on the provider because it is a fact about the
/// machine, and because a machine reached over ssh is where the model
/// has to be: a llama.cpp session serves the file from the machine
@@ -139,22 +87,11 @@ pub struct SshConfig {
/// Which translator runs a session. A new one is a new driver behind the same
/// trait -- never a branch in shared code.
///
/// Snake case, which is both Rust's and RON's: this is written into a config a
/// person edits by hand, and a hyphen is not a RON identifier, so kebab case
/// cost the file a `kind: r#claude-cli` escape. The same string is what the
/// phone compares against, so the two move together.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DriverKind {
/// The fake driver: echoes messages back as streamed events, proving the
/// pipe with no AI involved. Always available as a built-in provider.
Echo,
/// A GGUF model served by llama.cpp's `llama-server`. The model is one this
/// machine has downloaded; the provider's command is the server binary.
LlamaCpp,
/// The Claude Code CLI over stream-json. Named for the CLI specifically:
/// bare "claude" would suggest the credit-billed API, which this is not.
ClaudeCli,
}
@@ -162,13 +99,6 @@ impl DriverKind {
/// The longest edge, in pixels, an image should have when it reaches this
/// kind of session -- `None` where nothing here has a limit worth enforcing.
///
/// Reported to the phone rather than applied here, so the bytes are made
/// small before they cross the tunnel instead of after: a modern phone photo
/// is several megabytes, and every one of them was being uploaded over
/// WireGuard only to be rejected at the other end. What decides the number
/// is the provider, which is why it lives beside the kind rather than in the
/// app.
///
/// 1568 for the Claude CLI because that is the longest edge the API itself
/// resizes to; anything larger is charged the same and spends the upload for
/// nothing, and far larger is refused outright -- which is what "sending an
@@ -183,19 +113,10 @@ impl DriverKind {
/// Which paid service meters a session of this kind, and `None` for one
/// that costs nothing.
///
/// What decides which account -- if any -- a rate-limit bar is about is the
/// provider a session runs, not the machine it runs on: an echo session on
/// a machine that also has the Claude CLI was drawn with that CLI's
/// five-hour window, a quota it cannot spend.
///
/// Echo names a meter of its own that exists only when a test has asked for
/// one (`/usage` in `session::echo`), which is how the bar's states are
/// reached without an account. With none set there is no snapshot, and the
/// phone draws nothing.
///
/// The string is a [`crate::usage::UsageProvider::name`], and it is what
/// pairs a session with one of `GET /usage`'s snapshots -- so
/// `usage::providers_for` reads this rather than matching on kinds again.
pub fn usage_provider(self) -> Option<&'static str> {
match self {
Self::ClaudeCli => Some(crate::usage::CLAUDE),
@@ -214,22 +135,10 @@ impl DriverKind {
match self {
Self::ClaudeCli => "claude",
Self::LlamaCpp => "llama-server",
// Echo is translated in-process; nothing is spawned for it.
Self::Echo => "echo",
}
}
/// Whether the conversation exists outside this app, so that deleting the
/// session here does not end it.
///
/// The Claude Code CLI owns its own transcript and is resumable from it
/// whatever started it, so a session this app spawned is every bit as
/// recoverable as one it imported. Echo has nothing to keep, and a llama
/// session's conversation is folded out of *this* app's transcript.
///
/// Asked before warning somebody that a deletion cannot be undone, which is
/// the one sentence that has to be true: said of a session that can in fact
/// be brought back, it spends the credibility the warning needs.
pub fn keeps_own_transcript(self) -> bool {
match self {
Self::ClaudeCli => true,
@@ -237,17 +146,6 @@ impl DriverKind {
}
}
/// Whether a thinking level means anything to this kind, so the phone can
/// offer the control only where it does something.
///
/// Reported from here rather than decided on the phone, and asked of the
/// *kind* rather than branched on: the alternative is the session-type
/// `if` this app does not have anywhere else. `--effort` is the Claude
/// CLI's; a llama session's sampling is `params`, and echo does not think.
///
/// It matters more than a control that would simply do nothing, because
/// choosing a level stops the process -- so on a session that cannot use
/// one it is a button whose only effect is the cost.
pub fn takes_effort(self) -> bool {
match self {
Self::ClaudeCli => true,
@@ -260,23 +158,14 @@ impl DriverKind {
#[serde(rename_all = "camelCase")]
pub struct TokenEntry {
pub name: String,
/// Hex SHA-256 of the token. A plain hash is enough: the token is 256 bits
/// from the OS CSPRNG, so there is nothing to dictionary-attack.
pub sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionConfig {
/// Stable identifier; names the session's directory and its routes.
pub id: String,
/// Id of the [`SetupConfig`] this session runs on -- the id, not the label,
/// so the machine can be renamed without losing its sessions.
pub setup: String,
/// Name of the provider within that setup. Both stored by name rather than
/// resolved, so an edited setup takes effect on the next relaunch; a session
/// whose setup or provider is gone reports as exited and can still be
/// deleted.
pub provider: String,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -288,9 +177,6 @@ pub struct SessionConfig {
/// the CLI stays the one authority on which modes exist.
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
/// How hard the model thinks, passed straight to `--effort`. A string for
/// the same reason `permission_mode` is: the CLI owns which levels exist.
///
/// Unlike the model and the mode, there is no control request that changes
/// one -- checked against 2.1.258, whose only two are `set_model` and
/// `set_permission_mode` -- so this is settled at launch and `None` means
@@ -299,16 +185,8 @@ pub struct SessionConfig {
/// rather than a level with a default written here.
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
/// Settings the driver interprets, chosen at spawn.
///
/// Deliberately untyped: what a temperature or a context size means is the
/// driver's business, and a field per driver is how a shared model starts
/// carrying one dialect's vocabulary. `permission_mode` above predates this
/// and should fold into it. BTreeMap so the file's order is stable.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub params: BTreeMap<String, String>,
/// Whether a phone should be told when this session wants attention.
///
/// Stored here rather than on the phone because it is a fact about the
/// session: one that runs unattended overnight should be quiet on every
/// device.
@@ -318,9 +196,6 @@ pub struct SessionConfig {
/// turned off in one tap where one that never arrived is not diagnosable.
#[serde(default = "notify_default")]
pub notify: bool,
/// Whether a session stopped by the account's usage limit sends itself a
/// message once the limit lifts, instead of waiting for a person.
///
/// Off unless somebody asked for it. It spends quota the moment it becomes
/// available and it does so while nobody is looking, which is exactly the
/// kind of thing that must not happen because a default said so.
@@ -331,40 +206,17 @@ pub struct SessionConfig {
/// the field goes back to it rather than sending an empty message.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_resume_message: Option<String>,
/// The message this session owes itself once the limit lifts, and when to
/// try. Written when a limit is hit, moved when the wait turns out to be
/// wrong, and cleared when the message goes out or auto-resume is turned
/// off -- see [`ScheduledResume`].
///
/// Persisted rather than held in memory because the wait outlives the
/// process doing it: a five-hour window and a weekly one both routinely
/// outlast a backend restart, and a resume forgotten across one is a
/// session that silently never comes back.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resume: Option<ScheduledResume>,
/// Whether this session's process is stopped when the server exits, instead
/// of being left running for the next start to adopt.
///
/// A fact about the session rather than about the run that spawned it, which
/// is why it is persisted: whichever server is running when the time comes
/// is the one that has to act on it.
///
/// Written by a server started with `--throwaway-sessions`, the default in a
/// debug build. Under the ordinary rule a test session's `claude` outlives
/// every server that ever knew about it -- twelve accumulated on this
/// machine in a day. Absent means false.
#[serde(default, skip_serializing_if = "not_set")]
pub throwaway: bool,
pub created: f64,
}
/// A message owed to a session whose account ran out, and when to try sending
/// it.
///
/// `since` is the whole reason this is a struct: the wait is rescheduled every
/// time the meter is asked and still says no, so `at` alone cannot say how long
/// this has been going on -- and something has to, or a machine that can never
/// be asked is retried until somebody notices. See `crate::resume`.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledResume {
@@ -375,35 +227,20 @@ pub struct ScheduledResume {
pub since: f64,
}
/// What an auto-resume says when nothing else was chosen. One word, because
/// the session already knows what it was doing and this is only the nudge that
/// lets it carry on.
pub const DEFAULT_RESUME_MESSAGE: &str = "continue";
fn notify_default() -> bool {
true
}
/// Keeps the ordinary case out of the file entirely -- see
/// [`SessionConfig::throwaway`].
fn not_set(flag: &bool) -> bool {
!*flag
}
/// The name of the echo provider, and of the setup this machine gets on first
/// run.
///
/// Echo is seeded into the config rather than conjured at read time. An
/// implicit provider is one a person cannot see in the file or edit from the
/// phone; if somebody deletes it, that was a choice.
pub const ECHO_PROVIDER: &str = "echo";
pub const LOCAL_SETUP: &str = "this machine";
/// The id of the setup a fresh install seeds. Fixed rather than random so a
/// hand-written config can name it without looking one up.
pub const LOCAL_SETUP_ID: &str = "local";
/// Where `ai-server --enroll-link` leaves a token for the running server to
/// adopt: beside the config, since it is config in transit.
pub fn pending_enrollments_dir(config_path: &Path) -> PathBuf {
config_path.with_file_name("pending-enrollments")
}
@@ -413,15 +250,10 @@ impl Config {
self.setups.iter().find(|setup| setup.id == id)
}
/// A setup by the label a person sees, for messages and for the one place a
/// name still arrives from outside. Nothing else should look one up this
/// way, since labels are editable and ids are not.
pub fn setup_named(&self, name: &str) -> Option<&SetupConfig> {
self.setups.iter().find(|setup| setup.name == name)
}
/// This machine, offering whatever was found on it.
///
/// The providers are passed in rather than written here because they have to
/// be *discovered*: a hardcoded list is a claim about what is installed, and
/// this one was wrong -- every fresh install asserted a `claude-cli`
@@ -435,9 +267,6 @@ impl Config {
}
}
/// The one provider that needs no discovery, and the floor to fall back to
/// when discovery itself fails. Echo runs in-process, so it exists exactly
/// where this server does and nowhere else.
pub fn echo_provider() -> ProviderConfig {
ProviderConfig {
name: ECHO_PROVIDER.to_string(),
@@ -451,8 +280,6 @@ impl Config {
match std::fs::read_to_string(path) {
Ok(text) => format::parse(&text)
.with_context(|| format!("{} is not valid config RON", path.display())),
// A first run has no config -- the normal starting state; a token is
// generated and saved on that first start.
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
warn_about_a_config_left_behind(path);
Ok(Self::default())
@@ -461,8 +288,6 @@ impl Config {
}
}
/// Writes the config, owner-readable only.
///
/// The token hashes here are verifiers rather than secrets, but the file
/// also names every host this backend can reach and every session it is
/// running. The mode is set on the temporary file *before* the rename, so
@@ -472,15 +297,6 @@ impl Config {
}
}
/// Says so when the only config here is one this server no longer reads.
///
/// The format moved from JSON to RON and the switch is outright -- there is
/// no reader for the old file. Everywhere else that is invisible, but this
/// file holds the enrolled token hashes: starting empty leaves the phone
/// unable to talk to this server, and looks from the phone like the config
/// having been lost rather than renamed. The old file is named and left
/// alone rather than read or deleted, since it is the only record of what
/// was configured.
fn warn_about_a_config_left_behind(path: &Path) {
let old = path.with_extension("json");
if old.is_file() {
@@ -503,9 +319,6 @@ mod tests {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.ron");
// A missing file is the ordinary first-run state, not an error.
// Nothing is conjured to fill it: the seed setup is written by the
// manager, so the file always says what there is.
let first_run = Config::load(&path).expect("load");
assert!(first_run.tokens.is_empty());
assert!(first_run.setups.is_empty());
@@ -569,7 +382,6 @@ mod tests {
let loaded = Config::load(&path).expect("reload");
assert_eq!(loaded.tokens[0].name, "phone");
assert_eq!(loaded.sessions[0].setup, "vm");
// The label and the id are separate, and the session holds the id.
assert_eq!(loaded.setup("vm").expect("setup").name, "the vm");
assert_eq!(loaded.sessions[0].provider, "claude-cli");
assert_eq!(
@@ -582,8 +394,6 @@ mod tests {
.port,
Some(2222),
);
// The same provider name on two machines is the point, not a
// collision: names are unique within a setup and only within one.
assert!(
loaded
.setup(LOCAL_SETUP_ID)
@@ -593,14 +403,6 @@ mod tests {
);
assert!(loaded.setup(LOCAL_SETUP_ID).expect("local").ssh.is_none());
// The house rule both halves of `format` depend on: what is written
// is the *body* of the struct, with no outer parentheses and
// nothing indented for them. Asserted rather than trusted because
// `render` strips what `parse` adds back -- if only one of the two
// ever changed, every file on disk would still load and only look
// wrong. The absent `Some(...)` is the other half of the same
// bargain: implicit_some is what lets a person write `port: 2222`,
// and only `skip_serializing_if` keeps this from writing it back.
let text = std::fs::read_to_string(&path).expect("read back");
assert!(!text.trim_start().starts_with('('), "outer parens: {text}");
assert!(
@@ -614,14 +416,6 @@ mod tests {
}
#[test]
/// The seed is this machine and nothing more: a name, no ssh, and
/// exactly the providers it was handed.
///
/// It used to assert a `claude-cli` provider here, which is what made
/// the bug look correct -- the test agreed with the code that every
/// machine has `claude`, because both were written from the same
/// assumption. What a machine has is discovered, so the only thing
/// this can check is that the seed does not invent anything.
fn the_seed_is_this_machine_and_claims_only_what_it_was_given() {
let seed = Config::seed(vec![Config::echo_provider()]);
assert_eq!(seed.name, LOCAL_SETUP);
@@ -635,7 +429,6 @@ mod tests {
"the seed must not assert a provider nobody looked for",
);
// And it carries through whatever discovery did find.
let discovered = Config::seed(vec![
Config::echo_provider(),
ProviderConfig {