Cleanup pass: one home for duplicated logic, stale comments out
Nothing behavioral except two status codes; mostly removing places where the same rule was written down more than once and could drift. - server/src/private.rs: the owner-only create/write helpers, which config.rs, certs.rs, and the session dirs each had their own copy of (certs.rs even duplicated the explanatory comment). One module owns the modes now, so the "nothing this server writes is readable by anyone else" property is checkable in one place. - server/src/media.rs: the image media-type/extension table, which the four places that have to agree on it each spelled out separately -- storing an upload, serving it back, building a content block, saving a produced image. The differing *defaults* stay at the call sites with the reasoning, since they genuinely differ by direction. - routes.rs: a missing file was a 400 and an unreadable one a 400 with a hand-rolled log line; they are now 404 and Internal respectively. UnknownSession became NotFound, since it was the only 404-with-message. - main.rs: xdg_dir takes the variable's value instead of reading the environment, which drops the unsafe set_var from its test and lets the test actually assert the relative-path rule. - echo.rs had its own 4-byte hex generator beside session::random_hex. - claude.rs: the two impl Translator blocks were one type's methods. - Stale comments: phase-2 markers on shipped work, a permission-mode list that had drifted from the CLI's, "dev-updater" as the leaf certificate's fallback common name, a half-written sentence in build-apk.sh. - App: the JSONArray walk written out in four fetchers, the four near-identical BackHandlers in AppRoot, and SessionScreen's inline fully-qualified names where the file otherwise imports. - server/wg-test.log was committed by accident; *.log is ignored now, and the gitignore comments describe where state actually lives. - PLAN.md's backend layout gains the new modules and drops hosts.rs for the ssh.rs that was built instead. Verified: 35 server tests, clippy clean, app compiles warning-free, and a scratch server driven over curl -- attachment upload/serve round-trip with both a known and an unknown content type, the new 404s, transcript and session-dir deletion, plus a real claude-cli session answering a prompt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
1 parent
d4a4ee7808
commit
99bcc341c1
18 files changed
+295
-241
No files matched your search
@@ -339,21 +339,14 @@ fn attachment_block(session_dir: &Path, id: &str) -> Result<Value> {
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": media_type_of(id),
|
||||
// Ids carry the extension the upload was stored under, so an
|
||||
// unrecognized one means a name this server didn't write.
|
||||
"media_type": crate::media::media_type_for(id).unwrap_or("image/jpeg"),
|
||||
"data": base64::engine::general_purpose::STANDARD.encode(bytes),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn media_type_of(name: &str) -> &'static str {
|
||||
match name.rsplit('.').next() {
|
||||
Some("png") => "image/png",
|
||||
Some("gif") => "image/gif",
|
||||
Some("webp") => "image/webp",
|
||||
_ => "image/jpeg",
|
||||
}
|
||||
}
|
||||
|
||||
/// What answering a question produced.
|
||||
enum AnswerOutcome {
|
||||
/// Send this control_response line to the CLI.
|
||||
@@ -563,9 +556,7 @@ impl Translator {
|
||||
"response": {"subtype": "success", "request_id": request_id, "response": response},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Translator {
|
||||
/// `user` messages: tool results become ToolEnd, with any image parts
|
||||
/// saved into the session dir and referenced by an Image event (the
|
||||
/// phone fetches them from `/sessions/{id}/files/{ref}`). Replayed and
|
||||
@@ -620,16 +611,17 @@ impl Translator {
|
||||
let data = source.get("data")?.as_str()?;
|
||||
use base64::Engine;
|
||||
let bytes = base64::engine::general_purpose::STANDARD.decode(data).ok()?;
|
||||
let extension = match source.get("media_type").and_then(Value::as_str) {
|
||||
Some("image/jpeg") => "jpg",
|
||||
Some("image/gif") => "gif",
|
||||
Some("image/webp") => "webp",
|
||||
_ => "png",
|
||||
};
|
||||
// Screenshots are the overwhelming case, and they are PNG; an
|
||||
// unrecognized type is more likely a dialect change than a JPEG.
|
||||
let extension = source
|
||||
.get("media_type")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(crate::media::extension_for)
|
||||
.unwrap_or("png");
|
||||
let name = format!("{}.{extension}", super::random_hex());
|
||||
let dir = self.session_dir.join("files");
|
||||
if let Err(err) =
|
||||
crate::config::create_private_dir(&dir)
|
||||
crate::private::create_dir(&dir)
|
||||
.map_err(std::io::Error::other)
|
||||
.and_then(|()| std::fs::write(dir.join(&name), bytes))
|
||||
{
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Attachment id of an uploaded image, as returned by `POST /attachments`
|
||||
/// (arrives in phase 2; the vocabulary is fixed now so the trait doesn't
|
||||
/// change under the first two drivers).
|
||||
/// The name a session's image is stored and served under -- returned by
|
||||
/// `POST /attachments` for an upload, minted by a driver for one a tool
|
||||
/// produced, and fetched back from `/sessions/{id}/files/{ref}`. Both
|
||||
/// directions use the one id so the transcript renders them identically.
|
||||
pub type ImageRef = String;
|
||||
|
||||
/// Everything a session can tell the outside world. Every event is
|
||||
@@ -36,8 +37,8 @@ pub enum Event {
|
||||
},
|
||||
ToolUpdate { id: String, output: String },
|
||||
ToolEnd { id: String, output: String },
|
||||
/// An image the session produced, saved under the session dir and
|
||||
/// referenced by id; the phone fetches it by URL (phase 2).
|
||||
/// An image the session produced or was sent, saved under the session
|
||||
/// dir and referenced by id; the phone fetches it by URL.
|
||||
Image {
|
||||
#[serde(rename = "ref")]
|
||||
image: ImageRef,
|
||||
|
||||
@@ -46,7 +46,7 @@ impl Driver for EchoDriver {
|
||||
let sink = self.sink.clone();
|
||||
|
||||
if let Some(rest) = text.strip_prefix("/question") {
|
||||
let id = format!("q-{}", rand_id());
|
||||
let id = format!("q-{}", super::random_hex());
|
||||
let prompt = if rest.trim().is_empty() {
|
||||
"Echo asks: proceed?".to_string()
|
||||
} else {
|
||||
@@ -71,7 +71,7 @@ impl Driver for EchoDriver {
|
||||
send(Event::Status { state: SessionStatus::Running });
|
||||
|
||||
if let Some(input) = run_tool {
|
||||
let id = format!("t-{}", rand_id());
|
||||
let id = format!("t-{}", super::random_hex());
|
||||
send(Event::ToolStart {
|
||||
id: id.clone(),
|
||||
tool: "echo-tool".to_string(),
|
||||
@@ -132,12 +132,3 @@ impl Driver for EchoDriver {
|
||||
self.emit(Event::Status { state: SessionStatus::Exited });
|
||||
}
|
||||
}
|
||||
|
||||
/// Short random suffix for tool/question ids -- unique within a session is
|
||||
/// all that's needed.
|
||||
fn rand_id() -> String {
|
||||
use rand::Rng;
|
||||
let mut bytes = [0u8; 4];
|
||||
rand::rng().fill_bytes(&mut bytes);
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
@@ -139,15 +139,12 @@ impl LiveSession {
|
||||
/// references it by. Removed with the session directory on delete --
|
||||
/// the same path out as everything else in it.
|
||||
pub fn save_attachment(&self, bytes: &[u8], content_type: &str) -> Result<String> {
|
||||
let extension = match content_type {
|
||||
"image/png" => "png",
|
||||
"image/gif" => "gif",
|
||||
"image/webp" => "webp",
|
||||
_ => "jpg",
|
||||
};
|
||||
// An unrecognized type is almost always a phone photo whose
|
||||
// content type the picker didn't set; jpg is the useful guess.
|
||||
let extension = crate::media::extension_for(content_type).unwrap_or("jpg");
|
||||
let name = format!("{}.{extension}", random_hex());
|
||||
let dir = self.dir().join("attachments");
|
||||
crate::config::create_private_dir(&dir)?;
|
||||
crate::private::create_dir(&dir)?;
|
||||
std::fs::write(dir.join(&name), bytes)
|
||||
.with_context(|| format!("write attachment {name}"))?;
|
||||
Ok(name)
|
||||
@@ -189,7 +186,7 @@ impl SessionManager {
|
||||
/// session spawns its event pump).
|
||||
pub fn new(config_path: PathBuf, data_dir: PathBuf) -> Result<Self> {
|
||||
let config = Config::load(&config_path)?;
|
||||
crate::config::create_private_dir(&data_dir)?;
|
||||
crate::private::create_dir(&data_dir)?;
|
||||
|
||||
let mut live = HashMap::new();
|
||||
for meta in &config.sessions {
|
||||
@@ -451,7 +448,7 @@ fn launch(
|
||||
data_dir: &Path,
|
||||
) -> Result<Arc<LiveSession>> {
|
||||
let dir = data_dir.join(&meta.id);
|
||||
crate::config::create_private_dir(&dir)?;
|
||||
crate::private::create_dir(&dir)?;
|
||||
let transcript_path = dir.join("transcript.jsonl");
|
||||
let transcript = Transcript::open(&transcript_path)?;
|
||||
|
||||
|
||||
Reference in new issue
Block a user