@team-agent/installer 0.5.16 → 0.5.17
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/mod.rs +5 -69
- package/crates/team-agent/src/leader/registry.rs +104 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +85 -28
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -4060,79 +4060,15 @@ pub mod leader_port {
|
|
|
4060
4060
|
source: &str,
|
|
4061
4061
|
response: &mut Value,
|
|
4062
4062
|
) {
|
|
4063
|
-
let
|
|
4064
|
-
return;
|
|
4065
|
-
};
|
|
4066
|
-
let team_key = match team.filter(|t| !t.is_empty()) {
|
|
4067
|
-
Some(t) => t.to_string(),
|
|
4068
|
-
None => crate::state::projection::team_state_key(&state),
|
|
4069
|
-
};
|
|
4070
|
-
let receiver = state
|
|
4071
|
-
.get("teams")
|
|
4072
|
-
.and_then(|v| v.as_object())
|
|
4073
|
-
.and_then(|teams| teams.get(&team_key))
|
|
4074
|
-
.and_then(|t| t.get("leader_receiver"))
|
|
4075
|
-
.or_else(|| state.get("leader_receiver"))
|
|
4076
|
-
.cloned();
|
|
4077
|
-
let Some(receiver) = receiver else {
|
|
4078
|
-
return;
|
|
4079
|
-
};
|
|
4080
|
-
let transport_kind = receiver
|
|
4081
|
-
.get("transport_kind")
|
|
4082
|
-
.and_then(Value::as_str)
|
|
4083
|
-
.unwrap_or("direct_tmux")
|
|
4084
|
-
.to_string();
|
|
4085
|
-
let owner_epoch = receiver
|
|
4086
|
-
.get("owner_epoch")
|
|
4087
|
-
.and_then(Value::as_u64)
|
|
4088
|
-
.or_else(|| {
|
|
4089
|
-
state
|
|
4090
|
-
.get("teams")
|
|
4091
|
-
.and_then(|v| v.as_object())
|
|
4092
|
-
.and_then(|teams| teams.get(&team_key))
|
|
4093
|
-
.and_then(|t| t.get("owner_epoch"))
|
|
4094
|
-
.and_then(Value::as_u64)
|
|
4095
|
-
})
|
|
4096
|
-
.unwrap_or(0);
|
|
4097
|
-
let entry = crate::leader::registry::build_entry(
|
|
4063
|
+
let Some(outcome) = crate::leader::registry::register_binding_from_state_best_effort(
|
|
4098
4064
|
workspace,
|
|
4099
|
-
|
|
4100
|
-
&transport_kind,
|
|
4101
|
-
receiver,
|
|
4102
|
-
owner_epoch,
|
|
4065
|
+
team,
|
|
4103
4066
|
source,
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
let event_log = crate::event_log::EventLog::new(workspace);
|
|
4107
|
-
let write_result = crate::leader::registry::write_entry_best_effort(&entry);
|
|
4108
|
-
let registry_status = match &write_result {
|
|
4109
|
-
Some(path) => {
|
|
4110
|
-
let _ = event_log.write(
|
|
4111
|
-
crate::leader::registry::EVENT_REGISTERED,
|
|
4112
|
-
json!({
|
|
4113
|
-
"path": path.display().to_string(),
|
|
4114
|
-
"team_key": team_key,
|
|
4115
|
-
"workspace_hash": entry.workspace_hash,
|
|
4116
|
-
"source": source,
|
|
4117
|
-
"owner_epoch": entry.owner_epoch,
|
|
4118
|
-
}),
|
|
4119
|
-
);
|
|
4120
|
-
json!({"status": "registered", "path": path.display().to_string()})
|
|
4121
|
-
}
|
|
4122
|
-
None => {
|
|
4123
|
-
let _ = event_log.write(
|
|
4124
|
-
crate::leader::registry::EVENT_WRITE_FAILED,
|
|
4125
|
-
json!({
|
|
4126
|
-
"team_key": team_key,
|
|
4127
|
-
"workspace_hash": entry.workspace_hash,
|
|
4128
|
-
"source": source,
|
|
4129
|
-
}),
|
|
4130
|
-
);
|
|
4131
|
-
json!({"status": "write_failed"})
|
|
4132
|
-
}
|
|
4067
|
+
) else {
|
|
4068
|
+
return;
|
|
4133
4069
|
};
|
|
4134
4070
|
if let Some(obj) = response.as_object_mut() {
|
|
4135
|
-
obj.insert("leader_registry".to_string(),
|
|
4071
|
+
obj.insert("leader_registry".to_string(), outcome.response_json());
|
|
4136
4072
|
}
|
|
4137
4073
|
}
|
|
4138
4074
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
use std::path::{Path, PathBuf};
|
|
23
23
|
|
|
24
24
|
use serde::{Deserialize, Serialize};
|
|
25
|
+
use serde_json::{json, Value};
|
|
25
26
|
use sha2::{Digest, Sha256};
|
|
26
27
|
|
|
27
28
|
use crate::transport::Transport;
|
|
@@ -98,6 +99,26 @@ pub const AMBIGUOUS_NAMES_FIELD: &str = "ambiguous_names";
|
|
|
98
99
|
/// a fully-qualified form.
|
|
99
100
|
pub const REASON_AMBIGUOUS: &str = "name_ambiguous";
|
|
100
101
|
|
|
102
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
103
|
+
pub struct RegistryWriteOutcome {
|
|
104
|
+
pub status: &'static str,
|
|
105
|
+
pub path: Option<PathBuf>,
|
|
106
|
+
pub team_key: String,
|
|
107
|
+
pub workspace_hash: String,
|
|
108
|
+
pub source: String,
|
|
109
|
+
pub owner_epoch: u64,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
impl RegistryWriteOutcome {
|
|
113
|
+
#[must_use]
|
|
114
|
+
pub fn response_json(&self) -> Value {
|
|
115
|
+
match &self.path {
|
|
116
|
+
Some(path) => json!({"status": self.status, "path": path.display().to_string()}),
|
|
117
|
+
None => json!({"status": self.status}),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
/// Absolute path to `~/.team-agent/leaders`. Returns `None` when the
|
|
102
123
|
/// caller has no HOME (e.g. some CI shells) — callers should treat this as
|
|
103
124
|
/// "no registry" rather than an error, since registry is derived.
|
|
@@ -170,6 +191,89 @@ pub fn build_entry(
|
|
|
170
191
|
}
|
|
171
192
|
}
|
|
172
193
|
|
|
194
|
+
/// After a canonical leader binding succeeds, write the derived host
|
|
195
|
+
/// discovery entry from the just-persisted state. Registry failure is
|
|
196
|
+
/// non-fatal: callers get a write_failed outcome and the binding remains
|
|
197
|
+
/// successful.
|
|
198
|
+
pub fn register_binding_from_state_best_effort(
|
|
199
|
+
workspace: &Path,
|
|
200
|
+
team: Option<&str>,
|
|
201
|
+
source: &str,
|
|
202
|
+
) -> Option<RegistryWriteOutcome> {
|
|
203
|
+
let Ok(state) = crate::state::persist::load_runtime_state(workspace) else {
|
|
204
|
+
return None;
|
|
205
|
+
};
|
|
206
|
+
let team_key = match team.filter(|team| !team.is_empty()) {
|
|
207
|
+
Some(team) => team.to_string(),
|
|
208
|
+
None => crate::state::projection::team_state_key(&state),
|
|
209
|
+
};
|
|
210
|
+
let receiver = state
|
|
211
|
+
.get("teams")
|
|
212
|
+
.and_then(|value| value.as_object())
|
|
213
|
+
.and_then(|teams| teams.get(&team_key))
|
|
214
|
+
.and_then(|team| team.get("leader_receiver"))
|
|
215
|
+
.or_else(|| state.get("leader_receiver"))
|
|
216
|
+
.cloned()?;
|
|
217
|
+
let transport_kind = receiver
|
|
218
|
+
.get("transport_kind")
|
|
219
|
+
.and_then(Value::as_str)
|
|
220
|
+
.unwrap_or("direct_tmux")
|
|
221
|
+
.to_string();
|
|
222
|
+
let owner_epoch = receiver
|
|
223
|
+
.get("owner_epoch")
|
|
224
|
+
.and_then(Value::as_u64)
|
|
225
|
+
.or_else(|| {
|
|
226
|
+
state
|
|
227
|
+
.get("teams")
|
|
228
|
+
.and_then(|value| value.as_object())
|
|
229
|
+
.and_then(|teams| teams.get(&team_key))
|
|
230
|
+
.and_then(|team| team.get("owner_epoch"))
|
|
231
|
+
.and_then(Value::as_u64)
|
|
232
|
+
})
|
|
233
|
+
.unwrap_or(0);
|
|
234
|
+
let entry = build_entry(
|
|
235
|
+
workspace,
|
|
236
|
+
&team_key,
|
|
237
|
+
&transport_kind,
|
|
238
|
+
receiver,
|
|
239
|
+
owner_epoch,
|
|
240
|
+
source,
|
|
241
|
+
chrono::Utc::now().to_rfc3339(),
|
|
242
|
+
);
|
|
243
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
244
|
+
let write_result = write_entry_best_effort(&entry);
|
|
245
|
+
let status = if let Some(path) = &write_result {
|
|
246
|
+
let _ = event_log.write(
|
|
247
|
+
EVENT_REGISTERED,
|
|
248
|
+
json!({
|
|
249
|
+
"path": path.display().to_string(),
|
|
250
|
+
"team_key": team_key,
|
|
251
|
+
"workspace_hash": entry.workspace_hash,
|
|
252
|
+
"source": source,
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
"registered"
|
|
256
|
+
} else {
|
|
257
|
+
let _ = event_log.write(
|
|
258
|
+
EVENT_WRITE_FAILED,
|
|
259
|
+
json!({
|
|
260
|
+
"team_key": team_key,
|
|
261
|
+
"workspace_hash": entry.workspace_hash,
|
|
262
|
+
"source": source,
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
"write_failed"
|
|
266
|
+
};
|
|
267
|
+
Some(RegistryWriteOutcome {
|
|
268
|
+
status,
|
|
269
|
+
path: write_result,
|
|
270
|
+
team_key,
|
|
271
|
+
workspace_hash: entry.workspace_hash,
|
|
272
|
+
source: source.to_string(),
|
|
273
|
+
owner_epoch: entry.owner_epoch,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
173
277
|
fn entry_filename(entry: &LeaderRegistryEntry) -> String {
|
|
174
278
|
format!("{}__{}.json", entry.workspace_hash, entry.team_key)
|
|
175
279
|
}
|
|
@@ -1404,6 +1404,11 @@ fn try_autobind_leader_after_restart(
|
|
|
1404
1404
|
let team_str = team;
|
|
1405
1405
|
match crate::leader::attach_leader(workspace, team_str, None, provider) {
|
|
1406
1406
|
Ok(result) if result.ok => {
|
|
1407
|
+
let _ = crate::leader::registry::register_binding_from_state_best_effort(
|
|
1408
|
+
workspace,
|
|
1409
|
+
team_str,
|
|
1410
|
+
"restart-auto-attach",
|
|
1411
|
+
);
|
|
1407
1412
|
eprintln!(
|
|
1408
1413
|
"team_agent::restart auto_attach_leader ok pane={:?} team={:?}",
|
|
1409
1414
|
result.bound_pane_id.as_ref().map(|p| p.as_str()),
|
|
@@ -3,6 +3,9 @@ use crate::transport::test_support::OfflineTransport;
|
|
|
3
3
|
use serde_json::json;
|
|
4
4
|
use serial_test::serial;
|
|
5
5
|
|
|
6
|
+
#[allow(dead_code)]
|
|
7
|
+
struct HermeticTestEnv;
|
|
8
|
+
|
|
6
9
|
const QS_TEAM_MD: &str =
|
|
7
10
|
"---\nname: quickteam\nobjective: Quick start.\nprovider: codex\n---\n\nQuick-start team.\n";
|
|
8
11
|
pub(super) const QS_VALID_ROLE: &str = "---\nname: implementer\nrole: Implementation Engineer\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nImplement bounded tasks.\n";
|
|
@@ -19,6 +19,9 @@ use serde_json::json;
|
|
|
19
19
|
use std::collections::BTreeSet;
|
|
20
20
|
use std::time::Duration;
|
|
21
21
|
|
|
22
|
+
#[allow(dead_code)]
|
|
23
|
+
struct HermeticTestEnv;
|
|
24
|
+
|
|
22
25
|
#[test]
|
|
23
26
|
fn concurrent_reset_discard_session_serializes() {
|
|
24
27
|
let ids = (1..=6).map(|n| format!("w{n}")).collect::<Vec<_>>();
|
|
@@ -9,22 +9,89 @@ use crate::transport::WindowName;
|
|
|
9
9
|
use serde_json::{json, Map, Value};
|
|
10
10
|
use std::collections::{BTreeMap, BTreeSet};
|
|
11
11
|
use std::path::{Path, PathBuf};
|
|
12
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
12
13
|
|
|
13
14
|
const ANCESTRY_ENV: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
|
|
16
|
+
static HERMETIC_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
17
|
+
|
|
18
|
+
struct HermeticTestEnv {
|
|
19
|
+
root: PathBuf,
|
|
20
|
+
previous: Vec<(&'static str, Option<String>)>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl HermeticTestEnv {
|
|
24
|
+
fn enter(tag: &str) -> Self {
|
|
25
|
+
let base = std::env::var_os("TEAM_AGENT_TEST_TMP")
|
|
26
|
+
.map(PathBuf::from)
|
|
27
|
+
.unwrap_or_else(std::env::temp_dir);
|
|
28
|
+
std::fs::create_dir_all(&base).expect("create hermetic test tmp root");
|
|
29
|
+
let root = base.join(format!(
|
|
30
|
+
"ta-phase-golden-{tag}-{}-{}",
|
|
31
|
+
std::process::id(),
|
|
32
|
+
HERMETIC_COUNTER.fetch_add(1, Ordering::Relaxed)
|
|
33
|
+
));
|
|
34
|
+
let _ = std::fs::remove_dir_all(&root);
|
|
35
|
+
std::fs::create_dir_all(root.join("home/.team-agent/leaders"))
|
|
36
|
+
.expect("create phase golden hermetic root");
|
|
37
|
+
let root = std::fs::canonicalize(root).expect("canonicalize phase golden hermetic root");
|
|
38
|
+
let home = root.join("home");
|
|
39
|
+
let mut previous = Vec::new();
|
|
40
|
+
for key in std::iter::once("HOME").chain(hermetic_caller_envs().iter().copied()) {
|
|
41
|
+
previous.push((key, std::env::var(key).ok()));
|
|
42
|
+
}
|
|
43
|
+
unsafe {
|
|
44
|
+
std::env::set_var("HOME", &home);
|
|
45
|
+
for key in hermetic_caller_envs() {
|
|
46
|
+
std::env::remove_var(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
Self { root, previous }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn workspace(&self, tag: &str) -> PathBuf {
|
|
53
|
+
let path = self.root.join(format!(
|
|
54
|
+
"workspace-{tag}-{}",
|
|
55
|
+
HERMETIC_COUNTER.fetch_add(1, Ordering::Relaxed)
|
|
56
|
+
));
|
|
57
|
+
std::fs::create_dir_all(&path).expect("create phase golden workspace");
|
|
58
|
+
std::fs::canonicalize(path).expect("canonicalize phase golden workspace")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl Drop for HermeticTestEnv {
|
|
63
|
+
fn drop(&mut self) {
|
|
64
|
+
for (key, value) in self.previous.drain(..).rev() {
|
|
65
|
+
unsafe {
|
|
66
|
+
if let Some(value) = value {
|
|
67
|
+
std::env::set_var(key, value);
|
|
68
|
+
} else {
|
|
69
|
+
std::env::remove_var(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if std::env::var("TEAM_AGENT_KEEP_TEST_TMP").as_deref() != Ok("1") {
|
|
74
|
+
let _ = std::fs::remove_dir_all(&self.root);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn hermetic_caller_envs() -> &'static [&'static str] {
|
|
80
|
+
&[
|
|
81
|
+
"TMUX",
|
|
82
|
+
"TMUX_PANE",
|
|
83
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
84
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
85
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
86
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
87
|
+
"TEAM_AGENT_MACHINE_FINGERPRINT",
|
|
88
|
+
"TEAM_AGENT_WORKSPACE",
|
|
89
|
+
"TEAM_AGENT_TEAM_ID",
|
|
90
|
+
"TEAM_AGENT_OWNER_TEAM_ID",
|
|
91
|
+
"TEAM_AGENT_ACTIVE_TEAM",
|
|
92
|
+
"TEAM_AGENT_ID",
|
|
93
|
+
]
|
|
94
|
+
}
|
|
28
95
|
|
|
29
96
|
#[test]
|
|
30
97
|
#[serial_test::serial(env)]
|
|
@@ -154,12 +221,9 @@ struct PhaseGolden {
|
|
|
154
221
|
}
|
|
155
222
|
|
|
156
223
|
fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
224
|
+
let hermetic = HermeticTestEnv::enter(spec.phase);
|
|
157
225
|
let _permission_mode = EnvVarGuard::set(ANCESTRY_ENV, "[]");
|
|
158
|
-
let
|
|
159
|
-
.iter()
|
|
160
|
-
.map(|key| EnvVarGuard::unset(key))
|
|
161
|
-
.collect::<Vec<_>>();
|
|
162
|
-
let team = two_worker_team_dir();
|
|
226
|
+
let team = two_worker_team_dir(&hermetic);
|
|
163
227
|
let workspace = team.parent().expect("workspace").to_path_buf();
|
|
164
228
|
seed_healthy_coordinator(&workspace);
|
|
165
229
|
let launch_transport = codex_ready_transport();
|
|
@@ -298,8 +362,8 @@ fn phase_b_reset_discard_session(
|
|
|
298
362
|
}
|
|
299
363
|
}
|
|
300
364
|
|
|
301
|
-
fn two_worker_team_dir() -> PathBuf {
|
|
302
|
-
let team =
|
|
365
|
+
fn two_worker_team_dir(hermetic: &HermeticTestEnv) -> PathBuf {
|
|
366
|
+
let team = hermetic.workspace("team").join("teamdir");
|
|
303
367
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
304
368
|
std::fs::write(
|
|
305
369
|
team.join("TEAM.md"),
|
|
@@ -415,13 +479,6 @@ impl EnvVarGuard {
|
|
|
415
479
|
Self { key, previous }
|
|
416
480
|
}
|
|
417
481
|
|
|
418
|
-
fn unset(key: &'static str) -> Self {
|
|
419
|
-
let previous = std::env::var(key).ok();
|
|
420
|
-
unsafe {
|
|
421
|
-
std::env::remove_var(key);
|
|
422
|
-
}
|
|
423
|
-
Self { key, previous }
|
|
424
|
-
}
|
|
425
482
|
}
|
|
426
483
|
|
|
427
484
|
impl Drop for EnvVarGuard {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.17",
|
|
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.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.17",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.17",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.17"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|