core_ipc/
server.rs

1//! IPC bus server — manages subscriber registrations, event routing,
2//! and security level filtering over Unix domain sockets.
3//!
4//! The bus server lives inside `daemon-profile`. It binds a `UnixListener`,
5//! accepts client connections with `UCred` authentication, and routes
6//! postcard-framed messages between connected daemons.
7//!
8//! Every socket connection performs a Noise IK handshake before any application
9//! data flows. The `bind()` method requires a mandatory keypair.
10
11use core_types::{DaemonId, EventKind, SecurityLevel};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::time::Instant;
17use tokio::net::{UnixListener, UnixStream};
18use tokio::sync::{RwLock, mpsc};
19use uuid::Uuid;
20
21use crate::framing::{decode_frame, encode_frame};
22use crate::message::Message;
23use crate::registry::ClearanceRegistry;
24use crate::transport::{extract_ucred, local_credentials};
25
26/// Subscription filter for event routing.
27#[derive(Debug, Clone)]
28pub struct SubscriptionFilter {
29    /// Event kind prefix match (e.g. "Secret" matches all secret events).
30    pub kind_prefix: Option<String>,
31    /// Minimum security level required to receive events.
32    pub min_level: SecurityLevel,
33}
34
35/// Per-connection state tracked by the bus server.
36#[allow(dead_code)] // subscriptions used in future subscription-based routing
37struct ConnectionState {
38    daemon_id: Option<DaemonId>,
39    /// Registry-verified daemon name from Noise IK handshake.
40    /// `None` for unregistered clients (CLI, Open clearance).
41    verified_name: Option<String>,
42    tx: mpsc::Sender<Vec<u8>>,
43    peer: crate::transport::PeerCredentials,
44    security_clearance: SecurityLevel,
45    subscriptions: Vec<SubscriptionFilter>,
46    /// Trust assessment computed at connection time from handshake evidence.
47    trust_vector: Option<core_types::TrustVector>,
48}
49
50/// Shared state for the bus server, accessible from per-connection tasks.
51struct ServerState {
52    connections: RwLock<HashMap<u64, ConnectionState>>,
53    /// Maps request `msg_id` -> originating `connection_id` for response routing.
54    pending_requests: RwLock<HashMap<Uuid, u64>>,
55    next_conn_id: AtomicU64,
56    epoch: Instant,
57    /// Maps public keys to daemon identities and security clearance levels.
58    /// `RwLock` allows key rotation and revocation at runtime.
59    registry: RwLock<ClearanceRegistry>,
60    /// Confirmed RPC routing: maps `correlation_id` -> channel for host-side waiters.
61    /// When a correlated response matches a registered confirmation, the raw frame is
62    /// sent to the confirmation channel instead of the normal host delivery path.
63    confirmations: RwLock<HashMap<Uuid, mpsc::Sender<Vec<u8>>>>,
64    /// Maps verified daemon name -> connection ID for O(1) unicast.
65    /// Populated when `route_frame()` processes `DaemonStarted` with a `verified_sender_name`.
66    /// Invalidated on connection disconnect.
67    name_to_conn: RwLock<HashMap<String, u64>>,
68}
69
70/// RAII guard that deregisters a confirmation route on drop.
71///
72/// Returned by [`BusServer::register_confirmation()`]. When dropped (including
73/// on panic/early return), removes the confirmation entry from the routing table.
74/// This prevents stale entries from accumulating if the caller times out or errors.
75pub struct ConfirmationGuard {
76    correlation_id: Uuid,
77    state: Arc<ServerState>,
78}
79
80impl Drop for ConfirmationGuard {
81    fn drop(&mut self) {
82        // Use try_write to avoid blocking in Drop. If the lock is held,
83        // the entry will be cleaned up by the next write operation.
84        if let Ok(mut confirmations) = self.state.confirmations.try_write() {
85            confirmations.remove(&self.correlation_id);
86        } else {
87            // Spawn a task to clean up asynchronously if we can't get the lock.
88            let id = self.correlation_id;
89            let state = Arc::clone(&self.state);
90            tokio::spawn(async move {
91                state.confirmations.write().await.remove(&id);
92            });
93        }
94    }
95}
96
97/// The IPC bus server.
98pub struct BusServer {
99    listener: Option<UnixListener>,
100    socket_path: Option<PathBuf>,
101    state: Arc<ServerState>,
102    /// Noise IK static keypair for the bus server.
103    /// Always `Some` from `bind()`. `None` only from `new()` (channel-wired mode).
104    keypair: Option<Arc<snow::Keypair>>,
105}
106
107impl BusServer {
108    /// Create a new bus server without a listener (for in-process channel wiring).
109    ///
110    /// Used only for `register()`/`unregister()` based in-process testing where
111    /// clients are pre-wired via channels rather than socket connections.
112    /// Does NOT support socket accept — use `bind()` for socket-based servers.
113    #[must_use]
114    pub fn new() -> Self {
115        Self {
116            listener: None,
117            socket_path: None,
118            state: Arc::new(ServerState {
119                connections: RwLock::new(HashMap::new()),
120                pending_requests: RwLock::new(HashMap::new()),
121                next_conn_id: AtomicU64::new(1),
122                epoch: Instant::now(),
123                registry: RwLock::new(ClearanceRegistry::new()),
124                confirmations: RwLock::new(HashMap::new()),
125                name_to_conn: RwLock::new(HashMap::new()),
126            }),
127            keypair: None,
128        }
129    }
130
131    /// Bind a Unix domain socket listener at the given path.
132    ///
133    /// The `keypair` is the server's Noise IK static keypair (mandatory),
134    /// used to perform encrypted handshakes with connecting clients. Generated
135    /// via [`crate::generate_keypair()`] and published via
136    /// [`crate::noise::write_bus_keypair()`].
137    ///
138    /// Creates the parent directory if it does not exist. Removes any stale
139    /// socket file at the path before binding.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if directory creation or socket binding fails.
144    pub fn bind(
145        path: &Path,
146        keypair: snow::Keypair,
147        registry: ClearanceRegistry,
148    ) -> core_types::Result<Self> {
149        if let Some(parent) = path.parent() {
150            std::fs::create_dir_all(parent).map_err(|e| {
151                core_types::Error::Ipc(format!(
152                    "failed to create socket directory {}: {e}",
153                    parent.display()
154                ))
155            })?;
156        }
157
158        // Remove stale socket if it exists.
159        if path.exists() {
160            std::fs::remove_file(path).map_err(|e| {
161                core_types::Error::Ipc(format!(
162                    "failed to remove stale socket {}: {e}",
163                    path.display()
164                ))
165            })?;
166        }
167
168        let listener = UnixListener::bind(path).map_err(|e| {
169            core_types::Error::Ipc(format!("failed to bind socket {}: {e}", path.display()))
170        })?;
171
172        // Defense-in-depth: restrict socket and parent directory permissions to owner-only.
173        // UCred UID validation is the real security boundary, but this hardens against
174        // misconfigured XDG_RUNTIME_DIR permissions.
175        #[cfg(unix)]
176        {
177            use std::os::unix::fs::PermissionsExt;
178            if let Some(parent) = path.parent() {
179                std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).map_err(
180                    |e| {
181                        core_types::Error::Ipc(format!(
182                            "failed to set directory permissions on {}: {e}",
183                            parent.display()
184                        ))
185                    },
186                )?;
187            }
188            std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).map_err(
189                |e| {
190                    core_types::Error::Ipc(format!(
191                        "failed to set socket permissions on {}: {e}",
192                        path.display()
193                    ))
194                },
195            )?;
196        }
197
198        tracing::info!(path = %path.display(), "IPC bus server bound");
199
200        Ok(Self {
201            listener: Some(listener),
202            socket_path: Some(path.to_owned()),
203            state: Arc::new(ServerState {
204                connections: RwLock::new(HashMap::new()),
205                pending_requests: RwLock::new(HashMap::new()),
206                next_conn_id: AtomicU64::new(1),
207                epoch: Instant::now(),
208                registry: RwLock::new(registry),
209                confirmations: RwLock::new(HashMap::new()),
210                name_to_conn: RwLock::new(HashMap::new()),
211            }),
212            keypair: Some(Arc::new(keypair)),
213        })
214    }
215
216    /// Run the accept loop. This future never completes unless the listener
217    /// encounters a fatal error. Cancel it via `tokio::select!` with a
218    /// shutdown signal.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if no listener was bound (created via `new()` instead
223    /// of `bind()`).
224    ///
225    /// # Panics
226    ///
227    /// Panics if called on a `BusServer` created via `bind()` whose keypair
228    /// was somehow removed (should be unreachable).
229    pub async fn run(&self) -> core_types::Result<()> {
230        let listener = self.listener.as_ref().ok_or_else(|| {
231            core_types::Error::Ipc("BusServer::run() called without bind()".into())
232        })?;
233
234        loop {
235            match listener.accept().await {
236                Ok((stream, _addr)) => {
237                    let peer = match extract_ucred(&stream) {
238                        Ok(creds) => creds,
239                        Err(e) => {
240                            tracing::error!(error = %e, "rejecting connection: UCred extraction failed");
241                            continue;
242                        }
243                    };
244
245                    // Enforce same-UID policy: only the owning user may connect.
246                    let my_uid = local_credentials().uid;
247                    if peer.uid != my_uid {
248                        tracing::error!(
249                            peer_uid = peer.uid,
250                            my_uid,
251                            "rejecting connection: UID mismatch"
252                        );
253                        continue;
254                    }
255
256                    let conn_id = self.state.next_conn_id.fetch_add(1, Ordering::Relaxed);
257
258                    tracing::info!(conn_id, pid = peer.pid, uid = peer.uid, "client connected");
259
260                    // Per-connection outbound channel (server -> client).
261                    let (tx, rx) = mpsc::channel::<Vec<u8>>(256);
262
263                    let state = Arc::clone(&self.state);
264                    let keypair = self
265                        .keypair
266                        .clone()
267                        .expect("BusServer::run() requires bind() — keypair is always set");
268                    tokio::spawn(async move {
269                        handle_connection(state, conn_id, stream, tx, rx, peer, keypair).await;
270                    });
271                }
272                Err(e) => {
273                    tracing::error!(error = %e, "accept failed");
274                    // Transient errors: continue. Fatal errors would typically
275                    // be caught by the OS (e.g. too many open files).
276                    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
277                }
278            }
279        }
280    }
281
282    /// Register a subscriber with pre-wired channels (for in-process testing).
283    pub async fn register(
284        &self,
285        daemon_id: DaemonId,
286        peer: crate::transport::PeerCredentials,
287        security_clearance: SecurityLevel,
288        subscriptions: Vec<SubscriptionFilter>,
289        tx: mpsc::Sender<Vec<u8>>,
290    ) {
291        let conn_id = self.state.next_conn_id.fetch_add(1, Ordering::Relaxed);
292        let state = ConnectionState {
293            daemon_id: Some(daemon_id),
294            verified_name: None, // In-process subscribers have no Noise handshake.
295            tx,
296            security_clearance,
297            subscriptions,
298            peer,
299            trust_vector: None,
300        };
301        self.state.connections.write().await.insert(conn_id, state);
302        tracing::info!(%daemon_id, conn_id, "subscriber registered (in-process)");
303    }
304
305    /// Remove a subscriber by daemon ID.
306    pub async fn unregister(&self, daemon_id: &DaemonId) {
307        let mut conns = self.state.connections.write().await;
308        conns.retain(|_, state| state.daemon_id.as_ref() != Some(daemon_id));
309        tracing::info!(%daemon_id, "subscriber unregistered");
310    }
311
312    /// Publish an already-encoded frame to all matching subscribers.
313    pub async fn publish(&self, frame: &[u8], security_level: SecurityLevel) {
314        let conns = self.state.connections.read().await;
315        // Decode sender from the frame to skip echoing back to the publisher.
316        // This mirrors route_frame()'s `cid == sender_conn_id` skip for socket clients.
317        let sender_id: Option<DaemonId> = decode_frame::<Message<EventKind>>(frame)
318            .ok()
319            .map(|msg| msg.sender);
320        for (id, state) in conns.iter() {
321            // Skip the publisher's own connection to prevent feedback loops.
322            if let Some(sid) = sender_id
323                && state.daemon_id == Some(sid)
324            {
325                continue;
326            }
327            if state.security_clearance >= security_level
328                && state.tx.try_send(frame.to_vec()).is_err()
329            {
330                tracing::warn!(conn_id = id, "subscriber channel full, frame dropped");
331            }
332        }
333    }
334
335    /// Return the server's monotonic epoch for timestamp generation.
336    #[must_use]
337    pub fn epoch(&self) -> Instant {
338        self.state.epoch
339    }
340
341    /// Return the socket path if bound.
342    #[must_use]
343    pub fn socket_path(&self) -> Option<&Path> {
344        self.socket_path.as_deref()
345    }
346
347    /// Return the number of active connections.
348    pub async fn connection_count(&self) -> usize {
349        self.state.connections.read().await.len()
350    }
351
352    /// Send a frame to a specific connection by ID (unicast).
353    ///
354    /// Returns `true` if the frame was enqueued, `false` if the connection
355    /// was not found or its channel was full.
356    pub async fn send_to(&self, conn_id: u64, frame: &[u8]) -> bool {
357        let conns = self.state.connections.read().await;
358        if let Some(conn) = conns.get(&conn_id) {
359            conn.tx.try_send(frame.to_vec()).is_ok()
360        } else {
361            false
362        }
363    }
364
365    /// Access the clearance registry for mutation (key rotation, revocation).
366    pub async fn registry_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, ClearanceRegistry> {
367        self.state.registry.write().await
368    }
369
370    /// Look up and remove the originating connection for a correlated response.
371    ///
372    /// When a request arrives via `route_frame()`, the server records
373    /// `(msg_id -> sender_conn_id)` in `pending_requests`. This method
374    /// retrieves and removes that mapping so the response can be unicast
375    /// back to the original requester instead of broadcast.
376    pub async fn take_pending_request(&self, correlation_id: &uuid::Uuid) -> Option<u64> {
377        self.state
378            .pending_requests
379            .write()
380            .await
381            .remove(correlation_id)
382    }
383
384    /// Register a confirmation route for confirmed RPC.
385    ///
386    /// When a response with matching `correlation_id` arrives at `route_frame()`,
387    /// the raw frame is sent to `confirm_tx` instead of the normal host delivery path.
388    /// Returns a [`ConfirmationGuard`] that deregisters the route on drop (RAII cleanup).
389    pub async fn register_confirmation(
390        &self,
391        correlation_id: Uuid,
392        confirm_tx: mpsc::Sender<Vec<u8>>,
393    ) -> ConfirmationGuard {
394        self.state
395            .confirmations
396            .write()
397            .await
398            .insert(correlation_id, confirm_tx);
399        ConfirmationGuard {
400            correlation_id,
401            state: Arc::clone(&self.state),
402        }
403    }
404
405    /// Send a frame to a named daemon via O(1) lookup.
406    ///
407    /// Resolves `daemon_name` to a connection ID via `name_to_conn`, then delegates
408    /// to `send_to()`. Returns `Ok(())` on success, `Err` if the daemon is not
409    /// connected or the channel is full.
410    ///
411    /// # Errors
412    ///
413    /// Returns an error string if the daemon is not in `name_to_conn` (not connected)
414    /// or if the connection's outbound channel is full/closed.
415    pub async fn send_to_named(&self, daemon_name: &str, frame: &[u8]) -> Result<(), String> {
416        let conn_id = {
417            let map = self.state.name_to_conn.read().await;
418            map.get(daemon_name).copied()
419        };
420        match conn_id {
421            Some(id) => {
422                if self.send_to(id, frame).await {
423                    Ok(())
424                } else {
425                    Err(format!(
426                        "send_to_named({daemon_name}): connection gone or channel full"
427                    ))
428                }
429            }
430            None => Err(format!(
431                "send_to_named({daemon_name}): daemon not connected"
432            )),
433        }
434    }
435}
436
437impl Default for BusServer {
438    fn default() -> Self {
439        Self::new()
440    }
441}
442
443impl Drop for BusServer {
444    fn drop(&mut self) {
445        // Clean up the socket file.
446        if let Some(path) = &self.socket_path {
447            let _ = std::fs::remove_file(path);
448        }
449    }
450}
451
452/// Hex-encode a 32-byte key for logging. No `hex` crate dependency needed.
453fn hex_encode(bytes: &[u8; 32]) -> String {
454    use std::fmt::Write;
455    bytes.iter().fold(String::with_capacity(64), |mut s, b| {
456        let _ = write!(s, "{b:02x}");
457        s
458    })
459}
460
461/// Handle a single client connection: perform Noise handshake, then read/write encrypted frames.
462///
463/// The connection is registered in `state.connections` only AFTER the handshake
464/// succeeds. This prevents the race where broadcast frames arrive on the
465/// outbound channel before the writer task (which needs the `NoiseTransport`)
466/// is spawned.
467#[allow(clippy::too_many_lines)]
468async fn handle_connection(
469    state: Arc<ServerState>,
470    conn_id: u64,
471    stream: UnixStream,
472    tx: mpsc::Sender<Vec<u8>>,
473    mut outbound_rx: mpsc::Receiver<Vec<u8>>,
474    peer_creds: crate::transport::PeerCredentials,
475    keypair: Arc<snow::Keypair>,
476) {
477    let (reader, writer) = stream.into_split();
478    let mut reader = tokio::io::BufReader::new(reader);
479    let mut writer = tokio::io::BufWriter::new(writer);
480
481    // Perform Noise IK handshake — mandatory for all socket connections.
482    let local_creds = local_credentials();
483    let connected_at = Instant::now();
484
485    let mut transport = match crate::noise::server_handshake(
486        &mut reader,
487        &mut writer,
488        &keypair,
489        &local_creds,
490        &peer_creds,
491    )
492    .await
493    {
494        Ok(t) => t,
495        Err(e) => {
496            // Structured handshake failure audit.
497            tracing::error!(
498                audit = "connection-lifecycle",
499                event_type = "handshake-failed",
500                conn_id,
501                peer_pid = peer_creds.pid,
502                peer_uid = peer_creds.uid,
503                error = %e,
504                "Noise handshake failed, dropping connection"
505            );
506            return;
507        }
508    };
509
510    // Extract client's X25519 static public key from completed Noise IK handshake.
511    let client_pubkey: [u8; 32] = transport
512        .remote_static()
513        .expect("Noise IK pattern guarantees remote static key after handshake")
514        .try_into()
515        .expect("Curve25519 public key is exactly 32 bytes");
516
517    // Registry lookup: cryptographic identity -> (name, clearance).
518    let (security_clearance, verified_name) = {
519        let reg = state.registry.read().await;
520        if let Some(identity) = reg.lookup(&client_pubkey)
521            && let Some(name) = reg.lookup_name(&client_pubkey).map(str::to_owned)
522        {
523            tracing::info!(
524                audit = "connection-lifecycle",
525                event_type = "handshake-success",
526                conn_id,
527                daemon = %name,
528                clearance = ?identity.security_level,
529                pubkey = %hex_encode(&client_pubkey),
530                peer_pid = peer_creds.pid,
531                peer_uid = peer_creds.uid,
532                "daemon authenticated via registry"
533            );
534            (identity.security_level, Some(name))
535        } else {
536            tracing::info!(
537                audit = "connection-lifecycle",
538                event_type = "ephemeral-client-accepted",
539                conn_id,
540                clearance = ?core_types::SecurityLevel::SecretsOnly,
541                pubkey = %hex_encode(&client_pubkey),
542                peer_pid = peer_creds.pid,
543                peer_uid = peer_creds.uid,
544                "ephemeral client accepted via UCred same-UID validation"
545            );
546            (core_types::SecurityLevel::SecretsOnly, None)
547        }
548    };
549
550    // Register connection AFTER handshake succeeds — no frames can arrive
551    // on `tx` before the writer task is ready to handle them.
552    // Compute initial trust vector from handshake evidence.
553    let trust_vector = core_types::TrustVector {
554        authn_strength: core_types::TrustLevel::Low,
555        authz_freshness: std::time::Duration::ZERO,
556        delegation_depth: 0,
557        device_posture: 0.0,
558        network_exposure: core_types::NetworkTrust::Local,
559        agent_type: core_types::AgentType::Human,
560    };
561
562    let conn = ConnectionState {
563        daemon_id: None,
564        verified_name,
565        tx,
566        peer: peer_creds,
567        security_clearance,
568        subscriptions: vec![],
569        trust_vector: Some(trust_vector),
570    };
571    state.connections.write().await.insert(conn_id, conn);
572
573    // Single-task multiplexed I/O for encrypted transport.
574    //
575    // snow::TransportState requires &mut self for both encrypt and decrypt,
576    // so we cannot split it between separate reader/writer tasks without a
577    // Mutex. But the reader would hold the lock while awaiting socket I/O,
578    // starving the writer (deadlock). Instead, we use tokio::select! to
579    // multiplex reads and writes in a single task that owns the transport.
580    loop {
581        tokio::select! {
582            result = transport.read_encrypted_frame(&mut reader) => {
583                match result {
584                    Ok(mut payload) => {
585                        route_frame(&state, conn_id, &payload).await;
586                        // Zeroize decrypted postcard buffer — may contain secret values.
587                        zeroize::Zeroize::zeroize(&mut payload);
588                    }
589                    Err(e) => {
590                        let session_ms = connected_at.elapsed().as_millis();
591                        let daemon_name = state.connections.read().await
592                            .get(&conn_id)
593                            .and_then(|c| c.daemon_id)
594                            .map(|id| id.to_string());
595                        tracing::info!(
596                            audit = "connection-lifecycle",
597                            event_type = "disconnect",
598                            conn_id,
599                            daemon = daemon_name.as_deref().unwrap_or("unknown"),
600                            session_duration_ms = %session_ms,
601                            error = %e,
602                            "client disconnected"
603                        );
604                        break;
605                    }
606                }
607            }
608            Some(mut payload) = outbound_rx.recv() => {
609                let result = transport.write_encrypted_frame(&mut writer, &payload).await;
610                // Zeroize plaintext postcard buffer after encryption.
611                zeroize::Zeroize::zeroize(&mut payload);
612                if let Err(e) = result {
613                    tracing::debug!(conn_id, error = %e, "encrypted write failed, closing");
614                    break;
615                }
616            }
617            else => break,
618        }
619    }
620
621    state.connections.write().await.remove(&conn_id);
622    state
623        .pending_requests
624        .write()
625        .await
626        .retain(|_, cid| *cid != conn_id);
627    // Clean up name_to_conn on disconnect.
628    state
629        .name_to_conn
630        .write()
631        .await
632        .retain(|_, cid| *cid != conn_id);
633    let session_ms = connected_at.elapsed().as_millis();
634    tracing::debug!(
635        audit = "connection-lifecycle",
636        event_type = "cleanup",
637        conn_id,
638        session_duration_ms = %session_ms,
639        "connection cleaned up"
640    );
641}
642
643/// Send an `AccessDenied` error response back to the sender so the client
644/// gets an actionable error instead of a silent timeout.
645fn send_access_denied(
646    conn: &ConnectionState,
647    request_msg_id: Uuid,
648    epoch: Instant,
649    reason: String,
650) {
651    let ctx = crate::message::MessageContext::new(DaemonId::new());
652    let reply = Message::new(
653        &ctx,
654        EventKind::AccessDenied { reason },
655        SecurityLevel::Open,
656        epoch,
657    )
658    .with_correlation(request_msg_id);
659
660    if let Ok(reply_bytes) = encode_frame(&reply) {
661        let _ = conn.tx.try_send(reply_bytes);
662    }
663}
664
665/// Route a received frame to the appropriate destination(s).
666///
667/// - If the message has a `correlation_id`, it's a response — route only to
668///   the connection that originated the request.
669/// - Otherwise, it's a new request or broadcast — record the `msg_id` for
670///   response routing and forward to all other subscribers.
671#[allow(clippy::too_many_lines)]
672async fn route_frame(state: &ServerState, sender_conn_id: u64, payload: &[u8]) {
673    // Decode the message header to extract routing information.
674    let mut msg: Message<EventKind> = match decode_frame(payload) {
675        Ok(m) => m,
676        Err(e) => {
677            tracing::warn!(conn_id = sender_conn_id, error = %e, "malformed frame, dropping");
678            return;
679        }
680    };
681
682    // Update daemon_id, enforce sender clearance, and capture verified_name
683    // in a single lock acquisition to prevent TOCTOU.
684    let verified_name: Option<String>;
685    let conn_trust_vector: Option<core_types::TrustVector>;
686    {
687        let mut conns = state.connections.write().await;
688        if let Some(conn) = conns.get_mut(&sender_conn_id) {
689            // Sender identity verification: first message records the self-declared
690            // DaemonId. Subsequent messages verify consistency — a connection must
691            // not change its DaemonId mid-session (impersonation attempt).
692            if let Some(known_id) = conn.daemon_id {
693                if known_id != msg.sender {
694                    tracing::warn!(
695                        audit = "security",
696                        event_type = "sender-identity-mismatch",
697                        conn_id = sender_conn_id,
698                        expected_sender = %known_id,
699                        claimed_sender = %msg.sender,
700                        verified_name = conn.verified_name.as_deref().unwrap_or("unregistered"),
701                        "sender DaemonId changed mid-session, dropping frame"
702                    );
703                    send_access_denied(
704                        conn,
705                        msg.msg_id,
706                        state.epoch,
707                        "sender identity changed mid-session".into(),
708                    );
709                    return;
710                }
711            } else {
712                conn.daemon_id = Some(msg.sender);
713
714                // Log the binding between verified name and self-declared DaemonId.
715                if let Some(ref name) = conn.verified_name {
716                    tracing::info!(
717                        audit = "identity-binding",
718                        conn_id = sender_conn_id,
719                        daemon_name = %name,
720                        daemon_id = %msg.sender,
721                        "daemon identity bound: registry name <-> self-declared DaemonId"
722                    );
723                }
724            }
725
726            // Sender clearance enforcement: a daemon may only
727            // emit messages at or below its own clearance level. This prevents a
728            // compromised low-clearance daemon from injecting high-clearance messages.
729            if conn.security_clearance < msg.security_level {
730                tracing::warn!(
731                    conn_id = sender_conn_id,
732                    sender_clearance = ?conn.security_clearance,
733                    msg_level = ?msg.security_level,
734                    "sender clearance below message security level, rejecting"
735                );
736                send_access_denied(
737                    conn,
738                    msg.msg_id,
739                    state.epoch,
740                    format!(
741                        "sender clearance {:?} below message security level {:?}",
742                        conn.security_clearance, msg.security_level,
743                    ),
744                );
745                return;
746            }
747
748            // Capture verified_name and trust_vector from connection state.
749            verified_name = conn.verified_name.clone();
750            conn_trust_vector = conn.trust_vector.clone();
751        } else {
752            verified_name = None;
753            conn_trust_vector = None;
754        }
755    }
756
757    // Populate name_to_conn on DaemonStarted for O(1) unicast lookup.
758    // On key rotation, a daemon reconnects with a new connection while the old
759    // one is still technically alive (TCP hasn't noticed the drop yet). We must
760    // accept the new registration and evict the stale old connection, otherwise
761    // messages get routed to the dead old connection and the daemon starves.
762    if let EventKind::DaemonStarted { .. } = &msg.payload
763        && let Some(ref name) = verified_name
764    {
765        let existing = state.name_to_conn.read().await.get(name).copied();
766        if let Some(existing_conn_id) = existing
767            && existing_conn_id != sender_conn_id
768        {
769            // New connection for the same daemon name (key rotation).
770            // Evict the old connection and accept the new one.
771            state.connections.write().await.remove(&existing_conn_id);
772            tracing::info!(
773                audit = "connection-lifecycle",
774                event_type = "key-rotation-reconnect",
775                daemon_name = %name,
776                old_conn_id = existing_conn_id,
777                new_conn_id = sender_conn_id,
778                "evicted stale connection, accepting reconnect"
779            );
780        }
781        state
782            .name_to_conn
783            .write()
784            .await
785            .insert(name.clone(), sender_conn_id);
786        tracing::info!(
787            daemon_name = %name,
788            conn_id = sender_conn_id,
789            "name_to_conn mapping registered"
790        );
791    }
792
793    // Stamp server-verified sender identity onto the message.
794    // This prevents daemons from self-declaring any name via capabilities.
795    // Re-encode after stamping so downstream receivers get the verified name.
796    // Note: re-encode adds serialization overhead on every routed frame. For a
797    // local IPC bus with <10 daemons this is negligible (~µs per frame).
798    msg.verified_sender_name = verified_name;
799    if msg.trust_snapshot.is_none() {
800        msg.trust_snapshot = conn_trust_vector;
801    }
802    let stamped_payload = match encode_frame(&msg) {
803        Ok(p) => p,
804        Err(e) => {
805            tracing::error!(conn_id = sender_conn_id, error = %e, "failed to re-encode stamped frame");
806            return;
807        }
808    };
809
810    if let Some(corr_id) = msg.correlation_id {
811        // Check confirmed RPC routing table FIRST.
812        // If the host registered a confirmation for this correlation_id,
813        // deliver to the confirmation channel instead of normal routing.
814        let confirm_tx = state.confirmations.read().await.get(&corr_id).cloned();
815        if let Some(ref tx) = confirm_tx {
816            let _ = tx.try_send(stamped_payload.clone()).inspect_err(|_| {
817                tracing::warn!(
818                    correlation_id = %corr_id,
819                    "confirmed RPC response dropped: confirmation channel full"
820                );
821            });
822        }
823
824        // This is a response — route to the originating connection.
825        let target_conn = state.pending_requests.write().await.remove(&corr_id);
826        if let Some(target_id) = target_conn {
827            let conns = state.connections.read().await;
828            if let Some(target) = conns.get(&target_id)
829                && target.tx.try_send(stamped_payload.clone()).is_err()
830            {
831                tracing::warn!(
832                    conn_id = target_id,
833                    correlation_id = %corr_id,
834                    "response dropped: channel full"
835                );
836            }
837        } else if confirm_tx.is_none() {
838            tracing::debug!(
839                correlation_id = %corr_id,
840                "response for unknown request, dropping"
841            );
842        }
843    } else {
844        // New request or broadcast — record for response routing and forward.
845        state
846            .pending_requests
847            .write()
848            .await
849            .insert(msg.msg_id, sender_conn_id);
850
851        let conns = state.connections.read().await;
852        for (&cid, conn) in conns.iter() {
853            if cid == sender_conn_id {
854                continue; // Don't echo back to sender.
855            }
856            if conn.security_clearance < msg.security_level {
857                continue; // Recipient clearance too low for this message.
858            }
859            if conn.tx.try_send(stamped_payload.clone()).is_err() {
860                tracing::warn!(conn_id = cid, "subscriber channel full, frame dropped");
861            }
862        }
863    }
864}