@team-agent/installer 0.5.50 → 0.5.51

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.
Files changed (84) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +11 -48
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  27. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  28. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  29. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  30. package/crates/team-agent/src/db/message_store.rs +154 -44
  31. package/crates/team-agent/src/event_log.rs +73 -0
  32. package/crates/team-agent/src/leader/start.rs +28 -4
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  35. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  36. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  37. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  38. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  39. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  40. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  41. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  42. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  43. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  44. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  45. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  46. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  47. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  48. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  49. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  50. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  51. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  52. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  53. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  55. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  56. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  59. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  60. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  61. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  62. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  63. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  64. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  65. package/crates/team-agent/src/messaging/activity.rs +4 -2
  66. package/crates/team-agent/src/messaging/address.rs +86 -0
  67. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  68. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  69. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  70. package/crates/team-agent/src/messaging/mod.rs +10 -2
  71. package/crates/team-agent/src/messaging/persist.rs +309 -0
  72. package/crates/team-agent/src/messaging/results.rs +16 -24
  73. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  74. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  75. package/crates/team-agent/src/messaging/send.rs +101 -49
  76. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  77. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  78. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  79. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  80. package/crates/team-agent/src/redaction.rs +72 -2
  81. package/crates/team-agent/src/state/persist.rs +2 -1
  82. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  83. package/crates/team-agent/src/state/repository.rs +59 -16
  84. package/package.json +4 -4
