Skip to main content

capsule_runtime/
formation.rs

1//! The formation a session belongs to, and the one id that names it.
2//!
3//! A formation is the set of sessions launched together to carry out one task. Its id is minted
4//! once, by whatever launches the formation's members, and handed to each member through
5//! [`FORMATION_ID_ENV`]. A member's own delegation plane hands the same id to every child it
6//! spawns, so a child joins its parent's formation and never mints one. Nothing else mints: a
7//! standalone `mur run` that delegates grows a delegation tree, not a formation, and that tree's
8//! sessions carry no formation id — they are joined by lineage (`session_start.spawned_by`,
9//! `delegation_id`) instead.
10//!
11//! **The id groups; it grants nothing.** No authorization decision reads it alone — not the door's
12//! Bearer check, not task-origin classification, not peer handoff, not the spawn referee. What a
13//! member may call is decided by a formation token ([`crate::formation_credentials`]), which the
14//! launcher signs over the id together with the caller and the callee; a session that merely
15//! carries the id, a delegated child among them, holds no such token and reaches nobody by it.
16//!
17//! **It belongs to the session, not the process.** [`FormationId::from_env`] is the one read of
18//! [`FORMATION_ID_ENV`] from a process environment, and only `mur run` — whose process is one
19//! session — calls it, before the workspace `.env` is loaded. From there the id travels on
20//! [`crate::StageRequest::formation_id`] and the staged session, which is what hooks and the
21//! delegation plane read, so an in-process caller staging two sessions gives each its own.
22//!
23//! **The shape is strict.** `frm_` followed by the 32 lowercase hex digits of a UUIDv7: opaque,
24//! derived from nothing about the formation, its members or the host, and time-ordered so ids sort
25//! by launch. A value that sessions are grouped on admits no near-misses, so anything else in the
26//! variable — other than nothing at all — refuses the launch.
27//!
28//! **A member's guests are handed its callees' names.** [`FORMATION_PEERS_ENV`] is set by a
29//! member's runtime inside its own WASM guests, never by a launcher in a process environment: each
30//! callee at its virtual address `http://<name>.formation.invalid`, which only the runtime's egress
31//! can reach. `mur run` refuses a launch whose own environment carries the variable
32//! ([`refuse_formation_peers_in_process_env`]). A delegated child and a subprocess are never
33//! handed it.
34
35use std::fmt;
36
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38
39use crate::errors::RuntimeError;
40
41/// The variable a launcher sets in a member's environment, and a member's runtime sets in each
42/// child's, carrying its [`FormationId`].
43pub const FORMATION_ID_ENV: &str = "MURMUR_FORMATION_ID";
44
45/// Prefix on every formation id. Distinct from `ses_`/`ctx_`/`tsk_`/`dlg_`/`wrk_`, so a reader can
46/// tell at a glance which id space a value belongs to.
47pub const FORMATION_ID_PREFIX: &str = "frm_";
48
49/// How many hex digits follow [`FORMATION_ID_PREFIX`]: a UUID in its simple form.
50const FORMATION_ID_DIGITS: usize = 32;
51
52/// The id of the formation a session belongs to, matching `^frm_[0-9a-f]{32}$`.
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct FormationId(String);
55
56impl FormationId {
57    /// A fresh formation id, for a launcher starting a formation's members.
58    ///
59    /// Two launches of the same formation get two different ids.
60    pub fn mint() -> Self {
61        Self(format!(
62            "{FORMATION_ID_PREFIX}{}",
63            uuid::Uuid::now_v7().simple()
64        ))
65    }
66
67    /// Accept `value` only if it is exactly a formation id; nothing is trimmed.
68    ///
69    /// The error's `reason` names what is wrong with the value and never contains it.
70    pub fn parse(value: &str) -> Result<Self, RuntimeError> {
71        let unreadable = |reason: &str| RuntimeError::FormationIdUnreadable {
72            reason: reason.to_string(),
73        };
74        let Some(digits) = value.strip_prefix(FORMATION_ID_PREFIX) else {
75            return Err(unreadable("it does not start with 'frm_'"));
76        };
77        if digits.len() != FORMATION_ID_DIGITS {
78            return Err(unreadable(
79                "'frm_' must be followed by exactly 32 hex digits",
80            ));
81        }
82        if !digits
83            .bytes()
84            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
85        {
86            return Err(unreadable(
87                "it contains a character outside lowercase hex (0-9, a-f)",
88            ));
89        }
90        Ok(Self(value.to_string()))
91    }
92
93    /// This session's formation, read from [`FORMATION_ID_ENV`] in the process environment.
94    ///
95    /// `Ok(None)` for a session nobody placed in a formation — the variable absent, or blank —
96    /// which is the ordinary case. Any other value that is not a formation id is an error and not
97    /// an absence: a session that cannot tell which formation it belongs to would be grouped with
98    /// the wrong one, or with none.
99    pub fn from_env() -> Result<Option<Self>, RuntimeError> {
100        let Some(raw) = std::env::var_os(FORMATION_ID_ENV) else {
101            return Ok(None);
102        };
103        let raw = raw.to_string_lossy();
104        if raw.trim().is_empty() {
105            return Ok(None);
106        }
107        Self::parse(raw.trim()).map(Some)
108    }
109
110    pub fn as_str(&self) -> &str {
111        &self.0
112    }
113
114    /// When this id was minted, in milliseconds since the Unix epoch: the first 12 hex digits of
115    /// its UUIDv7, read the same way `retention::session_id_timestamp_ms` reads a `ses_` id.
116    ///
117    /// A member's session is minted after the formation it joins, so a reader looking for members
118    /// can pass over every session older than this.
119    pub fn minted_at_ms(&self) -> u64 {
120        // `parse` and `mint` admit only 32 lowercase hex digits, so this always reads.
121        crate::retention::uuid_v7_timestamp_ms(&self.0, FORMATION_ID_PREFIX).unwrap_or(0)
122    }
123
124    /// The `(name, value)` pair a launcher writes into a member's or a child's environment.
125    pub fn env_pair(&self) -> (&'static str, String) {
126        (FORMATION_ID_ENV, self.0.clone())
127    }
128}
129
130impl fmt::Display for FormationId {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.write_str(&self.0)
133    }
134}
135
136/// A bare string, `"frm_…"`.
137impl Serialize for FormationId {
138    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
139        serializer.serialize_str(self.as_str())
140    }
141}
142
143/// Only through [`FormationId::parse`], so a file carrying anything but a formation id does not
144/// deserialize at all. The error names what is wrong and never contains the value.
145impl<'de> Deserialize<'de> for FormationId {
146    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
147        let value = String::deserialize(deserializer)?;
148        Self::parse(&value).map_err(|error| match error {
149            RuntimeError::FormationIdUnreadable { reason } => {
150                serde::de::Error::custom(format!("not a formation id: {reason}"))
151            }
152            _ => serde::de::Error::custom("not a formation id"),
153        })
154    }
155}
156
157/// The variable a formation member's runtime sets in each of its WASM guests, naming every member
158/// the roster lets it call at that member's virtual address.
159///
160/// Runtime-owned: no declaration supplies or displaces it, no subprocess or delegated child is
161/// handed it, and `mur run` refuses a process environment that carries it.
162pub const FORMATION_PEERS_ENV: &str = "MURMUR_FORMATION_PEERS";
163
164/// The only scheme a peer address may carry. Members of one formation share a host.
165const PEER_URL_SCHEME: &str = "http://";
166
167/// One member a member may call, and the address its guests call it at.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct FormationPeer {
170    /// The member's roster name, matching `^[a-z][a-z0-9-]{0,31}$`.
171    pub name: String,
172    /// `http://host` or `http://host:port`, with no path: the virtual address a guest is handed,
173    /// or the real door URL the channel carries.
174    pub url: String,
175}
176
177/// The value of [`FORMATION_PEERS_ENV`]: `name=url` pairs separated by single spaces, in roster
178/// order, with at least one pair.
179///
180/// Space-separated rather than JSON so a shell guest reads it with
181/// `for p in $MURMUR_FORMATION_PEERS` and no parser. Unambiguous, because neither a member name
182/// nor an `http://host:port` URL can contain a space or an `=`.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct FormationPeers(Vec<FormationPeer>);
185
186impl FormationPeers {
187    /// The peers in the order given: one or more, each a member name at most once, each at an
188    /// `http://host[:port]` URL with no path.
189    pub fn new(peers: Vec<FormationPeer>) -> Result<Self, RuntimeError> {
190        if peers.is_empty() {
191            return Err(peers_unreadable(
192                "it names no peer; a member that may call nobody is handed no variable at all",
193            ));
194        }
195        let mut seen = std::collections::HashSet::new();
196        for (index, peer) in peers.iter().enumerate() {
197            let position = index + 1;
198            if let Some(problem) = murmur_artifact::member_name_format_error(&peer.name) {
199                return Err(peers_unreadable(&format!(
200                    "pair {position} names a member whose name {problem}"
201                )));
202            }
203            if !seen.insert(peer.name.as_str()) {
204                return Err(peers_unreadable(&format!(
205                    "pair {position} names '{}' a second time",
206                    peer.name
207                )));
208            }
209            if let Some(problem) = url_format_error(&peer.url, false) {
210                return Err(peers_unreadable(&format!(
211                    "pair {position} ('{}') {problem}",
212                    peer.name
213                )));
214            }
215        }
216        Ok(Self(peers))
217    }
218
219    /// The variable's value: `name=url` pairs joined by single spaces, in order.
220    pub fn render(&self) -> String {
221        self.0
222            .iter()
223            .map(|peer| format!("{}={}", peer.name, peer.url))
224            .collect::<Vec<_>>()
225            .join(" ")
226    }
227
228    /// The `(name, value)` pair a member's runtime writes into each of its WASM guests'
229    /// environments.
230    pub fn env_pair(&self) -> (&'static str, String) {
231        (FORMATION_PEERS_ENV, self.render())
232    }
233
234    pub fn peers(&self) -> &[FormationPeer] {
235        &self.0
236    }
237}
238
239/// Whether `name` is one of the variables that carry what a formation member was handed —
240/// [`FORMATION_PEERS_ENV`], [`crate::formation_credentials::FORMATION_CHANNEL_ENV`] or
241/// [`crate::lifeline::FORMATION_LIFELINE_ENV`]. None is ever resolved from the host for a guest or
242/// a subprocess, or handed to a delegated child: the channel and the lifeline name
243/// descriptors of the member's own process.
244pub(crate) fn is_member_grant_env(name: &str) -> bool {
245    name == FORMATION_PEERS_ENV
246        || name == crate::formation_credentials::FORMATION_CHANNEL_ENV
247        || name == crate::lifeline::FORMATION_LIFELINE_ENV
248}
249
250/// `Err` when [`FORMATION_PEERS_ENV`] is set in this process's environment.
251///
252/// Only a member's runtime sets the variable, and only inside its own guests, from what the
253/// formation channel delivered. A value in `mur run`'s own environment came from somewhere else —
254/// a shell, a parent that should not have passed it on — and is refused rather than ignored, so
255/// nobody is led to believe it was read. The value is never read or quoted.
256pub fn refuse_formation_peers_in_process_env() -> Result<(), RuntimeError> {
257    if std::env::var_os(FORMATION_PEERS_ENV).is_some() {
258        return Err(peers_unreadable("it is set in this process's environment"));
259    }
260    Ok(())
261}
262
263fn peers_unreadable(reason: &str) -> RuntimeError {
264    RuntimeError::FormationPeersUnreadable {
265        reason: reason.to_string(),
266    }
267}
268
269/// Why `url` is not `http://host:port`, or `None` when it is: the shape of a real door address.
270pub(crate) fn peer_url_format_error(url: &str) -> Option<String> {
271    url_format_error(url, true)
272}
273
274/// Why `url` is not `http://host[:port]`, or `None` when it is. The port is optional only when
275/// `port_required` is false.
276fn url_format_error(url: &str, port_required: bool) -> Option<String> {
277    let Some(authority) = url.strip_prefix(PEER_URL_SCHEME) else {
278        return Some("has a URL that does not start with 'http://'".to_string());
279    };
280    // A bracketed IPv6 host ends in `]` when no port follows it.
281    let (host, port) = match authority.rsplit_once(':') {
282        Some((host, port)) if !authority.ends_with(']') => (host, Some(port)),
283        _ => (authority, None),
284    };
285    if port.is_none() && port_required {
286        return Some("has a URL with no port".to_string());
287    }
288    let bracketed_ip = host.len() > 2 && host.starts_with('[') && host.ends_with(']');
289    let plain_host = !host.is_empty()
290        && host
291            .bytes()
292            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-'));
293    if !(bracketed_ip || plain_host) {
294        return Some("has a URL whose host is not a hostname or an IP address".to_string());
295    }
296    let port = port?;
297    let digits_only = !port.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit());
298    match port.parse::<u16>() {
299        Ok(port) if digits_only && port > 0 => None,
300        _ => Some("has a URL whose port is not a number from 1 to 65535".to_string()),
301    }
302}
303
304/// Serializes the unit tests that set or remove [`FORMATION_ID_ENV`] or [`FORMATION_PEERS_ENV`] in
305/// this test binary's process environment, which every test thread shares.
306#[cfg(test)]
307pub(crate) static FORMATION_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    fn env_guard() -> std::sync::MutexGuard<'static, ()> {
314        FORMATION_ENV_LOCK
315            .lock()
316            .unwrap_or_else(|poisoned| poisoned.into_inner())
317    }
318
319    fn refusal(value: &str) -> String {
320        match FormationId::parse(value) {
321            Err(error @ RuntimeError::FormationIdUnreadable { .. }) => error.to_string(),
322            other => panic!("expected FormationIdUnreadable, got {other:?}"),
323        }
324    }
325
326    #[test]
327    fn a_minted_formation_id_is_well_formed() {
328        let id = FormationId::mint();
329        assert_eq!(id.as_str().len(), 36);
330        assert!(id.as_str().starts_with(FORMATION_ID_PREFIX));
331        assert_eq!(FormationId::parse(id.as_str()).unwrap(), id);
332        assert_eq!(id.to_string(), id.as_str());
333        assert_eq!(id.env_pair(), (FORMATION_ID_ENV, id.as_str().to_string()));
334    }
335
336    #[test]
337    fn minted_formation_ids_are_distinct_and_sort_in_mint_order() {
338        let minted: Vec<FormationId> = (0..1_000).map(|_| FormationId::mint()).collect();
339        let distinct: std::collections::HashSet<&FormationId> = minted.iter().collect();
340        assert_eq!(distinct.len(), minted.len());
341        let mut sorted: Vec<&str> = minted.iter().map(FormationId::as_str).collect();
342        sorted.sort_unstable();
343        let in_mint_order: Vec<&str> = minted.iter().map(FormationId::as_str).collect();
344        assert_eq!(sorted, in_mint_order);
345    }
346
347    #[test]
348    fn a_formation_id_parse_refuses_near_misses_without_echoing_them() {
349        let minted = FormationId::mint();
350        let digits = &minted.as_str()[FORMATION_ID_PREFIX.len()..];
351        let near_misses = [
352            "frm_".to_string(),
353            format!("frm_{}", &digits[..31]),
354            format!("frm_{digits}0"),
355            format!("frm_{}", digits.to_uppercase()),
356            format!("frm_{}A", &digits[..31]),
357            format!("dlg_{digits}"),
358            format!("ses_{digits}"),
359            format!(" {}", minted.as_str()),
360            format!("{} ", minted.as_str()),
361            format!("x{}", minted.as_str()),
362            format!("{}x", minted.as_str()),
363            format!("frm_{}g", &digits[..31]),
364            format!("frm_{}-", &digits[..31]),
365            "not-a-formation".to_string(),
366        ];
367        for value in &near_misses {
368            let message = refusal(value);
369            // The bare prefix is the one input the refusal's own wording legitimately contains.
370            if value != FORMATION_ID_PREFIX {
371                assert!(
372                    !message.contains(value.trim()),
373                    "the refusal echoes the value {value:?}: {message}"
374                );
375            }
376            assert!(!message.contains(&digits[..16]), "{message}");
377            assert!(!message.contains(&digits[..16].to_uppercase()), "{message}");
378            assert!(message.contains("MURMUR_FORMATION_ID"), "{message}");
379        }
380        assert!(refusal("dlg_x").contains("does not start with 'frm_'"));
381        assert!(refusal(&format!("frm_{}", &digits[..31])).contains("exactly 32 hex digits"));
382        assert!(refusal(&format!("frm_{}", digits.to_uppercase())).contains("lowercase hex"));
383        assert!(refusal(&format!("frm_{}g", &digits[..31])).contains("lowercase hex"));
384    }
385
386    #[test]
387    fn a_formation_id_is_read_from_the_process_environment_only_when_well_formed() {
388        let _guard = env_guard();
389        std::env::remove_var(FORMATION_ID_ENV);
390        assert_eq!(FormationId::from_env().unwrap(), None);
391
392        std::env::set_var(FORMATION_ID_ENV, "   ");
393        assert_eq!(FormationId::from_env().unwrap(), None);
394
395        let minted = FormationId::mint();
396        std::env::set_var(FORMATION_ID_ENV, minted.as_str());
397        assert_eq!(FormationId::from_env().unwrap(), Some(minted.clone()));
398
399        std::env::set_var(FORMATION_ID_ENV, format!(" {} ", minted.as_str()));
400        assert_eq!(FormationId::from_env().unwrap(), Some(minted));
401
402        std::env::set_var(FORMATION_ID_ENV, "frm_ABC");
403        let result = FormationId::from_env();
404        std::env::remove_var(FORMATION_ID_ENV);
405        assert!(matches!(
406            result,
407            Err(RuntimeError::FormationIdUnreadable { .. })
408        ));
409    }
410
411    #[test]
412    fn a_formation_id_serializes_as_a_bare_string_and_round_trips() {
413        let id = FormationId::mint();
414        let json = serde_json::to_string(&id).unwrap();
415        assert_eq!(json, format!("\"{}\"", id.as_str()));
416        let back: FormationId = serde_json::from_str(&json).unwrap();
417        assert_eq!(back, id);
418    }
419
420    #[test]
421    fn a_malformed_formation_id_does_not_deserialize_and_is_not_echoed() {
422        let minted = FormationId::mint();
423        let digits = &minted.as_str()[FORMATION_ID_PREFIX.len()..];
424        for value in [
425            "frm_ABC".to_string(),
426            format!("frm_{}", digits.to_uppercase()),
427            format!("ses_{digits}"),
428        ] {
429            let json = serde_json::to_string(&value).unwrap();
430            let error = serde_json::from_str::<FormationId>(&json)
431                .expect_err("a malformed id deserializes")
432                .to_string();
433            assert!(
434                !error.contains(&value),
435                "the error echoes {value:?}: {error}"
436            );
437            assert!(!error.contains(&digits[..16]), "{error}");
438            assert!(!error.contains(&digits[..16].to_uppercase()), "{error}");
439            assert!(error.contains("not a formation id"), "{error}");
440        }
441        assert!(serde_json::from_str::<FormationId>("42").is_err());
442    }
443
444    #[test]
445    fn a_formation_id_was_minted_when_its_uuid_says() {
446        let id = FormationId::mint();
447        let tail = &id.as_str()[FORMATION_ID_PREFIX.len()..];
448        assert_eq!(
449            Some(id.minted_at_ms()),
450            crate::retention::session_id_timestamp_ms(&format!("ses_{tail}"))
451        );
452        let now_ms = std::time::SystemTime::now()
453            .duration_since(std::time::UNIX_EPOCH)
454            .unwrap()
455            .as_millis() as u64;
456        assert!(id.minted_at_ms() <= now_ms);
457        assert!(now_ms - id.minted_at_ms() < 60_000);
458        let fixed = FormationId::parse("frm_00000000000a0000000000000000000f").unwrap();
459        assert_eq!(fixed.minted_at_ms(), 10);
460    }
461
462    /// Every `.rs` file under the named workspace crate's `src`, as `(crate/src/path, contents)`.
463    fn sources_of(crate_dir: &str) -> Vec<(String, String)> {
464        crate::source_scan::sources_under(&crate::source_scan::sibling_crate_src(crate_dir))
465            .into_iter()
466            .map(|(path, text)| (format!("{crate_dir}/src/{path}"), text))
467            .collect()
468    }
469
470    const FORMATION_TOKENS: [&str; 4] = [
471        "FormationId",
472        "formation_id",
473        "FORMATION_ID_ENV",
474        "MURMUR_FORMATION_ID",
475    ];
476
477    #[test]
478    fn no_authorization_path_reads_a_formation_id() {
479        let own = sources_of("capsule-runtime");
480        let mut guarded = sources_of("mur-roost");
481        for file in [
482            "door_auth.rs",
483            "origin.rs",
484            "peer_handoff.rs",
485            "spawn_credential.rs",
486            "spawn_envelope.rs",
487        ] {
488            let path = format!("capsule-runtime/src/{file}");
489            let source = own
490                .iter()
491                .find(|(name, _)| *name == path)
492                .unwrap_or_else(|| panic!("{path} is gone"));
493            guarded.push(source.clone());
494        }
495        for (path, text) in guarded {
496            for token in FORMATION_TOKENS {
497                assert!(
498                    !text.contains(token),
499                    "{path} names {token}; a formation id confers no reachability"
500                );
501            }
502        }
503    }
504
505    /// Every `env::var(..)` / `env::var_os(..)` call in `text`, as the text of its argument list.
506    fn env_reads(text: &str) -> Vec<&str> {
507        let mut calls = Vec::new();
508        for opener in ["env::var(", "env::var_os("] {
509            let mut rest = text;
510            while let Some(at) = rest.find(opener) {
511                let args = &rest[at + opener.len()..];
512                let mut depth = 1usize;
513                let end = args
514                    .char_indices()
515                    .find_map(|(index, ch)| {
516                        match ch {
517                            '(' => depth += 1,
518                            ')' => depth -= 1,
519                            _ => {}
520                        }
521                        (depth == 0).then_some(index)
522                    })
523                    .unwrap_or(args.len());
524                calls.push(&args[..end]);
525                rest = &args[end..];
526            }
527        }
528        calls
529    }
530
531    #[test]
532    fn the_formation_id_is_read_from_the_process_environment_in_one_place() {
533        let mut sources = sources_of("capsule-runtime");
534        sources.extend(sources_of("murmur-cli"));
535        let names_the_variable =
536            |args: &&str| args.contains("MURMUR_FORMATION_ID") || args.contains("FORMATION_ID_ENV");
537        let mut reads_here = 0;
538        for (path, text) in sources {
539            let reads = env_reads(crate::source_scan::production_part(&text))
540                .into_iter()
541                .filter(names_the_variable)
542                .count();
543            if path == "capsule-runtime/src/formation.rs" {
544                reads_here = reads;
545            } else {
546                assert_eq!(
547                    reads, 0,
548                    "{path} reads the formation id from the process environment; only \
549                     FormationId::from_env does"
550                );
551            }
552        }
553        assert_eq!(reads_here, 1, "FormationId::from_env is the one reader");
554    }
555
556    #[test]
557    fn formation_peers_render_in_order() {
558        let peers = FormationPeers::new(vec![
559            FormationPeer {
560                name: "coder".to_string(),
561                url: "http://localhost:41873".to_string(),
562            },
563            FormationPeer {
564                name: "reviewer".to_string(),
565                url: "http://reviewer.formation.invalid".to_string(),
566            },
567        ])
568        .unwrap();
569        let value = "coder=http://localhost:41873 reviewer=http://reviewer.formation.invalid";
570        assert_eq!(peers.render(), value);
571        assert_eq!(peers.env_pair(), (FORMATION_PEERS_ENV, value.to_string()));
572    }
573
574    #[test]
575    fn formation_peers_refuse_every_malformed_peer() {
576        let refusal = |peers: &[(&str, &str)]| {
577            let peers = peers
578                .iter()
579                .map(|(name, url)| FormationPeer {
580                    name: name.to_string(),
581                    url: url.to_string(),
582                })
583                .collect();
584            match FormationPeers::new(peers) {
585                Err(error @ RuntimeError::FormationPeersUnreadable { .. }) => error.to_string(),
586                other => panic!("expected FormationPeersUnreadable, got {other:?}"),
587            }
588        };
589        for (peers, wording) in [
590            (
591                &[("Coder", "http://localhost:1")][..],
592                "must start with a lowercase letter",
593            ),
594            (
595                &[("co_der", "http://localhost:1")],
596                "pair 1 names a member whose name",
597            ),
598            (
599                &[
600                    ("coder", "http://localhost:1"),
601                    ("coder", "http://localhost:2"),
602                ],
603                "pair 2 names 'coder' a second time",
604            ),
605            (
606                &[("coder", "https://localhost:1")],
607                "does not start with 'http://'",
608            ),
609            (&[("coder", "localhost:1")], "does not start with 'http://'"),
610            (&[("coder", "http://localhost:0")], "port is not a number"),
611            (
612                &[("coder", "http://localhost:99999")],
613                "port is not a number",
614            ),
615            (&[("coder", "http://localhost:1/x")], "port is not a number"),
616            (&[("coder", "http://:1")], "host is not"),
617        ] {
618            let message = refusal(peers);
619            assert!(message.contains(wording), "{peers:?}: {message}");
620            assert!(message.contains(FORMATION_PEERS_ENV), "{message}");
621        }
622        assert!(FormationPeers::new(Vec::new()).is_err());
623        // A guest's peers are virtual addresses with no port; a real door always has one.
624        assert_eq!(
625            peer_url_format_error("http://localhost").as_deref(),
626            Some("has a URL with no port")
627        );
628        assert_eq!(peer_url_format_error("http://localhost:1"), None);
629        assert_eq!(peer_url_format_error("http://[::1]:9"), None);
630        assert!(peer_url_format_error("http://[::1]").is_some());
631    }
632
633    #[test]
634    fn formation_peers_in_the_process_environment_are_refused_and_never_read() {
635        let _guard = env_guard();
636        std::env::remove_var(FORMATION_PEERS_ENV);
637        assert!(refuse_formation_peers_in_process_env().is_ok());
638
639        for value in ["coder=http://localhost:7", ""] {
640            std::env::set_var(FORMATION_PEERS_ENV, value);
641            let refused = refuse_formation_peers_in_process_env();
642            std::env::remove_var(FORMATION_PEERS_ENV);
643            let message = refused.unwrap_err().to_string();
644            assert!(
645                message.contains("set in this process's environment"),
646                "{message}"
647            );
648            assert!(!message.contains("localhost"), "{message}");
649        }
650    }
651
652    #[test]
653    fn the_formation_peers_are_read_from_the_process_environment_in_one_place() {
654        let mut sources = sources_of("capsule-runtime");
655        sources.extend(sources_of("murmur-cli"));
656        let names_the_variable = |args: &&str| {
657            args.contains("MURMUR_FORMATION_PEERS") || args.contains("FORMATION_PEERS_ENV")
658        };
659        let mut reads_here = 0;
660        for (path, text) in sources {
661            let reads = env_reads(crate::source_scan::production_part(&text))
662                .into_iter()
663                .filter(names_the_variable)
664                .count();
665            if path == "capsule-runtime/src/formation.rs" {
666                reads_here = reads;
667            } else {
668                assert_eq!(
669                    reads, 0,
670                    "{path} reads the formation's peers from the process environment; nothing \
671                     does but the refusal of a value found there"
672                );
673            }
674        }
675        assert_eq!(
676            reads_here, 1,
677            "refuse_formation_peers_in_process_env is the one place that looks"
678        );
679    }
680
681    #[test]
682    fn the_formation_lifeline_is_read_from_the_process_environment_in_one_place() {
683        let mut sources = sources_of("capsule-runtime");
684        sources.extend(sources_of("murmur-cli"));
685        let names_the_variable = |args: &&str| {
686            args.contains("MURMUR_FORMATION_LIFELINE") || args.contains("FORMATION_LIFELINE_ENV")
687        };
688        let mut reads_here = 0;
689        for (path, text) in sources {
690            let reads = env_reads(crate::source_scan::production_part(&text))
691                .into_iter()
692                .filter(names_the_variable)
693                .count();
694            if path == "capsule-runtime/src/lifeline.rs" {
695                reads_here = reads;
696            } else {
697                assert_eq!(
698                    reads, 0,
699                    "{path} reads the formation lifeline from the process environment; only \
700                     FormationLifeline::from_env does"
701                );
702            }
703        }
704        assert_eq!(
705            reads_here, 1,
706            "FormationLifeline::from_env is the one reader"
707        );
708    }
709
710    #[test]
711    fn the_spawner_lifeline_is_read_from_the_process_environment_in_one_place() {
712        let mut sources = sources_of("capsule-runtime");
713        sources.extend(sources_of("murmur-cli"));
714        let names_the_variable = |args: &&str| {
715            args.contains("MURMUR_SPAWNER_LIFELINE") || args.contains("SPAWNER_LIFELINE_ENV")
716        };
717        let mut reads_here = 0;
718        for (path, text) in sources {
719            let reads = env_reads(crate::source_scan::production_part(&text))
720                .into_iter()
721                .filter(names_the_variable)
722                .count();
723            if path == "capsule-runtime/src/lifeline.rs" {
724                reads_here = reads;
725            } else {
726                assert_eq!(
727                    reads, 0,
728                    "{path} reads the spawner lifeline from the process environment; only \
729                     SpawnerLifeline::from_env does"
730                );
731            }
732        }
733        assert_eq!(reads_here, 1, "SpawnerLifeline::from_env is the one reader");
734    }
735}