@team-agent/installer 0.5.64 → 0.5.66
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/Cargo.toml +5 -0
- package/crates/team-agent/src/cli/adapters.rs +6 -1
- package/crates/team-agent/src/cli/emit.rs +15 -0
- package/crates/team-agent/src/cli/mod.rs +9 -8
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/tests/compile.rs +1 -1
- package/crates/team-agent/src/cli/tests/mod.rs +33 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
- package/crates/team-agent/src/cli/tests/status_send.rs +13 -3
- package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
- package/crates/team-agent/src/cli/types.rs +7 -0
- package/crates/team-agent/src/compiler/tests.rs +23 -20
- package/crates/team-agent/src/compiler.rs +25 -5
- package/crates/team-agent/src/conpty/backend.rs +16 -0
- package/crates/team-agent/src/coordinator/health.rs +17 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +55 -2
- package/crates/team-agent/src/db/migration.rs +2 -1
- package/crates/team-agent/src/db/schema.rs +16 -8
- package/crates/team-agent/src/leader/lease.rs +1 -2
- package/crates/team-agent/src/leader/start.rs +175 -10
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -2
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +7 -2
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +137 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -2
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +39 -2
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +105 -48
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -2
- package/crates/team-agent/src/lifecycle/launch.rs +117 -21
- package/crates/team-agent/src/lifecycle/restart/agent.rs +89 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +9 -12
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +59 -15
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +24 -32
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +13 -11
- package/crates/team-agent/src/lifecycle/tests/core.rs +0 -18
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +17 -30
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +11 -7
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests.rs +11 -7
- package/crates/team-agent/src/lifecycle/types.rs +0 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +115 -22
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +29 -0
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +12 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -0
- package/crates/team-agent/src/messaging/results.rs +5 -0
- package/crates/team-agent/src/messaging/send.rs +9 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +32 -0
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/wait.rs +366 -0
- package/crates/team-agent/src/messaging/watchers.rs +153 -6
- package/crates/team-agent/src/model/spec.rs +129 -6
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
- package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
- package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
- package/crates/team-agent/src/provider/bypass_flags.rs +80 -0
- package/crates/team-agent/src/provider/mod.rs +2 -0
- package/crates/team-agent/src/tmux_backend.rs +35 -12
- package/crates/team-agent/src/transport/test_support.rs +20 -0
- package/crates/team-agent/src/transport.rs +28 -11
- package/package.json +4 -4
- package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
|
@@ -1064,6 +1064,23 @@ pub fn render_event_line(event: &Value) -> Option<String> {
|
|
|
1064
1064
|
clean_field(event, &["provider"], "-"),
|
|
1065
1065
|
clean_field(event, &["matched_pattern_snippet", "snippet"], "-")
|
|
1066
1066
|
)),
|
|
1067
|
+
"result_wake.registered" => Some(format!(
|
|
1068
|
+
"result_wake.registered: task={} watcher={}",
|
|
1069
|
+
clean_field(event, &["task_id"], "-"),
|
|
1070
|
+
clean_field(event, &["watcher_id"], "-")
|
|
1071
|
+
)),
|
|
1072
|
+
"result_wake.notified" => Some(format!(
|
|
1073
|
+
"result_wake.notified: task={} result={} watcher={}",
|
|
1074
|
+
clean_field(event, &["task_id"], "-"),
|
|
1075
|
+
clean_field(event, &["result_id"], "-"),
|
|
1076
|
+
clean_field(event, &["watcher_id"], "-")
|
|
1077
|
+
)),
|
|
1078
|
+
"result_wake.notify_failed" => Some(format!(
|
|
1079
|
+
"result_wake.notify_failed: task={} watcher={} reason={}",
|
|
1080
|
+
clean_field(event, &["task_id"], "-"),
|
|
1081
|
+
clean_field(event, &["watcher_id"], "-"),
|
|
1082
|
+
clean_field(event, &["reason", "error"], "-")
|
|
1083
|
+
)),
|
|
1067
1084
|
_ => None,
|
|
1068
1085
|
}
|
|
1069
1086
|
}
|
|
@@ -578,7 +578,7 @@ fn agent_process_liveness(
|
|
|
578
578
|
// pane for POSITIVE provider evidence via current-command + worker
|
|
579
579
|
// exit marker. Under the 0.5.39 wrapper, `state.status=running` is
|
|
580
580
|
// administrative spawn accounting, not runtime truth — a provider
|
|
581
|
-
// may have exited into the shell
|
|
581
|
+
// may have exited into the inert shell tail while state still reads
|
|
582
582
|
// running. The marker probe positively proves that case; when it
|
|
583
583
|
// fires we return Dead here instead of the false Alive further
|
|
584
584
|
// down.
|
|
@@ -795,9 +795,29 @@ fn worker_provider_exit_marker_check(
|
|
|
795
795
|
.capture(&target, crate::transport::CaptureRange::Tail(200))
|
|
796
796
|
.ok()?;
|
|
797
797
|
if cap.text.contains(&marker) {
|
|
798
|
+
// The wrapper writes its exit code immediately after this marker. A
|
|
799
|
+
// truncated capture can leave the marker without a complete token;
|
|
800
|
+
// keep that case explicit rather than guessing success (or zero).
|
|
801
|
+
// Scope: this detail only covers exits that execute the wrapper's
|
|
802
|
+
// marker printf. SIGKILL, direct pane termination, and host power loss
|
|
803
|
+
// produce no marker; that absence is silent, not `rc=unknown`.
|
|
804
|
+
let rc = cap
|
|
805
|
+
.text
|
|
806
|
+
.split_once(&marker)
|
|
807
|
+
.and_then(|(_, tail)| {
|
|
808
|
+
let token = tail.split_whitespace().next()?;
|
|
809
|
+
let end = tail.find(token)? + token.len();
|
|
810
|
+
tail.as_bytes()
|
|
811
|
+
.get(end)
|
|
812
|
+
.is_some_and(u8::is_ascii_whitespace)
|
|
813
|
+
.then_some(token)
|
|
814
|
+
})
|
|
815
|
+
.and_then(|token| token.parse::<i32>().ok())
|
|
816
|
+
.map(|value| value.to_string())
|
|
817
|
+
.unwrap_or_else(|| "unknown".to_string());
|
|
798
818
|
Some(process_check(
|
|
799
819
|
ProcessLiveness::Dead,
|
|
800
|
-
format!("worker_provider_exited:{pane_id_str}"),
|
|
820
|
+
format!("worker_provider_exited:{pane_id_str}:rc={rc}"),
|
|
801
821
|
))
|
|
802
822
|
} else {
|
|
803
823
|
None
|
|
@@ -1816,6 +1836,39 @@ mod tests {
|
|
|
1816
1836
|
);
|
|
1817
1837
|
}
|
|
1818
1838
|
|
|
1839
|
+
#[test]
|
|
1840
|
+
fn worker_exit_marker_detail_includes_exit_code() {
|
|
1841
|
+
let pane_id = crate::transport::PaneId::new("%marker-rc");
|
|
1842
|
+
let marker = crate::tmux_backend::worker_provider_exit_marker("codex");
|
|
1843
|
+
let transport = crate::transport::test_support::OfflineTransport::new()
|
|
1844
|
+
.with_capture_for_pane(pane_id.as_str(), format!("\n{marker} 23\n"));
|
|
1845
|
+
let mut agent = test_abnormal_agent("/tmp/rollout.jsonl", Some(1), None);
|
|
1846
|
+
agent.pane_id = Some(pane_id.as_str().to_string());
|
|
1847
|
+
|
|
1848
|
+
let check = worker_provider_exit_marker_check(&agent, &transport).expect("marker hit");
|
|
1849
|
+
|
|
1850
|
+
assert_eq!(check.state, ProcessLiveness::Dead);
|
|
1851
|
+
assert_eq!(check.detail, "worker_provider_exited:%marker-rc:rc=23");
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
#[test]
|
|
1855
|
+
fn worker_exit_marker_detail_uses_unknown_for_truncated_exit_code() {
|
|
1856
|
+
let pane_id = crate::transport::PaneId::new("%marker-truncated");
|
|
1857
|
+
let marker = crate::tmux_backend::worker_provider_exit_marker("codex");
|
|
1858
|
+
let transport = crate::transport::test_support::OfflineTransport::new()
|
|
1859
|
+
.with_capture_for_pane(pane_id.as_str(), format!("\n{marker} "));
|
|
1860
|
+
let mut agent = test_abnormal_agent("/tmp/rollout.jsonl", Some(1), None);
|
|
1861
|
+
agent.pane_id = Some(pane_id.as_str().to_string());
|
|
1862
|
+
|
|
1863
|
+
let check = worker_provider_exit_marker_check(&agent, &transport).expect("marker hit");
|
|
1864
|
+
|
|
1865
|
+
assert_eq!(check.state, ProcessLiveness::Dead);
|
|
1866
|
+
assert_eq!(
|
|
1867
|
+
check.detail,
|
|
1868
|
+
"worker_provider_exited:%marker-truncated:rc=unknown"
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1819
1872
|
#[test]
|
|
1820
1873
|
fn abnormal_dead_fresh_error_event_reports_actual_dead_booleans() {
|
|
1821
1874
|
let dir = temp_abnormal_dir("dead-fresh");
|
|
@@ -98,6 +98,7 @@ pub const MANAGED_TABLE_LAYOUTS: &[(&str, &[&str])] = &[
|
|
|
98
98
|
"agent_id",
|
|
99
99
|
"message_id",
|
|
100
100
|
"leader_id",
|
|
101
|
+
"recipient",
|
|
101
102
|
"status",
|
|
102
103
|
"created_at",
|
|
103
104
|
"completed_at",
|
|
@@ -129,7 +130,7 @@ const CREATE_TABLE_TEMPLATES: &[(&str, &str)] = &[
|
|
|
129
130
|
("delivery_tokens", "create table if not exists __TABLE__ (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )"),
|
|
130
131
|
("agent_health", "create table if not exists __TABLE__ (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )"),
|
|
131
132
|
("peer_allowlist", "create table if not exists __TABLE__ (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )"),
|
|
132
|
-
("result_watchers", "create table if not exists __TABLE__ (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )"),
|
|
133
|
+
("result_watchers", "create table if not exists __TABLE__ (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n recipient text,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )"),
|
|
133
134
|
("leader_notification_log", "create table if not exists __TABLE__ (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )"),
|
|
134
135
|
];
|
|
135
136
|
|
|
@@ -13,7 +13,7 @@ use rusqlite::Connection;
|
|
|
13
13
|
use crate::db::DbError;
|
|
14
14
|
|
|
15
15
|
/// `schema.py:90`。
|
|
16
|
-
pub const SCHEMA_VERSION: i64 =
|
|
16
|
+
pub const SCHEMA_VERSION: i64 = 5;
|
|
17
17
|
|
|
18
18
|
/// 8 张表的 DDL(逐字照搬 `schema.py:initialize_schema` 的内联建表;含 `if not exists`)。
|
|
19
19
|
/// 顺序与 Python 一致(leader_notification_log 在 ensure 块后创建)。
|
|
@@ -23,7 +23,7 @@ const CREATE_SCHEDULED_EVENTS: &str = "create table if not exists scheduled_even
|
|
|
23
23
|
const CREATE_DELIVERY_TOKENS: &str = "create table if not exists delivery_tokens (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )";
|
|
24
24
|
const CREATE_AGENT_HEALTH: &str = "create table if not exists agent_health (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
|
|
25
25
|
const CREATE_PEER_ALLOWLIST: &str = "create table if not exists peer_allowlist (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )";
|
|
26
|
-
const CREATE_RESULT_WATCHERS: &str = "create table if not exists result_watchers (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )";
|
|
26
|
+
const CREATE_RESULT_WATCHERS: &str = "create table if not exists result_watchers (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n recipient text,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )";
|
|
27
27
|
const CREATE_LEADER_NOTIFICATION_LOG: &str = "create table if not exists leader_notification_log (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )";
|
|
28
28
|
const CREATE_AGENT_HEALTH_NEW: &str = "create table agent_health_new (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
|
|
29
29
|
|
|
@@ -104,6 +104,7 @@ const RESULT_WATCHER_COLUMNS: &[&str] = &[
|
|
|
104
104
|
"agent_id",
|
|
105
105
|
"message_id",
|
|
106
106
|
"leader_id",
|
|
107
|
+
"recipient",
|
|
107
108
|
"status",
|
|
108
109
|
"created_at",
|
|
109
110
|
"completed_at",
|
|
@@ -297,10 +298,16 @@ pub fn initialize_schema(
|
|
|
297
298
|
&tx,
|
|
298
299
|
"result_watchers",
|
|
299
300
|
RESULT_WATCHER_COLUMNS,
|
|
300
|
-
&[
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
&[
|
|
302
|
+
(
|
|
303
|
+
"owner_team_id",
|
|
304
|
+
"alter table result_watchers add column owner_team_id text",
|
|
305
|
+
),
|
|
306
|
+
(
|
|
307
|
+
"recipient",
|
|
308
|
+
"alter table result_watchers add column recipient text",
|
|
309
|
+
),
|
|
310
|
+
],
|
|
304
311
|
)?;
|
|
305
312
|
tx.execute(CREATE_LEADER_NOTIFICATION_LOG, [])?;
|
|
306
313
|
ensure_table_columns(
|
|
@@ -360,13 +367,13 @@ mod tests {
|
|
|
360
367
|
}
|
|
361
368
|
|
|
362
369
|
#[test]
|
|
363
|
-
fn
|
|
370
|
+
fn user_version_is_five() {
|
|
364
371
|
let conn = fresh();
|
|
365
372
|
let v: i64 = conn
|
|
366
373
|
.query_row("pragma user_version", [], |r| r.get(0))
|
|
367
374
|
.unwrap();
|
|
368
375
|
assert_eq!(v, SCHEMA_VERSION);
|
|
369
|
-
assert_eq!(v,
|
|
376
|
+
assert_eq!(v, 5);
|
|
370
377
|
}
|
|
371
378
|
|
|
372
379
|
#[test]
|
|
@@ -513,6 +520,7 @@ mod tests {
|
|
|
513
520
|
("agent_id", "TEXT", 0, None, 0),
|
|
514
521
|
("message_id", "TEXT", 0, None, 0),
|
|
515
522
|
("leader_id", "TEXT", 1, None, 0),
|
|
523
|
+
("recipient", "TEXT", 0, None, 0),
|
|
516
524
|
("status", "TEXT", 1, None, 0),
|
|
517
525
|
("created_at", "TEXT", 1, None, 0),
|
|
518
526
|
("completed_at", "TEXT", 0, None, 0),
|
|
@@ -505,7 +505,6 @@ pub fn claim_leader(
|
|
|
505
505
|
team: Option<&str>,
|
|
506
506
|
confirm: bool,
|
|
507
507
|
) -> Result<LeaseResult, LeaderError> {
|
|
508
|
-
let _ = confirm;
|
|
509
508
|
let caller = std::env::var("TMUX_PANE")
|
|
510
509
|
.ok()
|
|
511
510
|
.filter(|pane| !pane.is_empty())
|
|
@@ -598,7 +597,7 @@ pub fn claim_leader(
|
|
|
598
597
|
Some(team_id.as_str()),
|
|
599
598
|
&team_id,
|
|
600
599
|
&PaneId::new(caller),
|
|
601
|
-
|
|
600
|
+
confirm,
|
|
602
601
|
&event_log,
|
|
603
602
|
&liveness,
|
|
604
603
|
caller_target.as_ref(),
|
|
@@ -1059,6 +1059,16 @@ fn execute_managed_leader_plan_attempt(
|
|
|
1059
1059
|
workspace,
|
|
1060
1060
|
session_existed_before,
|
|
1061
1061
|
)?;
|
|
1062
|
+
// A-55 (window rename race): tmux defaults `allow-rename` + `automatic-rename`
|
|
1063
|
+
// ON, so the pane's shell wrapper can print an OSC title-set escape during
|
|
1064
|
+
// init (zshrc/starship/PROMPT_COMMAND) and rename the window away from WINDOW
|
|
1065
|
+
// between spawn-poll and attach — making `attach-session -t SESSION:WINDOW`
|
|
1066
|
+
// fail with "can't find window: WINDOW". Lock both off right after spawn,
|
|
1067
|
+
// before run_leader_argv. Best-effort: a failure must NOT abort the launcher
|
|
1068
|
+
// (old tmux may lack the option), but it is audited below.
|
|
1069
|
+
let (rename_result, rename_stderr) =
|
|
1070
|
+
lock_down_window_rename(&transport, session, &spawned.window);
|
|
1071
|
+
write_window_rename_lockdown_event(workspace, &rename_result, &rename_stderr);
|
|
1062
1072
|
if let Err(error) = persist_managed_leader_binding(plan, workspace, &spawned) {
|
|
1063
1073
|
cleanup_managed_leader_resources(
|
|
1064
1074
|
&transport,
|
|
@@ -1269,12 +1279,12 @@ fn ensure_managed_provider_live_after_attach(
|
|
|
1269
1279
|
/// 0.4.x (CR C-3 P0): leader provider health reconciliation. The default
|
|
1270
1280
|
/// `liveness()` check only proves the pane is ADDRESSABLE via tmux — it
|
|
1271
1281
|
/// returns `Live` even when the provider has exited and the wrapper shell
|
|
1272
|
-
///
|
|
1282
|
+
/// remains at its inert tail. This function distinguishes
|
|
1273
1283
|
/// `provider_alive` from `provider_exited` by:
|
|
1274
1284
|
/// 1. Reading `pane_current_command` (tmux `#{pane_current_command}`).
|
|
1275
1285
|
/// 2. If the current command matches the expected provider binary (or
|
|
1276
1286
|
/// one of its known aliases), report `Alive`.
|
|
1277
|
-
/// 3. If the current command is
|
|
1287
|
+
/// 3. If the current command is a shell tail process AND the pane
|
|
1278
1288
|
/// content contains the exit marker `[team-agent] <provider> exited`
|
|
1279
1289
|
/// (emitted by `leader_shell_wrapper_command`), report
|
|
1280
1290
|
/// `ProviderExited`.
|
|
@@ -1282,7 +1292,7 @@ fn ensure_managed_provider_live_after_attach(
|
|
|
1282
1292
|
/// conservative default — avoid false-positive exit alarms.
|
|
1283
1293
|
///
|
|
1284
1294
|
/// Note: when the leader shell wrapper is used (CR C-2), a provider exit
|
|
1285
|
-
/// leaves the pane as
|
|
1295
|
+
/// leaves the pane as an inert `/bin/sh -c` tail with the exit marker in
|
|
1286
1296
|
/// scrollback. Pre-wrapper code that hit `exec claude` would have left the
|
|
1287
1297
|
/// pane as `[exited]` and `liveness()` would have returned `Dead`. The new
|
|
1288
1298
|
/// failure mode requires this richer health check to surface
|
|
@@ -1340,16 +1350,16 @@ pub enum LeaderProviderHealth {
|
|
|
1340
1350
|
Unreachable,
|
|
1341
1351
|
}
|
|
1342
1352
|
|
|
1343
|
-
/// 0.4.x (CR R6 + R3): single-source
|
|
1353
|
+
/// 0.4.x (CR R6 + R3): single-source shell-tail detection.
|
|
1344
1354
|
/// Used by:
|
|
1345
|
-
/// - `leader_provider_health` to decide "pane
|
|
1346
|
-
/// - shutdown logic to recognise a leader pane in
|
|
1355
|
+
/// - `leader_provider_health` to decide "pane is at the shell tail"
|
|
1356
|
+
/// - shutdown logic to recognise a leader pane in shell-tail mode
|
|
1347
1357
|
/// as still owned by the leader (not stray).
|
|
1348
1358
|
///
|
|
1349
1359
|
/// Matches by basename (case-insensitive) — `pane_current_command` returns
|
|
1350
1360
|
/// the basename of the running binary. Conservative whitelist of POSIX +
|
|
1351
|
-
/// common
|
|
1352
|
-
/// (shell looks like provider absent → health says Alive) which is the
|
|
1361
|
+
/// common shell processes; missing entries here are false negatives
|
|
1362
|
+
/// (shell tail looks like provider absent → health says Alive) which is the
|
|
1353
1363
|
/// safe default per the CR R6 conservative-Alive rule.
|
|
1354
1364
|
fn is_interactive_shell_basename(name: &str) -> bool {
|
|
1355
1365
|
let trimmed = name.trim().to_ascii_lowercase();
|
|
@@ -2246,6 +2256,44 @@ fn write_leader_startup_prompt_event(workspace: &Path, event: &str, fields: serd
|
|
|
2246
2256
|
let _ = crate::event_log::EventLog::new(workspace).write(event, fields);
|
|
2247
2257
|
}
|
|
2248
2258
|
|
|
2259
|
+
/// A-55 (window rename race): lock the freshly-spawned leader window against
|
|
2260
|
+
/// rename — `allow-rename off` (refuse pane-internal OSC title-set renames)
|
|
2261
|
+
/// AND `automatic-rename off` (refuse tmux's own command-driven rename). Both
|
|
2262
|
+
/// must be set; tmux treats them as independent. Best-effort: a failure must
|
|
2263
|
+
/// NOT abort the launcher (old tmux may lack the option) — return the outcome
|
|
2264
|
+
/// for audit instead of propagating an error. Returns (result, tmux_stderr).
|
|
2265
|
+
fn lock_down_window_rename(
|
|
2266
|
+
transport: &dyn Transport,
|
|
2267
|
+
session: &SessionName,
|
|
2268
|
+
window: &WindowName,
|
|
2269
|
+
) -> (String, String) {
|
|
2270
|
+
let allow_rename = transport.set_window_option(session, window, "allow-rename", "off");
|
|
2271
|
+
let automatic_rename = transport.set_window_option(session, window, "automatic-rename", "off");
|
|
2272
|
+
let (result, stderr) = match (&allow_rename, &automatic_rename) {
|
|
2273
|
+
(Ok(()), Ok(())) => ("ok".to_string(), String::new()),
|
|
2274
|
+
(Ok(()), Err(error)) => ("failed".to_string(), error.to_string()),
|
|
2275
|
+
(Err(error), Ok(())) => ("failed".to_string(), error.to_string()),
|
|
2276
|
+
(Err(a), Err(b)) => (
|
|
2277
|
+
"failed".to_string(),
|
|
2278
|
+
format!("allow-rename: {a}; automatic-rename: {b}"),
|
|
2279
|
+
),
|
|
2280
|
+
};
|
|
2281
|
+
(result, stderr)
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/// A-55: audit the rename-lockdown outcome to the event log. Failure does not
|
|
2285
|
+
/// abort the launcher; record ok/failed + tmux stderr for regression triage.
|
|
2286
|
+
fn write_window_rename_lockdown_event(workspace: &Path, result: &str, stderr: &str) {
|
|
2287
|
+
write_leader_startup_prompt_event(
|
|
2288
|
+
workspace,
|
|
2289
|
+
"leader.launcher.window_rename_lockdown",
|
|
2290
|
+
serde_json::json!({
|
|
2291
|
+
"result": result,
|
|
2292
|
+
"tmux_stderr": stderr,
|
|
2293
|
+
}),
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2249
2297
|
fn ensure_tmux_installed() -> Result<(), LeaderError> {
|
|
2250
2298
|
match Command::new("tmux").arg("-V").output() {
|
|
2251
2299
|
Ok(output) if output.status.success() => Ok(()),
|
|
@@ -2350,8 +2398,8 @@ mod tests {
|
|
|
2350
2398
|
use super::{
|
|
2351
2399
|
ensure_managed_provider_live_after_attach, execute_leader_plan_after_ambient_authority,
|
|
2352
2400
|
format_launcher_failure, handle_exec_provider_startup_prompts,
|
|
2353
|
-
is_interactive_shell_basename, push_bounded_stderr, shlex_quote,
|
|
2354
|
-
VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
|
|
2401
|
+
is_interactive_shell_basename, lock_down_window_rename, push_bounded_stderr, shlex_quote,
|
|
2402
|
+
write_window_rename_lockdown_event, VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
|
|
2355
2403
|
};
|
|
2356
2404
|
|
|
2357
2405
|
struct ScriptedTransport {
|
|
@@ -2359,6 +2407,8 @@ mod tests {
|
|
|
2359
2407
|
sent: Mutex<Vec<(Target, Vec<Key>)>>,
|
|
2360
2408
|
liveness: PaneLiveness,
|
|
2361
2409
|
targets: Vec<PaneInfo>,
|
|
2410
|
+
window_options: Mutex<Vec<(String, String)>>,
|
|
2411
|
+
window_option_error: Mutex<Option<String>>,
|
|
2362
2412
|
}
|
|
2363
2413
|
|
|
2364
2414
|
impl ScriptedTransport {
|
|
@@ -2368,6 +2418,8 @@ mod tests {
|
|
|
2368
2418
|
sent: Mutex::new(Vec::new()),
|
|
2369
2419
|
liveness: PaneLiveness::Unknown,
|
|
2370
2420
|
targets: Vec::new(),
|
|
2421
|
+
window_options: Mutex::new(Vec::new()),
|
|
2422
|
+
window_option_error: Mutex::new(None),
|
|
2371
2423
|
}
|
|
2372
2424
|
}
|
|
2373
2425
|
|
|
@@ -2377,6 +2429,8 @@ mod tests {
|
|
|
2377
2429
|
sent: Mutex::new(Vec::new()),
|
|
2378
2430
|
liveness,
|
|
2379
2431
|
targets: Vec::new(),
|
|
2432
|
+
window_options: Mutex::new(Vec::new()),
|
|
2433
|
+
window_option_error: Mutex::new(None),
|
|
2380
2434
|
}
|
|
2381
2435
|
}
|
|
2382
2436
|
|
|
@@ -2386,6 +2440,21 @@ mod tests {
|
|
|
2386
2440
|
sent: Mutex::new(Vec::new()),
|
|
2387
2441
|
liveness,
|
|
2388
2442
|
targets,
|
|
2443
|
+
window_options: Mutex::new(Vec::new()),
|
|
2444
|
+
window_option_error: Mutex::new(None),
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/// A-55: a ScriptedTransport whose `set_window_option` always fails with
|
|
2449
|
+
/// the given tmux stderr — proves the lockdown never aborts the launcher.
|
|
2450
|
+
fn with_window_option_error(error: &str) -> Self {
|
|
2451
|
+
Self {
|
|
2452
|
+
screens: Mutex::new(Vec::new()),
|
|
2453
|
+
sent: Mutex::new(Vec::new()),
|
|
2454
|
+
liveness: PaneLiveness::Unknown,
|
|
2455
|
+
targets: Vec::new(),
|
|
2456
|
+
window_options: Mutex::new(Vec::new()),
|
|
2457
|
+
window_option_error: Mutex::new(Some(error.to_string())),
|
|
2389
2458
|
}
|
|
2390
2459
|
}
|
|
2391
2460
|
|
|
@@ -2395,6 +2464,14 @@ mod tests {
|
|
|
2395
2464
|
Err(poisoned) => poisoned.into_inner().clone(),
|
|
2396
2465
|
}
|
|
2397
2466
|
}
|
|
2467
|
+
|
|
2468
|
+
/// All `set-window-option option value` pairs, in call order.
|
|
2469
|
+
fn window_options(&self) -> Vec<(String, String)> {
|
|
2470
|
+
match self.window_options.lock() {
|
|
2471
|
+
Ok(guard) => guard.clone(),
|
|
2472
|
+
Err(poisoned) => poisoned.into_inner().clone(),
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2398
2475
|
}
|
|
2399
2476
|
|
|
2400
2477
|
impl Transport for ScriptedTransport {
|
|
@@ -2511,6 +2588,34 @@ mod tests {
|
|
|
2511
2588
|
Ok(SetEnvOutcome::Applied)
|
|
2512
2589
|
}
|
|
2513
2590
|
|
|
2591
|
+
fn set_window_option(
|
|
2592
|
+
&self,
|
|
2593
|
+
_session: &SessionName,
|
|
2594
|
+
_window: &WindowName,
|
|
2595
|
+
option: &str,
|
|
2596
|
+
value: &str,
|
|
2597
|
+
) -> Result<(), TransportError> {
|
|
2598
|
+
if let Some(stderr) = self
|
|
2599
|
+
.window_option_error
|
|
2600
|
+
.lock()
|
|
2601
|
+
.ok()
|
|
2602
|
+
.and_then(|guard| guard.clone())
|
|
2603
|
+
{
|
|
2604
|
+
return Err(TransportError::Subprocess {
|
|
2605
|
+
argv: vec!["tmux".to_string(), "set-window-option".to_string()],
|
|
2606
|
+
code: Some(1),
|
|
2607
|
+
stderr,
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
match self.window_options.lock() {
|
|
2611
|
+
Ok(mut guard) => guard.push((option.to_string(), value.to_string())),
|
|
2612
|
+
Err(poisoned) => poisoned
|
|
2613
|
+
.into_inner()
|
|
2614
|
+
.push((option.to_string(), value.to_string())),
|
|
2615
|
+
}
|
|
2616
|
+
Ok(())
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2514
2619
|
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
2515
2620
|
Ok(())
|
|
2516
2621
|
}
|
|
@@ -2988,4 +3093,64 @@ mod tests {
|
|
|
2988
3093
|
assert_eq!(captured.len(), LEADER_STDERR_LIMIT);
|
|
2989
3094
|
assert!(captured.ends_with(b"XYZ"));
|
|
2990
3095
|
}
|
|
3096
|
+
|
|
3097
|
+
#[test]
|
|
3098
|
+
fn managed_leader_launch_disables_window_rename() {
|
|
3099
|
+
let transport = ScriptedTransport::with_liveness(PaneLiveness::Live);
|
|
3100
|
+
let session = SessionName::new("team-agent-leader-claude_code-demo");
|
|
3101
|
+
let window = WindowName::new("claude_code");
|
|
3102
|
+
|
|
3103
|
+
let (result, stderr) = lock_down_window_rename(&transport, &session, &window);
|
|
3104
|
+
|
|
3105
|
+
assert_eq!(result, "ok", "both set-window-option calls must succeed");
|
|
3106
|
+
assert!(stderr.is_empty());
|
|
3107
|
+
let options = transport.window_options();
|
|
3108
|
+
assert_eq!(
|
|
3109
|
+
options,
|
|
3110
|
+
vec![
|
|
3111
|
+
("allow-rename".to_string(), "off".to_string()),
|
|
3112
|
+
("automatic-rename".to_string(), "off".to_string()),
|
|
3113
|
+
],
|
|
3114
|
+
"both rename lockdown options must be issued, in order"
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
#[test]
|
|
3119
|
+
fn launcher_survives_set_window_option_failure() {
|
|
3120
|
+
let workspace =
|
|
3121
|
+
std::env::temp_dir().join(format!("ta-rename-lockdown-fail-{}", std::process::id()));
|
|
3122
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
3123
|
+
let transport = ScriptedTransport::with_window_option_error(
|
|
3124
|
+
"set-window-option: unknown option: allow-rename",
|
|
3125
|
+
);
|
|
3126
|
+
let session = SessionName::new("team-agent-leader-claude_code-demo");
|
|
3127
|
+
let window = WindowName::new("claude_code");
|
|
3128
|
+
|
|
3129
|
+
// Lockdown must NOT abort the launcher: it returns a failed outcome
|
|
3130
|
+
// (recorded for audit) instead of propagating an error.
|
|
3131
|
+
let (result, stderr) = lock_down_window_rename(&transport, &session, &window);
|
|
3132
|
+
|
|
3133
|
+
assert_eq!(result, "failed", "a set-window-option failure is audited, not fatal");
|
|
3134
|
+
assert!(
|
|
3135
|
+
stderr.contains("unknown option"),
|
|
3136
|
+
"tmux stderr must surface in the audit payload: {stderr}"
|
|
3137
|
+
);
|
|
3138
|
+
|
|
3139
|
+
// The audit event lands in the workspace event log with result=failed.
|
|
3140
|
+
write_window_rename_lockdown_event(&workspace, &result, &stderr);
|
|
3141
|
+
let events = crate::event_log::EventLog::new(&workspace).tail(0).unwrap();
|
|
3142
|
+
let lockdown = events
|
|
3143
|
+
.iter()
|
|
3144
|
+
.find(|event| {
|
|
3145
|
+
event["event"] == "leader.launcher.window_rename_lockdown"
|
|
3146
|
+
})
|
|
3147
|
+
.expect("rename_lockdown event must be written even on failure");
|
|
3148
|
+
assert_eq!(lockdown["result"], "failed");
|
|
3149
|
+
assert!(
|
|
3150
|
+
lockdown["tmux_stderr"].as_str().unwrap_or_default().contains("unknown option"),
|
|
3151
|
+
"event must carry tmux stderr for regression triage: {lockdown}"
|
|
3152
|
+
);
|
|
3153
|
+
|
|
3154
|
+
std::fs::remove_dir_all(&workspace).ok();
|
|
3155
|
+
}
|
|
2991
3156
|
}
|
|
@@ -1054,7 +1054,7 @@ fn team_dir(root: &Path, name: &str, agents: &[(&str, &str)]) -> PathBuf {
|
|
|
1054
1054
|
|
|
1055
1055
|
fn role_doc(name: &str, role: &str) -> String {
|
|
1056
1056
|
format!(
|
|
1057
|
-
"---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{role}.\n"
|
|
1057
|
+
"---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\n{role}.\n"
|
|
1058
1058
|
)
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
@@ -337,7 +337,6 @@ pub(super) fn add_agent_with_transport_at_paths(
|
|
|
337
337
|
)));
|
|
338
338
|
}
|
|
339
339
|
inject_agent_into_spec(&mut spec, compiled.agent, &compiled.id)?;
|
|
340
|
-
let safety = effective_runtime_config(&spec)?;
|
|
341
340
|
// E5 spec 迁移:重编译的 spec 原子写到 .team/runtime/<team_key>/(不落用户目录 team_dir)。
|
|
342
341
|
let spec_path = crate::model::paths::runtime_spec_path(run_workspace, &canonical_team_key);
|
|
343
342
|
// E42 (0.3.24 P0): capture pre-write bytes for atomic rollback. If anything
|
|
@@ -362,7 +361,6 @@ pub(super) fn add_agent_with_transport_at_paths(
|
|
|
362
361
|
agent_id,
|
|
363
362
|
&meta,
|
|
364
363
|
role_file_path,
|
|
365
|
-
&safety,
|
|
366
364
|
) {
|
|
367
365
|
rollback_add_agent_atomic(
|
|
368
366
|
run_workspace,
|
|
@@ -116,7 +116,6 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
116
116
|
agent_id: &AgentId,
|
|
117
117
|
meta: &Value,
|
|
118
118
|
dynamic_role_file: &Path,
|
|
119
|
-
safety: &DangerousApproval,
|
|
120
119
|
) -> Result<(), LifecycleError> {
|
|
121
120
|
let mut state =
|
|
122
121
|
crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
@@ -195,7 +194,13 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
195
194
|
}
|
|
196
195
|
}
|
|
197
196
|
if let Some(obj) = entry.as_object_mut() {
|
|
198
|
-
|
|
197
|
+
// 0.5.66 bypass 单源:policy 从 role 文档字段派生,不再用全队 `DangerousApproval`。
|
|
198
|
+
let meta_provider = meta
|
|
199
|
+
.get("provider")
|
|
200
|
+
.and_then(Value::as_str)
|
|
201
|
+
.and_then(crate::provider::wire::parse_provider)
|
|
202
|
+
.unwrap_or(Provider::Codex);
|
|
203
|
+
persist_effective_approval_policy_from_yaml_agent(obj, meta, meta_provider);
|
|
199
204
|
}
|
|
200
205
|
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
201
206
|
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|