@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
|
@@ -116,12 +116,357 @@ pub(super) fn quick_start_session_capture_incomplete_agents(
|
|
|
116
116
|
/// ---
|
|
117
117
|
/// Host registry is the deliverability authority. Workspace `state.json`
|
|
118
118
|
/// is only a copy. Detection failure is unbound, never attached.
|
|
119
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
120
|
+
pub enum LeaderBindingClass {
|
|
121
|
+
Attached,
|
|
122
|
+
IndexMissing,
|
|
123
|
+
Unknown,
|
|
124
|
+
Unbound,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
impl LeaderBindingClass {
|
|
128
|
+
pub fn issue_id(self) -> &'static str {
|
|
129
|
+
match self {
|
|
130
|
+
Self::Attached => "leader_receiver_attached",
|
|
131
|
+
Self::IndexMissing => "leader_registry_index_missing",
|
|
132
|
+
Self::Unknown => "leader_binding_unknown",
|
|
133
|
+
Self::Unbound => "leader_receiver_unbound",
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub fn repair(self) -> &'static str {
|
|
138
|
+
match self {
|
|
139
|
+
Self::Attached => "",
|
|
140
|
+
Self::IndexMissing => {
|
|
141
|
+
"publish the leader registry index for the selected team; do not claim-leader"
|
|
142
|
+
}
|
|
143
|
+
Self::Unknown => {
|
|
144
|
+
"inspect the selected-team registry file and live leader channel; do not claim-leader"
|
|
145
|
+
}
|
|
146
|
+
Self::Unbound => "team-agent claim-leader --confirm --json",
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
pub fn selected_team_leader_receiver<'a>(
|
|
152
|
+
state: &'a serde_json::Value,
|
|
153
|
+
team_key: &str,
|
|
154
|
+
) -> Option<&'a serde_json::Value> {
|
|
155
|
+
if let Some(teams) = state.get("teams").and_then(serde_json::Value::as_object) {
|
|
156
|
+
if let Some(team) = teams.get(team_key) {
|
|
157
|
+
return team.get("leader_receiver");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
state.get("leader_receiver")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn owner_record<'a>(state: &'a serde_json::Value, team_key: &str) -> Option<&'a serde_json::Value> {
|
|
164
|
+
let owner = if let Some(team) = state
|
|
165
|
+
.get("teams")
|
|
166
|
+
.and_then(serde_json::Value::as_object)
|
|
167
|
+
.and_then(|teams| teams.get(team_key))
|
|
168
|
+
{
|
|
169
|
+
team.get("team_owner")
|
|
170
|
+
} else {
|
|
171
|
+
state.get("team_owner")
|
|
172
|
+
}?;
|
|
173
|
+
owner.as_object().map(|_| owner)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn canonical_identity_aligned(
|
|
177
|
+
workspace: &Path,
|
|
178
|
+
team_key: &str,
|
|
179
|
+
state: &serde_json::Value,
|
|
180
|
+
receiver: &serde_json::Value,
|
|
181
|
+
entry: &crate::leader::registry::LeaderRegistryEntry,
|
|
182
|
+
) -> bool {
|
|
183
|
+
let Some(owner) = owner_record(state, team_key) else {
|
|
184
|
+
return false;
|
|
185
|
+
};
|
|
186
|
+
let owner_pane = owner.get("pane_id").and_then(serde_json::Value::as_str);
|
|
187
|
+
let receiver_pane = receiver.get("pane_id").and_then(serde_json::Value::as_str);
|
|
188
|
+
let owner_epoch = owner.get("owner_epoch").and_then(serde_json::Value::as_u64);
|
|
189
|
+
let receiver_epoch = receiver
|
|
190
|
+
.get("owner_epoch")
|
|
191
|
+
.and_then(serde_json::Value::as_u64);
|
|
192
|
+
let channel_pane = entry
|
|
193
|
+
.channel
|
|
194
|
+
.get("pane_id")
|
|
195
|
+
.and_then(serde_json::Value::as_str)
|
|
196
|
+
.or_else(|| entry.channel.get("pane").and_then(serde_json::Value::as_str));
|
|
197
|
+
entry.team_key == team_key
|
|
198
|
+
&& entry.status == "attached"
|
|
199
|
+
&& receiver.get("status").and_then(serde_json::Value::as_str) == Some("attached")
|
|
200
|
+
&& owner_pane == receiver_pane
|
|
201
|
+
&& owner_pane.is_some()
|
|
202
|
+
&& owner_epoch == receiver_epoch
|
|
203
|
+
&& owner_epoch == Some(entry.owner_epoch)
|
|
204
|
+
&& channel_pane.is_none_or(|pane| Some(pane) == owner_pane)
|
|
205
|
+
&& crate::leader::registry::workspace_hash(workspace) == entry.workspace_hash
|
|
206
|
+
&& {
|
|
207
|
+
let receiver_socket = receiver
|
|
208
|
+
.get("tmux_socket")
|
|
209
|
+
.and_then(serde_json::Value::as_str);
|
|
210
|
+
let channel_socket = entry
|
|
211
|
+
.channel
|
|
212
|
+
.get("tmux_socket")
|
|
213
|
+
.and_then(serde_json::Value::as_str);
|
|
214
|
+
channel_socket.is_none_or(|socket| Some(socket) == receiver_socket)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
pub fn classify_leader_binding(workspace: &Path, team_key: &str) -> LeaderBindingClass {
|
|
219
|
+
let state = match load_runtime_state(workspace) {
|
|
220
|
+
Ok(state) => state,
|
|
221
|
+
Err(_) => return LeaderBindingClass::Unknown,
|
|
222
|
+
};
|
|
223
|
+
let receiver = selected_team_leader_receiver(&state, team_key);
|
|
224
|
+
let transport: Box<dyn crate::transport::Transport> = match receiver
|
|
225
|
+
.and_then(|value| value.get("tmux_socket"))
|
|
226
|
+
.and_then(serde_json::Value::as_str)
|
|
227
|
+
.filter(|endpoint| !endpoint.is_empty() && *endpoint != "default")
|
|
228
|
+
{
|
|
229
|
+
Some(endpoint) => crate::transport_factory::leader_endpoint_transport(endpoint),
|
|
230
|
+
None => return classify_leader_binding_at(workspace, team_key, &state, None),
|
|
231
|
+
};
|
|
232
|
+
classify_leader_binding_at(workspace, team_key, &state, Some(transport.as_ref()))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
pub(crate) fn classify_leader_binding_at(
|
|
236
|
+
workspace: &Path,
|
|
237
|
+
team_key: &str,
|
|
238
|
+
state: &serde_json::Value,
|
|
239
|
+
transport: Option<&dyn crate::transport::Transport>,
|
|
240
|
+
) -> LeaderBindingClass {
|
|
241
|
+
let registry = registry_deliverability(workspace, team_key);
|
|
242
|
+
let receiver = selected_team_leader_receiver(state, team_key);
|
|
243
|
+
let receiver_attached = receiver
|
|
244
|
+
.and_then(|value| value.get("status"))
|
|
245
|
+
.and_then(serde_json::Value::as_str)
|
|
246
|
+
== Some("attached");
|
|
247
|
+
let has_owner = owner_record(state, team_key).is_some();
|
|
248
|
+
match registry {
|
|
249
|
+
RegistryDeliverability::Undecidable => LeaderBindingClass::Unknown,
|
|
250
|
+
RegistryDeliverability::Attached => {
|
|
251
|
+
let Some(receiver) = receiver else {
|
|
252
|
+
return LeaderBindingClass::Unknown;
|
|
253
|
+
};
|
|
254
|
+
let dir = crate::leader::registry::registry_dir();
|
|
255
|
+
let entry = dir.and_then(|dir| {
|
|
256
|
+
let path = dir.join(format!(
|
|
257
|
+
"{}__{team_key}.json",
|
|
258
|
+
crate::leader::registry::workspace_hash(workspace)
|
|
259
|
+
));
|
|
260
|
+
std::fs::read_to_string(path)
|
|
261
|
+
.ok()
|
|
262
|
+
.and_then(|text| {
|
|
263
|
+
serde_json::from_str::<crate::leader::registry::LeaderRegistryEntry>(&text)
|
|
264
|
+
.ok()
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
let Some(entry) = entry else {
|
|
268
|
+
return LeaderBindingClass::Unknown;
|
|
269
|
+
};
|
|
270
|
+
if !canonical_identity_aligned(workspace, team_key, state, receiver, &entry) {
|
|
271
|
+
return LeaderBindingClass::Unknown;
|
|
272
|
+
}
|
|
273
|
+
let Some(transport) = transport else {
|
|
274
|
+
return LeaderBindingClass::Unknown;
|
|
275
|
+
};
|
|
276
|
+
match crate::messaging::leader_channel::resolve_live_leader_channel(
|
|
277
|
+
workspace, receiver, transport,
|
|
278
|
+
) {
|
|
279
|
+
crate::messaging::leader_channel::LeaderChannelResolution::Live(_) => {
|
|
280
|
+
LeaderBindingClass::Attached
|
|
281
|
+
}
|
|
282
|
+
_ => LeaderBindingClass::Unknown,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
RegistryDeliverability::Unbound if has_owner || receiver_attached => {
|
|
286
|
+
LeaderBindingClass::IndexMissing
|
|
287
|
+
}
|
|
288
|
+
RegistryDeliverability::Unbound => LeaderBindingClass::Unbound,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
119
292
|
pub fn launched_team_receiver_is_attached(workspace: &Path, team_key: &str) -> bool {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
293
|
+
classify_leader_binding(workspace, team_key) == LeaderBindingClass::Attached
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[cfg(test)]
|
|
297
|
+
mod claim_rework_class_tests {
|
|
298
|
+
use super::*;
|
|
299
|
+
use crate::transport::test_support::OfflineTransport;
|
|
300
|
+
use crate::transport::{PaneId, PaneInfo, SessionName};
|
|
301
|
+
use serde_json::json;
|
|
302
|
+
|
|
303
|
+
fn isolated() -> (std::path::PathBuf, std::path::PathBuf, Option<std::ffi::OsString>) {
|
|
304
|
+
let root = std::env::temp_dir().join(format!(
|
|
305
|
+
"ta-class-{}-{}",
|
|
306
|
+
std::process::id(),
|
|
307
|
+
std::time::SystemTime::now()
|
|
308
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
309
|
+
.unwrap()
|
|
310
|
+
.as_nanos()
|
|
311
|
+
));
|
|
312
|
+
let home = root.join("home");
|
|
313
|
+
let workspace = root.join("ws");
|
|
314
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
315
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
316
|
+
let previous = std::env::var_os("HOME");
|
|
317
|
+
std::env::set_var("HOME", &home);
|
|
318
|
+
(root, workspace, previous)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn restore_home(previous: Option<std::ffi::OsString>, root: &Path) {
|
|
322
|
+
match previous {
|
|
323
|
+
Some(home) => std::env::set_var("HOME", home),
|
|
324
|
+
None => std::env::remove_var("HOME"),
|
|
325
|
+
}
|
|
326
|
+
let _ = std::fs::remove_dir_all(root);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
#[serial_test::serial(env)]
|
|
331
|
+
fn classify_bad_registry_json_is_unknown() {
|
|
332
|
+
let (root, workspace, previous) = isolated();
|
|
333
|
+
crate::state::persist::save_runtime_state(
|
|
334
|
+
&workspace,
|
|
335
|
+
&json!({"teams": {"alpha": {"team_owner": {"pane_id": "%1", "owner_epoch": 1}}}}),
|
|
336
|
+
)
|
|
337
|
+
.unwrap();
|
|
338
|
+
let dir = crate::leader::registry::registry_dir().unwrap();
|
|
339
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
340
|
+
let path = dir.join(format!(
|
|
341
|
+
"{}__alpha.json",
|
|
342
|
+
crate::leader::registry::workspace_hash(&workspace)
|
|
343
|
+
));
|
|
344
|
+
std::fs::write(&path, "{not json").unwrap();
|
|
345
|
+
let class = classify_leader_binding(&workspace, "alpha");
|
|
346
|
+
restore_home(previous, &root);
|
|
347
|
+
assert_eq!(class, LeaderBindingClass::Unknown);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[test]
|
|
351
|
+
#[serial_test::serial(env)]
|
|
352
|
+
fn classify_null_owner_is_not_index_missing() {
|
|
353
|
+
let (root, workspace, previous) = isolated();
|
|
354
|
+
crate::state::persist::save_runtime_state(
|
|
355
|
+
&workspace,
|
|
356
|
+
&json!({"teams": {"alpha": {"team_owner": null}}}),
|
|
357
|
+
)
|
|
358
|
+
.unwrap();
|
|
359
|
+
let class = classify_leader_binding(&workspace, "alpha");
|
|
360
|
+
restore_home(previous, &root);
|
|
361
|
+
assert_eq!(class, LeaderBindingClass::Unbound);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn live_pane(workspace: &Path, pane: &str) -> PaneInfo {
|
|
365
|
+
PaneInfo {
|
|
366
|
+
pane_id: PaneId::new(pane),
|
|
367
|
+
session: SessionName::new("s"),
|
|
368
|
+
window_index: None,
|
|
369
|
+
window_name: None,
|
|
370
|
+
pane_index: None,
|
|
371
|
+
tty: None,
|
|
372
|
+
current_command: Some("codex".to_string()),
|
|
373
|
+
current_path: Some(workspace.to_path_buf()),
|
|
374
|
+
active: true,
|
|
375
|
+
pane_pid: None,
|
|
376
|
+
leader_env: Default::default(),
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[test]
|
|
381
|
+
#[serial_test::serial(env)]
|
|
382
|
+
fn classify_attached_requires_identity_and_live() {
|
|
383
|
+
let (root, workspace, previous) = isolated();
|
|
384
|
+
let endpoint = "/tmp/ta-class-live.sock";
|
|
385
|
+
let state = json!({
|
|
386
|
+
"teams": {
|
|
387
|
+
"alpha": {
|
|
388
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 3, "provider": "codex"},
|
|
389
|
+
"leader_receiver": {
|
|
390
|
+
"status": "attached",
|
|
391
|
+
"pane_id": "%1",
|
|
392
|
+
"owner_epoch": 3,
|
|
393
|
+
"tmux_socket": endpoint
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
399
|
+
let outcome = crate::leader::registry::register_binding_from_state_best_effort(
|
|
400
|
+
&workspace,
|
|
401
|
+
Some("alpha"),
|
|
402
|
+
"classify-attached",
|
|
403
|
+
);
|
|
404
|
+
assert_eq!(
|
|
405
|
+
outcome.as_ref().map(|value| value.status),
|
|
406
|
+
Some("registered")
|
|
407
|
+
);
|
|
408
|
+
let transport = OfflineTransport::default()
|
|
409
|
+
.with_tmux_endpoint(endpoint)
|
|
410
|
+
.with_targets(vec![live_pane(&workspace, "%1")]);
|
|
411
|
+
let class = crate::transport_factory::with_leader_endpoint_transport(
|
|
412
|
+
endpoint,
|
|
413
|
+
transport,
|
|
414
|
+
|| classify_leader_binding(&workspace, "alpha"),
|
|
415
|
+
);
|
|
416
|
+
restore_home(previous, &root);
|
|
417
|
+
assert_eq!(class, LeaderBindingClass::Attached);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[test]
|
|
421
|
+
#[serial_test::serial(env)]
|
|
422
|
+
fn classify_registry_socket_mismatch_is_unknown() {
|
|
423
|
+
let (root, workspace, previous) = isolated();
|
|
424
|
+
let endpoint = "/tmp/ta-class-live.sock";
|
|
425
|
+
let state = json!({
|
|
426
|
+
"teams": {
|
|
427
|
+
"alpha": {
|
|
428
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 3, "provider": "codex"},
|
|
429
|
+
"leader_receiver": {
|
|
430
|
+
"status": "attached",
|
|
431
|
+
"pane_id": "%1",
|
|
432
|
+
"owner_epoch": 3,
|
|
433
|
+
"tmux_socket": endpoint
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
439
|
+
let mut entry = crate::leader::registry::build_entry(
|
|
440
|
+
&workspace,
|
|
441
|
+
"alpha",
|
|
442
|
+
"direct_tmux",
|
|
443
|
+
json!({
|
|
444
|
+
"pane_id": "%1",
|
|
445
|
+
"tmux_socket": "/tmp/other-leader.sock"
|
|
446
|
+
}),
|
|
447
|
+
3,
|
|
448
|
+
"mismatch",
|
|
449
|
+
"2026-01-01T00:00:00Z".to_string(),
|
|
450
|
+
);
|
|
451
|
+
entry.status = "attached".to_string();
|
|
452
|
+
let dir = crate::leader::registry::registry_dir().unwrap();
|
|
453
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
454
|
+
let path = dir.join(format!(
|
|
455
|
+
"{}__alpha.json",
|
|
456
|
+
crate::leader::registry::workspace_hash(&workspace)
|
|
457
|
+
));
|
|
458
|
+
std::fs::write(&path, serde_json::to_vec(&entry).unwrap()).unwrap();
|
|
459
|
+
let transport = OfflineTransport::default()
|
|
460
|
+
.with_tmux_endpoint(endpoint)
|
|
461
|
+
.with_targets(vec![live_pane(&workspace, "%1")]);
|
|
462
|
+
let class = crate::transport_factory::with_leader_endpoint_transport(
|
|
463
|
+
endpoint,
|
|
464
|
+
transport,
|
|
465
|
+
|| classify_leader_binding(&workspace, "alpha"),
|
|
466
|
+
);
|
|
467
|
+
restore_home(previous, &root);
|
|
468
|
+
assert_eq!(class, LeaderBindingClass::Unknown);
|
|
123
469
|
}
|
|
124
|
-
load_runtime_state(workspace).is_ok()
|
|
125
470
|
}
|
|
126
471
|
|
|
127
472
|
enum RegistryDeliverability {
|
|
@@ -322,18 +322,18 @@ pub(super) fn spawn_agents(
|
|
|
322
322
|
if matches!(provider, Provider::CursorAgent) {
|
|
323
323
|
refuse_second_cursor_occupant(workspace, agent_id_raw, Some(spec))?;
|
|
324
324
|
apply_cursor_agent_rules_overlay(workspace, agent_id_raw, system_prompt.as_str())?;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
325
|
+
let project = crate::lifecycle::launch::prepare_cursor_seat_mcp(
|
|
326
|
+
workspace,
|
|
327
|
+
agent_id_raw,
|
|
328
|
+
&mcp_config,
|
|
329
|
+
)?;
|
|
330
|
+
enable_cursor_workspace_mcp_with_profile(
|
|
331
|
+
workspace,
|
|
332
|
+
project.as_deref(),
|
|
333
|
+
Some(&profile_launch),
|
|
334
|
+
)?;
|
|
335
335
|
apply_cursor_spawn_workspace_pointers(&mut plan.argv, workspace, agent_id_raw)?;
|
|
336
|
-
let proxy =
|
|
336
|
+
let proxy = apply_cursor_subscription_proxy_env_with_profile(&mut env, &profile_launch);
|
|
337
337
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
338
338
|
let _ = event_log.write(
|
|
339
339
|
"provider.cursor.proxy_presence",
|
|
@@ -203,7 +203,7 @@ pub(super) fn seed_launched_owner_from_env(
|
|
|
203
203
|
/// lookup_pane_provider: 由 pane 反查 provider 的函数,便于测试替换
|
|
204
204
|
/// returns: true 表示已写入 owner 记录
|
|
205
205
|
/// ---
|
|
206
|
-
pub
|
|
206
|
+
pub fn seed_launched_owner_from_caller_with_provider_lookup(
|
|
207
207
|
state: &mut serde_json::Value,
|
|
208
208
|
caller: crate::state::owner_gate::CallerIdentity,
|
|
209
209
|
lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
|
|
@@ -227,12 +227,12 @@ pub(super) fn seed_launched_owner_from_caller_with_provider_lookup(
|
|
|
227
227
|
});
|
|
228
228
|
let mut receiver = serde_json::json!({
|
|
229
229
|
"mode": "direct_tmux",
|
|
230
|
-
"status": "
|
|
230
|
+
"status": "pending",
|
|
231
231
|
"pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
232
232
|
"pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
233
233
|
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
234
234
|
"owner_epoch": owner_epoch,
|
|
235
|
-
"discovery": "
|
|
235
|
+
"discovery": "quick_start_seed",
|
|
236
236
|
});
|
|
237
237
|
if let Some(provider) = provider.as_ref() {
|
|
238
238
|
if let Some(owner) = owner.as_object_mut() {
|
|
@@ -452,6 +452,38 @@ pub fn worker_session_name_pub(spec: &Value) -> SessionName {
|
|
|
452
452
|
spec_session_name(spec)
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
#[cfg(test)]
|
|
456
|
+
mod claim_rework_seed_tests {
|
|
457
|
+
use super::*;
|
|
458
|
+
use crate::state::owner_gate::CallerIdentity;
|
|
459
|
+
|
|
460
|
+
#[test]
|
|
461
|
+
fn seed_writes_pending_receiver_not_attached() {
|
|
462
|
+
let mut state = serde_json::json!({
|
|
463
|
+
"session_name": "team-seed",
|
|
464
|
+
"workspace": "/tmp/claim-rework-seed"
|
|
465
|
+
});
|
|
466
|
+
let caller = CallerIdentity {
|
|
467
|
+
pane_id: "%1".to_string(),
|
|
468
|
+
provider: "codex".to_string(),
|
|
469
|
+
machine_fingerprint: "fp".to_string(),
|
|
470
|
+
leader_session_uuid: "uuid".to_string(),
|
|
471
|
+
leader_session_uuid_source: "env".to_string(),
|
|
472
|
+
};
|
|
473
|
+
assert!(seed_launched_owner_from_caller_with_provider_lookup(
|
|
474
|
+
&mut state,
|
|
475
|
+
caller,
|
|
476
|
+
|_| None
|
|
477
|
+
));
|
|
478
|
+
let status = state
|
|
479
|
+
.pointer("/teams/team-seed/leader_receiver/status")
|
|
480
|
+
.or_else(|| state.pointer("/leader_receiver/status"))
|
|
481
|
+
.and_then(serde_json::Value::as_str);
|
|
482
|
+
assert_eq!(status, Some("pending"));
|
|
483
|
+
assert_ne!(status, Some("attached"));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
455
487
|
/// ---
|
|
456
488
|
/// purpose: 取出 spec 里全部 agent id
|
|
457
489
|
/// returns: 按 spec 顺序的 agent id,无 id 字段的条目被跳过
|
|
@@ -505,6 +505,22 @@ pub fn apply_cursor_subscription_proxy_env(
|
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
pub(crate) fn apply_cursor_subscription_proxy_env_with_profile(
|
|
509
|
+
env: &mut BTreeMap<String, String>,
|
|
510
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
511
|
+
) -> CursorProxyPresence {
|
|
512
|
+
if crate::lifecycle::profile_launch::cursor_subscription_direct_mode(profile_launch) {
|
|
513
|
+
for key in &profile_launch.env_unset {
|
|
514
|
+
env.remove(key);
|
|
515
|
+
}
|
|
516
|
+
return CursorProxyPresence {
|
|
517
|
+
https_proxy: false,
|
|
518
|
+
no_proxy: env.keys().any(|key| key.eq_ignore_ascii_case("NO_PROXY")),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
apply_cursor_subscription_proxy_env(env)
|
|
522
|
+
}
|
|
523
|
+
|
|
508
524
|
/// ---
|
|
509
525
|
/// purpose: 把 profile 推出的环境清除与覆盖套用到 spawn 环境上
|
|
510
526
|
/// params:
|
|
@@ -210,6 +210,8 @@ pub fn launch_with_transport_in_workspace(
|
|
|
210
210
|
routes,
|
|
211
211
|
permissions,
|
|
212
212
|
leader_receiver_attached: false,
|
|
213
|
+
leader_bind_stage: None,
|
|
214
|
+
leader_bind_reason: None,
|
|
213
215
|
session_capture_incomplete_agents: Vec::new(),
|
|
214
216
|
})
|
|
215
217
|
}
|
|
@@ -309,6 +311,20 @@ pub(crate) use mcp_config::{
|
|
|
309
311
|
|
|
310
312
|
pub(crate) mod pi_mcp;
|
|
311
313
|
|
|
314
|
+
pub(crate) fn run_pi_catalog_preflight(
|
|
315
|
+
agents_dir: &std::path::Path,
|
|
316
|
+
discover: &mut dyn FnMut(&str) -> Result<Vec<String>, ()>,
|
|
317
|
+
) -> Result<(), crate::lifecycle::LifecycleError> {
|
|
318
|
+
crate::compiler::preflight_pi_models_in_team_with(agents_dir, discover).map_err(|error| {
|
|
319
|
+
crate::lifecycle::LifecycleError::PiModelPreflight {
|
|
320
|
+
requested: error.requested,
|
|
321
|
+
candidates: error.candidates,
|
|
322
|
+
action: error.action,
|
|
323
|
+
not_ready: error.not_ready,
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
312
328
|
mod cursor_mcp_iso;
|
|
313
329
|
pub use cursor_mcp_iso::{
|
|
314
330
|
cursor_mcp_isolation_enabled, cursor_mcp_json_path, cursor_mcp_project_dir,
|
|
@@ -317,9 +333,11 @@ pub use cursor_mcp_iso::{
|
|
|
317
333
|
mod cursor_mcp;
|
|
318
334
|
pub use cursor_mcp::{
|
|
319
335
|
apply_cursor_mcp_overlay, apply_cursor_spawn_workspace_pointers,
|
|
320
|
-
apply_cursor_workspace_physical_path, cursor_mcp_enable_argv,
|
|
321
|
-
|
|
336
|
+
apply_cursor_workspace_physical_path, cursor_mcp_enable_argv, cursor_mcp_enable_working_dir,
|
|
337
|
+
enable_cursor_workspace_mcp, enable_cursor_workspace_mcp_with_profile, physical_workspace_path,
|
|
338
|
+
prepare_cursor_seat_mcp, refuse_second_cursor_occupant,
|
|
322
339
|
};
|
|
340
|
+
pub(crate) use cursor_mcp::apply_cursor_mcp_enable_profile_env;
|
|
323
341
|
|
|
324
342
|
mod cursor_create_chat;
|
|
325
343
|
|
|
@@ -327,7 +345,8 @@ mod worker_env;
|
|
|
327
345
|
pub(super) use worker_env::*;
|
|
328
346
|
pub(crate) use worker_env::{
|
|
329
347
|
apply_copilot_instructions_overlay, apply_cursor_agent_rules_overlay,
|
|
330
|
-
|
|
348
|
+
apply_cursor_subscription_proxy_env_with_profile, apply_mcp_auto_approval_env,
|
|
349
|
+
apply_profile_launch_env, auth_mode_env_value,
|
|
331
350
|
fill_spawn_placeholders, fill_spawn_placeholders_full, inherited_env_with_team_overrides,
|
|
332
351
|
persist_command_plan_state, spawn_timestamp,
|
|
333
352
|
};
|
|
@@ -358,7 +377,10 @@ pub(crate) use quick_start_transport::{
|
|
|
358
377
|
};
|
|
359
378
|
|
|
360
379
|
pub mod readiness;
|
|
361
|
-
pub use readiness::
|
|
380
|
+
pub use readiness::{
|
|
381
|
+
classify_leader_binding, launched_team_receiver_is_attached, selected_team_leader_receiver,
|
|
382
|
+
LeaderBindingClass,
|
|
383
|
+
};
|
|
362
384
|
pub(super) use readiness::*;
|
|
363
385
|
|
|
364
386
|
mod add_agent;
|
|
@@ -388,14 +410,15 @@ pub(super) use ownership::*;
|
|
|
388
410
|
pub(crate) use ownership::{ensure_owner_allowed, ensure_owner_allowed_for_state, state_path};
|
|
389
411
|
|
|
390
412
|
pub mod spec_state;
|
|
391
|
-
pub use spec_state::
|
|
413
|
+
pub use spec_state::{
|
|
414
|
+
seed_launched_owner_from_caller_with_provider_lookup, worker_session_name_pub,
|
|
415
|
+
};
|
|
392
416
|
pub(crate) use spec_state::{
|
|
393
417
|
effective_runtime_config_for_worker_spawn, effective_runtime_config_for_worker_spawn_json,
|
|
394
418
|
};
|
|
395
419
|
use spec_state::{
|
|
396
420
|
env_nonempty, has_positive_caller_leader_env, initial_runtime_state,
|
|
397
|
-
override_spec_display_backend, override_spec_runtime_str,
|
|
398
|
-
seed_launched_owner_from_caller_with_provider_lookup, seed_launched_owner_from_env,
|
|
421
|
+
override_spec_display_backend, override_spec_runtime_str, seed_launched_owner_from_env,
|
|
399
422
|
spec_agent_values, spec_agents, spec_default_assignee, spec_routes, spec_session_name,
|
|
400
423
|
spec_tasks_json, team_workspace, yaml_value_to_json,
|
|
401
424
|
};
|
|
@@ -46,6 +46,26 @@ const COMPATIBLE_NETWORK_ENV_KEYS: &[&str] = &[
|
|
|
46
46
|
"REQUESTS_CA_BUNDLE",
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
+
// Cursor subscription direct mode removes only proxy URL inputs. NO_PROXY is
|
|
50
|
+
// deliberately retained: it is a bypass list, not a proxy endpoint, and the
|
|
51
|
+
// observed catalog failure came from the GLOBAL_AGENT proxy URL settings.
|
|
52
|
+
const CURSOR_DIRECT_PROXY_ENV_KEYS: &[&str] = &[
|
|
53
|
+
"HTTPS_PROXY",
|
|
54
|
+
"HTTP_PROXY",
|
|
55
|
+
"ALL_PROXY",
|
|
56
|
+
"https_proxy",
|
|
57
|
+
"http_proxy",
|
|
58
|
+
"all_proxy",
|
|
59
|
+
"GLOBAL_AGENT_HTTPS_PROXY",
|
|
60
|
+
"GLOBAL_AGENT_HTTP_PROXY",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
pub(crate) fn cursor_subscription_direct_mode(profile_launch: &ProviderProfileLaunch) -> bool {
|
|
64
|
+
CURSOR_DIRECT_PROXY_ENV_KEYS
|
|
65
|
+
.iter()
|
|
66
|
+
.all(|key| profile_launch.env_unset.contains(*key))
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
#[derive(Debug, Clone)]
|
|
50
70
|
pub(crate) struct ProfileValues {
|
|
51
71
|
pub(crate) path: PathBuf,
|
|
@@ -234,6 +254,15 @@ fn prepare_profile_launch(
|
|
|
234
254
|
env_overlay.remove(*key);
|
|
235
255
|
}
|
|
236
256
|
}
|
|
257
|
+
if agent.provider == Provider::CursorAgent
|
|
258
|
+
&& agent.auth_mode == AuthMode::Subscription
|
|
259
|
+
&& profile_proxy_mode(&loaded.values) == "direct"
|
|
260
|
+
{
|
|
261
|
+
for key in CURSOR_DIRECT_PROXY_ENV_KEYS {
|
|
262
|
+
env_unset.insert((*key).to_string());
|
|
263
|
+
env_overlay.remove(*key);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
237
266
|
|
|
238
267
|
let command_overrides = provider_command_overrides(&agent, &loaded.values);
|
|
239
268
|
write_runtime_env_file(workspace, &agent.id, &env_overlay, &env_unset)?;
|
|
@@ -353,6 +382,12 @@ fn validate_profile(
|
|
|
353
382
|
profile: &str,
|
|
354
383
|
loaded: &ProfileValues,
|
|
355
384
|
) -> Result<(), LifecycleError> {
|
|
385
|
+
let proxy_mode = profile_proxy_mode(&loaded.values);
|
|
386
|
+
if !matches!(proxy_mode.as_str(), "direct" | "inherit") {
|
|
387
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
388
|
+
"profile {profile} has invalid proxy mode {proxy_mode}; expected direct|inherit"
|
|
389
|
+
)));
|
|
390
|
+
}
|
|
356
391
|
if loaded
|
|
357
392
|
.values
|
|
358
393
|
.get("AUTH_MODE")
|
|
@@ -481,9 +516,8 @@ fn provider_env_exports(
|
|
|
481
516
|
}
|
|
482
517
|
}
|
|
483
518
|
}
|
|
484
|
-
// grok/cursor_agent 一期无 profile exports(subscription 已登录态)。
|
|
485
|
-
// Cursor
|
|
486
|
-
// 订阅路径见 apply_cursor_subscription_proxy_env(只记有无,不记值)。
|
|
519
|
+
// grok/cursor_agent 一期无 credential/profile exports(subscription 已登录态)。
|
|
520
|
+
// Cursor 订阅席默认继承代理;PROXY_MODE=direct 的局部清除在上方处理。
|
|
487
521
|
Provider::Grok | Provider::CursorAgent | Provider::Pi => {}
|
|
488
522
|
Provider::Fake => {}
|
|
489
523
|
}
|