@team-agent/installer 0.5.100 → 0.5.101
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/diagnose.rs +29 -7
- package/crates/team-agent/src/cli/mod.rs +24 -8
- package/crates/team-agent/src/compiler.rs +3 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
- package/crates/team-agent/src/lifecycle/launch.rs +11 -3
- package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +114 -1
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +10 -2
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +2 -0
- package/crates/team-agent/src/state/persist.rs +58 -2
- package/crates/team-agent/src/state/projection.rs +5 -1
- package/crates/team-agent/src/state/repository.rs +125 -4
- package/crates/team-agent/src/transport/test_support.rs +23 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +17 -46
- package/skills/team-agent/references/team-agent-operator.md +4 -2
|
@@ -9,55 +9,118 @@
|
|
|
9
9
|
//! - 不读 provider session 落盘
|
|
10
10
|
//! maturity: wired
|
|
11
11
|
//! ---
|
|
12
|
+
use std::fs::OpenOptions;
|
|
13
|
+
use std::io::Write;
|
|
14
|
+
use std::os::unix::fs::MetadataExt;
|
|
12
15
|
use std::path::{Path, PathBuf};
|
|
16
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
13
17
|
|
|
14
18
|
use crate::lifecycle::LifecycleError;
|
|
15
19
|
use crate::model::ids::AgentId;
|
|
16
20
|
use crate::model::yaml::{self, Value};
|
|
17
21
|
|
|
18
|
-
fn set_yaml_map_value(
|
|
19
|
-
value: &mut Value,
|
|
20
|
-
key: &str,
|
|
21
|
-
next: Value,
|
|
22
|
-
) -> Result<(), LifecycleError> {
|
|
22
|
+
fn set_yaml_map_value(value: &mut Value, key: &str, next: Value) -> Result<(), LifecycleError> {
|
|
23
23
|
let Value::Map(pairs) = value else {
|
|
24
24
|
return Err(LifecycleError::Compile(
|
|
25
25
|
"agent entry is not a map".to_string(),
|
|
26
26
|
));
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
let mut found = false;
|
|
29
|
+
for (_, existing) in pairs
|
|
30
|
+
.iter_mut()
|
|
31
|
+
.filter(|(existing_key, _)| existing_key.as_str() == key)
|
|
32
|
+
{
|
|
33
|
+
*existing = next.clone();
|
|
34
|
+
found = true;
|
|
35
|
+
}
|
|
36
|
+
if !found {
|
|
31
37
|
pairs.push((key.to_string(), next));
|
|
32
38
|
}
|
|
33
39
|
Ok(())
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
fn dump_role_frontmatter(meta: &Value) -> String {
|
|
43
|
+
let Value::Map(pairs) = meta else {
|
|
44
|
+
return yaml::dumps(meta);
|
|
45
|
+
};
|
|
46
|
+
let mut rendered = String::new();
|
|
47
|
+
for (key, value) in pairs {
|
|
48
|
+
if value.as_str().is_some_and(is_plain_frontmatter_scalar) {
|
|
49
|
+
if let Some(value) = value.as_str() {
|
|
50
|
+
rendered.push_str(&format!("{key}: {value}\n"));
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
rendered.push_str(&yaml::dumps(&Value::Map(vec![(
|
|
54
|
+
key.clone(),
|
|
55
|
+
value.clone(),
|
|
56
|
+
)])));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
rendered
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn is_plain_frontmatter_scalar(value: &str) -> bool {
|
|
63
|
+
!value.is_empty()
|
|
64
|
+
&& value.trim() == value
|
|
65
|
+
&& value
|
|
66
|
+
.chars()
|
|
67
|
+
.any(|character| character.is_ascii_alphabetic() || character == '_')
|
|
68
|
+
&& value.chars().all(|character| {
|
|
69
|
+
character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ' ' | '/')
|
|
70
|
+
})
|
|
71
|
+
&& !matches!(
|
|
72
|
+
value.to_ascii_lowercase().as_str(),
|
|
73
|
+
"true" | "false" | "null" | "yes" | "no" | "on" | "off"
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn strip_label_quotes(mut label: &str) -> &str {
|
|
78
|
+
loop {
|
|
79
|
+
let bytes = label.as_bytes();
|
|
80
|
+
if bytes.len() >= 2
|
|
81
|
+
&& matches!(bytes[0], b'\'' | b'"')
|
|
82
|
+
&& bytes[0] == bytes[bytes.len() - 1]
|
|
83
|
+
{
|
|
84
|
+
label = &label[1..label.len() - 1];
|
|
85
|
+
} else {
|
|
86
|
+
return label;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub(crate) struct MaterializedRole {
|
|
37
92
|
path: PathBuf,
|
|
93
|
+
device: u64,
|
|
94
|
+
inode: u64,
|
|
38
95
|
keep: bool,
|
|
39
96
|
}
|
|
40
97
|
|
|
41
98
|
impl MaterializedRole {
|
|
42
|
-
/// ---
|
|
43
|
-
/// purpose: 取物化出来的角色文件路径
|
|
44
|
-
/// returns: 落盘路径
|
|
45
|
-
/// ---
|
|
46
|
-
pub(
|
|
99
|
+
/// ---
|
|
100
|
+
/// purpose: 取物化出来的角色文件路径
|
|
101
|
+
/// returns: 落盘路径
|
|
102
|
+
/// ---
|
|
103
|
+
pub(crate) fn path(&self) -> &Path {
|
|
47
104
|
&self.path
|
|
48
105
|
}
|
|
49
106
|
|
|
50
|
-
/// ---
|
|
51
|
-
/// purpose: 标记该文件由调用方接管,Drop 时不再删除
|
|
52
|
-
/// ---
|
|
53
|
-
pub(
|
|
107
|
+
/// ---
|
|
108
|
+
/// purpose: 标记该文件由调用方接管,Drop 时不再删除
|
|
109
|
+
/// ---
|
|
110
|
+
pub(crate) fn keep(&mut self) {
|
|
54
111
|
self.keep = true;
|
|
55
112
|
}
|
|
56
113
|
}
|
|
57
114
|
|
|
58
115
|
impl Drop for MaterializedRole {
|
|
59
116
|
fn drop(&mut self) {
|
|
60
|
-
if !self.keep
|
|
117
|
+
if !self.keep
|
|
118
|
+
&& std::fs::symlink_metadata(&self.path).is_ok_and(|metadata| {
|
|
119
|
+
metadata.file_type().is_file()
|
|
120
|
+
&& metadata.dev() == self.device
|
|
121
|
+
&& metadata.ino() == self.inode
|
|
122
|
+
})
|
|
123
|
+
{
|
|
61
124
|
let _ = std::fs::remove_file(&self.path);
|
|
62
125
|
}
|
|
63
126
|
}
|
|
@@ -69,16 +132,18 @@ impl Drop for MaterializedRole {
|
|
|
69
132
|
/// state: 用于找源席的 dynamic_role_file,找不到时退到 team 目录下的同名 md
|
|
70
133
|
/// as_agent_id: 新席位名,写进 front matter 的 name
|
|
71
134
|
/// label: 非空时覆盖 front matter 的 role
|
|
135
|
+
/// agent_label: 非空时写入 front matter 的 label
|
|
72
136
|
/// returns: 物化结果,未调用 keep 时 Drop 会删掉该文件
|
|
73
137
|
/// errors: 源文件缺失、未声明 name 或声明与源席不符时返回 Compile;目标已存在返回 RequirementUnmet;建目录或写盘失败返回 StatePersist
|
|
74
138
|
/// ---
|
|
75
|
-
pub(
|
|
139
|
+
pub(crate) fn materialize_latest_role(
|
|
76
140
|
run_workspace: &Path,
|
|
77
141
|
team_dir: &Path,
|
|
78
142
|
state: &serde_json::Value,
|
|
79
143
|
source_agent_id: &AgentId,
|
|
80
144
|
as_agent_id: &AgentId,
|
|
81
145
|
label: Option<&str>,
|
|
146
|
+
agent_label: Option<&str>,
|
|
82
147
|
) -> Result<MaterializedRole, LifecycleError> {
|
|
83
148
|
let source_path = resolve_role_source(run_workspace, team_dir, state, source_agent_id)?;
|
|
84
149
|
let (mut meta, body) = crate::compiler::read_front_matter(&source_path)
|
|
@@ -104,32 +169,103 @@ pub(super) fn materialize_latest_role(
|
|
|
104
169
|
"name",
|
|
105
170
|
Value::Str(as_agent_id.as_str().to_string()),
|
|
106
171
|
)?;
|
|
107
|
-
if let Some(label) = label
|
|
172
|
+
if let Some(label) = label
|
|
173
|
+
.map(strip_label_quotes)
|
|
174
|
+
.filter(|value| !value.is_empty())
|
|
175
|
+
{
|
|
108
176
|
set_yaml_map_value(&mut meta, "role", Value::Str(label.to_string()))?;
|
|
109
177
|
}
|
|
178
|
+
if let Some(agent_label) = agent_label
|
|
179
|
+
.map(strip_label_quotes)
|
|
180
|
+
.filter(|value| !value.is_empty())
|
|
181
|
+
{
|
|
182
|
+
set_yaml_map_value(&mut meta, "label", Value::Str(agent_label.to_string()))?;
|
|
183
|
+
}
|
|
184
|
+
if let Some(role) = meta.get("role").and_then(Value::as_str).map(str::to_string) {
|
|
185
|
+
let role_without_quotes = strip_label_quotes(&role);
|
|
186
|
+
if !role_without_quotes.is_empty() && role_without_quotes != role {
|
|
187
|
+
set_yaml_map_value(
|
|
188
|
+
&mut meta,
|
|
189
|
+
"role",
|
|
190
|
+
Value::Str(role_without_quotes.to_string()),
|
|
191
|
+
)?;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
110
194
|
|
|
111
195
|
let managed_dir = run_workspace.join(".team").join("dynamic-role-files");
|
|
112
196
|
std::fs::create_dir_all(&managed_dir)
|
|
113
197
|
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
114
198
|
let path = managed_dir.join(format!("{}.md", as_agent_id.as_str()));
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
199
|
+
match std::fs::symlink_metadata(&path) {
|
|
200
|
+
Ok(_) => {
|
|
201
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
202
|
+
"managed role file already exists: {}",
|
|
203
|
+
path.display()
|
|
204
|
+
)))
|
|
205
|
+
}
|
|
206
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
|
207
|
+
Err(error) => return Err(LifecycleError::StatePersist(error.to_string())),
|
|
120
208
|
}
|
|
121
|
-
let rendered = format!("---\n{}---\n\n{}",
|
|
122
|
-
let
|
|
123
|
-
|
|
209
|
+
let rendered = format!("---\n{}---\n\n{}", dump_role_frontmatter(&meta), body);
|
|
210
|
+
let nonce = SystemTime::now()
|
|
211
|
+
.duration_since(UNIX_EPOCH)
|
|
212
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?
|
|
213
|
+
.as_nanos();
|
|
214
|
+
let temp = managed_dir.join(format!(
|
|
215
|
+
".{}.md.tmp-{}-{nonce}",
|
|
216
|
+
as_agent_id.as_str(),
|
|
217
|
+
std::process::id()
|
|
218
|
+
));
|
|
219
|
+
let mut file = OpenOptions::new()
|
|
220
|
+
.write(true)
|
|
221
|
+
.create_new(true)
|
|
222
|
+
.open(&temp)
|
|
124
223
|
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
125
|
-
if let Err(error) =
|
|
224
|
+
if let Err(error) = file
|
|
225
|
+
.write_all(rendered.as_bytes())
|
|
226
|
+
.and_then(|_| file.sync_all())
|
|
227
|
+
{
|
|
228
|
+
drop(file);
|
|
229
|
+
let _ = std::fs::remove_file(&temp);
|
|
230
|
+
return Err(LifecycleError::StatePersist(error.to_string()));
|
|
231
|
+
}
|
|
232
|
+
let metadata = match file.metadata() {
|
|
233
|
+
Ok(metadata) => metadata,
|
|
234
|
+
Err(error) => {
|
|
235
|
+
drop(file);
|
|
236
|
+
let _ = std::fs::remove_file(&temp);
|
|
237
|
+
return Err(LifecycleError::StatePersist(error.to_string()));
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
let (device, inode) = (metadata.dev(), metadata.ino());
|
|
241
|
+
drop(file);
|
|
242
|
+
if let Err(error) = std::fs::hard_link(&temp, &path) {
|
|
126
243
|
let _ = std::fs::remove_file(&temp);
|
|
244
|
+
if error.kind() == std::io::ErrorKind::AlreadyExists {
|
|
245
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
246
|
+
"managed role file already exists: {}",
|
|
247
|
+
path.display()
|
|
248
|
+
)));
|
|
249
|
+
}
|
|
250
|
+
return Err(LifecycleError::StatePersist(error.to_string()));
|
|
251
|
+
}
|
|
252
|
+
if let Err(error) = std::fs::remove_file(&temp) {
|
|
253
|
+
if std::fs::symlink_metadata(&path).is_ok_and(|metadata| {
|
|
254
|
+
metadata.file_type().is_file() && metadata.dev() == device && metadata.ino() == inode
|
|
255
|
+
}) {
|
|
256
|
+
let _ = std::fs::remove_file(&path);
|
|
257
|
+
}
|
|
127
258
|
return Err(LifecycleError::StatePersist(error.to_string()));
|
|
128
259
|
}
|
|
129
|
-
Ok(MaterializedRole {
|
|
260
|
+
Ok(MaterializedRole {
|
|
261
|
+
path,
|
|
262
|
+
device,
|
|
263
|
+
inode,
|
|
264
|
+
keep: false,
|
|
265
|
+
})
|
|
130
266
|
}
|
|
131
267
|
|
|
132
|
-
fn resolve_role_source(
|
|
268
|
+
pub(crate) fn resolve_role_source(
|
|
133
269
|
run_workspace: &Path,
|
|
134
270
|
team_dir: &Path,
|
|
135
271
|
state: &serde_json::Value,
|
|
@@ -23,6 +23,15 @@ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
|
23
23
|
|
|
24
24
|
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
25
25
|
|
|
26
|
+
#[derive(Clone, Debug)]
|
|
27
|
+
pub(crate) struct ForkCaptureSeed {
|
|
28
|
+
pub source_agent_id: AgentId,
|
|
29
|
+
pub captured: crate::provider::CapturedSession,
|
|
30
|
+
pub captured_at: String,
|
|
31
|
+
pub pi_sessions_root: PathBuf,
|
|
32
|
+
pub profile_dir: PathBuf,
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
use super::*;
|
|
27
36
|
|
|
28
37
|
// ── lifecycle::launch —— 冷启 / quick-start / 危险审批探测 ──────────────────
|
|
@@ -395,8 +404,7 @@ pub(super) use add_agent::*;
|
|
|
395
404
|
pub use add_agent::{add_agent, add_agent_force};
|
|
396
405
|
pub(crate) use add_agent::{add_agent_with_transport, add_agent_with_transport_force};
|
|
397
406
|
|
|
398
|
-
mod add_agent_state;
|
|
399
|
-
pub(crate) use add_agent_state::inject_agent_into_spec;
|
|
407
|
+
pub(crate) mod add_agent_state;
|
|
400
408
|
pub(super) use add_agent_state::*;
|
|
401
409
|
|
|
402
410
|
mod fork_agent;
|
|
@@ -406,7 +414,7 @@ pub use fork_agent::{fork_agent_with_transport, in_window_fork, in_window_fork_c
|
|
|
406
414
|
mod fork_entry;
|
|
407
415
|
pub use fork_entry::fork_agent;
|
|
408
416
|
|
|
409
|
-
mod role_source;
|
|
417
|
+
pub(crate) mod role_source;
|
|
410
418
|
pub(super) use role_source::*;
|
|
411
419
|
|
|
412
420
|
mod clone_agent;
|
|
@@ -217,7 +217,13 @@ fn prepare_profile_launch(
|
|
|
217
217
|
let Some(profile) = agent.profile.as_deref() else {
|
|
218
218
|
return Ok(ProviderProfileLaunch::default());
|
|
219
219
|
};
|
|
220
|
-
let
|
|
220
|
+
let Some(path) = find_profile_file(workspace, profile, agent.profile_dir.as_deref()) else {
|
|
221
|
+
if agent.provider == Provider::Pi && agent.auth_mode == AuthMode::Subscription {
|
|
222
|
+
return Ok(ProviderProfileLaunch::default());
|
|
223
|
+
}
|
|
224
|
+
return Err(profile_not_found(profile));
|
|
225
|
+
};
|
|
226
|
+
let loaded = read_profile_file(path)?;
|
|
221
227
|
validate_profile(&agent, profile, &loaded)?;
|
|
222
228
|
|
|
223
229
|
let mut env_overlay = provider_env_exports(agent.provider, agent.auth_mode, &loaded.values);
|
|
@@ -290,9 +296,14 @@ pub(crate) fn load_profile(
|
|
|
290
296
|
name: &str,
|
|
291
297
|
profile_dir: Option<&Path>,
|
|
292
298
|
) -> Result<ProfileValues, LifecycleError> {
|
|
299
|
+
let path =
|
|
300
|
+
find_profile_file(workspace, name, profile_dir).ok_or_else(|| profile_not_found(name))?;
|
|
301
|
+
read_profile_file(path)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fn find_profile_file(workspace: &Path, name: &str, profile_dir: Option<&Path>) -> Option<PathBuf> {
|
|
293
305
|
let dirs = profile_lookup_dirs(workspace, profile_dir);
|
|
294
|
-
|
|
295
|
-
.iter()
|
|
306
|
+
dirs.iter()
|
|
296
307
|
.map(|directory| directory.join(format!("{name}.env")))
|
|
297
308
|
.find(|path| path.exists())
|
|
298
309
|
.or_else(|| {
|
|
@@ -300,11 +311,15 @@ pub(crate) fn load_profile(
|
|
|
300
311
|
.map(|directory| directory.join(format!("{name}.example.env")))
|
|
301
312
|
.find(|path| path.exists())
|
|
302
313
|
})
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
fn profile_not_found(name: &str) -> LifecycleError {
|
|
317
|
+
LifecycleError::RequirementUnmet(format!(
|
|
318
|
+
"profile {name} not found; run team-agent profile init {name} --auth-mode subscription"
|
|
319
|
+
))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fn read_profile_file(path: PathBuf) -> Result<ProfileValues, LifecycleError> {
|
|
308
323
|
let text = std::fs::read_to_string(&path)
|
|
309
324
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", path.display())))?;
|
|
310
325
|
Ok(ProfileValues {
|
|
@@ -546,7 +546,7 @@ fn replay_worker_target_missing_messages(
|
|
|
546
546
|
Ok(())
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
fn verify_spawned_pane_matches_target(
|
|
549
|
+
pub(super) fn verify_spawned_pane_matches_target(
|
|
550
550
|
transport: &dyn crate::transport::Transport,
|
|
551
551
|
pane: &crate::transport::PaneId,
|
|
552
552
|
session: &crate::transport::SessionName,
|
|
@@ -1244,7 +1244,7 @@ pub(super) fn resolve_team_scoped_state_or_refuse(
|
|
|
1244
1244
|
})
|
|
1245
1245
|
}
|
|
1246
1246
|
|
|
1247
|
-
fn mark_agent_started(
|
|
1247
|
+
pub(super) fn mark_agent_started(
|
|
1248
1248
|
state: &mut serde_json::Value,
|
|
1249
1249
|
agent_id: &AgentId,
|
|
1250
1250
|
window: &str,
|