@@ -49,6 +49,7 @@ pub fn notify_result_watchers(
49
49
  if idx == 0 {
50
50
  primary_watcher_id = Some(watcher_id.to_string());
51
51
  notices.push(deliver_primary_watcher(
52
+ workspace,
52
53
  &conn,
53
54
  &store,
54
55
  event_log,
@@ -112,6 +113,7 @@ fn watcher_matches(
112
113
  }
113
114
 
114
115
  fn deliver_primary_watcher(
116
+ workspace: &Path,
115
117
  conn: &rusqlite::Connection,
116
118
  store: &MessageStore,
117
119
  event_log: &EventLog,
@@ -178,7 +180,10 @@ fn deliver_primary_watcher(
178
180
  );
179
181
  }
180
182
  let content = format_result_watcher_notification(result);
181
- let message_id = store.create_message(
183
+ let super::PersistResolution::Persisted(persisted) = super::persist::persist_internal_send(
184
+ workspace,
185
+ super::InternalSendKind::Watcher,
186
+ watcher.get("owner_team_id").and_then(|v| v.as_str()),
182
187
  result_task,
183
188
  watcher
184
189
  .get("leader_id")
@@ -188,8 +193,13 @@ fn deliver_primary_watcher(
188
193
  &content,
189
194
  None,
190
195
  false,
191
- watcher.get("owner_team_id").and_then(|v| v.as_str()),
192
- )?;
196
+ None,
197
+ super::InitialDisposition::Accepted,
198
+ )?
199
+ else {
200
+ unreachable!("watcher notifications do not accept caller-supplied ids")
201
+ };
202
+ let message_id = persisted.message_id;
193
203
  let claim = store.claim_leader_notification_delivery(NotificationClaimParams {
194
204
  result_id,
195
205
  owner_team_id: watcher.get("owner_team_id").and_then(|v| v.as_str()),
@@ -13,6 +13,7 @@ const SENSITIVE_FAMILIES: [&str; 7] = [
13
13
  "SECRET",
14
14
  "CREDENTIAL",
15
15
  ];
16
+ const SENSITIVE_KEYS: [&str; 2] = ["authorization_header", "credential_blob"];
16
17
  const STRUCTURAL_KEYS: [&str; 13] = [
17
18
  "active_team_key",
18
19
  "cohort_key",
@@ -31,7 +32,7 @@ const STRUCTURAL_KEYS: [&str; 13] = [
31
32
 
32
33
  static SHELL_ASSIGNMENT: LazyLock<Regex> = LazyLock::new(|| {
33
34
  Regex::new(
34
- r#"(?i)(?P<prefix>^|[\s\[,;('"])(?P<key>[a-z_][a-z0-9_]*)=(?P<value>'[^']*'|"[^"]*"|[^\s,\]\[};)]+)"#,
35
+ r#"(?i)(?P<prefix>^|[\s\[,;('"])(?P<key>[a-z_][a-z0-9_.]*)=(?P<value>'[^']*'|"[^"]*"|[^\s,\]\[};)]+)"#,
35
36
  )
36
37
  .expect("shell assignment redaction regex")
37
38
  });
@@ -41,6 +42,13 @@ static URL_USERINFO: LazyLock<Regex> = LazyLock::new(|| {
41
42
  .expect("URL userinfo redaction regex")
42
43
  });
43
44
 
45
+ static BEARER_TOKEN: LazyLock<Regex> = LazyLock::new(|| {
46
+ Regex::new(
47
+ r"(?i)(?P<prefix>\bBearer[ \t]+)(?P<token>[A-Za-z0-9][-A-Za-z0-9._~+/=]{14,}[-A-Za-z0-9._~+/=])",
48
+ )
49
+ .expect("Bearer token redaction regex")
50
+ });
51
+
44
52
  pub(crate) fn redact_external_value(value: &serde_json::Value) -> serde_json::Value {
45
53
  match value {
46
54
  Value::Object(object) => Value::Object(
@@ -49,6 +57,8 @@ pub(crate) fn redact_external_value(value: &serde_json::Value) -> serde_json::Va
49
57
  .map(|(key, value)| {
50
58
  let value = if is_sensitive_env_key(key) {
51
59
  Value::String(REDACTED.to_string())
60
+ } else if is_argument_array_key(key) {
61
+ redact_argument_array(value)
52
62
  } else {
53
63
  redact_external_value(value)
54
64
  };
@@ -62,6 +72,33 @@ pub(crate) fn redact_external_value(value: &serde_json::Value) -> serde_json::Va
62
72
  }
63
73
  }
64
74
 
75
+ fn is_argument_array_key(key: &str) -> bool {
76
+ key.eq_ignore_ascii_case("argv") || key.eq_ignore_ascii_case("command")
77
+ }
78
+
79
+ fn redact_argument_array(value: &Value) -> Value {
80
+ let Value::Array(values) = value else {
81
+ return redact_external_value(value);
82
+ };
83
+ let mut redact_next = false;
84
+ Value::Array(
85
+ values
86
+ .iter()
87
+ .map(|value| {
88
+ if redact_next {
89
+ redact_next = false;
90
+ return Value::String(REDACTED.to_string());
91
+ }
92
+ if let Some(flag) = value.as_str().filter(|flag| flag.starts_with('-')) {
93
+ let normalized = flag.trim_start_matches('-').replace('-', "_");
94
+ redact_next = is_sensitive_env_key(&normalized);
95
+ }
96
+ redact_external_value(value)
97
+ })
98
+ .collect(),
99
+ )
100
+ }
101
+
65
102
  pub(crate) fn redact_external_text(text: &str) -> String {
66
103
  let assignments = SHELL_ASSIGNMENT.replace_all(text, |captures: &Captures<'_>| {
67
104
  let key = captures.name("key").map_or("", |value| value.as_str());
@@ -90,18 +127,28 @@ pub(crate) fn redact_external_text(text: &str) -> String {
90
127
  };
91
128
  format!("{prefix}{key}={quote}{safe_value}{quote}")
92
129
  });
93
- URL_USERINFO
130
+ let urls = URL_USERINFO
94
131
  .replace_all(&assignments, "${scheme}[REDACTED]@")
132
+ .into_owned();
133
+ BEARER_TOKEN
134
+ .replace_all(&urls, "${prefix}[REDACTED]")
95
135
  .into_owned()
96
136
  }
97
137
 
98
138
  fn is_sensitive_env_key(key: &str) -> bool {
139
+ let key = key.rsplit('.').next().unwrap_or(key);
99
140
  if STRUCTURAL_KEYS
100
141
  .iter()
101
142
  .any(|structural| key.eq_ignore_ascii_case(structural))
102
143
  {
103
144
  return false;
104
145
  }
146
+ if SENSITIVE_KEYS
147
+ .iter()
148
+ .any(|sensitive| key.eq_ignore_ascii_case(sensitive))
149
+ {
150
+ return true;
151
+ }
105
152
  let key = key.to_ascii_uppercase();
106
153
  SENSITIVE_FAMILIES
107
154
  .iter()
@@ -182,4 +229,27 @@ mod tests {
182
229
  assert!(text.contains("https://[REDACTED]@proxy.invalid:8443/path"));
183
230
  assert_eq!(twice, once);
184
231
  }
232
+
233
+ #[test]
234
+ fn event_log_shapes_are_masked_without_hiding_structural_arguments() {
235
+ let marker = "synthetic-event-log-marker";
236
+ let input = json!({
237
+ "authorization_header": format!("Bearer {marker}"),
238
+ "credential_blob": marker,
239
+ "config": format!("mcp_servers.demo.env.OPENAI_API_KEY=\"{marker}\""),
240
+ "argv": ["provider", "--api-key", marker, "--team-key", "current"],
241
+ "command": ["provider", "--authorization-header", marker, "--auth-mode", "subscription"],
242
+ });
243
+
244
+ let redacted = redact_external_value(&input);
245
+
246
+ assert!(!redacted.to_string().contains(marker));
247
+ assert_eq!(redacted["authorization_header"], REDACTED);
248
+ assert_eq!(redacted["credential_blob"], REDACTED);
249
+ assert_eq!(redacted["argv"][2], REDACTED);
250
+ assert_eq!(redacted["argv"][4], "current");
251
+ assert_eq!(redacted["command"][2], REDACTED);
252
+ assert_eq!(redacted["command"][4], "subscription");
253
+ assert_eq!(redact_external_value(&redacted), redacted);
254
+ }
185
255
  }
@@ -47,7 +47,7 @@ const SESSION_STATE_FIELDS: [&str; 6] = [
47
47
  ];
48
48
  const LIVE_TOPOLOGY_FIELDS: [&str; 5] =
49
49
  ["pane_id", "pane_pid", "window", "spawned_at", "spawn_epoch"];
50
- const ROSTER_STUB_ALLOWLIST: [&str; 15] = [
50
+ const ROSTER_STUB_ALLOWLIST: [&str; 16] = [
51
51
  "agent_id",
52
52
  "provider",
53
53
  "auth_mode",
@@ -57,6 +57,7 @@ const ROSTER_STUB_ALLOWLIST: [&str; 15] = [
57
57
  "profile",
58
58
  "_profile_dir",
59
59
  "dynamic_role_file",
60
+ "role_source_ownership",
60
61
  "effort",
61
62
  "forked_from",
62
63
  "managed_mcp_config",
@@ -0,0 +1,47 @@
1
+ use super::{reapply_scope, ReapplyScope, StateWriteIntent};
2
+
3
+ #[test]
4
+ fn mcp_reapply_scope_matches_legacy_helpers() {
5
+ assert!(
6
+ reapply_scope(&StateWriteIntent::McpUpdateStateNote {
7
+ team_key: Some("team-a"),
8
+ }) == ReapplyScope::Team
9
+ );
10
+ assert!(
11
+ reapply_scope(&StateWriteIntent::McpAssignTask {
12
+ team_key: Some("team-a"),
13
+ task_id: "task-a",
14
+ }) == ReapplyScope::Root
15
+ );
16
+ }
17
+
18
+ #[test]
19
+ fn optional_unmigrated_read_preserves_missing_corrupt_and_valid_shapes(
20
+ ) -> Result<(), Box<dyn std::error::Error>> {
21
+ let nonce = std::time::SystemTime::now()
22
+ .duration_since(std::time::UNIX_EPOCH)?
23
+ .as_nanos();
24
+ let workspace = std::env::temp_dir().join(format!("state-repository-read-{nonce}"));
25
+ let repository = super::StateRepository::new(&workspace);
26
+ assert!(repository
27
+ .load_workspace_if_exists_without_migrations()?
28
+ .is_none());
29
+
30
+ let path = super::helper_workspace_path(&workspace);
31
+ if let Some(parent) = path.parent() {
32
+ std::fs::create_dir_all(parent)?;
33
+ }
34
+ std::fs::write(&path, b"{not-json")?;
35
+ assert!(matches!(
36
+ repository.load_workspace_if_exists_without_migrations(),
37
+ Err(super::StateError::Json(_))
38
+ ));
39
+
40
+ std::fs::write(&path, br#"{"legacy":true}"#)?;
41
+ assert_eq!(
42
+ repository.load_workspace_if_exists_without_migrations()?,
43
+ Some(serde_json::json!({"legacy": true}))
44
+ );
45
+ std::fs::remove_dir_all(workspace)?;
46
+ Ok(())
47
+ }
@@ -40,7 +40,8 @@ use super::StateError;
40
40
  // `save_team_scoped_state(` tokens that the governance scanner counts.
41
41
  #[allow(unused_imports)]
42
42
  use super::persist::{
43
- load_runtime_state as helper_load_workspace, save_runtime_state as helper_write_root,
43
+ load_runtime_state as helper_load_workspace,
44
+ runtime_state_path as helper_workspace_path, save_runtime_state as helper_write_root,
44
45
  save_runtime_state_reapplying_after_conflict as helper_write_root_reapply,
45
46
  save_runtime_state_with_deleted_agents as helper_write_root_with_deleted_agents,
46
47
  save_runtime_state_with_lifecycle_topology_authority as helper_write_root_with_lifecycle_topology_authority,
@@ -79,6 +80,19 @@ impl<'a> StateRepository<'a> {
79
80
  helper_load_workspace(self.workspace)
80
81
  }
81
82
 
83
+ /// Load the canonical workspace document without running read-time
84
+ /// migrations. `None` preserves the legacy raw-reader distinction between
85
+ /// a missing file and a present empty/default document.
86
+ pub fn load_workspace_if_exists_without_migrations(
87
+ &self,
88
+ ) -> Result<Option<Value>, StateError> {
89
+ if !helper_workspace_path(self.workspace).exists() {
90
+ return Ok(None);
91
+ }
92
+ let text = std::fs::read_to_string(helper_workspace_path(self.workspace))?;
93
+ serde_json::from_str(&text).map(Some).map_err(StateError::from)
94
+ }
95
+
82
96
  /// Resolve a team-scoped projection using the existing projection selector.
83
97
  /// `team_key = None` selects the ambient team the same way as pre-S1a.
84
98
  pub fn load_team(&self, team_key: Option<&str>) -> Result<Value, StateError> {
@@ -171,6 +185,10 @@ pub enum StateWriteIntent<'a> {
171
185
  team_key: Option<&'a str>,
172
186
  agent_id: &'a str,
173
187
  },
188
+ ForceRecreateRollback {
189
+ team_key: &'a str,
190
+ agent_id: &'a str,
191
+ },
174
192
  ClaimLeader {
175
193
  team_key: &'a str,
176
194
  },
@@ -193,7 +211,7 @@ pub enum StateWriteIntent<'a> {
193
211
  /// so future S1b migration can route it deliberately.
194
212
  CoordinatorApiErrorRecovery {
195
213
  team_key: Option<&'a str>,
196
- agent_id: &'a str,
214
+ agent_id: Option<&'a str>,
197
215
  },
198
216
  McpAssignTask {
199
217
  team_key: Option<&'a str>,
@@ -331,6 +349,16 @@ fn route_direct(
331
349
  Some(_) => helper_write_team_scoped_with_deleted_agents(workspace, state, &[agent_id]),
332
350
  None => helper_write_root_with_deleted_agents(workspace, state, &[agent_id]),
333
351
  },
352
+ // Force-recreate rollback restores an existing row after the
353
+ // replacement spawn has advanced its lifecycle tuple. The selected
354
+ // row is therefore the explicit topology authority.
355
+ StateWriteIntent::ForceRecreateRollback { agent_id, .. } => {
356
+ helper_write_team_scoped_with_lifecycle_topology_authority(
357
+ workspace,
358
+ state,
359
+ &[agent_id],
360
+ )
361
+ }
334
362
  // ClaimLeader -> leader/lease.rs:1625 uses the root helper, and the
335
363
  // scoped preserve-claim-fields variant at :1702 uses the team-tombstoned
336
364
  // agents helper.
@@ -396,16 +424,14 @@ fn route_direct(
396
424
  // Route intents that historically used the `_reapplying_after_conflict`
397
425
  // helper family. Behavior stays identical: S1a chooses the same helper the
398
426
  // legacy caller would have chosen for a reapply.
399
- fn route_reapply<F>(
400
- workspace: &Path,
401
- intent: StateWriteIntent<'_>,
402
- state: &Value,
403
- reapply: F,
404
- ) -> Result<(), StateError>
405
- where
406
- F: FnOnce(&mut Value),
407
- {
408
- let use_team_scoped = matches!(
427
+ #[derive(Clone, Copy, Eq, PartialEq)]
428
+ enum ReapplyScope {
429
+ Root,
430
+ Team,
431
+ }
432
+
433
+ fn reapply_scope(intent: &StateWriteIntent<'_>) -> ReapplyScope {
434
+ if matches!(
409
435
  intent,
410
436
  StateWriteIntent::RestartSessionRepair { .. }
411
437
  | StateWriteIntent::MessagingDeliveryState {
@@ -415,14 +441,31 @@ where
415
441
  owner_team_id: Some(_),
416
442
  }
417
443
  | StateWriteIntent::CoordinatorTick { .. }
418
- | StateWriteIntent::McpAssignTask {
444
+ | StateWriteIntent::McpUpdateStateNote {
419
445
  team_key: Some(_),
420
- ..
421
446
  }
422
- );
423
- if use_team_scoped {
447
+ ) {
448
+ ReapplyScope::Team
449
+ } else {
450
+ ReapplyScope::Root
451
+ }
452
+ }
453
+
454
+ fn route_reapply<F>(
455
+ workspace: &Path,
456
+ intent: StateWriteIntent<'_>,
457
+ state: &Value,
458
+ reapply: F,
459
+ ) -> Result<(), StateError>
460
+ where
461
+ F: FnOnce(&mut Value),
462
+ {
463
+ if reapply_scope(&intent) == ReapplyScope::Team {
424
464
  helper_write_team_scoped_reapply(workspace, state, reapply)
425
465
  } else {
426
466
  helper_write_root_reapply(workspace, state, reapply)
427
467
  }
428
468
  }
469
+
470
+ #[cfg(test)]
471
+ mod tests;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.50",
3
+ "version": "0.5.51",
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.50",
24
- "@team-agent/cli-darwin-x64": "0.5.50",
25
- "@team-agent/cli-linux-x64": "0.5.50"
23
+ "@team-agent/cli-darwin-arm64": "0.5.51",
24
+ "@team-agent/cli-darwin-x64": "0.5.51",
25
+ "@team-agent/cli-linux-x64": "0.5.51"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",