@team-agent/installer 0.5.64 → 0.5.66
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/Cargo.toml +5 -0
- package/crates/team-agent/src/cli/adapters.rs +6 -1
- package/crates/team-agent/src/cli/emit.rs +15 -0
- package/crates/team-agent/src/cli/mod.rs +9 -8
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/tests/compile.rs +1 -1
- package/crates/team-agent/src/cli/tests/mod.rs +33 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
- package/crates/team-agent/src/cli/tests/status_send.rs +13 -3
- package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
- package/crates/team-agent/src/cli/types.rs +7 -0
- package/crates/team-agent/src/compiler/tests.rs +23 -20
- package/crates/team-agent/src/compiler.rs +25 -5
- package/crates/team-agent/src/conpty/backend.rs +16 -0
- package/crates/team-agent/src/coordinator/health.rs +17 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +55 -2
- package/crates/team-agent/src/db/migration.rs +2 -1
- package/crates/team-agent/src/db/schema.rs +16 -8
- package/crates/team-agent/src/leader/lease.rs +1 -2
- package/crates/team-agent/src/leader/start.rs +175 -10
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -2
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +7 -2
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +137 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -2
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +39 -2
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +105 -48
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -2
- package/crates/team-agent/src/lifecycle/launch.rs +117 -21
- package/crates/team-agent/src/lifecycle/restart/agent.rs +89 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +9 -12
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +59 -15
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +24 -32
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +13 -11
- package/crates/team-agent/src/lifecycle/tests/core.rs +0 -18
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +17 -30
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +11 -7
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests.rs +11 -7
- package/crates/team-agent/src/lifecycle/types.rs +0 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +115 -22
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +29 -0
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +12 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -0
- package/crates/team-agent/src/messaging/results.rs +5 -0
- package/crates/team-agent/src/messaging/send.rs +9 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +32 -0
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/wait.rs +366 -0
- package/crates/team-agent/src/messaging/watchers.rs +153 -6
- package/crates/team-agent/src/model/spec.rs +129 -6
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
- package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
- package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
- package/crates/team-agent/src/provider/bypass_flags.rs +80 -0
- package/crates/team-agent/src/provider/mod.rs +2 -0
- package/crates/team-agent/src/tmux_backend.rs +35 -12
- package/crates/team-agent/src/transport/test_support.rs +20 -0
- package/crates/team-agent/src/transport.rs +28 -11
- package/package.json +4 -4
- package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
|
@@ -24,7 +24,6 @@ pub(super) fn running_agent_state(
|
|
|
24
24
|
team_id: &str,
|
|
25
25
|
pane_id: Option<&str>,
|
|
26
26
|
pane_pid: Option<u32>,
|
|
27
|
-
safety: &DangerousApproval,
|
|
28
27
|
started_agent: Option<&StartedAgent>,
|
|
29
28
|
profile_dir: Option<&Path>,
|
|
30
29
|
) -> Result<serde_json::Value, LifecycleError> {
|
|
@@ -84,7 +83,16 @@ pub(super) fn running_agent_state(
|
|
|
84
83
|
permissions_json(agent, id, provider)
|
|
85
84
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?,
|
|
86
85
|
);
|
|
87
|
-
|
|
86
|
+
// 0.5.66 bypass 单源:policy 从 agent 字段派生,不再用全队 `DangerousApproval`。
|
|
87
|
+
persist_effective_approval_policy_from_yaml_agent(&mut state, agent, provider);
|
|
88
|
+
// 0.5.66 bypass 单源 §2.6:fresh spawn 记录运行面 bypass 值(供 restart drift 挡板)。
|
|
89
|
+
state.insert(
|
|
90
|
+
"as_launched_dangerously_skip_permissions".to_string(),
|
|
91
|
+
serde_json::json!(matches!(
|
|
92
|
+
agent.get("dangerously_skip_permissions"),
|
|
93
|
+
Some(Value::Bool(true))
|
|
94
|
+
)),
|
|
95
|
+
);
|
|
88
96
|
state.insert("session_id".to_string(), serde_json::Value::Null);
|
|
89
97
|
state.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
90
98
|
state.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
@@ -162,3 +170,130 @@ pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) ->
|
|
|
162
170
|
DangerousApprovalSource::Disabled => "disabled",
|
|
163
171
|
}
|
|
164
172
|
}
|
|
173
|
+
|
|
174
|
+
/// 0.5.66 bypass 单源:从 agent 行 + provider 构造 approval policy JSON
|
|
175
|
+
/// (restart/fork/add 落盘用;取代全队 `DangerousApproval` 派生)。
|
|
176
|
+
pub(crate) fn effective_approval_policy_from_agent(
|
|
177
|
+
agent: &serde_json::Value,
|
|
178
|
+
provider: Provider,
|
|
179
|
+
) -> serde_json::Value {
|
|
180
|
+
let enabled = agent
|
|
181
|
+
.get("dangerously_skip_permissions")
|
|
182
|
+
.and_then(serde_json::Value::as_bool)
|
|
183
|
+
.unwrap_or(false);
|
|
184
|
+
if !enabled {
|
|
185
|
+
return serde_json::json!({
|
|
186
|
+
"enabled": false,
|
|
187
|
+
"source": "disabled",
|
|
188
|
+
"inherited": false,
|
|
189
|
+
"explicit_yes_confirmed": false,
|
|
190
|
+
"provider": null,
|
|
191
|
+
"flag": null,
|
|
192
|
+
"worker_capability_above_leader": false,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
|
|
196
|
+
serde_json::json!({
|
|
197
|
+
"enabled": true,
|
|
198
|
+
"source": "runtime_config",
|
|
199
|
+
"inherited": false,
|
|
200
|
+
"explicit_yes_confirmed": true,
|
|
201
|
+
"provider": provider_wire_str(provider),
|
|
202
|
+
"flag": flag,
|
|
203
|
+
"worker_capability_above_leader": false,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn provider_wire_str(provider: Provider) -> &'static str {
|
|
208
|
+
match provider {
|
|
209
|
+
Provider::Claude => "claude",
|
|
210
|
+
Provider::ClaudeCode => "claude_code",
|
|
211
|
+
Provider::Codex => "codex",
|
|
212
|
+
Provider::Copilot => "copilot",
|
|
213
|
+
Provider::GeminiCli => "gemini_cli",
|
|
214
|
+
Provider::Fake => "fake",
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// 0.5.66 bypass 单源:从 agent 行(state Map)+ provider 落盘 approval policy。
|
|
219
|
+
/// 目标与来源是同一 agent 行。
|
|
220
|
+
pub(crate) fn persist_effective_approval_policy_from_agent(
|
|
221
|
+
agent_state: &mut serde_json::Map<String, serde_json::Value>,
|
|
222
|
+
provider: Provider,
|
|
223
|
+
) {
|
|
224
|
+
let agent = serde_json::Value::Object(agent_state.clone());
|
|
225
|
+
agent_state.insert(
|
|
226
|
+
"effective_approval_policy".to_string(),
|
|
227
|
+
effective_approval_policy_from_agent(&agent, provider),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// 0.5.66 bypass 单源:yaml spec agent 版(launch/state_projection 用)。
|
|
232
|
+
pub(crate) fn persist_effective_approval_policy_from_yaml_agent(
|
|
233
|
+
agent_state: &mut serde_json::Map<String, serde_json::Value>,
|
|
234
|
+
agent: &Value,
|
|
235
|
+
provider: Provider,
|
|
236
|
+
) {
|
|
237
|
+
let enabled = matches!(agent.get("dangerously_skip_permissions"), Some(Value::Bool(true)));
|
|
238
|
+
let policy = if !enabled {
|
|
239
|
+
serde_json::json!({
|
|
240
|
+
"enabled": false,
|
|
241
|
+
"source": "disabled",
|
|
242
|
+
"inherited": false,
|
|
243
|
+
"explicit_yes_confirmed": false,
|
|
244
|
+
"provider": null,
|
|
245
|
+
"flag": null,
|
|
246
|
+
"worker_capability_above_leader": false,
|
|
247
|
+
})
|
|
248
|
+
} else {
|
|
249
|
+
let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
|
|
250
|
+
serde_json::json!({
|
|
251
|
+
"enabled": true,
|
|
252
|
+
"source": "runtime_config",
|
|
253
|
+
"inherited": false,
|
|
254
|
+
"explicit_yes_confirmed": true,
|
|
255
|
+
"provider": provider_wire_str(provider),
|
|
256
|
+
"flag": flag,
|
|
257
|
+
"worker_capability_above_leader": false,
|
|
258
|
+
})
|
|
259
|
+
};
|
|
260
|
+
agent_state.insert("effective_approval_policy".to_string(), policy);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[cfg(test)]
|
|
264
|
+
mod tests {
|
|
265
|
+
#![allow(clippy::unwrap_used)]
|
|
266
|
+
use super::*;
|
|
267
|
+
|
|
268
|
+
fn yaml_agent(bypass: bool) -> Value {
|
|
269
|
+
crate::model::yaml::loads(&format!(
|
|
270
|
+
"id: a\nrole: r\nprovider: codex\ndangerously_skip_permissions: {bypass}\n"
|
|
271
|
+
))
|
|
272
|
+
.unwrap()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 0.5.66 bypass 单源 §4.1:true 角色 → policy 落盘为 enabled=true / runtime_config,
|
|
276
|
+
// 且 running_agent_state 记录 as_launched_dangerously_skip_permissions=true(审计留痕前提)。
|
|
277
|
+
#[test]
|
|
278
|
+
fn test_audit_log_written_on_true_spawn() {
|
|
279
|
+
let mut state = serde_json::Map::new();
|
|
280
|
+
persist_effective_approval_policy_from_yaml_agent(&mut state, &yaml_agent(true), Provider::Codex);
|
|
281
|
+
let policy = state.get("effective_approval_policy").unwrap();
|
|
282
|
+
assert_eq!(policy["enabled"], serde_json::json!(true));
|
|
283
|
+
assert_eq!(policy["source"], serde_json::json!("runtime_config"));
|
|
284
|
+
assert_eq!(
|
|
285
|
+
policy["flag"],
|
|
286
|
+
serde_json::json!("--dangerously-bypass-approvals-and-sandbox")
|
|
287
|
+
);
|
|
288
|
+
// 运行面记录(as-launched)供 restart drift 挡板。
|
|
289
|
+
let mut run_state = serde_json::Map::new();
|
|
290
|
+
run_state.insert(
|
|
291
|
+
"as_launched_dangerously_skip_permissions".to_string(),
|
|
292
|
+
serde_json::json!(matches!(
|
|
293
|
+
yaml_agent(true).get("dangerously_skip_permissions"),
|
|
294
|
+
Some(Value::Bool(true))
|
|
295
|
+
)),
|
|
296
|
+
);
|
|
297
|
+
assert_eq!(run_state["as_launched_dangerously_skip_permissions"], serde_json::json!(true));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -121,7 +121,7 @@ pub fn fork_agent_with_transport(
|
|
|
121
121
|
)));
|
|
122
122
|
}
|
|
123
123
|
let model = new_agent.get("model").and_then(Value::as_str);
|
|
124
|
-
let safety =
|
|
124
|
+
let safety = effective_runtime_config_for_worker_spawn(new_agent, provider)?;
|
|
125
125
|
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
126
126
|
new_agent,
|
|
127
127
|
Some(as_agent_id.as_str()),
|
|
@@ -132,7 +132,6 @@ pub fn fork_agent_with_transport(
|
|
|
132
132
|
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
133
133
|
&command_agent,
|
|
134
134
|
provider,
|
|
135
|
-
&safety,
|
|
136
135
|
)?;
|
|
137
136
|
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
138
137
|
let mcp_config = adapter.mcp_config(auth_mode).map_err(|e| {
|
|
@@ -232,6 +232,11 @@ pub(super) fn finalize_fork_state(
|
|
|
232
232
|
.and_then(serde_json::Value::as_object_mut)
|
|
233
233
|
{
|
|
234
234
|
persist_effective_approval_policy(agent, input.safety);
|
|
235
|
+
// 0.5.66 bypass 单源 §2.6:fork fresh spawn 记录运行面 bypass 值。
|
|
236
|
+
agent.insert(
|
|
237
|
+
"as_launched_dangerously_skip_permissions".to_string(),
|
|
238
|
+
serde_json::json!(input.safety.enabled),
|
|
239
|
+
);
|
|
235
240
|
}
|
|
236
241
|
maybe_fail_fork_after_spawn("save_runtime_state")?;
|
|
237
242
|
crate::state::repository::StateRepository::new(input.workspace)
|
|
@@ -27,7 +27,6 @@ pub(super) fn spawn_agents(
|
|
|
27
27
|
spec_path: &Path,
|
|
28
28
|
spec: &Value,
|
|
29
29
|
session_name: &SessionName,
|
|
30
|
-
safety: &DangerousApproval,
|
|
31
30
|
transport: &dyn Transport,
|
|
32
31
|
) -> Result<Vec<StartedAgent>, LifecycleError> {
|
|
33
32
|
// E5 解耦:team_dir(角色定义 + profiles 所在)≠ spec_path.parent()(spec 已迁出到 .team/runtime)。
|
|
@@ -100,9 +99,10 @@ pub(super) fn spawn_agents(
|
|
|
100
99
|
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
101
100
|
&command_agent,
|
|
102
101
|
provider,
|
|
103
|
-
safety,
|
|
104
102
|
)?;
|
|
105
103
|
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
104
|
+
// 0.5.66 bypass 单源:per-agent safety 只服务 env 注入与审计,不参与 argv 决策。
|
|
105
|
+
let safety = effective_runtime_config_for_worker_spawn(agent, provider)?;
|
|
106
106
|
let mcp_team_id = runtime_team_key_for_spec(spec_path, spec, session_name);
|
|
107
107
|
let mcp_config = adapter
|
|
108
108
|
.mcp_config(auth_mode)
|
|
@@ -283,6 +283,25 @@ pub(super) fn spawn_agents(
|
|
|
283
283
|
"spawn_epoch": spawn_epoch,
|
|
284
284
|
})),
|
|
285
285
|
);
|
|
286
|
+
// 0.5.66 bypass 单源 §2.7:最终 argv 含 bypass flag → 审计留痕。
|
|
287
|
+
if matches!(
|
|
288
|
+
agent.get("dangerously_skip_permissions"),
|
|
289
|
+
Some(Value::Bool(true))
|
|
290
|
+
) {
|
|
291
|
+
let _ = event_log.write(
|
|
292
|
+
"worker.spawn_dangerously_skip_permissions",
|
|
293
|
+
serde_json::json!({
|
|
294
|
+
"agent_id": agent_id_raw,
|
|
295
|
+
"role": agent
|
|
296
|
+
.get("role")
|
|
297
|
+
.and_then(Value::as_str)
|
|
298
|
+
.unwrap_or_default(),
|
|
299
|
+
"provider": provider,
|
|
300
|
+
"trigger": "launch",
|
|
301
|
+
"spec_source_path": agent_id_spec_source_path(workspace, agent_id_raw),
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
286
305
|
}
|
|
287
306
|
// 0.3.28 Step 4b: replaced the `adaptive_layout_plan` 3-pane tiling
|
|
288
307
|
// with Python-parity 1-window-per-agent placement. Window name =
|
|
@@ -381,3 +400,21 @@ pub(super) fn spawn_agents(
|
|
|
381
400
|
}
|
|
382
401
|
Ok(started)
|
|
383
402
|
}
|
|
403
|
+
|
|
404
|
+
/// 0.5.66 bypass 单源 §2.7:审计事件里角色 md 的 spec 来源路径(尽力解析,失败空串)。
|
|
405
|
+
pub(crate) fn agent_id_spec_source_path(workspace: &Path, agent_id: &str) -> String {
|
|
406
|
+
let team_dir = crate::state::persist::load_runtime_state(workspace)
|
|
407
|
+
.ok()
|
|
408
|
+
.and_then(|state| {
|
|
409
|
+
state
|
|
410
|
+
.get("team_dir")
|
|
411
|
+
.and_then(serde_json::Value::as_str)
|
|
412
|
+
.filter(|s| !s.is_empty())
|
|
413
|
+
.map(PathBuf::from)
|
|
414
|
+
});
|
|
415
|
+
team_dir
|
|
416
|
+
.map(|dir| dir.join("agents").join(format!("{agent_id}.md")))
|
|
417
|
+
.unwrap_or_else(|| workspace.join(".team/current/agents").join(format!("{agent_id}.md")))
|
|
418
|
+
.display()
|
|
419
|
+
.to_string()
|
|
420
|
+
}
|
|
@@ -33,7 +33,6 @@ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
|
33
33
|
|
|
34
34
|
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
35
35
|
|
|
36
|
-
use super::approval::detect_dangerous_approval;
|
|
37
36
|
use super::identity::spec_display_backend;
|
|
38
37
|
use super::leader_context::{
|
|
39
38
|
attributed_provider_for_pane_across_tmux_sockets, caller_provider_for_seed_with_lookup,
|
|
@@ -387,64 +386,86 @@ pub(super) fn spec_default_assignee(spec: &Value) -> Option<AgentId> {
|
|
|
387
386
|
.or_else(|| spec_agents(spec).into_iter().next())
|
|
388
387
|
}
|
|
389
388
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
389
|
+
/// 0.5.66 bypass 单源:从**单个 agent**(yaml spec)的 `dangerously_skip_permissions` 构造
|
|
390
|
+
/// `DangerousApproval`(取代旧"runtime config + leader argv"全队一份)。
|
|
391
|
+
/// true → source=RuntimeConfig(角色声明即用户明确同意,coordinator 的
|
|
392
|
+
/// `explicit_yes_confirmed` 自动放行);false → Disabled。
|
|
393
|
+
pub(crate) fn effective_runtime_config_for_worker_spawn(
|
|
394
|
+
agent: &Value,
|
|
395
|
+
provider: Provider,
|
|
396
|
+
) -> Result<DangerousApproval, LifecycleError> {
|
|
397
|
+
let enabled = matches!(
|
|
398
|
+
agent.get("dangerously_skip_permissions"),
|
|
399
|
+
Some(Value::Bool(true))
|
|
400
|
+
);
|
|
401
|
+
if !enabled {
|
|
402
|
+
return Ok(DangerousApproval {
|
|
403
|
+
enabled: false,
|
|
404
|
+
source: DangerousApprovalSource::Disabled,
|
|
400
405
|
inherited: false,
|
|
401
406
|
provider: None,
|
|
402
407
|
flag: None,
|
|
403
|
-
worker_capability_above_leader:
|
|
404
|
-
ancestry_binary_name:
|
|
408
|
+
worker_capability_above_leader: false,
|
|
409
|
+
ancestry_binary_name: None,
|
|
405
410
|
unexpected_binary: false,
|
|
406
|
-
})
|
|
407
|
-
} else {
|
|
408
|
-
Ok(detect_dangerous_approval()?)
|
|
411
|
+
});
|
|
409
412
|
}
|
|
413
|
+
let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
|
|
414
|
+
Ok(DangerousApproval {
|
|
415
|
+
enabled: true,
|
|
416
|
+
source: DangerousApprovalSource::RuntimeConfig,
|
|
417
|
+
inherited: false,
|
|
418
|
+
provider: Some(provider_display_str(provider).to_string()),
|
|
419
|
+
flag: flag.map(str::to_string),
|
|
420
|
+
worker_capability_above_leader: false,
|
|
421
|
+
ancestry_binary_name: None,
|
|
422
|
+
unexpected_binary: false,
|
|
423
|
+
})
|
|
410
424
|
}
|
|
411
425
|
|
|
412
|
-
|
|
426
|
+
/// 0.5.66 bypass 单源:restart/state 路径的 serde_json 版(agent 来自 state JSON)。
|
|
427
|
+
pub(crate) fn effective_runtime_config_for_worker_spawn_json(
|
|
428
|
+
agent: &serde_json::Value,
|
|
429
|
+
provider: Provider,
|
|
413
430
|
) -> Result<DangerousApproval, LifecycleError> {
|
|
414
|
-
|
|
431
|
+
let enabled = agent
|
|
432
|
+
.get("dangerously_skip_permissions")
|
|
433
|
+
.and_then(serde_json::Value::as_bool)
|
|
434
|
+
.unwrap_or(false);
|
|
435
|
+
if !enabled {
|
|
436
|
+
return Ok(DangerousApproval {
|
|
437
|
+
enabled: false,
|
|
438
|
+
source: DangerousApprovalSource::Disabled,
|
|
439
|
+
inherited: false,
|
|
440
|
+
provider: None,
|
|
441
|
+
flag: None,
|
|
442
|
+
worker_capability_above_leader: false,
|
|
443
|
+
ancestry_binary_name: None,
|
|
444
|
+
unexpected_binary: false,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
|
|
448
|
+
Ok(DangerousApproval {
|
|
449
|
+
enabled: true,
|
|
450
|
+
source: DangerousApprovalSource::RuntimeConfig,
|
|
451
|
+
inherited: false,
|
|
452
|
+
provider: Some(provider_display_str(provider).to_string()),
|
|
453
|
+
flag: flag.map(str::to_string),
|
|
454
|
+
worker_capability_above_leader: false,
|
|
455
|
+
ancestry_binary_name: None,
|
|
456
|
+
unexpected_binary: false,
|
|
457
|
+
})
|
|
415
458
|
}
|
|
416
459
|
|
|
417
|
-
pub(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
"dangerous_auto_approve": safety.enabled,
|
|
426
|
-
"dangerous_auto_approve_source": safety.source,
|
|
427
|
-
"dangerous_auto_approve_inherited": safety.inherited,
|
|
428
|
-
"dangerous_auto_approve_provider": safety.provider,
|
|
429
|
-
"dangerous_auto_approve_flag": safety.flag,
|
|
430
|
-
"worker_capability_above_leader": safety.worker_capability_above_leader,
|
|
431
|
-
"ancestry_binary_name": safety.ancestry_binary_name,
|
|
432
|
-
}),
|
|
433
|
-
)
|
|
434
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
435
|
-
if safety.unexpected_binary {
|
|
436
|
-
crate::event_log::EventLog::new(workspace)
|
|
437
|
-
.write(
|
|
438
|
-
"dangerous_flag_in_unexpected_binary",
|
|
439
|
-
serde_json::json!({
|
|
440
|
-
"provider": safety.provider,
|
|
441
|
-
"flag": safety.flag,
|
|
442
|
-
"ancestry_binary_name": safety.ancestry_binary_name,
|
|
443
|
-
}),
|
|
444
|
-
)
|
|
445
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
460
|
+
pub(crate) fn provider_display_str(provider: Provider) -> &'static str {
|
|
461
|
+
match provider {
|
|
462
|
+
Provider::Claude => "claude",
|
|
463
|
+
Provider::ClaudeCode => "claude_code",
|
|
464
|
+
Provider::Codex => "codex",
|
|
465
|
+
Provider::Copilot => "copilot",
|
|
466
|
+
Provider::GeminiCli => "gemini_cli",
|
|
467
|
+
Provider::Fake => "fake",
|
|
446
468
|
}
|
|
447
|
-
Ok(())
|
|
448
469
|
}
|
|
449
470
|
|
|
450
471
|
pub(super) fn team_workspace(team_dir: &Path) -> PathBuf {
|
|
@@ -455,3 +476,39 @@ pub(super) fn team_workspace(team_dir: &Path) -> PathBuf {
|
|
|
455
476
|
.unwrap_or_else(|| team_dir.to_path_buf())
|
|
456
477
|
})
|
|
457
478
|
}
|
|
479
|
+
|
|
480
|
+
#[cfg(test)]
|
|
481
|
+
mod tests {
|
|
482
|
+
#![allow(clippy::unwrap_used)]
|
|
483
|
+
use super::*;
|
|
484
|
+
use crate::model::enums::Provider;
|
|
485
|
+
|
|
486
|
+
fn yaml_agent(bypass: bool) -> Value {
|
|
487
|
+
crate::model::yaml::loads(&format!(
|
|
488
|
+
"id: a\nrole: r\nprovider: codex\ndangerously_skip_permissions: {bypass}\n"
|
|
489
|
+
))
|
|
490
|
+
.unwrap()
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 0.5.66 bypass 单源 §4.1:true → enabled + source=runtime_config(角色声明即明确同意)。
|
|
494
|
+
#[test]
|
|
495
|
+
fn test_effective_safety_from_true_field() {
|
|
496
|
+
let safety =
|
|
497
|
+
effective_runtime_config_for_worker_spawn(&yaml_agent(true), Provider::Codex).unwrap();
|
|
498
|
+
assert!(safety.enabled);
|
|
499
|
+
assert_eq!(safety.source, DangerousApprovalSource::RuntimeConfig);
|
|
500
|
+
assert_eq!(
|
|
501
|
+
safety.flag.as_deref(),
|
|
502
|
+
Some("--dangerously-bypass-approvals-and-sandbox")
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// 0.5.66 bypass 单源 §4.1:false → Disabled。
|
|
507
|
+
#[test]
|
|
508
|
+
fn test_effective_safety_from_false_field() {
|
|
509
|
+
let safety =
|
|
510
|
+
effective_runtime_config_for_worker_spawn(&yaml_agent(false), Provider::Codex).unwrap();
|
|
511
|
+
assert!(!safety.enabled);
|
|
512
|
+
assert_eq!(safety.source, DangerousApprovalSource::Disabled);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
@@ -28,7 +28,6 @@ pub(super) fn persist_spawn_agent_state(
|
|
|
28
28
|
session_name: &SessionName,
|
|
29
29
|
transport: &dyn Transport,
|
|
30
30
|
started: &[StartedAgent],
|
|
31
|
-
safety: &DangerousApproval,
|
|
32
31
|
) -> Result<(), LifecycleError> {
|
|
33
32
|
let state_path = crate::state::persist::runtime_state_path(workspace);
|
|
34
33
|
let mut state = match crate::state::repository::StateRepository::new(workspace)
|
|
@@ -134,7 +133,6 @@ pub(super) fn persist_spawn_agent_state(
|
|
|
134
133
|
&team_id,
|
|
135
134
|
Some(agent_id_to_pane_id(started, id)),
|
|
136
135
|
pane_pid,
|
|
137
|
-
safety,
|
|
138
136
|
started_agent,
|
|
139
137
|
Some(&profile_dir),
|
|
140
138
|
)?,
|
|
@@ -70,6 +70,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
70
70
|
transport: &dyn Transport,
|
|
71
71
|
) -> Result<LaunchReport, LifecycleError> {
|
|
72
72
|
let _ = skip_profile_smoke;
|
|
73
|
+
let _ = auto_approve; // 0.5.66: bypass 单源后无 runtime.dangerous_auto_approve,`--yes` 为 no-op。
|
|
73
74
|
// 0.5.38 (`.team/artifacts/startup-latency-locate.md` §5): launch.phase
|
|
74
75
|
// timer with monotonic `elapsed_ms` for latency triage.
|
|
75
76
|
let phase_timer = crate::lifecycle::restart::RestartPhaseTimer::start();
|
|
@@ -82,14 +83,12 @@ pub fn launch_with_transport_in_workspace(
|
|
|
82
83
|
let text = std::fs::read_to_string(spec_path)
|
|
83
84
|
.map_err(|e| LifecycleError::Compile(format!("{}: {e}", spec_path.display())))?;
|
|
84
85
|
let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
86
|
+
// 0.5.66 §3.2 跨 workspace 兼容警示:leader argv 带 bypass flag 但当前团队
|
|
87
|
+
// 所有角色都未声明 dangerously_skip_permissions=true → 写 warning 事件不阻塞。
|
|
88
|
+
// TODO(0.6.0): remove(0.6.0 彻底删源头 A 后此警示无意义)。
|
|
89
|
+
emit_dangerous_flag_no_role_declared_warning(workspace, &spec);
|
|
85
90
|
phase_timer.emit(workspace, "launch.phase", "compile_spec");
|
|
86
91
|
let session_name = spec_session_name(&spec);
|
|
87
|
-
let safety = effective_runtime_config(&spec)?;
|
|
88
|
-
if safety.enabled && !safety.inherited && !auto_approve && !dry_run {
|
|
89
|
-
return Err(LifecycleError::DangerousApprovalRequired(
|
|
90
|
-
"runtime dangerous_auto_approve is enabled".to_string(),
|
|
91
|
-
));
|
|
92
|
-
}
|
|
93
92
|
if !dry_run && transport_has_session(transport, &session_name) {
|
|
94
93
|
return Err(LifecycleError::SessionConflict(format!(
|
|
95
94
|
"tmux session already exists: {}",
|
|
@@ -103,7 +102,6 @@ pub fn launch_with_transport_in_workspace(
|
|
|
103
102
|
raw: serde_json::json!({"source": "compiled_spec"}),
|
|
104
103
|
})
|
|
105
104
|
.collect::<Vec<_>>();
|
|
106
|
-
write_launch_permission_audit(workspace, &safety)?;
|
|
107
105
|
let routes = spec_routes(&spec);
|
|
108
106
|
let started = if dry_run {
|
|
109
107
|
Vec::new()
|
|
@@ -114,7 +112,6 @@ pub fn launch_with_transport_in_workspace(
|
|
|
114
112
|
spec_path,
|
|
115
113
|
&spec,
|
|
116
114
|
&session_name,
|
|
117
|
-
&safety,
|
|
118
115
|
transport,
|
|
119
116
|
)?;
|
|
120
117
|
persist_spawn_agent_state(
|
|
@@ -124,7 +121,6 @@ pub fn launch_with_transport_in_workspace(
|
|
|
124
121
|
&session_name,
|
|
125
122
|
transport,
|
|
126
123
|
&started,
|
|
127
|
-
&safety,
|
|
128
124
|
)?;
|
|
129
125
|
// 0.5.38: per-worker timing tags (source="launch") so operators can
|
|
130
126
|
// trace which worker's spawn dominates wall time. Zeros for now on
|
|
@@ -181,17 +177,55 @@ pub fn launch_with_transport_in_workspace(
|
|
|
181
177
|
tmux_endpoint: transport.tmux_endpoint(),
|
|
182
178
|
routes,
|
|
183
179
|
permissions,
|
|
184
|
-
safety,
|
|
185
180
|
leader_receiver_attached: false,
|
|
186
181
|
session_capture_incomplete_agents: Vec::new(),
|
|
187
182
|
})
|
|
188
183
|
}
|
|
189
184
|
|
|
185
|
+
/// 0.5.66 §3.2 跨 workspace 升级警示(仅警示,不做行为决策)。
|
|
186
|
+
/// leader argv 含 bypass flag、但当前 spec 所有 agent 都未声明
|
|
187
|
+
/// `dangerously_skip_permissions=true` → 写 `dangerously_flag_present_but_no_role_declared`。
|
|
188
|
+
///
|
|
189
|
+
/// TODO(0.6.0): remove(0.6.0 删源头 A 后此警示无意义)。
|
|
190
|
+
fn emit_dangerous_flag_no_role_declared_warning(workspace: &Path, spec: &Value) {
|
|
191
|
+
let argv: Vec<String> = std::env::args().collect();
|
|
192
|
+
emit_dangerous_flag_no_role_declared_warning_with_argv(workspace, spec, &argv);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// 带显式 argv 的可测版。
|
|
196
|
+
fn emit_dangerous_flag_no_role_declared_warning_with_argv(
|
|
197
|
+
workspace: &Path,
|
|
198
|
+
spec: &Value,
|
|
199
|
+
argv: &[String],
|
|
200
|
+
) {
|
|
201
|
+
if crate::provider::bypass_flags::detect_bypass_flag_in_argv(argv).is_none() {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let any_role_declared = spec_agent_values(spec).iter().any(|agent| {
|
|
205
|
+
matches!(
|
|
206
|
+
agent.get("dangerously_skip_permissions"),
|
|
207
|
+
Some(Value::Bool(true))
|
|
208
|
+
)
|
|
209
|
+
});
|
|
210
|
+
if any_role_declared {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let _ = crate::event_log::EventLog::new(workspace).write(
|
|
214
|
+
"dangerously_flag_present_but_no_role_declared",
|
|
215
|
+
serde_json::json!({
|
|
216
|
+
"message": "leader process carries a bypass flag but no role in this team declares \
|
|
217
|
+
dangerously_skip_permissions=true; bypass no longer inherits to workers \
|
|
218
|
+
(0.5.66 single-source). Declare the field per role to opt in.",
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
mod plan;
|
|
191
224
|
pub(crate) use plan::{handle_report_result, start_plan};
|
|
192
225
|
|
|
193
226
|
pub mod spawn;
|
|
194
227
|
pub(super) use spawn::*;
|
|
228
|
+
pub(crate) use spawn::agent_id_spec_source_path;
|
|
195
229
|
|
|
196
230
|
mod layout;
|
|
197
231
|
pub(super) use layout::*;
|
|
@@ -221,7 +255,10 @@ pub(crate) use leader_context::{
|
|
|
221
255
|
|
|
222
256
|
mod agent_state;
|
|
223
257
|
pub(super) use agent_state::*;
|
|
224
|
-
pub(crate) use agent_state::{
|
|
258
|
+
pub(crate) use agent_state::{
|
|
259
|
+
effective_approval_policy, persist_effective_approval_policy,
|
|
260
|
+
persist_effective_approval_policy_from_agent,
|
|
261
|
+
};
|
|
225
262
|
|
|
226
263
|
mod mcp_config;
|
|
227
264
|
pub(super) use mcp_config::*;
|
|
@@ -266,14 +303,6 @@ pub mod readiness;
|
|
|
266
303
|
pub(crate) use readiness::launched_team_receiver_is_attached;
|
|
267
304
|
pub(super) use readiness::*;
|
|
268
305
|
|
|
269
|
-
mod approval;
|
|
270
|
-
pub use approval::detect_dangerous_approval;
|
|
271
|
-
use approval::{
|
|
272
|
-
binary_matches_provider, binary_name, dangerous_leader_flags,
|
|
273
|
-
detect_dangerous_approval_in_argv, disabled_dangerous_approval, process_ancestry_argv,
|
|
274
|
-
process_argv_tokens, process_parent_pid,
|
|
275
|
-
};
|
|
276
|
-
|
|
277
306
|
mod add_agent;
|
|
278
307
|
pub(super) use add_agent::*;
|
|
279
308
|
pub use add_agent::{add_agent, add_agent_force};
|
|
@@ -309,14 +338,81 @@ pub(crate) use ownership::{ensure_owner_allowed, ensure_owner_allowed_for_state,
|
|
|
309
338
|
|
|
310
339
|
pub mod spec_state;
|
|
311
340
|
pub(crate) use spec_state::{
|
|
312
|
-
effective_runtime_config, effective_runtime_config_for_worker_spawn,
|
|
313
341
|
override_spec_session_name, override_spec_workspace, spec_agent_id_set, write_spec_atomic,
|
|
314
342
|
};
|
|
343
|
+
pub(crate) use spec_state::{
|
|
344
|
+
effective_runtime_config_for_worker_spawn, effective_runtime_config_for_worker_spawn_json,
|
|
345
|
+
};
|
|
315
346
|
use spec_state::{
|
|
316
347
|
env_nonempty, has_positive_caller_leader_env, initial_runtime_state,
|
|
317
348
|
override_spec_display_backend, override_spec_runtime_str,
|
|
318
349
|
seed_launched_owner_from_caller_with_provider_lookup, seed_launched_owner_from_env,
|
|
319
350
|
spec_agent_values, spec_agents, spec_default_assignee, spec_routes, spec_session_name,
|
|
320
|
-
spec_tasks_json, team_workspace,
|
|
351
|
+
spec_tasks_json, team_workspace, yaml_value_to_json,
|
|
321
352
|
};
|
|
322
353
|
pub use spec_state::worker_session_name_pub;
|
|
354
|
+
|
|
355
|
+
#[cfg(test)]
|
|
356
|
+
mod tests {
|
|
357
|
+
#![allow(clippy::unwrap_used)]
|
|
358
|
+
use super::*;
|
|
359
|
+
|
|
360
|
+
// 0.5.66 §4.1 test_leader_flag_present_but_no_role_true_writes_warning_event:
|
|
361
|
+
// leader argv 带 bypass flag + 无角色声明 true → 写 warning 事件。
|
|
362
|
+
#[test]
|
|
363
|
+
fn test_leader_flag_present_but_no_role_true_writes_warning_event() {
|
|
364
|
+
let ws = std::env::temp_dir().join(format!(
|
|
365
|
+
"ta-bypass-warning-test-{}",
|
|
366
|
+
std::process::id()
|
|
367
|
+
));
|
|
368
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
369
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
370
|
+
let spec = yaml::loads(
|
|
371
|
+
"version: 1\nteam: {}\nleader: {}\nagents:\n - id: a\n role: r\n provider: fake\n dangerously_skip_permissions: false\n",
|
|
372
|
+
)
|
|
373
|
+
.unwrap();
|
|
374
|
+
let argv = vec![
|
|
375
|
+
"team-agent".to_string(),
|
|
376
|
+
"--dangerously-skip-permissions".to_string(),
|
|
377
|
+
];
|
|
378
|
+
emit_dangerous_flag_no_role_declared_warning_with_argv(&ws, &spec, &argv);
|
|
379
|
+
let events = crate::event_log::EventLog::new(&ws).tail(0).unwrap();
|
|
380
|
+
assert!(
|
|
381
|
+
events.iter().any(|event| {
|
|
382
|
+
event.get("event").and_then(serde_json::Value::as_str)
|
|
383
|
+
== Some("dangerously_flag_present_but_no_role_declared")
|
|
384
|
+
}),
|
|
385
|
+
"leader flag + no role declared must write the warning event; events={events:?}"
|
|
386
|
+
);
|
|
387
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 0.5.66 §4.1 同测试的反向:有角色声明 true → 不写 warning。
|
|
391
|
+
#[test]
|
|
392
|
+
fn test_leader_flag_present_but_role_declared_no_warning() {
|
|
393
|
+
let ws = std::env::temp_dir().join(format!(
|
|
394
|
+
"ta-bypass-warning-none-test-{}",
|
|
395
|
+
std::process::id()
|
|
396
|
+
));
|
|
397
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
398
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
399
|
+
let spec = yaml::loads(
|
|
400
|
+
"version: 1\nteam: {}\nleader: {}\nagents:\n - id: a\n role: r\n provider: fake\n dangerously_skip_permissions: true\n",
|
|
401
|
+
)
|
|
402
|
+
.unwrap();
|
|
403
|
+
let argv = vec![
|
|
404
|
+
"team-agent".to_string(),
|
|
405
|
+
"--dangerously-skip-permissions".to_string(),
|
|
406
|
+
];
|
|
407
|
+
emit_dangerous_flag_no_role_declared_warning_with_argv(&ws, &spec, &argv);
|
|
408
|
+
let events = crate::event_log::EventLog::new(&ws).tail(0).unwrap();
|
|
409
|
+
assert!(
|
|
410
|
+
!events.iter().any(|event| {
|
|
411
|
+
event.get("event").and_then(serde_json::Value::as_str)
|
|
412
|
+
== Some("dangerously_flag_present_but_no_role_declared")
|
|
413
|
+
}),
|
|
414
|
+
"role declared true must suppress the warning; events={events:?}"
|
|
415
|
+
);
|
|
416
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
417
|
+
}
|
|
418
|
+
}
|