brokerd: state.sock answers memoryd's question about a session's state

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-28 11:05:42 -07:00
co-authored by ornith-1.5-35b-a3b
parent 5199bd7149
commit 9b4e9807b2
12 changed files with 758 additions and 14 deletions
+11 -1
View File
@@ -25,12 +25,14 @@ impl Default for Paths {
}
}
/// An empty path means "the default under `home`".
/// The sockets `brokerd` serves. An empty path means "the default under `home`".
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, default)]
pub struct Sockets {
pub broker: PathBuf,
pub admin: PathBuf,
/// `memoryd`'s session-state questions (M5c spec, section 7).
pub state: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -284,6 +286,14 @@ impl Config {
self.sockets.admin.clone()
}
}
/// `state.sock`, where `memoryd` asks for a session's state.
pub fn state_socket(&self) -> PathBuf {
if self.sockets.state.as_os_str().is_empty() {
self.paths.home.join("run/memory-broker/state.sock")
} else {
self.sockets.state.clone()
}
}
/// `gateway.sock`, when `[gateway]` is set.
pub fn gateway_socket(&self) -> Option<PathBuf> {
let gateway = self.gateway.as_ref()?;
+1
View File
@@ -18,3 +18,4 @@ pub mod policy;
pub mod runner;
pub mod serve;
pub mod state;
pub mod state_socket;
+3 -1
View File
@@ -1,4 +1,4 @@
//! `brokerd`: the broker role. `serve` takes the audit lock, binds both sockets, and serves.
//! `brokerd`: the broker role. `serve` takes the audit lock, binds its three sockets, and serves.
use std::ffi::OsString;
use std::path::PathBuf;
@@ -58,6 +58,7 @@ fn main() -> ExitCode {
// Keep the socket paths for the serving notice, before `cfg` is moved into `start`.
let broker_path = cfg.broker_socket();
let admin_path = cfg.admin_socket();
let state_path = cfg.state_socket();
// The runtime and the line that says which. A `[runner]` runs calls in containers; without it,
// every call is refused, as in M3a.
@@ -117,6 +118,7 @@ fn main() -> ExitCode {
broker_path.display(),
admin_path.display()
);
eprintln!("brokerd: serving session state on {}", state_path.display());
eprintln!("brokerd: {runtime_notice}");
match started.run() {
+16 -6
View File
@@ -1,4 +1,4 @@
//! `brokerd serve`: bring the broker up, listen on both sockets, and keep it running.
//! `brokerd serve`: bring the broker up, listen on its three sockets, and keep it running.
use std::fs::{DirBuilder, Permissions};
use std::io;
@@ -16,6 +16,7 @@ use crate::config::Config;
use crate::ledger::Ledger;
use crate::runner::Runtime;
use crate::state::StateStore;
use crate::state_socket;
use bxcore::{ChainFailure, Timestamp};
/// A failure while `brokerd serve` is starting up.
@@ -48,16 +49,17 @@ impl std::error::Error for ServeError {
}
}
/// The result of `start`: a running broker with both sockets open.
/// The result of `start`: a running broker with its three sockets open.
pub struct Started {
pub broker: Arc<Broker>,
pub recovered: bool,
pub accepted: Option<ChainFailure>,
tools: UnixListener,
admin: UnixListener,
state: UnixListener,
}
/// Start the broker: open the audit chain, bind both sockets, and build the ledger and broker.
/// Start the broker: open the audit chain, bind the three sockets, and build the ledger and broker.
pub fn start(
cfg: Config,
accept_break: bool,
@@ -67,7 +69,9 @@ pub fn start(
// The audit lock comes first: if the chain will not open, no directory or socket is made.
let opened = Writer::open(&cfg.audit_dir(), accept_break).map_err(ServeError::Audit)?;
// Both sockets, in the order the task gives.
// The three sockets. `state.sock` comes first: whoever waits for the tool and admin sockets
// then finds it bound too.
let state = listen(&cfg.state_socket())?;
let tools = listen(&cfg.broker_socket())?;
let admin = listen(&cfg.admin_socket())?;
@@ -93,6 +97,7 @@ pub fn start(
accepted: opened.accepted.map(|b| *b),
tools,
admin,
state,
})
}
@@ -156,6 +161,7 @@ fn listen(socket: &Path) -> Result<UnixListener, ServeError> {
enum Which {
Tools,
Admin,
State,
}
/// Run the broker: start the expiry thread, one accept thread per socket, then wait for the
@@ -166,6 +172,7 @@ impl Started {
broker,
tools,
admin,
state,
..
} = self;
@@ -187,7 +194,8 @@ impl Started {
let (tx, rx) = mpsc::channel::<io::Error>();
accept_loop(tools, Arc::clone(&broker), tx.clone(), Which::Tools)?;
accept_loop(admin, Arc::clone(&broker), tx, Which::Admin)?;
accept_loop(admin, Arc::clone(&broker), tx.clone(), Which::Admin)?;
accept_loop(state, Arc::clone(&broker), tx, Which::State)?;
// The first listener error stops the daemon.
match rx.recv() {
@@ -200,7 +208,7 @@ impl Started {
/// Accept streams from one listener forever, starting a handler thread per stream. When the
/// system is out of file descriptors or memory, the listener pauses and tries again instead of
/// stopping: otherwise a peer holding many idle connections could stop the daemon, and the other
/// socket with it. Any other error stops the daemon through `tx`.
/// sockets with it. Any other error stops the daemon through `tx`.
fn accept_loop(
listener: UnixListener,
broker: Arc<Broker>,
@@ -210,6 +218,7 @@ fn accept_loop(
let name = match which {
Which::Tools => "accept-broker",
Which::Admin => "accept-admin",
Which::State => "accept-state",
};
std::thread::Builder::new()
.name(name.to_string())
@@ -248,6 +257,7 @@ fn serve_one(stream: UnixStream, broker: &Arc<Broker>, which: Which) {
let started = std::thread::Builder::new().spawn(move || match which {
Which::Tools => broker::handle(stream, &for_thread),
Which::Admin => admin::handle(stream, &for_thread),
Which::State => state_socket::handle(stream, &for_thread),
});
if let Err(e) = started {
broker.log(&format!(
+51
View File
@@ -0,0 +1,51 @@
//! One `state.sock` connection: `memoryd` asks for a session's state when a turn ends, and the
//! answer is what the state store holds (M5c spec, section 7). It reads, and never changes
//! anything: no state, no audit record.
use std::os::unix::net::UnixStream;
use bxcore::{ErrorCode, Message, SessionStateReply, WireError};
use crate::broker::{Broker, forbid, read_request, send};
use crate::state::StateStore;
/// The socket's name in the lines `brokerd` logs and the errors it sends.
pub const SOCKET: &str = "state.sock";
/// Serve one `state.sock` connection from `memoryd`: one question, one final answer, then the
/// connection is closed.
pub fn handle(mut stream: UnixStream, broker: &Broker) {
// 1. The peer is gone before it speaks, or its frame is malformed: answered, then return.
let Some(envelope) = read_request(&mut stream) else {
return;
};
let id = envelope.id;
let msg = envelope.msg;
// 2. Only the question belongs here. `bxcore::SessionState` is matched through `Message`, so
// this file names neither it nor `policy::SessionState`, which the store returns.
let Message::SessionState(question) = msg else {
forbid(broker, &mut stream, id, &msg, SOCKET);
return;
};
// 3. The store the ledger writes. A session without a file is the store's default, `private`
// and trusted; a file that cannot be read is an error, never that default.
let store = StateStore::new(&broker.cfg().state_dir());
let response = match store.read(&question.session) {
Ok(state) => Message::SessionStateReply(SessionStateReply {
class: state.taint,
untrusted: state.untrusted,
}),
Err(e) => {
broker.log(&format!("brokerd: {e}"));
Message::Error(WireError {
code: ErrorCode::Internal,
detail: e.to_string(),
})
}
};
// 4. The final frame; a failed send is ignored, as the peer is gone.
let _ = send(&mut stream, id, true, response);
}