@team-agent/installer 0.5.74 → 0.5.75

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
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.74"
584
+ version = "0.5.75"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "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.74"
12
+ version = "0.5.75"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -29,8 +29,24 @@ use std::collections::BTreeMap;
29
29
  use std::path::{Path, PathBuf};
30
30
 
31
31
  use crate::model::yaml::Value as YamlValue;
32
+ use crate::provider::wire::{parse_provider, provider_wire};
32
33
  use crate::provider::Provider;
33
34
 
35
+ /// Current worker caller context is deliberately separate from `TEAM_AGENT_LEADER_*`.
36
+ /// The launcher writes only the typed provider; the tmux invocation boundary adds the
37
+ /// actual pane and socket for the provider command itself.
38
+ pub(crate) const CALLER_CONTEXT_PREFIX: &str = "TEAM_AGENT_CALLER_";
39
+ pub(crate) const CALLER_PROVIDER_ENV: &str = "TEAM_AGENT_CALLER_PROVIDER";
40
+ pub(crate) const CALLER_PANE_ENV: &str = "TEAM_AGENT_CALLER_PANE_ID";
41
+ pub(crate) const CALLER_ENDPOINT_ENV: &str = "TEAM_AGENT_CALLER_TMUX_ENDPOINT";
42
+
43
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
44
+ pub(crate) enum CallerProviderResolution {
45
+ Absent,
46
+ Valid(Provider),
47
+ Invalid,
48
+ }
49
+
34
50
  /// Env-key PREFIXES that are stripped from the inherited parent env
35
51
  /// before worker spawn. These carry leader-process identity and must
36
52
  /// not leak into worker provider TUIs (E60 root cause).
@@ -38,6 +54,7 @@ const STRIP_PREFIXES: &[&str] = &[
38
54
  "TEAM_AGENT_LEADER_",
39
55
  "TEAM_AGENT_LEADER_BYPASS",
40
56
  "TEAM_AGENT_MACHINE_FINGERPRINT",
57
+ CALLER_CONTEXT_PREFIX,
41
58
  ];
42
59
 
43
60
  /// Exact env-keys stripped from the inherited parent env.
@@ -114,6 +131,101 @@ where
114
131
  env
115
132
  }
116
133
 
