@team-agent/installer 0.3.13 → 0.3.14
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/cli/mod.rs +168 -75
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/launch.rs +132 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +50 -40
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +125 -32
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +181 -45
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +267 -65
- package/crates/team-agent/src/tmux_backend.rs +5 -0
- package/crates/team-agent/src/transport.rs +4 -0
- package/package.json +4 -4
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
use std::collections::BTreeMap;
|
|
4
4
|
use std::io::IsTerminal;
|
|
5
|
-
use std::path::Path;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
6
|
use std::process::{Command, Stdio};
|
|
7
7
|
|
|
8
8
|
use crate::provider::{get_adapter, Provider};
|
|
9
9
|
use crate::tmux_backend::TmuxBackend;
|
|
10
|
-
use crate::transport::{SessionName, Transport};
|
|
10
|
+
use crate::transport::{PaneId, SessionName, Target, Transport};
|
|
11
11
|
|
|
12
12
|
use super::helpers::{
|
|
13
13
|
provider_wire, resolve_workspace_for_hash, sanitize_session_folder, sha1_hex_prefix,
|
|
@@ -125,7 +125,10 @@ pub(crate) fn leader_env_for_identity(
|
|
|
125
125
|
identity.team_id.as_str().to_string(),
|
|
126
126
|
);
|
|
127
127
|
if provider == Provider::Copilot {
|
|
128
|
-
leader_env.insert(
|
|
128
|
+
leader_env.insert(
|
|
129
|
+
"COPILOT_DISABLE_TERMINAL_TITLE".to_string(),
|
|
130
|
+
"1".to_string(),
|
|
131
|
+
);
|
|
129
132
|
}
|
|
130
133
|
leader_env
|
|
131
134
|
}
|
|
@@ -171,7 +174,7 @@ pub fn execute_leader_plan(
|
|
|
171
174
|
let detached = plan.mode == LeaderStartMode::NewTmuxSession
|
|
172
175
|
&& !std::io::stdin().is_terminal()
|
|
173
176
|
&& insert_detach_flag(&mut argv);
|
|
174
|
-
let status = run_leader_argv(&argv, &plan.leader_env)?;
|
|
177
|
+
let status = run_leader_argv(&argv, &plan.leader_env, plan, workspace)?;
|
|
175
178
|
let code = status.code();
|
|
176
179
|
if !status.success() {
|
|
177
180
|
return Err(LeaderError::Start(format!(
|
|
@@ -294,6 +297,8 @@ fn insert_detach_flag(argv: &mut Vec<String>) -> bool {
|
|
|
294
297
|
fn run_leader_argv(
|
|
295
298
|
argv: &[String],
|
|
296
299
|
env: &BTreeMap<String, String>,
|
|
300
|
+
plan: &LeaderStartPlan,
|
|
301
|
+
workspace: &Path,
|
|
297
302
|
) -> Result<std::process::ExitStatus, LeaderError> {
|
|
298
303
|
let Some(program) = argv.first() else {
|
|
299
304
|
return Err(LeaderError::Start(
|
|
@@ -307,9 +312,84 @@ fn run_leader_argv(
|
|
|
307
312
|
.stdout(Stdio::inherit())
|
|
308
313
|
.stderr(Stdio::inherit())
|
|
309
314
|
.spawn()?;
|
|
315
|
+
if plan.mode == LeaderStartMode::ExecProvider {
|
|
316
|
+
spawn_exec_provider_startup_prompt_handler(plan.provider, workspace.to_path_buf());
|
|
317
|
+
}
|
|
310
318
|
child.wait().map_err(LeaderError::Io)
|
|
311
319
|
}
|
|
312
320
|
|
|
321
|
+
fn spawn_exec_provider_startup_prompt_handler(provider: Provider, workspace: PathBuf) {
|
|
322
|
+
let Some(pane_id) = std::env::var("TMUX_PANE")
|
|
323
|
+
.ok()
|
|
324
|
+
.filter(|value| !value.trim().is_empty())
|
|
325
|
+
else {
|
|
326
|
+
write_leader_startup_prompt_event(
|
|
327
|
+
&workspace,
|
|
328
|
+
"leader.startup_prompt_skipped",
|
|
329
|
+
serde_json::json!({
|
|
330
|
+
"provider": provider_wire(provider),
|
|
331
|
+
"reason": "tmux_pane_missing",
|
|
332
|
+
"action": "skip",
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
};
|
|
337
|
+
std::thread::spawn(move || {
|
|
338
|
+
let transport = tmux_transport_for_current_pane();
|
|
339
|
+
let _ = handle_exec_provider_startup_prompts(
|
|
340
|
+
provider, &workspace, &pane_id, &transport, 30, 0.5,
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
fn tmux_transport_for_current_pane() -> TmuxBackend {
|
|
346
|
+
crate::tmux_backend::socket_name_from_tmux_env()
|
|
347
|
+
.map(|endpoint| TmuxBackend::for_tmux_endpoint(&endpoint))
|
|
348
|
+
.unwrap_or_else(TmuxBackend::new)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
pub fn handle_exec_provider_startup_prompts(
|
|
352
|
+
provider: Provider,
|
|
353
|
+
workspace: &Path,
|
|
354
|
+
pane_id: &str,
|
|
355
|
+
transport: &dyn Transport,
|
|
356
|
+
checks: usize,
|
|
357
|
+
sleep_s: f64,
|
|
358
|
+
) -> crate::provider::StartupPromptOutcome {
|
|
359
|
+
let target = Target::Pane(PaneId::new(pane_id.to_string()));
|
|
360
|
+
let outcome =
|
|
361
|
+
get_adapter(provider).handle_startup_prompts_outcome(transport, &target, checks, sleep_s);
|
|
362
|
+
for handled in &outcome.handled {
|
|
363
|
+
write_leader_startup_prompt_event(
|
|
364
|
+
workspace,
|
|
365
|
+
"leader.startup_prompt_handled",
|
|
366
|
+
serde_json::json!({
|
|
367
|
+
"provider": provider_wire(provider),
|
|
368
|
+
"pane_id": pane_id,
|
|
369
|
+
"prompt": handled.prompt,
|
|
370
|
+
"action": handled.action,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if let Some(error) = &outcome.capture_error {
|
|
375
|
+
write_leader_startup_prompt_event(
|
|
376
|
+
workspace,
|
|
377
|
+
"leader.startup_prompt_capture_failed",
|
|
378
|
+
serde_json::json!({
|
|
379
|
+
"provider": provider_wire(provider),
|
|
380
|
+
"pane_id": pane_id,
|
|
381
|
+
"action": "capture",
|
|
382
|
+
"error": error,
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
outcome
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fn write_leader_startup_prompt_event(workspace: &Path, event: &str, fields: serde_json::Value) {
|
|
390
|
+
let _ = crate::event_log::EventLog::new(workspace).write(event, fields);
|
|
391
|
+
}
|
|
392
|
+
|
|
313
393
|
fn ensure_tmux_installed() -> Result<(), LeaderError> {
|
|
314
394
|
match Command::new("tmux").arg("-V").output() {
|
|
315
395
|
Ok(output) if output.status.success() => Ok(()),
|
|
@@ -388,3 +468,198 @@ fn shlex_quote(raw: &str) -> String {
|
|
|
388
468
|
format!("'{}'", raw.replace('\'', "'\"'\"'"))
|
|
389
469
|
}
|
|
390
470
|
}
|
|
471
|
+
|
|
472
|
+
#[cfg(test)]
|
|
473
|
+
mod tests {
|
|
474
|
+
use std::collections::BTreeMap;
|
|
475
|
+
use std::path::Path;
|
|
476
|
+
use std::sync::Mutex;
|
|
477
|
+
|
|
478
|
+
use crate::model::enums::PaneLiveness;
|
|
479
|
+
use crate::provider::{Provider, COPILOT_READY_MARKER, COPILOT_TRUST_PROMPT_MARKER};
|
|
480
|
+
use crate::transport::{
|
|
481
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
482
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName,
|
|
483
|
+
SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
|
|
484
|
+
TurnVerification, WindowName,
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
use super::handle_exec_provider_startup_prompts;
|
|
488
|
+
|
|
489
|
+
struct ScriptedTransport {
|
|
490
|
+
screens: Mutex<Vec<String>>,
|
|
491
|
+
sent: Mutex<Vec<(Target, Vec<Key>)>>,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
impl ScriptedTransport {
|
|
495
|
+
fn new(screens: Vec<String>) -> Self {
|
|
496
|
+
Self {
|
|
497
|
+
screens: Mutex::new(screens),
|
|
498
|
+
sent: Mutex::new(Vec::new()),
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fn sent(&self) -> Vec<(Target, Vec<Key>)> {
|
|
503
|
+
match self.sent.lock() {
|
|
504
|
+
Ok(guard) => guard.clone(),
|
|
505
|
+
Err(poisoned) => poisoned.into_inner().clone(),
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
impl Transport for ScriptedTransport {
|
|
511
|
+
fn kind(&self) -> BackendKind {
|
|
512
|
+
BackendKind::Tmux
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
fn spawn_first(
|
|
516
|
+
&self,
|
|
517
|
+
_session: &SessionName,
|
|
518
|
+
_window: &WindowName,
|
|
519
|
+
_argv: &[String],
|
|
520
|
+
_cwd: &Path,
|
|
521
|
+
_env: &BTreeMap<String, String>,
|
|
522
|
+
) -> Result<SpawnResult, TransportError> {
|
|
523
|
+
Err(TransportError::Io(std::io::Error::other(
|
|
524
|
+
"spawn_first not used by startup-prompt test",
|
|
525
|
+
)))
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fn spawn_into(
|
|
529
|
+
&self,
|
|
530
|
+
_session: &SessionName,
|
|
531
|
+
_window: &WindowName,
|
|
532
|
+
_argv: &[String],
|
|
533
|
+
_cwd: &Path,
|
|
534
|
+
_env: &BTreeMap<String, String>,
|
|
535
|
+
) -> Result<SpawnResult, TransportError> {
|
|
536
|
+
Err(TransportError::Io(std::io::Error::other(
|
|
537
|
+
"spawn_into not used by startup-prompt test",
|
|
538
|
+
)))
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn inject(
|
|
542
|
+
&self,
|
|
543
|
+
_target: &Target,
|
|
544
|
+
_payload: &InjectPayload,
|
|
545
|
+
_submit: Key,
|
|
546
|
+
_bracketed: bool,
|
|
547
|
+
) -> Result<InjectReport, TransportError> {
|
|
548
|
+
Ok(InjectReport {
|
|
549
|
+
stage_reached: InjectStage::Submit,
|
|
550
|
+
inject_verification: InjectVerification::CaptureContainsToken,
|
|
551
|
+
submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
552
|
+
turn_verification: TurnVerification::NotRequired,
|
|
553
|
+
attempts: 1,
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
fn send_keys(&self, target: &Target, keys: &[Key]) -> Result<(), TransportError> {
|
|
558
|
+
match self.sent.lock() {
|
|
559
|
+
Ok(mut guard) => guard.push((target.clone(), keys.to_vec())),
|
|
560
|
+
Err(poisoned) => poisoned.into_inner().push((target.clone(), keys.to_vec())),
|
|
561
|
+
}
|
|
562
|
+
Ok(())
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
fn capture(
|
|
566
|
+
&self,
|
|
567
|
+
_target: &Target,
|
|
568
|
+
range: CaptureRange,
|
|
569
|
+
) -> Result<CapturedText, TransportError> {
|
|
570
|
+
let text = match self.screens.lock() {
|
|
571
|
+
Ok(mut guard) => {
|
|
572
|
+
if guard.is_empty() {
|
|
573
|
+
String::new()
|
|
574
|
+
} else {
|
|
575
|
+
guard.remove(0)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
Err(poisoned) => {
|
|
579
|
+
let mut guard = poisoned.into_inner();
|
|
580
|
+
if guard.is_empty() {
|
|
581
|
+
String::new()
|
|
582
|
+
} else {
|
|
583
|
+
guard.remove(0)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
Ok(CapturedText { text, range })
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
fn query(
|
|
591
|
+
&self,
|
|
592
|
+
_target: &Target,
|
|
593
|
+
_field: PaneField,
|
|
594
|
+
) -> Result<Option<String>, TransportError> {
|
|
595
|
+
Ok(None)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
599
|
+
Ok(PaneLiveness::Unknown)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
603
|
+
Ok(Vec::new())
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
607
|
+
Ok(false)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
611
|
+
Ok(Vec::new())
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
fn set_session_env(
|
|
615
|
+
&self,
|
|
616
|
+
_session: &SessionName,
|
|
617
|
+
_key: &str,
|
|
618
|
+
_value: &str,
|
|
619
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
620
|
+
Ok(SetEnvOutcome::Applied)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
624
|
+
Ok(())
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
628
|
+
Ok(())
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
632
|
+
Ok(AttachOutcome::Unsupported {
|
|
633
|
+
reason: "not used by startup-prompt test".to_string(),
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
#[test]
|
|
639
|
+
fn exec_provider_leader_startup_prompt_handler_reuses_copilot_adapter() {
|
|
640
|
+
let transport = ScriptedTransport::new(vec![
|
|
641
|
+
COPILOT_TRUST_PROMPT_MARKER.to_string(),
|
|
642
|
+
COPILOT_READY_MARKER.to_string(),
|
|
643
|
+
]);
|
|
644
|
+
let workspace =
|
|
645
|
+
std::env::temp_dir().join(format!("ta_rs_red2_leader_startup_{}", std::process::id()));
|
|
646
|
+
let _ = std::fs::create_dir_all(&workspace);
|
|
647
|
+
|
|
648
|
+
let outcome = handle_exec_provider_startup_prompts(
|
|
649
|
+
Provider::Copilot,
|
|
650
|
+
&workspace,
|
|
651
|
+
"%0",
|
|
652
|
+
&transport,
|
|
653
|
+
5,
|
|
654
|
+
0.0,
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
assert_eq!(outcome.handled.len(), 1);
|
|
658
|
+
assert_eq!(outcome.handled[0].prompt, "copilot_workspace_trust");
|
|
659
|
+
assert_eq!(outcome.handled[0].action, "sent_enter_yes_session");
|
|
660
|
+
let sent = transport.sent();
|
|
661
|
+
assert_eq!(sent.len(), 1);
|
|
662
|
+
assert_eq!(sent[0].0, Target::Pane(PaneId::new("%0")));
|
|
663
|
+
assert_eq!(sent[0].1, vec![Key::Enter]);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
@@ -1059,23 +1059,13 @@ fn unbound_launched_provider(launched: &serde_json::Value) -> Option<String> {
|
|
|
1059
1059
|
{
|
|
1060
1060
|
return Some(provider);
|
|
1061
1061
|
}
|
|
1062
|
-
let workspace = launched
|
|
1063
|
-
.get("workspace")
|
|
1064
|
-
.and_then(serde_json::Value::as_str)
|
|
1065
|
-
.filter(|workspace| !workspace.is_empty())?;
|
|
1066
1062
|
let pane = launched
|
|
1067
1063
|
.get("team_owner")
|
|
1068
1064
|
.and_then(|owner| owner.get("pane_id"))
|
|
1069
1065
|
.and_then(serde_json::Value::as_str)
|
|
1070
1066
|
.filter(|pane| !pane.is_empty())?;
|
|
1071
1067
|
let target = PaneId::new(pane);
|
|
1072
|
-
|
|
1073
|
-
.list_targets()
|
|
1074
|
-
.ok()?
|
|
1075
|
-
.into_iter()
|
|
1076
|
-
.find(|info| info.pane_id == target)
|
|
1077
|
-
.and_then(|info| crate::leader::attribute_pane_provider(&info))
|
|
1078
|
-
.and_then(provider_wire_string)
|
|
1068
|
+
attributed_provider_for_pane_across_tmux_sockets(&target).and_then(provider_wire_string)
|
|
1079
1069
|
}
|
|
1080
1070
|
|
|
1081
1071
|
fn provider_wire_string(provider: Provider) -> Option<String> {
|
|
@@ -1084,9 +1074,38 @@ fn provider_wire_string(provider: Provider) -> Option<String> {
|
|
|
1084
1074
|
.and_then(|value| value.as_str().map(str::to_string))
|
|
1085
1075
|
}
|
|
1086
1076
|
|
|
1077
|
+
fn attributed_provider_for_pane_across_tmux_sockets(pane: &PaneId) -> Option<Provider> {
|
|
1078
|
+
crate::tmux_backend::tmux_socket_endpoints()
|
|
1079
|
+
.into_iter()
|
|
1080
|
+
.filter_map(|endpoint| {
|
|
1081
|
+
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
|
|
1082
|
+
.list_targets()
|
|
1083
|
+
.ok()
|
|
1084
|
+
})
|
|
1085
|
+
.flatten()
|
|
1086
|
+
.find(|info| info.pane_id == *pane)
|
|
1087
|
+
.and_then(|info| crate::leader::attribute_pane_provider(&info))
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
fn caller_provider_for_seed_with_lookup(
|
|
1091
|
+
caller: &crate::state::owner_gate::CallerIdentity,
|
|
1092
|
+
lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
|
|
1093
|
+
) -> Option<String> {
|
|
1094
|
+
if !caller.provider.is_empty() {
|
|
1095
|
+
if let Some(provider) = parse_provider(&caller.provider).and_then(provider_wire_string) {
|
|
1096
|
+
return Some(provider);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
(!caller.pane_id.is_empty())
|
|
1100
|
+
.then(|| PaneId::new(&caller.pane_id))
|
|
1101
|
+
.and_then(|pane| lookup_pane_provider(&pane))
|
|
1102
|
+
.and_then(provider_wire_string)
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1087
1105
|
#[cfg(test)]
|
|
1088
1106
|
mod e22_unbound_owner_provider_tests {
|
|
1089
1107
|
use super::*;
|
|
1108
|
+
use crate::state::owner_gate::CallerIdentity;
|
|
1090
1109
|
|
|
1091
1110
|
#[test]
|
|
1092
1111
|
fn unbound_owner_preserves_explicit_copilot_provider() {
|
|
@@ -1137,6 +1156,83 @@ mod e22_unbound_owner_provider_tests {
|
|
|
1137
1156
|
"unattributed unbound owner must not silently become codex: {launched}"
|
|
1138
1157
|
);
|
|
1139
1158
|
}
|
|
1159
|
+
|
|
1160
|
+
fn caller(provider: &str, pane_id: &str) -> CallerIdentity {
|
|
1161
|
+
CallerIdentity {
|
|
1162
|
+
pane_id: pane_id.to_string(),
|
|
1163
|
+
provider: provider.to_string(),
|
|
1164
|
+
machine_fingerprint: "machine".to_string(),
|
|
1165
|
+
leader_session_uuid: "leader-uuid".to_string(),
|
|
1166
|
+
leader_session_uuid_source: "derived".to_string(),
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
#[test]
|
|
1171
|
+
fn env_seed_attributes_in_tmux_node_form_copilot_from_caller_pane() {
|
|
1172
|
+
let mut state = serde_json::json!({
|
|
1173
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1174
|
+
"leader": {"provider": "copilot"},
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
assert!(seed_launched_owner_from_caller_with_provider_lookup(
|
|
1178
|
+
&mut state,
|
|
1179
|
+
caller("", "%0"),
|
|
1180
|
+
|pane| (pane.as_str() == "%0").then_some(Provider::Copilot),
|
|
1181
|
+
));
|
|
1182
|
+
|
|
1183
|
+
assert_eq!(
|
|
1184
|
+
state
|
|
1185
|
+
.pointer("/team_owner/provider")
|
|
1186
|
+
.and_then(serde_json::Value::as_str),
|
|
1187
|
+
Some("copilot")
|
|
1188
|
+
);
|
|
1189
|
+
assert_eq!(
|
|
1190
|
+
state
|
|
1191
|
+
.pointer("/leader_receiver/provider")
|
|
1192
|
+
.and_then(serde_json::Value::as_str),
|
|
1193
|
+
Some("copilot")
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
#[test]
|
|
1198
|
+
fn env_seed_unknown_caller_pane_does_not_default_codex() {
|
|
1199
|
+
let mut state = serde_json::json!({
|
|
1200
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1201
|
+
"leader": {"provider": "copilot"},
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
assert!(seed_launched_owner_from_caller_with_provider_lookup(
|
|
1205
|
+
&mut state,
|
|
1206
|
+
caller("", "%0"),
|
|
1207
|
+
|_| None,
|
|
1208
|
+
));
|
|
1209
|
+
assert_eq!(
|
|
1210
|
+
state
|
|
1211
|
+
.pointer("/team_owner/pane_id")
|
|
1212
|
+
.and_then(serde_json::Value::as_str),
|
|
1213
|
+
Some("%0")
|
|
1214
|
+
);
|
|
1215
|
+
assert_eq!(
|
|
1216
|
+
state
|
|
1217
|
+
.pointer("/leader_receiver/pane_id")
|
|
1218
|
+
.and_then(serde_json::Value::as_str),
|
|
1219
|
+
Some("%0")
|
|
1220
|
+
);
|
|
1221
|
+
assert!(
|
|
1222
|
+
state
|
|
1223
|
+
.pointer("/leader_receiver/provider")
|
|
1224
|
+
.and_then(serde_json::Value::as_str)
|
|
1225
|
+
!= Some("codex"),
|
|
1226
|
+
"unknown caller pane must not silently seed a codex receiver: {state}"
|
|
1227
|
+
);
|
|
1228
|
+
assert!(
|
|
1229
|
+
state
|
|
1230
|
+
.pointer("/team_owner/provider")
|
|
1231
|
+
.and_then(serde_json::Value::as_str)
|
|
1232
|
+
!= Some("codex"),
|
|
1233
|
+
"unknown caller pane must not silently become codex: {state}"
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1140
1236
|
}
|
|
1141
1237
|
|
|
1142
1238
|
fn owner_pane_belongs_to_other_team(
|
|
@@ -3711,19 +3807,26 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
3711
3807
|
) else {
|
|
3712
3808
|
return false;
|
|
3713
3809
|
};
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3810
|
+
seed_launched_owner_from_caller_with_provider_lookup(
|
|
3811
|
+
state,
|
|
3812
|
+
caller,
|
|
3813
|
+
attributed_provider_for_pane_across_tmux_sockets,
|
|
3814
|
+
)
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
fn seed_launched_owner_from_caller_with_provider_lookup(
|
|
3818
|
+
state: &mut serde_json::Value,
|
|
3819
|
+
caller: crate::state::owner_gate::CallerIdentity,
|
|
3820
|
+
lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
|
|
3821
|
+
) -> bool {
|
|
3822
|
+
if caller.pane_id.is_empty() {
|
|
3721
3823
|
return false;
|
|
3722
3824
|
}
|
|
3825
|
+
let provider = caller_provider_for_seed_with_lookup(&caller, lookup_pane_provider);
|
|
3826
|
+
let pane_id = caller.pane_id;
|
|
3723
3827
|
let owner_epoch = 1u64;
|
|
3724
|
-
let owner = serde_json::json!({
|
|
3828
|
+
let mut owner = serde_json::json!({
|
|
3725
3829
|
"pane_id": pane_id,
|
|
3726
|
-
"provider": provider.clone(),
|
|
3727
3830
|
"machine_fingerprint": caller.machine_fingerprint,
|
|
3728
3831
|
"leader_session_uuid": caller.leader_session_uuid,
|
|
3729
3832
|
"owner_epoch": owner_epoch,
|
|
@@ -3733,17 +3836,23 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
3733
3836
|
.or_else(|_| std::env::var("USERNAME"))
|
|
3734
3837
|
.unwrap_or_default(),
|
|
3735
3838
|
});
|
|
3736
|
-
let receiver = serde_json::json!({
|
|
3839
|
+
let mut receiver = serde_json::json!({
|
|
3737
3840
|
"mode": "direct_tmux",
|
|
3738
3841
|
"status": "attached",
|
|
3739
|
-
"provider": provider,
|
|
3740
3842
|
"pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
3741
3843
|
"pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
3742
3844
|
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
3743
3845
|
"owner_epoch": owner_epoch,
|
|
3744
3846
|
"discovery": "quick_start",
|
|
3745
3847
|
});
|
|
3746
|
-
let
|
|
3848
|
+
if let Some(provider) = provider.as_ref() {
|
|
3849
|
+
if let Some(owner) = owner.as_object_mut() {
|
|
3850
|
+
owner.insert("provider".to_string(), serde_json::json!(provider));
|
|
3851
|
+
}
|
|
3852
|
+
if let Some(receiver) = receiver.as_object_mut() {
|
|
3853
|
+
receiver.insert("provider".to_string(), serde_json::json!(provider));
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3747
3856
|
if let (Some(receiver), Some(socket)) = (
|
|
3748
3857
|
receiver.as_object_mut(),
|
|
3749
3858
|
crate::tmux_backend::socket_name_from_tmux_env(),
|
|
@@ -36,12 +36,11 @@ pub(super) fn spawn_agent_window(
|
|
|
36
36
|
detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
|
|
37
37
|
&detected_safety
|
|
38
38
|
};
|
|
39
|
-
let command_agent =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
39
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
|
|
40
|
+
agent,
|
|
41
|
+
Some(agent_id.as_str()),
|
|
42
|
+
provider,
|
|
43
|
+
);
|
|
45
44
|
let system_prompt =
|
|
46
45
|
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
47
46
|
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
@@ -60,7 +59,8 @@ pub(super) fn spawn_agent_window(
|
|
|
60
59
|
.and_then(|v| v.as_str())
|
|
61
60
|
.map(str::to_string)
|
|
62
61
|
.or_else(|| {
|
|
63
|
-
let key =
|
|
62
|
+
let key =
|
|
63
|
+
crate::messaging::leader_receiver::active_team_key(workspace, &state_for_team);
|
|
64
64
|
(!key.is_empty()).then_some(key)
|
|
65
65
|
});
|
|
66
66
|
let mcp_config = adapter
|
|
@@ -72,8 +72,11 @@ pub(super) fn spawn_agent_window(
|
|
|
72
72
|
agent_id.as_str(),
|
|
73
73
|
team_id.as_deref().unwrap_or(""),
|
|
74
74
|
);
|
|
75
|
-
let mcp_config_path =
|
|
76
|
-
|
|
75
|
+
let mcp_config_path = crate::lifecycle::launch::write_worker_mcp_config(
|
|
76
|
+
workspace,
|
|
77
|
+
agent_id.as_str(),
|
|
78
|
+
&mcp_config,
|
|
79
|
+
)?;
|
|
77
80
|
let profile_launch =
|
|
78
81
|
crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
|
|
79
82
|
workspace,
|
|
@@ -81,11 +84,7 @@ pub(super) fn spawn_agent_window(
|
|
|
81
84
|
agent,
|
|
82
85
|
Some(&mcp_config),
|
|
83
86
|
)?;
|
|
84
|
-
let command_model = profile_launch
|
|
85
|
-
.command_overrides
|
|
86
|
-
.model
|
|
87
|
-
.as_deref()
|
|
88
|
-
.or(model);
|
|
87
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
89
88
|
let context = crate::provider::ProviderCommandContext {
|
|
90
89
|
auth_mode,
|
|
91
90
|
mcp_config: Some(&mcp_config),
|
|
@@ -252,19 +251,30 @@ pub(super) fn resume_backing_exists_for_agent(
|
|
|
252
251
|
rollout_path: Option<&RolloutPath>,
|
|
253
252
|
) -> bool {
|
|
254
253
|
match provider {
|
|
254
|
+
provider if !provider_supports_resume(provider) => {
|
|
255
|
+
let _ = (workspace, agent_id, agent, session_id, rollout_path);
|
|
256
|
+
false
|
|
257
|
+
}
|
|
255
258
|
Provider::Codex => rollout_path_exists(rollout_path),
|
|
256
259
|
Provider::Claude | Provider::ClaudeCode => {
|
|
257
260
|
rollout_path_exists(rollout_path)
|
|
258
261
|
|| event_log_transcript_exists(workspace, agent_id.as_str(), session_id.as_str())
|
|
259
262
|
}
|
|
260
263
|
Provider::Copilot => copilot_session_store_has_session(session_id.as_str()),
|
|
261
|
-
Provider::GeminiCli | Provider::Fake =>
|
|
262
|
-
let _ = agent;
|
|
263
|
-
true
|
|
264
|
-
}
|
|
264
|
+
Provider::GeminiCli | Provider::Fake => false,
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
pub(super) fn provider_supports_resume(provider: Provider) -> bool {
|
|
269
|
+
crate::provider::get_adapter(provider).caps().resume
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
pub(super) fn provider_wire_supports_resume(provider: &str) -> bool {
|
|
273
|
+
parse_provider(provider)
|
|
274
|
+
.map(provider_supports_resume)
|
|
275
|
+
.unwrap_or(false)
|
|
276
|
+
}
|
|
277
|
+
|
|
268
278
|
fn rollout_path_exists(rollout_path: Option<&RolloutPath>) -> bool {
|
|
269
279
|
rollout_path
|
|
270
280
|
.as_ref()
|
|
@@ -308,13 +318,12 @@ fn copilot_session_store_has_session(session_id: &str) -> bool {
|
|
|
308
318
|
) else {
|
|
309
319
|
return false;
|
|
310
320
|
};
|
|
311
|
-
conn
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
.is_ok()
|
|
321
|
+
conn.query_row(
|
|
322
|
+
"select 1 from sessions where id = ?1 limit 1",
|
|
323
|
+
[session_id],
|
|
324
|
+
|_| Ok(()),
|
|
325
|
+
)
|
|
326
|
+
.is_ok()
|
|
318
327
|
}
|
|
319
328
|
|
|
320
329
|
pub(crate) fn refresh_missing_provider_sessions(
|
|
@@ -469,7 +478,10 @@ pub(super) fn load_team_spec(workspace: &Path) -> Result<YamlValue, LifecycleErr
|
|
|
469
478
|
yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))
|
|
470
479
|
}
|
|
471
480
|
|
|
472
|
-
pub(super) fn find_spec_agent<'a>(
|
|
481
|
+
pub(super) fn find_spec_agent<'a>(
|
|
482
|
+
spec: &'a YamlValue,
|
|
483
|
+
agent_id: &AgentId,
|
|
484
|
+
) -> Option<&'a YamlValue> {
|
|
473
485
|
let leader_is_agent = spec
|
|
474
486
|
.get("leader")
|
|
475
487
|
.and_then(|v| v.get("id"))
|
|
@@ -479,23 +491,23 @@ pub(super) fn find_spec_agent<'a>(spec: &'a YamlValue, agent_id: &AgentId) -> Op
|
|
|
479
491
|
if leader_is_agent {
|
|
480
492
|
return None;
|
|
481
493
|
}
|
|
482
|
-
spec.get("agents")
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
.map(|id| id == agent_id.as_str())
|
|
490
|
-
.unwrap_or(false)
|
|
491
|
-
})
|
|
494
|
+
spec.get("agents")?.as_list()?.iter().find(|agent| {
|
|
495
|
+
agent
|
|
496
|
+
.get("id")
|
|
497
|
+
.and_then(YamlValue::as_str)
|
|
498
|
+
.map(|id| id == agent_id.as_str())
|
|
499
|
+
.unwrap_or(false)
|
|
500
|
+
})
|
|
492
501
|
}
|
|
493
502
|
|
|
494
503
|
pub(super) fn unknown_worker(agent_id: &AgentId) -> LifecycleError {
|
|
495
504
|
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {agent_id}"))
|
|
496
505
|
}
|
|
497
506
|
|
|
498
|
-
pub(super) fn state_session_name_from_spec(
|
|
507
|
+
pub(super) fn state_session_name_from_spec(
|
|
508
|
+
state: &serde_json::Value,
|
|
509
|
+
spec: &YamlValue,
|
|
510
|
+
) -> SessionName {
|
|
499
511
|
state
|
|
500
512
|
.get("session_name")
|
|
501
513
|
.and_then(|v| v.as_str())
|
|
@@ -709,9 +721,7 @@ pub(super) fn agent_is_running(
|
|
|
709
721
|
agent_id: &AgentId,
|
|
710
722
|
transport: &dyn crate::transport::Transport,
|
|
711
723
|
) -> bool {
|
|
712
|
-
let agent_state = state
|
|
713
|
-
.get("agents")
|
|
714
|
-
.and_then(|v| v.get(agent_id.as_str()));
|
|
724
|
+
let agent_state = state.get("agents").and_then(|v| v.get(agent_id.as_str()));
|
|
715
725
|
let status = agent_state
|
|
716
726
|
.and_then(|v| v.get("status"))
|
|
717
727
|
.and_then(|v| v.as_str())
|