@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.
- package/Cargo.lock +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +57 -22
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +157 -8
- package/crates/team-agent/src/cli/models.rs +119 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +959 -0
- package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
- package/crates/team-agent/src/contract_runtime/config.rs +340 -0
- package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +383 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +294 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +16 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +269 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +197 -0
- package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +353 -21
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +571 -389
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +15 -0
- package/crates/team-agent/src/lifecycle/launch.rs +45 -28
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +34 -0
- package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- 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())
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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,
|