@team-agent/installer 0.5.41 → 0.5.43
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 +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +27 -7
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +47 -52
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/coordinator/tick.rs +19 -1
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +52 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +195 -82
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +6 -1
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +21 -22
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/repository.rs +27 -14
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +27 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -1,246 +1,388 @@
|
|
|
1
1
|
use super::*;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
3
|
+
#[test]
|
|
4
|
+
#[serial_test::serial(env)]
|
|
5
|
+
fn d1_claim_team_owner_includes_os_user() {
|
|
6
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
7
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
8
|
+
let ws = p2_temp_ws("d1_owner_keys");
|
|
9
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
10
|
+
let r = claim_lease_no_incident(
|
|
11
|
+
&ws,
|
|
12
|
+
&mut state,
|
|
13
|
+
None,
|
|
14
|
+
&TeamKey::new("current"),
|
|
15
|
+
&PaneId::new("%5"),
|
|
16
|
+
false,
|
|
17
|
+
&crate::event_log::EventLog::new(&ws),
|
|
18
|
+
&seeded_liveness(&["%5"]),
|
|
19
|
+
)
|
|
20
|
+
.unwrap();
|
|
21
|
+
assert!(
|
|
22
|
+
r.ok && r.status == LeaseStatus::Claimed,
|
|
23
|
+
"precondition: vacant acquire"
|
|
24
|
+
);
|
|
25
|
+
// Stage 3d (identity-boundary unified plan, architect direction
|
|
26
|
+
// 2026-06-23): canonical owner now at teams.<team_key>.team_owner.
|
|
27
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
28
|
+
let owner = state["teams"][&team_key]["team_owner"]
|
|
29
|
+
.as_object()
|
|
30
|
+
.expect("Stage 3d: team_owner at canonical teams.<team_key>");
|
|
31
|
+
assert!(
|
|
32
|
+
owner.contains_key("os_user"),
|
|
33
|
+
"golden claim-path team_owner carries os_user even when empty; keys={:?}",
|
|
34
|
+
owner.keys().collect::<Vec<_>>()
|
|
35
|
+
);
|
|
36
|
+
let keys: Vec<&str> = owner.keys().map(String::as_str).collect();
|
|
37
|
+
assert_eq!(
|
|
38
|
+
keys,
|
|
39
|
+
vec![
|
|
40
|
+
"pane_id",
|
|
41
|
+
"provider",
|
|
42
|
+
"machine_fingerprint",
|
|
43
|
+
"leader_session_uuid",
|
|
44
|
+
"owner_epoch",
|
|
45
|
+
"claimed_at",
|
|
46
|
+
"claimed_via",
|
|
47
|
+
"os_user"
|
|
48
|
+
],
|
|
49
|
+
"golden new_owner includes os_user"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// D2 [BLOCK] — claim-path leader_receiver is golden's 15 keys in golden order; NO
|
|
54
|
+
// fingerprint/requested_provider/warning. Golden _receiver_from_claim_target (__init__.py:861-877).
|
|
55
|
+
// Rust LeaderReceiver serializes all 17 (no skip_serializing_if) -> 3 always-null extras leak. RED.
|
|
56
|
+
// (The POPULATED tmux values session_name/window_*/pane_* come from the caller-target scan — a
|
|
57
|
+
// deferred real-tmux seam, see d2_receiver_populated_from_caller_target_seam; the KEY-SET + ORDER
|
|
58
|
+
// locked here are unchanged by that scan.)
|
|
59
|
+
#[test]
|
|
60
|
+
#[serial_test::serial(env)]
|
|
61
|
+
fn d2_claim_leader_receiver_is_fifteen_golden_keys_in_order_no_extras() {
|
|
62
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
63
|
+
let _e = EnvGuard::apply(&[
|
|
64
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
65
|
+
("TMUX", Some("/tmp/tmux-501/default,123,0")),
|
|
66
|
+
]);
|
|
67
|
+
let ws = p2_temp_ws("d2_recv_keys");
|
|
68
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
69
|
+
let r = claim_lease_no_incident(
|
|
70
|
+
&ws,
|
|
71
|
+
&mut state,
|
|
72
|
+
None,
|
|
73
|
+
&TeamKey::new("current"),
|
|
74
|
+
&PaneId::new("%5"),
|
|
75
|
+
false,
|
|
76
|
+
&crate::event_log::EventLog::new(&ws),
|
|
77
|
+
&seeded_liveness(&["%5"]),
|
|
78
|
+
)
|
|
79
|
+
.unwrap();
|
|
80
|
+
assert!(
|
|
81
|
+
r.ok && r.status == LeaseStatus::Claimed,
|
|
82
|
+
"precondition: vacant acquire"
|
|
83
|
+
);
|
|
84
|
+
// Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
|
|
85
|
+
let team_key = crate::state::projection::team_state_key(&state);
|
|
86
|
+
let recv = state["teams"][&team_key]["leader_receiver"]
|
|
87
|
+
.as_object()
|
|
88
|
+
.expect("Stage 3d: leader_receiver at canonical teams.<team_key>");
|
|
89
|
+
for extra in ["fingerprint", "requested_provider", "warning"] {
|
|
90
|
+
assert!(!recv.contains_key(extra),
|
|
52
91
|
"golden leader_receiver has NO '{extra}' key (Rust must add skip_serializing_if); keys={:?}", recv.keys().collect::<Vec<_>>());
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
92
|
+
}
|
|
93
|
+
let keys: Vec<&str> = recv.keys().map(String::as_str).collect();
|
|
94
|
+
assert_eq!(keys, vec![
|
|
56
95
|
"mode","status","provider","pane_id","session_name","window_index","window_name",
|
|
57
96
|
"pane_index","pane_tty","pane_current_command","tmux_socket","leader_session_uuid",
|
|
58
97
|
"owner_epoch","attached_at","discovery",
|
|
59
98
|
], "golden _receiver_from_claim_target 15-key set + ORDER (__init__.py:861-877 + BUG-4 socket-qualified receiver)");
|
|
60
|
-
|
|
99
|
+
}
|
|
61
100
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
// D2 seam — the caller-target SCAN that fills session_name/window_index/window_name/pane_index/
|
|
102
|
+
// pane_tty/pane_current_command from core_list_targets (golden _receiver_from_claim_target reads
|
|
103
|
+
// target[...]). Rust make_receiver leaves them None (no scan). Real-tmux: needs core_list_targets.
|
|
104
|
+
#[test]
|
|
105
|
+
#[ignore = "real-tmux seam: leader_receiver session_name/window_*/pane_* are populated from the \
|
|
67
106
|
caller target via core_list_targets (golden _receiver_from_claim_target); Rust has no \
|
|
68
107
|
scan (values null). Porter wires the target scan; this asserts the populated values."]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
108
|
+
fn d2_receiver_populated_from_caller_target_seam() {
|
|
109
|
+
// Golden contract: new_receiver.session_name == caller_target.session_name, etc. Needs a live
|
|
110
|
+
// tmux target list (core_list_targets) — out of in-process scope.
|
|
111
|
+
}
|
|
73
112
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
// D3 [BLOCK] — bound_pane is RECEIVER-first (golden :624 receiver.pane_id OR owner.pane_id). PROBE-B
|
|
114
|
+
// (receiver=%9, owner=%1, caller=%9): golden ok=True already_bound. Rust derives OWNER-first
|
|
115
|
+
// (owner %1 != caller %9) -> owner live + !confirm -> refused force_confirm_required. RED.
|
|
116
|
+
#[test]
|
|
117
|
+
#[serial_test::serial(env)]
|
|
118
|
+
fn d3_bound_pane_receiver_first_reclaim_from_receiver_pane_is_already_bound() {
|
|
119
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
120
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
121
|
+
let ws = p2_temp_ws("d3_receiver_first");
|
|
122
|
+
let mut state = serde_json::json!({
|
|
123
|
+
"session_name": "team-agent-x",
|
|
124
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
125
|
+
"leader_receiver": {"pane_id":"%9","owner_epoch":3,"leader_session_uuid":"U"},
|
|
126
|
+
});
|
|
127
|
+
let r = claim_lease_no_incident(
|
|
128
|
+
&ws,
|
|
129
|
+
&mut state,
|
|
130
|
+
None,
|
|
131
|
+
&TeamKey::new("current"),
|
|
132
|
+
&PaneId::new("%9"),
|
|
133
|
+
false,
|
|
134
|
+
&crate::event_log::EventLog::new(&ws),
|
|
135
|
+
&seeded_liveness(&["%9", "%1"]),
|
|
136
|
+
)
|
|
137
|
+
.unwrap();
|
|
138
|
+
assert!(
|
|
139
|
+
r.ok,
|
|
140
|
+
"a re-claim from the bound RECEIVER pane (%9) is idempotent ok=true; got {r:?}"
|
|
141
|
+
);
|
|
142
|
+
assert_eq!(r.status, LeaseStatus::AlreadyBound,
|
|
92
143
|
"golden :624 receiver-first: bound_pane=receiver(%9)==caller(%9) -> already_bound, NOT force_confirm");
|
|
93
|
-
|
|
144
|
+
}
|
|
94
145
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
146
|
+
// D4 [WARN] — precheck_epoch uses Python truthiness: owner_epoch=0 is FALSY -> falls through to
|
|
147
|
+
// receiver_epoch=5 (golden _lease_epoch :400 int(owner.epoch or recv.epoch or 0)). Rust
|
|
148
|
+
// current_owner_epoch Some(0) stops the chain -> 0. Surfaces in the force_confirm refusal. RED.
|
|
149
|
+
#[test]
|
|
150
|
+
#[serial_test::serial(env)]
|
|
151
|
+
fn d4_precheck_epoch_zero_is_falsy_falls_through_to_receiver() {
|
|
152
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
153
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
154
|
+
let ws = p2_temp_ws("d4_epoch_falsy");
|
|
155
|
+
let mut state = serde_json::json!({
|
|
156
|
+
"session_name": "team-agent-x",
|
|
157
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":0,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
158
|
+
"leader_receiver": {"pane_id":"%1","owner_epoch":5,"leader_session_uuid":"U"},
|
|
159
|
+
});
|
|
160
|
+
let r = claim_lease_no_incident(
|
|
161
|
+
&ws,
|
|
162
|
+
&mut state,
|
|
163
|
+
None,
|
|
164
|
+
&TeamKey::new("current"),
|
|
165
|
+
&PaneId::new("%2"),
|
|
166
|
+
false,
|
|
167
|
+
&crate::event_log::EventLog::new(&ws),
|
|
168
|
+
&seeded_liveness(&["%1", "%2"]),
|
|
169
|
+
)
|
|
170
|
+
.unwrap();
|
|
171
|
+
assert_eq!(
|
|
172
|
+
r.status,
|
|
173
|
+
LeaseStatus::Refused,
|
|
174
|
+
"precondition: live owner %1 + no confirm -> force_confirm"
|
|
175
|
+
);
|
|
176
|
+
assert_eq!(
|
|
177
|
+
r.owner_epoch,
|
|
178
|
+
Some(OwnerEpoch(5)),
|
|
179
|
+
"golden _lease_epoch: owner_epoch=0 is falsy -> receiver_epoch=5; Rust stops at Some(0)"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
115
182
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
183
|
+
// D5 [BLOCK] — success path emits leader_receiver.rebind_applied + owner_epoch_advanced (golden
|
|
184
|
+
// :712-729), NOT the incident-arm leader_receiver.claim_applied (:791). RED.
|
|
185
|
+
#[test]
|
|
186
|
+
#[serial_test::serial(env)]
|
|
187
|
+
fn d5_success_emits_rebind_applied_and_owner_epoch_advanced_not_claim_applied() {
|
|
188
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
189
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
190
|
+
let ws = p2_temp_ws("d5_events");
|
|
191
|
+
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
192
|
+
let _ = claim_lease_no_incident(
|
|
193
|
+
&ws,
|
|
194
|
+
&mut state,
|
|
195
|
+
None,
|
|
196
|
+
&TeamKey::new("current"),
|
|
197
|
+
&PaneId::new("%5"),
|
|
198
|
+
false,
|
|
199
|
+
&crate::event_log::EventLog::new(&ws),
|
|
200
|
+
&seeded_liveness(&["%5"]),
|
|
201
|
+
)
|
|
202
|
+
.unwrap();
|
|
203
|
+
let names = event_names(&ws);
|
|
204
|
+
assert!(
|
|
205
|
+
names.iter().any(|n| n == "leader_receiver.rebind_applied"),
|
|
206
|
+
"golden success emits leader_receiver.rebind_applied (:712); got {names:?}"
|
|
207
|
+
);
|
|
208
|
+
assert!(
|
|
209
|
+
names.iter().any(|n| n == "owner_epoch_advanced"),
|
|
210
|
+
"golden success emits owner_epoch_advanced (:721); got {names:?}"
|
|
211
|
+
);
|
|
212
|
+
assert!(
|
|
213
|
+
!names.iter().any(|n| n == "leader_receiver.claim_applied"),
|
|
214
|
+
"claim_applied is the INCIDENT-arm event, NEVER on the no-incident path; got {names:?}"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
135
217
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
218
|
+
// D6 [WARN] — EVERY lease refusal writes a leader_receiver audit event (golden _emit_lease_refusal).
|
|
219
|
+
// not_in_tmux_pane ∈ _LEASE_REBIND_REQUIRED_REASONS -> "leader_receiver.rebind_required" (:481).
|
|
220
|
+
// Rust refused() takes no event_log -> writes nothing. RED.
|
|
221
|
+
#[test]
|
|
222
|
+
#[serial_test::serial(env)]
|
|
223
|
+
fn d6_refusal_emits_leader_receiver_audit_event() {
|
|
224
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
225
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
226
|
+
let ws = p2_temp_ws("d6_refusal_event");
|
|
227
|
+
let mut state = serde_json::json!({});
|
|
228
|
+
let r = claim_lease_no_incident(
|
|
229
|
+
&ws,
|
|
230
|
+
&mut state,
|
|
231
|
+
None,
|
|
232
|
+
&TeamKey::new("current"),
|
|
233
|
+
&PaneId::new(""),
|
|
234
|
+
false,
|
|
235
|
+
&crate::event_log::EventLog::new(&ws),
|
|
236
|
+
&seeded_liveness(&[]),
|
|
237
|
+
)
|
|
238
|
+
.unwrap();
|
|
239
|
+
assert_eq!(
|
|
240
|
+
r.reason,
|
|
241
|
+
Some(LeaseReason::NotInTmuxPane),
|
|
242
|
+
"precondition: empty caller -> not_in_tmux_pane"
|
|
243
|
+
);
|
|
244
|
+
let names = event_names(&ws);
|
|
245
|
+
assert!(names.iter().any(|n| n == "leader_receiver.rebind_required"),
|
|
151
246
|
"golden writes a leader_receiver.rebind_required audit on the not_in_tmux_pane refusal; Rust writes none. got {names:?}");
|
|
152
|
-
|
|
247
|
+
}
|
|
153
248
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
249
|
+
// D8 [WARN] — provider PRESERVED from the prior receiver on re-claim (golden
|
|
250
|
+
// _receiver_from_claim_target provider=previous.provider or 'codex'; new_owner=new_recv.provider or
|
|
251
|
+
// owner.provider or 'codex'). A dead-owner recover of a CLAUDE leader keeps provider='claude'. Rust
|
|
252
|
+
// make_receiver/make_owner hardcode Provider::Codex. RED.
|
|
253
|
+
#[test]
|
|
254
|
+
#[serial_test::serial(env)]
|
|
255
|
+
fn d8_provider_preserved_from_prior_receiver_on_reclaim() {
|
|
256
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
257
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
258
|
+
let ws = p2_temp_ws("d8_provider");
|
|
259
|
+
let mut state = serde_json::json!({
|
|
260
|
+
"session_name": "team-agent-x",
|
|
261
|
+
"team_owner": {"pane_id":"%1","provider":"claude","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
262
|
+
"leader_receiver": {"pane_id":"%1","provider":"claude","owner_epoch":3,"leader_session_uuid":"U"},
|
|
263
|
+
});
|
|
264
|
+
let r = claim_lease_no_incident(
|
|
265
|
+
&ws,
|
|
266
|
+
&mut state,
|
|
267
|
+
None,
|
|
268
|
+
&TeamKey::new("current"),
|
|
269
|
+
&PaneId::new("%5"),
|
|
270
|
+
false,
|
|
271
|
+
&crate::event_log::EventLog::new(&ws),
|
|
272
|
+
&seeded_liveness(&["%5"]),
|
|
273
|
+
)
|
|
274
|
+
.unwrap();
|
|
275
|
+
assert!(
|
|
276
|
+
r.ok && r.status == LeaseStatus::Claimed,
|
|
277
|
+
"precondition: dead-owner %1 recover"
|
|
278
|
+
);
|
|
279
|
+
assert_eq!(
|
|
280
|
+
state
|
|
281
|
+
.pointer("/team_owner/provider")
|
|
282
|
+
.and_then(|v| v.as_str()),
|
|
283
|
+
Some("claude"),
|
|
284
|
+
"golden preserves the prior provider (claude) on re-claim; Rust hardcodes codex"
|
|
285
|
+
);
|
|
286
|
+
assert_eq!(
|
|
287
|
+
state
|
|
288
|
+
.pointer("/leader_receiver/provider")
|
|
289
|
+
.and_then(|v| v.as_str()),
|
|
290
|
+
Some("claude"),
|
|
291
|
+
"the new receiver also preserves the prior provider (golden previous.provider)"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
177
294
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
295
|
+
// TOCTOU CAS [BLOCK, flagged] — golden :654-671 re-reads owner_epoch INSIDE the lock and refuses
|
|
296
|
+
// owner_epoch_advanced on a concurrent bump (no double-bind). Rust has NO locked re-read; the race
|
|
297
|
+
// silently double-binds (LeaseReason::OwnerEpochAdvanced is unreachable). Simulated: on-disk
|
|
298
|
+
// state.json (the locked-re-read source) carries epoch 5 while the passed precheck state is epoch 3.
|
|
299
|
+
// RED: today Rust ignores disk and claims. Porter: re-read select_runtime_state under the lock and
|
|
300
|
+
// refuse owner_epoch_advanced when locked_epoch != precheck_epoch.
|
|
301
|
+
#[test]
|
|
302
|
+
#[serial_test::serial(env)]
|
|
303
|
+
fn toctou_locked_reread_refuses_owner_epoch_advanced_on_concurrent_bump() {
|
|
304
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
305
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
306
|
+
let ws = p2_temp_ws("toctou_cas");
|
|
307
|
+
// disk (the locked-re-read source): a concurrent claimer bumped owner_epoch to 5.
|
|
308
|
+
crate::state::persist::save_runtime_state(&ws, &serde_json::json!({
|
|
192
309
|
"session_name": "team-agent-x",
|
|
193
310
|
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":5,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
194
311
|
})).unwrap();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
312
|
+
// precheck state passed to the claim: epoch 3, vacant (no owner pane) -> no force_confirm gate.
|
|
313
|
+
let mut state =
|
|
314
|
+
serde_json::json!({"session_name":"team-agent-x","leader_receiver":{"owner_epoch":3}});
|
|
315
|
+
let r = claim_lease_no_incident(
|
|
316
|
+
&ws,
|
|
317
|
+
&mut state,
|
|
318
|
+
None,
|
|
319
|
+
&TeamKey::new("current"),
|
|
320
|
+
&PaneId::new("%5"),
|
|
321
|
+
false,
|
|
322
|
+
&crate::event_log::EventLog::new(&ws),
|
|
323
|
+
&seeded_liveness(&["%5"]),
|
|
324
|
+
)
|
|
325
|
+
.unwrap();
|
|
326
|
+
assert_eq!(r.status, LeaseStatus::Refused,
|
|
200
327
|
"golden TOCTOU CAS: locked epoch 5 != precheck 3 -> refused; Rust skips the re-read and claims. got {r:?}");
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
328
|
+
assert_eq!(
|
|
329
|
+
r.reason,
|
|
330
|
+
Some(LeaseReason::OwnerEpochAdvanced),
|
|
331
|
+
"the refusal reason is owner_epoch_advanced (:666)"
|
|
332
|
+
);
|
|
333
|
+
}
|
|
204
334
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
335
|
+
// attach_leader_to_state [BLOCK, flagged] — golden non-first-time path writes leader_receiver ONLY:
|
|
336
|
+
// NO team_owner write, NO epoch advance (after the strict-uuid gate). Rust stub writes BOTH and
|
|
337
|
+
// advances epoch (precheck+1), ignoring source/require_current. RED (locked before the autobind/launch
|
|
338
|
+
// wiring lands, so it does not silently overwrite the owner + bump epoch on every attach).
|
|
339
|
+
#[test]
|
|
340
|
+
#[serial_test::serial(env)]
|
|
341
|
+
fn attach_leader_to_state_nonfirst_does_not_overwrite_owner_or_advance_epoch() {
|
|
342
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
343
|
+
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
344
|
+
let ws = p2_temp_ws("attach_nonfirst");
|
|
345
|
+
let mut state = serde_json::json!({
|
|
346
|
+
"session_name": "team-agent-x",
|
|
347
|
+
"team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
|
|
348
|
+
});
|
|
349
|
+
let event_log = crate::event_log::EventLog::new(&ws);
|
|
350
|
+
let _ = attach_leader_to_state(
|
|
351
|
+
&ws,
|
|
352
|
+
&mut state,
|
|
353
|
+
Some(&PaneId::new("%2")),
|
|
354
|
+
crate::provider::Provider::Codex,
|
|
355
|
+
&event_log,
|
|
356
|
+
LeaseSource::Manual,
|
|
357
|
+
false,
|
|
358
|
+
);
|
|
359
|
+
assert_eq!(state.pointer("/team_owner/pane_id").and_then(|v| v.as_str()), Some("%1"),
|
|
223
360
|
"non-first-time attach must NOT overwrite team_owner.pane_id (golden writes leader_receiver only); Rust rebinds it to %2");
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
361
|
+
assert_eq!(
|
|
362
|
+
state
|
|
363
|
+
.pointer("/team_owner/owner_epoch")
|
|
364
|
+
.and_then(|v| v.as_u64()),
|
|
365
|
+
Some(3),
|
|
366
|
+
"non-first-time attach must NOT advance owner_epoch (golden keeps 3); Rust bumps to 4"
|
|
367
|
+
);
|
|
368
|
+
}
|
|
227
369
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
370
|
+
// C3 [WARN] — leader_identity.current_pane_id honors TEAM_AGENT_LEADER_PANE_ID priority (golden
|
|
371
|
+
// :366 = env(TEAM_AGENT_LEADER_PANE_ID) or env(TMUX_PANE) or None). Rust owner_bind.rs:32 reads
|
|
372
|
+
// TMUX_PANE only. RED: with TEAM_AGENT_LEADER_PANE_ID set + TMUX_PANE unset, current_pane_id must be
|
|
373
|
+
// the LEADER_PANE_ID. (Sibling gap, noted: last_seen_at = receiver.attached_at OR receiver.last_seen_at;
|
|
374
|
+
// Rust reads attached_at only.)
|
|
375
|
+
#[test]
|
|
376
|
+
#[serial_test::serial(env)]
|
|
377
|
+
fn c3_leader_identity_current_pane_honors_leader_pane_id_env() {
|
|
378
|
+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
379
|
+
let _e = EnvGuard::apply(&[
|
|
380
|
+
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
381
|
+
("TEAM_AGENT_LEADER_PANE_ID", Some("%foo")),
|
|
382
|
+
("TMUX_PANE", None),
|
|
383
|
+
]);
|
|
384
|
+
let ws = p2_temp_ws("c3_pane_env");
|
|
385
|
+
let v = leader_identity(&ws, None).unwrap();
|
|
386
|
+
assert_eq!(v["current_pane_id"], serde_json::json!("%foo"),
|
|
245
387
|
"golden :366 current_pane_id = TEAM_AGENT_LEADER_PANE_ID or TMUX_PANE or None; Rust reads TMUX_PANE only");
|
|
246
|
-
|
|
388
|
+
}
|