@team-agent/installer 0.3.18 → 0.3.19

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
@@ -566,7 +566,7 @@ dependencies = [
566
566
 
567
567
  [[package]]
568
568
  name = "team-agent"
569
- version = "0.3.18"
569
+ version = "0.3.19"
570
570
  dependencies = [
571
571
  "anyhow",
572
572
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.3.18"
12
+ version = "0.3.19"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -32,9 +32,10 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
32
32
  return emit_missing_subcommand_usage();
33
33
  };
34
34
  if command == "codex" || command == "claude" || command == "copilot" {
35
- return cmd_leader_passthrough(command, &argv[1..], cwd)
36
- .map(emit_result)
37
- .unwrap_or(ExitCode::Error);
35
+ return match cmd_leader_passthrough(command, &argv[1..], cwd) {
36
+ Ok(result) => emit_result(result),
37
+ Err(error) => emit_cli_error(command, &argv[1..], cwd, &error),
38
+ };
38
39
  }
39
40
  if matches!(command, "-h" | "--help" | "help") {
40
41
  println!("{}", command_help(None));
@@ -23,7 +23,14 @@ pub fn leader_launcher_args(values: &[String]) -> Result<LeaderLauncherArgs, Cli
23
23
  while idx < values.len() {
24
24
  let token = &values[idx];
25
25
  if token == "--" {
26
- out.provider_args.extend(values.iter().skip(idx).cloned());
26
+ for value in values.iter().skip(idx + 1) {
27
+ if is_leader_launcher_flag(value) {
28
+ return Err(CliError::Runtime(format!(
29
+ "Team Agent launcher flag {value} must appear before --"
30
+ )));
31
+ }
32
+ }
33
+ out.provider_args.extend(values.iter().skip(idx + 1).cloned());
27
34
  break;
28
35
  } else if token == "--attach" || token == "--attach-existing" {
29
36
  out.attach_existing = true;
@@ -49,6 +56,13 @@ pub fn leader_launcher_args(values: &[String]) -> Result<LeaderLauncherArgs, Cli
49
56
  Ok(out)
50
57
  }
51
58
 
59
+ fn is_leader_launcher_flag(value: &str) -> bool {
60
+ matches!(
61
+ value,
62
+ "--external-leader" | "--attach" | "--attach-existing" | "--confirm" | "--attach-session"
63
+ ) || value.starts_with("--attach-session=")
64
+ }
65
+
52
66
  fn leader_launcher_json(values: &[String]) -> bool {
53
67
  values
54
68
  .iter()
@@ -86,17 +86,18 @@ use super::*;
86
86
  assert!(!got.attach_existing);
87
87
  assert_eq!(
88
88
  got.provider_args,
89
- vec!["--".to_string(), "--model".to_string(), "opus".to_string()]
89
+ vec!["--model".to_string(), "opus".to_string()]
90
90
  );
91
91
  }
92
92
 
93
93
  #[test]
94
- fn leader_launcher_args_external_leader_after_dashdash_is_provider_arg() {
95
- let got = leader_launcher_args(&["--".into(), "--external-leader".into()]).unwrap();
96
- assert!(!got.external_leader);
97
- assert_eq!(
98
- got.provider_args,
99
- vec!["--".to_string(), "--external-leader".to_string()]
94
+ fn leader_launcher_args_external_leader_after_dashdash_errors() {
95
+ let err = leader_launcher_args(&["--".into(), "--external-leader".into()])
96
+ .expect_err("Team Agent flags after -- must not be silently passed to provider");
97
+ assert!(
98
+ err.to_string()
99
+ .contains("Team Agent launcher flag --external-leader must appear before --"),
100
+ "unexpected error: {err}"
100
101
  );
101
102
  }
102
103
 
@@ -116,22 +117,22 @@ use super::*;
116
117
  }
117
118
 
118
119
  #[test]
119
- fn leader_launcher_args_dashdash_passthrough_includes_following_flags() {
120
- // golden: ["--attach","--","-x","--confirm"] ->
121
- // provider_args=["--","-x","--confirm"], attach_existing=True, confirm_attach=False
122
- // (everything from `--` onward, INCLUDING the `--` token itself, is provider passthrough)
120
+ fn leader_launcher_args_dashdash_passthrough_strips_separator() {
121
+ // ["--attach","--","-x","--provider-confirm"] ->
122
+ // provider_args=["-x","--provider-confirm"], attach_existing=True, confirm_attach=False
123
+ // Known Team Agent launcher flags after `--` are rejected by a separate guard.
123
124
  let got = leader_launcher_args(&[
124
125
  "--attach".into(),
125
126
  "--".into(),
126
127
  "-x".into(),
127
- "--confirm".into(),
128
+ "--provider-confirm".into(),
128
129
  ])
129
130
  .unwrap();
130
131
  assert!(got.attach_existing);
131
- assert!(!got.confirm_attach, "--confirm AFTER -- must NOT set confirm_attach");
132
+ assert!(!got.confirm_attach);
132
133
  assert_eq!(
133
134
  got.provider_args,
134
- vec!["--".to_string(), "-x".to_string(), "--confirm".to_string()]
135
+ vec!["-x".to_string(), "--provider-confirm".to_string()]
135
136
  );
136
137
  }
137
138
 
@@ -415,6 +415,48 @@ fn run_send_unknown_task_renders_error_not_silent_swallow() {
415
415
  let _ = std::fs::remove_dir_all(&ws);
416
416
  }
417
417
 
418
+ #[test]
419
+ fn run_leader_passthrough_flag_after_dashdash_renders_error_not_silent_swallow() {
420
+ let ws = std::env::temp_dir().join(format!(
421
+ "ta-run-leaderflag-{}-{}",
422
+ std::process::id(),
423
+ std::time::SystemTime::now()
424
+ .duration_since(std::time::UNIX_EPOCH)
425
+ .unwrap()
426
+ .as_nanos()
427
+ ));
428
+ std::fs::create_dir_all(&ws).unwrap();
429
+ let argv: Vec<String> = ["codex", "--", "--external-leader"]
430
+ .iter()
431
+ .map(ToString::to_string)
432
+ .collect();
433
+
434
+ let code = run(&argv, &ws);
435
+
436
+ assert_eq!(
437
+ code,
438
+ ExitCode::Error,
439
+ "misplaced leader flag must fail before provider exec"
440
+ );
441
+ let logs_dir = ws.join(".team").join("logs");
442
+ let mut found = String::new();
443
+ if let Ok(entries) = std::fs::read_dir(&logs_dir) {
444
+ for entry in entries.flatten() {
445
+ let name = entry.file_name().to_string_lossy().to_string();
446
+ if name.starts_with("cli-error-") {
447
+ found = std::fs::read_to_string(entry.path()).unwrap_or_default();
448
+ break;
449
+ }
450
+ }
451
+ }
452
+ assert!(
453
+ found.contains("Team Agent launcher flag --external-leader must appear before --"),
454
+ "leader passthrough run() must render the misplaced flag error; a silent swallow leaves no \
455
+ cli-error log. got log body: {found:?}"
456
+ );
457
+ let _ = std::fs::remove_dir_all(&ws);
458
+ }
459
+
418
460
  // R8 D6 (c-lite offline byte-lock): the CLI requeued_exhausted_watchers return projection, extracted
419
461
  // into a pure helper, must project the golden event's watcher_ids STRING list (leader/__init__.py:56) —
420
462
  // NOT the Rust `requeued` Vec<WatcherNotice> objects.
@@ -101,6 +101,12 @@ impl CliError {
101
101
  .to_string(),
102
102
  ]);
103
103
  }
104
+ } else if error.contains("Team Agent launcher flag")
105
+ && error.contains("must appear before --")
106
+ {
107
+ payload.action = String::from(
108
+ "move the Team Agent launcher flag before `--`; only provider flags belong after `--`",
109
+ );
104
110
  }
105
111
  payload
106
112
  }
@@ -144,6 +144,7 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
144
144
  let mut targets = Vec::new();
145
145
  for endpoint in state_recorded_tmux_endpoints(state) {
146
146
  let backend = tmux_backend_for_endpoint(&endpoint);
147
+ let resolved_endpoint = backend.tmux_endpoint();
147
148
  targets.extend(
148
149
  backend
149
150
  .list_targets()
@@ -151,7 +152,7 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
151
152
  .into_iter()
152
153
  .map(|info| AttachLeaderTarget {
153
154
  info,
154
- endpoint: Some(endpoint.clone()),
155
+ endpoint: resolved_endpoint.clone(),
155
156
  }),
156
157
  );
157
158
  }
@@ -168,10 +169,8 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
168
169
  fn tmux_backend_for_endpoint(endpoint: &str) -> crate::tmux_backend::TmuxBackend {
169
170
  if endpoint.is_empty() || endpoint == "default" {
170
171
  crate::tmux_backend::TmuxBackend::new()
171
- } else if Path::new(endpoint).is_absolute() {
172
- crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
173
172
  } else {
174
- crate::tmux_backend::TmuxBackend::for_socket_name(endpoint)
173
+ crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
175
174
  }
176
175
  }
177
176
 
@@ -196,6 +196,9 @@ pub fn execute_leader_plan(
196
196
  let detached = plan.mode == LeaderStartMode::NewTmuxSession
197
197
  && !std::io::stdin().is_terminal()
198
198
  && insert_detach_flag(&mut argv);
199
+ if plan.is_external_leader {
200
+ persist_external_leader_topology_marker(plan, workspace)?;
201
+ }
199
202
  let status = run_leader_argv(&argv, &plan.leader_env, plan, workspace)?;
200
203
  let code = status.code();
201
204
  if !status.success() {
@@ -205,9 +208,6 @@ pub fn execute_leader_plan(
205
208
  .unwrap_or_else(|| "signal".to_string())
206
209
  )));
207
210
  }
