Skip to main content

agentmux_srv/server/agent_handlers/
identity_env.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The agent's identity variables in its spawn environment, beyond what
5//! `input.rs`'s `split_agent_identity_env` and `carry_agent_uid_env` set.
6
7use std::collections::HashMap;
8
9/// `AGENTMUX_AGENT_ID` (and its `MUXBUS_AGENT_ID` mirror) when the launch's
10/// `cmd:env` didn't carry one. It is the canonical, app-wide agent-identity
11/// variable (MCP tool routing, native memory, shell OSC titling, jekt
12/// auto-registration, gh-agent; see this repo's CLAUDE.md Naming Conventions
13/// table), and a `cmd:env` value takes precedence, unlike the reserved
14/// variables. The fallback is the row's slug, as every launch path sets it
15/// and as the name-keyed signing keys are filed (identity M4d,
16/// `agent_self_keys.rs`); for a pane with no row (quick launch), its display
17/// name as a route name, since a name that isn't a valid agent id (a fork's
18/// "Loap #3") fails every registration
19/// (REPORT_FORK_AGENT_ISOLATION_2026_10_12.md F3). `MUXBUS_AGENT_ID` mirrors
20/// it so muxbus-client picks it up under the prefix it checks first; scoped to
21/// that one hand-off point, not a second source of truth (ARCH-002).
22pub(crate) fn carry_agent_id_env(
23    env_vars: &mut HashMap<String, String>,
24    display_name: &str,
25    persisted_slug: Option<&str>,
26) {
27    if !env_vars.contains_key("AGENTMUX_AGENT_ID") {
28        let id = persisted_slug
29            .map(str::trim)
30            .filter(|s| !s.is_empty())
31            .map(str::to_string)
32            .or_else(|| (!display_name.trim().is_empty()).then(|| agentmux_common::slug::agent_route_name(display_name)));
33        if let Some(id) = id {
34            env_vars.insert("AGENTMUX_AGENT_ID".to_string(), id);
35        }
36    }
37    if !env_vars.contains_key("MUXBUS_AGENT_ID") {
38        if let Some(agent_id) = env_vars.get("AGENTMUX_AGENT_ID").cloned() {
39            env_vars.insert("MUXBUS_AGENT_ID".to_string(), agent_id);
40        }
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::carry_agent_id_env;
47    use std::collections::HashMap;
48
49    fn env(pairs: &[(&str, &str)]) -> HashMap<String, String> {
50        pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
51    }
52
53    /// With no `AGENTMUX_AGENT_ID` in `cmd:env`, the row's slug: what the
54    /// launch sets and the signing keys are filed under. A fork's display
55    /// name is never used when the row is known.
56    #[test]
57    fn a_missing_agent_id_is_the_rows_slug() {
58        let mut e = env(&[]);
59        carry_agent_id_env(&mut e, "Loap #3", Some("loap-loap-2-loap-3"));
60        assert_eq!(e["AGENTMUX_AGENT_ID"], "loap-loap-2-loap-3");
61        assert_eq!(e["MUXBUS_AGENT_ID"], "loap-loap-2-loap-3");
62    }
63
64    /// A pane with no row (quick launch) falls back to its display name, as
65    /// a valid id: "Loap #3" used to fail every registration.
66    #[test]
67    fn a_rowless_panes_agent_id_is_its_route_name() {
68        let mut e = env(&[]);
69        carry_agent_id_env(&mut e, "Loap #3", None);
70        assert_eq!(e["AGENTMUX_AGENT_ID"], "Loap-3");
71        let mut plain = env(&[]);
72        carry_agent_id_env(&mut plain, "AgentY", None);
73        assert_eq!(plain["AGENTMUX_AGENT_ID"], "AgentY", "a valid name keeps its casing");
74        let mut none = env(&[]);
75        carry_agent_id_env(&mut none, "  ", None);
76        assert!(!none.contains_key("AGENTMUX_AGENT_ID"));
77    }
78
79    /// A launch's own value wins, and its mirror is kept too.
80    #[test]
81    fn a_carried_agent_id_is_kept() {
82        let mut e = env(&[("AGENTMUX_AGENT_ID", "agenty-2"), ("MUXBUS_AGENT_ID", "bus-y")]);
83        carry_agent_id_env(&mut e, "AgentY", Some("agenty-3"));
84        assert_eq!(e["AGENTMUX_AGENT_ID"], "agenty-2");
85        assert_eq!(e["MUXBUS_AGENT_ID"], "bus-y");
86    }
87}