@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
|
@@ -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
|
+
}
|
|
@@ -181,6 +181,31 @@ pub(crate) fn start_agent_at_paths(
|
|
|
181
181
|
);
|
|
182
182
|
}
|
|
183
183
|
let agent_live = agent_live && (!has_collision || noop_pane.is_some());
|
|
184
|
+
// 0.5.66 bypass 单源 §2.6 — dangerously_skip_permissions drift 挡板:
|
|
185
|
+
// Noop 路径不 spawn,若声明面(角色 md / spec)与运行面(as-launched)不一致,
|
|
186
|
+
// 拒 Noop 报明,防"改了 md 却以为生效、进程仍带旧 bypass 跑着"的暗门。
|
|
187
|
+
let file_bypass = agent
|
|
188
|
+
.get("dangerously_skip_permissions")
|
|
189
|
+
.and_then(serde_json::Value::as_bool);
|
|
190
|
+
let launched_bypass = raw_agent
|
|
191
|
+
.get("as_launched_dangerously_skip_permissions")
|
|
192
|
+
.and_then(serde_json::Value::as_bool);
|
|
193
|
+
if !force && agent_live && check_bypass_drift(file_bypass, launched_bypass) {
|
|
194
|
+
// 0.5.66 bypass 单源 §2.7:Noop drift 拒也写审计事件。
|
|
195
|
+
let _ = crate::event_log::EventLog::new(workspace).write(
|
|
196
|
+
"worker.spawn_dangerously_skip_permissions_drift_denied",
|
|
197
|
+
serde_json::json!({
|
|
198
|
+
"agent_id": agent_id.as_str(),
|
|
199
|
+
"file_value": file_bypass,
|
|
200
|
+
"launched_value": launched_bypass,
|
|
201
|
+
"action": "noop_denied_require_remove_add_or_force",
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
205
|
+
"dangerously_skip_permissions drift for agent {agent_id}: file={file_bypass:?} but running process launched with {launched_bypass:?}. \
|
|
206
|
+
Run `remove-agent {agent_id}` then `add-agent {agent_id}` to apply, or `restart --force {agent_id}` to accept fresh spawn."
|
|
207
|
+
)));
|
|
208
|
+
}
|
|
184
209
|
if !force && agent_live {
|
|
185
210
|
let old_binding = pane_binding_snapshot(&raw_agent);
|
|
186
211
|
let refreshed_binding = noop_pane.as_ref().map(pane_binding_from_live);
|
|
@@ -257,7 +282,10 @@ pub(crate) fn start_agent_at_paths(
|
|
|
257
282
|
};
|
|
258
283
|
let into_existing_session =
|
|
259
284
|
session_live_or_default(transport, &session_name, session_name_present(&state));
|
|
260
|
-
let safety = crate::lifecycle::launch::
|
|
285
|
+
let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn_json(
|
|
286
|
+
&agent,
|
|
287
|
+
provider,
|
|
288
|
+
)?;
|
|
261
289
|
let layout_placement = if adaptive_layout {
|
|
262
290
|
crate::lifecycle::launch::adaptive_existing_placement_for_agent(
|
|
263
291
|
&state,
|
|
@@ -323,6 +351,30 @@ pub(crate) fn start_agent_at_paths(
|
|
|
323
351
|
&safety,
|
|
324
352
|
start_mode,
|
|
325
353
|
)?;
|
|
354
|
+
// 0.5.66 bypass 单源 §2.7:启动即带 bypass → 审计留痕。
|
|
355
|
+
if safety.enabled {
|
|
356
|
+
let state_agent = state
|
|
357
|
+
.get("agents")
|
|
358
|
+
.and_then(|v| v.get(agent_id.as_str()));
|
|
359
|
+
let _ = crate::event_log::EventLog::new(workspace).write(
|
|
360
|
+
"worker.spawn_dangerously_skip_permissions",
|
|
361
|
+
serde_json::json!({
|
|
362
|
+
"agent_id": agent_id.as_str(),
|
|
363
|
+
"role": state_agent
|
|
364
|
+
.and_then(|a| a.get("role"))
|
|
365
|
+
.and_then(serde_json::Value::as_str)
|
|
366
|
+
.unwrap_or_default(),
|
|
367
|
+
"provider": state_agent
|
|
368
|
+
.and_then(|a| a.get("provider"))
|
|
369
|
+
.and_then(serde_json::Value::as_str)
|
|
370
|
+
.unwrap_or_default(),
|
|
371
|
+
"trigger": "start-agent",
|
|
372
|
+
"spec_source_path": crate::lifecycle::launch::agent_id_spec_source_path(
|
|
373
|
+
workspace, agent_id.as_str(),
|
|
374
|
+
),
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
326
378
|
// **0.3.24 add-agent socket drift fix**: keep `state.tmux_endpoint` /
|
|
327
379
|
// `state.tmux_socket` synchronized with the transport actually used for the
|
|
328
380
|
// spawn. Without this, add-agent / fork-agent could spawn to a socket that
|
|
@@ -1078,6 +1130,12 @@ fn mark_agent_started(
|
|
|
1078
1130
|
// lifecycle/topology fields so absence == UNKNOWN until the next
|
|
1079
1131
|
// coordinator tick or pane fallback produces a post-spawn observation.
|
|
1080
1132
|
clear_agent_runtime_activity_observation(agent);
|
|
1133
|
+
// 0.5.66 bypass 单源 §2.6:fresh spawn 那一刻记录运行面 bypass 值,
|
|
1134
|
+
// 供下一次 restart 的 Noop drift 挡板比较。
|
|
1135
|
+
agent.insert(
|
|
1136
|
+
"as_launched_dangerously_skip_permissions".to_string(),
|
|
1137
|
+
serde_json::json!(safety.enabled),
|
|
1138
|
+
);
|
|
1081
1139
|
// S1-CAPTURE-001 (0.4.8, CR M3 provider-agnostic): on a Fresh /
|
|
1082
1140
|
// FreshAfterMissingRollout start, the prior session's authoritative
|
|
1083
1141
|
// capture tuple MUST be cleared before persist_command_plan_state
|
|
@@ -1609,7 +1667,6 @@ fn write_start_agent_start_event(
|
|
|
1609
1667
|
let adapter = crate::provider::get_adapter(provider);
|
|
1610
1668
|
// Contract C / F6.4: event log must record the same context-aware argv that the
|
|
1611
1669
|
// actual spawn used — so the role/tools/MCP context appears in `start_agent.agent_start`.
|
|
1612
|
-
let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
|
|
1613
1670
|
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
|
|
1614
1671
|
agent,
|
|
1615
1672
|
Some(agent_id.as_str()),
|
|
@@ -1620,7 +1677,6 @@ fn write_start_agent_start_event(
|
|
|
1620
1677
|
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
1621
1678
|
&command_agent,
|
|
1622
1679
|
provider,
|
|
1623
|
-
&safety,
|
|
1624
1680
|
)?;
|
|
1625
1681
|
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
1626
1682
|
let mcp_config = adapter
|
|
@@ -1831,6 +1887,17 @@ fn write_reset_complete_event(
|
|
|
1831
1887
|
Ok(())
|
|
1832
1888
|
}
|
|
1833
1889
|
|
|
1890
|
+
/// 0.5.66 bypass 单源 §2.6:声明面(file)与运行面(as-launched)是否 drift。
|
|
1891
|
+
/// 缺 as-launched(从未 spawn)= 视为未启动,不 drift(首次 Noop 无进程可比)。
|
|
1892
|
+
fn check_bypass_drift(file: Option<bool>, launched: Option<bool>) -> bool {
|
|
1893
|
+
match (file, launched) {
|
|
1894
|
+
(Some(file), Some(launched)) => file != launched,
|
|
1895
|
+
_ => false,
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
#[cfg(test)]
|
|
1900
|
+
|
|
1834
1901
|
#[cfg(test)]
|
|
1835
1902
|
mod tests {
|
|
1836
1903
|
use super::*;
|
|
@@ -1907,4 +1974,23 @@ mod tests {
|
|
|
1907
1974
|
"other-agent collision checks must also include tmux_socket"
|
|
1908
1975
|
);
|
|
1909
1976
|
}
|
|
1977
|
+
|
|
1978
|
+
// 0.5.66 bypass 单源 §4.1:Noop drift 检测——声明面与运行面不一致 → drift。
|
|
1979
|
+
#[test]
|
|
1980
|
+
fn test_restart_noop_denied_on_drift() {
|
|
1981
|
+
// file=true, launched=false → drift(改了 md 升 bypass 但进程没带)。
|
|
1982
|
+
assert!(check_bypass_drift(Some(true), Some(false)));
|
|
1983
|
+
// file=false, launched=true → drift(改了 md 撤 bypass 但进程还带着)。
|
|
1984
|
+
assert!(check_bypass_drift(Some(false), Some(true)));
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// 0.5.66 bypass 单源 §4.1:无 drift 或从未 spawn(as-launched 缺)→ 不拒。
|
|
1988
|
+
#[test]
|
|
1989
|
+
fn test_restart_fresh_spawn_after_drift_uses_file_value() {
|
|
1990
|
+
assert!(!check_bypass_drift(Some(true), Some(true)));
|
|
1991
|
+
assert!(!check_bypass_drift(Some(false), Some(false)));
|
|
1992
|
+
// 从未 spawn:as-launched 缺失 → 不 drift(首次 Noop 无进程可比)。
|
|
1993
|
+
assert!(!check_bypass_drift(Some(true), None));
|
|
1994
|
+
assert!(!check_bypass_drift(None, None));
|
|
1995
|
+
}
|
|
1910
1996
|
}
|
|
@@ -213,7 +213,10 @@ pub(super) fn spawn_agent_window(
|
|
|
213
213
|
let safety = if let Some(safety) = safety {
|
|
214
214
|
safety
|
|
215
215
|
} else {
|
|
216
|
-
detected_safety = crate::lifecycle::launch::
|
|
216
|
+
detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn_json(
|
|
217
|
+
agent,
|
|
218
|
+
provider,
|
|
219
|
+
)?;
|
|
217
220
|
&detected_safety
|
|
218
221
|
};
|
|
219
222
|
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
|
|
@@ -226,7 +229,6 @@ pub(super) fn spawn_agent_window(
|
|
|
226
229
|
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
227
230
|
&command_agent,
|
|
228
231
|
provider,
|
|
229
|
-
safety,
|
|
230
232
|
)?;
|
|
231
233
|
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
232
234
|
// owner_team_id resolution priority (Issue 2 fix):
|
|
@@ -591,6 +593,8 @@ fn merge_command_context_fields(
|
|
|
591
593
|
"effort",
|
|
592
594
|
"profile",
|
|
593
595
|
"permission_mode",
|
|
596
|
+
// 0.5.66 bypass 单源:rehydrate 同步合入,restart 的 safety 构造读到新值。
|
|
597
|
+
"dangerously_skip_permissions",
|
|
594
598
|
] {
|
|
595
599
|
if let Some(value) = spec_agent.get(field).and_then(yaml_value_to_json) {
|
|
596
600
|
obj.insert(field.to_string(), value);
|
|
@@ -799,13 +803,6 @@ pub(super) fn lifecycle_worker_tmux_backend_for_state(
|
|
|
799
803
|
.backend
|
|
800
804
|
}
|
|
801
805
|
|
|
802
|
-
pub(super) fn persist_effective_approval_policy_for_restart(
|
|
803
|
-
agent: &mut serde_json::Map<String, serde_json::Value>,
|
|
804
|
-
safety: &DangerousApproval,
|
|
805
|
-
) {
|
|
806
|
-
crate::lifecycle::launch::persist_effective_approval_policy(agent, safety);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
806
|
pub(super) fn save_restart_projected_state(
|
|
810
807
|
workspace: &Path,
|
|
811
808
|
state: &mut serde_json::Value,
|