134
+ /// Overlay the final typed provider after profile/env overlays. Any inherited or
135
+ /// profile-supplied caller namespace is discarded before this fresh value is written.
136
+ pub(crate) fn inject_current_caller_provider(
137
+ env: &mut BTreeMap<String, String>,
138
+ provider: Provider,
139
+ ) {
140
+ env.retain(|key, _| !key.starts_with(CALLER_CONTEXT_PREFIX));
141
+ env.insert(
142
+ CALLER_PROVIDER_ENV.to_string(),
143
+ provider_wire(provider).to_string(),
144
+ );
145
+ }
146
+
147
+ /// Resolve the narrow caller context used by both initial-state seeding and fresh bind.
148
+ /// `current_*` come from the invoking process; `scoped_endpoint` comes from the selected
149
+ /// transport. A partial, malformed, stale, or conflicting context is never downgraded to
150
+ /// command attribution.
151
+ pub(crate) fn caller_provider_resolution(
152
+ current_pane: Option<&str>,
153
+ current_endpoint: Option<&str>,
154
+ scoped_endpoint: Option<&str>,
155
+ ) -> CallerProviderResolution {
156
+ let provider = std::env::var(CALLER_PROVIDER_ENV).ok();
157
+ let context_pane = std::env::var(CALLER_PANE_ENV).ok();
158
+ let context_endpoint = std::env::var(CALLER_ENDPOINT_ENV).ok();
159
+ let explicit_provider = std::env::var("TEAM_AGENT_LEADER_PROVIDER").ok();
160
+ let explicit_pane = std::env::var("TEAM_AGENT_LEADER_PANE_ID").ok();
161
+ caller_provider_resolution_from_values(
162
+ provider.as_deref(),
163
+ context_pane.as_deref(),
164
+ context_endpoint.as_deref(),
165
+ explicit_provider.as_deref(),
166
+ explicit_pane.as_deref(),
167
+ current_pane,
168
+ current_endpoint,
169
+ scoped_endpoint,
170
+ )
171
+ }
172
+
173
+ pub(crate) fn caller_provider_resolution_from_values(
174
+ provider_raw: Option<&str>,
175
+ context_pane: Option<&str>,
176
+ context_endpoint: Option<&str>,
177
+ explicit_provider: Option<&str>,
178
+ explicit_pane: Option<&str>,
179
+ current_pane: Option<&str>,
180
+ current_endpoint: Option<&str>,
181
+ scoped_endpoint: Option<&str>,
182
+ ) -> CallerProviderResolution {
183
+ if provider_raw.is_none() && context_pane.is_none() && context_endpoint.is_none() {
184
+ return CallerProviderResolution::Absent;
185
+ }
186
+ let Some(provider_raw) = provider_raw.filter(|value| !value.is_empty()) else {
187
+ return CallerProviderResolution::Invalid;
188
+ };
189
+ let Some(provider) = parse_provider(provider_raw) else {
190
+ return CallerProviderResolution::Invalid;
191
+ };
192
+ let Some(context_pane) = context_pane.filter(|value| !value.is_empty()) else {
193
+ return CallerProviderResolution::Invalid;
194
+ };
195
+ let Some(context_endpoint) = context_endpoint.filter(|value| !value.is_empty()) else {
196
+ return CallerProviderResolution::Invalid;
197
+ };
198
+ if !valid_tmux_pane(context_pane)
199
+ || !valid_tmux_endpoint(context_endpoint)
200
+ || current_pane != Some(context_pane)
201
+ || current_endpoint != Some(context_endpoint)
202
+ || scoped_endpoint != Some(context_endpoint)
203
+ {
204
+ return CallerProviderResolution::Invalid;
205
+ }
206
+ if let Some(explicit) = explicit_provider.filter(|value| !value.is_empty()) {
207
+ if parse_provider(explicit) != Some(provider) {
208
+ return CallerProviderResolution::Invalid;
209
+ }
210
+ }
211
+ if let Some(explicit_pane) = explicit_pane.filter(|value| !value.is_empty()) {
212
+ if explicit_pane != context_pane {
213
+ return CallerProviderResolution::Invalid;
214
+ }
215
+ }
216
+ CallerProviderResolution::Valid(provider)
217
+ }
218
+
219
+ fn valid_tmux_pane(value: &str) -> bool {
220
+ value.strip_prefix('%').is_some_and(|digits| {
221
+ !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
222
+ })
223
+ }
224
+
225
+ fn valid_tmux_endpoint(value: &str) -> bool {
226
+ value.starts_with('/') && !value.contains(',')
227
+ }
228
+
117
229
  pub(crate) fn isolate_worker_spawn_env(
118
230
  _target_provider: Provider,
119
231
  env: &mut BTreeMap<String, String>,
@@ -122,6 +234,12 @@ pub(crate) fn isolate_worker_spawn_env(
122
234
  let mut env_unset = base_env_unset
123
235
  .into_iter()
124
236
  .collect::<std::collections::BTreeSet<_>>();
237
+ // Clear any stale values held by the tmux server before the scoped provider
238
+ // invocation assigns the fresh context. The inert shell tail must not retain it.
239
+ for key in [CALLER_PROVIDER_ENV, CALLER_PANE_ENV, CALLER_ENDPOINT_ENV] {
240
+ env.remove(key);
241
+ env_unset.insert(key.to_string());
242
+ }
125
243
  for key in WORKER_IDENTITY_EXACT {
126
244
  env.remove(*key);
127
245
  env_unset.insert((*key).to_string());
@@ -313,6 +431,254 @@ mod tests {
313
431
  assert!(!env.contains_key("CARGO_BIN_EXE_team-agent"));
314
432
  }
315
433
 
434
+ #[test]
435
+ fn worker_spawn_env_strips_inherited_caller_context() {
436
+ let parent = make_parent_env(&[
437
+ (CALLER_PROVIDER_ENV, "claude"),
438
+ (CALLER_PANE_ENV, "%1"),
439
+ (CALLER_ENDPOINT_ENV, "/tmp/old-tmux.sock"),
440
+ ]);
441
+ let env = worker_spawn_env(parent, Path::new("/ws"), "developer", None, None);
442
+ assert!(!env.keys().any(|key| key.starts_with(CALLER_CONTEXT_PREFIX)));
443
+ }
444
+
445
+ #[test]
446
+ fn inject_current_caller_provider_overwrites_namespace_from_final_provider() {
447
+ let mut env = BTreeMap::from([
448
+ (CALLER_PROVIDER_ENV.to_string(), "claude".to_string()),
449
+ (CALLER_PANE_ENV.to_string(), "%1".to_string()),
450
+ ("PATH".to_string(), "/usr/bin".to_string()),
451
+ ]);
452
+ inject_current_caller_provider(&mut env, Provider::Pi);
453
+ assert_eq!(env.get(CALLER_PROVIDER_ENV).map(String::as_str), Some("pi"));
454
+ assert!(!env.contains_key(CALLER_PANE_ENV));
455
+ assert_eq!(env.get("PATH").map(String::as_str), Some("/usr/bin"));
456
+ }
457
+
458
+ #[test]
459
+ fn isolate_worker_spawn_env_clears_context_from_outer_shell() {
460
+ let mut env = BTreeMap::from([(CALLER_PROVIDER_ENV.to_string(), "pi".to_string())]);
461
+ let unset = isolate_worker_spawn_env(Provider::Pi, &mut env, Vec::<String>::new());
462
+ assert!(!env.contains_key(CALLER_PROVIDER_ENV));
463
+ assert!(unset.iter().any(|key| key == CALLER_PROVIDER_ENV));
464
+ assert!(unset.iter().any(|key| key == CALLER_PANE_ENV));
465
+ assert!(unset.iter().any(|key| key == CALLER_ENDPOINT_ENV));
466
+ }
467
+
468
+ #[test]
469
+ fn caller_context_requires_exact_current_pane_and_endpoint() {
470
+ let resolution = caller_provider_resolution_from_values(
471
+ Some("pi"),
472
+ Some("%1"),
473
+ Some("/tmp/tmux.sock"),
474
+ None,
475
+ None,
476
+ Some("%1"),
477
+ Some("/tmp/tmux.sock"),
478
+ Some("/tmp/tmux.sock"),
479
+ );
480
+ assert_eq!(resolution, CallerProviderResolution::Valid(Provider::Pi));
481
+ assert_eq!(
482
+ caller_provider_resolution_from_values(
483
+ Some("pi"),
484
+ Some("%1"),
485
+ Some("/tmp/tmux.sock"),
486
+ None,
487
+ None,
488
+ Some("%2"),
489
+ Some("/tmp/tmux.sock"),
490
+ Some("/tmp/tmux.sock"),
491
+ ),
492
+ CallerProviderResolution::Invalid
493
+ );
494
+ assert_eq!(
495
+ caller_provider_resolution_from_values(
496
+ Some("pi"),
497
+ Some("%1"),
498
+ Some("/tmp/tmux.sock"),
499
+ Some("codex"),
500
+ None,
501
+ Some("%1"),
502
+ Some("/tmp/tmux.sock"),
503
+ Some("/tmp/tmux.sock"),
504
+ ),
505
+ CallerProviderResolution::Invalid
506
+ );
507
+ }
508
+
509
+ #[test]
510
+ fn caller_context_absent_empty_and_malformed_cases_are_distinct() {
511
+ assert_eq!(
512
+ caller_provider_resolution_from_values(
513
+ None,
514
+ None,
515
+ None,
516
+ None,
517
+ None,
518
+ Some("%1"),
519
+ Some("/tmp/tmux.sock"),
520
+ Some("/tmp/tmux.sock"),
521
+ ),
522
+ CallerProviderResolution::Absent
523
+ );
524
+ assert_eq!(
525
+ caller_provider_resolution_from_values(
526
+ Some(""),
527
+ None,
528
+ None,
529
+ None,
530
+ None,
531
+ Some("%1"),
532
+ Some("/tmp/tmux.sock"),
533
+ Some("/tmp/tmux.sock"),
534
+ ),
535
+ CallerProviderResolution::Invalid
536
+ );
537
+ assert_eq!(
538
+ caller_provider_resolution_from_values(
539
+ Some("pi"),
540
+ Some(""),
541
+ Some("/tmp/tmux.sock"),
542
+ None,
543
+ None,
544
+ Some("%1"),
545
+ Some("/tmp/tmux.sock"),
546
+ Some("/tmp/tmux.sock"),
547
+ ),
548
+ CallerProviderResolution::Invalid
549
+ );
550
+ assert_eq!(
551
+ caller_provider_resolution_from_values(
552
+ Some("pi"),
553
+ Some("%1"),
554
+ Some(""),
555
+ None,
556
+ None,
557
+ Some("%1"),
558
+ Some("/tmp/tmux.sock"),
559
+ Some("/tmp/tmux.sock"),
560
+ ),
561
+ CallerProviderResolution::Invalid
562
+ );
563
+ assert_eq!(
564
+ caller_provider_resolution_from_values(
565
+ Some("not-a-provider"),
566
+ Some("%1"),
567
+ Some("/tmp/tmux.sock"),
568
+ None,
569
+ None,
570
+ Some("%1"),
571
+ Some("/tmp/tmux.sock"),
572
+ Some("/tmp/tmux.sock"),
573
+ ),
574
+ CallerProviderResolution::Invalid
575
+ );
576
+ assert_eq!(
577
+ caller_provider_resolution_from_values(
578
+ Some("pi"),
579
+ Some("1"),
580
+ Some("/tmp/tmux.sock"),
581
+ None,
582
+ None,
583
+ Some("1"),
584
+ Some("/tmp/tmux.sock"),
585
+ Some("/tmp/tmux.sock"),
586
+ ),
587
+ CallerProviderResolution::Invalid
588
+ );
589
+ assert_eq!(
590
+ caller_provider_resolution_from_values(
591
+ Some("pi"),
592
+ Some("%1"),
593
+ Some("tmux.sock"),
594
+ None,
595
+ None,
596
+ Some("%1"),
597
+ Some("tmux.sock"),
598
+ Some("tmux.sock"),
599
+ ),
600
+ CallerProviderResolution::Invalid
601
+ );
602
+ assert_eq!(
603
+ caller_provider_resolution_from_values(
604
+ Some("pi"),
605
+ Some("%1"),
606
+ Some("/tmp/tmux.sock,0"),
607
+ None,
608
+ None,
609
+ Some("%1"),
610
+ Some("/tmp/tmux.sock,0"),
611
+ Some("/tmp/tmux.sock,0"),
612
+ ),
613
+ CallerProviderResolution::Invalid
614
+ );
615
+ assert_eq!(
616
+ caller_provider_resolution_from_values(
617
+ Some("pi"),
618
+ Some("%1"),
619
+ Some("/tmp/tmux.sock"),
620
+ None,
621
+ None,
622
+ Some("%1"),
623
+ Some("/tmp/other.sock"),
624
+ Some("/tmp/tmux.sock"),
625
+ ),
626
+ CallerProviderResolution::Invalid
627
+ );
628
+ assert_eq!(
629
+ caller_provider_resolution_from_values(
630
+ Some("pi"),
631
+ Some("%1"),
632
+ Some("/tmp/tmux.sock"),
633
+ None,
634
+ None,
635
+ Some("%1"),
636
+ Some("/tmp/tmux.sock"),
637
+ Some("/tmp/product.sock"),
638
+ ),
639
+ CallerProviderResolution::Invalid
640
+ );
641
+ assert_eq!(
642
+ caller_provider_resolution_from_values(
643
+ Some("pi"),
644
+ Some("%1"),
645
+ Some("/tmp/tmux.sock"),
646
+ None,
647
+ None,
648
+ Some("%1"),
649
+ Some("/tmp/tmux.sock"),
650
+ None,
651
+ ),
652
+ CallerProviderResolution::Invalid
653
+ );
654
+ assert_eq!(
655
+ caller_provider_resolution_from_values(
656
+ Some("pi"),
657
+ Some("%1"),
658
+ Some("/tmp/tmux.sock"),
659
+ None,
660
+ Some("%2"),
661
+ Some("%1"),
662
+ Some("/tmp/tmux.sock"),
663
+ Some("/tmp/tmux.sock"),
664
+ ),
665
+ CallerProviderResolution::Invalid
666
+ );
667
+ assert_eq!(
668
+ caller_provider_resolution_from_values(
669
+ Some("pi"),
670
+ Some("%1"),
671
+ Some("/tmp/tmux.sock"),
672
+ Some("pi"),
673
+ Some("%1"),
674
+ Some("%1"),
675
+ Some("/tmp/tmux.sock"),
676
+ Some("/tmp/tmux.sock"),
677
+ ),
678
+ CallerProviderResolution::Valid(Provider::Pi)
679
+ );
680
+ }
681
+
316
682
  #[test]
317
683
  fn worker_spawn_cwd_returns_workspace_when_no_spec_override() {
318
684
  // YAML agent with no spawn_cwd field.