@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 CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.48"
578
+ version = "0.5.50"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.48"
12
+ version = "0.5.50"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -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: "leader".to_string(),
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 TARGET MESSAGE... ",
319
- "[--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] ",
320
- "[--to-name NAME] [--pane PANE] [--task TASK] [--sender SENDER] ",
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: parsed.task,
1054
- sender: parsed.sender.unwrap_or_else(|| "leader".to_string()),
1055
- no_ack: parsed.no_ack && !parsed.requires_ack,
1056
- no_wait: parsed.no_wait,
1057
- watch_result: parsed.watch_result,
1058
- timeout: parsed.timeout.unwrap_or(30.0),
1059
- confirm_human: parsed.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: parsed.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
- Ok(Some((target_workspace, team)))
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 } if entity == "leader" => resolve_leader(
611
- sender_workspace,
612
- target_workspace,
613
- state,
614
- team,
615
- parsed,
616
- transport,
617
- ),
618
- ParsedTarget::TeamEntity { team, entity } => resolve_worker(
619
- sender_workspace,
620
- target_workspace,
621
- state,
622
- team,
623
- entity,
624
- parsed,
625
- transport,
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
- .ok_or_else(|| name_not_resolvable("team is missing session_name"))?;
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 => Err(name_not_live_worker(
749
+ 0 => Ok(resolved_worker_without_live(
750
+ sender_workspace,
751
+ target_workspace,
714
752
  team,
715
753
  agent,
716
- session,
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, team)
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, team) {
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