@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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +366 -0
- package/crates/team-agent/src/leader/lease.rs +228 -32
- package/crates/team-agent/src/leader/registry.rs +177 -19
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +12 -1
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1820 -60
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
- package/crates/team-agent/src/lifecycle/restart/common.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
- package/crates/team-agent/src/messaging/delivery.rs +200 -29
- package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
- package/crates/team-agent/src/state/persist.rs +166 -6
- package/crates/team-agent/src/state/repository.rs +36 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +350 -7
- package/crates/team-agent/src/tmux_backend.rs +217 -26
- package/crates/team-agent/src/transport/test_support.rs +18 -3
- package/crates/team-agent/src/transport.rs +99 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -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.
|