@team-agent/installer 0.5.113 → 0.5.115

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 (52) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +28 -15
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +57 -22
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/mod.rs +157 -8
  10. package/crates/team-agent/src/cli/models.rs +119 -18
  11. package/crates/team-agent/src/cli/spec.rs +7 -1
  12. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  13. package/crates/team-agent/src/cli/tests/base.rs +88 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
  15. package/crates/team-agent/src/compiler.rs +66 -18
  16. package/crates/team-agent/src/contract_runtime/backend.rs +959 -0
  17. package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
  18. package/crates/team-agent/src/contract_runtime/config.rs +340 -0
  19. package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
  20. package/crates/team-agent/src/contract_runtime/discovery.rs +383 -0
  21. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  22. package/crates/team-agent/src/contract_runtime/framework.rs +294 -0
  23. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  24. package/crates/team-agent/src/contract_runtime/mod.rs +16 -0
  25. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  26. package/crates/team-agent/src/contract_runtime/pump.rs +269 -0
  27. package/crates/team-agent/src/contract_runtime/registry.rs +197 -0
  28. package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
  29. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  30. package/crates/team-agent/src/leader/mod.rs +1 -0
  31. package/crates/team-agent/src/leader/prompt.rs +906 -0
  32. package/crates/team-agent/src/leader/start.rs +353 -21
  33. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  34. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  35. package/crates/team-agent/src/leader/types.rs +6 -0
  36. package/crates/team-agent/src/lib.rs +4 -0
  37. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +571 -389
  38. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  39. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +15 -0
  40. package/crates/team-agent/src/lifecycle/launch.rs +45 -28
  41. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -1
  42. package/crates/team-agent/src/lifecycle/restart/selection.rs +34 -0
  43. package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
  44. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  45. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  46. package/crates/team-agent/src/main.rs +6 -0
  47. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  48. package/crates/team-agent/src/messaging/send.rs +10 -0
  49. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  50. package/crates/team-agent/src/model/spec.rs +24 -12
  51. package/crates/team-agent/src/state/persist.rs +73 -0
  52. package/package.json +4 -4
