Skip to main content

car_server_core/
peers.rs

1//! Peer messaging between agents — `agents.peers` and `agents.message`.
2//!
3//! Delivery is a reverse-call down the authenticated connection the daemon
4//! already holds for each agent, the same mechanism `agents.chat` uses. There
5//! is deliberately no per-agent socket: a path that reached an agent without
6//! passing through here would make admission advisory, since nothing would sit
7//! between sender and recipient.
8//!
9//! Design: `docs/proposals/agent-to-agent-messaging.md`.
10//!
11//! ## What a message is, and is not
12//!
13//! A peer message is inert data. It is explicitly not a `proposal.submit`, so it
14//! cannot reach the executor; whatever the receiving agent decides to *do* about
15//! it goes through that agent's own gates unchanged. It cannot answer a pending
16//! permission prompt, and a slash command in the body arrives as text.
17//!
18//! ## Why the sender is not a parameter
19//!
20//! `from` is derived server-side from the connection's bound `agent_id`. A
21//! caller-supplied sender would let any agent attribute a message to any other,
22//! which would in turn make the anti-laundering rule — never ask a peer to do
23//! what was refused here — unenforceable, because the audit trail would be
24//! forgeable.
25
26use crate::session::{ClientSession, ServerState};
27use car_peers::{
28    DeliveryGuard, DeliveryOutcome, GuardVerdict, PeerAddress, PeerDescriptor, PeerDirectory,
29    PeerKind, PeerMessage, PeerSource, StaticProvider,
30};
31use futures::SinkExt;
32use serde_json::Value;
33use tokio::sync::oneshot;
34use tokio_tungstenite::tungstenite::Message;
35
36/// A peer message set aside for operator approval.
37///
38/// Carries the resolved target alongside the message because the recipient may
39/// have detached by the time a human answers — approval then fails with a
40/// structured error naming the agent, rather than silently re-resolving to
41/// whatever now answers to that name.
42#[derive(Debug, Clone, serde::Serialize)]
43pub struct HeldPeerMessage {
44    pub message: PeerMessage,
45    pub target: PeerDescriptor,
46    pub held_at_ms: u64,
47    pub reason: String,
48}
49
50/// How long to wait for a recipient to acknowledge a peer message.
51///
52/// Short on purpose. The ack means "your agent took delivery", not "your agent
53/// acted on it" — an agent that treats a message as work to do would otherwise
54/// hold the sender's call open for the length of a task.
55const PEER_ACK_TIMEOUT_SECS: u64 = 5;
56
57/// An MCP peer address is reaped after a full day without a request.
58///
59/// Interactive CLI sessions routinely last hours. A day leaves headroom for
60/// that use while bounding abandoned HTTP sessions whose client never sends the
61/// protocol DELETE request.
62pub(crate) const MCP_PEER_IDLE_TTL_MS: u64 = 24 * 60 * 60 * 1000;
63
64/// One MCP protocol session's peer state.
65#[derive(Debug)]
66pub(crate) struct McpPeerSession {
67    pub(crate) receive_capable: bool,
68    pub(crate) inbox: std::collections::VecDeque<PeerMessage>,
69    pub(crate) last_seen_ms: u64,
70}
71
72fn mcp_principal(session_id: &str) -> String {
73    format!("mcp:{session_id}")
74}
75
76/// Mint a protocol-session id and its unforgeable peer principal.
77pub(crate) async fn open_mcp_peer_session(
78    state: &ServerState,
79    receive_capable: bool,
80) -> (String, String) {
81    prune_mcp_peer_sessions(state).await;
82    let session_id = uuid::Uuid::new_v4().to_string();
83    let principal = mcp_principal(&session_id);
84    state.mcp_peer_sessions.lock().await.insert(
85        session_id.clone(),
86        McpPeerSession {
87            receive_capable,
88            inbox: std::collections::VecDeque::new(),
89            last_seen_ms: car_peers::now_ms(),
90        },
91    );
92    (session_id, principal)
93}
94
95/// Confirm and refresh one session, returning its peer principal.
96pub(crate) async fn touch_mcp_peer_session(
97    state: &ServerState,
98    session_id: &str,
99) -> Option<String> {
100    prune_mcp_peer_sessions(state).await;
101    let mut sessions = state.mcp_peer_sessions.lock().await;
102    let session = sessions.get_mut(session_id)?;
103    session.last_seen_ms = car_peers::now_ms();
104    Some(mcp_principal(session_id))
105}
106
107/// End one MCP peer session and discard its unread inbox and channel budget.
108pub(crate) async fn close_mcp_peer_session(state: &ServerState, session_id: &str) -> bool {
109    let principal = mcp_principal(session_id);
110    let removed = state
111        .mcp_peer_sessions
112        .lock()
113        .await
114        .remove(session_id)
115        .is_some();
116    if removed {
117        state.peer_guards.lock().await.remove(&principal);
118    }
119    removed
120}
121
122async fn prune_mcp_peer_sessions(state: &ServerState) {
123    let now = car_peers::now_ms();
124    let expired = {
125        let mut sessions = state.mcp_peer_sessions.lock().await;
126        let expired: Vec<String> = sessions
127            .iter()
128            .filter(|(_, session)| now.saturating_sub(session.last_seen_ms) > MCP_PEER_IDLE_TTL_MS)
129            .map(|(id, _)| id.clone())
130            .collect();
131        for id in &expired {
132            sessions.remove(id);
133        }
134        expired
135    };
136    if !expired.is_empty() {
137        let mut guards = state.peer_guards.lock().await;
138        for id in expired {
139            guards.remove(&mcp_principal(&id));
140        }
141    }
142}
143
144/// Snapshot receive-capable MCP sessions as addressable peers.
145pub async fn snapshot_mcp_sessions(state: &ServerState) -> Vec<PeerDescriptor> {
146    prune_mcp_peer_sessions(state).await;
147    state
148        .mcp_peer_sessions
149        .lock()
150        .await
151        .iter()
152        .filter(|(_, session)| session.receive_capable)
153        .map(|(session_id, session)| {
154            let principal = mcp_principal(session_id);
155            PeerDescriptor {
156                name: principal,
157                reference: None,
158                kind: PeerKind::McpSession,
159                source: PeerSource::Mcp,
160                address: PeerAddress::McpSession {
161                    session_id: session_id.clone(),
162                },
163                display_name: Some("MCP session".into()),
164                capability: Some("polling peer inbox".into()),
165                last_seen_ms: Some(session.last_seen_ms),
166                pubkey: None,
167            }
168        })
169        .collect()
170}
171
172/// Snapshot the agents currently attached to this daemon as peers.
173///
174/// A point-in-time copy rather than a live view: assembling a listing while the
175/// connection table shifts underneath would produce a list that never existed.
176/// The on-disk agent registry is deliberately not consulted — it is observe-only
177/// self-report whose reap sweep tolerates a 900s stale window, so routing on it
178/// would address agents that exited a quarter of an hour ago.
179pub async fn snapshot_attached(state: &ServerState) -> Vec<PeerDescriptor> {
180    let attached = state.attached_agents.lock().await.clone();
181    attached
182        .into_keys()
183        .filter(|id| car_peers::is_valid_peer_name(id))
184        .map(|agent_id| PeerDescriptor {
185            name: agent_id.clone(),
186            reference: None,
187            kind: PeerKind::CarAgent,
188            source: PeerSource::Attached,
189            address: PeerAddress::AttachedAgent { agent_id },
190            display_name: None,
191            capability: None,
192            last_seen_ms: Some(car_peers::now_ms()),
193            pubkey: None,
194        })
195        .collect()
196}
197
198/// Build the directory as seen by `session`.
199async fn directory_for(state: &ServerState, session: &ClientSession) -> PeerDirectory {
200    let self_name = session.agent_id.lock().await.clone().unwrap_or_default();
201    let mut dir = PeerDirectory::new(self_name).with_provider(Box::new(StaticProvider::new(
202        "attached",
203        snapshot_attached(state).await,
204    )));
205    for (label, peers) in [
206        ("mcp", snapshot_mcp_sessions(state).await),
207        ("parslee", snapshot_parslee(state).await),
208        ("lan", snapshot_lan(state)),
209    ] {
210        if !peers.is_empty() {
211            dir = dir.with_provider(Box::new(StaticProvider::new(label, peers)));
212        }
213    }
214    dir
215}
216
217/// CAR daemons this user's other devices announced over the synced oplog.
218///
219/// Authenticated by construction: the oplog is readable only with this user's
220/// own credentials and is end-to-end encrypted, so an entry here is a machine
221/// they enrolled. Empty when sync is not configured — "find my other Mac
222/// through Parslee" needs a login, and without one this is honestly nothing
223/// rather than a guess.
224pub async fn snapshot_parslee(state: &ServerState) -> Vec<PeerDescriptor> {
225    let handle = { state.sync.lock().unwrap_or_else(|e| e.into_inner()).clone() };
226    let Some(sync) = handle else {
227        return Vec::new();
228    };
229    let endpoints = { sync.lock().await.host_endpoints() };
230    endpoints
231        .into_iter()
232        .filter(|e| car_peers::is_valid_peer_name(&e.name))
233        .map(|e| PeerDescriptor {
234            name: e.name,
235            reference: None,
236            kind: PeerKind::RemoteCar,
237            source: PeerSource::Parslee,
238            address: PeerAddress::A2a { base_url: e.url },
239            display_name: Some(e.device_id),
240            capability: None,
241            last_seen_ms: None,
242            // The one source that knows it: the roster is readable only with
243            // the user's own bearer, so the key beside an endpoint there is one
244            // they enrolled. `refresh_peer_trust` already reads the same field.
245            pubkey: Some(e.pubkey).filter(|k| !k.is_empty()),
246        })
247        .collect()
248}
249
250/// Recompute which peer keys this host accepts.
251///
252/// Sourced from the oplog only. A key there arrived over an E2E-encrypted
253/// channel this login's key material protects, so publishing one requires
254/// already being the user's device — that is what makes it trustworthy without
255/// an operator comparing fingerprints.
256///
257/// mDNS keys are deliberately excluded. An advertisement is an unauthenticated
258/// claim, and accepting a key because it was broadcast would defeat the entire
259/// scheme: anyone on the network could then talk to CAR. A LAN-discovered host
260/// on the same login shows up here anyway, through the oplog.
261///
262/// Replaces the set wholesale so a device removed upstream stops being accepted.
263pub async fn refresh_peer_trust(state: &ServerState) {
264    let handle = { state.sync.lock().unwrap_or_else(|e| e.into_inner()).clone() };
265    let Some(sync) = handle else {
266        state.peer_trust.set_trusted(Vec::<String>::new());
267        return;
268    };
269    let keys: Vec<String> = sync
270        .lock()
271        .await
272        .host_endpoints()
273        .into_iter()
274        .map(|e| e.pubkey)
275        .filter(|k| !k.trim().is_empty())
276        .collect();
277    let n = keys.len();
278    state.peer_trust.set_trusted(keys);
279    tracing::debug!(trusted_peers = n, "refreshed CAR peer trust set");
280}
281
282/// CAR daemons advertising themselves on the local network.
283///
284/// Unauthenticated: anyone on the network can advertise any name. These are
285/// listed so an operator can see them, and `agents.message` refuses them until
286/// they are promoted through the A2A peer registry's trust gate — the same one
287/// `a2a.peers.add` uses. Discovery makes a peer visible; it does not make it
288/// reachable.
289pub fn snapshot_lan(state: &ServerState) -> Vec<PeerDescriptor> {
290    let guard = state
291        .lan_discovery
292        .lock()
293        .unwrap_or_else(|e| e.into_inner());
294    let Some(dir) = guard.as_ref() else {
295        return Vec::new();
296    };
297    let trusted: std::collections::HashSet<String> = car_a2a::peers::PeerRegistry::user_default()
298        .map(|r| r.list().into_iter().map(|p| p.url).collect())
299        .unwrap_or_default();
300    dir.peers()
301        .into_iter()
302        .filter(|p| car_peers::is_valid_peer_name(&p.name))
303        // A LAN peer the operator already promoted is reported under its
304        // trusted source instead, so it is addressable and not double-listed.
305        .filter(|p| !trusted.contains(&p.url))
306        .map(|p| PeerDescriptor {
307            name: p.name,
308            reference: None,
309            kind: PeerKind::RemoteCar,
310            source: PeerSource::Lan,
311            address: PeerAddress::A2a { base_url: p.url },
312            display_name: None,
313            capability: None,
314            last_seen_ms: None,
315            // Deliberately absent. An advertisement can carry a key, but anyone
316            // on the network can broadcast one — it is a claim, not a
317            // credential, and keying a durable record on it would let a
318            // stranger write into another peer's history.
319            pubkey: None,
320        })
321        .collect()
322}
323
324/// The stable delivery preflight shared by `agents.peers` and `agents.message`.
325///
326/// This intentionally covers only facts on [`PeerDescriptor`]: whether the kind
327/// has an inbox and whether its source is trusted. Later delivery guards depend
328/// on the sender, message, standing, and point-in-time channel state, so a peer
329/// that passes this snapshot can still be refused when a send is attempted.
330/// Keeping these two checks here means the listing cannot advertise a peer that
331/// the delivery path would reject before looking at the message.
332fn peer_reachability(target: &PeerDescriptor) -> Result<(), String> {
333    if !target.source.is_trusted_by_default() {
334        return Err(format!(
335            "`{}` was discovered on the local network and is not a trusted peer. Anyone on \
336             this network can advertise any name, so discovery makes a peer visible, not \
337             reachable. Promote it with `a2a.peers.add` first.",
338            target.name
339        ));
340    }
341
342    if !target.kind.can_receive() {
343        return Err(format!(
344            "`{}` is a {} — it can message CAR while it runs but has no inbox to deliver into",
345            target.name,
346            target.kind.as_str()
347        ));
348    }
349
350    Ok(())
351}
352
353/// Render the peer row used by `agents.peers`.
354fn peer_listing_row(peer: &PeerDescriptor, standing: Option<Value>) -> Value {
355    serde_json::json!({
356        "standing": standing,
357        "name": peer.name,
358        "address": peer.address_form(),
359        "reference": peer.reference,
360        "kind": peer.kind.as_str(),
361        "source": peer.source.as_str(),
362        "can_receive": peer.kind.can_receive(),
363        "reachable": peer_reachability(peer).is_ok(),
364        "display_name": peer.display_name,
365        "capability": peer.capability,
366        "last_seen_ms": peer.last_seen_ms,
367    })
368}
369
370/// `agents.peers` — who this caller can message.
371///
372/// Mirrors the shape of Claude Code's `/list-agents`: the caller's own name
373/// first (it is the address others use to reach it) and never among the rows,
374/// since a message addressed to yourself is an error rather than a loopback.
375pub async fn handle_agents_peers(
376    state: &ServerState,
377    session: &ClientSession,
378) -> Result<Value, String> {
379    let dir = directory_for(state, session).await;
380    let peers = dir.list();
381    // Whether OTHER hosts can find this one. Distinct from whether this host can
382    // find them: browsing needs nothing, advertising needs an A2A endpoint. A
383    // caller seeing an empty peer list needs to know which half is missing.
384    let discoverable = state
385        .lan_discovery
386        .lock()
387        .unwrap_or_else(|e| e.into_inner())
388        .is_some();
389    // Standing, so an operator can see WHY a peer is being throttled instead of
390    // discovering it as unexplained slowness. Rendered only where a record
391    // exists: absent means "nothing observed yet", which is different from a
392    // clean record and should not be dressed up as one.
393    let now = car_peers::now_ms();
394    let standing = {
395        let map = state.peer_standing.lock().await;
396        peers
397            .iter()
398            .map(|p| {
399                let key = p
400                    .pubkey
401                    .as_deref()
402                    .map(car_a2a::peer_principal)
403                    .unwrap_or_else(|| format!("agent:{}", p.name));
404                map.get(&key).map(|r| {
405                    serde_json::json!({
406                        "state": if r.is_degraded(now) { "degraded" } else { "ok" },
407                        "success": r.success_count,
408                        "fail": r.fail_count,
409                        "last_fail_reason": r.last_fail_reason,
410                        "last_fail_via": r.last_fail_via,
411                    })
412                })
413            })
414            .collect::<Vec<_>>()
415    };
416    Ok(serde_json::json!({
417        "self": if dir.self_name().is_empty() { Value::Null } else { Value::from(dir.self_name()) },
418        "lan_browsing": discoverable,
419        "peers": peers
420            .iter()
421            .zip(standing)
422            .map(|(peer, standing)| peer_listing_row(peer, standing))
423            .collect::<Vec<_>>(),
424        "count": peers.len(),
425    }))
426}
427
428/// `agents.message` — deliver text to one peer.
429///
430/// Params: `{ to, body, summary? }`. `from` is never read from params.
431/// Admission for a **synchronous turn** into a live agent — `agents.chat` and
432/// the A2A conversational responder.
433///
434/// Both reverse-call an agent through the same daemon-owned channel that
435/// [`handle_agents_message`] uses, and both went through no guard and no
436/// policy. So an agent set to `Deny` at the read-only tier could reach another
437/// agent by chatting at it instead, and two agents could answer each other with
438/// nothing to terminate the loop — the hazard [`DeliveryGuard`] exists for,
439/// since neither participant is misbehaving. Tightening one method while
440/// leaving its twins open just leaves the ungoverned one as the way around.
441///
442/// **Typed on a `&str` principal, not a `ClientSession`, and that is the
443/// point.** The MCP peer-message tool already had to invent its own principal
444/// because it has no session, the A2A responder has none either, and an inbound
445/// remote message will be in the same position. An admission helper that can
446/// only be called from a WS session is one the next caller has to route around,
447/// which is how the surfaces diverged in the first place.
448///
449/// ## Two tiers, because callers differ in what they can prove
450///
451/// 1. **The channel guard applies to everyone.** Dedupe inside 10s, 20 sends
452///    per sender per minute, keyed per recipient and *shared with
453///    `agents.message`* — a rate budget is a property of the channel, not of
454///    whichever method reached it. This is what bounds loops and floods
455///    regardless of who is calling.
456/// 2. **The `AgentPermissionPolicy` check applies only when the caller is a
457///    bound agent** (`sender_agent`). [`admit_with`] refuses a caller that is
458///    neither a bound agent nor the host, which is right for `agents.message`,
459///    where every sender is a session — and wrong for the A2A responder, whose
460///    caller is a remote party the operator deliberately exposed by passing
461///    `share_session_runtime`. Refusing there would delete a documented
462///    feature, so for that path the guard *is* the admission, and the surface's
463///    own gates (loopback-only bind unless overridden) carry the rest.
464///
465/// **The host is exempt from both.** It is the operator's own client,
466/// [`admit_with`] already passes it, and the dedupe window would otherwise
467/// swallow a person legitimately retyping the same prompt inside ten seconds.
468///
469/// Uses [`DeliveryGuard::admit_synchronous`], which stays out of `QUEUE_CAP`
470/// entirely. Attached recipients use that count for in-flight acknowledgements;
471/// polling MCP recipients use it for unread inbox entries. A blocking turn
472/// belongs to neither: taking a slot and releasing it immediately would let a
473/// minutes-long chat cost nothing while a cheap message is counted, and holding
474/// it for the turn would let fifty chats block all messaging. The
475/// dedupe window and rate budget *are* shared, because those are properties of
476/// the channel, and leaving them unshared is exactly what would let one method
477/// be used to escape the other's limit.
478///
479/// The caller must report a turn that never reached the agent via
480/// [`forget_synchronous_turn`], so an immediate retry is not refused as a
481/// duplicate of something that was never delivered.
482///
483/// Every refusal is written to the peer audit journal with its outcome. That
484/// matters more now that the budget is shared: an `agents.message` refused for
485/// rate could have had its budget drained entirely by chat traffic, and without
486/// a record there is nothing to show where it went.
487pub(crate) async fn admit_turn(
488    state: &ServerState,
489    principal: &str,
490    sender_agent: Option<String>,
491    is_host: bool,
492    target_agent: &str,
493    body: &str,
494) -> Result<(), String> {
495    if is_host {
496        return Ok(());
497    }
498
499    let msg = PeerMessage::new(principal, target_agent, body);
500    let target = local_agent_descriptor(target_agent);
501
502    let verdict = {
503        let mut guards = state.peer_guards.lock().await;
504        let guard = guards
505            .entry(target_agent.to_string())
506            .or_insert_with(DeliveryGuard::new);
507        guard.admit_synchronous(&msg, car_peers::now_ms())
508    };
509    if !verdict.is_accept() {
510        if matches!(
511            verdict,
512            car_peers::GuardVerdict::HopLimit { .. }
513                | car_peers::GuardVerdict::TooLarge { .. }
514                | car_peers::GuardVerdict::InvalidName { .. }
515        ) {
516            record_standing(state, principal, Some(&verdict.reason()), Some(&msg.via)).await;
517        }
518        let outcome = DeliveryOutcome::Refused {
519            reason: verdict.reason(),
520        };
521        append_peer_audit(state, &msg, &target, &outcome);
522        return Err(guard_error(&verdict));
523    }
524
525    // No bound agent means the caller cannot be graded against an agent
526    // posture. See tier 2 above: that is a refusal for `agents.message` and
527    // deliberately not one here.
528    let Some(agent) = sender_agent else {
529        return Ok(());
530    };
531
532    let outcome = admit_with(
533        &crate::agent_permissions::load_policy(),
534        Some(agent),
535        is_host,
536        principal,
537    );
538    match &outcome {
539        DeliveryOutcome::Delivered => Ok(()),
540        // Admission decides whether to *try*; it never reports on the attempt,
541        // so this outcome cannot arise here. Matched rather than wildcarded so
542        // a future admission that can produce it has to come back and decide.
543        DeliveryOutcome::Unacknowledged { .. } => Ok(()),
544        DeliveryOutcome::Refused { reason } => {
545            append_peer_audit(state, &msg, &target, &outcome);
546            Err(format!("chat refused: {reason}"))
547        }
548        // A synchronous turn has a caller blocked on it, so there is nothing to
549        // hold it in. Do NOT point at `agents.chat.approve` here: that exists,
550        // one namespace over, and approves a *tool prompt inside a running
551        // turn* — not the turn's admission. An operator told to look there
552        // would reasonably conclude one of the two is broken.
553        DeliveryOutcome::Held { reason } => {
554            append_peer_audit(state, &msg, &target, &outcome);
555            Err(format!(
556                "chat refused: {reason}. `RequireApproval` means a human sees it \
557                 first, which a blocking call cannot wait on without hanging the \
558                 caller. Use `agents.message`, which holds the message for \
559                 `agents.message.approve` and delivers it after the decision."
560            ))
561        }
562    }
563}
564
565/// How long a standing record survives without being touched.
566const STANDING_TTL_MS: u64 = 7 * 24 * 60 * 60 * 1000;
567/// Records kept before the least-recently-updated are pruned.
568const STANDING_MAP_CAP: usize = 4096;
569/// Successes stop accumulating here.
570const STANDING_SUCCESS_CAP: u64 = 50;
571/// Both counters halve once per elapsed span of this length.
572const STANDING_HALFLIFE_MS: u64 = 7 * 24 * 60 * 60 * 1000;
573
574/// What a sending principal has earned, from this daemon's own observations.
575///
576/// CAR grades *artifacts* on a track record — a skill degrades at
577/// `fail > success + 2` — and grades *actors* not at all: a peer's standing was
578/// a constant decided by where it was discovered, so one that reliably wasted
579/// your agents' time had the same access on its thousandth message as its
580/// first. This is the actor half.
581///
582/// **Only outcomes this daemon observed, and never message content.** A body
583/// that could move a sender's standing would make the envelope an authority
584/// channel, which the "a message is data, never instruction" rule forbids
585/// outright.
586#[derive(Debug, Default, Clone)]
587pub struct PeerStanding {
588    pub success_count: u64,
589    pub fail_count: u64,
590    pub last_fail_reason: Option<String>,
591    /// The chain of the most recent attributable failure.
592    ///
593    /// The consequence lands on the key, which is all a receiver can verify;
594    /// the evidence names the path, which is what an operator needs to act.
595    /// Enforce at the granularity you can check, attribute at the granularity
596    /// you can record.
597    pub last_fail_via: Option<Vec<String>>,
598    /// Send timestamps inside the degraded-throttle window.
599    pub window: std::collections::VecDeque<u64>,
600    pub updated_ms: u64,
601}
602
603impl PeerStanding {
604    /// Halve both counters once per elapsed half-life.
605    ///
606    /// Computed at read time from stored state rather than by a background
607    /// task: no timer, no drift, and the same input always yields the same
608    /// answer. Decay is what makes a degraded peer recoverable without anyone
609    /// remembering to forgive it — the alternative is a ratchet that only ever
610    /// tightens.
611    fn decayed(&self, now_ms: u64) -> (u64, u64) {
612        let elapsed = now_ms.saturating_sub(self.updated_ms);
613        let halvings = (elapsed / STANDING_HALFLIFE_MS).min(63) as u32;
614        (self.success_count >> halvings, self.fail_count >> halvings)
615    }
616
617    /// Whether this record is degraded right now.
618    pub fn is_degraded(&self, now_ms: u64) -> bool {
619        let (s, f) = self.decayed(now_ms);
620        car_policy::degrades(s, f, car_policy::DEGRADE_THRESHOLD)
621    }
622}
623
624/// What standing says about a sender's next message.
625#[derive(Debug, PartialEq, Eq)]
626pub enum StandingVerdict {
627    /// Not degraded, or degraded and inside the reduced budget.
628    Proceed,
629    /// Degraded and over the reduced budget.
630    Throttled { reason: String },
631}
632
633/// The standing check, host-scoped and run **before** the per-recipient guard.
634///
635/// That ordering is what makes laundering fail. `DeliveryGuard.rate` is keyed
636/// on the sender *inside* a per-recipient guard, so a degraded host that
637/// renamed its agents — or simply addressed a different recipient — would
638/// otherwise collect a fresh budget per name per recipient. Standing is keyed
639/// on the principal this daemon actually verified.
640///
641/// A healthy sender is untouched: this adds no aggregate ceiling to normal
642/// traffic. Only a degraded one meets [`car_peers::DEGRADED_RATE_LIMIT`].
643///
644/// Autonomous by design. Turning each reduction into an approval prompt would
645/// rebuild the approval fatigue that drives operators to switch a gate off
646/// entirely — the mitigation *is* the answer, which is the lesson
647/// `skill_trust`'s own deployment gate already encodes.
648pub async fn standing_gate(state: &ServerState, key: &str) -> StandingVerdict {
649    let now = car_peers::now_ms();
650    let mut map = state.peer_standing.lock().await;
651    let Some(rec) = map.get_mut(key) else {
652        return StandingVerdict::Proceed;
653    };
654    if !rec.is_degraded(now) {
655        return StandingVerdict::Proceed;
656    }
657    while rec
658        .window
659        .front()
660        .is_some_and(|t| now.saturating_sub(*t) > car_peers::RATE_WINDOW_MS)
661    {
662        rec.window.pop_front();
663    }
664    if rec.window.len() as u32 >= car_peers::DEGRADED_RATE_LIMIT {
665        return StandingVerdict::Throttled {
666            reason: format!(
667                "`{key}` is degraded ({} failures against {} successes) and is \
668                 limited to {} messages per minute until its record recovers",
669                rec.fail_count,
670                rec.success_count,
671                car_peers::DEGRADED_RATE_LIMIT
672            ),
673        };
674    }
675    rec.window.push_back(now);
676    StandingVerdict::Proceed
677}
678
679/// Record an outcome against a sender's standing.
680///
681/// `failure` is `Some(reason)` only for outcomes attributable to the sender's
682/// **own choices** — a chain too deep, an oversized body, a malformed name or
683/// lineage segment, an unresolvable recipient, an operator's explicit denial.
684///
685/// Deliberately excluded: a rate limit or a duplicate inside the window, both
686/// of which the channel guard exists to absorb precisely because *neither party
687/// is misbehaving* in a mutual loop — charging them would price correct
688/// behaviour as misconduct. Also excluded: a refusal by this host's own policy
689/// (our posture, not their conduct) and an unacknowledged delivery (the
690/// recipient's own agent did not answer).
691pub async fn record_standing(
692    state: &ServerState,
693    key: &str,
694    failure: Option<&str>,
695    via: Option<&[String]>,
696) {
697    let now = car_peers::now_ms();
698    let mut map = state.peer_standing.lock().await;
699    if map.len() >= STANDING_MAP_CAP && !map.contains_key(key) {
700        // Prune the least recently updated, and anything past its TTL.
701        map.retain(|_, r| now.saturating_sub(r.updated_ms) < STANDING_TTL_MS);
702        if map.len() >= STANDING_MAP_CAP {
703            if let Some(oldest) = map
704                .iter()
705                .min_by_key(|(_, r)| r.updated_ms)
706                .map(|(k, _)| k.clone())
707            {
708                map.remove(&oldest);
709            }
710        }
711    }
712    let rec = map.entry(key.to_string()).or_default();
713    // Fold the decay in before recording, so an old record does not carry its
714    // full weight forward the moment it is touched again.
715    let (s, f) = rec.decayed(now);
716    rec.success_count = s;
717    rec.fail_count = f;
718    match failure {
719        Some(reason) => {
720            rec.fail_count = rec.fail_count.saturating_add(1);
721            rec.last_fail_reason = Some(reason.to_string());
722            rec.last_fail_via = via.map(|v| v.to_vec());
723        }
724        None => {
725            // Saturating, not unbounded. Without a ceiling a peer with ten
726            // thousand successes could send ten thousand attributable failures
727            // before a throttle engaged — tolerable for an artifact whose
728            // numerator the host controls, indefensible for an actor that
729            // controls its own send rate. Headroom is the cap plus the
730            // threshold, whatever the history.
731            rec.success_count = rec
732                .success_count
733                .saturating_add(1)
734                .min(STANDING_SUCCESS_CAP);
735        }
736    }
737    rec.updated_ms = now;
738}
739
740/// The receiving half of the two-admission property.
741///
742/// `PeerAddress` has no variant naming a remote agent precisely so that a
743/// cross-host message must land on a *daemon* and be admitted there before it
744/// reaches anyone. Three doc comments asserted that happened. Nothing did it:
745/// an inbound peer message was compiled into an `ActionProposal` like any other
746/// A2A traffic and answered with an acknowledgement stub, so a message that had
747/// passed the sender's guard and policy passed nothing on arrival.
748///
749/// This is that missing admission. It deliberately does **not** consult
750/// `AgentPermissionPolicy`: those rows are keyed by *local* agent id, there is
751/// no local sender to resolve inbound, and letting a remote-supplied name
752/// select which local posture governs it would be a free pass around any
753/// `Deny` — per-agent rows are sparse overrides over a permissive default, so
754/// naming an unlisted id would land on "allow". The operator's per-peer control
755/// is the trust set the middleware already enforced, and later an explicit
756/// per-peer record; it is not this function guessing from a string the
757/// counterparty wrote.
758pub struct PeerInboundBroker {
759    /// Weak so a stopped listener cannot keep the daemon's state alive — the
760    /// same reasoning as the fleet responder wired beside it.
761    pub state: std::sync::Weak<ServerState>,
762}
763
764/// Append this host's boundary marker to the chain a peer attested.
765///
766/// APPEND, never substitute. Everything before the marker is what the sending
767/// key *claimed*; everything from it on is what a daemon *observed*. Collapsing
768/// the two — by replacing the prefix, or by trusting the claim unmarked — is
769/// what makes a chain unauditable, because a later reader can no longer tell
770/// which segments any host actually stood behind.
771///
772/// Split out of `deliver` so the property has somewhere to be asserted. It was
773/// previously inline, and the test named for it
774/// (`the_receiver_appends_its_own_boundary_marker`) passed with the append
775/// deleted — it checked that a guard entry appeared, which happens either way.
776/// The end-to-end test now reads the configured state's audit row and asserts
777/// this receiver-authored marker directly.
778fn stamp_boundary(attested: &[String], marker: &str) -> Vec<String> {
779    let mut via = attested.to_vec();
780    via.push(marker.to_string());
781    via
782}
783
784#[async_trait::async_trait]
785impl car_a2a::PeerInbox for PeerInboundBroker {
786    async fn deliver(
787        &self,
788        inbound: car_a2a::InboundPeerMessage,
789    ) -> Result<serde_json::Value, String> {
790        let state = self
791            .state
792            .upgrade()
793            .ok_or_else(|| "daemon is shutting down".to_string())?;
794
795        // The sender is the key the signature proved, never `carPeerFrom`. A
796        // receiving broker that used the claimed name would attribute a message
797        // on the strength of a string the counterparty minted — and the local
798        // module doc's own rule is that a caller-supplied sender makes the
799        // audit forgeable.
800        let from = car_a2a::peer_principal(&inbound.peer_pubkey);
801
802        // Standing first, host-scoped, before anything per-recipient. A
803        // degraded host that renamed its agents or simply picked a different
804        // recipient would otherwise collect a fresh budget each time, because
805        // the channel guard's rate bucket lives inside a per-recipient guard.
806        if let StandingVerdict::Throttled { reason } = standing_gate(&state, &from).await {
807            return Err(reason);
808        }
809
810        if !car_peers::is_valid_peer_name(&inbound.claimed.to) {
811            record_standing(&state, &from, Some("illegal recipient name"), None).await;
812            return Err(format!("`{}` is not a legal peer name", inbound.claimed.to));
813        }
814
815        // Inbound lineage is remote-supplied text that this host is about to
816        // show one of its own agents, and until here nothing in CAR had ever
817        // validated it. Bound the shape before an agent sees it; a segment that
818        // is merely *false* is not detectable at all, since everything before a
819        // boundary marker is the far side's account of itself.
820        if inbound.claimed.via.len() > car_peers::MAX_HOPS * 2 {
821            record_standing(&state, &from, Some("oversized lineage"), None).await;
822            return Err(format!(
823                "chain carries {} segments, over the {} the hop cap can produce",
824                inbound.claimed.via.len(),
825                car_peers::MAX_HOPS * 2
826            ));
827        }
828        if let Some(bad) = inbound
829            .claimed
830            .via
831            .iter()
832            .find(|s| !car_peers::is_valid_via_segment(s))
833        {
834            let bad = bad.clone();
835            record_standing(&state, &from, Some("malformed lineage segment"), None).await;
836            return Err(format!("`{bad}` is not a well-formed lineage segment"));
837        }
838        if inbound.claimed.trace.len() > 128 {
839            return Err("chain id is too long".to_string());
840        }
841
842        // Attached agents only. Resolving through the full directory would let
843        // this host forward to a peer it knows — turning it into an open relay
844        // where host A makes host B deliver to host C under B's signature,
845        // reaching hosts that never trusted A. The refusal is explicit rather
846        // than a resolution failure so the sender learns the rule.
847        let target = snapshot_attached(&state)
848            .await
849            .into_iter()
850            .find(|p| p.name == inbound.claimed.to)
851            .ok_or_else(|| {
852                format!(
853                    "`{}` is not an agent attached to this host; peer messages are \
854                     delivered to local agents only and are never relayed",
855                    inbound.claimed.to
856                )
857            });
858        let target = match target {
859            Ok(t) => t,
860            Err(e) => {
861                // Attributable: the sender chose an address this host does not
862                // serve, or tried to have it relayed.
863                record_standing(&state, &from, Some("unresolvable recipient"), None).await;
864                return Err(e);
865            }
866        };
867
868        // The id is the sender's, reused so both hosts' audit rows correlate —
869        // which is exactly why it is bounded before it reaches this host's
870        // journal and the `agent.peer_message` frame.
871        if inbound.claimed.message_id.len() > 128 {
872            return Err("message id is too long".to_string());
873        }
874
875        let mut msg = PeerMessage::new(&from, &target.name, &inbound.claimed.body);
876        msg.id = inbound.claimed.message_id;
877        // Stamp the chain BEFORE the guard runs. `PeerMessage::new` leaves
878        // `via` empty, so admitting first would mean `hops()` was 0 on every
879        // inbound message and `HopLimit` could never fire on cross-host traffic
880        // — the one case it exists for.
881        //
882        // The boundary marker is APPENDED, never substituted for the prefix.
883        // Everything before it is what this key *attested*; everything after is
884        // what a daemon observed. That is what lets a later host tell the two
885        // apart, and it is why the marker is stamped here — by the receiver,
886        // from the key it verified — and never by the sender.
887        msg.trace = if inbound.claimed.trace.is_empty() {
888            msg.id.clone()
889        } else {
890            inbound.claimed.trace.clone()
891        };
892        msg.via = stamp_boundary(&inbound.claimed.via, &from);
893        // `no_reply` is forced, not carried. `PeerMessage.from` is documented as
894        // the address a recipient replies to by copying back — and `peer:<key>`
895        // is not one: it contains `:` and base64's `+`/`/`, so
896        // `is_valid_peer_name` rejects it and no provider resolves it. It is an
897        // *attribution*, which is what an inbound message can honestly offer,
898        // since `PeerAddress` has no variant naming a remote agent to reply to.
899        // Telling the agent it may reply and handing it an unresolvable string
900        // would be worse than saying so.
901        msg.no_reply = true;
902
903        // The same channel guard the local path applies, on the same
904        // per-recipient budget. A remote sender that loops is bounded by the
905        // recipient's channel, not by whatever the far side chose to enforce.
906        let verdict = {
907            let mut guards = state.peer_guards.lock().await;
908            let guard = guards
909                .entry(target.name.clone())
910                .or_insert_with(DeliveryGuard::new);
911            guard.admit(&msg, car_peers::now_ms())
912        };
913        if !verdict.is_accept() {
914            // A rate limit or an in-window duplicate is NOT charged: the guard
915            // exists because in a mutual loop neither party is misbehaving, and
916            // pricing that as misconduct would penalise correct behaviour. What
917            // is charged is what the sender chose — a chain too deep, an
918            // oversized body, an illegal name.
919            let attributable = matches!(
920                verdict,
921                car_peers::GuardVerdict::HopLimit { .. }
922                    | car_peers::GuardVerdict::TooLarge { .. }
923                    | car_peers::GuardVerdict::InvalidName { .. }
924            );
925            if attributable {
926                record_standing(&state, &from, Some(&verdict.reason()), Some(&msg.via)).await;
927            }
928            let outcome = DeliveryOutcome::Refused {
929                reason: verdict.reason(),
930            };
931            append_peer_audit_dir(
932                &state,
933                &msg,
934                &target,
935                &outcome,
936                PeerAuditDir::In,
937                Some(&inbound.peer_pubkey),
938            );
939            return Err(guard_error(&verdict));
940        }
941
942        let result = deliver(&state, &target, &msg).await;
943        release(&state, &target.name).await;
944
945        // Do not flatten. `deliver` reports `unacknowledged` when the frame was
946        // written but the agent never answered inside the ack window, and
947        // collapsing that to `delivered` would put a delivery that did not
948        // happen into the inbound audit row and send the same false verdict
949        // back across the hop.
950        let reported = match &result {
951            Ok(v) => v
952                .get("outcome")
953                .and_then(|o| o.as_str())
954                .unwrap_or("delivered")
955                .to_string(),
956            Err(_) => "failed".to_string(),
957        };
958        // Do not squeeze `unacknowledged` through `Refused`. That variant's
959        // contract is "dropped, never delivered", and the frame demonstrably
960        // was written — recording it as a refusal would make the receiving
961        // host's journal contradict the sending host's report about the same
962        // message, with the durability claim pointing the wrong way.
963        let outcome = match &result {
964            Ok(_) if reported == "delivered" => DeliveryOutcome::Delivered,
965            Ok(v) => DeliveryOutcome::Unacknowledged {
966                detail: v
967                    .get("detail")
968                    .and_then(|d| d.as_str())
969                    .unwrap_or(reported.as_str())
970                    .to_string(),
971            },
972            Err(reason) => DeliveryOutcome::Refused {
973                reason: reason.clone(),
974            },
975        };
976        append_peer_audit_dir(
977            &state,
978            &msg,
979            &target,
980            &outcome,
981            PeerAuditDir::In,
982            Some(&inbound.peer_pubkey),
983        );
984
985        // A message that was admitted and then could not be delivered gives
986        // back its dedupe record. Otherwise the sender's honest retry is
987        // refused as "already delivered" — false, and it is on another host,
988        // so it cannot see why.
989        if result.is_err() {
990            if let Some(g) = state.peer_guards.lock().await.get_mut(&target.name) {
991                g.forget(&msg);
992            }
993        }
994        // A success is an acknowledged delivery, nothing weaker. An
995        // `unacknowledged` result is the RECIPIENT's agent not answering, which
996        // says nothing about the sender's conduct, so it moves no counter in
997        // either direction.
998        if matches!(outcome, DeliveryOutcome::Delivered) {
999            record_standing(&state, &from, None, None).await;
1000        }
1001
1002        // Hand back what `deliver` actually reported, structure intact, so the
1003        // sending host records the same verdict this one did rather than a
1004        // lossy rendering of it.
1005        result
1006    }
1007}
1008
1009/// Undo the dedupe record for a synchronous turn that was admitted and then
1010/// could not be delivered — the agent is not attached, or it raced a
1011/// disconnect. Without it, an immediate retry is refused as a duplicate and
1012/// told the message "was already delivered", which is false.
1013pub(crate) async fn forget_synchronous_turn(
1014    state: &ServerState,
1015    principal: &str,
1016    target_agent: &str,
1017    body: &str,
1018) {
1019    let msg = PeerMessage::new(principal, target_agent, body);
1020    if let Some(g) = state.peer_guards.lock().await.get_mut(target_agent) {
1021        g.forget(&msg);
1022    }
1023}
1024
1025/// A [`PeerDescriptor`] for a local attached agent, so a synchronous turn's
1026/// refusal lands in the same audit journal, in the same shape, as a refused
1027/// `agents.message`. One reader, one format.
1028fn local_agent_descriptor(agent_id: &str) -> PeerDescriptor {
1029    PeerDescriptor {
1030        name: agent_id.to_string(),
1031        reference: None,
1032        kind: car_peers::PeerKind::CarAgent,
1033        source: car_peers::PeerSource::Attached,
1034        address: car_peers::PeerAddress::AttachedAgent {
1035            agent_id: agent_id.to_string(),
1036        },
1037        display_name: None,
1038        capability: None,
1039        last_seen_ms: Some(car_peers::now_ms()),
1040        // A local agent has no signing key; its standing keys on `agent:<id>`.
1041        pubkey: None,
1042    }
1043}
1044
1045pub async fn handle_agents_message(
1046    req: &crate::handler::JsonRpcMessage,
1047    state: &ServerState,
1048    session: &ClientSession,
1049) -> Result<Value, String> {
1050    let to = req
1051        .params
1052        .get("to")
1053        .and_then(|v| v.as_str())
1054        .ok_or("missing `to`")?
1055        .to_string();
1056    let body = req
1057        .params
1058        .get("body")
1059        .and_then(|v| v.as_str())
1060        .ok_or("missing `body`")?
1061        .to_string();
1062    let summary = req
1063        .params
1064        .get("summary")
1065        .and_then(|v| v.as_str())
1066        .map(|s| s.to_string());
1067
1068    // Server-derived. A caller-supplied sender would make the audit forgeable.
1069    let from = crate::handler::session_principal_for_peers(session).await;
1070
1071    let dir = directory_for(state, session).await;
1072    let target = dir.resolve(&to).map_err(|e| e.to_string())?;
1073
1074    peer_reachability(&target)?;
1075
1076    let mut msg = PeerMessage::new(&from, &target.name, &body);
1077    msg.summary = summary;
1078
1079    // Stage 0: standing. Host-scoped and ahead of the per-recipient guard, for
1080    // the same reason it runs first inbound — a degraded sender must not be
1081    // able to collect a fresh budget by switching recipients.
1082    if let StandingVerdict::Throttled { reason } = standing_gate(state, &from).await {
1083        return Err(reason);
1084    }
1085
1086    // Stage 1: the channel guard. Runs before policy because it is cheap and
1087    // because a message loop must terminate even when both ends are fully
1088    // authorized.
1089    let verdict = {
1090        let mut guards = state.peer_guards.lock().await;
1091        let guard = guards
1092            .entry(target.name.clone())
1093            .or_insert_with(DeliveryGuard::new);
1094        guard.admit(&msg, car_peers::now_ms())
1095    };
1096    if !verdict.is_accept() {
1097        let outcome = DeliveryOutcome::Refused {
1098            reason: verdict.reason(),
1099        };
1100        append_peer_audit(state, &msg, &target, &outcome);
1101        return Err(guard_error(&verdict));
1102    }
1103
1104    // Stage 2: admission. Whether this sender may say this to this recipient.
1105    let outcome = admit(state, session, &msg, &target).await;
1106    append_peer_audit(state, &msg, &target, &outcome);
1107    match &outcome {
1108        DeliveryOutcome::Refused { reason } => {
1109            release(state, &target.name).await;
1110            return Err(format!("message refused: {reason}"));
1111        }
1112        // `admit` reports on whether to try, never on the attempt, so this
1113        // cannot arise from it. Matched rather than wildcarded so an admission
1114        // that could one day produce it must come back and choose.
1115        DeliveryOutcome::Unacknowledged { .. } => {}
1116        DeliveryOutcome::Held { reason } => {
1117            // Release the in-flight slot and move the message to the hold queue.
1118            // The two bounds are separate on purpose: QUEUE_CAP limits
1119            // outstanding delivery, HOLD_CAP limits what an operator has
1120            // yet to decide. Charging a held message against the delivery queue
1121            // would let a slow human block a healthy channel.
1122            release(state, &target.name).await;
1123            let held = HeldPeerMessage {
1124                message: msg.clone(),
1125                target: target.clone(),
1126                held_at_ms: car_peers::now_ms(),
1127                reason: reason.clone(),
1128            };
1129            let dropped = {
1130                let mut q = state.held_peer_messages.lock().await;
1131                q.push_back(held);
1132                if q.len() > car_peers::HOLD_CAP {
1133                    q.pop_front()
1134                } else {
1135                    None
1136                }
1137            };
1138            if let Some(evicted) = dropped {
1139                // Say so rather than losing it quietly: the operator never saw
1140                // this one, and the sender was told it was retained.
1141                tracing::warn!(
1142                    id = %evicted.message.id,
1143                    from = %evicted.message.from,
1144                    to = %evicted.target.name,
1145                    "hold queue full; dropped the oldest undecided peer message"
1146                );
1147                append_peer_audit(
1148                    state,
1149                    &evicted.message,
1150                    &evicted.target,
1151                    &DeliveryOutcome::Refused {
1152                        reason: format!(
1153                            "evicted from the hold queue at {} undecided messages",
1154                            car_peers::HOLD_CAP
1155                        ),
1156                    },
1157                );
1158            }
1159            return Ok(serde_json::json!({
1160                "id": msg.id,
1161                "to": target.name,
1162                "outcome": "held",
1163                "retained": true,
1164                "reason": reason,
1165            }));
1166        }
1167        DeliveryOutcome::Delivered => {}
1168    }
1169
1170    let result = deliver(state, &target, &msg).await;
1171    settle_delivery_slot(state, &target, result.is_ok()).await;
1172
1173    // A second row, on the ATTEMPT. The row above records the admission — that
1174    // this host decided to try — and until a far side could refuse, that was
1175    // the whole story. It is not any more: a cross-host message the remote
1176    // broker rejects returns `Err` here, and with only the admission row the
1177    // sending operator's journal would say `Delivered` about a message the
1178    // other host threw away. Two rows for one message is the honest shape,
1179    // because two decisions were made, in two places, and an incident needs
1180    // both.
1181    match &result {
1182        Ok(v) => {
1183            let reported = v.get("outcome").and_then(|o| o.as_str()).unwrap_or("");
1184            // `delivered` is already implied by the admission row; only record
1185            // an attempt that ended somewhere else.
1186            if reported != "delivered" {
1187                append_peer_audit(
1188                    state,
1189                    &msg,
1190                    &target,
1191                    &DeliveryOutcome::Unacknowledged {
1192                        detail: v
1193                            .get("detail")
1194                            .and_then(|d| d.as_str())
1195                            .unwrap_or(reported)
1196                            .to_string(),
1197                    },
1198                );
1199            }
1200        }
1201        Err(reason) => append_peer_audit(
1202            state,
1203            &msg,
1204            &target,
1205            &DeliveryOutcome::Refused {
1206                reason: reason.clone(),
1207            },
1208        ),
1209    }
1210    result
1211}
1212
1213/// `agents.message.pending` — peer messages awaiting an operator decision.
1214///
1215/// Host-only, mirroring `agents.chat.approve`: an agent must not be able to
1216/// read, or later approve, the queue that exists to gate it.
1217pub async fn handle_agents_message_pending(
1218    state: &ServerState,
1219    session: &ClientSession,
1220) -> Result<Value, String> {
1221    require_host(session, "agents.message.pending")?;
1222    Ok(pending_snapshot(state).await)
1223}
1224
1225/// [`handle_agents_message_pending`] without the host check.
1226///
1227/// Split so the queue's behaviour is testable without constructing a live
1228/// client session; the host check is tested directly on [`require_host`].
1229pub async fn pending_snapshot(state: &ServerState) -> Value {
1230    let q = state.held_peer_messages.lock().await;
1231    serde_json::json!({
1232        "held": q.iter().map(|h| serde_json::json!({
1233            "id": h.message.id,
1234            "from": h.message.from,
1235            "to": h.target.name,
1236            "body": h.message.body,
1237            "held_at_ms": h.held_at_ms,
1238            "reason": h.reason,
1239        })).collect::<Vec<_>>(),
1240        "count": q.len(),
1241        "cap": car_peers::HOLD_CAP,
1242    })
1243}
1244
1245/// `agents.message.approve` — release or drop one held message.
1246///
1247/// Params: `{ id, decision }`. `decision` is a bool, or a string the operator
1248/// surface finds natural (`approve`/`approved`/`yes`); anything else denies,
1249/// and an omitted decision denies. Same convention as `agents.chat.approve`, so
1250/// an operator does not have to remember two.
1251pub async fn handle_agents_message_approve(
1252    req: &crate::handler::JsonRpcMessage,
1253    state: &ServerState,
1254    session: &ClientSession,
1255) -> Result<Value, String> {
1256    require_host(session, "agents.message.approve")?;
1257    let id = req
1258        .params
1259        .get("id")
1260        .and_then(|v| v.as_str())
1261        .ok_or("missing `id`")?
1262        .to_string();
1263    let approved = match req.params.get("decision") {
1264        Some(Value::Bool(b)) => *b,
1265        Some(Value::String(sv)) => {
1266            matches!(
1267                sv.to_ascii_lowercase().as_str(),
1268                "approve" | "approved" | "yes"
1269            )
1270        }
1271        _ => false,
1272    };
1273
1274    decide_held(state, &id, approved).await
1275}
1276
1277/// [`handle_agents_message_approve`] without the host check. See
1278/// [`pending_snapshot`] for why the split exists.
1279pub async fn decide_held(state: &ServerState, id: &str, approved: bool) -> Result<Value, String> {
1280    let held = {
1281        let mut q = state.held_peer_messages.lock().await;
1282        let pos = q.iter().position(|h| h.message.id == id);
1283        match pos {
1284            Some(i) => q.remove(i).expect("position just found"),
1285            None => return Err(format!("no held message with id `{id}`")),
1286        }
1287    };
1288
1289    if !approved {
1290        // Ground truth. Every other failure signal is this runtime inferring
1291        // misconduct from shape; here a human looked at the message and said
1292        // no, which is the strongest evidence standing can have.
1293        record_standing(
1294            state,
1295            &held.message.from,
1296            Some("denied by the operator"),
1297            Some(&held.message.via),
1298        )
1299        .await;
1300        append_peer_audit(
1301            state,
1302            &held.message,
1303            &held.target,
1304            &DeliveryOutcome::Refused {
1305                reason: "denied by the operator".into(),
1306            },
1307        );
1308        return Ok(serde_json::json!({
1309            "id": id,
1310            "outcome": "denied",
1311        }));
1312    }
1313
1314    // Re-admit through the channel guard. The message passed it when it was
1315    // sent, but time has moved and the recipient may since have been flooded;
1316    // the guard bounds the channel, and an approval is not a licence to bypass
1317    // it. Its identical-repeat window has long since expired for anything that
1318    // sat awaiting a human, so this does not spuriously reject.
1319    let verdict = {
1320        let mut guards = state.peer_guards.lock().await;
1321        guards
1322            .entry(held.target.name.clone())
1323            .or_insert_with(DeliveryGuard::new)
1324            .admit(&held.message, car_peers::now_ms())
1325    };
1326    if !verdict.is_accept() {
1327        append_peer_audit(
1328            state,
1329            &held.message,
1330            &held.target,
1331            &DeliveryOutcome::Refused {
1332                reason: verdict.reason(),
1333            },
1334        );
1335        return Err(guard_error(&verdict));
1336    }
1337
1338    append_peer_audit(
1339        state,
1340        &held.message,
1341        &held.target,
1342        &DeliveryOutcome::Delivered,
1343    );
1344    let result = deliver(state, &held.target, &held.message).await;
1345    settle_delivery_slot(state, &held.target, result.is_ok()).await;
1346    result
1347}
1348
1349/// Refuse a surface that only the operator's own client may drive.
1350///
1351/// Separate helper because the reason matters more than the check: these two
1352/// methods exist to gate agents, so an agent reaching them would be approving
1353/// the very messages its posture was set to hold.
1354fn require_host(session: &ClientSession, method: &str) -> Result<(), String> {
1355    if session.is_host.load(std::sync::atomic::Ordering::Acquire) {
1356        return Ok(());
1357    }
1358    Err(require_host_message(method))
1359}
1360
1361/// The refusal text for a host-only peer surface.
1362fn require_host_message(method: &str) -> String {
1363    format!("`{method}` is host-only; an agent cannot approve the messages its own posture held")
1364}
1365
1366/// Decrement the recipient's outstanding-delivery count.
1367async fn release(state: &ServerState, recipient: &str) {
1368    if let Some(g) = state.peer_guards.lock().await.get_mut(recipient) {
1369        g.consumed();
1370    }
1371}
1372
1373/// Release an in-flight slot unless a successful MCP enqueue now owns it.
1374///
1375/// Attached/A2A delivery slots last only for the round trip. An MCP inbox slot
1376/// represents an unread queued message and is released by `peer_inbox` instead.
1377async fn settle_delivery_slot(state: &ServerState, target: &PeerDescriptor, delivered: bool) {
1378    if !delivered || !matches!(target.address, PeerAddress::McpSession { .. }) {
1379        release(state, &target.name).await;
1380    }
1381}
1382
1383/// Turn a guard verdict into the caller-facing error.
1384///
1385/// Named separately so the sender is told *which* limit stopped it and can act:
1386/// batching is the answer to a rate limit, waiting is the answer to a full
1387/// queue, and neither is the answer to an oversized body.
1388fn guard_error(v: &GuardVerdict) -> String {
1389    match v {
1390        GuardVerdict::Accept => "accepted".into(),
1391        GuardVerdict::TooLarge { .. } => {
1392            format!("{} — send a path or a state handle instead", v.reason())
1393        }
1394        GuardVerdict::RateLimited { .. } => {
1395            format!(
1396                "{} — batch the rest into one message. Inbound, this budget is \
1397                 per remote DAEMON, not per remote agent: the host key is the \
1398                 only principal a receiver can verify, so every agent on that \
1399                 host shares it.",
1400                v.reason()
1401            )
1402        }
1403        GuardVerdict::DuplicateWithinWindow => {
1404            format!("{} — it was already delivered; do not resend", v.reason())
1405        }
1406        GuardVerdict::QueueFull { .. } => {
1407            format!("{} — wait for it to drain", v.reason())
1408        }
1409        GuardVerdict::InvalidName { .. } => v.reason(),
1410        GuardVerdict::HopLimit { .. } => {
1411            format!(
1412                "{} — this chain has been forwarded far enough; act on it or \
1413                 answer the originator directly rather than passing it on",
1414                v.reason()
1415            )
1416        }
1417    }
1418}
1419
1420/// Admission: may this sender say this to this recipient?
1421///
1422/// Resolved against [`car_policy::AgentPermissionPolicy`] at the
1423/// [`PermissionTier::ReadOnly`] tier, because that is honestly what a peer
1424/// message is: it mutates nothing on the recipient, reaches no executor, and
1425/// grants no authority. Rating it higher would be theatre — and rating it lower
1426/// than a tier at all would leave operators no knob.
1427///
1428/// The tier is resolved for the **sender**. The question a peer message raises
1429/// is whether this agent may talk to other agents, which is the sender's
1430/// authority; what the recipient then does is gated by the recipient's own
1431/// runtime, unchanged.
1432///
1433/// Under the Balanced preset `ReadOnly` is `AlwaysAllow`, so the default is
1434/// permissive. The value is that an operator who sets a specific agent's
1435/// `ReadOnly` posture to `Deny` actually stops its peer messages, rather than
1436/// the rule living only in a system prompt the agent may or may not follow.
1437async fn admit(
1438    _state: &ServerState,
1439    session: &ClientSession,
1440    msg: &PeerMessage,
1441    _target: &PeerDescriptor,
1442) -> DeliveryOutcome {
1443    let sender_agent = session.agent_id.lock().await.clone();
1444    let is_host = session.is_host.load(std::sync::atomic::Ordering::Acquire);
1445    admit_with(
1446        &crate::agent_permissions::load_policy(),
1447        sender_agent,
1448        is_host,
1449        &msg.from,
1450    )
1451}
1452
1453/// [`admit`] against an explicit policy.
1454///
1455/// Split so the authorization branches can be tested without writing a policy
1456/// file under `CAR_HOME`, which is process-global and would race the rest of the
1457/// test binary. An untested authorization path is the one kind that must not
1458/// ship on a compile alone.
1459fn admit_with(
1460    policy: &car_policy::AgentPermissionPolicy,
1461    sender_agent: Option<String>,
1462    is_host: bool,
1463    from: &str,
1464) -> DeliveryOutcome {
1465    let Some(agent_id) = sender_agent else {
1466        if is_host {
1467            // The host is the operator's own client; it needs no agent posture.
1468            return DeliveryOutcome::Delivered;
1469        }
1470        return DeliveryOutcome::Refused {
1471            reason: format!(
1472                "sender `{from}` is neither a bound agent nor the host; a peer message needs an authenticated principal"
1473            ),
1474        };
1475    };
1476
1477    match policy.resolve(&agent_id, car_policy::PermissionTier::ReadOnly) {
1478        car_policy::agent_permissions::ApprovalMode::AlwaysAllow => DeliveryOutcome::Delivered,
1479        car_policy::agent_permissions::ApprovalMode::RequireApproval => DeliveryOutcome::Held {
1480            reason: format!(
1481                "`{agent_id}` is set to require approval; the message is held rather than dropped"
1482            ),
1483        },
1484        car_policy::agent_permissions::ApprovalMode::Deny => DeliveryOutcome::Refused {
1485            reason: format!("`{agent_id}` is denied at the read_only tier"),
1486        },
1487    }
1488}
1489
1490/// Reverse-call the recipient's attached channel.
1491async fn deliver(
1492    state: &ServerState,
1493    target: &PeerDescriptor,
1494    msg: &PeerMessage,
1495) -> Result<Value, String> {
1496    let agent_id = match &target.address {
1497        PeerAddress::AttachedAgent { agent_id } => agent_id,
1498        PeerAddress::McpSession { session_id } => {
1499            return enqueue_mcp_message(state, session_id, target, msg).await;
1500        }
1501        PeerAddress::A2a { base_url } => return deliver_remote(state, base_url, target, msg).await,
1502    };
1503
1504    let agent_client_id = state
1505        .attached_agents
1506        .lock()
1507        .await
1508        .get(agent_id)
1509        .cloned()
1510        .ok_or_else(|| format!("agent `{agent_id}` detached before the message could be sent"))?;
1511    let channel = {
1512        let sessions = state.sessions.lock().await;
1513        sessions
1514            .get(&agent_client_id)
1515            .map(|s| s.channel.clone())
1516            .ok_or_else(|| format!("agent `{agent_id}` raced with disconnect"))?
1517    };
1518
1519    let request_id = channel.next_request_id();
1520    let (tx, rx) = oneshot::channel();
1521    channel.pending.lock().await.insert(request_id.clone(), tx);
1522
1523    let rpc = serde_json::json!({
1524        "jsonrpc": "2.0",
1525        "method": "agent.peer_message",
1526        "params": {
1527            "id": msg.id,
1528            "from": msg.from,
1529            "body": msg.body,
1530            "sent_at_ms": msg.sent_at_ms,
1531            "no_reply": msg.no_reply,
1532        },
1533        "id": request_id,
1534    });
1535    let frame = Message::Text(
1536        serde_json::to_string(&rpc)
1537            .map_err(|e| e.to_string())?
1538            .into(),
1539    );
1540
1541    if let Err(e) = channel.write.lock().await.send(frame).await {
1542        channel.pending.lock().await.remove(&request_id);
1543        return Err(format!("failed to deliver to `{agent_id}`: {e}"));
1544    }
1545
1546    match tokio::time::timeout(std::time::Duration::from_secs(PEER_ACK_TIMEOUT_SECS), rx).await {
1547        Ok(Ok(_)) => Ok(serde_json::json!({
1548            "id": msg.id,
1549            "to": target.name,
1550            "outcome": "delivered",
1551        })),
1552        Ok(Err(_)) => Err(format!("agent `{agent_id}` closed before acknowledging")),
1553        Err(_) => {
1554            // Timed out: stop waiting, but do not claim non-delivery. The frame
1555            // was written; an agent that does not implement `agent.peer_message`
1556            // simply never answers, and saying "not delivered" would be a guess.
1557            channel.pending.lock().await.remove(&request_id);
1558            Ok(serde_json::json!({
1559                "id": msg.id,
1560                "to": target.name,
1561                "outcome": "unacknowledged",
1562                "detail": format!(
1563                    "written to `{agent_id}` but not acknowledged within {PEER_ACK_TIMEOUT_SECS}s"
1564                ),
1565            }))
1566        }
1567    }
1568}
1569
1570/// Queue one admitted message for a live MCP session to poll.
1571///
1572/// The caller has already taken a [`DeliveryGuard`] slot. Unlike the attached
1573/// WebSocket path, this slot remains occupied until `peer_inbox` drains the
1574/// message, so [`car_peers::QUEUE_CAP`] is a real unread-inbox bound here.
1575async fn enqueue_mcp_message(
1576    state: &ServerState,
1577    session_id: &str,
1578    target: &PeerDescriptor,
1579    msg: &PeerMessage,
1580) -> Result<Value, String> {
1581    let mut sessions = state.mcp_peer_sessions.lock().await;
1582    let session = sessions
1583        .get_mut(session_id)
1584        .ok_or_else(|| format!("MCP session `{}` disconnected before delivery", target.name))?;
1585    if !session.receive_capable {
1586        return Err(format!("MCP session `{}` is send-only", target.name));
1587    }
1588    session.inbox.push_back(msg.clone());
1589    Ok(serde_json::json!({
1590        "id": msg.id,
1591        "to": target.name,
1592        "outcome": "delivered",
1593        "delivery": "queued_for_poll",
1594    }))
1595}
1596
1597/// Drain messages for the MCP session on the current request.
1598async fn drain_mcp_inbox(
1599    state: &ServerState,
1600    principal: &str,
1601    limit: usize,
1602) -> Result<Value, car_mcp::ToolError> {
1603    let session_id = principal
1604        .strip_prefix("mcp:")
1605        .ok_or_else(|| car_mcp::ToolError::Internal("invalid MCP peer principal".into()))?;
1606
1607    let messages = {
1608        let mut sessions = state.mcp_peer_sessions.lock().await;
1609        let session = sessions.get_mut(session_id).ok_or_else(|| {
1610            car_mcp::ToolError::Internal("MCP peer session expired; reconnect".into())
1611        })?;
1612        if !session.receive_capable {
1613            return Err(car_mcp::ToolError::Internal(
1614                "CAR-spawned batch CLI sessions are send-only".into(),
1615            ));
1616        }
1617        session.last_seen_ms = car_peers::now_ms();
1618        let take = limit.min(session.inbox.len());
1619        session.inbox.drain(..take).collect::<Vec<_>>()
1620    };
1621
1622    if !messages.is_empty() {
1623        let mut guards = state.peer_guards.lock().await;
1624        if let Some(guard) = guards.get_mut(principal) {
1625            for _ in 0..messages.len() {
1626                guard.consumed();
1627            }
1628        }
1629    }
1630    Ok(serde_json::json!({
1631        "self": principal,
1632        "messages": messages,
1633        "count": messages.len(),
1634    }))
1635}
1636
1637/// Deliver to a CAR daemon on another host, over A2A.
1638///
1639/// The message is addressed to the remote **daemon**, not to one of its agents.
1640/// That is what makes a second admission *possible* on the far side, and it is
1641/// why `PeerAddress` has no variant for a remote agent.
1642///
1643/// That second admission now exists: [`PeerInboundBroker`] reads `carPeerTo`,
1644/// rebuilds the sender from the *verified* signing key rather than the name the
1645/// caller claimed, applies the recipient's own channel guard, refuses to relay
1646/// onward, and writes an inbound audit row. So a `Delivered` outcome here means
1647/// the far side's broker admitted the message and its agent was reverse-called
1648/// — not merely that the bytes were accepted.
1649///
1650/// What it still does not mean: that the far side graded the sender against any
1651/// per-peer posture. There is no local sender to resolve inbound, and a
1652/// remote-supplied name must never select which local policy row governs it, so
1653/// the operator's control there remains the trust set — not a policy lookup.
1654///
1655/// Errors are reported as delivery failures rather than swallowed: a peer that
1656/// is advertised but unreachable is exactly the case an operator needs to see,
1657/// and a network that silently drops messages is worse than one that refuses
1658/// them.
1659async fn deliver_remote(
1660    state: &ServerState,
1661    base_url: &str,
1662    target: &PeerDescriptor,
1663    msg: &PeerMessage,
1664) -> Result<Value, String> {
1665    use car_a2a::types::{Message as A2aMessage, MessageRole, Part, TextPart};
1666
1667    // Sign as this daemon. Without an identity the peer will refuse us, so say
1668    // that here rather than letting it surface as an opaque 401 from the far
1669    // side — the operator's fix is local, not remote.
1670    let identity = {
1671        state
1672            .peer_identity
1673            .lock()
1674            .unwrap_or_else(|e| e.into_inner())
1675            .clone()
1676    };
1677    let Some(identity) = identity else {
1678        return Err(format!(
1679            "cannot reach `{}`: this daemon has no peer identity, so a remote CAR would \
1680             refuse it. The identity is created when the A2A surface starts.",
1681            target.name
1682        ));
1683    };
1684    let client = car_a2a::client::A2aClient::new(base_url).with_peer_identity(identity);
1685    // The sender travels in metadata, not in the body: a recipient must be able
1686    // to tell who sent a message without parsing prose, and the body stays the
1687    // author's text verbatim. Both keys are surfaced camelCase, matching the
1688    // `correlationId`/`replyTo` convention the type's own docs pin.
1689    let mut metadata = std::collections::HashMap::new();
1690    // Sent for operator-visible sender labelling. The receiving broker
1691    // deliberately does NOT read it for routing or attribution — it rebuilds
1692    // the sender from the key it verified — so this is a display string, not a
1693    // credential, and it is written with the constant so the two ends of the
1694    // wire cannot drift apart on spelling.
1695    metadata.insert(
1696        car_a2a::PEER_FROM_KEY.to_string(),
1697        Value::from(msg.from.clone()),
1698    );
1699    // Lineage crosses the hop here, inside the body the signature hashes, so a
1700    // proxy cannot alter it in flight. What the far side does with it is
1701    // attest-not-verify: it appends its own boundary marker rather than
1702    // trusting this prefix.
1703    if !msg.trace.is_empty() {
1704        metadata.insert(
1705            car_a2a::PEER_TRACE_KEY.to_string(),
1706            Value::from(msg.trace.clone()),
1707        );
1708    }
1709    if !msg.via.is_empty() {
1710        metadata.insert(
1711            car_a2a::PEER_VIA_KEY.to_string(),
1712            Value::from(msg.via.clone()),
1713        );
1714    }
1715    metadata.insert(
1716        car_a2a::PEER_TO_KEY.to_string(),
1717        Value::from(target.name.clone()),
1718    );
1719    let a2a_msg = A2aMessage {
1720        message_id: msg.id.clone(),
1721        role: MessageRole::User,
1722        parts: vec![Part::Text(TextPart {
1723            text: msg.body.clone(),
1724            metadata: std::collections::HashMap::new(),
1725        })],
1726        task_id: None,
1727        context_id: None,
1728        metadata,
1729    };
1730
1731    match client.send_message(a2a_msg, true).await {
1732        // Read the far side's verdict rather than assuming one. A receiving
1733        // broker answers `carPeerOutcome`, and it is not always `delivered`:
1734        // an agent that never acknowledges inside the ack window yields
1735        // `unacknowledged` there, and writing `delivered` here would record a
1736        // delivery that did not happen — on the one journal an operator reads
1737        // during an incident. A peer that predates the broker sends no outcome
1738        // at all, so the fallback is the honest, weaker claim.
1739        Ok(result) => {
1740            // The far side's verdict, structure intact. A peer that predates
1741            // the broker sends none, and `accepted` is then the honest weaker
1742            // claim: the bytes were taken, and nothing is known about what
1743            // happened to them.
1744            let reported = match &result {
1745                car_a2a::types::SendMessageResult::Message(m) => {
1746                    m.metadata.get(car_a2a::PEER_OUTCOME_KEY).cloned()
1747                }
1748                _ => None,
1749            };
1750            let mut out = serde_json::json!({
1751                "id": msg.id,
1752                "to": target.name,
1753                "outcome": "accepted",
1754                "remote_reported": reported.is_some(),
1755                "transport": "a2a",
1756                "url": base_url,
1757            });
1758            if let Some(remote) = reported {
1759                if let Some(obj) = remote.as_object() {
1760                    for (k, v) in obj {
1761                        out[k.as_str()] = v.clone();
1762                    }
1763                } else if let Some(s) = remote.as_str() {
1764                    // A peer on the earlier flat contract.
1765                    out["outcome"] = serde_json::Value::from(s);
1766                }
1767            }
1768            Ok(out)
1769        }
1770        Err(e) => Err(format!(
1771            "failed to deliver to `{}` at {base_url}: {e}",
1772            target.name
1773        )),
1774    }
1775}
1776
1777/// Append a peer-message record to the configured state's audit journal.
1778///
1779/// Best-effort and non-fatal, mirroring `append_external_agent_audit`: an
1780/// unwritable journal must not fail the call, but every attempted delivery —
1781/// refused ones included — leaves a record. The path was resolved when
1782/// `ServerState` was built; this write must never re-read process-global
1783/// `CAR_HOME`.
1784pub fn append_peer_audit(
1785    state: &ServerState,
1786    msg: &PeerMessage,
1787    target: &PeerDescriptor,
1788    outcome: &DeliveryOutcome,
1789) {
1790    append_peer_audit_dir(state, msg, target, outcome, PeerAuditDir::Out, None);
1791}
1792
1793/// Which way a message was travelling when this row was written.
1794///
1795/// Every row CAR wrote before the receiving-side broker existed was outbound —
1796/// there was no inbound admission to record. A reader of
1797/// `~/.car/peer-messages.jsonl` therefore cannot tell "we sent this" from "a
1798/// peer sent us this" without the column, and the two are very different
1799/// questions during an incident.
1800#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1801pub enum PeerAuditDir {
1802    /// This host sent it.
1803    Out,
1804    /// This host received and admitted (or refused) it.
1805    In,
1806}
1807
1808impl PeerAuditDir {
1809    fn as_str(self) -> &'static str {
1810        match self {
1811            PeerAuditDir::Out => "out",
1812            PeerAuditDir::In => "in",
1813        }
1814    }
1815}
1816
1817/// [`append_peer_audit`] with an explicit direction and, inbound, the key that
1818/// was actually verified.
1819///
1820/// `attested_by` is the point of the inbound row. `msg.from` is derived from
1821/// that key rather than from anything the sender claimed, so recording the key
1822/// beside it lets an operator confirm the derivation instead of trusting it.
1823pub fn append_peer_audit_dir(
1824    state: &ServerState,
1825    msg: &PeerMessage,
1826    target: &PeerDescriptor,
1827    outcome: &DeliveryOutcome,
1828    dir: PeerAuditDir,
1829    attested_by: Option<&str>,
1830) {
1831    if let Some(parent) = state.peer_audit_journal.parent() {
1832        if std::fs::create_dir_all(parent).is_err() {
1833            return;
1834        }
1835    }
1836    append_peer_audit_at_dir(
1837        &state.peer_audit_journal,
1838        msg,
1839        target,
1840        outcome,
1841        dir,
1842        attested_by,
1843    );
1844}
1845
1846/// [`append_peer_audit`] against an explicit journal path.
1847///
1848/// Split out so the record shape can be tested without mutating `CAR_HOME`,
1849/// which is process-global and would race every other test in the binary.
1850pub fn append_peer_audit_at(
1851    path: &std::path::Path,
1852    msg: &PeerMessage,
1853    target: &PeerDescriptor,
1854    outcome: &DeliveryOutcome,
1855) {
1856    append_peer_audit_at_dir(path, msg, target, outcome, PeerAuditDir::Out, None);
1857}
1858
1859/// [`append_peer_audit_at`] with an explicit direction and attestation.
1860pub fn append_peer_audit_at_dir(
1861    path: &std::path::Path,
1862    msg: &PeerMessage,
1863    target: &PeerDescriptor,
1864    outcome: &DeliveryOutcome,
1865    dir: PeerAuditDir,
1866    attested_by: Option<&str>,
1867) {
1868    use std::io::Write;
1869    let mut record = serde_json::json!({
1870        "ts": chrono::Utc::now().to_rfc3339(),
1871        "id": msg.id,
1872        "from": msg.from,
1873        "to": target.name,
1874        "kind": target.kind.as_str(),
1875        "source": target.source.as_str(),
1876        "bytes": msg.body.len(),
1877        "outcome": outcome,
1878        "dir": dir.as_str(),
1879        "trace": msg.trace,
1880        "via": msg.via,
1881    });
1882    if let Some(key) = attested_by {
1883        record["attested_by"] = serde_json::Value::from(key);
1884    }
1885    let Ok(line) = serde_json::to_string(&record) else {
1886        return;
1887    };
1888    if let Ok(mut f) = std::fs::OpenOptions::new()
1889        .create(true)
1890        .append(true)
1891        .open(path)
1892    {
1893        let _ = writeln!(f, "{line}");
1894    } else {
1895        tracing::warn!(path = %path.display(), "failed to append peer-message audit record");
1896    }
1897}
1898
1899/// Record an `agents.chat` reverse-call.
1900///
1901/// `agents.chat` has driven another agent's turn since it shipped, with no
1902/// `is_host` gate at dispatch or inside the handler and — verified by grep over
1903/// the whole handler range — no eventlog, policy, or audit call of any kind,
1904/// while its sibling `agents.invoke_external` gets
1905/// `append_external_agent_audit`. That made agent-to-agent messaging shipped,
1906/// ungoverned behaviour rather than a design option.
1907///
1908/// Adding a governed `agents.message` beside an unrecorded `agents.chat` would
1909/// be worse than either alone: it would move well-behaved callers onto the
1910/// audited path and leave the unaudited one as the way to avoid the record. So
1911/// the record lands on both in the same change.
1912///
1913/// This closes the *observability* half for the success path. The authorization
1914/// half landed later: [`admit_turn`] now applies the same guard and policy that
1915/// `agents.message` does, and writes its own audit row for every refusal. What
1916/// this function records is the turn that was *allowed* and dispatched.
1917pub fn append_agent_chat_audit(
1918    state: &ServerState,
1919    principal: &str,
1920    agent_id: &str,
1921    session_id: &str,
1922) {
1923    use std::io::Write;
1924    let path = &state.peer_audit_journal;
1925    if let Some(parent) = path.parent() {
1926        if std::fs::create_dir_all(parent).is_err() {
1927            return;
1928        }
1929    }
1930    let record = serde_json::json!({
1931        "ts": chrono::Utc::now().to_rfc3339(),
1932        "surface": "agents.chat",
1933        "from": principal,
1934        "to": agent_id,
1935        "session_id": session_id,
1936    });
1937    let Ok(line) = serde_json::to_string(&record) else {
1938        return;
1939    };
1940    if let Ok(mut f) = std::fs::OpenOptions::new()
1941        .create(true)
1942        .append(true)
1943        .open(path)
1944    {
1945        let _ = writeln!(f, "{line}");
1946    }
1947}
1948
1949// ---------------------------------------------------------------------------
1950// MCP surface — the return path for external CLIs
1951// ---------------------------------------------------------------------------
1952
1953/// Register `peer_list`, `peer_message`, and `peer_inbox` on the daemon's MCP
1954/// endpoint.
1955///
1956/// The HTTP transport mints a principal on `initialize` and scopes it through
1957/// every later request carrying that MCP session id. Long-lived sessions are
1958/// addressable and drain their bounded queue with `peer_inbox`; CAR-spawned
1959/// batch children identify themselves in the generated MCP URL and remain
1960/// send-only because stdin closes immediately and there is no steady state to
1961/// receive work.
1962///
1963/// Registered here rather than in `car-mcp` for the same reason the assistant
1964/// trio is: the tool list is per-`Server`, so `car-mcp-server` — which has no
1965/// daemon and no connection table — cannot advertise a tool it could not serve.
1966pub fn register_peer_tools(
1967    server: &mut car_mcp::Server,
1968    state: std::sync::Arc<ServerState>,
1969) -> Result<(), car_mcp::RegisterError> {
1970    server.register_tool(
1971        peer_list_schema(),
1972        std::sync::Arc::new(PeerListTool(state.clone())),
1973    )?;
1974    server.register_tool(
1975        peer_message_schema(),
1976        std::sync::Arc::new(PeerMessageTool(state.clone())),
1977    )?;
1978    server.register_tool(
1979        peer_inbox_schema(),
1980        std::sync::Arc::new(PeerInboxTool(state)),
1981    )?;
1982    Ok(())
1983}
1984
1985fn peer_list_schema() -> Value {
1986    serde_json::json!({
1987        "name": "peer_list",
1988        "description": "List the CAR agents and live MCP sessions you can message. Returns \
1989                        this session's own address plus each peer's address, kind, and receive \
1990                        capability. Use an `address` verbatim as peer_message's `to`.",
1991        "inputSchema": { "type": "object", "properties": {} },
1992        "annotations": {
1993            "readOnlyHint": true,
1994            "destructiveHint": false,
1995            "idempotentHint": true,
1996            "openWorldHint": false,
1997        },
1998    })
1999}
2000
2001fn peer_message_schema() -> Value {
2002    serde_json::json!({
2003        "name": "peer_message",
2004        "description": "Send a short plain-text message to one CAR agent — a finding, a status, \
2005                        a decision it is blocked on. The message is text only: it cannot run a \
2006                        command, approve anything, or change the recipient's configuration, and \
2007                        whatever the recipient does about it goes through its own permissions. \
2008                        Get `to` from peer_list. Keep it to one self-contained first line; \
2009                        identical repeats within 10s are dropped.",
2010        "inputSchema": {
2011            "type": "object",
2012            "properties": {
2013                "to": { "type": "string", "description": "An `address` from peer_list." },
2014                "body": { "type": "string", "description": "Plain text. First line should stand alone." },
2015            },
2016            "required": ["to", "body"],
2017        },
2018        "annotations": {
2019            "readOnlyHint": false,
2020            "destructiveHint": false,
2021            "idempotentHint": false,
2022            "openWorldHint": true,
2023        },
2024    })
2025}
2026
2027fn peer_inbox_schema() -> Value {
2028    serde_json::json!({
2029        "name": "peer_inbox",
2030        "description": "Drain messages addressed to this live MCP session. Poll between turns; \
2031                        each message is inert text and grants no authority. CAR-spawned batch \
2032                        CLI sessions are send-only and this tool refuses them.",
2033        "inputSchema": {
2034            "type": "object",
2035            "properties": {
2036                "limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 50 }
2037            }
2038        },
2039        "annotations": {
2040            "readOnlyHint": false,
2041            "destructiveHint": false,
2042            "idempotentHint": false,
2043            "openWorldHint": false,
2044        },
2045    })
2046}
2047
2048struct PeerListTool(std::sync::Arc<ServerState>);
2049
2050#[async_trait::async_trait]
2051impl car_mcp::ToolHandler for PeerListTool {
2052    async fn call(&self, _args: Value) -> Result<String, car_mcp::ToolError> {
2053        let principal = crate::mcp::current_mcp_peer_principal().ok_or_else(|| {
2054            car_mcp::ToolError::Internal(
2055                "peer_list requires an initialized MCP session with MCP-Session-Id".into(),
2056            )
2057        })?;
2058        let mut peers = snapshot_attached(&self.0).await;
2059        peers.extend(snapshot_mcp_sessions(&self.0).await);
2060        peers.retain(|peer| peer.name != principal);
2061        let rows: Vec<Value> = peers
2062            .iter()
2063            .map(|p| {
2064                serde_json::json!({
2065                    "address": p.address_form(),
2066                    "kind": p.kind.as_str(),
2067                    "can_receive": p.kind.can_receive(),
2068                })
2069            })
2070            .collect();
2071        serde_json::to_string(&serde_json::json!({
2072            "self": principal,
2073            "peers": rows,
2074            "count": rows.len()
2075        }))
2076        .map_err(|e| car_mcp::ToolError::Internal(e.to_string()))
2077    }
2078}
2079
2080struct PeerMessageTool(std::sync::Arc<ServerState>);
2081
2082#[async_trait::async_trait]
2083impl car_mcp::ToolHandler for PeerMessageTool {
2084    async fn call(&self, args: Value) -> Result<String, car_mcp::ToolError> {
2085        let principal = crate::mcp::current_mcp_peer_principal().ok_or_else(|| {
2086            car_mcp::ToolError::Internal(
2087                "peer_message requires an initialized MCP session with MCP-Session-Id".into(),
2088            )
2089        })?;
2090        let to = args
2091            .get("to")
2092            .and_then(|v| v.as_str())
2093            .ok_or_else(|| car_mcp::ToolError::InvalidParams("missing `to`".into()))?;
2094        let body = args
2095            .get("body")
2096            .and_then(|v| v.as_str())
2097            .ok_or_else(|| car_mcp::ToolError::InvalidParams("missing `body`".into()))?;
2098
2099        let dir = PeerDirectory::new(&principal)
2100            .with_provider(Box::new(StaticProvider::new(
2101                "attached",
2102                snapshot_attached(&self.0).await,
2103            )))
2104            .with_provider(Box::new(StaticProvider::new(
2105                "mcp",
2106                snapshot_mcp_sessions(&self.0).await,
2107            )));
2108        let target = dir
2109            .resolve(to)
2110            .map_err(|e| car_mcp::ToolError::Internal(e.to_string()))?;
2111        if !target.kind.can_receive() {
2112            return Err(car_mcp::ToolError::Internal(format!(
2113                "`{}` has no inbox to deliver into",
2114                target.name
2115            )));
2116        }
2117
2118        let msg = PeerMessage::new(&principal, &target.name, body);
2119
2120        let verdict = {
2121            let mut guards = self.0.peer_guards.lock().await;
2122            let guard = guards
2123                .entry(target.name.clone())
2124                .or_insert_with(DeliveryGuard::new);
2125            guard.admit(&msg, car_peers::now_ms())
2126        };
2127        if !verdict.is_accept() {
2128            append_peer_audit(
2129                &self.0,
2130                &msg,
2131                &target,
2132                &DeliveryOutcome::Refused {
2133                    reason: verdict.reason(),
2134                },
2135            );
2136            return Err(car_mcp::ToolError::Internal(guard_error(&verdict)));
2137        }
2138
2139        append_peer_audit(&self.0, &msg, &target, &DeliveryOutcome::Delivered);
2140        let result = deliver(&self.0, &target, &msg).await;
2141        settle_delivery_slot(&self.0, &target, result.is_ok()).await;
2142        match result {
2143            Ok(v) => {
2144                serde_json::to_string(&v).map_err(|e| car_mcp::ToolError::Internal(e.to_string()))
2145            }
2146            Err(e) => Err(car_mcp::ToolError::Internal(e)),
2147        }
2148    }
2149}
2150
2151struct PeerInboxTool(std::sync::Arc<ServerState>);
2152
2153#[async_trait::async_trait]
2154impl car_mcp::ToolHandler for PeerInboxTool {
2155    async fn call(&self, args: Value) -> Result<String, car_mcp::ToolError> {
2156        let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(50);
2157        if !(1..=50).contains(&limit) {
2158            return Err(car_mcp::ToolError::InvalidParams(
2159                "`limit` must be between 1 and 50".into(),
2160            ));
2161        }
2162        let principal = crate::mcp::current_mcp_peer_principal().ok_or_else(|| {
2163            car_mcp::ToolError::Internal(
2164                "peer_inbox requires an initialized MCP session with MCP-Session-Id".into(),
2165            )
2166        })?;
2167        let value = drain_mcp_inbox(&self.0, &principal, limit as usize).await?;
2168        serde_json::to_string(&value).map_err(|e| car_mcp::ToolError::Internal(e.to_string()))
2169    }
2170}
2171
2172#[cfg(test)]
2173mod tests {
2174    use super::*;
2175    use std::sync::Arc;
2176
2177    async fn test_state() -> (Arc<ServerState>, tempfile::TempDir) {
2178        let temp = tempfile::tempdir().unwrap();
2179        let state = Arc::new(ServerState::with_config(
2180            crate::session::ServerStateConfig::new(temp.path().to_path_buf()),
2181        ));
2182        (state, temp)
2183    }
2184
2185    async fn attach(state: &ServerState, agent_id: &str) {
2186        state
2187            .attached_agents
2188            .lock()
2189            .await
2190            .insert(agent_id.to_string(), format!("client-{agent_id}"));
2191    }
2192
2193    fn held_fixture(id: &str, to: &str) -> HeldPeerMessage {
2194        let mut m = PeerMessage::new("agent:sender", to, format!("body-{id}"));
2195        m.id = id.to_string();
2196        HeldPeerMessage {
2197            message: m,
2198            target: PeerDescriptor {
2199                name: to.into(),
2200                reference: None,
2201                kind: PeerKind::CarAgent,
2202                source: PeerSource::Attached,
2203                address: PeerAddress::AttachedAgent {
2204                    agent_id: to.into(),
2205                },
2206                display_name: None,
2207                capability: None,
2208                last_seen_ms: None,
2209                pubkey: None,
2210            },
2211            held_at_ms: 1_000,
2212            reason: "requires approval".into(),
2213        }
2214    }
2215
2216    #[tokio::test]
2217    async fn pending_lists_held_messages_oldest_first() {
2218        let (state, _t) = test_state().await;
2219        {
2220            let mut q = state.held_peer_messages.lock().await;
2221            q.push_back(held_fixture("first", "milo"));
2222            q.push_back(held_fixture("second", "milo"));
2223        }
2224        let snap = pending_snapshot(&state).await;
2225        assert_eq!(snap["count"], 2);
2226        assert_eq!(snap["cap"], car_peers::HOLD_CAP);
2227        assert_eq!(snap["held"][0]["id"], "first");
2228        assert_eq!(snap["held"][1]["id"], "second");
2229    }
2230
2231    #[tokio::test]
2232    async fn denying_a_held_message_removes_it() {
2233        let (state, _t) = test_state().await;
2234        state
2235            .held_peer_messages
2236            .lock()
2237            .await
2238            .push_back(held_fixture("m1", "milo"));
2239
2240        let out = decide_held(&state, "m1", false).await.unwrap();
2241        assert_eq!(out["outcome"], "denied");
2242        assert_eq!(
2243            pending_snapshot(&state).await["count"],
2244            0,
2245            "a decided message must leave the queue"
2246        );
2247    }
2248
2249    #[tokio::test]
2250    async fn deciding_an_unknown_id_is_a_named_error() {
2251        let (state, _t) = test_state().await;
2252        let err = decide_held(&state, "ghost", true).await.unwrap_err();
2253        assert!(err.contains("ghost"), "error should name the id: {err}");
2254    }
2255
2256    #[tokio::test]
2257    async fn a_held_message_cannot_be_decided_twice() {
2258        let (state, _t) = test_state().await;
2259        state
2260            .held_peer_messages
2261            .lock()
2262            .await
2263            .push_back(held_fixture("m1", "milo"));
2264        assert!(decide_held(&state, "m1", false).await.is_ok());
2265        // The second decision must fail rather than re-deliver: removal on
2266        // decide is what makes approval idempotent-by-absence.
2267        assert!(decide_held(&state, "m1", true).await.is_err());
2268    }
2269
2270    #[tokio::test]
2271    async fn approving_a_detached_recipient_fails_loudly() {
2272        let (state, _t) = test_state().await;
2273        // Held while attached, decided after the agent went away.
2274        state
2275            .held_peer_messages
2276            .lock()
2277            .await
2278            .push_back(held_fixture("m1", "ghost"));
2279        let err = decide_held(&state, "m1", true).await.unwrap_err();
2280        assert!(
2281            err.contains("ghost"),
2282            "approval of a vanished recipient must name it: {err}"
2283        );
2284        assert_eq!(pending_snapshot(&state).await["count"], 0);
2285    }
2286
2287    #[test]
2288    fn only_the_host_may_read_or_decide_the_hold_queue() {
2289        // The queue exists to gate agents, so an agent reaching these surfaces
2290        // would be approving the very messages its posture held.
2291        let msg = require_host_message("agents.message.approve");
2292        assert!(msg.contains("host-only"), "{msg}");
2293        assert!(msg.contains("its own posture held"), "{msg}");
2294    }
2295
2296    #[test]
2297    fn an_unauthenticated_sender_is_refused() {
2298        let policy = car_policy::AgentPermissionPolicy::default();
2299        let out = admit_with(&policy, None, false, "conn:abc");
2300        assert!(
2301            matches!(out, DeliveryOutcome::Refused { .. }),
2302            "got {out:?}"
2303        );
2304    }
2305
2306    #[test]
2307    fn the_host_needs_no_agent_posture() {
2308        let policy = car_policy::AgentPermissionPolicy::default();
2309        assert_eq!(
2310            admit_with(&policy, None, true, "conn:host"),
2311            DeliveryOutcome::Delivered
2312        );
2313    }
2314
2315    #[test]
2316    fn a_bound_agent_is_allowed_by_default() {
2317        let policy = car_policy::AgentPermissionPolicy::default();
2318        assert_eq!(
2319            admit_with(&policy, Some("milo".into()), false, "agent:milo"),
2320            DeliveryOutcome::Delivered
2321        );
2322    }
2323
2324    #[test]
2325    fn denying_an_agent_at_read_only_actually_stops_its_messages() {
2326        // The whole point of resolving a tier: an operator's setting has to bind,
2327        // rather than the rule living only in a prompt the agent may ignore.
2328        let mut policy = car_policy::AgentPermissionPolicy::default();
2329        policy.set_agent(
2330            "milo",
2331            car_policy::PermissionTier::ReadOnly,
2332            car_policy::agent_permissions::ApprovalMode::Deny,
2333        );
2334        let out = admit_with(&policy, Some("milo".into()), false, "agent:milo");
2335        assert!(
2336            matches!(out, DeliveryOutcome::Refused { .. }),
2337            "got {out:?}"
2338        );
2339        // A different agent is unaffected by the per-agent override.
2340        assert_eq!(
2341            admit_with(&policy, Some("trader".into()), false, "agent:trader"),
2342            DeliveryOutcome::Delivered
2343        );
2344    }
2345
2346    #[test]
2347    fn require_approval_holds_rather_than_drops() {
2348        let mut policy = car_policy::AgentPermissionPolicy::default();
2349        policy.set_agent(
2350            "milo",
2351            car_policy::PermissionTier::ReadOnly,
2352            car_policy::agent_permissions::ApprovalMode::RequireApproval,
2353        );
2354        // Held is a third outcome on purpose: it can still be delivered later,
2355        // and the sender is told which of the two happened.
2356        assert!(matches!(
2357            admit_with(&policy, Some("milo".into()), false, "agent:milo"),
2358            DeliveryOutcome::Held { .. }
2359        ));
2360    }
2361
2362    #[tokio::test]
2363    async fn snapshot_lists_attached_agents() {
2364        let (state, _t) = test_state().await;
2365        attach(&state, "milo").await;
2366        attach(&state, "trader").await;
2367        let peers = snapshot_attached(&state).await;
2368        assert_eq!(peers.len(), 2);
2369        assert!(peers.iter().all(|p| p.kind == PeerKind::CarAgent));
2370        assert!(peers.iter().all(|p| p.source == PeerSource::Attached));
2371    }
2372
2373    #[tokio::test]
2374    async fn one_mcp_session_receives_only_its_own_addressed_message() {
2375        let (state, _t) = test_state().await;
2376        let (first_id, first) = open_mcp_peer_session(&state, true).await;
2377        let (_second_id, second) = open_mcp_peer_session(&state, true).await;
2378        let peers = snapshot_mcp_sessions(&state).await;
2379        let target = peers
2380            .iter()
2381            .find(|peer| peer.name == first)
2382            .expect("first session is addressable")
2383            .clone();
2384
2385        let msg = PeerMessage::new("agent:sender", &first, "only for first");
2386        let verdict = state
2387            .peer_guards
2388            .lock()
2389            .await
2390            .entry(first.clone())
2391            .or_insert_with(DeliveryGuard::new)
2392            .admit(&msg, 1_000);
2393        assert_eq!(verdict, GuardVerdict::Accept);
2394        let result = deliver(&state, &target, &msg).await;
2395        assert!(result.is_ok(), "queue delivery failed: {result:?}");
2396        settle_delivery_slot(&state, &target, result.is_ok()).await;
2397
2398        let drained = drain_mcp_inbox(&state, &first, 50).await.unwrap();
2399        assert_eq!(drained["count"], 1);
2400        assert_eq!(drained["messages"][0]["body"], "only for first");
2401        assert_eq!(drained["messages"][0]["to"], first);
2402        assert_eq!(
2403            state.mcp_peer_sessions.lock().await[&first_id].inbox.len(),
2404            0
2405        );
2406        assert_eq!(
2407            drain_mcp_inbox(&state, &second, 50).await.unwrap()["count"],
2408            0,
2409            "a message addressed to the first session must not leak to the second"
2410        );
2411        assert_eq!(state.peer_guards.lock().await[&first].queued(), 0);
2412    }
2413
2414    #[tokio::test]
2415    async fn queue_guards_are_scoped_per_mcp_session() {
2416        let (state, _t) = test_state().await;
2417        let (_, first) = open_mcp_peer_session(&state, true).await;
2418        let (_, second) = open_mcp_peer_session(&state, true).await;
2419        let mut guards = state.peer_guards.lock().await;
2420
2421        for i in 0..car_peers::QUEUE_CAP {
2422            let msg = PeerMessage::new(format!("agent:s{i}"), &first, format!("body-{i}"));
2423            assert!(guards
2424                .entry(first.clone())
2425                .or_insert_with(DeliveryGuard::new)
2426                .admit(&msg, 1_000)
2427                .is_accept());
2428        }
2429        let blocked = PeerMessage::new("agent:fresh", &first, "over cap");
2430        assert_eq!(
2431            guards.get_mut(&first).unwrap().admit(&blocked, 1_000),
2432            GuardVerdict::QueueFull {
2433                cap: car_peers::QUEUE_CAP
2434            }
2435        );
2436
2437        let other = PeerMessage::new("agent:fresh", &second, "over cap");
2438        assert!(
2439            guards
2440                .entry(second)
2441                .or_insert_with(DeliveryGuard::new)
2442                .admit(&other, 1_000)
2443                .is_accept(),
2444            "one session's full inbox must not consume another's queue budget"
2445        );
2446    }
2447
2448    #[tokio::test]
2449    async fn batch_mcp_sessions_remain_send_only() {
2450        let (state, _t) = test_state().await;
2451        let (batch_id, batch) = open_mcp_peer_session(&state, false).await;
2452        assert!(snapshot_mcp_sessions(&state)
2453            .await
2454            .iter()
2455            .all(|peer| peer.name != batch));
2456        let error = drain_mcp_inbox(&state, &batch, 50).await.unwrap_err();
2457        assert!(error.message().contains("send-only"), "{error:?}");
2458        assert!(state.mcp_peer_sessions.lock().await.contains_key(&batch_id));
2459    }
2460
2461    #[tokio::test]
2462    async fn snapshot_drops_names_that_are_not_addressable() {
2463        let (state, _t) = test_state().await;
2464        attach(&state, "milo").await;
2465        // A name that would escape the addressing charset must never become a
2466        // peer, regardless of how it got into the connection table.
2467        attach(&state, "../escape").await;
2468        let peers = snapshot_attached(&state).await;
2469        assert_eq!(peers.len(), 1);
2470        assert_eq!(peers[0].name, "milo");
2471    }
2472
2473    #[test]
2474    fn listing_reachability_matches_the_delivery_preflight() {
2475        let mut peer = PeerDescriptor {
2476            name: "discovered-mac".into(),
2477            reference: None,
2478            kind: PeerKind::RemoteCar,
2479            source: PeerSource::Lan,
2480            address: PeerAddress::A2a {
2481                base_url: "https://peer.invalid".into(),
2482            },
2483            display_name: None,
2484            capability: None,
2485            last_seen_ms: None,
2486            pubkey: None,
2487        };
2488
2489        let discovered = peer_listing_row(&peer, None);
2490        assert_eq!(discovered["can_receive"], true, "remote CAR has an inbox");
2491        assert_eq!(
2492            discovered["reachable"], false,
2493            "an untrusted LAN advertisement must not be offered for delivery"
2494        );
2495        let refusal = peer_reachability(&peer).expect_err("delivery must refuse the same peer");
2496        assert!(refusal.contains("not a trusted peer"), "{refusal}");
2497
2498        // Trust is a pure function of the source already present on the row. A
2499        // trusted remote descriptor therefore flips the listing and the send
2500        // preflight together without changing its kind-level capability.
2501        peer.source = PeerSource::Parslee;
2502        let trusted = peer_listing_row(&peer, None);
2503        assert_eq!(trusted["can_receive"], true);
2504        assert_eq!(trusted["reachable"], true);
2505        assert!(peer_reachability(&peer).is_ok());
2506
2507        // The other preflight guard is represented too: a trusted source does
2508        // not make an external batch CLI grow an inbox.
2509        peer.kind = PeerKind::ExternalCli;
2510        peer.source = PeerSource::Invocation;
2511        let no_inbox = peer_listing_row(&peer, None);
2512        assert_eq!(no_inbox["can_receive"], false);
2513        assert_eq!(no_inbox["reachable"], false);
2514        let refusal = peer_reachability(&peer).expect_err("delivery must refuse no-inbox kinds");
2515        assert!(refusal.contains("no inbox"), "{refusal}");
2516    }
2517
2518    #[tokio::test]
2519    async fn an_oversized_message_is_refused_before_delivery() {
2520        let (state, _t) = test_state().await;
2521        attach(&state, "milo").await;
2522
2523        let msg = PeerMessage::new("agent:sender", "milo", "x".repeat(2_000_000));
2524        let verdict = {
2525            let mut guards = state.peer_guards.lock().await;
2526            guards
2527                .entry("milo".to_string())
2528                .or_insert_with(DeliveryGuard::new)
2529                .admit(&msg, car_peers::now_ms())
2530        };
2531        assert!(matches!(verdict, GuardVerdict::TooLarge { .. }));
2532        // And the refusal names a remedy rather than just a limit.
2533        assert!(guard_error(&verdict).contains("state handle"));
2534    }
2535
2536    #[tokio::test]
2537    async fn a_detached_agent_yields_a_structured_error_not_a_hang() {
2538        let (state, _t) = test_state().await;
2539        // Present in the connection table but with no live session behind it —
2540        // exactly the disconnect race. It must resolve to a named error.
2541        attach(&state, "ghost").await;
2542        let target = snapshot_attached(&state).await.remove(0);
2543        let msg = PeerMessage::new("agent:sender", "ghost", "hello");
2544        let err = deliver(&state, &target, &msg).await.unwrap_err();
2545        assert!(
2546            err.contains("ghost") && err.contains("disconnect"),
2547            "error should name the agent and the cause, got: {err}"
2548        );
2549    }
2550
2551    #[tokio::test]
2552    async fn guards_are_per_recipient_not_global() {
2553        let (state, _t) = test_state().await;
2554        attach(&state, "a").await;
2555        attach(&state, "b").await;
2556
2557        let mut guards = state.peer_guards.lock().await;
2558        let dup = PeerMessage::new("agent:s", "a", "same body");
2559        assert!(guards
2560            .entry("a".into())
2561            .or_insert_with(DeliveryGuard::new)
2562            .admit(&dup, 1_000)
2563            .is_accept());
2564        // The identical body to a DIFFERENT recipient is unaffected: dedupe is
2565        // about one channel, not about the sender saying a thing twice.
2566        let to_b = PeerMessage::new("agent:s", "b", "same body");
2567        assert!(guards
2568            .entry("b".into())
2569            .or_insert_with(DeliveryGuard::new)
2570            .admit(&to_b, 1_000)
2571            .is_accept());
2572    }
2573
2574    #[tokio::test]
2575    async fn a_refused_message_still_leaves_an_audit_record() {
2576        let temp = tempfile::tempdir().unwrap();
2577        let journal = temp.path().join("peer-messages.jsonl");
2578
2579        let target = PeerDescriptor {
2580            name: "milo".into(),
2581            reference: None,
2582            kind: PeerKind::CarAgent,
2583            source: PeerSource::Attached,
2584            address: PeerAddress::AttachedAgent {
2585                agent_id: "milo".into(),
2586            },
2587            display_name: None,
2588            capability: None,
2589            last_seen_ms: None,
2590            pubkey: None,
2591        };
2592        let msg = PeerMessage::new("agent:sender", "milo", "hello");
2593        append_peer_audit_at(
2594            &journal,
2595            &msg,
2596            &target,
2597            &DeliveryOutcome::Refused {
2598                reason: "over the rate budget".into(),
2599            },
2600        );
2601
2602        // Refusals are the half an operator most needs to see, so they must be
2603        // recorded as loudly as deliveries.
2604        let body = std::fs::read_to_string(&journal).expect("journal written");
2605        let rec: Value = serde_json::from_str(body.trim()).expect("one json line");
2606        assert_eq!(rec["from"], "agent:sender");
2607        assert_eq!(rec["to"], "milo");
2608        assert_eq!(rec["outcome"]["outcome"], "refused");
2609        assert_eq!(rec["outcome"]["reason"], "over the rate budget");
2610    }
2611
2612    /// `agents.chat` must not be the way around `agents.message`'s admission.
2613    /// A denied agent is denied on both, and the refusal names chat so the
2614    /// caller is not left guessing which surface stopped it.
2615    #[test]
2616    fn a_denied_agent_cannot_reach_another_agent_by_chatting_instead() {
2617        let mut policy = car_policy::AgentPermissionPolicy::default();
2618        policy.set_agent(
2619            "noisy",
2620            car_policy::PermissionTier::ReadOnly,
2621            car_policy::agent_permissions::ApprovalMode::Deny,
2622        );
2623        let outcome = admit_with(&policy, Some("noisy".into()), false, "agent:noisy");
2624        assert!(
2625            matches!(outcome, DeliveryOutcome::Refused { .. }),
2626            "the shared admission denies it: {outcome:?}"
2627        );
2628    }
2629
2630    /// The host keeps its pass-through, or every CarHost chat turn would be
2631    /// graded against an agent posture the operator's own client does not have.
2632    #[test]
2633    fn the_host_is_exempt_from_chat_admission() {
2634        let policy = car_policy::AgentPermissionPolicy::default();
2635        assert!(matches!(
2636            admit_with(&policy, None, true, "host"),
2637            DeliveryOutcome::Delivered
2638        ));
2639    }
2640
2641    fn inbound(to: &str, body: &str, key: &str) -> car_a2a::InboundPeerMessage {
2642        car_a2a::InboundPeerMessage {
2643            peer_pubkey: key.to_string(),
2644            claimed: car_a2a::ClaimedByPeer {
2645                message_id: format!("m-{to}"),
2646                to: to.to_string(),
2647                body: body.to_string(),
2648                no_reply: false,
2649                trace: String::new(),
2650                via: Vec::new(),
2651            },
2652        }
2653    }
2654
2655    /// The relay refusal. Resolving inbound through the full directory would
2656    /// let host A make this host forward to host C under THIS host's signature,
2657    /// reaching hosts that never trusted A. A peer that is not a locally
2658    /// attached agent is refused, and the refusal names the rule.
2659    #[tokio::test]
2660    async fn an_inbound_message_is_never_relayed_onward() {
2661        let (state, _t) = test_state().await;
2662        let broker = PeerInboundBroker {
2663            state: Arc::downgrade(&state),
2664        };
2665        // Nothing attached under this name, so it cannot resolve locally — and
2666        // must not be looked for anywhere else.
2667        let err = car_a2a::PeerInbox::deliver(&broker, inbound("far-host", "fwd", "KEY1"))
2668            .await
2669            .expect_err("must refuse");
2670        assert!(err.contains("never relayed"), "{err}");
2671    }
2672
2673    /// An illegal recipient name is refused before anything is resolved or a
2674    /// guard entry is minted for it.
2675    #[tokio::test]
2676    async fn an_illegal_recipient_name_is_refused() {
2677        let (state, _t) = test_state().await;
2678        let broker = PeerInboundBroker {
2679            state: Arc::downgrade(&state),
2680        };
2681        let err = car_a2a::PeerInbox::deliver(&broker, inbound(".watcher", "hi", "KEY2"))
2682            .await
2683            .expect_err("must refuse");
2684        assert!(err.contains("not a legal peer name"), "{err}");
2685        assert!(
2686            state.peer_guards.lock().await.is_empty(),
2687            "a refused name must not mint a guard entry"
2688        );
2689    }
2690
2691    /// The sender an agent is shown is derived from the verified key, never
2692    /// from anything the calling daemon claimed, and the message is marked
2693    /// unreplyable because that derived form is an attribution, not an address.
2694    #[tokio::test]
2695    async fn the_sender_is_the_verified_key_and_the_message_is_unreplyable() {
2696        let (state, _t) = test_state().await;
2697        attach(&state, "milo").await;
2698        let broker = PeerInboundBroker {
2699            state: Arc::downgrade(&state),
2700        };
2701        // No session is registered for the attached client id, so delivery
2702        // fails after admission — which is exactly the path that exercises the
2703        // derivation and the dedupe rollback.
2704        let _ = car_a2a::PeerInbox::deliver(&broker, inbound("milo", "first", "KEYABC")).await;
2705
2706        // The dedupe record was given back, so an honest retry is not refused
2707        // as "already delivered".
2708        let retry = car_a2a::PeerInbox::deliver(&broker, inbound("milo", "first", "KEYABC")).await;
2709        let err = retry.expect_err("delivery still fails");
2710        assert!(
2711            !err.contains("already delivered"),
2712            "a retry after a failed delivery must not be called a duplicate: {err}"
2713        );
2714    }
2715
2716    /// A daemon that has gone away refuses rather than panicking on the Weak.
2717    #[tokio::test]
2718    async fn a_stopped_daemon_refuses_cleanly() {
2719        let (state, _t) = test_state().await;
2720        let broker = PeerInboundBroker {
2721            state: Arc::downgrade(&state),
2722        };
2723        drop(state);
2724        let err = car_a2a::PeerInbox::deliver(&broker, inbound("milo", "hi", "KEY3"))
2725            .await
2726            .expect_err("must refuse");
2727        assert!(err.contains("shutting down"), "{err}");
2728    }
2729
2730    fn inbound_with(to: &str, key: &str, via: Vec<String>) -> car_a2a::InboundPeerMessage {
2731        car_a2a::InboundPeerMessage {
2732            peer_pubkey: key.to_string(),
2733            claimed: car_a2a::ClaimedByPeer {
2734                message_id: format!("m-{to}"),
2735                to: to.to_string(),
2736                body: "body".into(),
2737                no_reply: false,
2738                trace: String::new(),
2739                via,
2740            },
2741        }
2742    }
2743
2744    /// The boundary marker is APPENDED by the receiver, from the key it
2745    /// verified — never substituted for the sender's prefix. The configured
2746    /// audit row is the durable end-to-end observable.
2747    ///
2748    /// This test re-enters itself in a child process so `CAR_HOME` can point at
2749    /// a real canary directory without racing any other test in this binary.
2750    /// The old write-time resolver would append there; the fixed writer uses
2751    /// the path captured on `ServerState` and leaves the canary untouched.
2752    #[tokio::test]
2753    async fn the_receiver_appends_its_own_boundary_marker() {
2754        const CHILD_STATE_DIR: &str = "CAR_PEER_AUDIT_TEST_STATE_DIR";
2755
2756        if let Some(state_dir) = std::env::var_os(CHILD_STATE_DIR) {
2757            let state_dir = std::path::PathBuf::from(state_dir);
2758            let state = Arc::new(ServerState::with_config(
2759                crate::session::ServerStateConfig::new(state_dir.clone()),
2760            ));
2761            assert_eq!(
2762                state.peer_audit_journal,
2763                state_dir.join("peer-messages.jsonl"),
2764                "a custom state directory owns its peer audit journal"
2765            );
2766            attach(&state, "milo").await;
2767            let broker = PeerInboundBroker {
2768                state: Arc::downgrade(&state),
2769            };
2770            let _ = car_a2a::PeerInbox::deliver(
2771                &broker,
2772                inbound_with("milo", "KEYZ", vec!["agent:remote".into()]),
2773            )
2774            .await;
2775            return;
2776        }
2777
2778        let temp = tempfile::tempdir().unwrap();
2779        let state_dir = temp.path().join("configured-state");
2780        let global_dir = temp.path().join("global-car-home");
2781        std::fs::create_dir_all(&global_dir).unwrap();
2782        let global_canary = global_dir.join("peer-messages.jsonl");
2783        std::fs::write(&global_canary, "operator-row\n").unwrap();
2784
2785        let status = std::process::Command::new(std::env::current_exe().unwrap())
2786            .arg("--exact")
2787            .arg("peers::tests::the_receiver_appends_its_own_boundary_marker")
2788            .arg("--test-threads=1")
2789            .env("CAR_HOME", &global_dir)
2790            .env(CHILD_STATE_DIR, &state_dir)
2791            .status()
2792            .expect("spawn isolated peer-audit test child");
2793        assert!(status.success(), "peer-audit test child failed: {status}");
2794
2795        let journal = state_dir.join("peer-messages.jsonl");
2796        let body = std::fs::read_to_string(&journal)
2797            .expect("inbound audit row written inside the configured test state directory");
2798        let row: Value = serde_json::from_str(body.trim()).expect("one audit JSON row");
2799        assert_eq!(row["dir"], "in");
2800        assert_eq!(row["attested_by"], "KEYZ");
2801        assert_eq!(row["trace"], "m-milo");
2802        assert_eq!(
2803            row["via"],
2804            serde_json::json!(["agent:remote", "peer:KEYZ"]),
2805            "the receiver's verified-key boundary is appended to the attested prefix"
2806        );
2807        assert_eq!(
2808            std::fs::read_to_string(global_canary).unwrap(),
2809            "operator-row\n",
2810            "the process-global CAR_HOME peer journal must remain untouched"
2811        );
2812    }
2813
2814    /// The chain this host builds APPENDS its marker to what the peer attested.
2815    ///
2816    /// Keep the pure shape assertion beside the end-to-end journal assertion so
2817    /// failures distinguish stamping logic from delivery/audit plumbing.
2818    #[test]
2819    fn the_boundary_marker_is_appended_not_substituted() {
2820        let attested = vec!["agent:alice".to_string(), "peer:KEYA".to_string()];
2821        let via = stamp_boundary(&attested, "peer:KEYB");
2822
2823        assert_eq!(
2824            via,
2825            vec!["agent:alice", "peer:KEYA", "peer:KEYB"],
2826            "the marker goes on the END, and nothing the peer attested is dropped"
2827        );
2828        assert_eq!(
2829            via.last().map(String::as_str),
2830            Some("peer:KEYB"),
2831            "the receiver's own marker is last, so a reader can tell where this \
2832             host's observation begins"
2833        );
2834        assert_eq!(
2835            &via[..attested.len()],
2836            attested.as_slice(),
2837            "substituting the prefix would erase which segments the sending key \
2838             actually stood behind"
2839        );
2840    }
2841
2842    /// A root message — no prior chain — still gets exactly one marker, so
2843    /// `hops()` can distinguish a first hop from a forged empty chain.
2844    #[test]
2845    fn a_root_message_gets_exactly_one_marker() {
2846        assert_eq!(stamp_boundary(&[], "peer:KEYA"), vec!["peer:KEYA"]);
2847    }
2848
2849    /// A malformed lineage segment is refused before an agent is shown it.
2850    /// Nothing in CAR validated inbound lineage before this.
2851    #[tokio::test]
2852    async fn a_malformed_lineage_segment_is_refused() {
2853        let (state, _t) = test_state().await;
2854        attach(&state, "milo").await;
2855        let broker = PeerInboundBroker {
2856            state: Arc::downgrade(&state),
2857        };
2858        let err = car_a2a::PeerInbox::deliver(
2859            &broker,
2860            inbound_with("milo", "KEYY", vec!["not-a-segment".into()]),
2861        )
2862        .await
2863        .expect_err("refused");
2864        assert!(err.contains("well-formed lineage segment"), "{err}");
2865    }
2866
2867    /// The hop cap fires on cross-host traffic — the case it exists for. The
2868    /// chain must be stamped BEFORE the guard runs, or `hops()` is 0 on every
2869    /// inbound message and this can never trigger.
2870    #[tokio::test]
2871    async fn the_hop_cap_fires_on_a_chain_that_arrived_deep() {
2872        let (state, _t) = test_state().await;
2873        attach(&state, "milo").await;
2874        let broker = PeerInboundBroker {
2875            state: Arc::downgrade(&state),
2876        };
2877        let deep: Vec<String> = (0..=car_peers::MAX_HOPS)
2878            .map(|i| format!("agent:a{i}"))
2879            .collect();
2880        let err = car_a2a::PeerInbox::deliver(&broker, inbound_with("milo", "KEYX", deep))
2881            .await
2882            .expect_err("refused");
2883        assert!(err.contains("hop cap"), "{err}");
2884    }
2885
2886    /// The threshold is asymmetric on purpose: a record is not condemned on its
2887    /// first bad day, and it takes more than a bare majority of failures.
2888    #[tokio::test]
2889    async fn standing_degrades_only_once_failures_outrun_successes() {
2890        let (state, _t) = test_state().await;
2891        for _ in 0..3 {
2892            record_standing(&state, "peer:K", Some("bad chain"), None).await;
2893        }
2894        let now = car_peers::now_ms();
2895        assert!(
2896            state.peer_standing.lock().await["peer:K"].is_degraded(now),
2897            "3 failures, 0 successes is past the threshold"
2898        );
2899
2900        let (state2, _t2) = test_state().await;
2901        record_standing(&state2, "peer:K", Some("bad chain"), None).await;
2902        record_standing(&state2, "peer:K", Some("bad chain"), None).await;
2903        assert!(
2904            !state2.peer_standing.lock().await["peer:K"].is_degraded(now),
2905            "2 failures is not yet degraded"
2906        );
2907    }
2908
2909    /// A rate limit and an in-window duplicate must never touch standing. The
2910    /// channel guard exists because in a mutual loop NEITHER party is
2911    /// misbehaving; charging them would price correct behaviour as misconduct.
2912    #[tokio::test]
2913    async fn the_loop_guard_verdicts_are_not_misconduct() {
2914        for v in [
2915            car_peers::GuardVerdict::RateLimited {
2916                sender: "peer:K".into(),
2917                window_ms: 60_000,
2918            },
2919            car_peers::GuardVerdict::DuplicateWithinWindow,
2920        ] {
2921            let attributable = matches!(
2922                v,
2923                car_peers::GuardVerdict::HopLimit { .. }
2924                    | car_peers::GuardVerdict::TooLarge { .. }
2925                    | car_peers::GuardVerdict::InvalidName { .. }
2926            );
2927            assert!(!attributable, "{v:?} must not be charged to the sender");
2928        }
2929    }
2930
2931    /// Successes saturate. Without a ceiling a peer with a long good history
2932    /// could spend it on an equally long run of failures before any throttle
2933    /// engaged — tolerable for an artifact, not for an actor that controls its
2934    /// own send rate.
2935    #[tokio::test]
2936    async fn success_is_capped_so_headroom_never_grows_without_bound() {
2937        let (state, _t) = test_state().await;
2938        for _ in 0..(STANDING_SUCCESS_CAP + 25) {
2939            record_standing(&state, "peer:K", None, None).await;
2940        }
2941        assert_eq!(
2942            state.peer_standing.lock().await["peer:K"].success_count,
2943            STANDING_SUCCESS_CAP
2944        );
2945    }
2946
2947    /// A degraded record recovers on its own. The alternative is a ratchet that
2948    /// only tightens and needs a human to remember to forgive it.
2949    #[tokio::test]
2950    async fn standing_decays_so_a_degraded_peer_recovers() {
2951        let now = car_peers::now_ms();
2952        let rec = PeerStanding {
2953            success_count: 0,
2954            fail_count: 8,
2955            updated_ms: now - STANDING_HALFLIFE_MS * 3,
2956            ..Default::default()
2957        };
2958        assert!(
2959            !rec.is_degraded(now),
2960            "three half-lives takes 8 failures to 1, under the threshold"
2961        );
2962        assert!(
2963            rec.is_degraded(rec.updated_ms),
2964            "and it was degraded when the failures were fresh"
2965        );
2966    }
2967
2968    /// A healthy sender meets no aggregate ceiling — standing adds nothing to
2969    /// normal traffic.
2970    #[tokio::test]
2971    async fn a_healthy_sender_is_never_throttled() {
2972        let (state, _t) = test_state().await;
2973        for _ in 0..50 {
2974            record_standing(&state, "peer:K", None, None).await;
2975            assert_eq!(
2976                standing_gate(&state, "peer:K").await,
2977                StandingVerdict::Proceed
2978            );
2979        }
2980    }
2981
2982    /// A degraded sender is throttled rather than severed, and the reason names
2983    /// the record so an operator can see why.
2984    #[tokio::test]
2985    async fn a_degraded_sender_is_throttled_not_cut_off() {
2986        let (state, _t) = test_state().await;
2987        for _ in 0..5 {
2988            record_standing(&state, "peer:K", Some("bad chain"), None).await;
2989        }
2990        // The reduced budget still lets some through.
2991        for _ in 0..car_peers::DEGRADED_RATE_LIMIT {
2992            assert_eq!(
2993                standing_gate(&state, "peer:K").await,
2994                StandingVerdict::Proceed
2995            );
2996        }
2997        match standing_gate(&state, "peer:K").await {
2998            StandingVerdict::Throttled { reason } => {
2999                assert!(reason.contains("degraded"), "{reason}");
3000                assert!(reason.contains("recover"), "{reason}");
3001            }
3002            v => panic!("expected a throttle, got {v:?}"),
3003        }
3004    }
3005
3006    /// Blame evidence is agent-granular even though the consequence lands on
3007    /// the key: enforce at the granularity you can verify, attribute at the
3008    /// granularity you can record.
3009    #[tokio::test]
3010    async fn a_failure_records_the_chain_that_caused_it() {
3011        let (state, _t) = test_state().await;
3012        let via = vec!["agent:scraper".to_string(), "peer:K".to_string()];
3013        record_standing(&state, "peer:K", Some("chain too deep"), Some(&via)).await;
3014        let map = state.peer_standing.lock().await;
3015        assert_eq!(map["peer:K"].last_fail_via.as_deref(), Some(via.as_slice()));
3016        assert_eq!(
3017            map["peer:K"].last_fail_reason.as_deref(),
3018            Some("chain too deep")
3019        );
3020    }
3021}