@team-agent/installer 0.5.77 → 0.5.80
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/adapters.rs +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +393 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +197 -88
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -8,6 +8,77 @@ use crate::model::enums::ProviderEffort;
|
|
|
8
8
|
use crate::provider::adapters::pi::{build_pi_command_argv, PiCommandRequest, PiSessionSelector};
|
|
9
9
|
|
|
10
10
|
const CATALOG: &[u8] = include_bytes!("fixtures/pi_list_models_g0.stdout.txt");
|
|
11
|
+
// Public pi --list-models rows captured for issue #186, preserving column widths.
|
|
12
|
+
const SPACED_IDS_CATALOG: &[u8] = include_bytes!("fixtures/pi_list_models_spaced_ids.stdout.txt");
|
|
13
|
+
|
|
14
|
+
#[test]
|
|
15
|
+
fn pi_catalog_preserves_spaced_ids_and_qualified_slug_variants() {
|
|
16
|
+
let models = parse_pi_list_models_table(SPACED_IDS_CATALOG).expect("public catalog rows");
|
|
17
|
+
assert_eq!(
|
|
18
|
+
models,
|
|
19
|
+
vec![
|
|
20
|
+
"cursor/claude-opus-4-7@1m:fast",
|
|
21
|
+
"cursor/claude-opus-4-7@1m:slow",
|
|
22
|
+
"openai-codex/gpt-5.6-luna",
|
|
23
|
+
"team-agent/Claude Opus 4.6 (Thinking)",
|
|
24
|
+
"team-agent/Claude Sonnet 4.6 (Thinking)",
|
|
25
|
+
"team-agent/Gemini 3.8 Flash",
|
|
26
|
+
"team-agent/gpt-5.6-luna",
|
|
27
|
+
"team-agent/qwen3.8-27b",
|
|
28
|
+
]
|
|
29
|
+
);
|
|
30
|
+
for exact in &models {
|
|
31
|
+
assert_eq!(select_exact_pi_model(&models, exact).unwrap(), *exact);
|
|
32
|
+
assert!(argv_has_pair(&command_for_model(exact), "--model", exact));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn pi_catalog_still_rejects_duplicate_complete_spaced_ids() {
|
|
38
|
+
let text = std::str::from_utf8(SPACED_IDS_CATALOG).unwrap();
|
|
39
|
+
let repeated = text
|
|
40
|
+
.lines()
|
|
41
|
+
.find(|line| line.contains("Claude Opus 4.6 (Thinking)"))
|
|
42
|
+
.unwrap();
|
|
43
|
+
let duplicate = format!("{text}{repeated}\n");
|
|
44
|
+
let error = parse_pi_list_models_table(duplicate.as_bytes()).unwrap_err();
|
|
45
|
+
assert!(error
|
|
46
|
+
.to_string()
|
|
47
|
+
.contains("duplicate exact id \"team-agent/Claude Opus 4.6 (Thinking)\""));
|
|
48
|
+
let exact = "team-agent/Claude Opus 4.6 (Thinking)";
|
|
49
|
+
assert!(select_exact_pi_model(&[exact.into(), exact.into()], exact).is_err());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn pi_catalog_preserves_internal_whitespace_with_compact_and_full_headers() {
|
|
54
|
+
for catalog in [
|
|
55
|
+
"provider model\r\nteam-agent\tClaude Opus 4.6 (Thinking) \r\n",
|
|
56
|
+
"provider model context max-out thinking images\r\nteam-agent\tClaude Opus 4.6 (Thinking)\t200K 32.8K\tyes yes \r\n",
|
|
57
|
+
] {
|
|
58
|
+
assert_eq!(
|
|
59
|
+
parse_pi_list_models_table(catalog.as_bytes()).unwrap(),
|
|
60
|
+
vec!["team-agent/Claude Opus 4.6 (Thinking)"]
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn pi_catalog_rejects_rows_without_model_or_required_trailing_fields() {
|
|
67
|
+
for row in [
|
|
68
|
+
"team-agent",
|
|
69
|
+
"team-agent 200K 32.8K yes yes",
|
|
70
|
+
"team-agent gpt-5.6-luna 200K 32.8K yes",
|
|
71
|
+
] {
|
|
72
|
+
let catalog = format!("provider model context max-out thinking images\n{row}\n");
|
|
73
|
+
assert!(
|
|
74
|
+
parse_pi_list_models_table(catalog.as_bytes())
|
|
75
|
+
.unwrap_err()
|
|
76
|
+
.to_string()
|
|
77
|
+
.contains("row 2 is malformed"),
|
|
78
|
+
"missing model or capability fields must refuse: {row}"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
11
82
|
|
|
12
83
|
fn argv_has_pair(argv: &[String], flag: &str, value: &str) -> bool {
|
|
13
84
|
argv.windows(2)
|
|
@@ -428,6 +428,84 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
|
|
|
428
428
|
"dynamic add must not create a second tmux session"
|
|
429
429
|
);
|
|
430
430
|
|
|
431
|
+
let pi_provider_root = hermetic.workspace("pi-add-provider");
|
|
432
|
+
let pi_bin = pi_provider_root.join("bin");
|
|
433
|
+
let pi_package = pi_provider_root.join("pi-mcp-adapter");
|
|
434
|
+
std::fs::create_dir_all(&pi_bin).expect("create Pi test bin");
|
|
435
|
+
std::fs::create_dir_all(&pi_package).expect("create Pi test adapter");
|
|
436
|
+
let pi_real = pi_provider_root.join("pi-cli");
|
|
437
|
+
std::fs::write(
|
|
438
|
+
&pi_real,
|
|
439
|
+
format!(
|
|
440
|
+
"#!/bin/sh\ncase \"$1\" in\n --version) printf '0.84.4\\n' ;;\n --list-models) printf 'provider model\\nteam-agent qwen3.8-27b\\n' ;;\n list) printf 'npm:pi-mcp-adapter\\n{}\\n' ;;\n *) exit 64 ;;\nesac\n",
|
|
441
|
+
pi_package.display()
|
|
442
|
+
),
|
|
443
|
+
)
|
|
444
|
+
.expect("write protocol-capable Pi test executable");
|
|
445
|
+
std::fs::set_permissions(
|
|
446
|
+
&pi_real,
|
|
447
|
+
std::os::unix::fs::PermissionsExt::from_mode(0o755),
|
|
448
|
+
)
|
|
449
|
+
.expect("make Pi test executable");
|
|
450
|
+
std::os::unix::fs::symlink(&pi_real, pi_bin.join("pi")).expect("link Pi test executable");
|
|
451
|
+
std::fs::write(
|
|
452
|
+
pi_package.join("package.json"),
|
|
453
|
+
br#"{"name":"pi-mcp-adapter","version":"2.30.0","pi":{"extensions":["./index.ts"]}}"#,
|
|
454
|
+
)
|
|
455
|
+
.expect("write Pi test adapter package");
|
|
456
|
+
std::fs::write(
|
|
457
|
+
pi_package.join("index.ts"),
|
|
458
|
+
b"export const createMcpAdapter = () => {};\n",
|
|
459
|
+
)
|
|
460
|
+
.expect("write Pi test adapter entry");
|
|
461
|
+
let _pi_path = EnvVarGuard::set("PATH", pi_bin.as_os_str());
|
|
462
|
+
let pi_success_role = "---\nname: mate\nrole: Pi Dynamic Worker\nprovider: pi\nmodel: team-agent/qwen3.8-27b\nauth_mode: subscription\neffort: max\ntools:\n - mcp_team\ndangerously_skip_permissions: true\n---\n\ndynamic pi\n";
|
|
463
|
+
let (pi_success_team, pi_success_role_path) =
|
|
464
|
+
dynamic_add_fixture(&dynamic_root, "dynamic-pi-success", pi_success_role);
|
|
465
|
+
let pi_success_transport = OfflineTransport::new().with_session_present(true);
|
|
466
|
+
crate::lifecycle::add_agent_with_transport(
|
|
467
|
+
&pi_success_team,
|
|
468
|
+
&AgentId::new("mate"),
|
|
469
|
+
&pi_success_role_path,
|
|
470
|
+
false,
|
|
471
|
+
None,
|
|
472
|
+
&pi_success_transport,
|
|
473
|
+
)
|
|
474
|
+
.expect("Pi dynamic add must commit after spawning its materialized plan");
|
|
475
|
+
let pi_spawn = pi_success_transport.spawn_records();
|
|
476
|
+
assert_eq!(pi_spawn.len(), 1, "Pi dynamic add must spawn exactly once");
|
|
477
|
+
let pi_state = crate::state::persist::load_runtime_state(&pi_success_team)
|
|
478
|
+
.expect("load successful Pi add state");
|
|
479
|
+
assert_eq!(
|
|
480
|
+
pi_state
|
|
481
|
+
.pointer("/agents/mate/status")
|
|
482
|
+
.and_then(serde_json::Value::as_str),
|
|
483
|
+
Some("running"),
|
|
484
|
+
"successful Pi add must commit the running seat"
|
|
485
|
+
);
|
|
486
|
+
let pi_events = crate::event_log::EventLog::new(&pi_success_team)
|
|
487
|
+
.tail(50)
|
|
488
|
+
.expect("read successful Pi add events");
|
|
489
|
+
let pi_start_event = pi_events
|
|
490
|
+
.iter()
|
|
491
|
+
.find(|event| {
|
|
492
|
+
event.get("event").and_then(serde_json::Value::as_str)
|
|
493
|
+
== Some("start_agent.agent_start")
|
|
494
|
+
})
|
|
495
|
+
.expect("successful Pi add must emit start event");
|
|
496
|
+
let pi_event_command = pi_start_event
|
|
497
|
+
.get("command")
|
|
498
|
+
.and_then(serde_json::Value::as_array)
|
|
499
|
+
.expect("successful Pi start event command")
|
|
500
|
+
.iter()
|
|
501
|
+
.map(|value| value.as_str().expect("Pi argv string").to_string())
|
|
502
|
+
.collect::<Vec<_>>();
|
|
503
|
+
assert_eq!(
|
|
504
|
+
pi_event_command, pi_spawn[0].1,
|
|
505
|
+
"Pi start event must record the exact materialized plan that was spawned"
|
|
506
|
+
);
|
|
507
|
+
drop(_pi_path);
|
|
508
|
+
|
|
431
509
|
let (rollback_team, rollback_role) =
|
|
432
510
|
dynamic_add_fixture(&dynamic_root, "dynamic-rollback", fake_role);
|
|
433
511
|
let events_path = rollback_team.join(".team/logs/events.jsonl");
|
|
@@ -35,6 +35,9 @@ use team_agent::transport::{
|
|
|
35
35
|
#[test]
|
|
36
36
|
#[serial(env)]
|
|
37
37
|
fn quick_start_team_id_persists_requested_team_key_for_lifecycle_selection() {
|
|
38
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("team-in-team-requested-key");
|
|
39
|
+
hermetic.scrub_tmux();
|
|
40
|
+
hermetic.assert_no_real_tmux();
|
|
38
41
|
let _env = EnvGuard::unset();
|
|
39
42
|
let root = tmp_dir("team-id-key");
|
|
40
43
|
seed_healthy_coordinator(&root);
|
|
@@ -131,18 +131,46 @@ fn r5_restart_auto_attach_registers_isolated_live_leader_entry() {
|
|
|
131
131
|
case.seed_restartable_workspace();
|
|
132
132
|
seed_healthy_coordinator(&case.workspace);
|
|
133
133
|
|
|
134
|
-
let
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
let live = crate::transport::test_support::OfflineTransport::default()
|
|
135
|
+
.with_tmux_endpoint(TMUX_SOCKET)
|
|
136
|
+
.with_targets(vec![PaneInfo {
|
|
137
|
+
pane_id: PaneId::new(CALLER_PANE),
|
|
138
|
+
session: SessionName::new("team-agent-leader-0515"),
|
|
139
|
+
window_index: Some(0),
|
|
140
|
+
window_name: Some(WindowName::new("leader")),
|
|
141
|
+
pane_index: Some(0),
|
|
142
|
+
tty: None,
|
|
143
|
+
current_command: Some("codex".to_string()),
|
|
144
|
+
current_path: Some(case.workspace.clone()),
|
|
145
|
+
active: true,
|
|
146
|
+
pane_pid: None,
|
|
147
|
+
leader_env: Default::default(),
|
|
148
|
+
}]);
|
|
149
|
+
let report = crate::transport_factory::with_leader_endpoint_transport(
|
|
150
|
+
TMUX_SOCKET,
|
|
151
|
+
live,
|
|
152
|
+
|| {
|
|
153
|
+
restart_with_transport_with_readiness_deadline(
|
|
154
|
+
&case.workspace,
|
|
155
|
+
true,
|
|
156
|
+
Some(TEAM_KEY),
|
|
157
|
+
&RestartAutoAttachTransport::new(),
|
|
158
|
+
Some(1_000),
|
|
159
|
+
)
|
|
160
|
+
},
|
|
140
161
|
)
|
|
141
162
|
.expect("R5 setup: restart should complete against fake transport");
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
163
|
+
match &report {
|
|
164
|
+
RestartReport::Restarted {
|
|
165
|
+
leader_bind_ok, ..
|
|
166
|
+
} => {
|
|
167
|
+
assert!(
|
|
168
|
+
*leader_bind_ok,
|
|
169
|
+
"R5: public restart bind must be ok; report={report:?}"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
other => panic!("R5 setup: restart must succeed before checking registry side effects; report={other:?}"),
|
|
173
|
+
}
|
|
146
174
|
let state = load_runtime_state(&case.workspace).expect("read state after restart");
|
|
147
175
|
assert_eq!(
|
|
148
176
|
canonical_leader_receiver(&state)
|
|
@@ -527,6 +555,19 @@ impl RestartAutoAttachCase {
|
|
|
527
555
|
"session_name": "team-ctxteam",
|
|
528
556
|
"tmux_endpoint": TMUX_SOCKET,
|
|
529
557
|
"tmux_socket": TMUX_SOCKET,
|
|
558
|
+
"team_owner": {
|
|
559
|
+
"pane_id": CALLER_PANE,
|
|
560
|
+
"owner_epoch": 1,
|
|
561
|
+
"provider": "codex"
|
|
562
|
+
},
|
|
563
|
+
"leader_receiver": {
|
|
564
|
+
"mode": "direct_tmux",
|
|
565
|
+
"status": "attached",
|
|
566
|
+
"pane_id": CALLER_PANE,
|
|
567
|
+
"owner_epoch": 1,
|
|
568
|
+
"provider": "codex",
|
|
569
|
+
"tmux_socket": TMUX_SOCKET
|
|
570
|
+
},
|
|
530
571
|
"agents": { WORKER: worker }
|
|
531
572
|
}
|
|
532
573
|
}
|
|
@@ -103,13 +103,37 @@ fn worker_mcp_owner_team_scope_must_match_runtime_team_key_not_spec_name() {
|
|
|
103
103
|
fn quick_start_preserves_external_leader_receiver_when_worker_pane_id_collides_across_tmux_sockets()
|
|
104
104
|
{
|
|
105
105
|
let hermetic = HermeticTestEnv::enter("rs031-external-pane-collision");
|
|
106
|
-
let
|
|
106
|
+
let caller_endpoint = "/tmp/default-tmux-socket";
|
|
107
|
+
let _tmux = hermetic.with_env("TMUX", &format!("{caller_endpoint},123,0"));
|
|
107
108
|
let _pane = hermetic.with_env("TMUX_PANE", "%0");
|
|
109
|
+
let _provider = hermetic.with_env("TEAM_AGENT_LEADER_PROVIDER", "codex");
|
|
108
110
|
let team = codex_only_team_dir(&hermetic, "external-pane-collision");
|
|
109
|
-
let
|
|
111
|
+
let workspace = team.parent().expect("teamdir has parent workspace");
|
|
112
|
+
let caller = PaneInfo {
|
|
113
|
+
pane_id: PaneId::new("%0"),
|
|
114
|
+
session: SessionName::new("leader"),
|
|
115
|
+
window_index: Some(0),
|
|
116
|
+
window_name: Some(WindowName::new("leader")),
|
|
117
|
+
pane_index: Some(0),
|
|
118
|
+
tty: None,
|
|
119
|
+
current_command: Some("codex".to_string()),
|
|
120
|
+
current_path: Some(workspace.to_path_buf()),
|
|
121
|
+
active: true,
|
|
122
|
+
pane_pid: None,
|
|
123
|
+
leader_env: Default::default(),
|
|
124
|
+
};
|
|
125
|
+
let transport = RecordingTransport::new()
|
|
126
|
+
.with_windows(vec![WindowName::new("codexer")])
|
|
127
|
+
.with_tmux_endpoint(caller_endpoint)
|
|
128
|
+
.with_targets(vec![caller.clone()]);
|
|
129
|
+
let live = crate::transport::test_support::OfflineTransport::default()
|
|
130
|
+
.with_tmux_endpoint(caller_endpoint)
|
|
131
|
+
.with_targets(vec![caller]);
|
|
110
132
|
let launch = ready_launch(
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
crate::transport_factory::with_leader_endpoint_transport(caller_endpoint, live, || {
|
|
134
|
+
quick_start_with_transport(&team, None, true, None, &transport)
|
|
135
|
+
})
|
|
136
|
+
.expect("quick-start should reach worker spawn path"),
|
|
113
137
|
);
|
|
114
138
|
let worker_pane = launch
|
|
115
139
|
.started
|
|
@@ -122,7 +146,6 @@ fn quick_start_preserves_external_leader_receiver_when_worker_pane_id_collides_a
|
|
|
122
146
|
"fixture precondition: first worker in the product -L socket receives bare pane id %0"
|
|
123
147
|
);
|
|
124
148
|
|
|
125
|
-
let workspace = team.parent().expect("teamdir has parent workspace");
|
|
126
149
|
let state = load_runtime_state(workspace).expect("runtime state written by launch");
|
|
127
150
|
let top_receiver_pane = state
|
|
128
151
|
.get("leader_receiver")
|
|
@@ -765,6 +788,7 @@ struct RecordingState {
|
|
|
765
788
|
spawn_argvs: Vec<Vec<String>>,
|
|
766
789
|
liveness: BTreeMap<String, PaneLiveness>,
|
|
767
790
|
targets: Vec<PaneInfo>,
|
|
791
|
+
tmux_endpoint: Option<String>,
|
|
768
792
|
}
|
|
769
793
|
|
|
770
794
|
impl RecordingTransport {
|
|
@@ -789,6 +813,11 @@ impl RecordingTransport {
|
|
|
789
813
|
self
|
|
790
814
|
}
|
|
791
815
|
|
|
816
|
+
fn with_tmux_endpoint(self, endpoint: impl Into<String>) -> Self {
|
|
817
|
+
self.with_state(|state| state.tmux_endpoint = Some(endpoint.into()));
|
|
818
|
+
self
|
|
819
|
+
}
|
|
820
|
+
|
|
792
821
|
fn inject_targets(&self) -> Vec<Target> {
|
|
793
822
|
self.with_state(|state| state.inject_targets.clone())
|
|
794
823
|
}
|
|
@@ -812,6 +841,10 @@ impl Transport for RecordingTransport {
|
|
|
812
841
|
BackendKind::Tmux
|
|
813
842
|
}
|
|
814
843
|
|
|
844
|
+
fn tmux_endpoint(&self) -> Option<String> {
|
|
845
|
+
self.with_state(|state| state.tmux_endpoint.clone())
|
|
846
|
+
}
|
|
847
|
+
|
|
815
848
|
fn spawn_first(
|
|
816
849
|
&self,
|
|
817
850
|
session: &SessionName,
|
|
@@ -875,8 +908,18 @@ impl Transport for RecordingTransport {
|
|
|
875
908
|
})
|
|
876
909
|
}
|
|
877
910
|
|
|
878
|
-
fn query(&self, _target: &Target,
|
|
879
|
-
|
|
911
|
+
fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
|
|
912
|
+
match field {
|
|
913
|
+
PaneField::PaneCurrentCommand => Ok(Some("codex".to_string())),
|
|
914
|
+
PaneField::PaneCurrentPath => Ok(self.with_state(|state| {
|
|
915
|
+
state
|
|
916
|
+
.targets
|
|
917
|
+
.first()
|
|
918
|
+
.and_then(|target| target.current_path.as_ref())
|
|
919
|
+
.map(|path| path.to_string_lossy().into_owned())
|
|
920
|
+
})),
|
|
921
|
+
_ => Ok(None),
|
|
922
|
+
}
|
|
880
923
|
}
|
|
881
924
|
|
|
882
925
|
fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
@@ -95,6 +95,103 @@ fn worker_spawn_inherits_parent_process_env_for_proxy_and_ca() {
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
#[test]
|
|
99
|
+
#[serial(env)]
|
|
100
|
+
fn cursor_subscription_proxy_profile_reaches_fresh_and_restart_transport_final_env() {
|
|
101
|
+
let _guard = EnvGuard::set([
|
|
102
|
+
("HTTPS_PROXY", "https://upper-https:1"),
|
|
103
|
+
("HTTP_PROXY", "http://upper-http:1"),
|
|
104
|
+
("ALL_PROXY", "socks5://upper-all:1"),
|
|
105
|
+
("https_proxy", "https://lower-https:1"),
|
|
106
|
+
("http_proxy", "http://lower-http:1"),
|
|
107
|
+
("all_proxy", "socks5://lower-all:1"),
|
|
108
|
+
("NO_PROXY", "localhost,127.0.0.1"),
|
|
109
|
+
("no_proxy", "localhost,127.0.0.1"),
|
|
110
|
+
("GLOBAL_AGENT_HTTPS_PROXY", "https://global-https:1"),
|
|
111
|
+
("GLOBAL_AGENT_HTTP_PROXY", "http://global-http:1"),
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
let launch_direct = compiled_cursor_profile_team("cursor-direct-launch", "direct");
|
|
115
|
+
let launch_transport = RecordingTransport::new().with_session_present(false);
|
|
116
|
+
launch_with_transport(
|
|
117
|
+
&launch_direct.join("team.spec.yaml"),
|
|
118
|
+
false,
|
|
119
|
+
true,
|
|
120
|
+
true,
|
|
121
|
+
&launch_transport,
|
|
122
|
+
)
|
|
123
|
+
.expect("direct Cursor launch fixture should spawn");
|
|
124
|
+
assert_cursor_proxy_spawn(&launch_transport.single_spawn(), true);
|
|
125
|
+
|
|
126
|
+
let restart_direct = compiled_cursor_profile_team("cursor-direct-restart", "direct");
|
|
127
|
+
seed_restart_state_with_profile(&restart_direct, "worker_a", "cursor_proxy");
|
|
128
|
+
let restart_transport = RecordingTransport::new().with_session_present(false);
|
|
129
|
+
restart_with_transport(&restart_direct, true, None, &restart_transport)
|
|
130
|
+
.expect("direct Cursor restart fixture should spawn");
|
|
131
|
+
assert_cursor_proxy_spawn(&restart_transport.single_spawn(), true);
|
|
132
|
+
|
|
133
|
+
let launch_inherit = compiled_cursor_profile_team("cursor-inherit-launch", "inherit");
|
|
134
|
+
let inherit_transport = RecordingTransport::new().with_session_present(false);
|
|
135
|
+
launch_with_transport(
|
|
136
|
+
&launch_inherit.join("team.spec.yaml"),
|
|
137
|
+
false,
|
|
138
|
+
true,
|
|
139
|
+
true,
|
|
140
|
+
&inherit_transport,
|
|
141
|
+
)
|
|
142
|
+
.expect("inherit Cursor launch fixture should spawn");
|
|
143
|
+
assert_cursor_proxy_spawn(&inherit_transport.single_spawn(), false);
|
|
144
|
+
|
|
145
|
+
let restart_inherit = compiled_cursor_profile_team("cursor-inherit-restart", "inherit");
|
|
146
|
+
seed_restart_state_with_profile(&restart_inherit, "worker_a", "cursor_proxy");
|
|
147
|
+
let restart_inherit_transport = RecordingTransport::new().with_session_present(false);
|
|
148
|
+
restart_with_transport(
|
|
149
|
+
&restart_inherit,
|
|
150
|
+
true,
|
|
151
|
+
None,
|
|
152
|
+
&restart_inherit_transport,
|
|
153
|
+
)
|
|
154
|
+
.expect("inherit Cursor restart fixture should spawn");
|
|
155
|
+
assert_cursor_proxy_spawn(&restart_inherit_transport.single_spawn(), false);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn assert_cursor_proxy_spawn(spawn: &RecordedSpawn, direct: bool) {
|
|
159
|
+
let keys = [
|
|
160
|
+
"HTTPS_PROXY",
|
|
161
|
+
"HTTP_PROXY",
|
|
162
|
+
"ALL_PROXY",
|
|
163
|
+
"https_proxy",
|
|
164
|
+
"http_proxy",
|
|
165
|
+
"all_proxy",
|
|
166
|
+
"GLOBAL_AGENT_HTTPS_PROXY",
|
|
167
|
+
"GLOBAL_AGENT_HTTP_PROXY",
|
|
168
|
+
];
|
|
169
|
+
for key in keys {
|
|
170
|
+
if direct {
|
|
171
|
+
assert!(!spawn.env.contains_key(key), "direct env must omit {key}");
|
|
172
|
+
assert!(
|
|
173
|
+
spawn.env_unset.iter().any(|candidate| candidate == key),
|
|
174
|
+
"direct env_unset must contain {key}; got {:?}",
|
|
175
|
+
spawn.env_unset
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
assert!(spawn.env.contains_key(key), "inherit env must preserve {key}");
|
|
179
|
+
assert!(
|
|
180
|
+
spawn.env_unset.iter().all(|candidate| candidate != key),
|
|
181
|
+
"inherit env_unset must not contain {key}; got {:?}",
|
|
182
|
+
spawn.env_unset
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for key in ["NO_PROXY", "no_proxy"] {
|
|
187
|
+
assert!(spawn.env.contains_key(key), "NO_PROXY bypass key must remain: {key}");
|
|
188
|
+
assert!(
|
|
189
|
+
spawn.env_unset.iter().all(|candidate| candidate != key),
|
|
190
|
+
"NO_PROXY bypass key must not be unset: {key}"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
98
195
|
#[test]
|
|
99
196
|
fn tmux_control_filter_uses_env_keys_not_substring_in_inherited_values() {
|
|
100
197
|
let mut env = BTreeMap::from([
|
|
@@ -410,6 +507,39 @@ fn compiled_team_dir(label: &str, agents: &[(&str, &str)]) -> PathBuf {
|
|
|
410
507
|
compiled_team_dir_for_provider(label, agents, "codex")
|
|
411
508
|
}
|
|
412
509
|
|
|
510
|
+
fn compiled_cursor_profile_team(label: &str, proxy_mode: &str) -> PathBuf {
|
|
511
|
+
let team = tmp_dir(label).join("teamdir");
|
|
512
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
513
|
+
std::fs::write(
|
|
514
|
+
team.join("TEAM.md"),
|
|
515
|
+
"---\nname: envteam\nobjective: Cursor proxy environment contract.\nprovider: cursor_agent\n---\n\nTeam.\n",
|
|
516
|
+
)
|
|
517
|
+
.unwrap();
|
|
518
|
+
std::fs::write(
|
|
519
|
+
team.join("agents").join("worker_a.md"),
|
|
520
|
+
"---\nname: worker_a\nrole: Cursor Worker\nprovider: cursor_agent\nmodel: sonnet-4-thinking\nauth_mode: subscription\nprofile: cursor_proxy\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nCursor worker.\n",
|
|
521
|
+
)
|
|
522
|
+
.unwrap();
|
|
523
|
+
for directory in [
|
|
524
|
+
team.join("profiles"),
|
|
525
|
+
team.join(".team/current/profiles"),
|
|
526
|
+
] {
|
|
527
|
+
std::fs::create_dir_all(&directory).unwrap();
|
|
528
|
+
std::fs::write(
|
|
529
|
+
directory.join("cursor_proxy.env"),
|
|
530
|
+
format!("AUTH_MODE=subscription\nPROFILE_NAME=cursor_proxy\nPROXY_MODE={proxy_mode}\n"),
|
|
531
|
+
)
|
|
532
|
+
.unwrap();
|
|
533
|
+
}
|
|
534
|
+
let spec = team_agent::compiler::compile_team(&team).unwrap();
|
|
535
|
+
std::fs::write(
|
|
536
|
+
team.join("team.spec.yaml"),
|
|
537
|
+
team_agent::model::yaml::dumps(&spec),
|
|
538
|
+
)
|
|
539
|
+
.unwrap();
|
|
540
|
+
team
|
|
541
|
+
}
|
|
542
|
+
|
|
413
543
|
fn compiled_team_dir_for_provider(label: &str, agents: &[(&str, &str)], provider: &str) -> PathBuf {
|
|
414
544
|
let team = tmp_dir(label).join("teamdir");
|
|
415
545
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
@@ -450,6 +580,13 @@ fn seed_restart_state(team: &Path, agent_id: &str) {
|
|
|
450
580
|
seed_restart_state_as(team, agent_id, "codex");
|
|
451
581
|
}
|
|
452
582
|
|
|
583
|
+
fn seed_restart_state_with_profile(team: &Path, agent_id: &str, profile: &str) {
|
|
584
|
+
seed_restart_state_as(team, agent_id, "cursor_agent");
|
|
585
|
+
let mut state = team_agent::state::persist::load_runtime_state(team).unwrap();
|
|
586
|
+
state["agents"][agent_id]["profile"] = json!(profile);
|
|
587
|
+
team_agent::state::persist::save_runtime_state(team, &state).unwrap();
|
|
588
|
+
}
|
|
589
|
+
|
|
453
590
|
fn seed_restart_state_as(team: &Path, agent_id: &str, provider: &str) {
|
|
454
591
|
team_agent::state::persist::save_runtime_state(
|
|
455
592
|
team,
|
|
@@ -544,6 +544,8 @@ pub struct LaunchReport {
|
|
|
544
544
|
pub permissions: Vec<PermissionSummary>,
|
|
545
545
|
/// leader receiver(attach 成功时;经 step10 leader::attach_leader_to_state)。
|
|
546
546
|
pub leader_receiver_attached: bool,
|
|
547
|
+
pub leader_bind_stage: Option<String>,
|
|
548
|
+
pub leader_bind_reason: Option<String>,
|
|
547
549
|
pub session_capture_incomplete_agents: Vec<String>,
|
|
548
550
|
}
|
|
549
551
|
|
|
@@ -619,6 +621,7 @@ pub enum QuickStartReport {
|
|
|
619
621
|
/// [`QuickStartReadiness::PendingToolLoad`] (or `Degraded` if any agent
|
|
620
622
|
/// failed to spawn) so the CLI surface cannot lie about availability.
|
|
621
623
|
worker_readiness: QuickStartReadiness,
|
|
624
|
+
team: String,
|
|
622
625
|
},
|
|
623
626
|
/// 已有 runtime state → quick-start 拒绝并引导用 restart。
|
|
624
627
|
ExistingRuntime {
|
|
@@ -627,6 +630,7 @@ pub enum QuickStartReport {
|
|
|
627
630
|
state_path: Option<PathBuf>,
|
|
628
631
|
next_actions: Vec<String>,
|
|
629
632
|
attach_commands: Vec<String>,
|
|
633
|
+
agent_ids: Vec<String>,
|
|
630
634
|
},
|
|
631
635
|
/// preflight 阻塞(`quick_start.py:59`)。
|
|
632
636
|
PreflightBlocked {
|
|
@@ -799,6 +803,8 @@ pub enum RestartReport {
|
|
|
799
803
|
attach_commands: Vec<String>,
|
|
800
804
|
/// Debt returned by this restart's best-effort auto-attach, if any.
|
|
801
805
|
attach_window_failures: Option<serde_json::Value>,
|
|
806
|
+
leader_bind_ok: bool,
|
|
807
|
+
leader_bind_reason: Option<String>,
|
|
802
808
|
},
|
|
803
809
|
/// At least one worker failed during live spawn, but other workers were isolated
|
|
804
810
|
/// and restarted. The CLI reports `status=partial` and exits non-zero.
|
|
@@ -815,6 +821,8 @@ pub enum RestartReport {
|
|
|
815
821
|
attach_commands: Vec<String>,
|
|
816
822
|
/// Debt returned by this restart's best-effort auto-attach, if any.
|
|
817
823
|
attach_window_failures: Option<serde_json::Value>,
|
|
824
|
+
leader_bind_ok: bool,
|
|
825
|
+
leader_bind_reason: Option<String>,
|
|
818
826
|
},
|
|
819
827
|
/// All workers failed during live spawn. No worker is reported as restarted.
|
|
820
828
|
Failed {
|