Skip to main content

majordomus_cli/capability/builtin/
mesh.rs

1//! The `mesh` module: this process's mesh runtime, projected. Discovery (`mesh.status`,
2//! `mesh.nodes`, `mesh.identity`, `mesh.doctor`, `mesh.register`) reads the one
3//! [`crate::mesh::MeshRegistry`]; cooperation (`mesh.cooperation`, `mesh.peers`,
4//! `mesh.peer`, `mesh.state`, `mesh.events`, `mesh.verify` and the commands) reads and
5//! writes through the one [`crate::mesh::cooperation::Cooperation`] runtime. No capability
6//! holds state of its own, so the CLI, HTTP, OpenAPI, MCP and the Cockpit cannot disagree
7//! about a peer, a claim or a link (ADR 0050, ADR 0067).
8//!
9//! Discovery grants nothing. Cooperation is admitted per link — same repository, trusted
10//! key, compatible protocol — and what it carries is metadata about work: sessions,
11//! claims, handovers, reviews. There is no remote execution here for a linked peer to gain.
12
13use std::collections::BTreeMap;
14use std::net::Ipv4Addr;
15
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19use crate::capability::benchmark::{BenchmarkCases, CaseContext, NamedCase};
20use crate::capability::handler::{CapabilityError, Context};
21use crate::capability::model::{
22    BenchmarkPolicy, CachePolicy, CapabilityKind, CliExposure, Exposure, Stability, WaiverReason,
23};
24use crate::capability::module::ModuleDescriptor;
25use crate::mesh::address::interfaces;
26use crate::mesh::cooperation::{
27    Cooperation, CooperationError, CooperationStatus, MeshVerifyReport, PeerView, RefusedView,
28    Written,
29};
30use crate::mesh::firewall::{FirewallApplyReport, FirewallReport};
31use crate::mesh::journal::{ClaimMode, MeshEvent, SessionInfo, StreamId, StreamLiveness};
32use crate::mesh::link::{LinkReply, RefusalCode, Signed, HELLO_PATH, SYNC_PATH};
33use crate::mesh::page::{self, ClaimBrief, Query, Summary, View};
34use crate::mesh::rendezvous::REGISTER_PATH;
35use crate::mesh::state::{ClaimView, CooperationState, HandoverView, SessionState};
36use crate::mesh::{
37    default_identity_path, MeshConfig, MeshDoctorReport, MeshStatus, NodeIdentity, NodeRecord,
38    PublicIdentity, RegisterAnswer,
39};
40use crate::{capability, module};
41
42use super::{get, mcp, post, Empty};
43
44/// The MCP resource `mesh.status` is projected as.
45pub const MESH_URI: &str = "majordomus://mesh";
46
47// ---------------------------------------------------------------- mesh.status
48
49fn mesh_status(ctx: &Context, _: Empty) -> Result<MeshStatus, CapabilityError> {
50    Ok(ctx.mesh.status())
51}
52
53// ---------------------------------------------------------------- mesh.nodes
54
55#[derive(Debug, Serialize, Deserialize, JsonSchema)]
56/// The answer of `mesh.nodes`: every node this process has observed.
57pub struct NodeList {
58    /// How many nodes the registry holds.
59    pub count: usize,
60    /// The nodes, in node-id order — the one canonical order every surface shows.
61    pub nodes: Vec<NodeRecord>,
62}
63
64fn mesh_nodes(ctx: &Context, _: Empty) -> Result<NodeList, CapabilityError> {
65    let nodes = ctx.mesh.nodes();
66    Ok(NodeList {
67        count: nodes.len(),
68        nodes,
69    })
70}
71
72// ---------------------------------------------------------------- mesh.identity
73
74#[derive(Debug, Serialize, Deserialize, JsonSchema)]
75/// The answer of `mesh.identity`: this machine's node identity, public half only.
76pub struct MeshIdentityReport {
77    /// Where the identity file lives (or would).
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub path: Option<String>,
80    /// Whether the file exists.
81    pub present: bool,
82    /// The public identity, when the file exists and loads. The signing key is not
83    /// here, not in any projection, and not in any log.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub identity: Option<PublicIdentity>,
86    /// Why the identity did not load, when it did not.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub error: Option<String>,
89}
90
91fn mesh_identity(_: &Context, _: Empty) -> Result<MeshIdentityReport, CapabilityError> {
92    let Some(path) = default_identity_path() else {
93        return Ok(MeshIdentityReport {
94            path: None,
95            present: false,
96            identity: None,
97            error: Some("no HOME and no XDG_STATE_HOME: nowhere to keep a node identity".into()),
98        });
99    };
100    if !path.is_file() {
101        return Ok(MeshIdentityReport {
102            path: Some(path.display().to_string()),
103            present: false,
104            identity: None,
105            error: None,
106        });
107    }
108    match NodeIdentity::load_or_create(&path) {
109        Ok(identity) => Ok(MeshIdentityReport {
110            path: Some(path.display().to_string()),
111            present: true,
112            identity: Some(identity.public),
113            error: None,
114        }),
115        Err(e) => Ok(MeshIdentityReport {
116            path: Some(path.display().to_string()),
117            present: true,
118            identity: None,
119            error: Some(e.to_string()),
120        }),
121    }
122}
123
124// ---------------------------------------------------------------- mesh.doctor
125
126fn mesh_doctor(ctx: &Context, _: Empty) -> Result<MeshDoctorReport, CapabilityError> {
127    let root = std::path::Path::new(&ctx.index.repository.root);
128    // The runtime's verdict is reported only where a shared server made one: in the
129    // command line's process nothing activates the mesh, and that absence is not news.
130    let runtime = ctx.mesh.decided().then(|| ctx.mesh.status());
131    let resolved = resolved_declaration(ctx);
132    let source = crate::mesh::doctor::declaration_source(&resolved);
133    let mut report =
134        crate::mesh::doctor::doctor_at(resolved.declaration, Some(root), runtime.as_ref());
135    if let Some(check) = source {
136        report.checks.push(check);
137    }
138    // the journal is judged only where a runtime holds one
139    if let Some(cooperation) = ctx.mesh.cooperation() {
140        report.checks.push(crate::mesh::doctor::journal_check(
141            &cooperation.journal().tallies(),
142        ));
143    }
144    // one conjunction over every check, so neither added check's failure can be lost
145    report.ok = report.checks.iter().all(|c| c.ok);
146    Ok(report)
147}
148
149// ---------------------------------------------------------------- mesh.firewall
150
151#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
152#[serde(deny_unknown_fields)]
153/// The input of `mesh.firewall` and `mesh.firewall.apply`.
154pub struct FirewallInput {
155    /// The port this checkout's server listens on beyond loopback, when it does: a peer
156    /// dials it there on the link protocol, so it needs admitting too. Omitted, the plan
157    /// holds the discovery ports and the hub ports the declaration gives this machine.
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub port: Option<u16>,
160}
161
162impl BenchmarkCases for FirewallInput {
163    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
164        vec![NamedCase::new("declared", FirewallInput { port: None })]
165    }
166}
167
168/// The report over this checkout's declaration and this machine's interfaces.
169fn firewall_report(ctx: &Context, input: &FirewallInput) -> FirewallReport {
170    let config = declaration(ctx).and_then(|d| d.ok());
171    let local: Vec<Ipv4Addr> = interfaces().into_iter().map(|(_, ip)| ip).collect();
172    crate::mesh::firewall::report(config.as_ref(), &local, input.port)
173}
174
175fn mesh_firewall(ctx: &Context, input: FirewallInput) -> Result<FirewallReport, CapabilityError> {
176    Ok(firewall_report(ctx, &input))
177}
178
179fn mesh_firewall_apply(
180    ctx: &Context,
181    input: FirewallInput,
182) -> Result<FirewallApplyReport, CapabilityError> {
183    Ok(crate::mesh::firewall::apply(firewall_report(ctx, &input)))
184}
185
186/// Put the declaration's trust in force on a running cooperation runtime, ending the links of
187/// every key it no longer trusts (I2134). The server calls this with each new generation of the
188/// repository it reads, so a key withdrawn from `trust.allow` and committed loses its links
189/// without a restart. Returns the runtime keys whose links ended.
190pub(crate) fn retrust(ctx: &Context) -> Vec<String> {
191    let (Some(cooperation), Some(Ok(config))) = (ctx.mesh.cooperation(), declaration(ctx)) else {
192        return Vec::new();
193    };
194    cooperation.retrust(config.trust)
195}
196
197/// The repository's mesh declaration in force: `None` when no object of the kind exists,
198/// the parse verdict when one does — the working tree's copy with every widening of the
199/// trunk's copy removed ([`crate::mesh::root`]). Every reader of the declaration asks this,
200/// so the runtime, the doctor and every status agree on what is obeyed.
201pub fn declaration(ctx: &Context) -> Option<Result<MeshConfig, crate::mesh::MeshError>> {
202    resolved_declaration(ctx).declaration
203}
204
205/// [`declaration`], with the trunk it was held against and what was narrowed.
206pub(crate) fn resolved_declaration(ctx: &Context) -> crate::mesh::root::Resolved {
207    let root = std::path::Path::new(&ctx.index.repository.root);
208    let working = ctx
209        .index
210        .objects
211        .iter()
212        .find(|o| o.kind == crate::mesh::KIND);
213    crate::mesh::root::resolve(root, working)
214}
215
216// ---------------------------------------------------------------- mesh.register
217
218#[derive(Debug, Serialize, Deserialize, JsonSchema)]
219#[serde(deny_unknown_fields)]
220/// The input of `mesh.register`: one signed envelope, exactly as the wire carries it.
221pub struct RegisterInput {
222    /// The envelope. Verified here exactly as a datagram would be — bounds, staleness,
223    /// signature — before anything is recorded.
224    pub envelope: serde_json::Value,
225}
226
227impl BenchmarkCases for RegisterInput {
228    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
229        // A malformed envelope: the handler answers `accepted: false` without touching
230        // the registry, which is exactly the cheap, side-effect-free path to measure.
231        vec![NamedCase::new(
232            "refused",
233            RegisterInput {
234                envelope: serde_json::json!({"v": 0}),
235            },
236        )]
237    }
238}
239
240fn mesh_register(ctx: &Context, input: RegisterInput) -> Result<RegisterAnswer, CapabilityError> {
241    Ok(ctx.mesh.register(&input.envelope, "http"))
242}
243
244// ---------------------------------------------------------------- cooperation: shared
245
246fn cooperation(ctx: &Context) -> Result<std::sync::Arc<Cooperation>, CapabilityError> {
247    ctx.mesh.cooperation().ok_or_else(|| {
248        CapabilityError::Refused(format!(
249            "not_active: {}",
250            ctx.mesh
251                .cooperation_status()
252                .reason
253                .unwrap_or_else(|| "cooperation is not running in this process".into())
254        ))
255    })
256}
257
258fn map_error(e: CooperationError) -> CapabilityError {
259    match e {
260        CooperationError::Conflict(_) => CapabilityError::Refused(format!("claim_conflict: {e}")),
261        CooperationError::NotOwn(m) => CapabilityError::Refused(format!("not_own: {m}")),
262        CooperationError::NotFound(m) => CapabilityError::NotFound(m),
263        CooperationError::FeatureUnsupported(m) => {
264            CapabilityError::Refused(format!("{}: {m}", RefusalCode::FeatureUnsupported.as_str()))
265        }
266        CooperationError::Invalid(m) => CapabilityError::InvalidInput(m),
267    }
268}
269
270/// The session a call acts for: the one it names, else the calling MCP session's own board
271/// session — the same `board-<peer>` session the board projection opened for it.
272fn session_of(ctx: &Context, given: Option<String>) -> Result<String, CapabilityError> {
273    if let Some(session) = given.filter(|s| !s.trim().is_empty()) {
274        return Ok(session);
275    }
276    ctx.caller
277        .as_ref()
278        .map(|peer| format!("board-{}", peer.as_str()))
279        .ok_or_else(|| {
280            CapabilityError::InvalidInput(
281                "name the session: over the command line or plain HTTP there is no calling session"
282                    .into(),
283            )
284        })
285}
286
287// ---------------------------------------------------------------- mesh.cooperation
288
289fn mesh_cooperation(ctx: &Context, _: Empty) -> Result<CooperationStatus, CapabilityError> {
290    Ok(ctx.mesh.cooperation_status())
291}
292
293// ---------------------------------------------------------------- mesh.peers
294
295/// One session in the peer tree.
296#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
297pub struct SessionNode {
298    /// `<stream>/<session>`.
299    pub key: String,
300    /// What the session last said about itself.
301    pub info: SessionInfo,
302    /// Its standing.
303    pub state: SessionState,
304    /// The claims it holds or held; empty in the summary view, which counts them instead.
305    pub claims: Vec<ClaimView>,
306    /// How many of its claims are live.
307    pub claims_held: usize,
308    /// The review requests it opened, by key.
309    pub reviews: Vec<String>,
310}
311
312/// One runtime in the peer tree.
313#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
314pub struct RuntimeNode {
315    /// `<node>-<runtime>`.
316    pub runtime: String,
317    /// Whether this is the runtime answering.
318    pub this_runtime: bool,
319    /// Whether its stream beats: own, live, or expired.
320    pub liveness: StreamLiveness,
321    /// Milliseconds since its beat last rose, on this runtime's clock.
322    #[serde(default, skip_serializing_if = "Option::is_none")]
323    pub last_beat_ms: Option<u64>,
324    /// This runtime's link to it, when there is one.
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub link: Option<PeerView>,
327    /// Its newest live claim, told briefly.
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub newest_claim: Option<ClaimBrief>,
330    /// Its sessions; in the summary view only the active ones.
331    pub sessions: Vec<SessionNode>,
332}
333
334/// One machine in the peer tree.
335#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
336pub struct MachineNode {
337    /// The node id.
338    pub node: String,
339    /// Its display name, when a card or this node said.
340    pub name: String,
341    /// Whether it is this machine.
342    pub local: bool,
343    /// Its runtimes.
344    pub runtimes: Vec<RuntimeNode>,
345}
346
347/// This machine first, then the others by the name a person reads; the node id ends the key,
348/// because two machines can carry one name and the order must still be total.
349impl crate::order::Ordered for MachineNode {
350    fn order_key(&self) -> crate::order::OrderKey<'_> {
351        crate::order::OrderKey::plain(&self.name, &self.node).ranked(i64::from(!self.local))
352    }
353}
354
355/// The answer of `mesh.peers`: machine → runtime → session → claim, every level from the
356/// one journal and the one link table.
357#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
358pub struct PeerTree {
359    /// Whether cooperation runs.
360    pub active: bool,
361    /// What the answer carries: `summary` or `full`.
362    pub view: View,
363    /// Why not.
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub reason: Option<String>,
366    /// This runtime.
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub runtime: Option<String>,
369    /// The mesh repository id.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub repository: Option<String>,
372    /// The machines, local first.
373    pub machines: Vec<MachineNode>,
374    /// Candidates refused, with the rule.
375    pub refused: Vec<RefusedView>,
376    /// The digest of the cooperation state: equal digests, equal state.
377    #[serde(default, skip_serializing_if = "Option::is_none")]
378    pub digest: Option<String>,
379}
380
381fn rank(l: StreamLiveness) -> u8 {
382    match l {
383        StreamLiveness::Own => 0,
384        StreamLiveness::Live => 1,
385        StreamLiveness::Expired => 2,
386    }
387}
388
389/// Build the peer tree from a running cooperation, narrowed and viewed as `query` asks. The
390/// summary view keeps every runtime and only the active sessions, each with its claims
391/// counted rather than listed and its intent cut; the full view is every session with every
392/// claim. Either way the digest is the whole fold's.
393pub fn peer_tree(c: &Cooperation, query: &Query) -> PeerTree {
394    let status = c.status();
395    let whole = c.state();
396    let state = page::narrow(&whole, query);
397    let summary = query.view() == View::Summary;
398    let links: BTreeMap<String, PeerView> = status
399        .peers
400        .iter()
401        .map(|p| (p.runtime.clone(), p.clone()))
402        .collect();
403    let own = status.runtime.clone().unwrap_or_default();
404    let own_node = own.get(..32).unwrap_or_default().to_string();
405
406    // Runtimes: this one, every linked one, and every one the journal has heard of.
407    let mut runtimes: BTreeMap<String, RuntimeNode> = BTreeMap::new();
408    let mut touch = |key: &str, liveness: StreamLiveness, age: Option<u64>| {
409        let node = runtimes
410            .entry(key.to_string())
411            .or_insert_with(|| RuntimeNode {
412                runtime: key.to_string(),
413                this_runtime: key == own,
414                liveness,
415                last_beat_ms: age,
416                link: None,
417                newest_claim: None,
418                sessions: Vec::new(),
419            });
420        if rank(liveness) < rank(node.liveness) {
421            node.liveness = liveness;
422        }
423        if let Some(a) = age {
424            node.last_beat_ms = Some(node.last_beat_ms.map_or(a, |b| b.min(a)));
425        }
426    };
427    for (stream, liveness, age) in c.streams() {
428        touch(&stream.runtime_key(), liveness, age);
429    }
430    for key in links.keys() {
431        touch(key, StreamLiveness::Expired, None);
432    }
433    for (key, node) in runtimes.iter_mut() {
434        node.link = links.get(key).cloned();
435    }
436    for claim in state.claims.iter().filter(|cl| cl.state.is_live()) {
437        // every claim's runtime is a node: each stream of the journal was touched above
438        runtimes.entry(claim.runtime.clone()).and_modify(|runtime| {
439            let newer = runtime
440                .newest_claim
441                .as_ref()
442                .is_none_or(|b| claim.acquired_lamport > b.acquired_lamport);
443            if newer {
444                runtime.newest_claim = Some(ClaimBrief {
445                    key: claim.key.clone(),
446                    session: claim.session.clone(),
447                    issue: claim.issue.clone(),
448                    intent: claim.intent.as_deref().map(page::cut),
449                    paths: claim.scope.len(),
450                    mode: claim.mode,
451                    acquired_lamport: claim.acquired_lamport,
452                });
453            }
454        });
455    }
456    for session in &state.sessions {
457        if summary && session.state != SessionState::Active {
458            continue;
459        }
460        let claims: Vec<ClaimView> = state
461            .claims
462            .iter()
463            .filter(|cl| cl.session == session.key)
464            .cloned()
465            .collect();
466        let claims_held = claims.iter().filter(|cl| cl.state.is_live()).count();
467        let mut info = session.info.clone();
468        if summary {
469            info.intent = info.intent.as_deref().map(page::cut);
470        }
471        let reviews = state
472            .reviews
473            .iter()
474            .filter(|r| r.session == session.key)
475            .map(|r| r.key.clone())
476            .collect();
477        if let Some(runtime) = runtimes.get_mut(&session.runtime) {
478            runtime.sessions.push(SessionNode {
479                key: session.key.clone(),
480                info,
481                state: session.state,
482                claims: if summary { Vec::new() } else { claims },
483                claims_held,
484                reviews,
485            });
486        }
487    }
488
489    let own_name = status
490        .peers
491        .iter()
492        .find(|p| p.local)
493        .map(|p| p.name.clone());
494    let mut machines: BTreeMap<String, MachineNode> = BTreeMap::new();
495    for (key, runtime) in runtimes {
496        let node = key.get(..32).unwrap_or_default().to_string();
497        let name = runtime
498            .link
499            .as_ref()
500            .map(|l| l.name.clone())
501            .or_else(|| (node == own_node).then(|| own_name.clone()).flatten())
502            .unwrap_or_else(|| node.chars().take(8).collect());
503        machines
504            .entry(node.clone())
505            .or_insert_with(|| MachineNode {
506                node: node.clone(),
507                name,
508                local: node == own_node,
509                runtimes: Vec::new(),
510            })
511            .runtimes
512            .push(runtime);
513    }
514    let mut machines: Vec<MachineNode> = machines.into_values().collect();
515    crate::order::canonical(&mut machines);
516    PeerTree {
517        active: status.active,
518        view: query.view(),
519        reason: status.reason,
520        runtime: status.runtime,
521        repository: status.repository.map(|r| r.id),
522        machines,
523        refused: status.refused,
524        digest: Some(whole.digest),
525    }
526}
527
528fn mesh_peers(ctx: &Context, query: Query) -> Result<PeerTree, CapabilityError> {
529    Ok(match ctx.mesh.cooperation() {
530        Some(c) => peer_tree(&c, &query),
531        None => PeerTree {
532            active: false,
533            view: query.view(),
534            reason: ctx.mesh.cooperation_status().reason,
535            runtime: None,
536            repository: None,
537            machines: Vec::new(),
538            refused: Vec::new(),
539            digest: None,
540        },
541    })
542}
543
544// ---------------------------------------------------------------- mesh.peer
545
546#[derive(Debug, Serialize, Deserialize, JsonSchema)]
547#[serde(deny_unknown_fields)]
548/// The input of `mesh.peer`.
549pub struct PeerInput {
550    /// The runtime, `<node>-<runtime>` (a node id alone matches its first runtime).
551    pub runtime: String,
552}
553
554impl BenchmarkCases for PeerInput {
555    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
556        vec![NamedCase::new(
557            "unknown",
558            PeerInput {
559                runtime: "0".repeat(49),
560            },
561        )]
562    }
563}
564
565/// The answer of `mesh.peer`.
566#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
567pub struct PeerDetail {
568    /// Whether the runtime is known here.
569    pub found: bool,
570    /// Why not, when it is not.
571    #[serde(default, skip_serializing_if = "Option::is_none")]
572    pub reason: Option<String>,
573    /// The machine it runs on.
574    #[serde(default, skip_serializing_if = "Option::is_none")]
575    pub machine: Option<String>,
576    /// The runtime, its link and its sessions.
577    #[serde(default, skip_serializing_if = "Option::is_none")]
578    pub runtime: Option<RuntimeNode>,
579    /// Refusals recorded against it.
580    pub refused: Vec<RefusedView>,
581}
582
583fn mesh_peer(ctx: &Context, input: PeerInput) -> Result<PeerDetail, CapabilityError> {
584    let Some(c) = ctx.mesh.cooperation() else {
585        return Ok(PeerDetail {
586            found: false,
587            reason: ctx.mesh.cooperation_status().reason,
588            machine: None,
589            runtime: None,
590            refused: Vec::new(),
591        });
592    };
593    let full = Query {
594        view: Some(View::Full),
595        ..Query::default()
596    };
597    let tree = peer_tree(&c, &full);
598    let wanted = input.runtime.trim();
599    let hit = tree.machines.iter().find_map(|m| {
600        m.runtimes
601            .iter()
602            .find(|r| r.runtime == wanted || (wanted.len() == 32 && r.runtime.starts_with(wanted)))
603            .map(|r| (m.name.clone(), r.clone()))
604    });
605    let refused = tree
606        .refused
607        .into_iter()
608        .filter(|r| r.runtime.as_deref() == Some(wanted))
609        .collect();
610    Ok(match hit {
611        Some((machine, runtime)) => PeerDetail {
612            found: true,
613            reason: None,
614            machine: Some(machine),
615            runtime: Some(runtime),
616            refused,
617        },
618        None => PeerDetail {
619            found: false,
620            reason: Some(format!("no runtime {wanted} is linked or heard here")),
621            machine: None,
622            runtime: None,
623            refused,
624        },
625    })
626}
627
628// ---------------------------------------------------------------- mesh.state
629
630/// One stream's liveness.
631#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
632pub struct StreamView {
633    /// The stream.
634    pub stream: StreamId,
635    /// Own, live or expired.
636    pub liveness: StreamLiveness,
637    /// Milliseconds since its beat rose.
638    #[serde(default, skip_serializing_if = "Option::is_none")]
639    pub last_beat_ms: Option<u64>,
640}
641
642/// The answer of `mesh.state`.
643#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
644pub struct MeshStateAnswer {
645    /// Whether cooperation runs.
646    pub active: bool,
647    /// What the answer carries: `summary` or `full`.
648    pub view: View,
649    /// Why not.
650    #[serde(default, skip_serializing_if = "Option::is_none")]
651    pub reason: Option<String>,
652    /// The summary, in the summary view: counts, each runtime's newest live claim, the
653    /// open reviews and the untaken handovers.
654    #[serde(default, skip_serializing_if = "Option::is_none")]
655    pub summary: Option<Summary>,
656    /// The folded state narrowed by the filters, in the full view; its digest is the whole
657    /// fold's.
658    #[serde(default, skip_serializing_if = "Option::is_none")]
659    pub state: Option<CooperationState>,
660    /// Every stream's liveness.
661    pub streams: Vec<StreamView>,
662}
663
664fn mesh_state(ctx: &Context, query: Query) -> Result<MeshStateAnswer, CapabilityError> {
665    let view = query.view();
666    let Some(c) = ctx.mesh.cooperation() else {
667        return Ok(MeshStateAnswer {
668            active: false,
669            view,
670            reason: ctx.mesh.cooperation_status().reason,
671            summary: None,
672            state: None,
673            streams: Vec::new(),
674        });
675    };
676    let state = page::narrow(&c.state(), &query);
677    let streams: Vec<StreamView> = c
678        .streams()
679        .into_iter()
680        .filter(|(stream, _, _)| query.admits_runtime(&stream.runtime_key()))
681        .map(|(stream, liveness, last_beat_ms)| StreamView {
682            stream,
683            liveness,
684            last_beat_ms,
685        })
686        .collect();
687    Ok(match view {
688        View::Summary => {
689            let beats: Vec<_> = streams
690                .iter()
691                .map(|s| (s.stream.runtime_key(), s.liveness, s.last_beat_ms))
692                .collect();
693            MeshStateAnswer {
694                active: true,
695                view,
696                reason: None,
697                summary: Some(page::summarize(&state, &beats)),
698                state: None,
699                // the streams are the summary's runtimes, told once
700                streams: Vec::new(),
701            }
702        }
703        View::Full => MeshStateAnswer {
704            active: true,
705            view,
706            reason: None,
707            summary: None,
708            state: Some(state),
709            streams,
710        },
711    })
712}
713
714// ---------------------------------------------------------------- mesh.events
715
716#[derive(Debug, Serialize, Deserialize, JsonSchema)]
717#[serde(deny_unknown_fields)]
718/// The input of `mesh.events`.
719pub struct MeshEventsInput {
720    /// Only events whose Lamport stamp is above this.
721    #[serde(default, skip_serializing_if = "Option::is_none")]
722    pub after: Option<u64>,
723    /// At most this many (default 100, at most 1000).
724    #[serde(default, skip_serializing_if = "Option::is_none")]
725    pub limit: Option<u64>,
726    /// The newest `limit` events above `after` instead of the first ones: what a reader who
727    /// wants the journal as it stands now asks for. Still in Lamport order, and `lamport` is
728    /// then the newest stamp the journal holds, so resuming after it reads only what comes
729    /// next. Without it the journal is read forward from `after`, a page at a time.
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub newest: Option<bool>,
732}
733
734impl BenchmarkCases for MeshEventsInput {
735    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
736        vec![NamedCase::new(
737            "default",
738            MeshEventsInput {
739                after: None,
740                limit: Some(100),
741                newest: None,
742            },
743        )]
744    }
745}
746
747/// The answer of `mesh.events`.
748#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
749pub struct EventList {
750    /// Whether cooperation runs.
751    pub active: bool,
752    /// How many events this answer carries.
753    pub count: usize,
754    /// The `after` of the next page: the stamp of the last event this page carries, or the
755    /// `after` that was asked when it carries none. A page never ends inside a run of equal
756    /// stamps, so resuming after it misses nothing (I2140).
757    pub lamport: u64,
758    /// The events, in Lamport order.
759    pub events: Vec<MeshEvent>,
760}
761
762fn mesh_events(ctx: &Context, input: MeshEventsInput) -> Result<EventList, CapabilityError> {
763    let Some(c) = ctx.mesh.cooperation() else {
764        return Ok(EventList {
765            active: false,
766            count: 0,
767            lamport: 0,
768            events: Vec::new(),
769        });
770    };
771    let limit = input.limit.unwrap_or(100).clamp(1, 1000) as usize;
772    let after = input.after.unwrap_or(0);
773    let events = if input.newest == Some(true) {
774        // the whole tail above `after`, in order, and then its end: the journal is in memory
775        // and bounded by its own retention, so this is a walk and not a second index
776        let mut all = c.journal().events_after(after, usize::MAX);
777        let from = all.len().saturating_sub(limit);
778        all.drain(..from);
779        all
780    } else {
781        c.journal().events_after(after, limit)
782    };
783    Ok(EventList {
784        active: true,
785        count: events.len(),
786        lamport: events.last().map_or(after, |e| e.lamport),
787        events,
788    })
789}
790
791// ---------------------------------------------------------------- mesh.verify
792
793fn mesh_verify(ctx: &Context, _: Empty) -> Result<MeshVerifyReport, CapabilityError> {
794    Ok(match ctx.mesh.cooperation() {
795        Some(c) => c.verify(),
796        None => MeshVerifyReport::inactive(
797            &ctx.mesh
798                .cooperation_status()
799                .reason
800                .unwrap_or_else(|| "cooperation is not running".into()),
801        ),
802    })
803}
804
805// ---------------------------------------------------------------- mesh.session.*
806
807#[derive(Debug, Serialize, Deserialize, JsonSchema)]
808#[serde(deny_unknown_fields)]
809/// The input of `mesh.session.open`: what a session says about itself.
810pub struct SessionInput {
811    /// The session id within this runtime; the calling MCP session's own when omitted.
812    #[serde(default, skip_serializing_if = "Option::is_none")]
813    pub session: Option<String>,
814    /// The client (`claude-code`, `codex`, `cli`); `cli` when omitted.
815    #[serde(default, skip_serializing_if = "Option::is_none")]
816    pub client: Option<String>,
817    /// The worker's name for itself.
818    #[serde(default, skip_serializing_if = "Option::is_none")]
819    pub worker: Option<String>,
820    /// What it is doing.
821    #[serde(default, skip_serializing_if = "Option::is_none")]
822    pub intent: Option<String>,
823    /// The task id.
824    #[serde(default, skip_serializing_if = "Option::is_none")]
825    pub task: Option<String>,
826    /// The issue.
827    #[serde(default, skip_serializing_if = "Option::is_none")]
828    pub issue: Option<String>,
829    /// The milestone.
830    #[serde(default, skip_serializing_if = "Option::is_none")]
831    pub milestone: Option<String>,
832    /// The branch.
833    #[serde(default, skip_serializing_if = "Option::is_none")]
834    pub branch: Option<String>,
835    /// The head.
836    #[serde(default, skip_serializing_if = "Option::is_none")]
837    pub head: Option<String>,
838    /// Whether the working tree is dirty.
839    #[serde(default, skip_serializing_if = "Option::is_none")]
840    pub dirty: Option<bool>,
841    /// The context revision it works from.
842    #[serde(default, skip_serializing_if = "Option::is_none")]
843    pub context: Option<String>,
844}
845
846impl BenchmarkCases for SessionInput {
847    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
848        vec![NamedCase::new(
849            "named",
850            SessionInput {
851                session: Some("bench".into()),
852                client: None,
853                worker: None,
854                intent: Some("benchmark".into()),
855                task: None,
856                issue: None,
857                milestone: None,
858                branch: None,
859                head: None,
860                dirty: None,
861                context: None,
862            },
863        )]
864    }
865}
866
867fn mesh_session_open(ctx: &Context, input: SessionInput) -> Result<Written, CapabilityError> {
868    let c = cooperation(ctx)?;
869    let session = session_of(ctx, input.session)?;
870    c.open_session(SessionInfo {
871        session,
872        client: input.client.unwrap_or_else(|| "cli".into()),
873        worker: input.worker,
874        intent: input.intent,
875        task: input.task,
876        issue: input.issue,
877        milestone: input.milestone,
878        checkout: None,
879        branch: input.branch,
880        head: input.head,
881        dirty: input.dirty,
882        context: input.context,
883    })
884    .map_err(map_error)
885}
886
887#[derive(Debug, Serialize, Deserialize, JsonSchema)]
888#[serde(deny_unknown_fields)]
889/// The input of `mesh.session.close`.
890pub struct SessionCloseInput {
891    /// The session; the calling MCP session's own when omitted.
892    #[serde(default, skip_serializing_if = "Option::is_none")]
893    pub session: Option<String>,
894}
895
896impl BenchmarkCases for SessionCloseInput {
897    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
898        vec![NamedCase::new(
899            "named",
900            SessionCloseInput {
901                session: Some("bench".into()),
902            },
903        )]
904    }
905}
906
907fn mesh_session_close(ctx: &Context, input: SessionCloseInput) -> Result<Written, CapabilityError> {
908    let c = cooperation(ctx)?;
909    let session = session_of(ctx, input.session)?;
910    c.close_session(&session).map_err(map_error)
911}
912
913// ---------------------------------------------------------------- mesh.claim / release
914
915#[derive(Debug, Serialize, Deserialize, JsonSchema)]
916#[serde(deny_unknown_fields)]
917/// The input of `mesh.claim`.
918pub struct ClaimInput {
919    /// The claiming session; the calling MCP session's own when omitted. Opened when new.
920    #[serde(default, skip_serializing_if = "Option::is_none")]
921    pub session: Option<String>,
922    /// The client, for a session this opens; `cli` when omitted.
923    #[serde(default, skip_serializing_if = "Option::is_none")]
924    pub client: Option<String>,
925    /// Repository-relative paths.
926    pub scope: Vec<String>,
927    /// What the claim is for.
928    #[serde(default, skip_serializing_if = "Option::is_none")]
929    pub intent: Option<String>,
930    /// `exclusive` (the default) or `advisory`.
931    #[serde(default, skip_serializing_if = "Option::is_none")]
932    pub mode: Option<ClaimMode>,
933    /// The issue the claim is for.
934    #[serde(default, skip_serializing_if = "Option::is_none")]
935    pub issue: Option<String>,
936    /// The task, for a session this opens.
937    #[serde(default, skip_serializing_if = "Option::is_none")]
938    pub task: Option<String>,
939}
940
941impl BenchmarkCases for ClaimInput {
942    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
943        vec![NamedCase::new(
944            "exclusive",
945            ClaimInput {
946                session: Some("bench".into()),
947                client: None,
948                scope: vec!["docs".into()],
949                intent: None,
950                mode: None,
951                issue: None,
952                task: None,
953            },
954        )]
955    }
956}
957
958fn mesh_claim(ctx: &Context, input: ClaimInput) -> Result<Written, CapabilityError> {
959    let c = cooperation(ctx)?;
960    let session = session_of(ctx, input.session)?;
961    let info = SessionInfo {
962        session,
963        client: input.client.unwrap_or_else(|| "cli".into()),
964        task: input.task,
965        issue: input.issue.clone(),
966        intent: input.intent.clone(),
967        ..SessionInfo::default()
968    };
969    c.claim(
970        &info,
971        input.scope,
972        input.intent,
973        input.mode.unwrap_or_default(),
974        input.issue,
975    )
976    .map_err(map_error)
977}
978
979#[derive(Debug, Serialize, Deserialize, JsonSchema)]
980#[serde(deny_unknown_fields)]
981/// The input of `mesh.release`.
982pub struct ReleaseInput {
983    /// The claim's key, `<stream>/<claim>`, as `mesh.claim` answered it.
984    pub claim: String,
985}
986
987impl BenchmarkCases for ReleaseInput {
988    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
989        vec![NamedCase::new(
990            "unknown",
991            ReleaseInput {
992                claim: "none/c-0".into(),
993            },
994        )]
995    }
996}
997
998fn mesh_release(ctx: &Context, input: ReleaseInput) -> Result<Written, CapabilityError> {
999    cooperation(ctx)?.release(&input.claim).map_err(map_error)
1000}
1001
1002// ---------------------------------------------------------------- mesh.handover.*
1003
1004#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1005#[serde(deny_unknown_fields)]
1006/// The input of `mesh.handover.publish`.
1007pub struct HandoverPublishInput {
1008    /// The record, relative to the repository root and inside
1009    /// `.ai/local/state/handovers/`; the newest record when omitted. Nothing outside that
1010    /// directory is ever read by this command.
1011    #[serde(default, skip_serializing_if = "Option::is_none")]
1012    pub path: Option<String>,
1013    /// The issue the handover belongs to.
1014    #[serde(default, skip_serializing_if = "Option::is_none")]
1015    pub issue: Option<String>,
1016    /// The milestone it belongs to.
1017    #[serde(default, skip_serializing_if = "Option::is_none")]
1018    pub milestone: Option<String>,
1019}
1020
1021impl BenchmarkCases for HandoverPublishInput {
1022    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
1023        vec![NamedCase::new(
1024            "latest",
1025            HandoverPublishInput {
1026                path: None,
1027                issue: None,
1028                milestone: None,
1029            },
1030        )]
1031    }
1032}
1033
1034fn mesh_handover_publish(
1035    ctx: &Context,
1036    input: HandoverPublishInput,
1037) -> Result<Written, CapabilityError> {
1038    let c = cooperation(ctx)?;
1039    let root = std::path::Path::new(&ctx.index.repository.root);
1040    let dir = crate::mesh::handover::directory(root);
1041    let path = match input.path {
1042        None => crate::mesh::handover::latest(root).map_err(CapabilityError::NotFound)?,
1043        Some(p) => {
1044            let candidate = root.join(p.trim_start_matches('/'));
1045            let (Ok(real), Ok(real_dir)) = (candidate.canonicalize(), dir.canonicalize()) else {
1046                return Err(CapabilityError::NotFound(format!(
1047                    "no handover record at {p}"
1048                )));
1049            };
1050            if !real.starts_with(&real_dir) {
1051                return Err(CapabilityError::Refused(format!(
1052                    "{p} is not a handover record: only files under .ai/local/state/handovers/ are published"
1053                )));
1054            }
1055            real
1056        }
1057    };
1058    let body = crate::mesh::handover::to_body(&path, input.issue, input.milestone)
1059        .map_err(CapabilityError::InvalidInput)?;
1060    c.publish_handover(body).map_err(map_error)
1061}
1062
1063#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1064#[serde(deny_unknown_fields)]
1065/// The input of `mesh.handover.consume`.
1066pub struct HandoverConsumeInput {
1067    /// The handover's id (its content digest).
1068    pub handover: String,
1069    /// The consuming session; the calling MCP session's own when omitted.
1070    #[serde(default, skip_serializing_if = "Option::is_none")]
1071    pub session: Option<String>,
1072    /// Whether to write it as a local handover record `majordomus handover --resolve`
1073    /// finds (default true).
1074    #[serde(default, skip_serializing_if = "Option::is_none")]
1075    pub materialize: Option<bool>,
1076}
1077
1078impl BenchmarkCases for HandoverConsumeInput {
1079    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
1080        vec![NamedCase::new(
1081            "unknown",
1082            HandoverConsumeInput {
1083                handover: "0".repeat(32),
1084                session: Some("bench".into()),
1085                materialize: Some(false),
1086            },
1087        )]
1088    }
1089}
1090
1091/// The answer of `mesh.handover.consume`.
1092#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1093pub struct ConsumeAnswer {
1094    /// The handover, as the journal holds it.
1095    pub handover: HandoverView,
1096    /// The consumption event.
1097    pub written: Written,
1098    /// The local record written, relative to the repository root, when materialized.
1099    #[serde(default, skip_serializing_if = "Option::is_none")]
1100    pub path: Option<String>,
1101}
1102
1103fn mesh_handover_consume(
1104    ctx: &Context,
1105    input: HandoverConsumeInput,
1106) -> Result<ConsumeAnswer, CapabilityError> {
1107    let c = cooperation(ctx)?;
1108    let session = session_of(ctx, input.session)?;
1109    let (view, written) = c
1110        .consume_handover(&input.handover, &session)
1111        .map_err(map_error)?;
1112    let root = std::path::Path::new(&ctx.index.repository.root);
1113    let path = if input.materialize.unwrap_or(true) {
1114        let written =
1115            crate::mesh::handover::materialize(root, &view).map_err(CapabilityError::Internal)?;
1116        Some(
1117            written
1118                .strip_prefix(root)
1119                .unwrap_or(&written)
1120                .display()
1121                .to_string(),
1122        )
1123    } else {
1124        None
1125    };
1126    Ok(ConsumeAnswer {
1127        handover: view,
1128        written,
1129        path,
1130    })
1131}
1132
1133// ---------------------------------------------------------------- mesh.review.*
1134
1135#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1136#[serde(deny_unknown_fields)]
1137/// The input of `mesh.review.request`.
1138pub struct ReviewRequestInput {
1139    /// The requesting session; the calling MCP session's own when omitted.
1140    #[serde(default, skip_serializing_if = "Option::is_none")]
1141    pub session: Option<String>,
1142    /// What to review: a branch, a commit, a pull request.
1143    pub subject: String,
1144    /// The paths it covers.
1145    #[serde(default)]
1146    pub scope: Vec<String>,
1147    /// The issue.
1148    #[serde(default, skip_serializing_if = "Option::is_none")]
1149    pub issue: Option<String>,
1150    /// The runtime asked (`<node>-<runtime>`); anyone when omitted.
1151    #[serde(default, skip_serializing_if = "Option::is_none")]
1152    pub reviewer: Option<String>,
1153    /// What the reviewer needs beyond the subject: evidence, commands, the question. Up to
1154    /// 8 KiB, several lines.
1155    #[serde(default, skip_serializing_if = "Option::is_none")]
1156    pub body: Option<String>,
1157}
1158
1159impl BenchmarkCases for ReviewRequestInput {
1160    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
1161        vec![NamedCase::new(
1162            "open",
1163            ReviewRequestInput {
1164                session: Some("bench".into()),
1165                subject: "feature/bench".into(),
1166                scope: vec![],
1167                issue: None,
1168                reviewer: None,
1169                body: None,
1170            },
1171        )]
1172    }
1173}
1174
1175fn mesh_review_request(
1176    ctx: &Context,
1177    input: ReviewRequestInput,
1178) -> Result<Written, CapabilityError> {
1179    let c = cooperation(ctx)?;
1180    let session = session_of(ctx, input.session)?;
1181    c.request_review(
1182        &session,
1183        input.subject,
1184        input.scope,
1185        input.issue,
1186        input.reviewer,
1187        input.body,
1188    )
1189    .map_err(map_error)
1190}
1191
1192#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1193#[serde(deny_unknown_fields)]
1194/// The input of `mesh.review.answer`.
1195pub struct ReviewAnswerInput {
1196    /// The request's key, `<stream>/<review>`.
1197    pub request: String,
1198    /// The answering session; the calling MCP session's own when omitted.
1199    #[serde(default, skip_serializing_if = "Option::is_none")]
1200    pub session: Option<String>,
1201    /// `approved`, `changes_requested` or `commented`.
1202    pub verdict: String,
1203    /// The note.
1204    #[serde(default, skip_serializing_if = "Option::is_none")]
1205    pub note: Option<String>,
1206}
1207
1208impl BenchmarkCases for ReviewAnswerInput {
1209    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
1210        vec![NamedCase::new(
1211            "unknown",
1212            ReviewAnswerInput {
1213                request: "none/r-0".into(),
1214                session: Some("bench".into()),
1215                verdict: "commented".into(),
1216                note: None,
1217            },
1218        )]
1219    }
1220}
1221
1222fn mesh_review_answer(ctx: &Context, input: ReviewAnswerInput) -> Result<Written, CapabilityError> {
1223    let c = cooperation(ctx)?;
1224    let session = session_of(ctx, input.session)?;
1225    c.answer_review(&input.request, &session, input.verdict, input.note)
1226        .map_err(map_error)
1227}
1228
1229#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1230#[serde(deny_unknown_fields)]
1231/// The input of `mesh.review.withdraw`.
1232pub struct ReviewWithdrawInput {
1233    /// The request's key, `<stream>/<review>`.
1234    pub request: String,
1235    /// The withdrawing session of the asking runtime; the calling MCP session's own when
1236    /// omitted.
1237    #[serde(default, skip_serializing_if = "Option::is_none")]
1238    pub session: Option<String>,
1239    /// The requester's result or reason, up to 8 KiB: what came of it, or why it is void.
1240    #[serde(default, skip_serializing_if = "Option::is_none")]
1241    pub note: Option<String>,
1242}
1243
1244impl BenchmarkCases for ReviewWithdrawInput {
1245    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
1246        vec![NamedCase::new(
1247            "unknown",
1248            ReviewWithdrawInput {
1249                request: "none/r-0".into(),
1250                session: Some("bench".into()),
1251                note: Some("superseded".into()),
1252            },
1253        )]
1254    }
1255}
1256
1257fn mesh_review_withdraw(
1258    ctx: &Context,
1259    input: ReviewWithdrawInput,
1260) -> Result<Written, CapabilityError> {
1261    let c = cooperation(ctx)?;
1262    let session = session_of(ctx, input.session)?;
1263    c.withdraw_review(&input.request, &session, input.note)
1264        .map_err(map_error)
1265}
1266
1267// ---------------------------------------------------------------- mesh.link.*
1268
1269impl BenchmarkCases for Signed {
1270    fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> {
1271        // Malformed: refused before any signature is checked or any state is touched.
1272        vec![NamedCase::new(
1273            "malformed",
1274            Signed {
1275                body: serde_json::json!({}),
1276                sig: String::new(),
1277            },
1278        )]
1279    }
1280}
1281
1282fn not_active(ctx: &Context) -> LinkReply {
1283    LinkReply::refused(
1284        RefusalCode::NotActive,
1285        ctx.mesh
1286            .cooperation_status()
1287            .reason
1288            .unwrap_or_else(|| "cooperation is not running".into()),
1289    )
1290}
1291
1292fn mesh_link_hello(ctx: &Context, input: Signed) -> Result<LinkReply, CapabilityError> {
1293    Ok(match ctx.mesh.cooperation() {
1294        Some(c) => c.accept_hello(&input),
1295        None => not_active(ctx),
1296    })
1297}
1298
1299fn mesh_link_sync(ctx: &Context, input: Signed) -> Result<LinkReply, CapabilityError> {
1300    Ok(match ctx.mesh.cooperation() {
1301        Some(c) => c.accept_sync(&input),
1302        None => not_active(ctx),
1303    })
1304}
1305
1306/// The module.
1307pub fn module() -> ModuleDescriptor {
1308    module! {
1309        id: "mesh",
1310        title: "Mesh",
1311        description: "The mesh of this process: discovery — authenticated observations of other running Majordomus instances in one registry — and cooperation — authenticated links to trusted runtimes of the same repository, replicating sessions, claims, handovers and reviews through one journal every runtime folds into the same state. Discovery grants nothing; a link is admitted per peer, and nothing a peer sends executes anything here.",
1312        stability: Stability::Experimental,
1313        capabilities: [
1314            capability! {
1315                id: "mesh.status",
1316                title: "The mesh, at a glance",
1317                description: "Whether the mesh runs in this process and why not when it does not; this node's public identity; every discovery provider with its state and counters; the registry's tallies; and the refused datagrams by reason. The one status every surface shows.",
1318                input: Empty,
1319                output: MeshStatus,
1320                stability: Stability::Experimental,
1321                exposure: Exposure {
1322                    mcp: Some(crate::capability::model::McpExposure {
1323                        tool: Some("majordomus_mesh".into()),
1324                        resource: Some(crate::capability::model::McpResource {
1325                            uri: MESH_URI.into(),
1326                            name: "mesh".into(),
1327                        }),
1328                    }),
1329                    http: get("/api/v1/mesh"),
1330                    cli: None,
1331                },
1332                tags: ["mesh", "coordination", "discovery"],
1333                handler: mesh_status,
1334            },
1335            capability! {
1336                id: "mesh.nodes",
1337                title: "The discovered nodes",
1338                description: "Every runtime this process has observed, one record per node and runtime slot, deduplicated across every discovery source, in node-id order: identity, trust, presence, endpoints, capabilities, repositories, provenance and versions. In memory, gone with the process.",
1339                input: Empty,
1340                output: NodeList,
1341                stability: Stability::Experimental,
1342                exposure: Exposure { mcp: mcp("majordomus_mesh_nodes"), http: get("/api/v1/mesh/nodes"), cli: None },
1343                tags: ["mesh", "coordination", "discovery"],
1344                handler: mesh_nodes,
1345            },
1346            capability! {
1347                id: "mesh.identity",
1348                title: "This machine's node identity",
1349                description: "The node identity kept under the user's state directory, public half only: node id, public key, display name. The signing key appears in no projection. Absent is an answer, not an error — the identity is created when a mesh first activates.",
1350                input: Empty,
1351                output: MeshIdentityReport,
1352                stability: Stability::Experimental,
1353                exposure: Exposure {
1354                    mcp: mcp("majordomus_mesh_identity"),
1355                    http: get("/api/v1/mesh/identity"),
1356                    cli: Some(CliExposure { path: vec!["mesh".into(), "identity".into()] }),
1357                },
1358                tags: ["mesh", "identity"],
1359                handler: mesh_identity,
1360            },
1361            capability! {
1362                id: "mesh.doctor",
1363                title: "The mesh self-check",
1364                description: "Every prerequisite proved on this machine alone — the declaration parses, the identity loads and, under an allowlist, is on it, the repository has a mesh identity, a UDP socket binds, the multicast group joins, broadcast enables, and the discovery and link protocols sign and verify end to end in memory — and then the server's own verdict: in a shared server, whether an enabled declaration actually activated the mesh and, when it did not, the server's reason, which fails the report. Each failed check names its impact and its remedy. Deterministic, no second node required.",
1365                input: Empty,
1366                output: MeshDoctorReport,
1367                stability: Stability::Experimental,
1368                exposure: Exposure {
1369                    mcp: mcp("majordomus_mesh_doctor"),
1370                    http: get("/api/v1/mesh/doctor"),
1371                    cli: Some(CliExposure { path: vec!["mesh".into(), "doctor".into()] }),
1372                },
1373                tags: ["mesh", "diagnostics"],
1374                handler: mesh_doctor,
1375            },
1376            capability! {
1377                id: "mesh.firewall",
1378                title: "The host firewall, as the mesh needs it",
1379                description: "What inbound traffic the declaration implies this machine must admit — the multicast group's port, the broadcast port when it is a permitted fallback, every declared hub port whose address is this machine's, and the server's own port when it listens beyond loopback — from the private networks the declaration names; which firewall front this host runs (ufw, nftables, the macOS application firewall); the commands that admit the plan there; what the firewall itself says about the plan now (present, missing, inactive, or unobservable without root); and what the kernel logged it dropping toward those ports in the last five minutes. Fails when a rule is observed missing or a drop was logged. Reads the host; changes nothing.",
1380                input: FirewallInput,
1381                output: FirewallReport,
1382                stability: Stability::Experimental,
1383                exposure: Exposure {
1384                    mcp: mcp("majordomus_mesh_firewall"),
1385                    http: get("/api/v1/mesh/firewall"),
1386                    cli: Some(CliExposure { path: vec!["mesh".into(), "firewall".into()] }),
1387                },
1388                tags: ["mesh", "diagnostics", "firewall"],
1389                cache: CachePolicy::Disabled,
1390                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::ExternalDependency },
1391                handler: mesh_firewall,
1392            },
1393            capability! {
1394                id: "mesh.firewall.apply",
1395                kind: CapabilityKind::Command,
1396                title: "Admit the mesh through the host firewall",
1397                description: "Run the commands mesh.firewall renders, on this host, as root: one allow per rule and source network on ufw or nftables, the executable admitted on the macOS application firewall; nothing else is touched, and every rule written carries the comment `majordomus mesh` so it can be told from an operator's own. Refuses, running nothing, without root or without a backend; records every command with its exit and output; and asks the firewall again afterwards, so the verdict is the firewall's. Offered on the command line only: it runs a privileged host tool, which nothing reachable over HTTP or MCP may do.",
1398                input: FirewallInput,
1399                output: FirewallApplyReport,
1400                stability: Stability::Experimental,
1401                exposure: Exposure {
1402                    mcp: None,
1403                    http: None,
1404                    cli: Some(CliExposure { path: vec!["mesh".into(), "firewall".into(), "apply".into()] }),
1405                },
1406                tags: ["mesh", "firewall"],
1407                cache: CachePolicy::Disabled,
1408                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::Destructive },
1409                handler: mesh_firewall_apply,
1410            },
1411            capability! {
1412                id: "mesh.register",
1413                kind: CapabilityKind::Command,
1414                title: "Register with this node's mesh",
1415                description: "Present one signed envelope; it is verified exactly as a datagram — bounds, staleness, signature, trust policy — and recorded as a rendezvous observation when it holds. The answer carries this node's own envelope and the candidates its registry holds, each verifiable end to end on its own signature. Registration grants nothing: the caller becomes a record, never an authorization. Changes this process's memory only.",
1416                input: RegisterInput,
1417                output: RegisterAnswer,
1418                stability: Stability::Experimental,
1419                exposure: Exposure { mcp: mcp("majordomus_mesh_register"), http: post(REGISTER_PATH), cli: None },
1420                tags: ["mesh", "coordination", "discovery"],
1421                handler: mesh_register,
1422            }
1423            // the caller is another machine by design, admitted by the signature on its envelope
1424            // and the trust policy, never by its address (ADR 0126)
1425            .authenticates_its_input(),
1426            capability! {
1427                id: "mesh.cooperation",
1428                title: "Cooperation, at a glance",
1429                description: "Whether this runtime cooperates and why not when it does not: its runtime and stream, the repository identity links are matched on, the link protocol and features, the heartbeat and expiry, every linked peer with its link state, last exchange, round trip and failures, every refused candidate with the rule that refused it, and the counters of handshakes, syncs, reconnects, expiries, replicated and refused events.",
1430                input: Empty,
1431                output: CooperationStatus,
1432                stability: Stability::Experimental,
1433                exposure: Exposure { mcp: mcp("majordomus_mesh_cooperation"), http: get("/api/v1/mesh/cooperation"), cli: None },
1434                tags: ["mesh", "coordination", "cooperation"],
1435                handler: mesh_cooperation,
1436            },
1437            capability! {
1438                id: "mesh.peers",
1439                title: "Who cooperates, machine by machine",
1440                description: "Machine → runtime → session → claim: every runtime this one is linked to or has heard through the journal, grouped by machine, local first; each runtime with its liveness, the milliseconds since its beat rose, its link and its newest live claim. The default `summary` view lists only the active sessions, each with its live claims counted and its intent cut; `view=full` lists every session with every claim. `session` and `runtime` narrow either. Remote and local runtimes share one shape, and the whole fold's digest closes the answer: equal digests, equal state.",
1441                input: Query,
1442                output: PeerTree,
1443                stability: Stability::Experimental,
1444                exposure: Exposure { mcp: mcp("majordomus_mesh_peers"), http: get("/api/v1/mesh/peers"), cli: None },
1445                tags: ["mesh", "coordination", "cooperation"],
1446                handler: mesh_peers,
1447            },
1448            capability! {
1449                id: "mesh.peer",
1450                title: "One runtime",
1451                description: "One runtime by its key (`<node>-<runtime>`, or a node id for its first runtime): the machine it runs on, its liveness, its link from here, its sessions and claims, and any refusal recorded against it. Not found is an answer with the reason.",
1452                input: PeerInput,
1453                output: PeerDetail,
1454                stability: Stability::Experimental,
1455                exposure: Exposure { mcp: mcp("majordomus_mesh_peer"), http: get("/api/v1/mesh/peer"), cli: None },
1456                tags: ["mesh", "coordination", "cooperation"],
1457                handler: mesh_peer,
1458            },
1459            capability! {
1460                id: "mesh.state",
1461                title: "The state every linked runtime converges on",
1462                description: "The journal folded. The default `summary` view is bounded whatever the history: sessions and claims counted by standing, each runtime with its liveness, its counts and its newest live claim, the open review requests and the handovers nobody has taken, and the digest two runtimes compare. `view=full` is every session, every claim with its standing (held, released, expired, conflicted with its winner), the advisory overlaps, every handover with who consumed it, every review request with its answers, and every stream's liveness on this runtime's clock. `claims=live`, `after` (a Lamport stamp), `session` and `runtime` narrow either view; the digest is always the whole fold's.",
1463                input: Query,
1464                output: MeshStateAnswer,
1465                stability: Stability::Experimental,
1466                exposure: Exposure { mcp: mcp("majordomus_mesh_state"), http: get("/api/v1/mesh/state"), cli: None },
1467                tags: ["mesh", "coordination", "cooperation"],
1468                handler: mesh_state,
1469            },
1470            capability! {
1471                id: "mesh.events",
1472                title: "The cooperation journal",
1473                description: "The journal's events above a Lamport stamp, in Lamport order, at most a page: each with its stream, sequence, stamp, repository, signing key, kind and body. The answer's lamport is the next page's `after`.",
1474                input: MeshEventsInput,
1475                output: EventList,
1476                stability: Stability::Experimental,
1477                exposure: Exposure { mcp: mcp("majordomus_mesh_events"), http: get("/api/v1/mesh/events"), cli: None },
1478                tags: ["mesh", "coordination", "cooperation"],
1479                handler: mesh_events,
1480            },
1481            capability! {
1482                id: "mesh.verify",
1483                kind: CapabilityKind::Command,
1484                title: "Prove cooperation now",
1485                description: "Run a live verification: cooperation is active, the repository identity resolves, the endpoints are reachable beyond loopback, the heartbeat beats, the journal holds no stuck gap; then one sync round with every peer this runtime dials, timed, and whether both sides now hold the same high-water marks. Each failed check names its impact and remedy. Changes nothing but the journal's replication.",
1486                input: Empty,
1487                output: MeshVerifyReport,
1488                stability: Stability::Experimental,
1489                exposure: Exposure { mcp: mcp("majordomus_mesh_verify"), http: post("/api/v1/mesh/verify"), cli: None },
1490                tags: ["mesh", "diagnostics", "cooperation"],
1491                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::ExternalDependency },
1492                handler: mesh_verify,
1493            },
1494            capability! {
1495                id: "mesh.session.open",
1496                kind: CapabilityKind::Command,
1497                title: "Open or update a session on the mesh",
1498                description: "Say what a session of this runtime is: its client, worker, intent, task, issue, milestone, branch, head, dirtiness and context revision. Replicated to every linked runtime; a later word replaces an earlier one. Writes this runtime's journal only.",
1499                input: SessionInput,
1500                output: Written,
1501                stability: Stability::Experimental,
1502                exposure: Exposure { mcp: mcp("majordomus_mesh_session_open"), http: post("/api/v1/mesh/sessions"), cli: None },
1503                tags: ["mesh", "coordination", "cooperation"],
1504                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1505                handler: mesh_session_open,
1506            },
1507            capability! {
1508                id: "mesh.session.close",
1509                kind: CapabilityKind::Command,
1510                title: "Close a session on the mesh",
1511                description: "End a session of this runtime; every claim it holds ends with it, on every linked runtime. Writes this runtime's journal only.",
1512                input: SessionCloseInput,
1513                output: Written,
1514                stability: Stability::Experimental,
1515                exposure: Exposure { mcp: mcp("majordomus_mesh_session_close"), http: post("/api/v1/mesh/sessions/close"), cli: None },
1516                tags: ["mesh", "coordination", "cooperation"],
1517                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1518                handler: mesh_session_close,
1519            },
1520            capability! {
1521                id: "mesh.claim",
1522                kind: CapabilityKind::Command,
1523                title: "Claim a scope across the mesh",
1524                description: "Claim repository paths for a session. An exclusive claim that meets a live exclusive claim of another session — on this runtime or any runtime this one has heard — is refused as `claim_conflict` with the claims it meets; an advisory claim is recorded and its overlaps reported. A claim lives while its session is open and its runtime beats: a crashed holder's claim expires everywhere on its own. Writes this runtime's journal only.",
1525                input: ClaimInput,
1526                output: Written,
1527                stability: Stability::Experimental,
1528                exposure: Exposure { mcp: mcp("majordomus_mesh_claim"), http: post("/api/v1/mesh/claims"), cli: None },
1529                tags: ["mesh", "coordination", "cooperation"],
1530                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1531                handler: mesh_claim,
1532            },
1533            capability! {
1534                id: "mesh.release",
1535                kind: CapabilityKind::Command,
1536                title: "Release a claim",
1537                description: "Release a claim this runtime's current run holds, by its key. A claim written elsewhere is refused as `not_own`: only its holder releases it, and a dead holder's claim expires instead. Writes this runtime's journal only.",
1538                input: ReleaseInput,
1539                output: Written,
1540                stability: Stability::Experimental,
1541                exposure: Exposure { mcp: mcp("majordomus_mesh_release"), http: post("/api/v1/mesh/claims/release"), cli: None },
1542                tags: ["mesh", "coordination", "cooperation"],
1543                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1544                handler: mesh_release,
1545            },
1546            capability! {
1547                id: "mesh.handover.publish",
1548                kind: CapabilityKind::Command,
1549                title: "Publish a handover to the mesh",
1550                description: "Publish a handover record of this checkout — the newest, or the one named under .ai/local/state/handovers/ — to every linked runtime: its task, branch, head and time from its front matter, the issue and milestone given, and its Markdown body, bounded and identified by the body's digest. No path of this machine travels, and no file outside the handovers directory is ever read.",
1551                input: HandoverPublishInput,
1552                output: Written,
1553                stability: Stability::Experimental,
1554                exposure: Exposure { mcp: mcp("majordomus_mesh_handover_publish"), http: post("/api/v1/mesh/handovers"), cli: None },
1555                tags: ["mesh", "continuity", "cooperation"],
1556                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1557                handler: mesh_handover_publish,
1558            },
1559            capability! {
1560                id: "mesh.handover.consume",
1561                kind: CapabilityKind::Command,
1562                title: "Consume a handover from the mesh",
1563                description: "Take a handover another runtime published: record the consumption on the mesh, and write it into this checkout's handovers directory as a record `majordomus handover --resolve` finds on the same branch — once, however often it is consumed.",
1564                input: HandoverConsumeInput,
1565                output: ConsumeAnswer,
1566                stability: Stability::Experimental,
1567                exposure: Exposure { mcp: mcp("majordomus_mesh_handover_consume"), http: post("/api/v1/mesh/handovers/consume"), cli: None },
1568                tags: ["mesh", "continuity", "cooperation"],
1569                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1570                handler: mesh_handover_consume,
1571            },
1572            capability! {
1573                id: "mesh.review.request",
1574                kind: CapabilityKind::Command,
1575                title: "Ask the mesh for a review",
1576                description: "Ask for a review of a branch, commit or pull request, optionally of one named runtime — which must be linked and carry the `reviews` feature, or the request is refused as `feature_unsupported`. Replicated to every linked runtime. Writes this runtime's journal only.",
1577                input: ReviewRequestInput,
1578                output: Written,
1579                stability: Stability::Experimental,
1580                exposure: Exposure { mcp: mcp("majordomus_mesh_review_request"), http: post("/api/v1/mesh/reviews"), cli: None },
1581                tags: ["mesh", "coordination", "cooperation"],
1582                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1583                handler: mesh_review_request,
1584            },
1585            capability! {
1586                id: "mesh.review.answer",
1587                kind: CapabilityKind::Command,
1588                title: "Answer a review request",
1589                description: "Answer a review request from any runtime with approved, changes_requested or commented, and a note. Replicated to every linked runtime. Writes this runtime's journal only.",
1590                input: ReviewAnswerInput,
1591                output: Written,
1592                stability: Stability::Experimental,
1593                exposure: Exposure { mcp: mcp("majordomus_mesh_review_answer"), http: post("/api/v1/mesh/reviews/answer"), cli: None },
1594                tags: ["mesh", "coordination", "cooperation"],
1595                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1596                handler: mesh_review_answer,
1597            },
1598            capability! {
1599                id: "mesh.review.withdraw",
1600                kind: CapabilityKind::Command,
1601                title: "Withdraw a review request",
1602                description: "End a review request this runtime asked for — done, void or superseded — with an optional result note the request then carries. Any session of the asking runtime may (a reconnected asker is another session of it); no other runtime may, and it is never counted as an answer. An unanswered request also reads expired while its runtime has stopped beating. Replicated to every linked runtime. Writes this runtime's journal only.",
1603                input: ReviewWithdrawInput,
1604                output: Written,
1605                stability: Stability::Experimental,
1606                exposure: Exposure { mcp: mcp("majordomus_mesh_review_withdraw"), http: post("/api/v1/mesh/reviews/withdraw"), cli: None },
1607                tags: ["mesh", "coordination", "cooperation"],
1608                benchmark: BenchmarkPolicy::Waived { reason: WaiverReason::TransientState },
1609                handler: mesh_review_withdraw,
1610            },
1611            capability! {
1612                id: "mesh.link.hello",
1613                kind: CapabilityKind::Command,
1614                title: "Open a link (the handshake)",
1615                description: "The link handshake another runtime posts: a signed hello carrying its runtime card, a fresh nonce and its protocol range. Refused, typed, when malformed, oversized, mis-signed, stale, replayed, of an unsupported protocol, of another repository, untrusted, or this runtime itself; otherwise answered with a signed welcome: this runtime's card, a link id, its marks and the echoed nonce. Changes this process's link table only.",
1616                input: Signed,
1617                output: LinkReply,
1618                stability: Stability::Experimental,
1619                exposure: Exposure { mcp: None, http: post(HELLO_PATH), cli: None },
1620                tags: ["mesh", "cooperation", "link"],
1621                handler: mesh_link_hello,
1622            }
1623            // a hello is signed by the dialling node and verified before a link is admitted
1624            .authenticates_its_input(),
1625            capability! {
1626                id: "mesh.link.sync",
1627                kind: CapabilityKind::Command,
1628                title: "One sync round of a link",
1629                description: "The replication round a linked runtime posts every heartbeat, signed under its link id with a rising counter: its marks and the events this runtime lacks. Ingested — verified end to end, deduplicated, applied in stream order — and answered, signed, with this runtime's marks and the events the peer lacks. An unknown link or a restarted peer is told to say hello again.",
1630                input: Signed,
1631                output: LinkReply,
1632                stability: Stability::Experimental,
1633                exposure: Exposure { mcp: None, http: post(SYNC_PATH), cli: None },
1634                tags: ["mesh", "cooperation", "link"],
1635                handler: mesh_link_sync,
1636            }
1637            // a sync round is signed under the link's key and verified before anything is ingested
1638            .authenticates_its_input(),
1639        ],
1640    }
1641}
1642
1643#[cfg(test)]
1644mod tests {
1645    use super::*;
1646
1647    /// The declaration is the only place these names exist; this is the assertion a
1648    /// refactor that dropped a projection would fail.
1649    #[test]
1650    fn the_declaration_yields_the_projections_it_claims() {
1651        let m = module();
1652        assert_eq!(m.id.as_str(), "mesh");
1653        let expected: &[(&str, Option<&str>, &str)] = &[
1654            ("mesh.status", Some("majordomus_mesh"), "/api/v1/mesh"),
1655            (
1656                "mesh.nodes",
1657                Some("majordomus_mesh_nodes"),
1658                "/api/v1/mesh/nodes",
1659            ),
1660            (
1661                "mesh.identity",
1662                Some("majordomus_mesh_identity"),
1663                "/api/v1/mesh/identity",
1664            ),
1665            (
1666                "mesh.doctor",
1667                Some("majordomus_mesh_doctor"),
1668                "/api/v1/mesh/doctor",
1669            ),
1670            (
1671                "mesh.firewall",
1672                Some("majordomus_mesh_firewall"),
1673                "/api/v1/mesh/firewall",
1674            ),
1675            // the one command served nowhere over HTTP: it runs a privileged host tool
1676            ("mesh.firewall.apply", None, ""),
1677            (
1678                "mesh.register",
1679                Some("majordomus_mesh_register"),
1680                REGISTER_PATH,
1681            ),
1682            (
1683                "mesh.cooperation",
1684                Some("majordomus_mesh_cooperation"),
1685                "/api/v1/mesh/cooperation",
1686            ),
1687            (
1688                "mesh.peers",
1689                Some("majordomus_mesh_peers"),
1690                "/api/v1/mesh/peers",
1691            ),
1692            (
1693                "mesh.peer",
1694                Some("majordomus_mesh_peer"),
1695                "/api/v1/mesh/peer",
1696            ),
1697            (
1698                "mesh.state",
1699                Some("majordomus_mesh_state"),
1700                "/api/v1/mesh/state",
1701            ),
1702            (
1703                "mesh.events",
1704                Some("majordomus_mesh_events"),
1705                "/api/v1/mesh/events",
1706            ),
1707            (
1708                "mesh.verify",
1709                Some("majordomus_mesh_verify"),
1710                "/api/v1/mesh/verify",
1711            ),
1712            (
1713                "mesh.session.open",
1714                Some("majordomus_mesh_session_open"),
1715                "/api/v1/mesh/sessions",
1716            ),
1717            (
1718                "mesh.session.close",
1719                Some("majordomus_mesh_session_close"),
1720                "/api/v1/mesh/sessions/close",
1721            ),
1722            (
1723                "mesh.claim",
1724                Some("majordomus_mesh_claim"),
1725                "/api/v1/mesh/claims",
1726            ),
1727            (
1728                "mesh.release",
1729                Some("majordomus_mesh_release"),
1730                "/api/v1/mesh/claims/release",
1731            ),
1732            (
1733                "mesh.handover.publish",
1734                Some("majordomus_mesh_handover_publish"),
1735                "/api/v1/mesh/handovers",
1736            ),
1737            (
1738                "mesh.handover.consume",
1739                Some("majordomus_mesh_handover_consume"),
1740                "/api/v1/mesh/handovers/consume",
1741            ),
1742            (
1743                "mesh.review.request",
1744                Some("majordomus_mesh_review_request"),
1745                "/api/v1/mesh/reviews",
1746            ),
1747            (
1748                "mesh.review.answer",
1749                Some("majordomus_mesh_review_answer"),
1750                "/api/v1/mesh/reviews/answer",
1751            ),
1752            (
1753                "mesh.review.withdraw",
1754                Some("majordomus_mesh_review_withdraw"),
1755                "/api/v1/mesh/reviews/withdraw",
1756            ),
1757            ("mesh.link.hello", None, HELLO_PATH),
1758            ("mesh.link.sync", None, SYNC_PATH),
1759        ];
1760        let ids: Vec<&str> = m
1761            .capabilities
1762            .iter()
1763            .map(|e| e.capability.id.as_str())
1764            .collect();
1765        let want: Vec<&str> = expected.iter().map(|(id, _, _)| *id).collect();
1766        assert_eq!(
1767            ids, want,
1768            "the module declares a different set of capabilities"
1769        );
1770        for (executable, (id, tool, path)) in m.capabilities.iter().zip(expected) {
1771            let exposure = &executable.capability.exposure;
1772            assert_eq!(
1773                exposure.mcp.as_ref().and_then(|m| m.tool.as_deref()),
1774                *tool,
1775                "{id} lost or renamed its MCP tool"
1776            );
1777            assert_eq!(
1778                exposure.http.as_ref().map(|h| h.path.as_str()),
1779                (!path.is_empty()).then_some(*path),
1780                "{id} lost or renamed its HTTP route"
1781            );
1782        }
1783    }
1784
1785    /// Reads are queries; everything that changes the journal, the link table or the
1786    /// registry is a command, reachable only by POST — and none of them writes a tracked
1787    /// file, so none is a repository writer.
1788    #[test]
1789    fn what_changes_state_is_a_command_and_nothing_writes_the_repository() {
1790        let queries = [
1791            "mesh.status",
1792            "mesh.nodes",
1793            "mesh.identity",
1794            "mesh.doctor",
1795            "mesh.firewall",
1796            "mesh.cooperation",
1797            "mesh.peers",
1798            "mesh.peer",
1799            "mesh.state",
1800            "mesh.events",
1801        ];
1802        for e in module().capabilities {
1803            let c = &e.capability;
1804            let is_query = queries.contains(&c.id.as_str());
1805            assert_eq!(
1806                c.kind == CapabilityKind::Command,
1807                !is_query,
1808                "{}: reads are queries, changes are commands",
1809                c.id
1810            );
1811            if !is_query {
1812                match c.exposure.http.as_ref() {
1813                    Some(h) => assert_eq!(
1814                        h.method.as_str(),
1815                        "POST",
1816                        "{} changes state and must be a POST",
1817                        c.id
1818                    ),
1819                    // a command served nowhere over HTTP runs a privileged host tool,
1820                    // which nothing reachable over the network may do; there is one
1821                    None => assert_eq!(
1822                        c.id.as_str(),
1823                        "mesh.firewall.apply",
1824                        "{}: a command without an HTTP route must be the firewall's",
1825                        c.id
1826                    ),
1827                }
1828            }
1829            assert_ne!(
1830                c.execution.effect,
1831                crate::capability::model::Effect::RepositoryMutation,
1832                "{}: the mesh writes no tracked file",
1833                c.id
1834            );
1835        }
1836    }
1837
1838    /// A cooperation runtime that dials nobody until it is started, which these never are.
1839    fn cooperation() -> std::sync::Arc<Cooperation> {
1840        crate::mesh::testing::cooperation()
1841    }
1842
1843    /// A context over a synthetic repository, with a mesh declaration in its working tree
1844    /// when `declared`.
1845    fn context(declared: bool) -> (crate::synthetic::SyntheticRepository, Context) {
1846        let repo = crate::synthetic::SyntheticRepository::small().unwrap();
1847        let mut index = repo.index().unwrap();
1848        if declared {
1849            index.objects.push(crate::model::Object {
1850                kind: crate::mesh::KIND.into(),
1851                identity: "majordomus".into(),
1852                uri: "majordomus://mesh-declaration/majordomus".into(),
1853                title: None,
1854                description: None,
1855                metadata: serde_json::json!({
1856                    "schema": "mesh/v1", "kind": "mesh-declaration", "id": "majordomus",
1857                    "enabled": true, "trust": { "policy": "deny_unknown", "allow": [] },
1858                }),
1859                body: String::new(),
1860                content: String::new(),
1861                media_type: "application/yaml",
1862                provenance: crate::model::Provenance {
1863                    path: ".ai/repo/mesh/majordomus.yaml".into(),
1864                    directory: ".ai/repo/mesh".into(),
1865                    source_class: "mesh-declaration".into(),
1866                    section: None,
1867                    bytes: 0,
1868                    member: None,
1869                },
1870            });
1871        }
1872        let registry = capability::CapabilityRegistry::builder()
1873            .with_modules(vec![module()])
1874            .with_index(&index)
1875            .build()
1876            .expect("the registry builds");
1877        let ctx = Context::new(std::sync::Arc::new(index), std::sync::Arc::new(registry));
1878        (repo, ctx)
1879    }
1880
1881    #[test]
1882    fn retrust_asks_the_running_cooperation_with_the_declaration_in_force() {
1883        let (_repo, ctx) = context(true);
1884        assert!(
1885            matches!(declaration(&ctx), Some(Ok(_))),
1886            "the declaration parses"
1887        );
1888        assert!(retrust(&ctx).is_empty(), "no cooperation, nothing to end");
1889        ctx.mesh.attach_cooperation(cooperation());
1890        assert!(
1891            retrust(&ctx).is_empty(),
1892            "a runtime with no links ends none"
1893        );
1894        let (_repo, undeclared) = context(false);
1895        undeclared.mesh.attach_cooperation(cooperation());
1896        assert!(
1897            retrust(&undeclared).is_empty(),
1898            "no declaration, no trust to put in force"
1899        );
1900    }
1901
1902    #[test]
1903    fn a_peer_asked_of_a_server_without_cooperation_is_not_found_with_the_reason() {
1904        let (_repo, ctx) = context(false);
1905        ctx.mesh.decline_cooperation("no endpoint to be dialed at");
1906        let detail = mesh_peer(
1907            &ctx,
1908            PeerInput {
1909                runtime: "anything".into(),
1910            },
1911        )
1912        .unwrap();
1913        assert!(!detail.found);
1914        assert_eq!(
1915            detail.reason.as_deref(),
1916            Some("no endpoint to be dialed at")
1917        );
1918        assert!(detail.machine.is_none() && detail.runtime.is_none() && detail.refused.is_empty());
1919    }
1920
1921    #[test]
1922    fn a_peer_is_found_by_its_runtime_key_in_the_full_tree_and_another_is_not() {
1923        let (_repo, ctx) = context(false);
1924        let c = cooperation();
1925        ctx.mesh.attach_cooperation(std::sync::Arc::clone(&c));
1926        let own = c.runtime_key().to_string();
1927        let found = mesh_peer(
1928            &ctx,
1929            PeerInput {
1930                runtime: format!(" {own} "),
1931            },
1932        )
1933        .unwrap();
1934        assert!(found.found, "{found:?}");
1935        assert_eq!(found.runtime.map(|r| r.runtime), Some(own));
1936        let missing = mesh_peer(
1937            &ctx,
1938            PeerInput {
1939                runtime: "nobody".into(),
1940            },
1941        )
1942        .unwrap();
1943        assert!(!missing.found);
1944        assert_eq!(
1945            missing.reason.as_deref(),
1946            Some("no runtime nobody is linked or heard here")
1947        );
1948    }
1949
1950    #[test]
1951    fn the_summary_tree_lists_active_sessions_counted_and_the_full_one_every_claim() {
1952        let c = cooperation();
1953        let s1 = SessionInfo::named("s1", "test");
1954        c.open_session(s1.clone()).unwrap();
1955        // the older claim's id sorts last, so the fold lists the newer one first and the
1956        // older one meets a newest it does not replace
1957        for (claim, path, issue) in [("z1", "apps", "#1"), ("a2", "docs", "#2")] {
1958            c.journal()
1959                .append_own(crate::mesh::journal::EventBody::ClaimAcquired {
1960                    claim: claim.into(),
1961                    session: s1.session.clone(),
1962                    scope: vec![path.into()],
1963                    intent: Some("a long intent ".repeat(20)),
1964                    mode: ClaimMode::Exclusive,
1965                    issue: Some(issue.into()),
1966                })
1967                .unwrap();
1968        }
1969        c.open_session(SessionInfo::named("s2", "test")).unwrap();
1970        c.close_session("s2").unwrap();
1971        c.request_review("s1", "feature/x".into(), vec![], None, None, None)
1972            .unwrap();
1973        let own = |tree: &PeerTree| -> RuntimeNode {
1974            tree.machines
1975                .iter()
1976                .flat_map(|m| m.runtimes.iter())
1977                .find(|r| r.this_runtime)
1978                .cloned()
1979                .expect("this runtime is in its own tree")
1980        };
1981
1982        let summary = own(&peer_tree(&c, &Query::default()));
1983        assert_eq!(
1984            summary
1985                .newest_claim
1986                .as_ref()
1987                .and_then(|b| b.issue.as_deref()),
1988            Some("#2"),
1989            "the newest live claim"
1990        );
1991        assert_eq!(summary.sessions.len(), 1, "only the active session");
1992        let s = &summary.sessions[0];
1993        assert_eq!(s.state, SessionState::Active);
1994        assert!(s.claims.is_empty(), "counted, not listed");
1995        assert_eq!(s.claims_held, 2);
1996        assert_eq!(s.reviews.len(), 1, "the review the session asked for");
1997
1998        let full = own(&peer_tree(
1999            &c,
2000            &Query {
2001                view: Some(View::Full),
2002                ..Query::default()
2003            },
2004        ));
2005        assert_eq!(full.sessions.len(), 2, "every session");
2006        let s1 = full
2007            .sessions
2008            .iter()
2009            .find(|s| s.state == SessionState::Active)
2010            .unwrap();
2011        assert_eq!(s1.claims.len(), 2, "every claim listed");
2012        assert_eq!(s1.claims_held, 2);
2013    }
2014}