@team-agent/installer 0.5.49 → 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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +6 -4
- package/crates/team-agent/src/cli/emit.rs +91 -39
- package/crates/team-agent/src/cli/mod.rs +33 -15
- package/crates/team-agent/src/cli/named_address.rs +82 -53
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +103 -1308
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +2 -2
- package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +26 -15
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +11 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +133 -58
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::*;
|
|
6
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
7
|
+
use crate::model::ids::AgentId;
|
|
8
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
9
|
+
use crate::model::yaml::{self, Value};
|
|
10
|
+
use crate::state::persist::load_runtime_state;
|
|
11
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
12
|
+
|
|
13
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
/// E42 (0.3.24 P0, double-spec deadlock): best-effort atomic rollback for a
|
|
18
|
+
/// failed add-agent. Restores the canonical spec to its pre-write bytes (or
|
|
19
|
+
/// removes the file if it didn't exist), and restores runtime state to its
|
|
20
|
+
/// pre-write JSON (so the half-written `status:starting` row is gone). The
|
|
21
|
+
/// caller propagates the ORIGINAL operation error after rollback; rollback
|
|
22
|
+
/// errors are swallowed (best-effort, no panic).
|
|
23
|
+
pub(super) fn rollback_add_agent_atomic(
|
|
24
|
+
run_workspace: &Path,
|
|
25
|
+
spec_path: &Path,
|
|
26
|
+
pre_spec_text: Option<&str>,
|
|
27
|
+
pre_runtime_state: Option<&serde_json::Value>,
|
|
28
|
+
agent_id: &AgentId,
|
|
29
|
+
reason: &str,
|
|
30
|
+
) {
|
|
31
|
+
let spec_restored = if let Some(text) = pre_spec_text {
|
|
32
|
+
std::fs::write(spec_path, text).is_ok()
|
|
33
|
+
} else {
|
|
34
|
+
std::fs::remove_file(spec_path)
|
|
35
|
+
.or_else(|error| {
|
|
36
|
+
if error.kind() == std::io::ErrorKind::NotFound {
|
|
37
|
+
Ok(())
|
|
38
|
+
} else {
|
|
39
|
+
Err(error)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
.is_ok()
|
|
43
|
+
};
|
|
44
|
+
// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.4):
|
|
45
|
+
// rollback must tombstone the newly-added agent so the persist merge
|
|
46
|
+
// does not re-attach a `roster_stub` from the latest on disk. Without
|
|
47
|
+
// the tombstone the half-added `agents.standards` / `teams.<key>.agents.standards`
|
|
48
|
+
// survives the restore-from-pre_state pass and the retry sees
|
|
49
|
+
// "agent id already exists".
|
|
50
|
+
let state_restored = if let Some(state) = pre_runtime_state {
|
|
51
|
+
crate::state::repository::StateRepository::new(run_workspace)
|
|
52
|
+
.save(
|
|
53
|
+
crate::state::repository::StateWriteIntent::AgentRollback {
|
|
54
|
+
team_key: None,
|
|
55
|
+
agent_id: agent_id.as_str(),
|
|
56
|
+
},
|
|
57
|
+
state,
|
|
58
|
+
)
|
|
59
|
+
.is_ok()
|
|
60
|
+
} else {
|
|
61
|
+
// No prior runtime state — drop just the agent we added (load → strip → save).
|
|
62
|
+
if let Ok(mut state) = crate::state::persist::load_runtime_state(run_workspace) {
|
|
63
|
+
if let Some(agents) = state
|
|
64
|
+
.get_mut("agents")
|
|
65
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
66
|
+
{
|
|
67
|
+
agents.remove(agent_id.as_str());
|
|
68
|
+
}
|
|
69
|
+
if let Some(teams) = state
|
|
70
|
+
.get_mut("teams")
|
|
71
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
72
|
+
{
|
|
73
|
+
for team in teams.values_mut() {
|
|
74
|
+
if let Some(agents) = team
|
|
75
|
+
.get_mut("agents")
|
|
76
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
77
|
+
{
|
|
78
|
+
agents.remove(agent_id.as_str());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
crate::state::repository::StateRepository::new(run_workspace)
|
|
83
|
+
.save(
|
|
84
|
+
crate::state::repository::StateWriteIntent::AgentRollback {
|
|
85
|
+
team_key: None,
|
|
86
|
+
agent_id: agent_id.as_str(),
|
|
87
|
+
},
|
|
88
|
+
&state,
|
|
89
|
+
)
|
|
90
|
+
.is_ok()
|
|
91
|
+
} else {
|
|
92
|
+
false
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
let rollback_ok = spec_restored && state_restored;
|
|
96
|
+
let _ = crate::event_log::EventLog::new(run_workspace).write(
|
|
97
|
+
"add_agent.rollback",
|
|
98
|
+
serde_json::json!({
|
|
99
|
+
"agent_id": agent_id.as_str(),
|
|
100
|
+
"reason": reason,
|
|
101
|
+
"rollback_ok": rollback_ok,
|
|
102
|
+
"spec_restored": spec_restored,
|
|
103
|
+
"state_restored": state_restored,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub(super) fn upsert_agent_state_from_role(
|
|
109
|
+
workspace: &Path,
|
|
110
|
+
canonical_team_key: &str,
|
|
111
|
+
agent_id: &AgentId,
|
|
112
|
+
meta: &Value,
|
|
113
|
+
dynamic_role_file: &Path,
|
|
114
|
+
safety: &DangerousApproval,
|
|
115
|
+
) -> Result<(), LifecycleError> {
|
|
116
|
+
let mut state =
|
|
117
|
+
crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
118
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
119
|
+
if !state.is_object() {
|
|
120
|
+
state = serde_json::json!({});
|
|
121
|
+
}
|
|
122
|
+
let Some(root) = state.as_object_mut() else {
|
|
123
|
+
return Err(LifecycleError::StatePersist(
|
|
124
|
+
"runtime state root is not an object".to_string(),
|
|
125
|
+
));
|
|
126
|
+
};
|
|
127
|
+
let agents = root
|
|
128
|
+
.entry("agents".to_string())
|
|
129
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
130
|
+
if !agents.is_object() {
|
|
131
|
+
*agents = serde_json::json!({});
|
|
132
|
+
}
|
|
133
|
+
let Some(agent_map) = agents.as_object_mut() else {
|
|
134
|
+
return Err(LifecycleError::StatePersist(
|
|
135
|
+
"runtime state agents is not an object".to_string(),
|
|
136
|
+
));
|
|
137
|
+
};
|
|
138
|
+
let provider = meta
|
|
139
|
+
.get("provider")
|
|
140
|
+
.and_then(Value::as_str)
|
|
141
|
+
.unwrap_or("codex");
|
|
142
|
+
let auth_mode = meta
|
|
143
|
+
.get("auth_mode")
|
|
144
|
+
.and_then(Value::as_str)
|
|
145
|
+
.unwrap_or("subscription");
|
|
146
|
+
let role = meta
|
|
147
|
+
.get("role")
|
|
148
|
+
.and_then(Value::as_str)
|
|
149
|
+
.unwrap_or_else(|| agent_id.as_str());
|
|
150
|
+
// E42 (0.3.24 P0, double-spec deadlock): persist the initial state row as
|
|
151
|
+
// "starting" (not "running"). The caller (add_agent_with_transport_at_paths)
|
|
152
|
+
// promotes to "running" only after start_agent_at_paths returns Running.
|
|
153
|
+
// If the spawn fails, the rollback below removes the entry entirely.
|
|
154
|
+
let mut entry = serde_json::json!({
|
|
155
|
+
"provider": provider,
|
|
156
|
+
"auth_mode": auth_mode,
|
|
157
|
+
"role": role,
|
|
158
|
+
"status": "starting",
|
|
159
|
+
"dynamic_role_file": dynamic_role_file.to_string_lossy().to_string(),
|
|
160
|
+
"role_source_ownership": role_source_ownership(workspace, dynamic_role_file),
|
|
161
|
+
});
|
|
162
|
+
if let Some(model) = meta.get("model").and_then(Value::as_str) {
|
|
163
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
164
|
+
obj.insert("model".to_string(), serde_json::json!(model));
|
|
165
|
+
obj.insert("model_source".to_string(), serde_json::json!("role"));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if let Some(profile) = meta.get("profile").and_then(Value::as_str) {
|
|
169
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
170
|
+
obj.insert("profile".to_string(), serde_json::json!(profile));
|
|
171
|
+
if let Some(team_dir) = dynamic_role_file.parent().and_then(Path::parent) {
|
|
172
|
+
obj.insert(
|
|
173
|
+
"_profile_dir".to_string(),
|
|
174
|
+
serde_json::json!(team_dir.join("profiles").to_string_lossy().to_string()),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if !obj.contains_key("model_source") {
|
|
178
|
+
obj.insert("model_source".to_string(), serde_json::json!("default"));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// 0.4.x provider effort MVP step 8 (dynamic add-agent): persist effort
|
|
183
|
+
// from the role doc front matter (compiler.rs validates syntax/semantics
|
|
184
|
+
// at compile; add-agent path validates here too in case of direct YAML).
|
|
185
|
+
if let Some(effort_str) = meta.get("effort").and_then(Value::as_str) {
|
|
186
|
+
if !effort_str.is_empty() {
|
|
187
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
188
|
+
obj.insert("effort".to_string(), serde_json::json!(effort_str));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
193
|
+
persist_effective_approval_policy(obj, safety);
|
|
194
|
+
}
|
|
195
|
+
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
196
|
+
save_launched_team_state_for_key(workspace, &state, Some(canonical_team_key))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pub(super) fn role_source_ownership(workspace: &Path, role_file: &Path) -> &'static str {
|
|
200
|
+
let managed_root = workspace.join(".team").join("dynamic-role-files");
|
|
201
|
+
match (
|
|
202
|
+
std::fs::canonicalize(&managed_root),
|
|
203
|
+
std::fs::canonicalize(role_file),
|
|
204
|
+
) {
|
|
205
|
+
(Ok(root), Ok(path)) if path.starts_with(&root) => "managed",
|
|
206
|
+
_ => "external",
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// E5 Bug1:把 add-agent 就地编译出的 agent 条目注入 base team spec(`agents` 列表 +
|
|
211
|
+
/// `routing.rules` 加 `route-<id>`),复刻 [`compile_team`] 的路由规则形态。不落任何文件。
|
|
212
|
+
///
|
|
213
|
+
/// 0.5.30 (`.team/artifacts/add-agent-restart-saveconflict-locate.md` §5.2):
|
|
214
|
+
/// `pub(crate)` 让 restart/rebuild.rs::rebuild_runtime_spec_from_roles 复用
|
|
215
|
+
/// 同一去重注入逻辑,把 add-agent 记录的 dynamic_role_file 合并回 restart
|
|
216
|
+
/// 重建 spec,防止 live helper 被 prune 后触发 SaveConflict。行为不变。
|
|
217
|
+
pub(crate) fn inject_agent_into_spec(
|
|
218
|
+
spec: &mut Value,
|
|
219
|
+
agent: Value,
|
|
220
|
+
agent_id: &str,
|
|
221
|
+
) -> Result<(), LifecycleError> {
|
|
222
|
+
let Value::Map(pairs) = spec else {
|
|
223
|
+
return Err(LifecycleError::Compile("spec is not a map".to_string()));
|
|
224
|
+
};
|
|
225
|
+
// agents 列表追加。
|
|
226
|
+
match pairs.iter_mut().find(|(k, _)| k == "agents") {
|
|
227
|
+
Some((_, Value::List(agents))) => {
|
|
228
|
+
if !agents
|
|
229
|
+
.iter()
|
|
230
|
+
.any(|existing| yaml_agent_id(existing) == Some(agent_id))
|
|
231
|
+
{
|
|
232
|
+
agents.push(agent);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
_ => {
|
|
236
|
+
return Err(LifecycleError::Compile(
|
|
237
|
+
"spec.agents missing or not a list".to_string(),
|
|
238
|
+
))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// routing.rules 追加 route-<id>(与 compile_team 同形)。
|
|
242
|
+
if let Some((_, Value::Map(routing))) = pairs.iter_mut().find(|(k, _)| k == "routing") {
|
|
243
|
+
if let Some((_, Value::List(rules))) = routing.iter_mut().find(|(k, _)| k == "rules") {
|
|
244
|
+
if !rules
|
|
245
|
+
.iter()
|
|
246
|
+
.any(|rule| yaml_route_assigns_to(rule) == Some(agent_id))
|
|
247
|
+
{
|
|
248
|
+
rules.push(Value::Map(vec![
|
|
249
|
+
("id".to_string(), Value::Str(format!("route-{agent_id}"))),
|
|
250
|
+
(
|
|
251
|
+
"match".to_string(),
|
|
252
|
+
Value::Map(vec![(
|
|
253
|
+
"assignee".to_string(),
|
|
254
|
+
Value::List(vec![Value::Str(agent_id.to_string())]),
|
|
255
|
+
)]),
|
|
256
|
+
),
|
|
257
|
+
("assign_to".to_string(), Value::Str(agent_id.to_string())),
|
|
258
|
+
("priority".to_string(), Value::Int(10)),
|
|
259
|
+
]));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
Ok(())
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
pub(super) fn runtime_agent_exists(state: &serde_json::Value, agent_id: &AgentId) -> bool {
|
|
267
|
+
state
|
|
268
|
+
.get("agents")
|
|
269
|
+
.and_then(serde_json::Value::as_object)
|
|
270
|
+
.is_some_and(|agents| agents.contains_key(agent_id.as_str()))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
pub(super) fn yaml_agent_id(agent: &Value) -> Option<&str> {
|
|
274
|
+
let Value::Map(pairs) = agent else {
|
|
275
|
+
return None;
|
|
276
|
+
};
|
|
277
|
+
pairs
|
|
278
|
+
.iter()
|
|
279
|
+
.find(|(key, _)| key == "id")
|
|
280
|
+
.and_then(|(_, value)| match value {
|
|
281
|
+
Value::Str(id) => Some(id.as_str()),
|
|
282
|
+
_ => None,
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pub(super) fn yaml_route_assigns_to(rule: &Value) -> Option<&str> {
|
|
287
|
+
let Value::Map(pairs) = rule else {
|
|
288
|
+
return None;
|
|
289
|
+
};
|
|
290
|
+
pairs
|
|
291
|
+
.iter()
|
|
292
|
+
.find(|(key, _)| key == "assign_to")
|
|
293
|
+
.and_then(|(_, value)| match value {
|
|
294
|
+
Value::Str(id) => Some(id.as_str()),
|
|
295
|
+
_ => None,
|
|
296
|
+
})
|
|
297
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::*;
|
|
6
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
7
|
+
use crate::model::ids::AgentId;
|
|
8
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
9
|
+
use crate::model::yaml::{self, Value};
|
|
10
|
+
use crate::state::persist::load_runtime_state;
|
|
11
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
12
|
+
|
|
13
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
pub(super) fn running_agent_state(
|
|
18
|
+
agent: &Value,
|
|
19
|
+
id: &str,
|
|
20
|
+
provider: Provider,
|
|
21
|
+
workspace: &Path,
|
|
22
|
+
spawn_cwd: &Path,
|
|
23
|
+
spawned_at: &str,
|
|
24
|
+
team_id: &str,
|
|
25
|
+
pane_id: Option<&str>,
|
|
26
|
+
pane_pid: Option<u32>,
|
|
27
|
+
safety: &DangerousApproval,
|
|
28
|
+
started_agent: Option<&StartedAgent>,
|
|
29
|
+
profile_dir: Option<&Path>,
|
|
30
|
+
) -> Result<serde_json::Value, LifecycleError> {
|
|
31
|
+
let model = agent.get("model").and_then(Value::as_str);
|
|
32
|
+
let auth_mode = agent
|
|
33
|
+
.get("auth_mode")
|
|
34
|
+
.and_then(Value::as_str)
|
|
35
|
+
.and_then(parse_auth_mode)
|
|
36
|
+
.unwrap_or(AuthMode::Subscription);
|
|
37
|
+
let profile = agent
|
|
38
|
+
.get("profile")
|
|
39
|
+
.map(yaml_value_to_json)
|
|
40
|
+
.unwrap_or(serde_json::Value::Null);
|
|
41
|
+
let window = started_agent
|
|
42
|
+
.and_then(|started| started.layout_window.as_ref())
|
|
43
|
+
.map(WindowName::as_str)
|
|
44
|
+
.or_else(|| agent.get("window").and_then(Value::as_str))
|
|
45
|
+
.unwrap_or(id);
|
|
46
|
+
let mcp_config = crate::provider::get_adapter(provider)
|
|
47
|
+
.mcp_config(auth_mode)
|
|
48
|
+
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
49
|
+
let mcp_config = resolve_mcp_config(mcp_config, workspace, id, team_id);
|
|
50
|
+
let mcp_config_path =
|
|
51
|
+
write_worker_mcp_config_for_provider(workspace, id, &mcp_config, Some(provider))?;
|
|
52
|
+
let mut state = serde_json::Map::new();
|
|
53
|
+
state.insert("status".to_string(), serde_json::json!("running"));
|
|
54
|
+
state.insert("provider".to_string(), serde_json::json!(provider));
|
|
55
|
+
state.insert("agent_id".to_string(), serde_json::json!(id));
|
|
56
|
+
state.insert(
|
|
57
|
+
"model".to_string(),
|
|
58
|
+
model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)),
|
|
59
|
+
);
|
|
60
|
+
state.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
|
|
61
|
+
// 0.4.x provider effort MVP step 8: persist resolved effort so restart
|
|
62
|
+
// / resume reads the same value (no re-resolution from role/team).
|
|
63
|
+
if let Some(effort_str) = agent.get("effort").and_then(Value::as_str) {
|
|
64
|
+
if !effort_str.is_empty() {
|
|
65
|
+
state.insert("effort".to_string(), serde_json::json!(effort_str));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
state.insert("profile".to_string(), profile);
|
|
69
|
+
if agent.get("profile").is_some() {
|
|
70
|
+
if let Some(profile_dir) = profile_dir {
|
|
71
|
+
state.insert(
|
|
72
|
+
"_profile_dir".to_string(),
|
|
73
|
+
serde_json::json!(profile_dir.to_string_lossy().to_string()),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
state.insert("window".to_string(), serde_json::json!(window));
|
|
78
|
+
state.insert(
|
|
79
|
+
"mcp_config".to_string(),
|
|
80
|
+
serde_json::json!(mcp_config_path.to_string_lossy().to_string()),
|
|
81
|
+
);
|
|
82
|
+
state.insert(
|
|
83
|
+
"permissions".to_string(),
|
|
84
|
+
permissions_json(agent, id, provider)
|
|
85
|
+
.map_err(|e| LifecycleError::Compile(e.to_string()))?,
|
|
86
|
+
);
|
|
87
|
+
persist_effective_approval_policy(&mut state, safety);
|
|
88
|
+
state.insert("session_id".to_string(), serde_json::Value::Null);
|
|
89
|
+
state.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
90
|
+
state.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
91
|
+
state.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
92
|
+
state.insert(
|
|
93
|
+
"attribution_confidence".to_string(),
|
|
94
|
+
serde_json::Value::Null,
|
|
95
|
+
);
|
|
96
|
+
if let Some(started_agent) = started_agent {
|
|
97
|
+
persist_started_agent_plan_state(&mut state, started_agent);
|
|
98
|
+
if let Some(layout_window) = started_agent.layout_window.as_ref() {
|
|
99
|
+
state.insert(
|
|
100
|
+
"layout_window".to_string(),
|
|
101
|
+
serde_json::json!(layout_window.as_str()),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if let Some(layout_index) = started_agent.layout_index {
|
|
105
|
+
state.insert("layout_index".to_string(), serde_json::json!(layout_index));
|
|
106
|
+
}
|
|
107
|
+
if let Some(pane_index) = started_agent.pane_index {
|
|
108
|
+
state.insert("pane_index".to_string(), serde_json::json!(pane_index));
|
|
109
|
+
}
|
|
110
|
+
if !matches!(started_agent.display, WorkerDisplay::Blocked { .. }) {
|
|
111
|
+
state.insert(
|
|
112
|
+
"display".to_string(),
|
|
113
|
+
serde_json::to_value(&started_agent.display)
|
|
114
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
state.insert(
|
|
119
|
+
"spawn_cwd".to_string(),
|
|
120
|
+
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
121
|
+
);
|
|
122
|
+
state.insert("spawned_at".to_string(), serde_json::json!(spawned_at));
|
|
123
|
+
if let Some(pane_id) = pane_id.filter(|pane| !pane.is_empty()) {
|
|
124
|
+
state.insert("pane_id".to_string(), serde_json::json!(pane_id));
|
|
125
|
+
}
|
|
126
|
+
if let Some(pane_pid) = pane_pid {
|
|
127
|
+
state.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
128
|
+
}
|
|
129
|
+
Ok(serde_json::Value::Object(state))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
|
|
133
|
+
serde_json::json!({
|
|
134
|
+
"enabled": safety.enabled,
|
|
135
|
+
"source": dangerous_approval_source_str(safety.source),
|
|
136
|
+
"inherited": safety.inherited,
|
|
137
|
+
"explicit_yes_confirmed": safety.enabled && matches!(safety.source, DangerousApprovalSource::RuntimeConfig),
|
|
138
|
+
"provider": safety.provider,
|
|
139
|
+
"flag": safety.flag,
|
|
140
|
+
"worker_capability_above_leader": safety.worker_capability_above_leader,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pub(crate) fn persist_effective_approval_policy(
|
|
145
|
+
agent_state: &mut serde_json::Map<String, serde_json::Value>,
|
|
146
|
+
safety: &DangerousApproval,
|
|
147
|
+
) {
|
|
148
|
+
agent_state.insert(
|
|
149
|
+
"effective_approval_policy".to_string(),
|
|
150
|
+
effective_approval_policy(safety),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
|
|
155
|
+
match source {
|
|
156
|
+
DangerousApprovalSource::RuntimeConfig => "runtime_config",
|
|
157
|
+
DangerousApprovalSource::LeaderProcess => "leader_process",
|
|
158
|
+
DangerousApprovalSource::Disabled => "disabled",
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use crate::lifecycle::{DangerousApproval, DangerousApprovalSource, LifecycleError};
|
|
4
|
+
|
|
5
|
+
/// `detect_inherited_dangerous_permissions`(`launch/config.py`):扫进程祖先链找
|
|
6
|
+
/// `--dangerously-*` flag,产出危险审批继承态。launch 在 inherited=false 且无 --yes 时拒。
|
|
7
|
+
pub fn detect_dangerous_approval() -> Result<DangerousApproval, LifecycleError> {
|
|
8
|
+
if let Ok(raw) = std::env::var("TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON") {
|
|
9
|
+
let argv_tokens = serde_json::from_str::<Vec<String>>(&raw).map_err(|e| {
|
|
10
|
+
LifecycleError::StatePersist(format!("invalid test ancestry argv: {e}"))
|
|
11
|
+
})?;
|
|
12
|
+
return Ok(detect_dangerous_approval_in_argv(&argv_tokens)
|
|
13
|
+
.unwrap_or_else(disabled_dangerous_approval));
|
|
14
|
+
}
|
|
15
|
+
for argv_tokens in process_ancestry_argv(std::process::id()) {
|
|
16
|
+
if let Some(detected) = detect_dangerous_approval_in_argv(&argv_tokens) {
|
|
17
|
+
return Ok(detected);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
Ok(disabled_dangerous_approval())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub(super) fn disabled_dangerous_approval() -> DangerousApproval {
|
|
24
|
+
DangerousApproval {
|
|
25
|
+
enabled: false,
|
|
26
|
+
source: DangerousApprovalSource::Disabled,
|
|
27
|
+
inherited: false,
|
|
28
|
+
provider: None,
|
|
29
|
+
flag: None,
|
|
30
|
+
worker_capability_above_leader: false,
|
|
31
|
+
ancestry_binary_name: None,
|
|
32
|
+
unexpected_binary: false,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub(super) fn detect_dangerous_approval_in_argv(
|
|
37
|
+
argv_tokens: &[String],
|
|
38
|
+
) -> Option<DangerousApproval> {
|
|
39
|
+
let argv0 = argv_tokens.first().map(String::as_str).unwrap_or("");
|
|
40
|
+
let ancestry_binary_name = binary_name(argv0);
|
|
41
|
+
for token in argv_tokens {
|
|
42
|
+
for (provider, flag) in dangerous_leader_flags() {
|
|
43
|
+
if token == flag {
|
|
44
|
+
let unexpected_binary =
|
|
45
|
+
!binary_matches_provider(provider, ancestry_binary_name.as_deref());
|
|
46
|
+
return Some(DangerousApproval {
|
|
47
|
+
enabled: true,
|
|
48
|
+
source: DangerousApprovalSource::LeaderProcess,
|
|
49
|
+
inherited: true,
|
|
50
|
+
provider: Some((*provider).to_string()),
|
|
51
|
+
flag: Some((*flag).to_string()),
|
|
52
|
+
worker_capability_above_leader: false,
|
|
53
|
+
ancestry_binary_name,
|
|
54
|
+
unexpected_binary,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
None
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub(super) fn dangerous_leader_flags() -> &'static [(&'static str, &'static str)] {
|
|
63
|
+
// Process argv command grammar, not persisted provider identity parsing.
|
|
64
|
+
&[
|
|
65
|
+
("claude", "--dangerously-skip-permissions"),
|
|
66
|
+
("claude", "--dangerously-skip-permission"),
|
|
67
|
+
("codex", "--dangerously-bypass-approvals-and-sandbox"),
|
|
68
|
+
// 0.3.27 P1 (E54 symptom 2): copilot's full-permission flags. Without
|
|
69
|
+
// these entries, a copilot leader running with --allow-all / --yolo would
|
|
70
|
+
// NOT propagate bypass to claude/codex workers because
|
|
71
|
+
// detect_dangerous_approval scans the coordinator's ancestor argv and
|
|
72
|
+
// only matches entries in THIS table.
|
|
73
|
+
("copilot", "--allow-all"),
|
|
74
|
+
("copilot", "--yolo"),
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub(super) fn binary_matches_provider(provider: &str, binary: Option<&str>) -> bool {
|
|
79
|
+
// Binary-name command grammar, not provider identity parsing.
|
|
80
|
+
match (provider, binary) {
|
|
81
|
+
("codex", Some("codex")) => true,
|
|
82
|
+
("claude", Some("claude" | "claude-code" | "claude_code")) => true,
|
|
83
|
+
// 0.3.27 P1 (E54): copilot binary detection for dangerous-approval scan.
|
|
84
|
+
("copilot", Some("copilot" | "github-copilot" | "gh-copilot")) => true,
|
|
85
|
+
_ => false,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(super) fn binary_name(argv0: &str) -> Option<String> {
|
|
90
|
+
Path::new(argv0)
|
|
91
|
+
.file_name()
|
|
92
|
+
.and_then(|v| v.to_str())
|
|
93
|
+
.filter(|s| !s.is_empty())
|
|
94
|
+
.map(str::to_string)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pub(super) fn process_ancestry_argv(pid: u32) -> Vec<Vec<String>> {
|
|
98
|
+
let mut out = Vec::new();
|
|
99
|
+
let mut current = pid;
|
|
100
|
+
let mut seen = std::collections::BTreeSet::new();
|
|
101
|
+
for _ in 0..12 {
|
|
102
|
+
if current == 0 || !seen.insert(current) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if let Some(argv_tokens) = process_argv_tokens(current) {
|
|
106
|
+
out.push(argv_tokens);
|
|
107
|
+
}
|
|
108
|
+
let Some(parent) = process_parent_pid(current) else {
|
|
109
|
+
break;
|
|
110
|
+
};
|
|
111
|
+
if parent <= 1 || parent == current {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
current = parent;
|
|
115
|
+
}
|
|
116
|
+
out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 0.5.x Windows portability Batch 4: `process_argv_tokens` and
|
|
120
|
+
// `process_parent_pid` route through `crate::platform::argv`. Unix
|
|
121
|
+
// impls are byte-preserving (Linux `/proc/<pid>/cmdline`, macOS
|
|
122
|
+
// `sysctl(KERN_PROCARGS2)`, other Unix `ps -p ... -o command=`).
|
|
123
|
+
// Windows returns `None` per design §Batch 4 conservative anchor:
|
|
124
|
+
// unknown argv must never infer elevated approval; callers already
|
|
125
|
+
// treat `None` as "no elevation inherited" via
|
|
126
|
+
// `disabled_dangerous_approval()`.
|
|
127
|
+
|
|
128
|
+
pub(super) fn process_argv_tokens(pid: u32) -> Option<Vec<String>> {
|
|
129
|
+
crate::platform::argv::argv_tokens(pid)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
pub(super) fn process_parent_pid(pid: u32) -> Option<u32> {
|
|
133
|
+
crate::platform::argv::parent_pid(pid)
|
|
134
|
+
}
|