@team-agent/installer 0.5.73 → 0.5.74
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 +19 -13
- package/crates/team-agent/src/cli/emit.rs +6 -3
- package/crates/team-agent/src/cli/mod.rs +151 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
- package/crates/team-agent/src/cli/types.rs +6 -0
- package/crates/team-agent/src/compiler.rs +14 -1
- package/crates/team-agent/src/leader/lease.rs +106 -21
- package/crates/team-agent/src/leader/registry.rs +0 -89
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +457 -73
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +0 -11
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +31 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
- package/crates/team-agent/src/messaging/delivery.rs +26 -8
- package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
- package/crates/team-agent/src/model/spec.rs +81 -7
- package/crates/team-agent/src/provider/adapters/pi.rs +14 -0
- package/crates/team-agent/src/state/persist.rs +272 -3
- package/crates/team-agent/src/state/repository.rs +9 -2
- package/crates/team-agent/src/tmux_backend/tests.rs +64 -0
- package/crates/team-agent/src/tmux_backend.rs +57 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -0
- package/skills/team-agent/command-coverage.json +21 -0
- package/skills/team-agent/references/team-agent-operator.md +9 -2
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -129,6 +129,9 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
129
129
|
.get("status")
|
|
130
130
|
.and_then(Value::as_str)
|
|
131
131
|
.map(str::to_string);
|
|
132
|
+
if !args.detail {
|
|
133
|
+
lifecycle_port::compact_quick_start_value(&mut value);
|
|
134
|
+
}
|
|
132
135
|
if args.json
|
|
133
136
|
|| value.get("ok").and_then(Value::as_bool) == Some(false)
|
|
134
137
|
|| session_capture_incomplete
|
|
@@ -1377,10 +1380,12 @@ pub fn cmd_takeover(args: &TakeoverArgs) -> Result<CmdResult, CliError> {
|
|
|
1377
1380
|
|
|
1378
1381
|
/// `cmd_claim_leader`(`commands.py:156`)。
|
|
1379
1382
|
pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
|
|
1380
|
-
|
|
1381
|
-
leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)
|
|
1382
|
-
|
|
1383
|
-
|
|
1383
|
+
let mut value =
|
|
1384
|
+
leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?;
|
|
1385
|
+
if !args.detail {
|
|
1386
|
+
leader_port::compact_lease_value(&mut value);
|
|
1387
|
+
}
|
|
1388
|
+
Ok(CmdResult::from_json(value, args.json))
|
|
1384
1389
|
}
|
|
1385
1390
|
|
|
1386
1391
|
/// `cmd_identity`(`commands.py:160`)。
|
|
@@ -1401,15 +1406,16 @@ pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
|
|
|
1401
1406
|
|
|
1402
1407
|
/// `cmd_restart`(`commands.py:344`)。
|
|
1403
1408
|
pub fn cmd_restart(args: &RestartArgs) -> Result<CmdResult, CliError> {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1409
|
+
let mut value = lifecycle_port::restart(
|
|
1410
|
+
&args.workspace,
|
|
1411
|
+
args.allow_fresh,
|
|
1412
|
+
args.team.as_deref(),
|
|
1413
|
+
args.session_converge_deadline_ms,
|
|
1414
|
+
)?;
|
|
1415
|
+
if !args.detail {
|
|
1416
|
+
lifecycle_port::compact_restart_value(&mut value);
|
|
1417
|
+
}
|
|
1418
|
+
Ok(CmdResult::from_json(value, args.json))
|
|
1413
1419
|
}
|
|
1414
1420
|
|
|
1415
1421
|
/// `cmd_start_agent`(`commands.py:348`)。
|
|
@@ -374,7 +374,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
374
374
|
match command {
|
|
375
375
|
None => default_help(),
|
|
376
376
|
Some("init") => compat_hidden_help("init", "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]"),
|
|
377
|
-
Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).\n\nAfter a successful start, use the returned `send_commands` (or choose an agent explicitly) with `team-agent send AGENT MESSAGE`.".to_string(),
|
|
377
|
+
Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json] [--detail]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).\n\n--detail includes internal receiver/topology diagnostics in JSON output.\n\nAfter a successful start, use the returned `send_commands` (or choose an agent explicitly) with `team-agent send AGENT MESSAGE`.".to_string(),
|
|
378
378
|
Some("start") => compat_hidden_help("start", "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]"),
|
|
379
379
|
Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
|
|
380
380
|
Some("send") => concat!(
|
|
@@ -391,7 +391,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
391
391
|
Some("models") => "usage: team-agent models --provider pi [--search TEXT] [--json]\n\nPrints models.v1 exact role_model entries; each entry includes current=true|false. Catalog readiness is reported as auth=ok|not_ready with auth_basis=catalog_visibility.".to_string(),
|
|
392
392
|
Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
|
|
393
393
|
Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
|
|
394
|
-
Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]".to_string(),
|
|
394
|
+
Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]".to_string(),
|
|
395
395
|
Some("restart-agent") => compat_hidden_help("restart-agent", "usage: team-agent restart-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]"),
|
|
396
396
|
Some("reset-agent") => "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]".to_string(),
|
|
397
397
|
Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]\n\nAfter a successful start, use the returned `send_commands` with `team-agent send AGENT MESSAGE`.".to_string(),
|
|
@@ -405,7 +405,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
405
405
|
Some("stuck-cancel") => "usage: team-agent stuck-cancel AGENT [--workspace WORKSPACE] [--alert-type stuck|idle_fallback|cross_worker_deadlock|all] [--json]".to_string(),
|
|
406
406
|
Some("acknowledge-idle") => "usage: team-agent acknowledge-idle [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
407
407
|
Some("takeover") => "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
|
|
408
|
-
Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
|
|
408
|
+
Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json] [--detail]".to_string(),
|
|
409
409
|
Some("attach-leader") => "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--pane PANE] [--provider PROVIDER] [--confirm] [--json]".to_string(),
|
|
410
410
|
Some("attach-app-server-leader") => "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]".to_string(),
|
|
411
411
|
Some("identity") => "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
@@ -1063,6 +1063,7 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
|
|
|
1063
1063
|
yes: parsed.yes,
|
|
1064
1064
|
no_display: parsed.no_display,
|
|
1065
1065
|
json: parsed.json,
|
|
1066
|
+
detail: parsed.detail,
|
|
1066
1067
|
backend: parsed.backend,
|
|
1067
1068
|
})
|
|
1068
1069
|
}
|
|
@@ -1404,6 +1405,7 @@ fn claim_leader_args(args: &[String], cwd: &Path) -> ClaimLeaderArgs {
|
|
|
1404
1405
|
team: parsed.team,
|
|
1405
1406
|
confirm: parsed.confirm,
|
|
1406
1407
|
json: parsed.json,
|
|
1408
|
+
detail: parsed.detail,
|
|
1407
1409
|
}
|
|
1408
1410
|
}
|
|
1409
1411
|
|
|
@@ -1477,6 +1479,7 @@ fn restart_args(args: &[String], cwd: &Path) -> Result<RestartArgs, CliError> {
|
|
|
1477
1479
|
allow_fresh: parsed.allow_fresh,
|
|
1478
1480
|
session_converge_deadline_ms: parsed.session_converge_deadline_ms,
|
|
1479
1481
|
json: parsed.json,
|
|
1482
|
+
detail: parsed.detail,
|
|
1480
1483
|
})
|
|
1481
1484
|
}
|
|
1482
1485
|
|
|
@@ -3233,6 +3233,33 @@ pub mod lifecycle_port {
|
|
|
3233
3233
|
);
|
|
3234
3234
|
}
|
|
3235
3235
|
|
|
3236
|
+
const COMPACT_READINESS_KEYS: [&str; 6] = [
|
|
3237
|
+
"all_workers_spawned",
|
|
3238
|
+
"state",
|
|
3239
|
+
"ready",
|
|
3240
|
+
"reason",
|
|
3241
|
+
"next_action",
|
|
3242
|
+
"unhealthy_agents",
|
|
3243
|
+
];
|
|
3244
|
+
|
|
3245
|
+
fn compact_readiness_object(value: Option<&mut Value>) {
|
|
3246
|
+
let Some(object) = value.and_then(Value::as_object_mut) else {
|
|
3247
|
+
return;
|
|
3248
|
+
};
|
|
3249
|
+
object.retain(|key, _| COMPACT_READINESS_KEYS.contains(&key.as_str()));
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
pub(crate) fn compact_quick_start_value(value: &mut Value) {
|
|
3253
|
+
if let Some(object) = value.as_object_mut() {
|
|
3254
|
+
// Default contract: outcome, error/next action, attach/send, and a
|
|
3255
|
+
// short readiness verdict. Diagnostic aliases stay behind --detail.
|
|
3256
|
+
object.remove("agent_ids");
|
|
3257
|
+
object.remove("dry_run");
|
|
3258
|
+
compact_readiness_object(object.get_mut("readiness"));
|
|
3259
|
+
compact_readiness_object(object.get_mut("worker_readiness"));
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3236
3263
|
fn quick_start_value(report: crate::lifecycle::QuickStartReport) -> Value {
|
|
3237
3264
|
match report {
|
|
3238
3265
|
crate::lifecycle::QuickStartReport::Ready {
|
|
@@ -3265,6 +3292,7 @@ pub mod lifecycle_port {
|
|
|
3265
3292
|
),
|
|
3266
3293
|
false,
|
|
3267
3294
|
json!({
|
|
3295
|
+
"reason": "worker_unhealthy",
|
|
3268
3296
|
"all_spawned": all_spawned,
|
|
3269
3297
|
"all_workers_spawned": all_workers_spawned,
|
|
3270
3298
|
"all_attached_receiver": all_attached_receiver,
|
|
@@ -3383,6 +3411,8 @@ pub mod lifecycle_port {
|
|
|
3383
3411
|
attach_commands,
|
|
3384
3412
|
} => json!({
|
|
3385
3413
|
"ok": false,
|
|
3414
|
+
"status": "existing_runtime",
|
|
3415
|
+
"reason": "team already has runtime state; use restart",
|
|
3386
3416
|
"summary": "existing runtime",
|
|
3387
3417
|
"team": team,
|
|
3388
3418
|
"session_name": session_name.map(|s| s.as_str().to_string()),
|
|
@@ -3398,6 +3428,8 @@ pub mod lifecycle_port {
|
|
|
3398
3428
|
attach_commands,
|
|
3399
3429
|
} => json!({
|
|
3400
3430
|
"ok": false,
|
|
3431
|
+
"status": "preflight_blocked",
|
|
3432
|
+
"reason": "quick-start preflight blocked",
|
|
3401
3433
|
"summary": summary,
|
|
3402
3434
|
"blockers": blockers,
|
|
3403
3435
|
"next_actions": next_actions,
|
|
@@ -3411,6 +3443,89 @@ pub mod lifecycle_port {
|
|
|
3411
3443
|
mod quick_start_value_tests {
|
|
3412
3444
|
use super::*;
|
|
3413
3445
|
|
|
3446
|
+
#[test]
|
|
3447
|
+
fn quick_start_default_receipt_is_compact_but_detail_preserves_readiness() {
|
|
3448
|
+
let readiness = json!({
|
|
3449
|
+
"all_spawned": true,
|
|
3450
|
+
"all_workers_spawned": true,
|
|
3451
|
+
"all_attached_receiver": false,
|
|
3452
|
+
"attached_receiver": false,
|
|
3453
|
+
"leader_receiver_attached": false,
|
|
3454
|
+
"all_resumable_have_session": true,
|
|
3455
|
+
"all_resumable_agents_have_sessions": true,
|
|
3456
|
+
"ready": false,
|
|
3457
|
+
"state": "leader_receiver_unbound",
|
|
3458
|
+
"session_capture_complete": true,
|
|
3459
|
+
"session_capture_incomplete": false,
|
|
3460
|
+
"incomplete_session_capture_agents": [],
|
|
3461
|
+
"pending_session_agent_ids": [],
|
|
3462
|
+
"reason": "launched team has no attached leader receiver",
|
|
3463
|
+
"next_action": "claim-leader"
|
|
3464
|
+
});
|
|
3465
|
+
let detail = json!({
|
|
3466
|
+
"ok": false,
|
|
3467
|
+
"status": "leader_receiver_unbound",
|
|
3468
|
+
"reason": "launched team has no attached leader receiver",
|
|
3469
|
+
"ready": false,
|
|
3470
|
+
"session_name": "team-fresh",
|
|
3471
|
+
"next_actions": ["claim-leader"],
|
|
3472
|
+
"attach_commands": ["tmux attach"],
|
|
3473
|
+
"send_commands": ["team-agent send worker hi"],
|
|
3474
|
+
"agent_ids": ["worker"],
|
|
3475
|
+
"readiness": readiness.clone(),
|
|
3476
|
+
"worker_readiness": readiness,
|
|
3477
|
+
"dry_run": false
|
|
3478
|
+
});
|
|
3479
|
+
let mut default = detail.clone();
|
|
3480
|
+
compact_quick_start_value(&mut default);
|
|
3481
|
+
assert_eq!(default["status"], json!("leader_receiver_unbound"));
|
|
3482
|
+
assert_eq!(
|
|
3483
|
+
default["reason"],
|
|
3484
|
+
json!("launched team has no attached leader receiver")
|
|
3485
|
+
);
|
|
3486
|
+
assert_eq!(default["next_actions"], json!(["claim-leader"]));
|
|
3487
|
+
assert_eq!(default["attach_commands"], json!(["tmux attach"]));
|
|
3488
|
+
assert_eq!(
|
|
3489
|
+
default["send_commands"],
|
|
3490
|
+
json!(["team-agent send worker hi"])
|
|
3491
|
+
);
|
|
3492
|
+
assert!(default.get("agent_ids").is_none());
|
|
3493
|
+
assert!(default.get("dry_run").is_none());
|
|
3494
|
+
assert_eq!(default["readiness"]["all_workers_spawned"], json!(true));
|
|
3495
|
+
assert_eq!(default["readiness"]["state"], json!("leader_receiver_unbound"));
|
|
3496
|
+
assert_eq!(default["readiness"]["next_action"], json!("claim-leader"));
|
|
3497
|
+
assert!(default["readiness"].get("all_spawned").is_none());
|
|
3498
|
+
assert!(default["readiness"].get("all_attached_receiver").is_none());
|
|
3499
|
+
assert!(default["readiness"].get("leader_receiver_attached").is_none());
|
|
3500
|
+
assert_eq!(
|
|
3501
|
+
default["worker_readiness"]["all_workers_spawned"],
|
|
3502
|
+
json!(true)
|
|
3503
|
+
);
|
|
3504
|
+
assert!(default["worker_readiness"].get("all_spawned").is_none());
|
|
3505
|
+
assert!(detail["readiness"].get("all_spawned").is_some());
|
|
3506
|
+
assert!(detail["worker_readiness"].get("leader_receiver_attached").is_some());
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
#[test]
|
|
3510
|
+
fn restart_default_receipt_is_compact_but_detail_preserves_coordinator() {
|
|
3511
|
+
let detail = json!({
|
|
3512
|
+
"ok": false,
|
|
3513
|
+
"status": "partial",
|
|
3514
|
+
"reason": "restart_agent_failed",
|
|
3515
|
+
"failed_agents": [{"agent_id": "worker", "error": "spawn failed"}],
|
|
3516
|
+
"next_actions": ["restart-agent worker"],
|
|
3517
|
+
"attach_commands": [],
|
|
3518
|
+
"coordinator": {"status": "running", "transport": "default"}
|
|
3519
|
+
});
|
|
3520
|
+
let mut default = detail.clone();
|
|
3521
|
+
compact_restart_value(&mut default);
|
|
3522
|
+
assert_eq!(default["status"], json!("partial"));
|
|
3523
|
+
assert_eq!(default["reason"], json!("restart_agent_failed"));
|
|
3524
|
+
assert_eq!(default["next_actions"], json!(["restart-agent worker"]));
|
|
3525
|
+
assert!(default.get("coordinator").is_none());
|
|
3526
|
+
assert!(detail.get("coordinator").is_some());
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3414
3529
|
#[test]
|
|
3415
3530
|
fn existing_runtime_json_includes_attach_commands() {
|
|
3416
3531
|
let value = quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
|
|
@@ -3500,6 +3615,15 @@ pub mod lifecycle_port {
|
|
|
3500
3615
|
}
|
|
3501
3616
|
}
|
|
3502
3617
|
|
|
3618
|
+
pub(crate) fn compact_restart_value(value: &mut Value) {
|
|
3619
|
+
if let Some(object) = value.as_object_mut() {
|
|
3620
|
+
// Coordinator internals remain available only through --detail;
|
|
3621
|
+
// outcome, failed-agent errors, attach commands and next actions
|
|
3622
|
+
// stay in the default receipt.
|
|
3623
|
+
object.remove("coordinator");
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3503
3627
|
fn restart_value(report: crate::lifecycle::RestartReport, team: Option<&str>) -> Value {
|
|
3504
3628
|
match report {
|
|
3505
3629
|
crate::lifecycle::RestartReport::Restarted {
|
|
@@ -3855,6 +3979,7 @@ pub mod lifecycle_port {
|
|
|
3855
3979
|
"status_class": "refused_resume_atomicity",
|
|
3856
3980
|
"allow_fresh": allow_fresh,
|
|
3857
3981
|
"error": error_str,
|
|
3982
|
+
"next_action": "pass --allow-fresh to start fresh, or restore the provider session backing files",
|
|
3858
3983
|
"unresumable": unresumable_detail,
|
|
3859
3984
|
"unresumable_ids": unresumable_ids,
|
|
3860
3985
|
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
@@ -3894,6 +4019,7 @@ pub mod lifecycle_port {
|
|
|
3894
4019
|
"status": "refused_invalid_first_send_at",
|
|
3895
4020
|
"allow_fresh": allow_fresh,
|
|
3896
4021
|
"error": error,
|
|
4022
|
+
"next_action": "pass --allow-fresh to discard the invalid session marker and start fresh",
|
|
3897
4023
|
"invalid": invalid.iter().map(|w| w.worker_id.as_str()).collect::<Vec<_>>(),
|
|
3898
4024
|
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
3899
4025
|
}),
|
|
@@ -5075,6 +5201,31 @@ pub mod leader_port {
|
|
|
5075
5201
|
})
|
|
5076
5202
|
}
|
|
5077
5203
|
|
|
5204
|
+
pub(crate) fn compact_lease_value(value: &mut Value) {
|
|
5205
|
+
let topology_status = value
|
|
5206
|
+
.get("topology_convergence")
|
|
5207
|
+
.and_then(|topology| topology.get("status"))
|
|
5208
|
+
.cloned();
|
|
5209
|
+
if value.get("reason").is_none() {
|
|
5210
|
+
if let Some(status) = topology_status {
|
|
5211
|
+
if status.as_str() != Some("converged") {
|
|
5212
|
+
value["reason"] = status;
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5216
|
+
if let Some(obj) = value.as_object_mut() {
|
|
5217
|
+
for key in [
|
|
5218
|
+
"owner_epoch",
|
|
5219
|
+
"leader_receiver",
|
|
5220
|
+
"team_owner",
|
|
5221
|
+
"topology_convergence",
|
|
5222
|
+
"leader_registry",
|
|
5223
|
+
] {
|
|
5224
|
+
obj.remove(key);
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5078
5229
|
fn lease_value(result: crate::leader::LeaseResult) -> Value {
|
|
5079
5230
|
let mut out = serde_json::Map::new();
|
|
5080
5231
|
out.insert("ok".to_string(), json!(result.ok));
|
|
@@ -238,6 +238,34 @@ fn leader_port_takeover_confirm_without_pane_refuses_caller_pane_missing() {
|
|
|
238
238
|
);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
#[test]
|
|
242
|
+
fn claim_leader_default_response_hides_internal_binding_details() {
|
|
243
|
+
let mut value = serde_json::json!({
|
|
244
|
+
"ok": true,
|
|
245
|
+
"status": "already_bound",
|
|
246
|
+
"owner_epoch": 3,
|
|
247
|
+
"bound_pane_id": "%77",
|
|
248
|
+
"leader_receiver": {"pane_id": "%77"},
|
|
249
|
+
"team_owner": {"pane_id": "%77"},
|
|
250
|
+
"topology_convergence": {"status": "not_converged_old_endpoint_live"},
|
|
251
|
+
"leader_registry": {"status": "registered"}
|
|
252
|
+
});
|
|
253
|
+
super::leader_port::compact_lease_value(&mut value);
|
|
254
|
+
assert_eq!(value["ok"], json!(true));
|
|
255
|
+
assert_eq!(value["status"], json!("already_bound"));
|
|
256
|
+
assert_eq!(value["bound_pane_id"], json!("%77"));
|
|
257
|
+
assert_eq!(value["reason"], json!("not_converged_old_endpoint_live"));
|
|
258
|
+
for field in [
|
|
259
|
+
"owner_epoch",
|
|
260
|
+
"leader_receiver",
|
|
261
|
+
"team_owner",
|
|
262
|
+
"topology_convergence",
|
|
263
|
+
"leader_registry",
|
|
264
|
+
] {
|
|
265
|
+
assert!(value.get(field).is_none(), "default claim response leaked {field}");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
241
269
|
// RED — claim_leader(confirm=false) with no $TMUX_PANE: runtime.claim_leader is
|
|
242
270
|
// ALSO Family A (runtime.py:791) -> the bind gate fires FIRST, so the no-pane
|
|
243
271
|
// refusal is caller_pane_missing (NOT the leader-lane "not_in_tmux_pane").
|
|
@@ -225,6 +225,7 @@ fn cli_quick_start_invokes_real_lifecycle_compiles_spec() {
|
|
|
225
225
|
no_display: false,
|
|
226
226
|
backend: None,
|
|
227
227
|
json: true,
|
|
228
|
+
detail: false,
|
|
228
229
|
};
|
|
229
230
|
let _ = cmd_quick_start(&args); // real quick_start compiles the spec before any coordinator/launch step
|
|
230
231
|
// E5: spec compiled to .team/runtime/<team_key>/, NOT the user team dir.
|
|
@@ -260,6 +261,7 @@ fn cli_quick_start_invalid_spec_surfaces_real_compile_error() {
|
|
|
260
261
|
no_display: false,
|
|
261
262
|
backend: None,
|
|
262
263
|
json: true,
|
|
264
|
+
detail: false,
|
|
263
265
|
};
|
|
264
266
|
let text = outcome_text(cmd_quick_start(&args));
|
|
265
267
|
assert!(
|
|
@@ -281,6 +283,7 @@ fn cli_restart_missing_spec_surfaces_real_teamselect() {
|
|
|
281
283
|
allow_fresh: false,
|
|
282
284
|
session_converge_deadline_ms: None,
|
|
283
285
|
json: true,
|
|
286
|
+
detail: false,
|
|
284
287
|
};
|
|
285
288
|
let text = outcome_text(cmd_restart(&args));
|
|
286
289
|
// RED-2-STILL: entry gate now resolves via canonical_run_workspace; for a no-spec workspace
|
|
@@ -297,6 +297,8 @@ pub struct QuickStartArgs {
|
|
|
297
297
|
pub yes: bool,
|
|
298
298
|
pub no_display: bool,
|
|
299
299
|
pub json: bool,
|
|
300
|
+
/// Include internal receiver/topology diagnostics in the response.
|
|
301
|
+
pub detail: bool,
|
|
300
302
|
/// 0.5.x Phase 1d Batch 2: explicit backend override
|
|
301
303
|
/// (`--backend <tmux|conpty>`). `None` = default (factory 5-layer
|
|
302
304
|
/// resolution; almost always `tmux` on a cold quick-start).
|
|
@@ -433,6 +435,8 @@ pub struct ClaimLeaderArgs {
|
|
|
433
435
|
pub team: Option<String>,
|
|
434
436
|
pub confirm: bool,
|
|
435
437
|
pub json: bool,
|
|
438
|
+
/// Include internal receiver/topology diagnostics in the response.
|
|
439
|
+
pub detail: bool,
|
|
436
440
|
}
|
|
437
441
|
|
|
438
442
|
/// `attach-leader` public CLI args. `cmd_attach_leader` consumes the typed pane/provider
|
|
@@ -482,6 +486,8 @@ pub struct RestartArgs {
|
|
|
482
486
|
pub allow_fresh: bool,
|
|
483
487
|
pub session_converge_deadline_ms: Option<u64>,
|
|
484
488
|
pub json: bool,
|
|
489
|
+
/// Include internal receiver/topology diagnostics in the response.
|
|
490
|
+
pub detail: bool,
|
|
485
491
|
}
|
|
486
492
|
|
|
487
493
|
/// `start-agent`(`parser.py:369`)。
|
|
@@ -30,7 +30,8 @@ use std::path::Path;
|
|
|
30
30
|
use crate::communication_mode::CommunicationMode;
|
|
31
31
|
use crate::model::enums::{Provider, ProviderEffort};
|
|
32
32
|
use crate::model::yaml::Value;
|
|
33
|
-
use crate::model::{paths, spec, yaml, ModelError};
|
|
33
|
+
use crate::model::{paths, permissions, spec, yaml, ModelError};
|
|
34
|
+
use crate::provider::adapters::pi::first_unsupported_pi_tool_category;
|
|
34
35
|
use crate::provider::wire::{
|
|
35
36
|
builtin_provider_model as wire_builtin_provider_model, is_claude_family,
|
|
36
37
|
parse_canonical_provider, provider_model_keys,
|
|
@@ -764,6 +765,18 @@ fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<
|
|
|
764
765
|
path.display()
|
|
765
766
|
)));
|
|
766
767
|
}
|
|
768
|
+
let expanded = permissions::expand_tool_strings(tools.iter().map(String::as_str));
|
|
769
|
+
if let Some(category) = first_unsupported_pi_tool_category(
|
|
770
|
+
expanded
|
|
771
|
+
.iter()
|
|
772
|
+
.filter(|category| permissions::is_canonical_tool(category))
|
|
773
|
+
.map(String::as_str),
|
|
774
|
+
) {
|
|
775
|
+
return Err(ModelError::Validation(format!(
|
|
776
|
+
"{}: Pi does not support Team Agent tool category {category:?}; remove it from the role's tools",
|
|
777
|
+
path.display()
|
|
778
|
+
)));
|
|
779
|
+
}
|
|
767
780
|
Ok(())
|
|
768
781
|
}
|
|
769
782
|
|
|
@@ -288,6 +288,23 @@ struct AttachLeaderTarget {
|
|
|
288
288
|
|
|
289
289
|
fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTarget> {
|
|
290
290
|
let mut targets = Vec::new();
|
|
291
|
+
// The caller's verified tmux server is authoritative for discovering its
|
|
292
|
+
// leader pane. Keep state-recorded worker endpoints as additional targets,
|
|
293
|
+
// but do not let a cross-socket pane-id collision hide the caller pane.
|
|
294
|
+
if let Some(endpoint) = crate::tmux_backend::socket_name_from_tmux_env() {
|
|
295
|
+
let backend = tmux_backend_for_endpoint(&endpoint);
|
|
296
|
+
let resolved_endpoint = backend.tmux_endpoint();
|
|
297
|
+
targets.extend(
|
|
298
|
+
backend
|
|
299
|
+
.list_targets()
|
|
300
|
+
.unwrap_or_default()
|
|
301
|
+
.into_iter()
|
|
302
|
+
.map(|info| AttachLeaderTarget {
|
|
303
|
+
info,
|
|
304
|
+
endpoint: resolved_endpoint.clone(),
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
291
308
|
for endpoint in state_recorded_tmux_endpoints(state) {
|
|
292
309
|
let backend = tmux_backend_for_endpoint(&endpoint);
|
|
293
310
|
let resolved_endpoint = backend.tmux_endpoint();
|
|
@@ -554,7 +571,18 @@ pub fn claim_leader(
|
|
|
554
571
|
"explicit cross-workspace claim requires an absolute tmux endpoint".to_string(),
|
|
555
572
|
));
|
|
556
573
|
}
|
|
557
|
-
|
|
574
|
+
// A live pane nonce identifies the pane instance, not this team's
|
|
575
|
+
// claim. Preserve it so another child team remains authorized.
|
|
576
|
+
let nonce = pane_binding_nonce_for_claim(
|
|
577
|
+
workspace,
|
|
578
|
+
endpoint,
|
|
579
|
+
&candidate.info.pane_id,
|
|
580
|
+
candidate
|
|
581
|
+
.info
|
|
582
|
+
.leader_env
|
|
583
|
+
.get(crate::tmux_backend::PANE_BINDING_NONCE_METADATA_KEY)
|
|
584
|
+
.map(String::as_str),
|
|
585
|
+
);
|
|
558
586
|
target.scope_authority = Some("explicit_claim".to_string());
|
|
559
587
|
target.authorized_team_workspace = Some(canonical_workspace(workspace));
|
|
560
588
|
target.binding_nonce = Some(nonce);
|
|
@@ -600,7 +628,7 @@ pub fn claim_leader(
|
|
|
600
628
|
confirm,
|
|
601
629
|
&event_log,
|
|
602
630
|
&liveness,
|
|
603
|
-
caller_target.
|
|
631
|
+
caller_target.as_mut(),
|
|
604
632
|
caller_pane_info,
|
|
605
633
|
scoped_team.map(|_| team_id.as_str()),
|
|
606
634
|
)?;
|
|
@@ -692,7 +720,7 @@ fn claim_lease_no_incident_with_target(
|
|
|
692
720
|
confirm: bool,
|
|
693
721
|
event_log: &crate::event_log::EventLog,
|
|
694
722
|
liveness: &dyn crate::state::owner_gate::PaneLivenessProbe,
|
|
695
|
-
caller_target: Option<&LeaderClaimTarget>,
|
|
723
|
+
mut caller_target: Option<&mut LeaderClaimTarget>,
|
|
696
724
|
caller_pane_info: Option<&PaneInfo>,
|
|
697
725
|
scoped_team: Option<&str>,
|
|
698
726
|
) -> Result<LeaseResult, LeaderError> {
|
|
@@ -734,9 +762,13 @@ fn claim_lease_no_incident_with_target(
|
|
|
734
762
|
let non_empty_caller_pane = NonEmptyPaneId::try_from_pane(caller_pane)?;
|
|
735
763
|
let bound_endpoint_matches_caller = bound_endpoint_matches_current_process(state);
|
|
736
764
|
if bound_pane_id.as_deref() == Some(caller_pane.as_str()) && bound_endpoint_matches_caller {
|
|
737
|
-
|
|
765
|
+
if let Some(target) = caller_target.as_deref_mut() {
|
|
766
|
+
write_claim_target_pane_nonce(target)?;
|
|
767
|
+
}
|
|
738
768
|
let current_endpoint = crate::tmux_backend::socket_name_from_tmux_env();
|
|
739
|
-
let observed_endpoint = caller_target
|
|
769
|
+
let observed_endpoint = caller_target
|
|
770
|
+
.as_deref()
|
|
771
|
+
.and_then(|target| target.endpoint.as_deref());
|
|
740
772
|
let convergence_candidate = observed_endpoint.or(current_endpoint.as_deref());
|
|
741
773
|
let candidate_source = if observed_endpoint.is_some() {
|
|
742
774
|
Some("observed_target_endpoint")
|
|
@@ -758,7 +790,7 @@ fn claim_lease_no_incident_with_target(
|
|
|
758
790
|
pane_info,
|
|
759
791
|
observed_endpoint.or(current_endpoint.as_deref()),
|
|
760
792
|
scoped_team.or(team),
|
|
761
|
-
caller_target,
|
|
793
|
+
caller_target.as_deref(),
|
|
762
794
|
)
|
|
763
795
|
});
|
|
764
796
|
if converged || observation_refreshed {
|
|
@@ -924,23 +956,33 @@ fn claim_lease_no_incident_with_target(
|
|
|
924
956
|
} else {
|
|
925
957
|
LeaseReason::VacantAcquired
|
|
926
958
|
};
|
|
927
|
-
|
|
959
|
+
if let Some(target) = caller_target.as_deref_mut() {
|
|
960
|
+
write_claim_target_pane_nonce(target)?;
|
|
961
|
+
}
|
|
928
962
|
let mut identity = leader_identity_context(workspace, Some(team_id.as_str()), Some(state))?;
|
|
929
|
-
if let Some(uuid) = caller_target
|
|
963
|
+
if let Some(uuid) = caller_target
|
|
964
|
+
.as_deref()
|
|
965
|
+
.and_then(|target| target.leader_session_uuid.as_ref()) {
|
|
930
966
|
identity.leader_session_uuid = uuid.clone();
|
|
931
967
|
}
|
|
932
968
|
let next_epoch = OwnerEpoch(pre_epoch.0.saturating_add(1));
|
|
933
|
-
let provider = caller_target
|
|
934
|
-
|
|
969
|
+
let provider = caller_target
|
|
970
|
+
.as_deref()
|
|
971
|
+
.map_or_else(|| prior_provider(state), |target| target.provider);
|
|
972
|
+
let observed_endpoint = caller_target
|
|
973
|
+
.as_deref()
|
|
974
|
+
.and_then(|target| target.endpoint.clone());
|
|
935
975
|
let mut receiver = make_receiver(
|
|
936
976
|
provider,
|
|
937
977
|
&non_empty_caller_pane,
|
|
938
978
|
&identity.leader_session_uuid,
|
|
939
979
|
next_epoch,
|
|
940
980
|
Discovery::ClaimLeader,
|
|
941
|
-
caller_target
|
|
981
|
+
caller_target
|
|
982
|
+
.as_deref()
|
|
983
|
+
.and_then(|target| target.pane_info.clone()),
|
|
942
984
|
);
|
|
943
|
-
if let Some(target) = caller_target {
|
|
985
|
+
if let Some(target) = caller_target.as_deref() {
|
|
944
986
|
receiver.scope_authority.clone_from(&target.scope_authority);
|
|
945
987
|
receiver
|
|
946
988
|
.authorized_team_workspace
|
|
@@ -1540,10 +1582,10 @@ fn workspace_claim_target_from_pane_info(
|
|
|
1540
1582
|
claim_target_from_pane_info(target)
|
|
1541
1583
|
}
|
|
1542
1584
|
|
|
1543
|
-
fn write_claim_target_pane_nonce(target:
|
|
1544
|
-
|
|
1585
|
+
fn write_claim_target_pane_nonce(target: &mut LeaderClaimTarget) -> Result<(), LeaderError> {
|
|
1586
|
+
if target.scope_authority.is_none() {
|
|
1545
1587
|
return Ok(());
|
|
1546
|
-
}
|
|
1588
|
+
}
|
|
1547
1589
|
let endpoint = target.endpoint.as_deref().ok_or_else(|| {
|
|
1548
1590
|
LeaderError::Validation("explicit claim is missing its tmux endpoint".to_string())
|
|
1549
1591
|
})?;
|
|
@@ -1557,13 +1599,30 @@ fn write_claim_target_pane_nonce(target: Option<&LeaderClaimTarget>) -> Result<(
|
|
|
1557
1599
|
let nonce = target.binding_nonce.as_deref().ok_or_else(|| {
|
|
1558
1600
|
LeaderError::Validation("explicit claim is missing its pane binding nonce".to_string())
|
|
1559
1601
|
})?;
|
|
1560
|
-
|
|
1561
|
-
.
|
|
1562
|
-
.
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1602
|
+
let live_nonce = target
|
|
1603
|
+
.pane_info
|
|
1604
|
+
.as_ref()
|
|
1605
|
+
.and_then(|info| {
|
|
1606
|
+
info.leader_env
|
|
1607
|
+
.get(crate::tmux_backend::PANE_BINDING_NONCE_METADATA_KEY)
|
|
1608
|
+
.map(String::as_str)
|
|
1566
1609
|
})
|
|
1610
|
+
.filter(|nonce| !nonce.is_empty());
|
|
1611
|
+
// The pane option is the pane-instance identity shared by all authorized
|
|
1612
|
+
// teams. Rewriting an observed value would revoke another team's grant.
|
|
1613
|
+
let effective_nonce = if let Some(live_nonce) = live_nonce {
|
|
1614
|
+
live_nonce.to_string()
|
|
1615
|
+
} else {
|
|
1616
|
+
tmux_backend_for_endpoint(endpoint)
|
|
1617
|
+
.set_pane_binding_nonce_if_unset(pane, nonce)
|
|
1618
|
+
.map_err(|error| {
|
|
1619
|
+
LeaderError::Validation(format!(
|
|
1620
|
+
"failed to bind the explicit leader pane instance: {error}"
|
|
1621
|
+
))
|
|
1622
|
+
})?
|
|
1623
|
+
};
|
|
1624
|
+
target.binding_nonce = Some(effective_nonce);
|
|
1625
|
+
Ok(())
|
|
1567
1626
|
}
|
|
1568
1627
|
|
|
1569
1628
|
fn canonical_workspace(workspace: &Path) -> PathBuf {
|
|
@@ -1591,6 +1650,18 @@ fn next_pane_binding_nonce(workspace: &Path, endpoint: &str, pane: &PaneId) -> S
|
|
|
1591
1650
|
.collect()
|
|
1592
1651
|
}
|
|
1593
1652
|
|
|
1653
|
+
fn pane_binding_nonce_for_claim(
|
|
1654
|
+
workspace: &Path,
|
|
1655
|
+
endpoint: &str,
|
|
1656
|
+
pane: &PaneId,
|
|
1657
|
+
live_nonce: Option<&str>,
|
|
1658
|
+
) -> String {
|
|
1659
|
+
live_nonce
|
|
1660
|
+
.filter(|nonce| !nonce.is_empty())
|
|
1661
|
+
.map(str::to_string)
|
|
1662
|
+
.unwrap_or_else(|| next_pane_binding_nonce(workspace, endpoint, pane))
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1594
1665
|
fn target_leader_session_uuid(target: &PaneInfo) -> Option<crate::model::ids::LeaderSessionUuid> {
|
|
1595
1666
|
target
|
|
1596
1667
|
.leader_env
|
|
@@ -2568,6 +2639,20 @@ mod tests {
|
|
|
2568
2639
|
assert_ne!(first, second);
|
|
2569
2640
|
}
|
|
2570
2641
|
|
|
2642
|
+
#[test]
|
|
2643
|
+
fn explicit_claim_generates_nonce_only_when_live_nonce_is_missing() {
|
|
2644
|
+
let workspace = Path::new("/tmp/workspace-a");
|
|
2645
|
+
let endpoint = "/tmp/team-agent-live.sock";
|
|
2646
|
+
let pane = PaneId::new("%7");
|
|
2647
|
+
let generated = pane_binding_nonce_for_claim(workspace, endpoint, &pane, None);
|
|
2648
|
+
assert_eq!(generated.len(), 32);
|
|
2649
|
+
assert!(generated.chars().all(|ch| ch.is_ascii_hexdigit()));
|
|
2650
|
+
assert_eq!(
|
|
2651
|
+
pane_binding_nonce_for_claim(workspace, endpoint, &pane, Some("live-nonce")),
|
|
2652
|
+
"live-nonce"
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2571
2656
|
#[test]
|
|
2572
2657
|
fn claim_authority_preserves_recorded_absolute_observation() {
|
|
2573
2658
|
let workspace = Path::new("/tmp/workspace-a");
|