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:
1 parent
a6ece28344
commit
967fc814ab
13 files changed
+3349
No files matched your search
@@ -0,0 +1,93 @@
|
||||
//! The common event model and the `Driver` trait -- the one abstraction
|
||||
//! everything hangs off (see PLAN.md).
|
||||
//!
|
||||
//! A driver translates its child process's JSONL dialect into [`Event`]s
|
||||
//! and accepts the small inbound vocabulary below. The transcript, the SSE
|
||||
//! stream, and the phone UI work purely in this model; nothing downstream
|
||||
//! of a driver may branch on the session kind.
|
||||
|
||||
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).
|
||||
pub type ImageRef = String;
|
||||
|
||||
/// Everything a session can tell the outside world. Every event is
|
||||
/// appended to the session's transcript with a sequence number, then fanned
|
||||
/// out to SSE subscribers; the phone renders purely from this stream, so
|
||||
/// reconnecting is just "events after seq N" -- no separate history path
|
||||
/// to drift from the live one.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Event {
|
||||
/// What the user sent, echoed into the transcript by the manager (not
|
||||
/// by drivers) so every device renders the full conversation from the
|
||||
/// one stream.
|
||||
UserMessage { text: String },
|
||||
/// Streaming assistant text; the phone renders the concatenation as
|
||||
/// markdown.
|
||||
AssistantText { delta: String },
|
||||
ToolStart {
|
||||
id: String,
|
||||
tool: String,
|
||||
input: serde_json::Value,
|
||||
},
|
||||
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).
|
||||
Image {
|
||||
#[serde(rename = "ref")]
|
||||
image: ImageRef,
|
||||
},
|
||||
/// Anything the session needs a human for: AskUserQuestion, and
|
||||
/// permission requests, are the same shape with different options.
|
||||
Question {
|
||||
id: String,
|
||||
prompt: String,
|
||||
options: Vec<String>,
|
||||
},
|
||||
/// The manager's record of a question being answered, so a rendered
|
||||
/// question card resolves on every device, not just the one that
|
||||
/// answered it.
|
||||
Answered { id: String, answer: String },
|
||||
Status { state: SessionStatus },
|
||||
/// Per-turn token counts, where the dialect reports them.
|
||||
UsageDelta { tokens: u64 },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SessionStatus {
|
||||
Idle,
|
||||
Running,
|
||||
AwaitingInput,
|
||||
Compacting,
|
||||
Exited,
|
||||
}
|
||||
|
||||
/// Where a driver reports events. Unbounded because producers are child
|
||||
/// processes a slow phone must never be able to stall; the transcript file
|
||||
/// is the backpressure-free buffer of record.
|
||||
pub type EventSink = mpsc::UnboundedSender<Event>;
|
||||
|
||||
/// The inbound half of a session. Deliberately small; see PLAN.md for the
|
||||
/// per-driver mapping of each method onto its dialect.
|
||||
///
|
||||
/// `send_user_message` during a run is the point of the whole app: both
|
||||
/// real dialects queue it for injection at the next tool boundary rather
|
||||
/// than the end of the turn.
|
||||
pub trait Driver: Send + Sync {
|
||||
fn send_user_message(&self, text: String, images: Vec<ImageRef>);
|
||||
fn answer_question(&self, id: &str, answer: &str);
|
||||
/// Stop mid-run; the session survives.
|
||||
fn interrupt(&self);
|
||||
fn set_model(&self, model: &str);
|
||||
/// pi: native compaction; claude: `/compact`.
|
||||
fn compact(&self);
|
||||
/// Graceful process exit.
|
||||
fn shutdown(&self);
|
||||
}
|
||||
Reference in new issue
Block a user