@team-agent/installer 0.3.36 → 0.3.37
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
CHANGED
package/Cargo.toml
CHANGED
|
@@ -308,14 +308,13 @@ fn pane_conflicts_with_leader_or_other(
|
|
|
308
308
|
else {
|
|
309
309
|
return false;
|
|
310
310
|
};
|
|
311
|
+
let state_socket = runtime_tmux_socket(state);
|
|
312
|
+
let agent_socket = tmux_socket_field(agent).or(state_socket);
|
|
311
313
|
// Check leader anchor.
|
|
312
314
|
for key in ["leader_receiver", "team_owner"] {
|
|
313
|
-
if state
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
.and_then(serde_json::Value::as_str)
|
|
317
|
-
.is_some_and(|lp| lp == pane_id)
|
|
318
|
-
{
|
|
315
|
+
if state.get(key).and_then(pane_socket_binding).is_some_and(|leader| {
|
|
316
|
+
pane_conflicts_on_same_socket(pane_id, agent_socket, leader)
|
|
317
|
+
}) {
|
|
319
318
|
return true;
|
|
320
319
|
}
|
|
321
320
|
}
|
|
@@ -325,10 +324,13 @@ fn pane_conflicts_with_leader_or_other(
|
|
|
325
324
|
if id == agent_id.as_str() {
|
|
326
325
|
continue;
|
|
327
326
|
}
|
|
327
|
+
let other_socket = tmux_socket_field(other).or(state_socket);
|
|
328
328
|
if other
|
|
329
329
|
.get("pane_id")
|
|
330
330
|
.and_then(serde_json::Value::as_str)
|
|
331
|
-
.is_some_and(|op|
|
|
331
|
+
.is_some_and(|op| {
|
|
332
|
+
op == pane_id && !tmux_sockets_known_different(agent_socket, other_socket)
|
|
333
|
+
})
|
|
332
334
|
{
|
|
333
335
|
return true;
|
|
334
336
|
}
|
|
@@ -337,6 +339,53 @@ fn pane_conflicts_with_leader_or_other(
|
|
|
337
339
|
false
|
|
338
340
|
}
|
|
339
341
|
|
|
342
|
+
#[derive(Clone, Copy)]
|
|
343
|
+
struct PaneSocketBinding<'a> {
|
|
344
|
+
pane_id: &'a str,
|
|
345
|
+
tmux_socket: Option<&'a str>,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn pane_conflicts_on_same_socket(
|
|
349
|
+
pane_id: &str,
|
|
350
|
+
agent_socket: Option<&str>,
|
|
351
|
+
other: PaneSocketBinding<'_>,
|
|
352
|
+
) -> bool {
|
|
353
|
+
other.pane_id == pane_id && !tmux_sockets_known_different(agent_socket, other.tmux_socket)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn tmux_sockets_known_different(left: Option<&str>, right: Option<&str>) -> bool {
|
|
357
|
+
let (Some(left), Some(right)) = (left, right) else {
|
|
358
|
+
return false;
|
|
359
|
+
};
|
|
360
|
+
if left == right {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
std::path::Path::new(left).is_absolute() && std::path::Path::new(right).is_absolute()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fn runtime_tmux_socket(state: &serde_json::Value) -> Option<&str> {
|
|
367
|
+
tmux_socket_field(state)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fn tmux_socket_field(value: &serde_json::Value) -> Option<&str> {
|
|
371
|
+
value
|
|
372
|
+
.get("tmux_endpoint")
|
|
373
|
+
.or_else(|| value.get("tmux_socket"))
|
|
374
|
+
.and_then(serde_json::Value::as_str)
|
|
375
|
+
.filter(|value| !value.is_empty())
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
fn pane_socket_binding(value: &serde_json::Value) -> Option<PaneSocketBinding<'_>> {
|
|
379
|
+
let pane_id = value
|
|
380
|
+
.get("pane_id")
|
|
381
|
+
.and_then(serde_json::Value::as_str)
|
|
382
|
+
.filter(|s| !s.is_empty() && *s != "__team_agent_unbound__")?;
|
|
383
|
+
Some(PaneSocketBinding {
|
|
384
|
+
pane_id,
|
|
385
|
+
tmux_socket: tmux_socket_field(value),
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
340
389
|
fn agent_pane_live(
|
|
341
390
|
transport: &dyn crate::transport::Transport,
|
|
342
391
|
agent: &serde_json::Value,
|
|
@@ -950,3 +999,81 @@ fn write_reset_complete_event(
|
|
|
950
999
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
951
1000
|
Ok(())
|
|
952
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
#[cfg(test)]
|
|
1004
|
+
mod tests {
|
|
1005
|
+
use super::*;
|
|
1006
|
+
|
|
1007
|
+
#[test]
|
|
1008
|
+
fn e51_restart_allows_leader_same_pane_id_on_different_tmux_sockets() {
|
|
1009
|
+
let state = serde_json::json!({
|
|
1010
|
+
"tmux_endpoint": "/private/tmp/tmux-501/ta-worker",
|
|
1011
|
+
"leader_receiver": {
|
|
1012
|
+
"pane_id": "%0",
|
|
1013
|
+
"tmux_socket": "/private/tmp/tmux-501/default"
|
|
1014
|
+
},
|
|
1015
|
+
"agents": {
|
|
1016
|
+
"architect": {
|
|
1017
|
+
"pane_id": "%0",
|
|
1018
|
+
"tmux_socket": "/private/tmp/tmux-501/ta-worker"
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
let agent_id = AgentId::new("architect");
|
|
1023
|
+
let agent = state["agents"]["architect"].clone();
|
|
1024
|
+
|
|
1025
|
+
assert!(
|
|
1026
|
+
!pane_conflicts_with_leader_or_other(&state, &agent_id, &agent),
|
|
1027
|
+
"same pane id on different tmux sockets must not force a fresh restart"
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
#[test]
|
|
1032
|
+
fn e51_restart_keeps_leader_conflict_on_same_tmux_socket() {
|
|
1033
|
+
let socket = "/private/tmp/tmux-501/default";
|
|
1034
|
+
let state = serde_json::json!({
|
|
1035
|
+
"tmux_endpoint": socket,
|
|
1036
|
+
"leader_receiver": {
|
|
1037
|
+
"pane_id": "%0",
|
|
1038
|
+
"tmux_socket": socket
|
|
1039
|
+
},
|
|
1040
|
+
"agents": {
|
|
1041
|
+
"architect": {
|
|
1042
|
+
"pane_id": "%0",
|
|
1043
|
+
"tmux_socket": socket
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
let agent_id = AgentId::new("architect");
|
|
1048
|
+
let agent = state["agents"]["architect"].clone();
|
|
1049
|
+
|
|
1050
|
+
assert!(
|
|
1051
|
+
pane_conflicts_with_leader_or_other(&state, &agent_id, &agent),
|
|
1052
|
+
"same pane id on the same tmux socket must keep the E51 guard"
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
#[test]
|
|
1057
|
+
fn e51_restart_allows_other_agent_same_pane_id_on_different_tmux_sockets() {
|
|
1058
|
+
let state = serde_json::json!({
|
|
1059
|
+
"tmux_endpoint": "/private/tmp/tmux-501/ta-worker",
|
|
1060
|
+
"agents": {
|
|
1061
|
+
"architect": {
|
|
1062
|
+
"pane_id": "%0",
|
|
1063
|
+
"tmux_socket": "/private/tmp/tmux-501/ta-worker"
|
|
1064
|
+
},
|
|
1065
|
+
"reviewer": {
|
|
1066
|
+
"pane_id": "%0",
|
|
1067
|
+
"tmux_socket": "/private/tmp/tmux-501/default"
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
let agent_id = AgentId::new("architect");
|
|
1072
|
+
let agent = state["agents"]["architect"].clone();
|
|
1073
|
+
|
|
1074
|
+
assert!(
|
|
1075
|
+
!pane_conflicts_with_leader_or_other(&state, &agent_id, &agent),
|
|
1076
|
+
"other-agent collision checks must also include tmux_socket"
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
@@ -587,8 +587,10 @@ fn resolve_inject_target(
|
|
|
587
587
|
// is in lease.rs (E51 guard #1) which prevents the conflation; this is the
|
|
588
588
|
// defence-in-depth in the delivery layer.
|
|
589
589
|
if let Some(pane) = cached_pane.as_ref() {
|
|
590
|
-
let
|
|
591
|
-
if
|
|
590
|
+
let worker_socket = worker_tmux_socket(state, agent, transport);
|
|
591
|
+
if leader_receiver_pane_binding(state).is_some_and(|leader| {
|
|
592
|
+
pane_conflicts_on_same_socket(pane.as_str(), worker_socket.as_deref(), leader)
|
|
593
|
+
}) {
|
|
592
594
|
return Target::SessionWindow {
|
|
593
595
|
session: SessionName::new(session),
|
|
594
596
|
window: WindowName::new(format!("{recipient}_pane_conflicts_with_leader")),
|
|
@@ -639,6 +641,57 @@ fn cached_pane_known_dead(transport: &dyn Transport, pane: &PaneId) -> bool {
|
|
|
639
641
|
matches!(transport.liveness(pane), Ok(PaneLiveness::Dead))
|
|
640
642
|
}
|
|
641
643
|
|
|
644
|
+
#[derive(Clone, Copy)]
|
|
645
|
+
struct PaneSocketBinding<'a> {
|
|
646
|
+
pane_id: &'a str,
|
|
647
|
+
tmux_socket: Option<&'a str>,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
fn pane_conflicts_on_same_socket(
|
|
651
|
+
pane_id: &str,
|
|
652
|
+
worker_socket: Option<&str>,
|
|
653
|
+
leader: PaneSocketBinding<'_>,
|
|
654
|
+
) -> bool {
|
|
655
|
+
leader.pane_id == pane_id
|
|
656
|
+
&& !tmux_sockets_known_different(worker_socket, leader.tmux_socket)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
fn tmux_sockets_known_different(left: Option<&str>, right: Option<&str>) -> bool {
|
|
660
|
+
let (Some(left), Some(right)) = (left, right) else {
|
|
661
|
+
return false;
|
|
662
|
+
};
|
|
663
|
+
if left == right {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
std::path::Path::new(left).is_absolute() && std::path::Path::new(right).is_absolute()
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
fn worker_tmux_socket(
|
|
670
|
+
state: &serde_json::Value,
|
|
671
|
+
agent: Option<&serde_json::Value>,
|
|
672
|
+
transport: &dyn Transport,
|
|
673
|
+
) -> Option<String> {
|
|
674
|
+
agent
|
|
675
|
+
.and_then(tmux_socket_field)
|
|
676
|
+
.or_else(|| runtime_tmux_socket(state))
|
|
677
|
+
.map(str::to_string)
|
|
678
|
+
.or_else(|| transport.tmux_endpoint())
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
fn runtime_tmux_socket(state: &serde_json::Value) -> Option<&str> {
|
|
682
|
+
tmux_socket_field(state)
|
|
683
|
+
.or_else(|| active_team_entry(state).and_then(tmux_socket_field))
|
|
684
|
+
.or_else(|| only_team_entry(state).and_then(tmux_socket_field))
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
fn tmux_socket_field(value: &serde_json::Value) -> Option<&str> {
|
|
688
|
+
value
|
|
689
|
+
.get("tmux_endpoint")
|
|
690
|
+
.or_else(|| value.get("tmux_socket"))
|
|
691
|
+
.and_then(serde_json::Value::as_str)
|
|
692
|
+
.filter(|value| !value.is_empty())
|
|
693
|
+
}
|
|
694
|
+
|
|
642
695
|
/// Read the bound leader pane id off the projected or team-scoped runtime state.
|
|
643
696
|
fn leader_receiver_pane_id(state: &serde_json::Value) -> Option<&str> {
|
|
644
697
|
leader_receiver_pane_id_in_state(state)
|
|
@@ -646,6 +699,12 @@ fn leader_receiver_pane_id(state: &serde_json::Value) -> Option<&str> {
|
|
|
646
699
|
.or_else(|| only_team_entry(state).and_then(leader_receiver_pane_id_in_state))
|
|
647
700
|
}
|
|
648
701
|
|
|
702
|
+
fn leader_receiver_pane_binding(state: &serde_json::Value) -> Option<PaneSocketBinding<'_>> {
|
|
703
|
+
leader_receiver_pane_binding_in_state(state)
|
|
704
|
+
.or_else(|| active_team_entry(state).and_then(leader_receiver_pane_binding_in_state))
|
|
705
|
+
.or_else(|| only_team_entry(state).and_then(leader_receiver_pane_binding_in_state))
|
|
706
|
+
}
|
|
707
|
+
|
|
649
708
|
/// `state` is "usable" for leader delivery when the bound `leader_receiver.pane_id`
|
|
650
709
|
/// is present and alive (in `live_targets` or `liveness` probe returns not-Dead).
|
|
651
710
|
///
|
|
@@ -761,6 +820,25 @@ fn leader_receiver_pane_id_in_state(state: &serde_json::Value) -> Option<&str> {
|
|
|
761
820
|
})
|
|
762
821
|
}
|
|
763
822
|
|
|
823
|
+
fn leader_receiver_pane_binding_in_state(
|
|
824
|
+
state: &serde_json::Value,
|
|
825
|
+
) -> Option<PaneSocketBinding<'_>> {
|
|
826
|
+
["leader_receiver", "team_owner"]
|
|
827
|
+
.into_iter()
|
|
828
|
+
.find_map(|key| state.get(key).and_then(pane_socket_binding))
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
fn pane_socket_binding(value: &serde_json::Value) -> Option<PaneSocketBinding<'_>> {
|
|
832
|
+
let pane_id = value
|
|
833
|
+
.get("pane_id")
|
|
834
|
+
.and_then(serde_json::Value::as_str)
|
|
835
|
+
.filter(|s| !s.is_empty() && *s != "__team_agent_unbound__")?;
|
|
836
|
+
Some(PaneSocketBinding {
|
|
837
|
+
pane_id,
|
|
838
|
+
tmux_socket: tmux_socket_field(value),
|
|
839
|
+
})
|
|
840
|
+
}
|
|
841
|
+
|
|
764
842
|
fn leader_receiver_tmux_socket(state: &serde_json::Value) -> Option<&str> {
|
|
765
843
|
leader_receiver_field(state, "tmux_socket")
|
|
766
844
|
}
|
|
@@ -1812,6 +1812,88 @@ fn u1_resolve_inject_target_uses_live_pane_when_cached_pane_drifted() {
|
|
|
1812
1812
|
);
|
|
1813
1813
|
}
|
|
1814
1814
|
|
|
1815
|
+
#[test]
|
|
1816
|
+
fn e51_delivery_allows_same_pane_id_on_different_tmux_sockets() {
|
|
1817
|
+
let ws = tmp_ws("e51diffsocket");
|
|
1818
|
+
let store = store_for(&ws);
|
|
1819
|
+
let log = EventLog::new(&ws);
|
|
1820
|
+
let state = serde_json::json!({
|
|
1821
|
+
"session_name": "team-e51",
|
|
1822
|
+
"tmux_endpoint": "/private/tmp/tmux-501/ta-worker",
|
|
1823
|
+
"leader_receiver": {
|
|
1824
|
+
"pane_id": "%0",
|
|
1825
|
+
"tmux_socket": "/private/tmp/tmux-501/default"
|
|
1826
|
+
},
|
|
1827
|
+
"agents": {
|
|
1828
|
+
"architect": {
|
|
1829
|
+
"provider": "fake",
|
|
1830
|
+
"pane_id": "%0",
|
|
1831
|
+
"window": "architect"
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
1836
|
+
let _ = store
|
|
1837
|
+
.create_message(None, "peer", "architect", "socket-aware", None, false, None)
|
|
1838
|
+
.unwrap();
|
|
1839
|
+
let transport = OfflineTransport::new()
|
|
1840
|
+
.with_tmux_endpoint("/private/tmp/tmux-501/ta-worker")
|
|
1841
|
+
.with_targets(vec![pane_info("%0", "team-e51", "architect")]);
|
|
1842
|
+
|
|
1843
|
+
let delivered = deliver_pending_messages(&ws, &state, &transport, &log)
|
|
1844
|
+
.expect("same pane id on different sockets must not poison delivery");
|
|
1845
|
+
|
|
1846
|
+
assert_eq!(delivered.len(), 1);
|
|
1847
|
+
assert_eq!(
|
|
1848
|
+
transport.inject_targets(),
|
|
1849
|
+
vec![Target::Pane(PaneId::new("%0"))],
|
|
1850
|
+
"E51 must compare pane_id with tmux_socket; different sockets are not a leader conflict"
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
#[test]
|
|
1855
|
+
fn e51_delivery_keeps_same_socket_pane_conflict_guard() {
|
|
1856
|
+
let ws = tmp_ws("e51samesocket");
|
|
1857
|
+
let store = store_for(&ws);
|
|
1858
|
+
let log = EventLog::new(&ws);
|
|
1859
|
+
let socket = "/private/tmp/tmux-501/default";
|
|
1860
|
+
let state = serde_json::json!({
|
|
1861
|
+
"session_name": "team-e51",
|
|
1862
|
+
"tmux_endpoint": socket,
|
|
1863
|
+
"leader_receiver": {
|
|
1864
|
+
"pane_id": "%0",
|
|
1865
|
+
"tmux_socket": socket
|
|
1866
|
+
},
|
|
1867
|
+
"agents": {
|
|
1868
|
+
"architect": {
|
|
1869
|
+
"provider": "fake",
|
|
1870
|
+
"pane_id": "%0",
|
|
1871
|
+
"window": "architect"
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
1876
|
+
let _ = store
|
|
1877
|
+
.create_message(None, "peer", "architect", "same-socket", None, false, None)
|
|
1878
|
+
.unwrap();
|
|
1879
|
+
let transport = OfflineTransport::new()
|
|
1880
|
+
.with_tmux_endpoint(socket)
|
|
1881
|
+
.with_targets(vec![pane_info("%0", "team-e51", "architect")]);
|
|
1882
|
+
|
|
1883
|
+
let delivered = deliver_pending_messages(&ws, &state, &transport, &log)
|
|
1884
|
+
.expect("same-socket E51 guard should still resolve to a structured target");
|
|
1885
|
+
|
|
1886
|
+
assert_eq!(delivered.len(), 1);
|
|
1887
|
+
assert_eq!(
|
|
1888
|
+
transport.inject_targets(),
|
|
1889
|
+
vec![Target::SessionWindow {
|
|
1890
|
+
session: SessionName::new("team-e51"),
|
|
1891
|
+
window: WindowName::new("architect_pane_conflicts_with_leader"),
|
|
1892
|
+
}],
|
|
1893
|
+
"same pane id on the same socket must keep the E51 loop guard"
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1815
1897
|
#[test]
|
|
1816
1898
|
fn u1_multi_team_send_does_not_backfill_top_level_leader_binding() {
|
|
1817
1899
|
let ws = tmp_ws("u1backfill");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.37",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.3.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.3.37",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.37",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.37"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|