@team-agent/installer 0.5.70 → 0.5.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/mod.rs +42 -8
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +40 -13
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -7
- package/crates/team-agent/src/lifecycle/restart.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +122 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +1 -1
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3393,6 +3393,40 @@ pub mod lifecycle_port {
|
|
|
3393
3393
|
Some(crate::cli::QUICK_START_REMINDER)
|
|
3394
3394
|
);
|
|
3395
3395
|
}
|
|
3396
|
+
|
|
3397
|
+
#[test]
|
|
3398
|
+
fn spawn_failure_without_session_does_not_claim_a_worker_pane_exists() {
|
|
3399
|
+
let value = restart_value(
|
|
3400
|
+
crate::lifecycle::RestartReport::Failed {
|
|
3401
|
+
session_name: crate::transport::SessionName::new("team-current"),
|
|
3402
|
+
failed_agents: vec![crate::lifecycle::RestartFailedAgent {
|
|
3403
|
+
agent_id: crate::model::ids::AgentId::new("coder"),
|
|
3404
|
+
restart_mode: crate::lifecycle::StartMode::Fresh,
|
|
3405
|
+
decision: crate::lifecycle::ResumeDecision::FreshStart,
|
|
3406
|
+
session_id: None,
|
|
3407
|
+
phase: "spawn".to_string(),
|
|
3408
|
+
error: "provider io error".to_string(),
|
|
3409
|
+
}],
|
|
3410
|
+
next_actions: Vec::new(),
|
|
3411
|
+
attach_commands: Vec::new(),
|
|
3412
|
+
},
|
|
3413
|
+
None,
|
|
3414
|
+
);
|
|
3415
|
+
let action = value
|
|
3416
|
+
.pointer("/failed_agents/0/action")
|
|
3417
|
+
.and_then(Value::as_str)
|
|
3418
|
+
.expect("spawn failure action");
|
|
3419
|
+
|
|
3420
|
+
assert!(
|
|
3421
|
+
action.contains("before a pane was created"),
|
|
3422
|
+
"action={action}"
|
|
3423
|
+
);
|
|
3424
|
+
assert!(
|
|
3425
|
+
!action.contains("inspect worker coder output"),
|
|
3426
|
+
"action={action}"
|
|
3427
|
+
);
|
|
3428
|
+
assert!(!action.contains("restart-agent"), "action={action}");
|
|
3429
|
+
}
|
|
3396
3430
|
}
|
|
3397
3431
|
|
|
3398
3432
|
fn restart_value(report: crate::lifecycle::RestartReport, team: Option<&str>) -> Value {
|
|
@@ -3436,10 +3470,10 @@ pub mod lifecycle_port {
|
|
|
3436
3470
|
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
3437
3471
|
"phase": failure.phase,
|
|
3438
3472
|
"error": failure.error,
|
|
3439
|
-
"action":
|
|
3440
|
-
|
|
3441
|
-
failure.
|
|
3442
|
-
failure.
|
|
3473
|
+
"action": crate::lifecycle::restart_failure_action(
|
|
3474
|
+
&failure.agent_id,
|
|
3475
|
+
&failure.phase,
|
|
3476
|
+
failure.session_id.as_ref(),
|
|
3443
3477
|
),
|
|
3444
3478
|
"log": format!(
|
|
3445
3479
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -3470,10 +3504,10 @@ pub mod lifecycle_port {
|
|
|
3470
3504
|
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
3471
3505
|
"phase": failure.phase,
|
|
3472
3506
|
"error": failure.error,
|
|
3473
|
-
"action":
|
|
3474
|
-
|
|
3475
|
-
failure.
|
|
3476
|
-
failure.
|
|
3507
|
+
"action": crate::lifecycle::restart_failure_action(
|
|
3508
|
+
&failure.agent_id,
|
|
3509
|
+
&failure.phase,
|
|
3510
|
+
failure.session_id.as_ref(),
|
|
3477
3511
|
),
|
|
3478
3512
|
"log": format!(
|
|
3479
3513
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -450,13 +450,24 @@ pub(crate) fn validate_pi_wrapper_source(
|
|
|
450
450
|
/// errors: identity、序列化、目录、写盘或 rename 失败时返回 ProviderError
|
|
451
451
|
/// ---
|
|
452
452
|
pub(crate) fn write_pi_wrapper(request: PiWrapperRequest<'_>) -> Result<PathBuf, ProviderError> {
|
|
453
|
+
write_pi_wrapper_with_publish(request, |from, to| std::fs::rename(from, to))
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
pub(crate) fn write_pi_wrapper_with_publish(
|
|
457
|
+
request: PiWrapperRequest<'_>,
|
|
458
|
+
publish: impl FnOnce(&Path, &Path) -> std::io::Result<()>,
|
|
459
|
+
) -> Result<PathBuf, ProviderError> {
|
|
453
460
|
let source = render_pi_wrapper(&request)?;
|
|
454
461
|
validate_pi_wrapper_source(&source, request.adapter, request.candidate_executable)?;
|
|
455
462
|
let parent = request.destination.parent().ok_or_else(|| {
|
|
456
463
|
ProviderError::Io("Pi wrapper destination has no parent directory".to_string())
|
|
457
464
|
})?;
|
|
458
|
-
std::fs::create_dir_all(parent)
|
|
459
|
-
|
|
465
|
+
std::fs::create_dir_all(parent).map_err(|error| {
|
|
466
|
+
ProviderError::Io(format!(
|
|
467
|
+
"Pi wrapper atomic write stage=create_parent path={}: {error}",
|
|
468
|
+
parent.display()
|
|
469
|
+
))
|
|
470
|
+
})?;
|
|
460
471
|
let temp = parent.join(format!(".team-mcp.ts.{}.tmp", std::process::id()));
|
|
461
472
|
let write_result = (|| {
|
|
462
473
|
let mut file = OpenOptions::new()
|
|
@@ -464,22 +475,38 @@ pub(crate) fn write_pi_wrapper(request: PiWrapperRequest<'_>) -> Result<PathBuf,
|
|
|
464
475
|
.write(true)
|
|
465
476
|
.mode(0o600)
|
|
466
477
|
.open(&temp)
|
|
467
|
-
.map_err(|error|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
478
|
+
.map_err(|error| {
|
|
479
|
+
ProviderError::Io(format!(
|
|
480
|
+
"Pi wrapper atomic write stage=open_temp path={}: {error}",
|
|
481
|
+
temp.display()
|
|
482
|
+
))
|
|
483
|
+
})?;
|
|
484
|
+
file.write_all(source.as_bytes()).map_err(|error| {
|
|
473
485
|
ProviderError::Io(format!(
|
|
474
|
-
"
|
|
475
|
-
temp.display()
|
|
476
|
-
request.destination.display()
|
|
486
|
+
"Pi wrapper atomic write stage=write_temp path={}: {error}",
|
|
487
|
+
temp.display()
|
|
477
488
|
))
|
|
478
489
|
})?;
|
|
479
|
-
|
|
490
|
+
file.set_permissions(std::fs::Permissions::from_mode(0o600))
|
|
480
491
|
.map_err(|error| {
|
|
481
|
-
ProviderError::Io(format!(
|
|
492
|
+
ProviderError::Io(format!(
|
|
493
|
+
"Pi wrapper atomic write stage=chmod_temp_fd path={}: {error}",
|
|
494
|
+
temp.display()
|
|
495
|
+
))
|
|
482
496
|
})?;
|
|
497
|
+
file.sync_all().map_err(|error| {
|
|
498
|
+
ProviderError::Io(format!(
|
|
499
|
+
"Pi wrapper atomic write stage=fsync_temp path={}: {error}",
|
|
500
|
+
temp.display()
|
|
501
|
+
))
|
|
502
|
+
})?;
|
|
503
|
+
publish(&temp, request.destination).map_err(|error| {
|
|
504
|
+
ProviderError::Io(format!(
|
|
505
|
+
"Pi wrapper atomic write stage=rename_publish from={} to={}: {error}",
|
|
506
|
+
temp.display(),
|
|
507
|
+
request.destination.display()
|
|
508
|
+
))
|
|
509
|
+
})?;
|
|
483
510
|
Ok(request.destination.to_path_buf())
|
|
484
511
|
})();
|
|
485
512
|
if write_result.is_err() {
|
|
@@ -2522,10 +2522,10 @@ fn write_restart_agent_failed_event(
|
|
|
2522
2522
|
"session_id": decision.session_id.as_ref().map(|session| session.as_str()),
|
|
2523
2523
|
"phase": phase,
|
|
2524
2524
|
"error": error,
|
|
2525
|
-
"action":
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
decision.
|
|
2525
|
+
"action": restart_failure_action(
|
|
2526
|
+
&decision.agent_id,
|
|
2527
|
+
phase,
|
|
2528
|
+
decision.session_id.as_ref(),
|
|
2529
2529
|
),
|
|
2530
2530
|
"log": format!(
|
|
2531
2531
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -3008,14 +3008,30 @@ fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<Str
|
|
|
3008
3008
|
failed_agents
|
|
3009
3009
|
.iter()
|
|
3010
3010
|
.map(|failure| {
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
failure.
|
|
3011
|
+
restart_failure_action(
|
|
3012
|
+
&failure.agent_id,
|
|
3013
|
+
&failure.phase,
|
|
3014
|
+
failure.session_id.as_ref(),
|
|
3014
3015
|
)
|
|
3015
3016
|
})
|
|
3016
3017
|
.collect()
|
|
3017
3018
|
}
|
|
3018
3019
|
|
|
3020
|
+
pub(crate) fn restart_failure_action(
|
|
3021
|
+
agent_id: &AgentId,
|
|
3022
|
+
phase: &str,
|
|
3023
|
+
session_id: Option<&SessionId>,
|
|
3024
|
+
) -> String {
|
|
3025
|
+
if phase == "spawn" && session_id.is_none() {
|
|
3026
|
+
return format!(
|
|
3027
|
+
"worker {agent_id} failed before a pane was created; inspect the reported provider error, then rerun `team-agent restart --allow-fresh`"
|
|
3028
|
+
);
|
|
3029
|
+
}
|
|
3030
|
+
format!(
|
|
3031
|
+
"inspect worker {agent_id} output, then reset that worker with `team-agent reset-agent {agent_id}` or rerun `team-agent restart --allow-fresh`"
|
|
3032
|
+
)
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3019
3035
|
fn start_mode_wire(mode: StartMode) -> &'static str {
|
|
3020
3036
|
match mode {
|
|
3021
3037
|
StartMode::Resumed => "resumed",
|
|
@@ -92,7 +92,8 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
|
|
|
92
92
|
// timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
|
|
93
93
|
// can emit the same instrumentation event family with `source="launch"`.
|
|
94
94
|
pub(crate) use rebuild::{
|
|
95
|
-
provider_wire_from_state,
|
|
95
|
+
provider_wire_from_state, restart_failure_action, write_worker_spawn_timing_event,
|
|
96
|
+
RestartPhaseTimer,
|
|
96
97
|
};
|
|
97
98
|
pub use rebuild::{restart, restart_with_session_convergence_deadline};
|
|
98
99
|
pub(crate) use rebuild::{
|
|
@@ -3,9 +3,10 @@ use std::path::{Path, PathBuf};
|
|
|
3
3
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
4
4
|
|
|
5
5
|
use crate::lifecycle::launch::pi_mcp::{
|
|
6
|
-
parse_pi_list_models_table,
|
|
7
|
-
validate_pi_executable_chain, validate_pi_wrapper_source,
|
|
8
|
-
|
|
6
|
+
parse_pi_list_models_table, pi_seat_paths, resolve_pi_executable_chain,
|
|
7
|
+
validate_pi_adapter_identity, validate_pi_executable_chain, validate_pi_wrapper_source,
|
|
8
|
+
write_pi_wrapper, write_pi_wrapper_with_publish, PiAdapterIdentity, PiExecutableChain,
|
|
9
|
+
PiExecutableFileType, PiWrapperRequest,
|
|
9
10
|
};
|
|
10
11
|
use crate::provider::McpConfig;
|
|
11
12
|
|
|
@@ -336,6 +337,124 @@ fn pi_wrapper_is_atomic_body(hermetic: &HermeticTestEnv) {
|
|
|
336
337
|
std::fs::remove_dir_all(root).expect("remove wrapper fixture");
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
#[test]
|
|
341
|
+
fn pi_leader_wrapper_materializes_from_empty_unicode_wsl_workspace() {
|
|
342
|
+
let root = temp_root("empty-unicode-leader");
|
|
343
|
+
let workspace = root.join("mnt/f/测试基础设施");
|
|
344
|
+
let candidate = root.join("candidate/team-agent");
|
|
345
|
+
std::fs::create_dir_all(&workspace).expect("create empty Unicode workspace");
|
|
346
|
+
std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
|
|
347
|
+
.expect("create candidate parent");
|
|
348
|
+
std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
|
|
349
|
+
assert!(
|
|
350
|
+
!workspace.join(".team").exists(),
|
|
351
|
+
"fixture must begin without a .team directory"
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
let paths = pi_seat_paths(&workspace, "current", "leader");
|
|
355
|
+
let adapter = adapter_identity(&root);
|
|
356
|
+
let written = write_pi_wrapper(PiWrapperRequest {
|
|
357
|
+
destination: &paths.wrapper,
|
|
358
|
+
adapter: &adapter,
|
|
359
|
+
candidate_executable: &candidate,
|
|
360
|
+
mcp_config: &mcp_config(&candidate, &workspace, "leader"),
|
|
361
|
+
team_id: "current",
|
|
362
|
+
agent_id: "leader",
|
|
363
|
+
workspace: &workspace,
|
|
364
|
+
include_tools: &["assign_task", "send_message"],
|
|
365
|
+
})
|
|
366
|
+
.expect("materialize Pi leader wrapper from an empty Unicode workspace");
|
|
367
|
+
|
|
368
|
+
assert_eq!(written, paths.wrapper);
|
|
369
|
+
assert!(written.is_file(), "published leader wrapper must exist");
|
|
370
|
+
assert!(
|
|
371
|
+
paths.runtime_root.is_dir(),
|
|
372
|
+
"leader runtime root must be created recursively"
|
|
373
|
+
);
|
|
374
|
+
let source = std::fs::read_to_string(&written).expect("read complete leader wrapper");
|
|
375
|
+
validate_pi_wrapper_source(&source, &adapter, &candidate)
|
|
376
|
+
.expect("published leader wrapper must be complete and valid");
|
|
377
|
+
assert!(source.contains("TEAM_AGENT_ID") && source.contains("leader"));
|
|
378
|
+
assert!(source.contains("TEAM_AGENT_OWNER_TEAM_ID") && source.contains("current"));
|
|
379
|
+
std::fs::remove_dir_all(root).expect("remove Unicode leader fixture");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn pi_wrapper_permissions_are_finalized_before_atomic_publish() {
|
|
384
|
+
let source = include_str!("../launch/pi_mcp.rs");
|
|
385
|
+
let write_start = source
|
|
386
|
+
.find("pub(crate) fn write_pi_wrapper")
|
|
387
|
+
.expect("write_pi_wrapper source");
|
|
388
|
+
let write_source = &source[write_start..];
|
|
389
|
+
let permissions = write_source
|
|
390
|
+
.find("file.set_permissions")
|
|
391
|
+
.expect("wrapper permissions must use the open temp-file handle");
|
|
392
|
+
let publish = write_source
|
|
393
|
+
.find("publish(&temp, request.destination)")
|
|
394
|
+
.expect("atomic wrapper publish");
|
|
395
|
+
|
|
396
|
+
assert!(
|
|
397
|
+
permissions < publish,
|
|
398
|
+
"DrvFS may not resolve the destination immediately after rename; finalize mode through the open temp-file handle before publishing"
|
|
399
|
+
);
|
|
400
|
+
assert!(
|
|
401
|
+
!write_source.contains("std::fs::set_permissions(request.destination"),
|
|
402
|
+
"atomic publish must not require a second destination-path lookup"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn pi_wrapper_publish_needs_no_final_path_metadata_lookup() {
|
|
408
|
+
let root = temp_root("publish-path-enoent");
|
|
409
|
+
let workspace = root.join("mnt/f/测试基础设施");
|
|
410
|
+
let candidate = root.join("candidate/team-agent");
|
|
411
|
+
std::fs::create_dir_all(&workspace).expect("create Unicode workspace");
|
|
412
|
+
std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
|
|
413
|
+
.expect("create candidate parent");
|
|
414
|
+
std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
|
|
415
|
+
|
|
416
|
+
let paths = pi_seat_paths(&workspace, "current", "leader");
|
|
417
|
+
let published_parent = paths.runtime_root.clone();
|
|
418
|
+
let hidden_parent = published_parent.with_file_name("leader-after-rename");
|
|
419
|
+
let adapter = adapter_identity(&root);
|
|
420
|
+
write_pi_wrapper_with_publish(
|
|
421
|
+
PiWrapperRequest {
|
|
422
|
+
destination: &paths.wrapper,
|
|
423
|
+
adapter: &adapter,
|
|
424
|
+
candidate_executable: &candidate,
|
|
425
|
+
mcp_config: &mcp_config(&candidate, &workspace, "leader"),
|
|
426
|
+
team_id: "current",
|
|
427
|
+
agent_id: "leader",
|
|
428
|
+
workspace: &workspace,
|
|
429
|
+
include_tools: &["assign_task", "send_message"],
|
|
430
|
+
},
|
|
431
|
+
|temp, destination| {
|
|
432
|
+
std::fs::rename(temp, destination)?;
|
|
433
|
+
std::fs::rename(&published_parent, &hidden_parent)?;
|
|
434
|
+
assert_eq!(
|
|
435
|
+
std::fs::metadata(destination)
|
|
436
|
+
.expect_err("simulate DrvFS final-path metadata ENOENT after rename")
|
|
437
|
+
.kind(),
|
|
438
|
+
std::io::ErrorKind::NotFound
|
|
439
|
+
);
|
|
440
|
+
Ok(())
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
.expect("successful publish must not perform final-path metadata operations");
|
|
444
|
+
|
|
445
|
+
let published = hidden_parent.join("team-mcp.ts");
|
|
446
|
+
assert!(published.is_file(), "rename published a complete wrapper");
|
|
447
|
+
assert_eq!(
|
|
448
|
+
std::fs::metadata(&published)
|
|
449
|
+
.expect("published wrapper metadata through converged path")
|
|
450
|
+
.permissions()
|
|
451
|
+
.mode()
|
|
452
|
+
& 0o777,
|
|
453
|
+
0o600
|
|
454
|
+
);
|
|
455
|
+
std::fs::remove_dir_all(root).expect("remove publish-path fixture");
|
|
456
|
+
}
|
|
457
|
+
|
|
339
458
|
#[test]
|
|
340
459
|
fn pi_wrapper_runtime_registration_does_not_copy_ambient_config() {
|
|
341
460
|
run_process_isolated(PI_WRAPPER_AMBIENT_CHILD, PI_WRAPPER_AMBIENT_TEST, || {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.71",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.71",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.71",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.71"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: team-agent
|
|
3
3
|
description: Use only when the user explicitly asks to start, operate, inspect, shutdown, or restart a Team Agent team. Treat the team-agent CLI as a sealed appliance.
|
|
4
4
|
requires_team_agent: ">=0.5.0"
|
|
5
|
-
last_verified_against: "0.5.
|
|
5
|
+
last_verified_against: "0.5.71"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Team Agent
|