@team-agent/installer 0.5.50 → 0.5.52

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 (93) 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 +46 -115
  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/main_preserved.rs +1 -0
  25. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  26. package/crates/team-agent/src/cli/types.rs +1 -0
  27. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  29. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  30. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  31. package/crates/team-agent/src/db/message_store.rs +154 -44
  32. package/crates/team-agent/src/event_log.rs +73 -0
  33. package/crates/team-agent/src/leader/lease.rs +202 -16
  34. package/crates/team-agent/src/leader/start.rs +28 -4
  35. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  36. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
  37. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  38. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  39. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  41. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  42. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  43. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  44. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  45. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  46. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  48. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  49. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  50. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  52. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  53. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  54. package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
  55. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  56. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
  57. package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
  58. package/crates/team-agent/src/lifecycle/restart.rs +26 -2
  59. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
  60. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
  61. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  62. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  64. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  65. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  66. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  67. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  68. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  69. package/crates/team-agent/src/messaging/activity.rs +4 -2
  70. package/crates/team-agent/src/messaging/address.rs +86 -0
  71. package/crates/team-agent/src/messaging/delivery.rs +288 -183
  72. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  73. package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
  74. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  75. package/crates/team-agent/src/messaging/mod.rs +15 -2
  76. package/crates/team-agent/src/messaging/persist.rs +309 -0
  77. package/crates/team-agent/src/messaging/results.rs +16 -24
  78. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  79. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  80. package/crates/team-agent/src/messaging/send.rs +101 -49
  81. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
  84. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  85. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  86. package/crates/team-agent/src/redaction.rs +72 -2
  87. package/crates/team-agent/src/state/persist.rs +284 -21
  88. package/crates/team-agent/src/state/projection.rs +245 -30
  89. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  90. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  91. package/crates/team-agent/src/state/repository.rs +94 -27
  92. package/crates/team-agent/src/state/selector.rs +9 -18
  93. package/package.json +4 -4
@@ -0,0 +1,303 @@
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
+ crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
197
+ save_launched_team_state_for_key(
198
+ workspace,
199
+ &state,
200
+ Some(canonical_team_key),
201
+ Some(agent_id.as_str()),
202
+ )
203
+ }
204
+
205
+ pub(super) fn role_source_ownership(workspace: &Path, role_file: &Path) -> &'static str {
206
+ let managed_root = workspace.join(".team").join("dynamic-role-files");
207
+ match (
208
+ std::fs::canonicalize(&managed_root),
209
+ std::fs::canonicalize(role_file),
210
+ ) {
211
+ (Ok(root), Ok(path)) if path.starts_with(&root) => "managed",
212
+ _ => "external",
213
+ }
214
+ }
215
+
216
+ /// E5 Bug1:把 add-agent 就地编译出的 agent 条目注入 base team spec(`agents` 列表 +
217
+ /// `routing.rules` 加 `route-<id>`),复刻 [`compile_team`] 的路由规则形态。不落任何文件。
218
+ ///
219
+ /// 0.5.30 (`.team/artifacts/add-agent-restart-saveconflict-locate.md` §5.2):
220
+ /// `pub(crate)` 让 restart/rebuild.rs::rebuild_runtime_spec_from_roles 复用
221
+ /// 同一去重注入逻辑,把 add-agent 记录的 dynamic_role_file 合并回 restart
222
+ /// 重建 spec,防止 live helper 被 prune 后触发 SaveConflict。行为不变。
223
+ pub(crate) fn inject_agent_into_spec(
224
+ spec: &mut Value,
225
+ agent: Value,
226
+ agent_id: &str,
227
+ ) -> Result<(), LifecycleError> {
228
+ let Value::Map(pairs) = spec else {
229
+ return Err(LifecycleError::Compile("spec is not a map".to_string()));
230
+ };
231
+ // agents 列表追加。
232
+ match pairs.iter_mut().find(|(k, _)| k == "agents") {
233
+ Some((_, Value::List(agents))) => {
234
+ if !agents
235
+ .iter()
236
+ .any(|existing| yaml_agent_id(existing) == Some(agent_id))
237
+ {
238
+ agents.push(agent);
239
+ }
240
+ }
241
+ _ => {
242
+ return Err(LifecycleError::Compile(
243
+ "spec.agents missing or not a list".to_string(),
244
+ ))
245
+ }
246
+ }
247
+ // routing.rules 追加 route-<id>(与 compile_team 同形)。
248
+ if let Some((_, Value::Map(routing))) = pairs.iter_mut().find(|(k, _)| k == "routing") {
249
+ if let Some((_, Value::List(rules))) = routing.iter_mut().find(|(k, _)| k == "rules") {
250
+ if !rules
251
+ .iter()
252
+ .any(|rule| yaml_route_assigns_to(rule) == Some(agent_id))
253
+ {
254
+ rules.push(Value::Map(vec![
255
+ ("id".to_string(), Value::Str(format!("route-{agent_id}"))),
256
+ (
257
+ "match".to_string(),
258
+ Value::Map(vec![(
259
+ "assignee".to_string(),
260
+ Value::List(vec![Value::Str(agent_id.to_string())]),
261
+ )]),
262
+ ),
263
+ ("assign_to".to_string(), Value::Str(agent_id.to_string())),
264
+ ("priority".to_string(), Value::Int(10)),
265
+ ]));
266
+ }
267
+ }
268
+ }
269
+ Ok(())
270
+ }
271
+
272
+ pub(super) fn runtime_agent_exists(state: &serde_json::Value, agent_id: &AgentId) -> bool {
273
+ state
274
+ .get("agents")
275
+ .and_then(serde_json::Value::as_object)
276
+ .is_some_and(|agents| agents.contains_key(agent_id.as_str()))
277
+ }
278
+
279
+ pub(super) fn yaml_agent_id(agent: &Value) -> Option<&str> {
280
+ let Value::Map(pairs) = agent else {
281
+ return None;
282
+ };
283
+ pairs
284
+ .iter()
285
+ .find(|(key, _)| key == "id")
286
+ .and_then(|(_, value)| match value {
287
+ Value::Str(id) => Some(id.as_str()),
288
+ _ => None,
289
+ })
290
+ }
291
+
292
+ pub(super) fn yaml_route_assigns_to(rule: &Value) -> Option<&str> {
293
+ let Value::Map(pairs) = rule else {
294
+ return None;
295
+ };
296
+ pairs
297
+ .iter()
298
+ .find(|(key, _)| key == "assign_to")
299
+ .and_then(|(_, value)| match value {
300
+ Value::Str(id) => Some(id.as_str()),
301
+ _ => None,
302
+ })
303
+ }
@@ -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
+ }