@team-agent/installer 0.3.9 → 0.3.10

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.
@@ -10,6 +10,8 @@ pub struct ApprovalPrompt {
10
10
  pub prompt: String,
11
11
  pub choices: Vec<String>,
12
12
  #[serde(skip_serializing_if = "Option::is_none")]
13
+ pub server: Option<String>,
14
+ #[serde(skip_serializing_if = "Option::is_none")]
13
15
  pub tool: Option<String>,
14
16
  #[serde(skip_serializing_if = "Option::is_none")]
15
17
  pub command: Option<String>,
@@ -21,6 +23,9 @@ impl ApprovalPrompt {
21
23
  map.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
22
24
  map.insert("state".to_string(), serde_json::json!(self.state));
23
25
  map.insert("kind".to_string(), serde_json::json!(self.kind));
26
+ if let Some(server) = &self.server {
27
+ map.insert("server".to_string(), serde_json::json!(server));
28
+ }
24
29
  if let Some(tool) = &self.tool {
25
30
  map.insert("tool".to_string(), serde_json::json!(tool));
26
31
  }
@@ -54,17 +59,17 @@ pub fn extract_approval_prompt(agent_id: &str, capture: &str) -> Option<Approval
54
59
  let lines: Vec<&str> = capture.lines().collect();
55
60
  let control_idx = active_approval_control_index(&lines)?;
56
61
 
57
- if let Some((matched_idx, prompt, tool)) = explicit_mcp_prompt(&lines, control_idx) {
62
+ if let Some((matched_idx, prompt, server, tool)) = explicit_mcp_prompt(&lines, control_idx) {
58
63
  let choices = approval_choices_between(&lines, matched_idx, control_idx);
59
- return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(tool), None));
64
+ return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(server), Some(tool), None));
60
65
  }
61
- if let Some((matched_idx, prompt, tool)) = short_mcp_prompt(&lines, control_idx) {
66
+ if let Some((matched_idx, prompt, server, tool)) = short_mcp_prompt(&lines, control_idx) {
62
67
  let choices = approval_choices_between(&lines, matched_idx, control_idx);
63
- return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(tool), None));
68
+ return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(server), Some(tool), None));
64
69
  }
65
70
  if let Some((matched_idx, prompt, command)) = command_prompt(&lines, control_idx) {
66
71
  let choices = approval_choices_between(&lines, matched_idx, control_idx);
67
- return Some(approval(agent_id, ApprovalKind::Command, prompt, choices, None, Some(command)));
72
+ return Some(approval(agent_id, ApprovalKind::Command, prompt, choices, None, None, Some(command)));
68
73
  }
69
74
  let choices = approval_choices_between(&lines, 0, control_idx);
70
75
  Some(approval(
@@ -74,6 +79,7 @@ pub fn extract_approval_prompt(agent_id: &str, capture: &str) -> Option<Approval
74
79
  choices,
75
80
  None,
76
81
  None,
82
+ None,
77
83
  ))
78
84
  }
79
85
 
