@team-agent/installer 0.5.48 → 0.5.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -1
- package/crates/team-agent/src/cli/emit.rs +89 -38
- package/crates/team-agent/src/cli/mod.rs +1 -1
- package/crates/team-agent/src/cli/named_address.rs +127 -25
- package/crates/team-agent/src/cli/send.rs +442 -521
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +32 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -4
- package/crates/team-agent/src/cli/tests/status_send.rs +59 -33
- package/crates/team-agent/src/cli/types.rs +4 -8
- package/crates/team-agent/src/lifecycle/launch.rs +23 -9
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +33 -16
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -2
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +16 -5
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +12 -10
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/mod.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +34 -11
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -721,7 +721,7 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
|
|
|
721
721
|
&SendOptions {
|
|
722
722
|
task_id: Some(TaskId::new("task_impl")),
|
|
723
723
|
route_task_id: false,
|
|
724
|
-
sender:
|
|
724
|
+
sender: messaging::TrustedSender::leader(),
|
|
725
725
|
requires_ack: true,
|
|
726
726
|
..SendOptions::default()
|
|
727
727
|
},
|
|
@@ -315,21 +315,9 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
315
315
|
Some("start") => compat_hidden_help("start", "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]"),
|
|
316
316
|
Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
|
|
317
317
|
Some("send") => concat!(
|
|
318
|
-
"usage: team-agent send
|
|
319
|
-
"[--workspace WORKSPACE] [--team TEAM] [--
|
|
320
|
-
"
|
|
321
|
-
"[--watch-result] [--requires-ack|--no-ack] [--no-wait] ",
|
|
322
|
-
"[--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]\n\n",
|
|
323
|
-
"TARGET is a short id scoped by --team; MCP `to` is a short id scoped ",
|
|
324
|
-
"by the worker's owner team.\n",
|
|
325
|
-
"--to-name accepts: --to-name AGENT, --to-name TEAM/AGENT, or ",
|
|
326
|
-
"--to-name WORKSPACE::TEAM/AGENT.\n",
|
|
327
|
-
"--team scopes only a bare --to-name AGENT; qualified forms keep the ",
|
|
328
|
-
"scope in the address.\n",
|
|
329
|
-
"TEAM/AGENT and WORKSPACE::TEAM/AGENT are not valid positional TARGET ",
|
|
330
|
-
"or MCP `to` values.\n\n",
|
|
331
|
-
"MVP: name-based cross-workspace addressing assumes trusted local ",
|
|
332
|
-
"caller; no auth gate."
|
|
318
|
+
"usage: team-agent send TO MESSAGE... ",
|
|
319
|
+
"[--workspace WORKSPACE] [--team TEAM] [--json]\n\n",
|
|
320
|
+
"TO is a logical recipient; send returns after the message is persisted."
|
|
333
321
|
)
|
|
334
322
|
.to_string(),
|
|
335
323
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
@@ -732,7 +720,6 @@ struct ParsedArgs {
|
|
|
732
720
|
team_id: Option<String>,
|
|
733
721
|
targets: Option<String>,
|
|
734
722
|
task: Option<String>,
|
|
735
|
-
sender: Option<String>,
|
|
736
723
|
watch_result: bool,
|
|
737
724
|
requires_ack: bool,
|
|
738
725
|
no_ack: bool,
|
|
@@ -816,7 +803,6 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
816
803
|
"--targets" | "--target" | "--to" => parsed.targets = next_arg(args, &mut i),
|
|
817
804
|
"--task" => parsed.task = next_arg(args, &mut i),
|
|
818
805
|
"--task-id" => parsed.task_id = next_arg(args, &mut i),
|
|
819
|
-
"--sender" => parsed.sender = next_arg(args, &mut i),
|
|
820
806
|
"--agent-id" => parsed.agent_id = next_arg(args, &mut i),
|
|
821
807
|
"--watch-result" => parsed.watch_result = true,
|
|
822
808
|
"--requires-ack" => parsed.requires_ack = true,
|
|
@@ -1027,6 +1013,8 @@ fn resolve_cli_path(cwd: &Path, path: &Path) -> PathBuf {
|
|
|
1027
1013
|
}
|
|
1028
1014
|
|
|
1029
1015
|
fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
1016
|
+
validate_send_flags(args)?;
|
|
1017
|
+
warn_send_legacy_delivery_flags(args);
|
|
1030
1018
|
let parsed = parse_args(args);
|
|
1031
1019
|
let target = if parsed.targets.is_some()
|
|
1032
1020
|
|| parsed.pane.is_some()
|
|
@@ -1050,21 +1038,96 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
|
1050
1038
|
targets: parsed.targets,
|
|
1051
1039
|
workspace,
|
|
1052
1040
|
team: parsed.team,
|
|
1053
|
-
task:
|
|
1054
|
-
sender:
|
|
1055
|
-
no_ack:
|
|
1056
|
-
no_wait:
|
|
1057
|
-
watch_result:
|
|
1058
|
-
timeout:
|
|
1059
|
-
confirm_human:
|
|
1041
|
+
task: None,
|
|
1042
|
+
sender: trusted_cli_sender(),
|
|
1043
|
+
no_ack: false,
|
|
1044
|
+
no_wait: true,
|
|
1045
|
+
watch_result: false,
|
|
1046
|
+
timeout: 0.0,
|
|
1047
|
+
confirm_human: false,
|
|
1060
1048
|
json: parsed.json,
|
|
1061
|
-
message_id:
|
|
1049
|
+
message_id: None,
|
|
1062
1050
|
pane: parsed.pane.clone(),
|
|
1063
1051
|
to_name: parsed.to_name.clone(),
|
|
1064
1052
|
to_leader: parsed.to_leader.clone(),
|
|
1065
1053
|
})
|
|
1066
1054
|
}
|
|
1067
1055
|
|
|
1056
|
+
fn trusted_cli_sender() -> TrustedSender {
|
|
1057
|
+
std::env::var("TEAM_AGENT_ID")
|
|
1058
|
+
.ok()
|
|
1059
|
+
.map(|value| value.trim().to_string())
|
|
1060
|
+
.filter(|value| !value.is_empty())
|
|
1061
|
+
.map(crate::model::ids::AgentId::new)
|
|
1062
|
+
.map(TrustedSender::from_runtime_identity)
|
|
1063
|
+
.unwrap_or_else(TrustedSender::leader)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
fn warn_send_legacy_delivery_flags(args: &[String]) {
|
|
1067
|
+
const FLAGS: &[&str] = &[
|
|
1068
|
+
"--task",
|
|
1069
|
+
"--watch-result",
|
|
1070
|
+
"--requires-ack",
|
|
1071
|
+
"--no-ack",
|
|
1072
|
+
"--no-wait",
|
|
1073
|
+
"--timeout",
|
|
1074
|
+
"--confirm-human",
|
|
1075
|
+
"--message-id",
|
|
1076
|
+
];
|
|
1077
|
+
let spec = command_spec("send");
|
|
1078
|
+
let sunset = spec
|
|
1079
|
+
.and_then(|spec| spec.sunset)
|
|
1080
|
+
.unwrap_or("next compatibility release");
|
|
1081
|
+
let action = spec
|
|
1082
|
+
.and_then(|spec| spec.action)
|
|
1083
|
+
.unwrap_or("use positional logical TO and the returned message id");
|
|
1084
|
+
for flag in FLAGS {
|
|
1085
|
+
if args
|
|
1086
|
+
.iter()
|
|
1087
|
+
.any(|arg| arg == flag || arg.starts_with(&format!("{flag}=")))
|
|
1088
|
+
{
|
|
1089
|
+
eprintln!("warning: {flag} is deprecated; sunset: {sunset}; action: {action}");
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
fn validate_send_flags(args: &[String]) -> Result<(), CliError> {
|
|
1095
|
+
const ALLOWED: &[&str] = &[
|
|
1096
|
+
"--workspace",
|
|
1097
|
+
"--team",
|
|
1098
|
+
"--targets",
|
|
1099
|
+
"--target",
|
|
1100
|
+
"--to",
|
|
1101
|
+
"--to-name",
|
|
1102
|
+
"--to-leader",
|
|
1103
|
+
"--pane",
|
|
1104
|
+
"--task",
|
|
1105
|
+
"--watch-result",
|
|
1106
|
+
"--requires-ack",
|
|
1107
|
+
"--no-ack",
|
|
1108
|
+
"--no-wait",
|
|
1109
|
+
"--timeout",
|
|
1110
|
+
"--confirm-human",
|
|
1111
|
+
"--message-id",
|
|
1112
|
+
"--json",
|
|
1113
|
+
"-h",
|
|
1114
|
+
"--help",
|
|
1115
|
+
];
|
|
1116
|
+
const ALLOWED_PREFIXES: &[&str] = &["--team=", "--pane=", "--to-name=", "--to-leader="];
|
|
1117
|
+
if let Some(flag) = args.iter().find(|arg| {
|
|
1118
|
+
arg.starts_with('-')
|
|
1119
|
+
&& !ALLOWED.contains(&arg.as_str())
|
|
1120
|
+
&& !ALLOWED_PREFIXES
|
|
1121
|
+
.iter()
|
|
1122
|
+
.any(|prefix| arg.starts_with(prefix))
|
|
1123
|
+
}) {
|
|
1124
|
+
return Err(CliError::Usage(format!(
|
|
1125
|
+
"unrecognized argument for `send`: {flag}"
|
|
1126
|
+
)));
|
|
1127
|
+
}
|
|
1128
|
+
Ok(())
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1068
1131
|
/// Stage 4 of identity-boundary unified plan (architect direction
|
|
1069
1132
|
/// 2026-06-24, .team/artifacts/identity-boundary-unified-plan.md §2 Stage
|
|
1070
1133
|
/// 4): destructive command ambiguity gate. When the workspace has 2+
|
|
@@ -1760,19 +1823,7 @@ mod tests {
|
|
|
1760
1823
|
"quick-start",
|
|
1761
1824
|
&["--workspace", "--team-id", "--yes", "--json"][..],
|
|
1762
1825
|
),
|
|
1763
|
-
(
|
|
1764
|
-
"send",
|
|
1765
|
-
&[
|
|
1766
|
-
"--workspace",
|
|
1767
|
-
"--team",
|
|
1768
|
-
"--targets",
|
|
1769
|
-
"--to-name",
|
|
1770
|
-
"--pane",
|
|
1771
|
-
"--watch-result",
|
|
1772
|
-
"--timeout",
|
|
1773
|
-
"--json",
|
|
1774
|
-
][..],
|
|
1775
|
-
),
|
|
1826
|
+
("send", &["--workspace", "--team", "--json"][..]),
|
|
1776
1827
|
(
|
|
1777
1828
|
"status",
|
|
1778
1829
|
&["--workspace", "--team", "--summary", "--json", "--detail"][..],
|
|
@@ -44,7 +44,7 @@ use serde_json::{json, Map, Value};
|
|
|
44
44
|
use thiserror::Error;
|
|
45
45
|
|
|
46
46
|
// REUSE in-tree(只 import,不 redefine):
|
|
47
|
-
use crate::messaging::{self, AlertType, MessageTarget, SendOptions};
|
|
47
|
+
use crate::messaging::{self, AlertType, MessageTarget, SendOptions, TrustedSender};
|
|
48
48
|
use crate::model::ids::{TaskId, TeamKey};
|
|
49
49
|
|
|
50
50
|
pub(crate) const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
@@ -472,7 +472,10 @@ pub(crate) fn parse_leader_target_workspace_and_team(
|
|
|
472
472
|
let target_workspace = resolve_workspace(sender_workspace, parsed.workspace.as_deref())?;
|
|
473
473
|
match parsed.target {
|
|
474
474
|
ParsedTarget::TeamEntity { team, entity } if entity == "leader" => {
|
|
475
|
-
|
|
475
|
+
let state = load_state_checked(&target_workspace)?;
|
|
476
|
+
let canonical = canonical_named_team_key(&target_workspace, &state, &team)
|
|
477
|
+
.unwrap_or(team);
|
|
478
|
+
Ok(Some((target_workspace, canonical)))
|
|
476
479
|
}
|
|
477
480
|
_ => Ok(None),
|
|
478
481
|
}
|
|
@@ -607,23 +610,30 @@ fn resolve_in_workspace(
|
|
|
607
610
|
ParsedTarget::BareAgent(agent) => {
|
|
608
611
|
resolve_bare_agent(sender_workspace, target_workspace, state, agent, transport)
|
|
609
612
|
}
|
|
610
|
-
ParsedTarget::TeamEntity { team, entity }
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
613
|
+
ParsedTarget::TeamEntity { team, entity } => {
|
|
614
|
+
let canonical = canonical_named_team_key(target_workspace, state, team)
|
|
615
|
+
.unwrap_or_else(|| team.clone());
|
|
616
|
+
if entity == "leader" {
|
|
617
|
+
resolve_leader(
|
|
618
|
+
sender_workspace,
|
|
619
|
+
target_workspace,
|
|
620
|
+
state,
|
|
621
|
+
&canonical,
|
|
622
|
+
parsed,
|
|
623
|
+
transport,
|
|
624
|
+
)
|
|
625
|
+
} else {
|
|
626
|
+
resolve_worker(
|
|
627
|
+
sender_workspace,
|
|
628
|
+
target_workspace,
|
|
629
|
+
state,
|
|
630
|
+
&canonical,
|
|
631
|
+
entity,
|
|
632
|
+
parsed,
|
|
633
|
+
transport,
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
627
637
|
ParsedTarget::SessionWindow { session, window } => resolve_session_window(
|
|
628
638
|
sender_workspace,
|
|
629
639
|
target_workspace,
|
|
@@ -670,12 +680,38 @@ fn resolve_worker(
|
|
|
670
680
|
.with_scoped_suggestions_for_agent(team_entry, team, agent, parsed));
|
|
671
681
|
}
|
|
672
682
|
};
|
|
673
|
-
let session = string_field(team_entry, "session_name")
|
|
674
|
-
|
|
683
|
+
let Some(session) = string_field(team_entry, "session_name") else {
|
|
684
|
+
return Ok(resolved_worker_without_live(
|
|
685
|
+
sender_workspace,
|
|
686
|
+
target_workspace,
|
|
687
|
+
team,
|
|
688
|
+
agent,
|
|
689
|
+
parsed,
|
|
690
|
+
agent_entry,
|
|
691
|
+
None,
|
|
692
|
+
agent,
|
|
693
|
+
transport.tmux_endpoint(),
|
|
694
|
+
));
|
|
695
|
+
};
|
|
675
696
|
let window = string_field(agent_entry, "window")
|
|
676
697
|
.or_else(|| string_field(agent_entry, "window_name"))
|
|
677
698
|
.unwrap_or(agent);
|
|
678
|
-
let targets = list_targets(transport)
|
|
699
|
+
let targets = match list_targets(transport) {
|
|
700
|
+
Ok(targets) => targets,
|
|
701
|
+
Err(_) => {
|
|
702
|
+
return Ok(resolved_worker_without_live(
|
|
703
|
+
sender_workspace,
|
|
704
|
+
target_workspace,
|
|
705
|
+
team,
|
|
706
|
+
agent,
|
|
707
|
+
parsed,
|
|
708
|
+
agent_entry,
|
|
709
|
+
Some(session),
|
|
710
|
+
window,
|
|
711
|
+
transport.tmux_endpoint(),
|
|
712
|
+
));
|
|
713
|
+
}
|
|
714
|
+
};
|
|
679
715
|
let matches = matching_session_window(&targets, session, window);
|
|
680
716
|
match matches.len() {
|
|
681
717
|
1 => {
|
|
@@ -710,10 +746,14 @@ fn resolve_worker(
|
|
|
710
746
|
warning,
|
|
711
747
|
})
|
|
712
748
|
}
|
|
713
|
-
0 =>
|
|
749
|
+
0 => Ok(resolved_worker_without_live(
|
|
750
|
+
sender_workspace,
|
|
751
|
+
target_workspace,
|
|
714
752
|
team,
|
|
715
753
|
agent,
|
|
716
|
-
|
|
754
|
+
parsed,
|
|
755
|
+
agent_entry,
|
|
756
|
+
Some(session),
|
|
717
757
|
window,
|
|
718
758
|
transport.tmux_endpoint(),
|
|
719
759
|
)),
|
|
@@ -737,6 +777,42 @@ fn resolve_worker(
|
|
|
737
777
|
}
|
|
738
778
|
}
|
|
739
779
|
|
|
780
|
+
#[allow(clippy::too_many_arguments)]
|
|
781
|
+
fn resolved_worker_without_live(
|
|
782
|
+
sender_workspace: &Path,
|
|
783
|
+
target_workspace: &Path,
|
|
784
|
+
team: &str,
|
|
785
|
+
agent: &str,
|
|
786
|
+
parsed: &ParsedNamedAddress,
|
|
787
|
+
agent_entry: &Value,
|
|
788
|
+
session: Option<&str>,
|
|
789
|
+
window: &str,
|
|
790
|
+
tmux_endpoint: Option<String>,
|
|
791
|
+
) -> ResolvedNamedAddress {
|
|
792
|
+
let state_pane_id = string_field(agent_entry, "pane_id").map(str::to_string);
|
|
793
|
+
ResolvedNamedAddress {
|
|
794
|
+
raw_name: parsed.display_name(),
|
|
795
|
+
target_kind: NamedTargetKind::Worker,
|
|
796
|
+
sender_workspace: sender_workspace.to_path_buf(),
|
|
797
|
+
target_workspace: target_workspace.to_path_buf(),
|
|
798
|
+
team_key: Some(team.to_string()),
|
|
799
|
+
agent_id: Some(agent.to_string()),
|
|
800
|
+
pane_id: state_pane_id.clone().unwrap_or_default(),
|
|
801
|
+
session_name: session.map(str::to_string),
|
|
802
|
+
window_name: Some(window.to_string()),
|
|
803
|
+
tmux_endpoint,
|
|
804
|
+
transport_kind: Some("direct_tmux".to_string()),
|
|
805
|
+
app_server: None,
|
|
806
|
+
state_pane_id,
|
|
807
|
+
state_pane_stale: true,
|
|
808
|
+
agent_status: string_field(agent_entry, "status").map(str::to_string),
|
|
809
|
+
warning: Some(
|
|
810
|
+
"agent has no live pane; message will be persisted for standard delivery recovery"
|
|
811
|
+
.to_string(),
|
|
812
|
+
),
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
740
816
|
fn resolve_bare_agent(
|
|
741
817
|
sender_workspace: &Path,
|
|
742
818
|
target_workspace: &Path,
|
|
@@ -1149,8 +1225,10 @@ fn transport_for_cli_target(
|
|
|
1149
1225
|
parsed: &ParsedNamedAddress,
|
|
1150
1226
|
) -> Box<dyn Transport> {
|
|
1151
1227
|
if let ParsedTarget::TeamEntity { team, entity } = &parsed.target {
|
|
1228
|
+
let canonical = canonical_named_team_key(target_workspace, state, team)
|
|
1229
|
+
.unwrap_or_else(|| team.clone());
|
|
1152
1230
|
if entity == "leader" {
|
|
1153
|
-
let team_scoped_socket = team_entry(state,
|
|
1231
|
+
let team_scoped_socket = team_entry(state, &canonical)
|
|
1154
1232
|
.and_then(|entry| entry.get("leader_receiver"))
|
|
1155
1233
|
.and_then(|receiver| string_field(receiver, "tmux_socket"));
|
|
1156
1234
|
let socket = team_scoped_socket.map(str::to_string).or_else(|| {
|
|
@@ -1164,7 +1242,7 @@ fn transport_for_cli_target(
|
|
|
1164
1242
|
return Box::new(crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&socket));
|
|
1165
1243
|
}
|
|
1166
1244
|
}
|
|
1167
|
-
if let Some(entry) = team_entry(state,
|
|
1245
|
+
if let Some(entry) = team_entry(state, &canonical) {
|
|
1168
1246
|
let selected = crate::tmux_backend::tmux_backend_for_runtime_state_or_workspace(
|
|
1169
1247
|
target_workspace,
|
|
1170
1248
|
Some(entry),
|
|
@@ -1247,6 +1325,30 @@ fn team_entry<'a>(state: &'a Value, team: &str) -> Option<&'a Value> {
|
|
|
1247
1325
|
.and_then(|teams| teams.get(team))
|
|
1248
1326
|
}
|
|
1249
1327
|
|
|
1328
|
+
fn canonical_named_team_key(
|
|
1329
|
+
target_workspace: &Path,
|
|
1330
|
+
state: &Value,
|
|
1331
|
+
requested: &str,
|
|
1332
|
+
) -> Option<String> {
|
|
1333
|
+
if team_entry(state, requested).is_some() || is_legacy_single_team_active(state, requested) {
|
|
1334
|
+
return Some(requested.to_string());
|
|
1335
|
+
}
|
|
1336
|
+
let selected = crate::state::projection::select_runtime_state(
|
|
1337
|
+
target_workspace,
|
|
1338
|
+
Some(requested),
|
|
1339
|
+
)
|
|
1340
|
+
.ok()?;
|
|
1341
|
+
let canonical = selected
|
|
1342
|
+
.get("active_team_key")
|
|
1343
|
+
.and_then(Value::as_str)
|
|
1344
|
+
.filter(|team| !team.is_empty())?;
|
|
1345
|
+
state
|
|
1346
|
+
.get("teams")
|
|
1347
|
+
.and_then(Value::as_object)
|
|
1348
|
+
.is_some_and(|teams| teams.contains_key(canonical))
|
|
1349
|
+
.then(|| canonical.to_string())
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1250
1352
|
/// E6 (0.5.9 offline-mailbox-toname-design §3.1): decide which
|
|
1251
1353
|
/// `leader_receiver` object, if any, applies to `<team>/leader` before any
|
|
1252
1354
|
/// downstream code reads it. Wrong runtime keys are rejected here with
|