Phase 1 server: TLS + token auth, session registry, EchoDriver, SSE with cursors

The whole pipe behind one Driver trait and a common event model:
spawn/list/delete sessions, message + question answering, append-only
JSONL transcripts whose sequence numbers are the phone's resume cursor
(surviving backend restarts), bearer-token middleware wrapping every
route including the fallback, wg0-only binding that fails closed, and
first-run token enrollment via a terminal QR.

Verified: cargo test (10), clippy clean, and curl end-to-end over pinned
TLS -- auth rejection, spawn, streamed SSE replay/resume, /question
round trip, restart continuing seq numbers, delete removing everything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Fable 5 committed 2026-08-24 20:51:34 -04:00
1 parent a6ece28344
commit 967fc814ab
13 files changed
+3349

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
//! The phase-1 fake driver: no child process, just events. It exists to
//! prove the whole pipe -- spawn, transcript, SSE cursors, questions,
//! interrupts -- before any AI is involved, and stays useful afterwards as
//! a connectivity check that costs no tokens.
//!
//! Behavior: every message is echoed back as a few streamed text deltas. A
//! message starting with `/tool` also emits a fake tool run, and one
//! starting with `/question` asks one (exercising the answer path). This is
//! exactly the event vocabulary the real drivers produce, so a UI that
//! renders echo sessions correctly renders the real thing.
use std::sync::Mutex;
use std::time::Duration;
use super::driver::{Driver, Event, EventSink, ImageRef, SessionStatus};
/// Delay between streamed deltas -- long enough that streaming is visibly
/// streaming in the UI, short enough that tests waiting on a full turn
/// stay fast.
const DELTA_DELAY: Duration = Duration::from_millis(50);
pub struct EchoDriver {
sink: EventSink,
/// Id of the question currently awaiting an answer, if any. One at a
/// time is all the echo behavior ever produces.
pending_question: Mutex<Option<String>>,
}
impl EchoDriver {
pub fn new(sink: EventSink) -> Self {
let driver = Self { sink, pending_question: Mutex::new(None) };
driver.emit(Event::Status { state: SessionStatus::Idle });
driver
}
/// Sends are infallible from the driver's point of view: a closed sink
/// means the session is being torn down, and there is nobody left to
/// report to.
fn emit(&self, event: Event) {
let _ = self.sink.send(event);
}
}
impl Driver for EchoDriver {
fn send_user_message(&self, text: String, _images: Vec<ImageRef>) {
let sink = self.sink.clone();
if let Some(rest) = text.strip_prefix("/question") {
let id = format!("q-{}", rand_id());
let prompt = if rest.trim().is_empty() {
"Echo asks: proceed?".to_string()
} else {
format!("Echo asks: {}", rest.trim())
};
*self.pending_question.lock().unwrap() = Some(id.clone());
self.emit(Event::Status { state: SessionStatus::Running });
self.emit(Event::Question {
id,
prompt,
options: vec!["Yes".to_string(), "No".to_string()],
});
self.emit(Event::Status { state: SessionStatus::AwaitingInput });
return;
}
let run_tool = text.strip_prefix("/tool").map(|rest| rest.trim().to_string());
tokio::spawn(async move {
let send = |event: Event| {
let _ = sink.send(event);
};
send(Event::Status { state: SessionStatus::Running });
if let Some(input) = run_tool {
let id = format!("t-{}", rand_id());
send(Event::ToolStart {
id: id.clone(),
tool: "echo-tool".to_string(),
input: serde_json::json!({ "input": input }),
});
tokio::time::sleep(DELTA_DELAY).await;
send(Event::ToolUpdate { id: id.clone(), output: "working...".to_string() });
tokio::time::sleep(DELTA_DELAY).await;
send(Event::ToolEnd { id, output: format!("echoed: {input}") });
}
// Word-at-a-time so streaming is visibly streaming.
for word in format!("You said: {text}").split_inclusive(' ') {
send(Event::AssistantText { delta: word.to_string() });
tokio::time::sleep(DELTA_DELAY).await;
}
send(Event::UsageDelta { tokens: text.split_whitespace().count() as u64 });
send(Event::Status { state: SessionStatus::Idle });
});
}
fn answer_question(&self, id: &str, answer: &str) {
let mut pending = self.pending_question.lock().unwrap();
match pending.as_deref() {
Some(expected) if expected == id => {
*pending = None;
self.emit(Event::AssistantText {
delta: format!("You answered: {answer}"),
});
self.emit(Event::Status { state: SessionStatus::Idle });
}
_ => self.emit(Event::Error {
message: format!("no question {id} is awaiting an answer"),
}),
}
}
fn interrupt(&self) {
// Nothing real to stop; a pending question is abandoned so the
// session isn't stuck awaiting input forever.
*self.pending_question.lock().unwrap() = None;
self.emit(Event::Status { state: SessionStatus::Idle });
}
fn set_model(&self, model: &str) {
self.emit(Event::Error {
message: format!("echo sessions have no model to change to {model}"),
});
}
fn compact(&self) {
self.emit(Event::Error {
message: "echo sessions have nothing to compact".to_string(),
});
}
fn shutdown(&self) {
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()
}