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:
1 parent
e3e02d55f7
commit
79682f03a7
24 files changed
+4572
-6821
No files matched your search
+114
-171
@@ -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 {
|
||||
@@ -107,59 +95,49 @@ pub struct SshConfig {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub options: Vec<String>,
|
||||
/// 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),
|
||||
@@ -167,21 +145,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,
|
||||
@@ -193,11 +167,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,
|
||||
}
|
||||
|
||||
@@ -206,73 +178,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,
|
||||
}
|
||||
|
||||
@@ -281,29 +236,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")
|
||||
}
|
||||
@@ -313,22 +264,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(),
|
||||
@@ -338,12 +286,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(),
|
||||
@@ -357,8 +302,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())
|
||||
@@ -369,12 +314,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