@team-agent/installer 0.5.74 → 0.5.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +366 -0
- package/crates/team-agent/src/leader/lease.rs +228 -32
- package/crates/team-agent/src/leader/registry.rs +177 -19
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +12 -1
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1820 -60
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
- package/crates/team-agent/src/lifecycle/restart/common.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
- package/crates/team-agent/src/messaging/delivery.rs +200 -29
- package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
- package/crates/team-agent/src/state/persist.rs +166 -6
- package/crates/team-agent/src/state/repository.rs +36 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +350 -7
- package/crates/team-agent/src/tmux_backend.rs +217 -26
- package/crates/team-agent/src/transport/test_support.rs +18 -3
- package/crates/team-agent/src/transport.rs +99 -0
- package/package.json +4 -4
|
@@ -277,6 +277,10 @@ pub(super) fn spawn_agents(
|
|
|
277
277
|
provider,
|
|
278
278
|
),
|
|
279
279
|
);
|
|
280
|
+
// The provider is the final typed value after profile overlays. Keep it
|
|
281
|
+
// separate from inherited LEADER_* identity and let the tmux invocation
|
|
282
|
+
// boundary add the actual pane/socket only for the provider process.
|
|
283
|
+
crate::layout::worker_env::inject_current_caller_provider(&mut env, provider);
|
|
280
284
|
// BUG / C-1-2 / C-6-1 cr verdict — Copilot system_prompt 走 spawn env overlay +
|
|
281
285
|
// per-worker AGENTS.md(B2 灵魂件降级):写
|
|
282
286
|
// <workspace>/.team/runtime/copilot-instructions/<agent_id>/AGENTS.md
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
//! sites can reference the phases by name. The phase fns themselves
|
|
52
52
|
//! remain in launch.rs until the next batch of relocations.
|
|
53
53
|
|
|
54
|
+
#[cfg(test)]
|
|
55
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
56
|
+
mod hermetic;
|
|
57
|
+
|
|
54
58
|
use std::collections::{BTreeMap, BTreeSet};
|
|
55
59
|
use std::path::{Path, PathBuf};
|
|
56
60
|
use std::process::Command;
|
|
@@ -66,16 +70,14 @@ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
|
66
70
|
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
67
71
|
|
|
68
72
|
use super::identity::spec_display_backend;
|
|
69
|
-
use super::leader_context::{
|
|
70
|
-
attributed_provider_for_pane_across_tmux_sockets, caller_provider_for_seed_with_lookup,
|
|
71
|
-
seed_unbound_launched_owner,
|
|
72
|
-
};
|
|
73
|
+
use super::leader_context::{caller_provider_for_seed_with_lookup, seed_unbound_launched_owner};
|
|
73
74
|
use super::worker_env::spawn_timestamp;
|
|
74
75
|
|
|
75
76
|
/// ---
|
|
76
77
|
/// purpose: 由 spec 生成首份 runtime state 树
|
|
77
78
|
/// params:
|
|
78
79
|
/// team_key: 本团队的 runtime 键
|
|
80
|
+
/// scoped_endpoint: 本次起队已选中的 transport tmux endpoint,与 fresh bind 同源
|
|
79
81
|
/// returns: 含 spec_path、workspace、team_dir、session_name、leader、agents、tasks 与 display_backend 的 state;随后按环境种入 launched owner,种不到则种一份 unbound owner
|
|
80
82
|
/// ---
|
|
81
83
|
pub(super) fn initial_runtime_state(
|
|
@@ -84,6 +86,7 @@ pub(super) fn initial_runtime_state(
|
|
|
84
86
|
workspace: &Path,
|
|
85
87
|
team_dir: &Path,
|
|
86
88
|
team_key: &str,
|
|
89
|
+
scoped_endpoint: Option<&str>,
|
|
87
90
|
) -> serde_json::Value {
|
|
88
91
|
let mut agents = serde_json::Map::new();
|
|
89
92
|
for agent in spec_agent_values(spec) {
|
|
@@ -144,7 +147,7 @@ pub(super) fn initial_runtime_state(
|
|
|
144
147
|
);
|
|
145
148
|
state.insert("is_external_leader".to_string(), serde_json::json!(false));
|
|
146
149
|
let mut state = serde_json::Value::Object(state);
|
|
147
|
-
if !seed_launched_owner_from_env(&mut state) {
|
|
150
|
+
if !seed_launched_owner_from_env(&mut state, scoped_endpoint) {
|
|
148
151
|
let team_id = crate::state::projection::team_state_key(&state);
|
|
149
152
|
seed_unbound_launched_owner(&mut state, &team_id);
|
|
150
153
|
}
|
|
@@ -157,9 +160,12 @@ pub(super) fn initial_runtime_state(
|
|
|
157
160
|
/// state: 待改写的 state,成功时写入 owner 与 leader receiver
|
|
158
161
|
/// returns: true 表示已种入,取不到 caller 身份或无 pane 时为 false
|
|
159
162
|
/// ---
|
|
160
|
-
pub(super) fn seed_launched_owner_from_env(
|
|
163
|
+
pub(super) fn seed_launched_owner_from_env(
|
|
164
|
+
state: &mut serde_json::Value,
|
|
165
|
+
scoped_endpoint: Option<&str>,
|
|
166
|
+
) -> bool {
|
|
161
167
|
let team_id = crate::state::projection::team_state_key(state);
|
|
162
|
-
let Ok(caller) = crate::state::identity::caller_identity_from_env(
|
|
168
|
+
let Ok(mut caller) = crate::state::identity::caller_identity_from_env(
|
|
163
169
|
Some(state),
|
|
164
170
|
&crate::state::identity::SystemEnv,
|
|
165
171
|
Some(&team_id),
|
|
@@ -167,11 +173,27 @@ pub(super) fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> boo
|
|
|
167
173
|
) else {
|
|
168
174
|
return false;
|
|
169
175
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
let current_pane = std::env::var("TMUX_PANE")
|
|
177
|
+
.ok()
|
|
178
|
+
.filter(|pane| !pane.is_empty());
|
|
179
|
+
let current_endpoint = crate::tmux_backend::socket_name_from_tmux_env();
|
|
180
|
+
match crate::layout::worker_env::caller_provider_resolution(
|
|
181
|
+
current_pane.as_deref(),
|
|
182
|
+
current_endpoint.as_deref(),
|
|
183
|
+
scoped_endpoint,
|
|
184
|
+
) {
|
|
185
|
+
crate::layout::worker_env::CallerProviderResolution::Invalid => return false,
|
|
186
|
+
crate::layout::worker_env::CallerProviderResolution::Valid(provider) => {
|
|
187
|
+
caller.provider = crate::provider::wire::provider_wire(provider).to_string();
|
|
188
|
+
}
|
|
189
|
+
crate::layout::worker_env::CallerProviderResolution::Absent => {}
|
|
190
|
+
}
|
|
191
|
+
// Absent keeps the existing explicit/direct pane path. Do not unbind a
|
|
192
|
+
// caller TMUX_PANE just because launcher context and LEADER_PROVIDER are
|
|
193
|
+
// both missing; that is the external-leader same-id-across-sockets case.
|
|
194
|
+
// Invalid already returned. Do not replace a missing provider with broad
|
|
195
|
+
// pane/process attribution.
|
|
196
|
+
seed_launched_owner_from_caller_with_provider_lookup(state, caller, |_| None)
|
|
175
197
|
}
|
|
176
198
|
|
|
177
199
|
/// ---
|
|
@@ -619,9 +641,13 @@ pub(super) fn team_workspace(team_dir: &Path) -> PathBuf {
|
|
|
619
641
|
|
|
620
642
|
#[cfg(test)]
|
|
621
643
|
mod tests {
|
|
622
|
-
#![allow(clippy::unwrap_used)]
|
|
644
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
623
645
|
use super::*;
|
|
646
|
+
use crate::layout::worker_env::{CALLER_ENDPOINT_ENV, CALLER_PANE_ENV, CALLER_PROVIDER_ENV};
|
|
624
647
|
use crate::model::enums::Provider;
|
|
648
|
+
use serde_json::json;
|
|
649
|
+
|
|
650
|
+
use super::hermetic::HermeticTestEnv;
|
|
625
651
|
|
|
626
652
|
fn yaml_agent(bypass: bool) -> Value {
|
|
627
653
|
crate::model::yaml::loads(&format!(
|
|
@@ -651,4 +677,103 @@ mod tests {
|
|
|
651
677
|
assert!(!safety.enabled);
|
|
652
678
|
assert_eq!(safety.source, DangerousApprovalSource::Disabled);
|
|
653
679
|
}
|
|
680
|
+
|
|
681
|
+
fn seed_state() -> serde_json::Value {
|
|
682
|
+
json!({
|
|
683
|
+
"team_key": "verify031",
|
|
684
|
+
"workspace": "/tmp/ws",
|
|
685
|
+
"session_name": "team-verify031",
|
|
686
|
+
"agents": {}
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
fn owner_pane(state: &serde_json::Value) -> Option<&str> {
|
|
691
|
+
state
|
|
692
|
+
.pointer("/teams/verify031/team_owner/pane_id")
|
|
693
|
+
.and_then(serde_json::Value::as_str)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
fn owner_provider(state: &serde_json::Value) -> Option<&str> {
|
|
697
|
+
state
|
|
698
|
+
.pointer("/teams/verify031/team_owner/provider")
|
|
699
|
+
.and_then(serde_json::Value::as_str)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
fn receiver_pane(state: &serde_json::Value) -> Option<&str> {
|
|
703
|
+
state
|
|
704
|
+
.pointer("/teams/verify031/leader_receiver/pane_id")
|
|
705
|
+
.and_then(serde_json::Value::as_str)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
#[test]
|
|
709
|
+
#[serial_test::serial(env)]
|
|
710
|
+
fn seed_absent_caller_keeps_direct_tmux_pane_without_unbinding() {
|
|
711
|
+
let hermetic = HermeticTestEnv::enter("seed-absent-direct-pane");
|
|
712
|
+
let _tmux = hermetic.with_env("TMUX", "/tmp/default-tmux-socket,123,0");
|
|
713
|
+
let _pane = hermetic.with_env("TMUX_PANE", "%0");
|
|
714
|
+
let mut state = seed_state();
|
|
715
|
+
assert!(
|
|
716
|
+
seed_launched_owner_from_env(&mut state, Some("/tmp/product-team.sock")),
|
|
717
|
+
"Absent caller context must still seed the caller's TMUX_PANE; state={state}"
|
|
718
|
+
);
|
|
719
|
+
assert_eq!(owner_pane(&state), Some("%0"));
|
|
720
|
+
assert_eq!(receiver_pane(&state), Some("%0"));
|
|
721
|
+
assert!(
|
|
722
|
+
owner_provider(&state).is_none(),
|
|
723
|
+
"direct pane seed must not invent a provider; state={state}"
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
#[test]
|
|
728
|
+
#[serial_test::serial(env)]
|
|
729
|
+
fn seed_valid_caller_uses_typed_provider_and_same_scoped_endpoint() {
|
|
730
|
+
let hermetic = HermeticTestEnv::enter("seed-valid-scoped");
|
|
731
|
+
let _tmux = hermetic.with_env("TMUX", "/tmp/tmux.sock,1,0");
|
|
732
|
+
let _pane = hermetic.with_env("TMUX_PANE", "%1");
|
|
733
|
+
let _provider = hermetic.with_env(CALLER_PROVIDER_ENV, "pi");
|
|
734
|
+
let _caller_pane = hermetic.with_env(CALLER_PANE_ENV, "%1");
|
|
735
|
+
let _caller_endpoint = hermetic.with_env(CALLER_ENDPOINT_ENV, "/tmp/tmux.sock");
|
|
736
|
+
let mut state = seed_state();
|
|
737
|
+
assert!(seed_launched_owner_from_env(
|
|
738
|
+
&mut state,
|
|
739
|
+
Some("/tmp/tmux.sock")
|
|
740
|
+
));
|
|
741
|
+
assert_eq!(owner_pane(&state), Some("%1"));
|
|
742
|
+
assert_eq!(owner_provider(&state), Some("pi"));
|
|
743
|
+
assert_eq!(receiver_pane(&state), Some("%1"));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
#[test]
|
|
747
|
+
#[serial_test::serial(env)]
|
|
748
|
+
fn seed_invalid_caller_does_not_fall_back_to_direct_pane() {
|
|
749
|
+
let hermetic = HermeticTestEnv::enter("seed-invalid-scoped");
|
|
750
|
+
let _tmux = hermetic.with_env("TMUX", "/tmp/tmux.sock,1,0");
|
|
751
|
+
let _pane = hermetic.with_env("TMUX_PANE", "%1");
|
|
752
|
+
let _provider = hermetic.with_env(CALLER_PROVIDER_ENV, "pi");
|
|
753
|
+
let _caller_pane = hermetic.with_env(CALLER_PANE_ENV, "%1");
|
|
754
|
+
let _caller_endpoint = hermetic.with_env(CALLER_ENDPOINT_ENV, "/tmp/tmux.sock");
|
|
755
|
+
let mut state = seed_state();
|
|
756
|
+
assert!(
|
|
757
|
+
!seed_launched_owner_from_env(&mut state, Some("/tmp/product.sock")),
|
|
758
|
+
"scoped endpoint mismatch must fail closed; state={state}"
|
|
759
|
+
);
|
|
760
|
+
assert!(owner_pane(&state).is_none(), "state={state}");
|
|
761
|
+
assert!(receiver_pane(&state).is_none(), "state={state}");
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
#[test]
|
|
765
|
+
#[serial_test::serial(env)]
|
|
766
|
+
fn seed_absent_keeps_explicit_leader_provider() {
|
|
767
|
+
let hermetic = HermeticTestEnv::enter("seed-absent-explicit");
|
|
768
|
+
let _tmux = hermetic.with_env("TMUX", "/tmp/tmux.sock,1,0");
|
|
769
|
+
let _pane = hermetic.with_env("TMUX_PANE", "%3");
|
|
770
|
+
let _leader = hermetic.with_env("TEAM_AGENT_LEADER_PROVIDER", "pi");
|
|
771
|
+
let mut state = seed_state();
|
|
772
|
+
assert!(seed_launched_owner_from_env(
|
|
773
|
+
&mut state,
|
|
774
|
+
Some("/tmp/tmux.sock")
|
|
775
|
+
));
|
|
776
|
+
assert_eq!(owner_pane(&state), Some("%3"));
|
|
777
|
+
assert_eq!(owner_provider(&state), Some("pi"));
|
|
778
|
+
}
|
|
654
779
|
}
|
|
@@ -523,6 +523,9 @@ pub(super) fn spawn_agent_window(
|
|
|
523
523
|
provider,
|
|
524
524
|
),
|
|
525
525
|
);
|
|
526
|
+
// Keep the launcher-known typed provider independent from inherited
|
|
527
|
+
// LEADER_* identity. The tmux wrapper binds pane/socket at invocation time.
|
|
528
|
+
crate::layout::worker_env::inject_current_caller_provider(&mut env, provider);
|
|
526
529
|
|
|
527
530
|
// 0.4.6 Stage 2: write actual spawn plan event BEFORE invoking the
|
|
528
531
|
// transport spawn. Mirrors `launch.rs:359-380` (the reference impl)
|
|
@@ -54,14 +54,14 @@ fn worker_spawn_inherits_parent_process_env_for_proxy_and_ca() {
|
|
|
54
54
|
&launch_transport,
|
|
55
55
|
)
|
|
56
56
|
.expect("launch fixture should spawn worker");
|
|
57
|
-
let
|
|
57
|
+
let launch_spawn = launch_transport.single_spawn();
|
|
58
58
|
|
|
59
59
|
let restart_team = compiled_team_dir("restart", &[("worker_a", "Restart Worker")]);
|
|
60
60
|
seed_restart_state(&restart_team, "worker_a");
|
|
61
61
|
let restart_transport = RecordingTransport::new().with_session_present(false);
|
|
62
62
|
restart_with_transport(&restart_team, true, None, &restart_transport)
|
|
63
63
|
.expect("restart fixture should spawn worker");
|
|
64
|
-
let
|
|
64
|
+
let restart_spawn = restart_transport.single_spawn();
|
|
65
65
|
|
|
66
66
|
let add_team = compiled_team_dir("add-agent", &[("worker_a", "Existing Worker")]);
|
|
67
67
|
seed_running_add_agent_state(&add_team, "worker_a");
|
|
@@ -77,17 +77,15 @@ fn worker_spawn_inherits_parent_process_env_for_proxy_and_ca() {
|
|
|
77
77
|
&add_transport,
|
|
78
78
|
)
|
|
79
79
|
.expect("add-agent fixture should spawn worker");
|
|
80
|
-
let
|
|
80
|
+
let add_spawn = add_transport.single_spawn();
|
|
81
81
|
|
|
82
82
|
let failures = [
|
|
83
|
-
("quick-start launch",
|
|
84
|
-
("restart",
|
|
85
|
-
("add-agent",
|
|
83
|
+
("quick-start launch", launch_spawn, "worker_a"),
|
|
84
|
+
("restart", restart_spawn, "worker_a"),
|
|
85
|
+
("add-agent", add_spawn, "worker_b"),
|
|
86
86
|
]
|
|
87
87
|
.into_iter()
|
|
88
|
-
.flat_map(|(surface,
|
|
89
|
-
spawn_env_contract_failures(surface, &command_line, agent_id)
|
|
90
|
-
})
|
|
88
|
+
.flat_map(|(surface, spawn, agent_id)| spawn_env_contract_failures(surface, &spawn, agent_id))
|
|
91
89
|
.collect::<Vec<_>>();
|
|
92
90
|
|
|
93
91
|
assert!(
|
|
@@ -97,6 +95,61 @@ fn worker_spawn_inherits_parent_process_env_for_proxy_and_ca() {
|
|
|
97
95
|
);
|
|
98
96
|
}
|
|
99
97
|
|
|
98
|
+
#[test]
|
|
99
|
+
fn tmux_control_filter_uses_env_keys_not_substring_in_inherited_values() {
|
|
100
|
+
let mut env = BTreeMap::from([
|
|
101
|
+
("TA_SPAWN_ENV_CANARY".to_string(), "FOO".to_string()),
|
|
102
|
+
(
|
|
103
|
+
"HTTP_PROXY".to_string(),
|
|
104
|
+
"http://canary-proxy:1".to_string(),
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"NODE_EXTRA_CA_CERTS".to_string(),
|
|
108
|
+
"/canary/ca.pem".to_string(),
|
|
109
|
+
),
|
|
110
|
+
("TEAM_AGENT_WORKSPACE".to_string(), "/ws".to_string()),
|
|
111
|
+
(
|
|
112
|
+
"TEAM_AGENT_OWNER_TEAM_ID".to_string(),
|
|
113
|
+
"teamdir".to_string(),
|
|
114
|
+
),
|
|
115
|
+
("TEAM_AGENT_AGENT_ID".to_string(), "worker_a".to_string()),
|
|
116
|
+
(
|
|
117
|
+
"HARNESS_PROBE".to_string(),
|
|
118
|
+
"printf 'LS_USR_BIN_TMUX=%s\\n'\n".to_string(),
|
|
119
|
+
),
|
|
120
|
+
]);
|
|
121
|
+
let spawn = RecordedSpawn {
|
|
122
|
+
argv: vec!["codex".to_string()],
|
|
123
|
+
env: env.clone(),
|
|
124
|
+
env_unset: Vec::new(),
|
|
125
|
+
session: SessionName::new("s"),
|
|
126
|
+
window: WindowName::new("w"),
|
|
127
|
+
pane_id: PaneId::new("%1"),
|
|
128
|
+
};
|
|
129
|
+
assert!(
|
|
130
|
+
spawn_env_contract_failures("harness-value", &spawn, "worker_a").is_empty(),
|
|
131
|
+
"inherited values may contain the letters TMUX= without leaking TMUX/TMUX_PANE keys"
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
env.insert(
|
|
135
|
+
"TMUX".to_string(),
|
|
136
|
+
"/tmp/tmux-canary/default,1".to_string(),
|
|
137
|
+
);
|
|
138
|
+
let leaked = RecordedSpawn {
|
|
139
|
+
env,
|
|
140
|
+
..spawn
|
|
141
|
+
};
|
|
142
|
+
let failures = spawn_env_contract_failures("key-leak", &leaked, "worker_a");
|
|
143
|
+
assert!(
|
|
144
|
+
failures.iter().any(|row| row.contains("present_keys=") && row.contains("TMUX")),
|
|
145
|
+
"actual TMUX key must still fail closed; failures={failures:?}"
|
|
146
|
+
);
|
|
147
|
+
assert!(
|
|
148
|
+
failures.iter().all(|row| !row.contains("command_line=")),
|
|
149
|
+
"failures must not dump command_line; failures={failures:?}"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
100
153
|
#[test]
|
|
101
154
|
#[serial(env)]
|
|
102
155
|
fn worker_spawn_scrubs_cross_provider_process_identity_but_keeps_config_env() {
|
|
@@ -234,42 +287,67 @@ fn worker_spawn_stays_out_of_coordinator_tick_and_daemon_preserves_parent_env()
|
|
|
234
287
|
);
|
|
235
288
|
}
|
|
236
289
|
|
|
237
|
-
fn
|
|
290
|
+
fn spawn_env_has_tmux_control_assignment(spawn: &RecordedSpawn) -> bool {
|
|
291
|
+
spawn.env.contains_key("TMUX")
|
|
292
|
+
|| spawn.env.contains_key("TMUX_PANE")
|
|
293
|
+
|| spawn.argv.iter().any(|part| {
|
|
294
|
+
part.split_whitespace().any(|tok| {
|
|
295
|
+
tok == "TMUX" || tok.starts_with("TMUX=") || tok.starts_with("TMUX_PANE=")
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn spawn_env_contract_failures(
|
|
301
|
+
surface: &str,
|
|
302
|
+
spawn: &RecordedSpawn,
|
|
303
|
+
agent_id: &str,
|
|
304
|
+
) -> Vec<String> {
|
|
238
305
|
let mut failures = Vec::new();
|
|
239
|
-
for expected in [
|
|
240
|
-
"TA_SPAWN_ENV_CANARY
|
|
241
|
-
"HTTP_PROXY
|
|
242
|
-
"NODE_EXTRA_CA_CERTS
|
|
243
|
-
"
|
|
244
|
-
"TEAM_AGENT_OWNER_TEAM_ID=teamdir",
|
|
306
|
+
for (key, expected) in [
|
|
307
|
+
("TA_SPAWN_ENV_CANARY", Some("FOO")),
|
|
308
|
+
("HTTP_PROXY", Some("http://canary-proxy:1")),
|
|
309
|
+
("NODE_EXTRA_CA_CERTS", Some("/canary/ca.pem")),
|
|
310
|
+
("TEAM_AGENT_OWNER_TEAM_ID", Some("teamdir")),
|
|
245
311
|
] {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
312
|
+
match (spawn.env.get(key).map(String::as_str), expected) {
|
|
313
|
+
(Some(actual), Some(wanted)) if actual == wanted => {}
|
|
314
|
+
(Some(_), Some(wanted)) => failures.push(format!(
|
|
315
|
+
"{surface}: `{key}` present but not `{wanted}`"
|
|
316
|
+
)),
|
|
317
|
+
_ => failures.push(format!(
|
|
318
|
+
"{surface}: missing `{key}` overlay/inherit"
|
|
319
|
+
)),
|
|
250
320
|
}
|
|
251
321
|
}
|
|
252
|
-
if
|
|
322
|
+
if spawn
|
|
323
|
+
.env
|
|
324
|
+
.get("TEAM_AGENT_WORKSPACE")
|
|
325
|
+
.is_none_or(|value| value.is_empty())
|
|
326
|
+
{
|
|
327
|
+
failures.push(format!("{surface}: missing `TEAM_AGENT_WORKSPACE` overlay"));
|
|
328
|
+
}
|
|
329
|
+
if spawn.env.get("TEAM_AGENT_AGENT_ID").map(String::as_str) != Some(agent_id) {
|
|
253
330
|
failures.push(format!(
|
|
254
|
-
"{surface}: missing `TEAM_AGENT_AGENT_ID={agent_id}` overlay
|
|
331
|
+
"{surface}: missing `TEAM_AGENT_AGENT_ID={agent_id}` overlay"
|
|
255
332
|
));
|
|
256
333
|
}
|
|
257
|
-
if
|
|
334
|
+
if spawn_env_has_tmux_control_assignment(spawn) {
|
|
335
|
+
let present: Vec<&str> = ["TMUX", "TMUX_PANE"]
|
|
336
|
+
.into_iter()
|
|
337
|
+
.filter(|key| spawn.env.contains_key(*key))
|
|
338
|
+
.collect();
|
|
258
339
|
failures.push(format!(
|
|
259
|
-
"{surface}: worker spawn must filter tmux control env from inherited parent env;
|
|
340
|
+
"{surface}: worker spawn must filter tmux control env from inherited parent env; present_keys={present:?}"
|
|
260
341
|
));
|
|
261
342
|
}
|
|
262
|
-
if !
|
|
343
|
+
if !spawn.argv.iter().any(|part| part == "codex") {
|
|
263
344
|
failures.push(format!(
|
|
264
|
-
"{surface}: provider executable must remain the command name `codex
|
|
345
|
+
"{surface}: provider executable must remain the command name `codex`"
|
|
265
346
|
));
|
|
266
347
|
}
|
|
267
|
-
if
|
|
268
|
-
.split_whitespace()
|
|
269
|
-
.any(|part| part.ends_with("/codex"))
|
|
270
|
-
{
|
|
348
|
+
if spawn.argv.iter().any(|part| part.ends_with("/codex")) {
|
|
271
349
|
failures.push(format!(
|
|
272
|
-
"{surface}: provider executable must not be hard-coded as an absolute codex path
|
|
350
|
+
"{surface}: provider executable must not be hard-coded as an absolute codex path"
|
|
273
351
|
));
|
|
274
352
|
}
|
|
275
353
|
failures
|
|
@@ -580,6 +658,7 @@ impl RecordingTransport {
|
|
|
580
658
|
self
|
|
581
659
|
}
|
|
582
660
|
|
|
661
|
+
#[allow(dead_code)]
|
|
583
662
|
fn single_spawn_command_line(&self) -> String {
|
|
584
663
|
self.single_spawn().command_line()
|
|
585
664
|
}
|