@@ -0,0 +1,160 @@
1
+ //! Stable per-seat primary cwd. The project stays an additional directory;
2
+ //! private MCP files never overwrite a shared project or another seat's config.
3
+ use std::io::{Read, Write};
4
+ use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt};
5
+ use std::path::Path;
6
+
7
+ use serde::{Deserialize, Serialize};
8
+ use team_agent_contract::contract::{session::CwdIdentity, types::SeatId};
9
+ use team_agent_contract::host::process::resolve_cwd;
10
+
11
+ use super::backend::{root_for, BackendError, Binding};
12
+
13
+ #[derive(Serialize, Deserialize)]
14
+ #[serde(deny_unknown_fields)]
15
+ struct CwdRecord {
16
+ version: u32,
17
+ seat: SeatId,
18
+ cwd: CwdIdentity,
19
+ }
20
+
21
+ /// Requires an already owned framework root. Existing unrecorded directories
22
+ /// are refused, not adopted. A replaced inode or any symlink is a hard error.
23
+ pub(super) fn prepare(binding: &Binding, seat: &SeatId) -> Result<CwdIdentity, BackendError> {
24
+ let root = root_for(&binding.workspace.path, &binding.scope);
25
+ validate_directory(&root)?;
26
+ let parent = root.join("workspaces");
27
+ let path = parent.join(seat.as_str());
28
+ let record_path = root.join(format!("cwd-{}.json", seat.as_str()));
29
+ match std::fs::symlink_metadata(&record_path) {
30
+ Ok(metadata) => {
31
+ if !metadata.is_file() || metadata.file_type().is_symlink() {
32
+ return Err(BackendError::Metadata);
33
+ }
34
+ let mut file = std::fs::OpenOptions::new().read(true)
35
+ .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC).open(&record_path)?;
36
+ let opened = file.metadata()?;
37
+ if opened.dev() != metadata.dev() || opened.ino() != metadata.ino() {
38
+ return Err(BackendError::Metadata);
39
+ }
40
+ let mut bytes = Vec::new();
41
+ Read::by_ref(&mut file).take(65537).read_to_end(&mut bytes)?;
42
+ if bytes.len() > 65536 {
43
+ return Err(BackendError::Metadata);
44
+ }
45
+ let record: CwdRecord = serde_json::from_slice(&bytes).map_err(|_| BackendError::Metadata)?;
46
+ if record.version != 1 || record.seat != *seat || record.cwd.path != path {
47
+ return Err(BackendError::Metadata);
48
+ }
49
+ validate_directory(&parent)?;
50
+ validate_directory(&path)?;
51
+ if resolve_cwd(&path)? != record.cwd {
52
+ return Err(BackendError::Metadata);
53
+ }
54
+ return Ok(record.cwd);
55
+ }
56
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
57
+ Err(error) => return Err(error.into()),
58
+ }
59
+ match std::fs::symlink_metadata(&parent) {
60
+ Ok(_) => validate_directory(&parent)?,
61
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
62
+ std::fs::DirBuilder::new().mode(0o700).create(&parent)?;
63
+ }
64
+ Err(error) => return Err(error.into()),
65
+ }
66
+ // Exclusive create is deliberate: missing record + existing path is not
67
+ // enough evidence that this framework owns the previous incarnation.
68
+ std::fs::DirBuilder::new().mode(0o700).create(&path)?;
69
+ validate_directory(&path)?;
70
+ let cwd = resolve_cwd(&path)?;
71
+ let record = CwdRecord { version: 1, seat: seat.clone(), cwd: cwd.clone() };
72
+ let bytes = serde_json::to_vec(&record).map_err(|_| BackendError::Metadata)?;
73
+ let mut file = std::fs::OpenOptions::new().write(true).create_new(true)
74
+ .mode(0o600).custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC).open(record_path)?;
75
+ file.write_all(&bytes)?;
76
+ file.sync_all()?;
77
+ Ok(cwd)
78
+ }
79
+
80
+ fn validate_directory(path: &Path) -> Result<(), BackendError> {
81
+ let metadata = std::fs::symlink_metadata(path)?;
82
+ if !metadata.is_dir() || metadata.file_type().is_symlink() || path.canonicalize()? != path
83
+ || metadata.uid() != unsafe { libc::geteuid() } || metadata.mode() & 0o077 != 0
84
+ {
85
+ return Err(BackendError::Metadata);
86
+ }
87
+ Ok(())
88
+ }
89
+
90
+ #[cfg(test)]
91
+ mod tests {
92
+ use super::*;
93
+ use std::path::PathBuf;
94
+ use std::sync::atomic::{AtomicU64, Ordering};
95
+ use team_agent_contract::contract::types::{Digest, ScopeId};
96
+
97
+ static SEQUENCE: AtomicU64 = AtomicU64::new(0);
98
+ struct Fixture { path: PathBuf, binding: Binding }
99
+ impl Fixture {
100
+ fn new() -> Self {
101
+ let path = std::env::temp_dir().canonicalize().unwrap().join(format!(
102
+ "contract-cwd-{}-{}-{}", std::process::id(),
103
+ std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos(),
104
+ SEQUENCE.fetch_add(1, Ordering::Relaxed),
105
+ ));
106
+ std::fs::DirBuilder::new().mode(0o700).create(&path).unwrap();
107
+ let binding = Binding {
108
+ version: 1, workspace: resolve_cwd(&path).unwrap(), team: "test".into(),
109
+ scope: ScopeId::new("cwd-test").unwrap(), endpoint: "framework:cwd-test".into(),
110
+ candidate: "/controlled/candidate".into(), candidate_sha256: Digest([1; 32]),
111
+ tmux: "/controlled/tmux".into(), transport_root: path.join("transport"),
112
+ };
113
+ let root = root_for(&path, &binding.scope);
114
+ std::fs::create_dir_all(root.parent().unwrap()).unwrap();
115
+ std::fs::DirBuilder::new().mode(0o700).create(&root).unwrap();
116
+ Self { path, binding }
117
+ }
118
+ }
119
+ impl Drop for Fixture {
120
+ fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.path); }
121
+ }
122
+
123
+ #[test]
124
+ fn stable_cwd_is_reused_and_distinct_seats_do_not_share_a_primary() {
125
+ let f = Fixture::new();
126
+ let a = SeatId::new("a").unwrap();
127
+ let b = SeatId::new("b").unwrap();
128
+ let first = prepare(&f.binding, &a).unwrap();
129
+ assert_eq!(prepare(&f.binding, &a).unwrap(), first);
130
+ let second = prepare(&f.binding, &b).unwrap();
131
+ assert_ne!(first.path, second.path);
132
+ assert_ne!(first.identity, second.identity);
133
+ assert!(!f.path.join(".cursor").exists());
134
+ }
135
+
136
+ #[test]
137
+ fn replaced_inode_or_symlink_is_not_adopted_on_resume() {
138
+ let f = Fixture::new();
139
+ let seat = SeatId::new("worker").unwrap();
140
+ let original = prepare(&f.binding, &seat).unwrap();
141
+ let retained = original.path.with_extension("old");
142
+ std::fs::rename(&original.path, &retained).unwrap();
143
+ std::fs::DirBuilder::new().mode(0o700).create(&original.path).unwrap();
144
+ assert!(prepare(&f.binding, &seat).is_err());
145
+ std::fs::remove_dir(&original.path).unwrap();
146
+ std::os::unix::fs::symlink(&retained, &original.path).unwrap();
147
+ assert!(prepare(&f.binding, &seat).is_err());
148
+ }
149
+
150
+ #[test]
151
+ fn unrecorded_existing_directory_is_not_claimed() {
152
+ let f = Fixture::new();
153
+ let root = root_for(&f.path, &f.binding.scope);
154
+ let parent = root.join("workspaces");
155
+ std::fs::DirBuilder::new().mode(0o700).create(&parent).unwrap();
156
+ std::fs::DirBuilder::new().mode(0o700).create(parent.join("worker")).unwrap();
157
+ assert!(prepare(&f.binding, &SeatId::new("worker").unwrap()).is_err());
158
+ assert!(!root.join("cwd-worker.json").exists());
159
+ }
160
+ }
@@ -359,6 +359,19 @@ impl Coordinator {
359
359
  let event_log = EventLog::new(self.workspace.as_path());
360
360
  increment_coordinator_tick_iteration_count(&self.workspace);
361
361
 
362
+ #[cfg(unix)]
363
+ if let Err(error) =
364
+ crate::contract_runtime::pump::tick(self.workspace.as_path(), &mut state)
365
+ {
366
+ let _ = event_log.write(
367
+ "contract.tick.failed",
368
+ serde_json::json!({"error":error.to_string()}),
369
+ );
370
+ }
371
+ #[cfg(unix)]
372
+ let contract_only = crate::contract_runtime::framework::only_contract(&state);
373
+ #[cfg(not(unix))]
374
+ let contract_only = false;
362
375
  self.record_step(TickStepGroup::SessionGate, "tmux_session_gate");
363
376
  if let Some(session_name) = state
364
377
  .get("session_name")
@@ -367,7 +380,7 @@ impl Coordinator {
367
380
  .map(str::to_owned)
368
381
  {
369
382
  let session = crate::transport::SessionName::new(&session_name);
370
- if !self.transport.has_session(&session)? {
383
+ if !contract_only && !self.transport.has_session(&session)? {
371
384
  event_log.write(
372
385
  "coordinator.session_missing",
373
386
  serde_json::json!({"session": session_name}),
@@ -564,13 +577,14 @@ impl Coordinator {
564
577
  // Keep the sampled topology guard, but commit only this tick's
565
578
  // observations onto latest so task/note writers cannot be lost.
566
579
  let team_key = crate::state::projection::team_state_key(&state);
567
- crate::state::repository::StateRepository::new(self.workspace.as_path()).commit_observations(
568
- crate::state::repository::StateWriteIntent::CoordinatorTick {
569
- team_key: team_key.as_str(),
570
- },
571
- &before_observation,
572
- &state,
573
- )
580
+ crate::state::repository::StateRepository::new(self.workspace.as_path())
581
+ .commit_observations(
582
+ crate::state::repository::StateWriteIntent::CoordinatorTick {
583
+ team_key: team_key.as_str(),
584
+ },
585
+ &before_observation,
586
+ &state,
587
+ )
574
588
  }
575
589
  };
576
590
  if saved.is_err() {
@@ -1876,6 +1890,10 @@ fn tick_has_work_obligation(store: &crate::message_store::MessageStore) -> bool
1876
1890
  }
1877
1891
 
1878
1892
  fn agent_probe_base_eligible(agent: &Value) -> bool {
1893
+ #[cfg(unix)]
1894
+ if crate::contract_runtime::framework::is_contract(agent) {
1895
+ return false;
1896
+ }
1879
1897
  let status = agent.get("status").and_then(Value::as_str);
1880
1898
  !matches!(
1881
1899
  status,
@@ -81,6 +81,7 @@ pub mod incident;
81
81
  pub mod lease;
82
82
  pub mod owner_bind;
83
83
  pub mod provider_attribution;
84
+ pub mod prompt;
84
85
  pub mod rediscover;
85
86
  pub mod registry;
86
87
  pub mod start;