Merge branch 'main' of git.arirex.me:iris/ai-app
# Conflicts: # AGENTS.md # PLAN.md # app/androidApp/src/main/kotlin/com/example/aiapp/SessionUsageBar.kt # app/androidApp/src/main/kotlin/com/example/aiapp/SpawnScreen.kt # server/src/config.rs # server/src/main.rs # server/src/routes.rs # server/src/session/echo.rs # server/src/session/llama.rs # server/src/session/transport.rs # server/src/ssh.rs # server/src/usage.rs
This commit is contained in:
commit
3c0214ece8
94 files changed
+8299
-9454
No files matched your search
+127
-192
@@ -1,20 +1,17 @@
|
||||
//! The server's persistent state: the enrolled token hashes and the
|
||||
//! sessions that exist.
|
||||
//! 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` (the registry pattern), so in-memory
|
||||
//! and on-disk state can't come apart.
|
||||
//! 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 format, and the same two house rules, as the sibling dev-updater
|
||||
//! project's config, because both are written and read by hand, and both
|
||||
//! now read and write them through the one module.
|
||||
//! 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 (see `session::transcript`); this file
|
||||
//! holds only the metadata needed to list and respawn sessions.
|
||||
//! 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};
|
||||
@@ -27,24 +24,20 @@ 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.
|
||||
/// 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>,
|
||||
/// Every machine this server can run something on, and what each of
|
||||
/// them can run. See [`SetupConfig`].
|
||||
pub setups: Vec<SetupConfig>,
|
||||
pub sessions: Vec<SessionConfig>,
|
||||
}
|
||||
|
||||
/// A machine, and the things it can run.
|
||||
///
|
||||
/// This is the unit a session is spawned against: pick a setup, then one
|
||||
/// of its providers. Grouping them this way is what stops the spawn
|
||||
/// screen offering combinations that cannot work -- a provider only
|
||||
/// exists on a machine where that program is installed, and the previous
|
||||
/// model, which let any provider be paired with any host, offered the
|
||||
/// whole cross-product including the impossible parts of it.
|
||||
/// 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 {
|
||||
@@ -53,15 +46,12 @@ pub struct SetupConfig {
|
||||
/// renaming a machine on the phone does not orphan its sessions --
|
||||
/// which is the whole reason the two are separate fields.
|
||||
pub id: String,
|
||||
/// The label a person reads and may edit.
|
||||
pub name: String,
|
||||
/// How to reach it, absent for this machine. A setup with no `ssh` is
|
||||
/// where the server itself runs.
|
||||
/// 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.
|
||||
/// within it: two machines may each have a `claude-cli`, which is the point.
|
||||
#[serde(default)]
|
||||
pub providers: Vec<ProviderConfig>,
|
||||
}
|
||||
@@ -88,12 +78,10 @@ pub struct ProviderConfig {
|
||||
pub models: Vec<String>,
|
||||
}
|
||||
|
||||
/// 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 (PLAN.md, rule 23).
|
||||
///
|
||||
/// A remote session is the identical command with `ssh host …` in front,
|
||||
/// and nothing downstream of the spawn knows the difference.
|
||||
/// 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 {
|
||||
@@ -120,59 +108,49 @@ pub struct SshConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub models_dir: Option<PathBuf>,
|
||||
/// Where a file attached from the phone is put on this machine so the
|
||||
/// session can read it. Absent means the session's own working
|
||||
/// directory, or the login home for a session that has none. A `~`
|
||||
/// prefix is the remote home.
|
||||
/// session can read it. Absent means the session's own working directory,
|
||||
/// or the login home for a session that has none. A `~` prefix is the
|
||||
/// remote home.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub attachments_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Which translator runs a session. A new one is a new driver behind the
|
||||
/// same trait -- never a branch in shared code.
|
||||
/// 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 to say a name
|
||||
/// nobody would type that way. The same string is what the phone compares
|
||||
/// against (`SpawnScreen.kt`), so the two move together.
|
||||
/// 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 phase-1 fake: echoes messages back as streamed events. Proves
|
||||
/// the pipe (spawn, SSE, transcript cursors, questions) with no AI
|
||||
/// involved, and stays useful as a connectivity check that costs no
|
||||
/// tokens. Always available as a built-in provider.
|
||||
/// 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` (see
|
||||
/// `session::llama`). The model itself is one this machine has
|
||||
/// downloaded; the provider's command is the server binary.
|
||||
/// 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 (see `session::claude`).
|
||||
/// Named for the CLI specifically: bare "claude" would suggest the
|
||||
/// credit-billed API, which this is not.
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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.
|
||||
/// 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 twelve megapixels, and every one of
|
||||
/// those bytes 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 -- a phone that knew
|
||||
/// each provider's limits would be a second place to update when one
|
||||
/// changes.
|
||||
/// 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 image is broken" turned out to be. The others take images
|
||||
/// through no path that cares, so they get no limit rather than a made-up
|
||||
/// one.
|
||||
/// 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
|
||||
/// image is broken" turned out to be.
|
||||
pub fn max_image_edge(self) -> Option<u32> {
|
||||
match self {
|
||||
DriverKind::ClaudeCli => Some(1568),
|
||||
@@ -180,30 +158,22 @@ impl DriverKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which paid service meters a session of this kind, and `None` for
|
||||
/// one that costs nothing.
|
||||
/// Which paid service meters a session of this kind, and `None` for one
|
||||
/// that costs nothing.
|
||||
///
|
||||
/// The rate-limit bars answer a question about an *account*, and what
|
||||
/// decides which account -- if any -- is the provider a session runs,
|
||||
/// not the machine it runs on. Those were the same thing only for as
|
||||
/// long as a machine ran one kind of session: an echo session on a
|
||||
/// laptop that also has the Claude CLI was drawn with that CLI's
|
||||
/// five-hour window under its header, reporting a quota it cannot
|
||||
/// spend and could not run down. A llama.cpp session is the same
|
||||
/// story with the model on the far side.
|
||||
/// 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.
|
||||
///
|
||||
/// [`DriverKind::Echo`] names a meter of its own, which exists only
|
||||
/// when a test has asked for one (`/usage` in `session::echo`). That
|
||||
/// is what makes the bar's states -- a number, a machine nobody
|
||||
/// logged into, one that could not be reached -- reachable without an
|
||||
/// account and without spending a turn on somebody else's. With no
|
||||
/// fixture set there is no snapshot for it, which the phone draws as
|
||||
/// nothing at all.
|
||||
/// 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 the snapshots `GET /usage`
|
||||
/// returns; the two lists have to agree, so `usage::providers_for`
|
||||
/// reads this rather than matching on kinds a second time.
|
||||
/// 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),
|
||||
@@ -212,21 +182,17 @@ impl DriverKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the conversation exists outside this app, so that deleting
|
||||
/// the session here does not end it.
|
||||
/// 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 under
|
||||
/// `~/.claude/projects/` and is resumable from it whatever started
|
||||
/// it -- so a session this app spawned is every bit as recoverable as
|
||||
/// one it imported, and the difference between those two is only how
|
||||
/// it got here. Echo has nothing to keep, and a llama session's
|
||||
/// conversation is folded out of *this* app's transcript, so for both
|
||||
/// of those a delete is the end of 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 on the sessions where it is real.
|
||||
/// 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,
|
||||
@@ -238,11 +204,9 @@ impl DriverKind {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenEntry {
|
||||
/// Which device this token belongs to, for the human rotating it.
|
||||
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
|
||||
/// and no stretching needed.
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -251,73 +215,56 @@ pub struct TokenEntry {
|
||||
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.
|
||||
/// 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 (a new command path, another
|
||||
/// model) takes effect on the next relaunch; a session whose setup or
|
||||
/// provider is gone reports as exited and can still be deleted.
|
||||
/// 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")]
|
||||
pub model: Option<String>,
|
||||
/// Working directory the session's process runs in.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Claude permission mode chosen at spawn. Meaningless for other
|
||||
/// kinds, and kept as a string because it is passed straight to the
|
||||
/// CLI's `--permission-mode` rather than interpreted here -- so the
|
||||
/// CLI stays the one authority on which modes exist, and a new one
|
||||
/// needs no change on this side.
|
||||
/// Claude permission mode chosen at spawn. Kept as a string because it is
|
||||
/// passed straight to `--permission-mode` rather than interpreted here, so
|
||||
/// the CLI stays the one authority on which modes exist.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub permission_mode: Option<String>,
|
||||
/// Settings the driver interprets, chosen at spawn.
|
||||
///
|
||||
/// Deliberately untyped here: what a temperature or a context size
|
||||
/// means is the driver's business, and giving this schema 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. A map rather than a list so the phone can send exactly
|
||||
/// what a person changed, and BTreeMap so the file's order is stable
|
||||
/// across writes.
|
||||
/// 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, and answering that question again on each new phone
|
||||
/// is how two devices come to disagree about which sessions matter.
|
||||
/// session: one that runs unattended overnight should be quiet on every
|
||||
/// device.
|
||||
///
|
||||
/// Defaults to on, and on for a config written before this field
|
||||
/// existed. The alternative -- silent unless asked -- makes the
|
||||
/// feature invisible to anyone who does not go looking for it, and a
|
||||
/// notification nobody wanted is turned off in one tap where one that
|
||||
/// never arrived is not diagnosable at all.
|
||||
/// Defaults to on. Silent-unless-asked makes the feature invisible to
|
||||
/// anyone who does not go looking, and a notification nobody wanted is
|
||||
/// turned off in one tap where one that never arrived is not diagnosable.
|
||||
#[serde(default = "notify_default")]
|
||||
pub notify: bool,
|
||||
/// Whether this session's process is stopped when the server exits,
|
||||
/// instead of being left running for the next start to adopt.
|
||||
/// 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, and a session nobody
|
||||
/// meant to keep should not depend on the same server still being up
|
||||
/// to clean it away.
|
||||
/// 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`, which is
|
||||
/// the default in a debug build. A session spawned while testing is
|
||||
/// one nobody means to keep, and under the ordinary rule its `claude`
|
||||
/// outlives every server that ever knew about it -- twelve of them
|
||||
/// accumulated on this machine in a day, each holding a conversation
|
||||
/// open.
|
||||
///
|
||||
/// Absent means false: every session written before this existed, and
|
||||
/// every one spawned by a release build.
|
||||
/// 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,
|
||||
/// Epoch seconds when the session was spawned.
|
||||
pub created: f64,
|
||||
}
|
||||
|
||||
@@ -326,29 +273,25 @@ fn notify_default() -> bool {
|
||||
}
|
||||
|
||||
/// Keeps the ordinary case out of the file entirely -- see
|
||||
/// [`SessionConfig::throwaway`], which is false for every session a
|
||||
/// production build writes.
|
||||
/// [`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.
|
||||
/// 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 the
|
||||
/// way it used to be. An implicit provider is one a person cannot see in
|
||||
/// the file or edit from the phone, and the point of this app is that
|
||||
/// configuration is visible and editable; if somebody deletes it, that was
|
||||
/// a choice.
|
||||
/// 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.
|
||||
/// 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. See
|
||||
/// `wg_app_link::enroll::spool_pending`.
|
||||
/// 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")
|
||||
}
|
||||
@@ -358,22 +301,19 @@ 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.
|
||||
/// 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` provider whether or not `claude` existed, which on a
|
||||
/// machine without it is a spawn option that cannot work and a
|
||||
/// statement the server never checked. Providers are discovered by
|
||||
/// asking the machine, here exactly as for any other setup.
|
||||
/// 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`
|
||||
/// provider whether or not `claude` existed.
|
||||
pub fn seed(providers: Vec<ProviderConfig>) -> SetupConfig {
|
||||
SetupConfig {
|
||||
id: LOCAL_SETUP_ID.to_string(),
|
||||
@@ -383,12 +323,9 @@ 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 -- there is nothing to probe for, and offering it
|
||||
/// on a remote machine would be a choice that changes nothing.
|
||||
/// 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(),
|
||||
@@ -402,8 +339,8 @@ 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.
|
||||
// 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())
|
||||
@@ -414,12 +351,10 @@ impl Config {
|
||||
|
||||
/// Writes the config, owner-readable only.
|
||||
///
|
||||
/// The token hashes here are verifiers, not secrets -- a 256-bit
|
||||
/// random token can't be recovered from its SHA-256 -- but the file
|
||||
/// also names every host this backend can reach and every session it
|
||||
/// is running, which is nobody else's business on a shared machine.
|
||||
/// The mode is set on the temporary file *before* the rename, so the
|
||||
/// config is never briefly world-readable at its real path.
|
||||
/// 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
|
||||
/// the config is never briefly world-readable at its real path.
|
||||
pub fn save(&self, path: &Path) -> Result<()> {
|
||||
format::write(path, self)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user