@team-agent/installer 0.5.119 → 0.5.121
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/README.md +46 -1
- package/README.zh.md +46 -1
- package/crates/team-agent/src/cli/adapters.rs +11 -0
- package/crates/team-agent/src/cli/arguments.rs +401 -0
- package/crates/team-agent/src/cli/emit.rs +241 -142
- package/crates/team-agent/src/cli/intent/runtime.rs +1175 -0
- package/crates/team-agent/src/cli/intent/runtime_tests.rs +698 -0
- package/crates/team-agent/src/cli/intent/tests.rs +654 -0
- package/crates/team-agent/src/cli/intent.rs +642 -0
- package/crates/team-agent/src/cli/mod.rs +53 -6
- package/crates/team-agent/src/cli/named_address.rs +10 -1
- package/crates/team-agent/src/cli/spec.rs +22 -10
- package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
- package/crates/team-agent/src/cli/tests/base.rs +6 -1
- package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
- package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +65 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
- package/crates/team-agent/src/lifecycle/launch.rs +29 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +7 -1
- package/crates/team-agent/src/lifecycle/restart.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +64 -19
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +34 -10
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +47 -4
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
- package/crates/team-agent/src/lifecycle/types.rs +10 -0
- package/crates/team-agent/src/mcp_server/mod.rs +2 -0
- package/crates/team-agent/src/mcp_server/readiness.rs +432 -0
- package/crates/team-agent/src/mcp_server/readiness_tests.rs +242 -0
- package/crates/team-agent/src/mcp_server/wire.rs +96 -11
- package/crates/team-agent/src/messaging/delivery.rs +62 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +41 -1
- package/crates/team-agent/src/provider/adapter.rs +7 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
- package/crates/team-agent/src/tmux_backend.rs +8 -1
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +71 -54
- package/skills/team-agent/command-coverage.json +283 -13
- package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
- package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
- package/skills/team-agent/references/recovery-runbook.md +61 -212
- package/skills/team-agent/references/team-agent-operator.md +181 -48
- package/skills/team-agent/references/team-in-team.md +47 -80
|
@@ -944,52 +944,56 @@ fn preflight_fresh_leader_identity(
|
|
|
944
944
|
.ok()
|
|
945
945
|
.filter(|pane| !pane.is_empty());
|
|
946
946
|
let current_endpoint = crate::tmux_backend::socket_name_from_tmux_env();
|
|
947
|
-
|
|
947
|
+
let refusal = |stage: &str, reason: &str, action: &str| {
|
|
948
|
+
Some((
|
|
949
|
+
format!("quick-start refused before spawn: {}", reason.replace('_', " ")),
|
|
950
|
+
vec![format!("stage={stage} reason={reason}")],
|
|
951
|
+
vec![action.to_string()],
|
|
952
|
+
))
|
|
953
|
+
};
|
|
954
|
+
let resolution = crate::layout::worker_env::caller_provider_resolution(
|
|
948
955
|
pane.as_deref(),
|
|
949
956
|
current_endpoint.as_deref(),
|
|
950
957
|
transport.tmux_endpoint().as_deref(),
|
|
951
|
-
)
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
"quick-start refused before spawn: command unobservable".to_string(),
|
|
973
|
-
vec!["stage=command_observation reason=command_unobservable".to_string()],
|
|
974
|
-
vec!["run from a live provider pane; do not run claim-leader".to_string()],
|
|
975
|
-
));
|
|
976
|
-
}
|
|
977
|
-
if crate::leader::owner_bind::strict_owner_bind_provider(explicit.as_deref(), &command)
|
|
978
|
-
.is_some()
|
|
979
|
-
{
|
|
980
|
-
None
|
|
981
|
-
} else {
|
|
982
|
-
Some((
|
|
983
|
-
"quick-start refused before spawn: provider unresolved".to_string(),
|
|
984
|
-
vec!["stage=strict_provider reason=provider_unresolved".to_string()],
|
|
985
|
-
vec![
|
|
986
|
-
"set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader"
|
|
987
|
-
.to_string(),
|
|
988
|
-
],
|
|
989
|
-
))
|
|
990
|
-
}
|
|
958
|
+
);
|
|
959
|
+
if matches!(resolution, crate::layout::worker_env::CallerProviderResolution::Invalid) {
|
|
960
|
+
return refusal("strict_provider", "invalid_caller_tuple",
|
|
961
|
+
"fix conflicting CALLER_* / TEAM_AGENT_LEADER_* identity; do not run claim-leader");
|
|
962
|
+
}
|
|
963
|
+
let Some(pane) = pane else {
|
|
964
|
+
return refusal("caller_pane", "caller_pane_missing",
|
|
965
|
+
"run start from the intended live Leader provider pane; no workers or coordinator were started");
|
|
966
|
+
};
|
|
967
|
+
let pane = PaneId::new(pane);
|
|
968
|
+
let command = query_caller_pane_command(transport, &pane).unwrap_or_default();
|
|
969
|
+
if command.trim().is_empty() {
|
|
970
|
+
return refusal("command_observation", "command_unobservable",
|
|
971
|
+
"run from a live provider pane; do not run claim-leader");
|
|
972
|
+
}
|
|
973
|
+
if matches!(resolution, crate::layout::worker_env::CallerProviderResolution::Absent) {
|
|
974
|
+
let explicit = std::env::var("TEAM_AGENT_LEADER_PROVIDER")
|
|
975
|
+
.ok().filter(|provider| !provider.is_empty());
|
|
976
|
+
if crate::leader::owner_bind::strict_owner_bind_provider(explicit.as_deref(), &command).is_none() {
|
|
977
|
+
return refusal("strict_provider", "provider_unresolved",
|
|
978
|
+
"set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader");
|
|
991
979
|
}
|
|
992
980
|
}
|
|
981
|
+
// A typed caller tuple is identity evidence, not proof that its pane still
|
|
982
|
+
// exists. Use the caller's endpoint, never the worker server's same %id.
|
|
983
|
+
let targets = match list_caller_targets(transport) {
|
|
984
|
+
Ok(targets) => targets,
|
|
985
|
+
Err(_) => return refusal("caller_pane", "caller_pane_unobservable",
|
|
986
|
+
"inspect the intended Leader endpoint; no workers or coordinator were started"),
|
|
987
|
+
};
|
|
988
|
+
let Some(target) = targets.iter().find(|target| target.pane_id == pane) else {
|
|
989
|
+
return refusal("caller_pane", "caller_pane_not_live",
|
|
990
|
+
"run from the intended live Leader pane; no workers or coordinator were started");
|
|
991
|
+
};
|
|
992
|
+
if target.current_path.as_ref().is_none_or(|path| path.as_os_str().is_empty()) {
|
|
993
|
+
return refusal("caller_pane", "caller_cwd_unobservable",
|
|
994
|
+
"inspect the intended Leader pane workspace before retrying start");
|
|
995
|
+
}
|
|
996
|
+
None
|
|
993
997
|
}
|
|
994
998
|
|
|
995
999
|
fn bind_fresh_quick_start_leader(
|
|
@@ -1151,6 +1155,12 @@ pub fn quick_start_in_workspace_with_backend(
|
|
|
1151
1155
|
// team_key. When quick-start's downstream code runs, state.json
|
|
1152
1156
|
// still has whatever it had before (empty on fresh launch), so
|
|
1153
1157
|
// the spec-compile + worker-spawn path runs normally.
|
|
1158
|
+
let caller_transport = quick_start_tmux_backend(&workspace);
|
|
1159
|
+
if let Some((summary, blockers, next_actions)) = preflight_fresh_leader_identity(&caller_transport) {
|
|
1160
|
+
return Ok(QuickStartReport::PreflightBlocked {
|
|
1161
|
+
summary, blockers, next_actions, attach_commands: Vec::new(),
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1154
1164
|
let run_ws = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
1155
1165
|
let start_report =
|
|
1156
1166
|
crate::coordinator::health::start_coordinator_with_team(&run_ws, Some(team_key_str))
|
|
@@ -1508,46 +1518,53 @@ pub(crate) fn quick_start_with_transport_in_workspace_pi_preflight(
|
|
|
1508
1518
|
let quick_start_phase_timer = crate::lifecycle::restart::RestartPhaseTimer::start();
|
|
1509
1519
|
let mut launch =
|
|
1510
1520
|
launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1521
|
+
let mut coordinator_state = "not_attempted";
|
|
1522
|
+
let completion = (|| -> Result<(), LifecycleError> {
|
|
1523
|
+
annotate_persisted_team_depth(
|
|
1524
|
+
&workspace, &state_team_key,
|
|
1525
|
+
team_depth.parent_team_key.as_deref(), team_depth.team_depth,
|
|
1526
|
+
)?;
|
|
1527
|
+
// Recheck at the original bind/commit point: preflight is not a lease
|
|
1528
|
+
// over the caller pane. A lost caller must not start a coordinator.
|
|
1529
|
+
let _ = take_fresh_bind_refusal();
|
|
1530
|
+
launch.leader_receiver_attached = bind_fresh_quick_start_leader(
|
|
1531
|
+
&workspace, &state_team_key, seeded_owner.as_ref(),
|
|
1532
|
+
seeded_receiver.as_ref(), transport,
|
|
1533
|
+
)?;
|
|
1534
|
+
if !launch.leader_receiver_attached {
|
|
1535
|
+
if let Some((stage, reason)) = take_fresh_bind_refusal() {
|
|
1536
|
+
launch.leader_bind_stage = Some(stage.to_string());
|
|
1537
|
+
launch.leader_bind_reason = Some(reason.to_string());
|
|
1538
|
+
}
|
|
1539
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
1540
|
+
"Leader binding failed after worker launch: {}",
|
|
1541
|
+
launch.leader_bind_reason.as_deref().unwrap_or("unknown")
|
|
1542
|
+
)));
|
|
1532
1543
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
LifecycleError::StatePersist(format!(
|
|
1544
|
+
launch.session_capture_incomplete_agents =
|
|
1545
|
+
quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
|
|
1546
|
+
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
1547
|
+
coordinator_state = "attempted_unknown";
|
|
1548
|
+
let coordinator_report = crate::coordinator::start_coordinator(&coordinator_workspace)
|
|
1549
|
+
.map_err(|error| LifecycleError::StatePersist(format!(
|
|
1540
1550
|
"coordinator start failed for {}: {error}; inspect {} before retrying",
|
|
1541
1551
|
workspace.display(),
|
|
1542
1552
|
crate::coordinator::coordinator_log_path(&coordinator_workspace).display()
|
|
1543
|
-
))
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1553
|
+
)))?;
|
|
1554
|
+
quick_start_phase_timer.emit(
|
|
1555
|
+
&workspace, "launch.phase",
|
|
1556
|
+
crate::lifecycle::restart::LifecyclePhase::CoordinatorStart,
|
|
1557
|
+
);
|
|
1558
|
+
require_quick_start_coordinator_started(&coordinator_report, &workspace)?;
|
|
1559
|
+
coordinator_state = "running_confirmed";
|
|
1560
|
+
Ok(())
|
|
1561
|
+
})();
|
|
1562
|
+
if let Err(error) = completion {
|
|
1563
|
+
return Ok(QuickStartReport::Partial {
|
|
1564
|
+
workspace, session_name, launch: Box::new(launch), team: state_team_key,
|
|
1565
|
+
error: error.to_string(), coordinator_state: coordinator_state.to_string(),
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1551
1568
|
let coordinator_action = "coordinator started";
|
|
1552
1569
|
// BUG-7: build an honest readiness verdict from the post-spawn runtime state.
|
|
1553
1570
|
// - If persist_spawn_agent_state (BUG-2 fix) marked any agent non-running, the
|
|
@@ -1701,6 +1718,78 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
1701
1718
|
|
|
1702
1719
|
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
|
1703
1720
|
|
|
1721
|
+
#[test]
|
|
1722
|
+
#[serial_test::serial(env)]
|
|
1723
|
+
fn issue337_missing_caller_is_refused_before_any_launch_effect() {
|
|
1724
|
+
let env = HermeticTestEnv::enter("issue337-no-caller");
|
|
1725
|
+
env.scrub_tmux();
|
|
1726
|
+
env.assert_no_real_tmux();
|
|
1727
|
+
let transport = OfflineTransport::new();
|
|
1728
|
+
let refusal = preflight_fresh_leader_identity(&transport)
|
|
1729
|
+
.expect("a missing caller must not reach worker/coordinator spawn");
|
|
1730
|
+
assert!(refusal.1.iter().any(|reason| reason.contains("caller_pane_missing")));
|
|
1731
|
+
assert!(!env.home().join(".team").exists());
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
#[test]
|
|
1735
|
+
#[serial_test::serial(env)]
|
|
1736
|
+
fn issue337_typed_caller_still_requires_a_current_live_pane_observation() {
|
|
1737
|
+
let env = HermeticTestEnv::enter("issue337-unobservable-caller");
|
|
1738
|
+
env.scrub_tmux();
|
|
1739
|
+
env.assert_no_real_tmux();
|
|
1740
|
+
std::env::set_var("TMUX", "/private/tmp/issue337/default,1,0");
|
|
1741
|
+
std::env::set_var("TMUX_PANE", "%42");
|
|
1742
|
+
std::env::set_var(CALLER_PANE_ENV, "%42");
|
|
1743
|
+
std::env::set_var(CALLER_PROVIDER_ENV, "pi");
|
|
1744
|
+
std::env::set_var(CALLER_ENDPOINT_ENV, "/private/tmp/issue337/default");
|
|
1745
|
+
let transport = OfflineTransport::new().with_tmux_endpoint("/private/tmp/issue337/default");
|
|
1746
|
+
let refusal = preflight_fresh_leader_identity(&transport)
|
|
1747
|
+
.expect("typed identity is not a live pane observation");
|
|
1748
|
+
assert!(refusal.1.iter().any(|reason| reason.contains("unobservable") || reason.contains("not_live")));
|
|
1749
|
+
assert!(!env.home().join(".team").exists());
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
#[test]
|
|
1753
|
+
#[serial_test::serial(env)]
|
|
1754
|
+
fn issue337_fresh_start_without_caller_creates_no_runtime_or_worker() {
|
|
1755
|
+
let env = HermeticTestEnv::enter("issue337-zero-effects");
|
|
1756
|
+
env.scrub_tmux();
|
|
1757
|
+
env.assert_no_real_tmux();
|
|
1758
|
+
let workspace = env.workspace("ws");
|
|
1759
|
+
let team = workspace.join("roles");
|
|
1760
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
1761
|
+
std::fs::write(team.join("TEAM.md"), "---\nname: fresh\nobjective: test\nprovider: codex\n---\nTeam.\n").unwrap();
|
|
1762
|
+
std::fs::write(team.join("agents/worker.md"), "---\nname: worker\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\n---\nTest.\n").unwrap();
|
|
1763
|
+
let transport = OfflineTransport::new();
|
|
1764
|
+
let report = quick_start_with_transport_in_workspace(
|
|
1765
|
+
&workspace, &team, None, true, Some("fresh"), &transport,
|
|
1766
|
+
).unwrap();
|
|
1767
|
+
assert!(matches!(report, QuickStartReport::PreflightBlocked { .. }));
|
|
1768
|
+
assert!(transport.spawn_records().is_empty());
|
|
1769
|
+
assert!(!workspace.join(".team/team.db").exists());
|
|
1770
|
+
assert!(!crate::state::persist::runtime_state_path(&workspace).exists());
|
|
1771
|
+
assert!(!crate::model::paths::runtime_spec_path(&workspace, "fresh").exists());
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
#[test]
|
|
1775
|
+
#[serial_test::serial(env)]
|
|
1776
|
+
fn issue337_exact_live_typed_caller_passes_readonly_preflight() {
|
|
1777
|
+
let env = HermeticTestEnv::enter("issue337-positive");
|
|
1778
|
+
env.scrub_tmux();
|
|
1779
|
+
env.assert_no_real_tmux();
|
|
1780
|
+
let _tmux = env.with_env("TMUX", "/private/tmp/issue337/default,1,0");
|
|
1781
|
+
let _pane = env.with_env("TMUX_PANE", "%1");
|
|
1782
|
+
let _provider = env.with_env(CALLER_PROVIDER_ENV, "pi");
|
|
1783
|
+
let _caller = env.with_env(CALLER_PANE_ENV, "%1");
|
|
1784
|
+
let _endpoint = env.with_env(CALLER_ENDPOINT_ENV, "/private/tmp/issue337/default");
|
|
1785
|
+
let transport = OfflineTransport::new()
|
|
1786
|
+
.with_tmux_endpoint("/private/tmp/issue337/default")
|
|
1787
|
+
.with_pane_current_command("%1", "bash")
|
|
1788
|
+
.with_targets(vec![caller_target(env.workspace("caller"), None)]);
|
|
1789
|
+
assert!(preflight_fresh_leader_identity(&transport).is_none());
|
|
1790
|
+
assert!(transport.spawn_records().is_empty());
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1704
1793
|
#[test]
|
|
1705
1794
|
fn bootstrap_binding_is_preserved_without_epoch_reset_or_takeover() {
|
|
1706
1795
|
let bootstrap = json!({
|
|
@@ -36,6 +36,26 @@ use super::*;
|
|
|
36
36
|
|
|
37
37
|
// ── lifecycle::launch —— 冷启 / quick-start / 危险审批探测 ──────────────────
|
|
38
38
|
|
|
39
|
+
/// White-box producer/worker-engine fixture, not a public start admission path.
|
|
40
|
+
/// Keep unbound producer assertions separate from public caller admission.
|
|
41
|
+
#[cfg(test)]
|
|
42
|
+
pub(crate) fn materialize_unbound_state_for_test(
|
|
43
|
+
workspace: &Path, team_dir: &Path, team_key: &str,
|
|
44
|
+
transport: &crate::transport::test_support::OfflineTransport,
|
|
45
|
+
) -> Result<LaunchReport, LifecycleError> {
|
|
46
|
+
let spec = crate::compiler::compile_team(team_dir)
|
|
47
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
48
|
+
let path = crate::model::paths::runtime_spec_path(workspace, team_key);
|
|
49
|
+
spec_state::write_spec_atomic(&path, &spec)?;
|
|
50
|
+
let path = path.canonicalize().map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
51
|
+
let mut state = spec_state::initial_runtime_state(
|
|
52
|
+
&spec, &path, workspace, team_dir, team_key, transport.tmux_endpoint().as_deref(),
|
|
53
|
+
);
|
|
54
|
+
quick_start_transport::annotate_runtime_transport(&mut state, transport, workspace, None);
|
|
55
|
+
state_projection::save_launched_team_state_for_key(workspace, &state, Some(team_key), None)?;
|
|
56
|
+
launch_with_transport_in_workspace(workspace, &path, false, true, true, transport)
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
/// ---
|
|
40
60
|
/// purpose: 冷启全队的默认入口,按 spec 所在目录绑定 per-team tmux socket 后转 launch_with_transport
|
|
41
61
|
/// params:
|
|
@@ -355,6 +375,7 @@ pub(crate) fn run_pi_catalog_preflight(
|
|
|
355
375
|
}
|
|
356
376
|
|
|
357
377
|
mod cursor_mcp_iso;
|
|
378
|
+
pub(crate) use cursor_mcp_iso::cursor_mcp_primary_matches_scope;
|
|
358
379
|
pub use cursor_mcp_iso::{
|
|
359
380
|
cursor_mcp_isolation_enabled, cursor_mcp_json_path, cursor_mcp_project_dir,
|
|
360
381
|
materialize_cursor_mcp_project,
|
|
@@ -384,6 +405,14 @@ pub use worker_env::{apply_cursor_subscription_proxy_env, CursorProxyPresence};
|
|
|
384
405
|
mod identity;
|
|
385
406
|
pub(super) use identity::*;
|
|
386
407
|
|
|
408
|
+
// Read-only CLI surface facades; predicate bodies and launch behavior stay unchanged.
|
|
409
|
+
pub(crate) fn surface_has_runtime_team(state: &serde_json::Value, team: &str) -> bool {
|
|
410
|
+
identity::runtime_state_has_quick_start_team(state, team)
|
|
411
|
+
}
|
|
412
|
+
pub(crate) fn surface_is_leader_bootstrap(state: &serde_json::Value) -> bool {
|
|
413
|
+
identity::is_quick_start_leader_bootstrap(state)
|
|
414
|
+
}
|
|
415
|
+
|
|
387
416
|
// Test-only seam for the real leader producer -> unchanged bootstrap classifier.
|
|
388
417
|
#[cfg(test)]
|
|
389
418
|
pub(crate) fn quick_start_has_runtime_for_test(state: &serde_json::Value, team: &str) -> bool {
|
|
@@ -462,6 +462,9 @@ pub(super) fn spawn_agent_window(
|
|
|
462
462
|
&mut env,
|
|
463
463
|
)?;
|
|
464
464
|
}
|
|
465
|
+
// Keep provider/native cwd distinct from the real Team workspace. Cursor
|
|
466
|
+
// changes cwd to --workspace before writing its cwd-keyed session archive.
|
|
467
|
+
let mut native_spawn_cwd = spawn_cwd.to_path_buf();
|
|
465
468
|
// 0.5.67 Cursor 方案 1 变体: role 经 workspace rules 文件注入 (不 argv)。
|
|
466
469
|
if provider == crate::provider::Provider::CursorAgent {
|
|
467
470
|
crate::lifecycle::launch::refuse_second_cursor_occupant(
|
|
@@ -479,6 +482,9 @@ pub(super) fn spawn_agent_window(
|
|
|
479
482
|
agent_id.as_str(),
|
|
480
483
|
&mcp_config,
|
|
481
484
|
)?;
|
|
485
|
+
if let Some(primary) = project.as_ref() {
|
|
486
|
+
native_spawn_cwd = primary.clone();
|
|
487
|
+
}
|
|
482
488
|
crate::lifecycle::launch::enable_cursor_workspace_mcp_with_profile(
|
|
483
489
|
workspace,
|
|
484
490
|
project.as_deref(),
|
|
@@ -694,7 +700,7 @@ pub(super) fn spawn_agent_window(
|
|
|
694
700
|
plan,
|
|
695
701
|
profile_launch,
|
|
696
702
|
layout_placement: layout_placement.cloned(),
|
|
697
|
-
spawn_cwd:
|
|
703
|
+
spawn_cwd: native_spawn_cwd,
|
|
698
704
|
owner_team_id: team_id,
|
|
699
705
|
})
|
|
700
706
|
}
|
|
@@ -110,6 +110,23 @@ pub(crate) use rebuild::{
|
|
|
110
110
|
pub(crate) use remove::remove_agent_with_transport;
|
|
111
111
|
pub use remove::{remove_agent, remove_agent_flag_requirements};
|
|
112
112
|
pub use selection::decide_start_mode;
|
|
113
|
+
pub(crate) use selection::restart_agent_never_captured;
|
|
114
|
+
|
|
115
|
+
/// Read-only CLI preflight facade. Reuses the original provider-specific probe;
|
|
116
|
+
/// does not capture a session, write state, or authorize a future spawn.
|
|
117
|
+
pub(crate) fn surface_resume_backing_probe(
|
|
118
|
+
workspace: &Path,
|
|
119
|
+
agent_id: &AgentId,
|
|
120
|
+
agent: &serde_json::Value,
|
|
121
|
+
provider: Provider,
|
|
122
|
+
session: &SessionId,
|
|
123
|
+
rollout: Option<&RolloutPath>,
|
|
124
|
+
) -> (bool, Vec<std::path::PathBuf>) {
|
|
125
|
+
let probe = common::resume_backing_probe_for_agent(
|
|
126
|
+
workspace, agent_id, agent, provider, session, rollout,
|
|
127
|
+
);
|
|
128
|
+
(probe.exists, probe.checked_paths)
|
|
129
|
+
}
|
|
113
130
|
pub(crate) use selection::{classify_first_send_at, classify_restart_plan, python_type_name};
|
|
114
131
|
// Layer 2 (leader follow-up 2026-06-22): test-visible workspace-aware
|
|
115
132
|
// classification so lifecycle/tests/restart.rs can exercise the
|
|
@@ -147,9 +147,7 @@ gate — N35/MUST-17); found {hits} occurrence(s)"
|
|
|
147
147
|
#[test]
|
|
148
148
|
#[serial(env)]
|
|
149
149
|
fn b2_quick_start_ready_has_attach_commands_and_socket_miss_is_observable() {
|
|
150
|
-
let
|
|
151
|
-
hermetic.scrub_tmux();
|
|
152
|
-
hermetic.assert_no_real_tmux();
|
|
150
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("acceptance-b-b2-quick-start");
|
|
153
151
|
let _g = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
154
152
|
let ws = tmp_ws("b2-attach");
|
|
155
153
|
let team_dir = write_min_team(&ws, "b2team");
|
|
@@ -54,9 +54,7 @@ const NEUTRAL_ANCESTRY: &str = "[\"/bin/zsh\"]";
|
|
|
54
54
|
#[test]
|
|
55
55
|
#[serial(env)]
|
|
56
56
|
fn d1_d2_d3_codex_argv_matches_python_golden_escape_timeout_profile() {
|
|
57
|
-
let
|
|
58
|
-
hermetic.scrub_tmux();
|
|
59
|
-
hermetic.assert_no_real_tmux();
|
|
57
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("behavioral-d123-codex");
|
|
60
58
|
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
61
59
|
let ws = tmp_dir("d123-codex-argv");
|
|
62
60
|
let team_dir = write_team(
|
|
@@ -181,9 +179,7 @@ fn d1_d2_d3_codex_argv_matches_python_golden_escape_timeout_profile() {
|
|
|
181
179
|
#[test]
|
|
182
180
|
#[serial(env)]
|
|
183
181
|
fn d4_worker_shell_env_sets_team_agent_id_to_worker_self() {
|
|
184
|
-
let
|
|
185
|
-
hermetic.scrub_tmux();
|
|
186
|
-
hermetic.assert_no_real_tmux();
|
|
182
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("behavioral-d4-shell-env");
|
|
187
183
|
let _guard = EnvGuard::set(&[
|
|
188
184
|
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
189
185
|
("TEAM_AGENT_ID", "stale-mcp-caller"),
|
|
@@ -224,9 +220,7 @@ fn d4_worker_shell_env_sets_team_agent_id_to_worker_self() {
|
|
|
224
220
|
#[test]
|
|
225
221
|
#[serial(env)]
|
|
226
222
|
fn d5_fresh_launch_spawn_cwd_and_state_use_workspace() {
|
|
227
|
-
let
|
|
228
|
-
hermetic.scrub_tmux();
|
|
229
|
-
hermetic.assert_no_real_tmux();
|
|
223
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("behavioral-d5-spawn-cwd");
|
|
230
224
|
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
231
225
|
let ws = tmp_dir("d5-spawn-cwd");
|
|
232
226
|
let team_dir = write_team(&ws, &simple_codex_team("d5team", "worker_a", "Cwd Worker"));
|
|
@@ -268,9 +262,7 @@ fn d5_fresh_launch_spawn_cwd_and_state_use_workspace() {
|
|
|
268
262
|
#[test]
|
|
269
263
|
#[serial(env)]
|
|
270
264
|
fn d6_compiled_system_prompt_first_section_is_identity() {
|
|
271
|
-
let
|
|
272
|
-
hermetic.scrub_tmux();
|
|
273
|
-
hermetic.assert_no_real_tmux();
|
|
265
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("behavioral-d6-prompt");
|
|
274
266
|
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
275
267
|
let ws = tmp_dir("d6-prompt-order");
|
|
276
268
|
let team_dir = write_team(
|
|
@@ -323,9 +315,7 @@ chunks=[identity, TEAMMATE_SYSTEM_PROMPT, ...]; live ps confirms identity-first)
|
|
|
323
315
|
#[test]
|
|
324
316
|
#[serial(env)]
|
|
325
317
|
fn d7_runtime_fast_sends_codex_fast_mode_toggle() {
|
|
326
|
-
let
|
|
327
|
-
hermetic.scrub_tmux();
|
|
328
|
-
hermetic.assert_no_real_tmux();
|
|
318
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("behavioral-d7-fast-mode");
|
|
329
319
|
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
330
320
|
let ws = tmp_dir("d7-fast-mode");
|
|
331
321
|
let mut fixture = simple_codex_team("d7team", "worker_a", "Fast Worker");
|
|
@@ -32,10 +32,9 @@ use team_agent::transport::{
|
|
|
32
32
|
#[test]
|
|
33
33
|
#[serial(env)]
|
|
34
34
|
fn compatible_api_profile_writes_claude_settings_and_project_state_with_mcp_servers() {
|
|
35
|
-
let
|
|
36
|
-
hermetic.scrub_tmux();
|
|
37
|
-
hermetic.assert_no_real_tmux();
|
|
35
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("claude-compatible-config");
|
|
38
36
|
let ws = tmp_dir("compatible-config-files");
|
|
37
|
+
super::launch_spawn::seed_healthy_coordinator(&ws);
|
|
39
38
|
let team = write_claude_profile_team(&ws, "cfgteam", "clauder");
|
|
40
39
|
let transport = RecordingTransport::default();
|
|
41
40
|
|
|
@@ -34,12 +34,11 @@ use team_agent::transport::{
|
|
|
34
34
|
#[test]
|
|
35
35
|
#[serial(env)]
|
|
36
36
|
fn compatible_api_profile_quick_start_spawns_claude_with_profile_env_and_state_metadata() {
|
|
37
|
-
let
|
|
38
|
-
hermetic.scrub_tmux();
|
|
39
|
-
hermetic.assert_no_real_tmux();
|
|
37
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("claude-profile-quick-start");
|
|
40
38
|
assert_s0_profile_launch_shape();
|
|
41
39
|
|
|
42
40
|
let ws = tmp_dir("profile-launch");
|
|
41
|
+
super::launch_spawn::seed_healthy_coordinator(&ws);
|
|
43
42
|
let team = write_claude_profile_team(&ws, "claudeprof", "clauder");
|
|
44
43
|
let transport = RecordingTransport::default();
|
|
45
44
|
|
|
@@ -46,7 +46,7 @@ struct Fixture {
|
|
|
46
46
|
}
|
|
47
47
|
impl Fixture {
|
|
48
48
|
fn new(tag: &str) -> Self {
|
|
49
|
-
let env = hermetic::HermeticTestEnv::
|
|
49
|
+
let env = hermetic::HermeticTestEnv::enter_with_leader(tag);
|
|
50
50
|
let ws = env.workspace("workspace");
|
|
51
51
|
let team = ws.join("roles");
|
|
52
52
|
fs::create_dir_all(team.join("agents")).unwrap();
|
|
@@ -68,7 +68,12 @@ impl Fixture {
|
|
|
68
68
|
let real_pi = env.root().join("pi-real");
|
|
69
69
|
executable(&real_pi, &script);
|
|
70
70
|
std::os::unix::fs::symlink(&real_pi, bin.join("pi")).unwrap();
|
|
71
|
-
|
|
71
|
+
let caller = std::env::var("TMUX").expect("owned fixture caller").split(',').next().unwrap().to_string();
|
|
72
|
+
let native_tmux = hermetic::fixture_tmux_binary();
|
|
73
|
+
executable(&bin.join("tmux"), &format!(
|
|
74
|
+
"#!/bin/sh\nwant_socket=0\nfor argument in \"$@\"; do\n if [ \"$want_socket\" = 1 ]; then\n if [ \"$argument\" = {} ]; then exec {} \"$@\"; fi\n break\n fi\n if [ \"$argument\" = -S ]; then want_socket=1; fi\ndone\ncase \"$*\" in\n*-V*) echo 'tmux 3.4';;\n*has-session*) exit 1;;\n*list-sessions*|*list-windows*|*list-panes*) exit 0;;\n*) exit 0;;\nesac\n",
|
|
75
|
+
crate::cli::shell_quote(&caller), crate::cli::shell_quote(&native_tmux.to_string_lossy()),
|
|
76
|
+
));
|
|
72
77
|
let guards = vec![
|
|
73
78
|
env.with_env("PATH", &format!("{}:/usr/bin:/bin", bin.display())),
|
|
74
79
|
env.with_env(SWITCH, "off"),
|
|
@@ -1219,6 +1224,9 @@ fn a13_materializer_version_catalog_and_nonagent_command_builders_are_not_routed
|
|
|
1219
1224
|
#[serial(env)]
|
|
1220
1225
|
fn a07_attach_existing_never_opens_enabled_invalid_route_config() {
|
|
1221
1226
|
let f = Fixture::new("argv-attach");
|
|
1227
|
+
// This planner-only case explicitly models attach from outside tmux,
|
|
1228
|
+
// not the owned caller used by positive worker-start fixtures.
|
|
1229
|
+
f.env.scrub_tmux();
|
|
1222
1230
|
f.raw("invalid JSON");
|
|
1223
1231
|
f.switch(Some("on"));
|
|
1224
1232
|
let plan = f.leader(Provider::Pi, true, true).unwrap();
|
|
@@ -54,7 +54,7 @@ struct Case {
|
|
|
54
54
|
impl Case {
|
|
55
55
|
fn start() -> Self {
|
|
56
56
|
ensure_team_agent_cli();
|
|
57
|
-
let env = HermeticTestEnv::
|
|
57
|
+
let env = HermeticTestEnv::enter_with_leader("g1ct");
|
|
58
58
|
let workspace = env.workspace("ws");
|
|
59
59
|
write_team_docs(&workspace);
|
|
60
60
|
Self { env, workspace }
|
|
@@ -134,6 +134,12 @@ impl Case {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
impl Drop for Case {
|
|
138
|
+
fn drop(&mut self) {
|
|
139
|
+
self.shutdown();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
/// `cargo test --lib` does not build `bin/team-agent`. Hermetic CLI
|
|
138
144
|
/// lookup then panics and impl_check.sh sees 1 FAILED. Build the bin
|
|
139
145
|
/// into the same CARGO_TARGET_DIR the lib test already uses.
|
|
@@ -338,6 +344,7 @@ fn clone_ok(case: &Case, source: &str, dest: &str, phase: &str) {
|
|
|
338
344
|
|
|
339
345
|
/// clone 后分身 role 保留源席 tools;编译 spec 不投影 tools。
|
|
340
346
|
#[test]
|
|
347
|
+
#[serial_test::serial(env)]
|
|
341
348
|
fn clone_agent_preserves_source_tools() {
|
|
342
349
|
let case = Case::start();
|
|
343
350
|
let qs = case.run(&[
|
|
@@ -168,7 +168,7 @@ impl Case {
|
|
|
168
168
|
/// product's existing test seam so the derived approval policy is a
|
|
169
169
|
/// function of the FIXTURE, never of the host process tree.
|
|
170
170
|
fn start_with_ancestry(tag: &str, ancestry_argv: &[&str]) -> Self {
|
|
171
|
-
let env = HermeticTestEnv::
|
|
171
|
+
let env = HermeticTestEnv::enter_with_leader(tag);
|
|
172
172
|
let workspace = env.workspace("ws");
|
|
173
173
|
write_team_docs(&workspace, "claude");
|
|
174
174
|
let shim_dir = write_claude_backing_shim(&workspace);
|
|
@@ -500,8 +500,9 @@ fn r8_copilot_fork_is_no_longer_capability_refused() {
|
|
|
500
500
|
// the capability gate — a CLI-shaped R8 could never be RED. An in-process
|
|
501
501
|
// RecordingTransport stands the team up without a live copilot TUI (no
|
|
502
502
|
// ready-screen hang).
|
|
503
|
-
let env = HermeticTestEnv::
|
|
503
|
+
let env = HermeticTestEnv::enter_with_leader("cf-r8");
|
|
504
504
|
let workspace = env.workspace("ws");
|
|
505
|
+
super::launch_spawn::seed_healthy_coordinator(&workspace);
|
|
505
506
|
let team_dir = write_copilot_team_docs(&workspace);
|
|
506
507
|
// Session ABSENT at quick-start so it can create the team session; spawn_first
|
|
507
508
|
// flips session_present true, so the later fork sees a live session. (Starting
|
|
@@ -31,10 +31,9 @@ const CONTRACT_HEADING_PREFIX: &str = "# Team Agent communication contract: ";
|
|
|
31
31
|
#[test]
|
|
32
32
|
#[serial_test::serial(env)]
|
|
33
33
|
fn t06_each_official_mode_selects_one_runtime_contract_in_real_spawn_prompt() {
|
|
34
|
-
let
|
|
35
|
-
hermetic.scrub_tmux();
|
|
36
|
-
hermetic.assert_no_real_tmux();
|
|
34
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("communication-mode-official");
|
|
37
35
|
let root = fixture("official", None);
|
|
36
|
+
super::launch_spawn::seed_healthy_coordinator(&root);
|
|
38
37
|
let transport = RecordingTransport::default();
|
|
39
38
|
quick_start_with_transport_in_workspace(
|
|
40
39
|
&root,
|
|
@@ -87,10 +86,9 @@ fn t06_each_official_mode_selects_one_runtime_contract_in_real_spawn_prompt() {
|
|
|
87
86
|
#[test]
|
|
88
87
|
#[serial_test::serial(env)]
|
|
89
88
|
fn t06_selected_templates_project_the_two_signed_communication_boundaries() {
|
|
90
|
-
let
|
|
91
|
-
hermetic.scrub_tmux();
|
|
92
|
-
hermetic.assert_no_real_tmux();
|
|
89
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("communication-mode-semantics");
|
|
93
90
|
let root = fixture("semantics", None);
|
|
91
|
+
super::launch_spawn::seed_healthy_coordinator(&root);
|
|
94
92
|
let transport = RecordingTransport::default();
|
|
95
93
|
quick_start_with_transport_in_workspace(
|
|
96
94
|
&root,
|
|
@@ -141,10 +139,9 @@ fn t06_selected_templates_project_the_two_signed_communication_boundaries() {
|
|
|
141
139
|
#[test]
|
|
142
140
|
#[serial_test::serial(env)]
|
|
143
141
|
fn t07_final_spawn_prompt_does_not_leak_leader_centric_obligations_into_orchestrated() {
|
|
144
|
-
let
|
|
145
|
-
hermetic.scrub_tmux();
|
|
146
|
-
hermetic.assert_no_real_tmux();
|
|
142
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter_with_leader("communication-mode-negative-boundary");
|
|
147
143
|
let root = fixture("negative-boundary", None);
|
|
144
|
+
super::launch_spawn::seed_healthy_coordinator(&root);
|
|
148
145
|
let transport = RecordingTransport::default();
|
|
149
146
|
quick_start_with_transport_in_workspace(
|
|
150
147
|
&root,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Copilot provider contracts (
|
|
1
|
+
//! Copilot provider contracts (zero model sessions; owned physical caller fixture).
|
|
2
2
|
//!
|
|
3
3
|
//! Basis docs (sole truth sources — where the in-tree implementation deviates, these
|
|
4
4
|
//! contracts stay red):
|
|
@@ -32,10 +32,7 @@ mod hermetic_guard;
|
|
|
32
32
|
fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
|
|
33
33
|
|
|
34
34
|
fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
|
|
35
|
-
|
|
36
|
-
env.scrub_tmux();
|
|
37
|
-
env.assert_no_real_tmux();
|
|
38
|
-
env
|
|
35
|
+
hermetic_guard::HermeticTestEnv::enter_with_leader(tag)
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
use std::collections::{BTreeMap, BTreeSet};
|
|
@@ -875,6 +872,8 @@ fn copilot_mcp_residual_scan_unavailable_degrades_honestly_no_false_positive() {
|
|
|
875
872
|
let home = tmp_ws("residual-unavail-home");
|
|
876
873
|
std::fs::create_dir_all(home.join(".copilot")).unwrap();
|
|
877
874
|
let empty_bin = tmp_ws("no-copilot-bin");
|
|
875
|
+
#[cfg(unix)]
|
|
876
|
+
std::os::unix::fs::symlink(hermetic_guard::fixture_tmux_binary(), empty_bin.join("tmux")).unwrap();
|
|
878
877
|
let _guard = EnvGuard::set(&[
|
|
879
878
|
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
880
879
|
(
|
|
@@ -1942,6 +1941,9 @@ fn copilot_empty_mcp_list_shim(tag: &str) -> TempWorkspace {
|
|
|
1942
1941
|
#[cfg(unix)]
|
|
1943
1942
|
{
|
|
1944
1943
|
use std::os::unix::fs::PermissionsExt;
|
|
1944
|
+
// Only the explicit owned caller needs tmux; no provider is exposed by
|
|
1945
|
+
// this symlink and the worker transport remains the recording fixture.
|
|
1946
|
+
std::os::unix::fs::symlink(hermetic_guard::fixture_tmux_binary(), bin.join("tmux")).unwrap();
|
|
1945
1947
|
// coordinator health needs the seeded test PID's real non-zombie status.
|
|
1946
1948
|
// Keep PATH isolated and refuse any broader process inspection.
|
|
1947
1949
|
std::fs::write(
|