@@ -117,6 +123,7 @@ fn approval(
117
123
  kind: ApprovalKind,
118
124
  prompt: String,
119
125
  choices: Vec<String>,
126
+ server: Option<String>,
120
127
  tool: Option<String>,
121
128
  command: Option<String>,
122
129
  ) -> ApprovalPrompt {
@@ -126,6 +133,7 @@ fn approval(
126
133
  kind,
127
134
  prompt,
128
135
  choices,
136
+ server,
129
137
  tool,
130
138
  command,
131
139
  }
@@ -137,15 +145,18 @@ fn is_control_line(line: &str) -> bool {
137
145
  || (lower.contains("esc to cancel") && lower.contains("tab to amend"))
138
146
  }
139
147
 
140
- fn explicit_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String)> {
148
+ fn explicit_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String, String)> {
141
149
  for (idx, raw) in lines.iter().enumerate().take(control_idx).rev() {
142
150
  let line = clean_line(raw);
143
- let prefix = "Allow the team_orchestrator MCP server to run tool \"";
151
+ let prefix = "Allow the ";
144
152
  if let Some(rest) = line.strip_prefix(prefix) {
145
- if let Some((tool, _)) = rest.split_once('"') {
146
- if !tool.is_empty() {
147
- let tool = tool.to_string();
148
- return Some((idx, line, tool));
153
+ if let Some((server, rest)) = rest.split_once(" MCP server to run tool \"") {
154
+ if let Some((tool, _)) = rest.split_once('"') {
155
+ if !server.is_empty() && !tool.is_empty() {
156
+ let server = server.to_string();
157
+ let tool = tool.to_string();
158
+ return Some((idx, line, server, tool));
159
+ }
149
160
  }
150
161
  }
151
162
  }
@@ -153,17 +164,17 @@ fn explicit_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, Str
153
164
  None
154
165
  }
155
166
 
156
- fn short_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String)> {
167
+ fn short_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String, String)> {
157
168
  for (idx, raw) in lines.iter().enumerate().take(control_idx).rev() {
158
169
  let line = clean_line(raw);
159
170
  if parse_choice_line(&line).is_some() {
160
171
  continue;
161
172
  }
162
173
  if let Some(tool) = team_orchestrator_tool(&line, '-') {
163
- return Some((idx, format!("team_orchestrator - {tool}"), tool));
174
+ return Some((idx, format!("team_orchestrator - {tool}"), "team_orchestrator".to_string(), tool));
164
175
  }
165
176
  if let Some(tool) = team_orchestrator_tool(&line, '.') {
166
- return Some((idx, format!("team_orchestrator - {tool}"), tool));
177
+ return Some((idx, format!("team_orchestrator - {tool}"), "team_orchestrator".to_string(), tool));
167
178
  }
168
179
  }
169
180
  None
@@ -287,6 +298,7 @@ mod tests {
287
298
  assert_eq!(prompt.agent_id, "agent-x");
288
299
  assert_eq!(prompt.state, "waiting_approval");
289
300
  assert_eq!(prompt.kind, ApprovalKind::McpTool);
301
+ assert_eq!(prompt.server.as_deref(), Some("team_orchestrator"));
290
302
  assert_eq!(prompt.tool.as_deref(), Some("send_message"));
291
303
  assert_eq!(prompt.prompt, "Allow the team_orchestrator MCP server to run tool \"send_message\"?");
292
304
  assert_eq!(prompt.choices, vec!["Allow for this session", "Deny"]);
@@ -300,10 +312,23 @@ mod tests {
300
312
  )
301
313
  .unwrap();
302
314
  assert_eq!(prompt.kind, ApprovalKind::McpTool);
315
+ assert_eq!(prompt.server.as_deref(), Some("team_orchestrator"));
303
316
  assert_eq!(prompt.tool.as_deref(), Some("assign_task"));
304
317
  assert_eq!(prompt.prompt, "team_orchestrator - assign_task");
305
318
  }
306
319
 
320
+ #[test]
321
+ fn explicit_mcp_prompt_preserves_custom_server_name() {
322
+ let prompt = extract_approval_prompt(
323
+ "agent-x",
324
+ "Allow the custom_server MCP server to run tool \"write_file\"?\n 1. Allow\nEnter to submit | Esc to cancel\n",
325
+ )
326
+ .unwrap();
327
+ assert_eq!(prompt.kind, ApprovalKind::McpTool);
328
+ assert_eq!(prompt.server.as_deref(), Some("custom_server"));
329
+ assert_eq!(prompt.tool.as_deref(), Some("write_file"));
330
+ }
331
+
307
332
  #[test]
308
333
  fn command_prompt_extracts_command_before_prompt() {
309
334
  let prompt = extract_approval_prompt(
@@ -354,6 +379,7 @@ mod tests {
354
379
  ],
355
380
  None,
356
381
  None,
382
+ None,
357
383
  );
358
384
  assert_eq!(choose_internal_mcp_approval_choice(&prompt), "Allow for this session");
359
385
  prompt.choices = vec!["Maybe".to_string()];
@@ -374,6 +400,7 @@ mod tests {
374
400
  vec!["Allow".to_string(), "Deny".to_string()],
375
401
  None,
376
402
  None,
403
+ None,
377
404
  );
378
405
  assert_eq!(approval_choice_keys(&prompt, "❯ 1. Allow\n", "Missing"), vec!["Down", "Enter"]);
379
406
  assert_eq!(approval_choice_keys(&prompt, " 1. Allow\n 2. Deny\n", "Deny"), vec!["2", "Enter"]);
@@ -413,6 +440,7 @@ mod tests {
413
440
  ApprovalKind::McpTool,
414
441
  "p".to_string(),
415
442
  vec!["Allow".to_string()],
443
+ None,
416
444
  Some("send_message".to_string()),
417
445
  None,
418
446
  )
@@ -426,6 +454,7 @@ mod tests {
426
454
  "p".to_string(),
427
455
  vec!["Yes".to_string()],
428
456
  None,
457
+ None,
429
458
  Some("Bash(echo hi)".to_string()),
430
459
  )
431
460
  .to_ordered_value();
@@ -5,15 +5,15 @@ use sha2::{Digest, Sha256};
5
5
 
6
6
  use crate::provider::{ApprovalFingerprint, ApprovalKind, ApprovalPrompt};
7
7
 
8
- pub const RUNTIME_MCP_APPROVAL_ALLOWLIST: &[&str] = &[
9
- "send_message",
10
- "report_result",
11
- "get_team_status",
12
- "request_human",
13
- ];
8
+ pub const RUNTIME_MCP_APPROVAL_SERVER: &str = "team_orchestrator";
14
9
 
15
10
  pub fn runtime_mcp_tool_allowlisted(tool: &str) -> bool {
16
- RUNTIME_MCP_APPROVAL_ALLOWLIST.contains(&tool)
11
+ !tool.trim().is_empty()
12
+ }
13
+
14
+ pub fn runtime_mcp_prompt_allowlisted(prompt: &ApprovalPrompt) -> bool {
15
+ prompt.server.as_deref() == Some(RUNTIME_MCP_APPROVAL_SERVER)
16
+ && prompt.tool.as_deref().is_some_and(runtime_mcp_tool_allowlisted)
17
17
  }
18
18
 
19
19
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -79,8 +79,7 @@ pub fn awaiting_human_confirm_reason(
79
79
  ) -> Option<&'static str> {
80
80
  match prompt.kind {
81
81
  ApprovalKind::McpTool => {
82
- let tool = prompt.tool.as_deref()?;
83
- if !runtime_mcp_tool_allowlisted(tool) {
82
+ if !runtime_mcp_prompt_allowlisted(prompt) {
84
83
  Some("tool_not_allowlisted")
85
84
  } else if !leader_auto_approval_allowed {
86
85
  Some("leader_restricted")
@@ -105,6 +104,10 @@ pub fn approval_prompt_fingerprint(team: &str, agent_id: &str, prompt: &Approval
105
104
  hasher.update(tool.as_bytes());
106
105
  }
107
106
  hasher.update([0]);
107
+ if let Some(server) = prompt.server.as_deref() {
108
+ hasher.update(server.as_bytes());
109
+ }
110
+ hasher.update([0]);
108
111
  if let Some(command) = prompt.command.as_deref() {
109
112
  hasher.update(command.as_bytes());
110
113
  }
@@ -25,6 +25,7 @@ struct OfflineState {
25
25
  targets: Vec<PaneInfo>,
26
26
  windows: Vec<WindowName>,
27
27
  pane_presence: BTreeMap<String, bool>,
28
+ spawned_panes_addressable: bool,
28
29
  liveness: BTreeMap<String, PaneLiveness>,
29
30
  default_liveness: PaneLiveness,
30
31
  calls: Vec<&'static str>,
@@ -41,6 +42,7 @@ impl Default for OfflineState {
41
42
  targets: Vec::new(),
42
43
  windows: Vec::new(),
43
44
  pane_presence: BTreeMap::new(),
45
+ spawned_panes_addressable: true,
44
46
  liveness: BTreeMap::new(),
45
47
  default_liveness: PaneLiveness::Unknown,
46
48
  calls: Vec::new(),
@@ -100,6 +102,11 @@ impl OfflineTransport {
100
102
  self
101
103
  }
102
104
 
105
+ pub fn with_spawned_panes_addressable(self, present: bool) -> Self {
106
+ self.with_state(|state| state.spawned_panes_addressable = present);
107
+ self
108
+ }
109
+
103
110
  pub fn calls(&self) -> Vec<&'static str> {
104
111
  self.with_state(|state| state.calls.clone())
105
112
  }
@@ -149,7 +156,11 @@ impl OfflineTransport {
149
156
  if kind == "spawn_first" && !state.session_absent_after_spawn_first {
150
157
  state.session_present = true;
151
158
  }
152
- state.spawns.len().saturating_sub(1)
159
+ let pane_index = state.spawns.len().saturating_sub(1);
160
+ state
161
+ .pane_presence
162
+ .insert(format!("%{pane_index}"), state.spawned_panes_addressable);
163
+ pane_index
153
164
  });
154
165
  SpawnResult {
155
166
  pane_id: PaneId::new(format!("%{pane_index}")),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.3.9",
24
- "@team-agent/cli-darwin-x64": "0.3.9",
25
- "@team-agent/cli-linux-x64": "0.3.9"
23
+ "@team-agent/cli-darwin-arm64": "0.3.10",
24
+ "@team-agent/cli-darwin-x64": "0.3.10",
25
+ "@team-agent/cli-linux-x64": "0.3.10"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",