@team-agent/installer 0.5.65 → 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 +1 -1
- package/crates/team-agent/src/cli/tests/compile.rs +1 -1
- package/crates/team-agent/src/cli/tests/mod.rs +2 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
- package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
- 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/leader/start.rs +167 -2
- 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 +6 -9
- 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 +1 -1
- 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 +1 -1
- 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 +0 -2
- 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/messaging/tests/main_preserved.rs +1 -1
- 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 +19 -0
- package/crates/team-agent/src/transport/test_support.rs +20 -0
- package/crates/team-agent/src/transport.rs +17 -0
- 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
|
@@ -1059,6 +1059,16 @@ fn execute_managed_leader_plan_attempt(
|
|
|
1059
1059
|
workspace,
|
|
1060
1060
|
session_existed_before,
|
|
1061
1061
|
)?;
|
|
1062
|
+
// A-55 (window rename race): tmux defaults `allow-rename` + `automatic-rename`
|
|
1063
|
+
// ON, so the pane's shell wrapper can print an OSC title-set escape during
|
|
1064
|
+
// init (zshrc/starship/PROMPT_COMMAND) and rename the window away from WINDOW
|
|
1065
|
+
// between spawn-poll and attach — making `attach-session -t SESSION:WINDOW`
|
|
1066
|
+
// fail with "can't find window: WINDOW". Lock both off right after spawn,
|
|
1067
|
+
// before run_leader_argv. Best-effort: a failure must NOT abort the launcher
|
|
1068
|
+
// (old tmux may lack the option), but it is audited below.
|
|
1069
|
+
let (rename_result, rename_stderr) =
|
|
1070
|
+
lock_down_window_rename(&transport, session, &spawned.window);
|
|
1071
|
+
write_window_rename_lockdown_event(workspace, &rename_result, &rename_stderr);
|
|
1062
1072
|
if let Err(error) = persist_managed_leader_binding(plan, workspace, &spawned) {
|
|
1063
1073
|
cleanup_managed_leader_resources(
|
|
1064
1074
|
&transport,
|
|
@@ -2246,6 +2256,44 @@ fn write_leader_startup_prompt_event(workspace: &Path, event: &str, fields: serd
|
|
|
2246
2256
|
let _ = crate::event_log::EventLog::new(workspace).write(event, fields);
|
|
2247
2257
|
}
|
|
2248
2258
|
|
|
2259
|
+
/// A-55 (window rename race): lock the freshly-spawned leader window against
|
|
2260
|
+
/// rename — `allow-rename off` (refuse pane-internal OSC title-set renames)
|
|
2261
|
+
/// AND `automatic-rename off` (refuse tmux's own command-driven rename). Both
|
|
2262
|
+
/// must be set; tmux treats them as independent. Best-effort: a failure must
|
|
2263
|
+
/// NOT abort the launcher (old tmux may lack the option) — return the outcome
|
|
2264
|
+
/// for audit instead of propagating an error. Returns (result, tmux_stderr).
|
|
2265
|
+
fn lock_down_window_rename(
|
|
2266
|
+
transport: &dyn Transport,
|
|
2267
|
+
session: &SessionName,
|
|
2268
|
+
window: &WindowName,
|
|
2269
|
+
) -> (String, String) {
|
|
2270
|
+
let allow_rename = transport.set_window_option(session, window, "allow-rename", "off");
|
|
2271
|
+
let automatic_rename = transport.set_window_option(session, window, "automatic-rename", "off");
|
|
2272
|
+
let (result, stderr) = match (&allow_rename, &automatic_rename) {
|
|
2273
|
+
(Ok(()), Ok(())) => ("ok".to_string(), String::new()),
|
|
2274
|
+
(Ok(()), Err(error)) => ("failed".to_string(), error.to_string()),
|
|
2275
|
+
(Err(error), Ok(())) => ("failed".to_string(), error.to_string()),
|
|
2276
|
+
(Err(a), Err(b)) => (
|
|
2277
|
+
"failed".to_string(),
|
|
2278
|
+
format!("allow-rename: {a}; automatic-rename: {b}"),
|
|
2279
|
+
),
|
|
2280
|
+
};
|
|
2281
|
+
(result, stderr)
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/// A-55: audit the rename-lockdown outcome to the event log. Failure does not
|
|
2285
|
+
/// abort the launcher; record ok/failed + tmux stderr for regression triage.
|
|
2286
|
+
fn write_window_rename_lockdown_event(workspace: &Path, result: &str, stderr: &str) {
|
|
2287
|
+
write_leader_startup_prompt_event(
|
|
2288
|
+
workspace,
|
|
2289
|
+
"leader.launcher.window_rename_lockdown",
|
|
2290
|
+
serde_json::json!({
|
|
2291
|
+
"result": result,
|
|
2292
|
+
"tmux_stderr": stderr,
|
|
2293
|
+
}),
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2249
2297
|
fn ensure_tmux_installed() -> Result<(), LeaderError> {
|
|
2250
2298
|
match Command::new("tmux").arg("-V").output() {
|
|
2251
2299
|
Ok(output) if output.status.success() => Ok(()),
|
|
@@ -2350,8 +2398,8 @@ mod tests {
|
|
|
2350
2398
|
use super::{
|
|
2351
2399
|
ensure_managed_provider_live_after_attach, execute_leader_plan_after_ambient_authority,
|
|
2352
2400
|
format_launcher_failure, handle_exec_provider_startup_prompts,
|
|
2353
|
-
is_interactive_shell_basename, push_bounded_stderr, shlex_quote,
|
|
2354
|
-
VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
|
|
2401
|
+
is_interactive_shell_basename, lock_down_window_rename, push_bounded_stderr, shlex_quote,
|
|
2402
|
+
write_window_rename_lockdown_event, VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
|
|
2355
2403
|
};
|
|
2356
2404
|
|
|
2357
2405
|
struct ScriptedTransport {
|
|
@@ -2359,6 +2407,8 @@ mod tests {
|
|
|
2359
2407
|
sent: Mutex<Vec<(Target, Vec<Key>)>>,
|
|
2360
2408
|
liveness: PaneLiveness,
|
|
2361
2409
|
targets: Vec<PaneInfo>,
|
|
2410
|
+
window_options: Mutex<Vec<(String, String)>>,
|
|
2411
|
+
window_option_error: Mutex<Option<String>>,
|
|
2362
2412
|
}
|
|
2363
2413
|
|
|
2364
2414
|
impl ScriptedTransport {
|
|
@@ -2368,6 +2418,8 @@ mod tests {
|
|
|
2368
2418
|
sent: Mutex::new(Vec::new()),
|
|
2369
2419
|
liveness: PaneLiveness::Unknown,
|
|
2370
2420
|
targets: Vec::new(),
|
|
2421
|
+
window_options: Mutex::new(Vec::new()),
|
|
2422
|
+
window_option_error: Mutex::new(None),
|
|
2371
2423
|
}
|
|
2372
2424
|
}
|
|
2373
2425
|
|
|
@@ -2377,6 +2429,8 @@ mod tests {
|
|
|
2377
2429
|
sent: Mutex::new(Vec::new()),
|
|
2378
2430
|
liveness,
|
|
2379
2431
|
targets: Vec::new(),
|
|
2432
|
+
window_options: Mutex::new(Vec::new()),
|
|
2433
|
+
window_option_error: Mutex::new(None),
|
|
2380
2434
|
}
|
|
2381
2435
|
}
|
|
2382
2436
|
|
|
@@ -2386,6 +2440,21 @@ mod tests {
|
|
|
2386
2440
|
sent: Mutex::new(Vec::new()),
|
|
2387
2441
|
liveness,
|
|
2388
2442
|
targets,
|
|
2443
|
+
window_options: Mutex::new(Vec::new()),
|
|
2444
|
+
window_option_error: Mutex::new(None),
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/// A-55: a ScriptedTransport whose `set_window_option` always fails with
|
|
2449
|
+
/// the given tmux stderr — proves the lockdown never aborts the launcher.
|
|
2450
|
+
fn with_window_option_error(error: &str) -> Self {
|
|
2451
|
+
Self {
|
|
2452
|
+
screens: Mutex::new(Vec::new()),
|
|
2453
|
+
sent: Mutex::new(Vec::new()),
|
|
2454
|
+
liveness: PaneLiveness::Unknown,
|
|
2455
|
+
targets: Vec::new(),
|
|
2456
|
+
window_options: Mutex::new(Vec::new()),
|
|
2457
|
+
window_option_error: Mutex::new(Some(error.to_string())),
|
|
2389
2458
|
}
|
|
2390
2459
|
}
|
|
2391
2460
|
|
|
@@ -2395,6 +2464,14 @@ mod tests {
|
|
|
2395
2464
|
Err(poisoned) => poisoned.into_inner().clone(),
|
|
2396
2465
|
}
|
|
2397
2466
|
}
|
|
2467
|
+
|
|
2468
|
+
/// All `set-window-option option value` pairs, in call order.
|
|
2469
|
+
fn window_options(&self) -> Vec<(String, String)> {
|
|
2470
|
+
match self.window_options.lock() {
|
|
2471
|
+
Ok(guard) => guard.clone(),
|
|
2472
|
+
Err(poisoned) => poisoned.into_inner().clone(),
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2398
2475
|
}
|
|
2399
2476
|
|
|
2400
2477
|
impl Transport for ScriptedTransport {
|
|
@@ -2511,6 +2588,34 @@ mod tests {
|
|
|
2511
2588
|
Ok(SetEnvOutcome::Applied)
|
|
2512
2589
|
}
|
|
2513
2590
|
|
|
2591
|
+
fn set_window_option(
|
|
2592
|
+
&self,
|
|
2593
|
+
_session: &SessionName,
|
|
2594
|
+
_window: &WindowName,
|
|
2595
|
+
option: &str,
|
|
2596
|
+
value: &str,
|
|
2597
|
+
) -> Result<(), TransportError> {
|
|
2598
|
+
if let Some(stderr) = self
|
|
2599
|
+
.window_option_error
|
|
2600
|
+
.lock()
|
|
2601
|
+
.ok()
|
|
2602
|
+
.and_then(|guard| guard.clone())
|
|
2603
|
+
{
|
|
2604
|
+
return Err(TransportError::Subprocess {
|
|
2605
|
+
argv: vec!["tmux".to_string(), "set-window-option".to_string()],
|
|
2606
|
+
code: Some(1),
|
|
2607
|
+
stderr,
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
match self.window_options.lock() {
|
|
2611
|
+
Ok(mut guard) => guard.push((option.to_string(), value.to_string())),
|
|
2612
|
+
Err(poisoned) => poisoned
|
|
2613
|
+
.into_inner()
|
|
2614
|
+
.push((option.to_string(), value.to_string())),
|
|
2615
|
+
}
|
|
2616
|
+
Ok(())
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2514
2619
|
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
2515
2620
|
Ok(())
|
|
2516
2621
|
}
|
|
@@ -2988,4 +3093,64 @@ mod tests {
|
|
|
2988
3093
|
assert_eq!(captured.len(), LEADER_STDERR_LIMIT);
|
|
2989
3094
|
assert!(captured.ends_with(b"XYZ"));
|
|
2990
3095
|
}
|
|
3096
|
+
|
|
3097
|
+
#[test]
|
|
3098
|
+
fn managed_leader_launch_disables_window_rename() {
|
|
3099
|
+
let transport = ScriptedTransport::with_liveness(PaneLiveness::Live);
|
|
3100
|
+
let session = SessionName::new("team-agent-leader-claude_code-demo");
|
|
3101
|
+
let window = WindowName::new("claude_code");
|
|
3102
|
+
|
|
3103
|
+
let (result, stderr) = lock_down_window_rename(&transport, &session, &window);
|
|
3104
|
+
|
|
3105
|
+
assert_eq!(result, "ok", "both set-window-option calls must succeed");
|
|
3106
|
+
assert!(stderr.is_empty());
|
|
3107
|
+
let options = transport.window_options();
|
|
3108
|
+
assert_eq!(
|
|
3109
|
+
options,
|
|
3110
|
+
vec![
|
|
3111
|
+
("allow-rename".to_string(), "off".to_string()),
|
|
3112
|
+
("automatic-rename".to_string(), "off".to_string()),
|
|
3113
|
+
],
|
|
3114
|
+
"both rename lockdown options must be issued, in order"
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
#[test]
|
|
3119
|
+
fn launcher_survives_set_window_option_failure() {
|
|
3120
|
+
let workspace =
|
|
3121
|
+
std::env::temp_dir().join(format!("ta-rename-lockdown-fail-{}", std::process::id()));
|
|
3122
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
3123
|
+
let transport = ScriptedTransport::with_window_option_error(
|
|
3124
|
+
"set-window-option: unknown option: allow-rename",
|
|
3125
|
+
);
|
|
3126
|
+
let session = SessionName::new("team-agent-leader-claude_code-demo");
|
|
3127
|
+
let window = WindowName::new("claude_code");
|
|
3128
|
+
|
|
3129
|
+
// Lockdown must NOT abort the launcher: it returns a failed outcome
|
|
3130
|
+
// (recorded for audit) instead of propagating an error.
|
|
3131
|
+
let (result, stderr) = lock_down_window_rename(&transport, &session, &window);
|
|
3132
|
+
|
|
3133
|
+
assert_eq!(result, "failed", "a set-window-option failure is audited, not fatal");
|
|
3134
|
+
assert!(
|
|
3135
|
+
stderr.contains("unknown option"),
|
|
3136
|
+
"tmux stderr must surface in the audit payload: {stderr}"
|
|
3137
|
+
);
|
|
3138
|
+
|
|
3139
|
+
// The audit event lands in the workspace event log with result=failed.
|
|
3140
|
+
write_window_rename_lockdown_event(&workspace, &result, &stderr);
|
|
3141
|
+
let events = crate::event_log::EventLog::new(&workspace).tail(0).unwrap();
|
|
3142
|
+
let lockdown = events
|
|
3143
|
+
.iter()
|
|
3144
|
+
.find(|event| {
|
|
3145
|
+
event["event"] == "leader.launcher.window_rename_lockdown"
|
|
3146
|
+
})
|
|
3147
|
+
.expect("rename_lockdown event must be written even on failure");
|
|
3148
|
+
assert_eq!(lockdown["result"], "failed");
|
|
3149
|
+
assert!(
|
|
3150
|
+
lockdown["tmux_stderr"].as_str().unwrap_or_default().contains("unknown option"),
|
|
3151
|
+
"event must carry tmux stderr for regression triage: {lockdown}"
|
|
3152
|
+
);
|
|
3153
|
+
|
|
3154
|
+
std::fs::remove_dir_all(&workspace).ok();
|
|
3155
|
+
}
|
|
2991
3156
|
}
|
|
@@ -1054,7 +1054,7 @@ fn team_dir(root: &Path, name: &str, agents: &[(&str, &str)]) -> PathBuf {
|
|
|
1054
1054
|
|
|
1055
1055
|
fn role_doc(name: &str, role: &str) -> String {
|
|
1056
1056
|
format!(
|
|
1057
|
-
"---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{role}.\n"
|
|
1057
|
+
"---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\n{role}.\n"
|
|
1058
1058
|
)
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
@@ -337,7 +337,6 @@ pub(super) fn add_agent_with_transport_at_paths(
|
|
|
337
337
|
)));
|
|
338
338
|
}
|
|
339
339
|
inject_agent_into_spec(&mut spec, compiled.agent, &compiled.id)?;
|
|
340
|
-
let safety = effective_runtime_config(&spec)?;
|
|
341
340
|
// E5 spec 迁移:重编译的 spec 原子写到 .team/runtime/<team_key>/(不落用户目录 team_dir)。
|
|
342
341
|
let spec_path = crate::model::paths::runtime_spec_path(run_workspace, &canonical_team_key);
|
|
343
342
|
// E42 (0.3.24 P0): capture pre-write bytes for atomic rollback. If anything
|
|
@@ -362,7 +361,6 @@ pub(super) fn add_agent_with_transport_at_paths(
|
|
|
362
361
|
agent_id,
|
|
363
362
|
&meta,
|
|
364
363
|
role_file_path,
|
|
365
|
-
&safety,
|
|
366
364
|
) {
|
|
367
365
|
rollback_add_agent_atomic(
|
|
368
366
|
run_workspace,
|
|
@@ -116,7 +116,6 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
116
116
|
agent_id: &AgentId,
|
|
117
117
|
meta: &Value,
|
|
118
118
|
dynamic_role_file: &Path,
|
|
119
|
-
safety: &DangerousApproval,
|
|
120
119
|
) -> Result<(), LifecycleError> {
|
|
121
120
|
let mut state =
|
|
122
121
|
crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
@@ -195,7 +194,13 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
195
194
|
}
|
|
196
195
|
}
|
|
197
196
|
if let Some(obj) = entry.as_object_mut() {
|
|
198
|
-
|
|
197
|
+
// 0.5.66 bypass 单源:policy 从 role 文档字段派生,不再用全队 `DangerousApproval`。
|
|
198
|
+
let meta_provider = meta
|
|
199
|
+
.get("provider")
|
|
200
|
+
.and_then(Value::as_str)
|
|
201
|
+
.and_then(crate::provider::wire::parse_provider)
|
|
202
|
+
.unwrap_or(Provider::Codex);
|
|
203
|
+
persist_effective_approval_policy_from_yaml_agent(obj, meta, meta_provider);
|
|
199
204
|
}
|
|
200
205
|
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
201
206
|
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|
|
@@ -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
|
+
}
|