@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 CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.16"
578
+ version = "0.5.17"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.16"
12
+ version = "0.5.17"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -4060,79 +4060,15 @@ pub mod leader_port {
4060
4060
  source: &str,
4061
4061
  response: &mut Value,
4062
4062
  ) {
4063
- let Ok(state) = crate::state::persist::load_runtime_state(workspace) else {
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
- &team_key,
4100
- &transport_kind,
4101
- receiver,
4102
- owner_epoch,
4065
+ team,
4103
4066
  source,
4104
- chrono::Utc::now().to_rfc3339(),
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(), registry_status);
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
- const CALLER_IDENTITY_ENVS: &[&str] = &[
15
- "TMUX",
16
- "TMUX_PANE",
17
- "TEAM_AGENT_LEADER_PANE_ID",
18
- "TEAM_AGENT_LEADER_SESSION_UUID",
19
- "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
20
- "TEAM_AGENT_LEADER_PROVIDER",
21
- "TEAM_AGENT_MACHINE_FINGERPRINT",
22
- "TEAM_AGENT_WORKSPACE",
23
- "TEAM_AGENT_TEAM_ID",
24
- "TEAM_AGENT_OWNER_TEAM_ID",
25
- "TEAM_AGENT_ACTIVE_TEAM",
26
- "TEAM_AGENT_ID",
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 _caller_identity = CALLER_IDENTITY_ENVS
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 = temp_ws().join("teamdir");
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.16",
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.16",
24
- "@team-agent/cli-darwin-x64": "0.5.16",
25
- "@team-agent/cli-linux-x64": "0.5.16"
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",