208
- if plan.is_external_leader {
209
- persist_external_leader_topology_marker(plan, workspace)?;
210
- }
211
211
  if detached {
212
212
  Ok(LeaderLaunchOutcome {
213
213
  status: LeaderLaunchStatus::Detached,
@@ -258,7 +258,7 @@ fn start_argv(
258
258
  match mode {
259
259
  LeaderStartMode::ExecProvider => {
260
260
  let mut argv = vec![provider_cmd];
261
- argv.extend(provider_args.iter().cloned());
261
+ argv.extend(normalized_provider_args(provider_args));
262
262
  Ok(argv)
263
263
  }
264
264
  LeaderStartMode::ManagedTmuxClient => {
@@ -289,7 +289,7 @@ fn start_argv(
289
289
  exports.push(shlex_quote(&format!("PATH={path}")));
290
290
  }
291
291
  let mut provider_argv = vec![provider_cmd];
292
- provider_argv.extend(provider_args.iter().cloned());
292
+ provider_argv.extend(normalized_provider_args(provider_args));
293
293
  let shell = format!(
294
294
  "cd {} && export {} && exec {}",
295
295
  shlex_quote(&resolved_workspace.to_string_lossy()),
@@ -316,10 +316,17 @@ fn start_argv(
316
316
 
317
317
  fn provider_command_argv(provider: Provider, provider_args: &[String]) -> Vec<String> {
318
318
  let mut argv = vec![provider_command_name(provider).to_string()];
319
- argv.extend(provider_args.iter().cloned());
319
+ argv.extend(normalized_provider_args(provider_args));
320
320
  argv
321
321
  }
322
322
 
323
+ fn normalized_provider_args(provider_args: &[String]) -> impl Iterator<Item = String> + '_ {
324
+ provider_args
325
+ .iter()
326
+ .skip(usize::from(provider_args.first().is_some_and(|arg| arg == "--")))
327
+ .cloned()
328
+ }
329
+
323
330
  fn managed_team_session_name(identity: &LeaderIdentity) -> SessionName {
324
331
  let team = identity.team_id.as_str();
325
332
  if team.starts_with("team-") {
@@ -777,7 +784,12 @@ mod tests {
777
784
  use std::path::Path;
778
785
  use std::sync::Mutex;
779
786
 
787
+ use crate::leader::{
788
+ LeaderIdentity, LeaderLaunchSocket, LeaderSessionUuidSource, LeaderStartMode,
789
+ LeaderStartPlan,
790
+ };
780
791
  use crate::model::enums::PaneLiveness;
792
+ use crate::model::ids::{LeaderSessionUuid, TeamKey};
781
793
  use crate::provider::{Provider, COPILOT_READY_MARKER, COPILOT_TRUST_PROMPT_MARKER};
782
794
  use crate::transport::{
783
795
  AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
@@ -786,7 +798,7 @@ mod tests {
786
798
  TurnVerification, WindowName,
787
799
  };
788
800
 
789
- use super::handle_exec_provider_startup_prompts;
801
+ use super::{execute_leader_plan, handle_exec_provider_startup_prompts, shlex_quote};
790
802
 
791
803
  struct ScriptedTransport {
792
804
  screens: Mutex<Vec<String>>,
@@ -937,6 +949,57 @@ mod tests {
937
949
  }
938
950
  }
939
951
 
952
+ #[test]
953
+ fn external_exec_provider_persists_topology_before_provider_exec() {
954
+ let workspace = std::env::temp_dir().join(format!(
955
+ "ta-external-pre-exec-{}",
956
+ std::process::id()
957
+ ));
958
+ std::fs::create_dir_all(&workspace).unwrap();
959
+ let state_path = crate::state::persist::runtime_state_path(&workspace);
960
+ let command = format!(
961
+ "test -f {path} && grep -q is_external_leader {path}",
962
+ path = shlex_quote(&state_path.to_string_lossy())
963
+ );
964
+ let identity = LeaderIdentity {
965
+ leader_session_uuid: LeaderSessionUuid::derive(
966
+ "fp",
967
+ &workspace.to_string_lossy(),
968
+ "tester",
969
+ "current",
970
+ )
971
+ .unwrap(),
972
+ leader_session_uuid_source: LeaderSessionUuidSource::Derived,
973
+ machine_fingerprint: "fp".to_string(),
974
+ workspace_abspath: workspace.clone(),
975
+ os_user: "tester".to_string(),
976
+ team_id: TeamKey::new("current"),
977
+ };
978
+ let plan = LeaderStartPlan {
979
+ mode: LeaderStartMode::ExecProvider,
980
+ provider: Provider::Codex,
981
+ workspace: workspace.clone(),
982
+ socket: LeaderLaunchSocket::Workspace,
983
+ session_name: None,
984
+ argv: vec!["sh".to_string(), "-c".to_string(), command],
985
+ provider_argv: vec!["codex".to_string()],
986
+ leader_window: None,
987
+ is_external_leader: true,
988
+ leader_env: BTreeMap::new(),
989
+ identity: Some(identity),
990
+ detached: false,
991
+ };
992
+
993
+ let outcome = execute_leader_plan(&plan, &workspace)
994
+ .expect("external marker must be present before provider argv runs");
995
+
996
+ assert_eq!(outcome.status, crate::leader::LeaderLaunchStatus::Exited);
997
+ let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
998
+ assert_eq!(state["is_external_leader"], serde_json::json!(true));
999
+ assert_eq!(state["teams"]["current"]["is_external_leader"], serde_json::json!(true));
1000
+ let _ = std::fs::remove_dir_all(&workspace);
1001
+ }
1002
+
940
1003
  #[test]
941
1004
  fn exec_provider_leader_startup_prompt_handler_reuses_copilot_adapter() {
942
1005
  let transport = ScriptedTransport::new(vec![
@@ -108,12 +108,20 @@ use super::*;
108
108
  let ws = std::env::temp_dir().join(format!("ta_rs_lsp_external_{}", std::process::id()));
109
109
  std::fs::create_dir_all(&ws).unwrap();
110
110
 
111
- let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, true).unwrap();
111
+ let provider_args = vec!["--".to_string(), "--model".to_string(), "opus".to_string()];
112
+ let plan = leader_start_plan(Provider::Fake, &provider_args, &ws, false, false, None, true).unwrap();
112
113
 
113
114
  assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
114
115
  assert!(plan.is_external_leader);
115
116
  assert_eq!(plan.leader_window, None);
116
- assert_eq!(plan.argv, vec!["fake".to_string()]);
117
+ assert_eq!(
118
+ plan.argv,
119
+ vec!["fake".to_string(), "--model".to_string(), "opus".to_string()]
120
+ );
121
+ assert_eq!(
122
+ plan.provider_argv,
123
+ vec!["fake".to_string(), "--model".to_string(), "opus".to_string()]
124
+ );
117
125
  }
118
126
 
119
127
  #[test]
@@ -8,6 +8,7 @@
8
8
  #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
9
9
 
10
10
  use std::collections::{BTreeMap, VecDeque};
11
+ use std::os::unix::net::UnixListener;
11
12
  use std::path::Path;
12
13
  use std::sync::{Arc, Mutex};
13
14
 
@@ -251,8 +252,20 @@
251
252
  }
252
253
 
253
254
  #[test]
255
+ #[serial_test::serial(env)]
254
256
  fn leader_receiver_short_endpoint_must_not_reconstruct_tmux_l_socket() {
255
257
  let endpoint = "dl9aa40c88";
258
+ let uid = unsafe { libc::geteuid() };
259
+ let tmp = std::env::temp_dir().join(format!(
260
+ "ta-tmux-short-endpoint-{}",
261
+ std::process::id()
262
+ ));
263
+ let root = tmp.join(format!("tmux-{uid}"));
264
+ std::fs::create_dir_all(&root).unwrap();
265
+ let socket_path = root.join(endpoint);
266
+ let _listener = UnixListener::bind(&socket_path).unwrap();
267
+ let socket_path = socket_path.canonicalize().unwrap();
268
+ let _env = EnvGuard::apply(&[("TMPDIR", Some(tmp.to_str().unwrap()))]);
256
269
  let (be, rec) = {
257
270
  let recorded = Arc::new(Mutex::new(Vec::new()));
258
271
  let runner = MockCommandRunner {
@@ -275,6 +288,14 @@
275
288
  "non-canonical leader endpoints must be rejected or left unbound, never reconstructed as \
276
289
  tmux -L <short> under the coordinator socket root; calls={calls:?}"
277
290
  );
291
+ assert!(
292
+ calls.iter().all(|call| call.starts_with(&[
293
+ "tmux".to_string(),
294
+ "-S".to_string(),
295
+ socket_path.to_string_lossy().to_string()
296
+ ])),
297
+ "short leader endpoints must resolve to the existing physical socket path with -S; calls={calls:?}"
298
+ );
278
299
  }
279
300
 
280
301
  // ── 1. has_session: exit 0 -> true, exit 1 -> false; argv = `tmux has-session -t <s>` ──────────
@@ -233,6 +233,12 @@ impl TmuxBackend {
233
233
  socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())),
234
234
  event_workspace: None,
235
235
  }
236
+ } else if let Some(path) = socket_path_for_name(endpoint) {
237
+ Self {
238
+ runner: Box::new(RealCommandRunner),
239
+ socket: Some(TmuxSocketEndpoint::Path(path.to_string_lossy().into_owned())),
240
+ event_workspace: None,
241
+ }
236
242
  } else {
237
243
  Self::new()
238
244
  }
@@ -275,6 +281,12 @@ impl TmuxBackend {
275
281
  socket: None,
276
282
  event_workspace: None,
277
283
  }
284
+ } else if let Some(path) = socket_path_for_name(endpoint) {
285
+ Self {
286
+ runner,
287
+ socket: Some(TmuxSocketEndpoint::Path(path.to_string_lossy().into_owned())),
288
+ event_workspace: None,
289
+ }
278
290
  } else {
279
291
  Self {
280
292
  runner,
@@ -340,13 +352,7 @@ pub(crate) fn socket_name_for_workspace(workspace: &Path) -> String {
340
352
  }
341
353
 
342
354
  pub(crate) fn socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
343
- if let Some(existing) = existing_socket_path_for_workspace(workspace) {
344
- return Some(existing);
345
- }
346
- let uid = unsafe { libc::geteuid() };
347
- let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
348
- let default_root = default_root.canonicalize().unwrap_or(default_root);
349
- Some(default_root.join(socket_name_for_workspace(workspace)))
355
+ socket_path_for_name(&socket_name_for_workspace(workspace))
350
356
  }
351
357
 
352
358
  pub(crate) fn socket_probe_missing_for_workspace(workspace: &Path) -> bool {
@@ -354,11 +360,27 @@ pub(crate) fn socket_probe_missing_for_workspace(workspace: &Path) -> bool {
354
360
  }
355
361
 
356
362
  fn existing_socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
357
- let socket_name = socket_name_for_workspace(workspace);
363
+ existing_socket_path_for_name(&socket_name_for_workspace(workspace))
364
+ }
365
+
366
+ pub(crate) fn socket_path_for_name(socket_name: &str) -> Option<PathBuf> {
367
+ if socket_name.is_empty() || socket_name == "default" || Path::new(socket_name).is_absolute() {
368
+ return None;
369
+ }
370
+ if let Some(existing) = existing_socket_path_for_name(socket_name) {
371
+ return Some(existing);
372
+ }
373
+ let uid = unsafe { libc::geteuid() };
374
+ let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
375
+ let default_root = default_root.canonicalize().unwrap_or(default_root);
376
+ Some(default_root.join(socket_name))
377
+ }
378
+
379
+ fn existing_socket_path_for_name(socket_name: &str) -> Option<PathBuf> {
358
380
  let roots = tmux_socket_roots();
359
381
  for root in &roots {
360
382
  let root = root.canonicalize().unwrap_or_else(|_| root.clone());
361
- let candidate = root.join(&socket_name);
383
+ let candidate = root.join(socket_name);
362
384
  if candidate.exists() {
363
385
  return Some(candidate.canonicalize().unwrap_or(candidate));
364
386
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
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.18",
24
- "@team-agent/cli-darwin-x64": "0.3.18",
25
- "@team-agent/cli-linux-x64": "0.3.18"
23
+ "@team-agent/cli-darwin-arm64": "0.3.19",
24
+ "@team-agent/cli-darwin-x64": "0.3.19",
25
+ "@team-agent/cli-linux-x64": "0.3.19"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",