@team-agent/installer 0.4.1 → 0.4.3
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 +82 -22
- package/crates/team-agent/src/cli/diagnose.rs +6 -2
- package/crates/team-agent/src/cli/emit.rs +301 -38
- package/crates/team-agent/src/cli/mod.rs +7 -14
- package/crates/team-agent/src/cli/status_port.rs +12 -1
- package/crates/team-agent/src/cli/tests/base.rs +6 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/leader_watch.rs +5 -3
- package/crates/team-agent/src/cli/tests/main_preserved.rs +28 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +19 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +72 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +35 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +7 -2
- package/crates/team-agent/src/diagnose/comms.rs +10 -2
- package/crates/team-agent/src/leader/lease.rs +98 -19
- package/crates/team-agent/src/leader/rediscover/tests.rs +21 -7
- package/crates/team-agent/src/leader/rediscover.rs +16 -7
- package/crates/team-agent/src/leader/start.rs +25 -12
- package/crates/team-agent/src/leader/tests/byte_findings.rs +11 -2
- package/crates/team-agent/src/leader/tests/lease_claim.rs +248 -7
- package/crates/team-agent/src/lifecycle/launch.rs +126 -100
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +40 -12
- package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +16 -15
- package/crates/team-agent/src/lifecycle/types.rs +1 -1
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -8
- package/crates/team-agent/src/messaging/send.rs +21 -4
- package/crates/team-agent/src/provider/adapter.rs +72 -0
- package/crates/team-agent/src/provider/session/capture.rs +228 -0
- package/crates/team-agent/src/state/identity.rs +42 -12
- package/crates/team-agent/src/state/identity_keys.rs +134 -0
- package/crates/team-agent/src/state/mod.rs +8 -0
- package/crates/team-agent/src/state/owner_gate.rs +11 -1
- package/crates/team-agent/src/state/ownership.rs +556 -0
- package/crates/team-agent/src/state/paths.rs +358 -0
- package/crates/team-agent/src/state/persist.rs +43 -5
- package/crates/team-agent/src/state/projection.rs +13 -5
- package/crates/team-agent/src/tmux_backend.rs +12 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -3
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//! Stage 0 of the identity-boundary unified plan (architect direction
|
|
2
|
+
//! 2026-06-23): `SessionAttributionKey`.
|
|
3
|
+
//!
|
|
4
|
+
//! Pure additive: nobody constructs this type yet. Stage 1 (Claude strict
|
|
5
|
+
//! capture) starts populating it inside `CaptureSessionContext`; Stage 4
|
|
6
|
+
//! (CLI/DB scope gate) starts using it as the DB/event filter key.
|
|
7
|
+
//!
|
|
8
|
+
//! Why a struct, not a tuple:
|
|
9
|
+
//! - The four fields are not interchangeable: `provider` selects the
|
|
10
|
+
//! tombstone/claimed-session table, `team_key` is the multi-team boundary,
|
|
11
|
+
//! `agent_id` is the worker identity inside a team, `session_id` is the
|
|
12
|
+
//! provider-issued transcript identity.
|
|
13
|
+
//! - The architect's verdict §1 names this exact shape as the "shared
|
|
14
|
+
//! identity primitive" that owner / capture / multi-team all need so they
|
|
15
|
+
//! don't drift into per-module ad-hoc keys.
|
|
16
|
+
//!
|
|
17
|
+
//! Single-team behaviour: `team_key` is still populated (from
|
|
18
|
+
//! `state::projection::team_state_key` or the same selector path Stage 0
|
|
19
|
+
//! introduces). The key is invariant — capture/owner/CLI all read it from
|
|
20
|
+
//! the same source.
|
|
21
|
+
|
|
22
|
+
use crate::model::enums::Provider;
|
|
23
|
+
|
|
24
|
+
/// The canonical identity of a "this worker owns this provider transcript"
|
|
25
|
+
/// claim. Stage 1 + Stage 4 will both store and compare via this struct.
|
|
26
|
+
///
|
|
27
|
+
/// All four fields are non-empty by construction (the constructor refuses
|
|
28
|
+
/// empty `team_key`, `agent_id`, or `session_id` — those would collapse
|
|
29
|
+
/// distinct sessions into one).
|
|
30
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
31
|
+
pub struct SessionAttributionKey {
|
|
32
|
+
provider: Provider,
|
|
33
|
+
team_key: String,
|
|
34
|
+
agent_id: String,
|
|
35
|
+
session_id: String,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl SessionAttributionKey {
|
|
39
|
+
pub fn new(
|
|
40
|
+
provider: Provider,
|
|
41
|
+
team_key: impl Into<String>,
|
|
42
|
+
agent_id: impl Into<String>,
|
|
43
|
+
session_id: impl Into<String>,
|
|
44
|
+
) -> Option<Self> {
|
|
45
|
+
let team_key = team_key.into();
|
|
46
|
+
let agent_id = agent_id.into();
|
|
47
|
+
let session_id = session_id.into();
|
|
48
|
+
if team_key.is_empty() || agent_id.is_empty() || session_id.is_empty() {
|
|
49
|
+
return None;
|
|
50
|
+
}
|
|
51
|
+
Some(Self {
|
|
52
|
+
provider,
|
|
53
|
+
team_key,
|
|
54
|
+
agent_id,
|
|
55
|
+
session_id,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub fn provider(&self) -> Provider {
|
|
60
|
+
self.provider
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn team_key(&self) -> &str {
|
|
64
|
+
&self.team_key
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn agent_id(&self) -> &str {
|
|
68
|
+
&self.agent_id
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn session_id(&self) -> &str {
|
|
72
|
+
&self.session_id
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[cfg(test)]
|
|
77
|
+
mod tests {
|
|
78
|
+
use super::*;
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn refuses_empty_team_key() {
|
|
82
|
+
assert!(
|
|
83
|
+
SessionAttributionKey::new(Provider::Claude, "", "agent_a", "sess_1").is_none()
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn refuses_empty_agent_id() {
|
|
89
|
+
assert!(
|
|
90
|
+
SessionAttributionKey::new(Provider::Claude, "alpha", "", "sess_1").is_none()
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn refuses_empty_session_id() {
|
|
96
|
+
assert!(
|
|
97
|
+
SessionAttributionKey::new(Provider::Claude, "alpha", "agent_a", "").is_none()
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[test]
|
|
102
|
+
fn accepts_well_formed_key_and_preserves_fields() {
|
|
103
|
+
let key = SessionAttributionKey::new(
|
|
104
|
+
Provider::Claude,
|
|
105
|
+
"alpha",
|
|
106
|
+
"reviewer",
|
|
107
|
+
"36dd1d1a-2766-4636-856c-03f14a4bb803",
|
|
108
|
+
)
|
|
109
|
+
.unwrap();
|
|
110
|
+
assert_eq!(key.provider(), Provider::Claude);
|
|
111
|
+
assert_eq!(key.team_key(), "alpha");
|
|
112
|
+
assert_eq!(key.agent_id(), "reviewer");
|
|
113
|
+
assert_eq!(key.session_id(), "36dd1d1a-2766-4636-856c-03f14a4bb803");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Same provider + agent_id + session_id but different team_key → distinct
|
|
117
|
+
/// identity. This is the multi-team isolation invariant the unified plan
|
|
118
|
+
/// requires (architect §1 cross point: tombstone/claimed by full
|
|
119
|
+
/// (provider, team_key, agent_id, session_id) tuple).
|
|
120
|
+
#[test]
|
|
121
|
+
fn different_team_keys_produce_different_keys() {
|
|
122
|
+
let alpha = SessionAttributionKey::new(Provider::Claude, "alpha", "reviewer", "s1").unwrap();
|
|
123
|
+
let beta = SessionAttributionKey::new(Provider::Claude, "beta", "reviewer", "s1").unwrap();
|
|
124
|
+
assert_ne!(alpha, beta);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Same team but different agent_id → distinct (per-worker isolation).
|
|
128
|
+
#[test]
|
|
129
|
+
fn different_agent_ids_produce_different_keys() {
|
|
130
|
+
let reviewer = SessionAttributionKey::new(Provider::Claude, "alpha", "reviewer", "s1").unwrap();
|
|
131
|
+
let frontend = SessionAttributionKey::new(Provider::Claude, "alpha", "frontend", "s1").unwrap();
|
|
132
|
+
assert_ne!(reviewer, frontend);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -15,7 +15,15 @@
|
|
|
15
15
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
16
16
|
|
|
17
17
|
pub mod identity;
|
|
18
|
+
// Stage 0 of identity-boundary unified plan (architect direction 2026-06-23):
|
|
19
|
+
// shared identity primitives. Pure additive — Stage 1+ consume these.
|
|
20
|
+
pub mod identity_keys;
|
|
18
21
|
pub mod owner_gate;
|
|
22
|
+
// Stage 2 of identity-boundary unified plan: single read entry point for
|
|
23
|
+
// `team_owner` lookups. Stage 3 will migrate writers; Stage 5 will swap
|
|
24
|
+
// the data source to per-team canonical state without touching call sites.
|
|
25
|
+
pub mod ownership;
|
|
26
|
+
pub mod paths;
|
|
19
27
|
pub mod persist;
|
|
20
28
|
pub mod projection;
|
|
21
29
|
pub mod selector;
|
|
@@ -39,13 +39,23 @@ pub struct CallerIdentity {
|
|
|
39
39
|
|
|
40
40
|
/// `check_team_owner`(`state.py:366`)的纯判定版:caller 身份 + 是否有 TEAM_AGENT_ID + liveness 注入。
|
|
41
41
|
/// 返回 `None`=允许(own / 无 owner / 死 owner pane 可接管);`Some(dict)`=拒绝(team_owner_mismatch)。
|
|
42
|
+
///
|
|
43
|
+
/// Stage 2 (identity-boundary unified plan, architect direction 2026-06-23):
|
|
44
|
+
/// the owner is now looked up through `state::ownership::read_owner_value`
|
|
45
|
+
/// instead of `state::projection::read_owner` directly. The two are
|
|
46
|
+
/// equivalent today for the gate's `None` (empty team_key) path — both
|
|
47
|
+
/// resolve to top-level `state.team_owner` — but routing through the
|
|
48
|
+
/// repository means Stage 5 can swap the data source (per-team canonical
|
|
49
|
+
/// `state.json`) without touching this gate. The empty-team-key argument
|
|
50
|
+
/// preserves the pre-Stage-2 shape: gate callers feed an
|
|
51
|
+
/// already-team-projected state and expect top-level reads.
|
|
42
52
|
pub fn check_team_owner(
|
|
43
53
|
state: &Value,
|
|
44
54
|
caller: &CallerIdentity,
|
|
45
55
|
has_team_agent_id: bool,
|
|
46
56
|
liveness: &dyn PaneLivenessProbe,
|
|
47
57
|
) -> Option<Value> {
|
|
48
|
-
let owner = crate::state::
|
|
58
|
+
let owner = crate::state::ownership::read_owner_value(state, "")?;
|
|
49
59
|
if !owner.is_object() || owner.as_object().is_none_or(serde_json::Map::is_empty) {
|
|
50
60
|
return None;
|
|
51
61
|
}
|