@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
|
@@ -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):
|
|
@@ -405,9 +407,9 @@ pub(super) fn spawn_agent_window(
|
|
|
405
407
|
|
|
406
408
|
// 0.5.39 Slice 2 (tmux-server-death-locate §7 Slice 2): route the
|
|
407
409
|
// primary worker spawn through the worker shell wrapper so provider
|
|
408
|
-
// exit leaves the pane at an
|
|
409
|
-
//
|
|
410
|
-
//
|
|
410
|
+
// exit leaves the pane at an inert sh tail with an explicit exit marker
|
|
411
|
+
// instead of collapsing the pane into `[exited]`. The inert tail does
|
|
412
|
+
// not read pane stdin. The split path stays
|
|
411
413
|
// on plain spawn_split — split panes are display overlays, not the
|
|
412
414
|
// primary worker process.
|
|
413
415
|
let provider_label = crate::provider::wire::command_name(provider);
|
|
@@ -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,
|
|
@@ -270,7 +270,6 @@ fn restart_with_selected_team_and_transport(
|
|
|
270
270
|
let spec_workspace = runtime_spec.parent().ok_or_else(|| {
|
|
271
271
|
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
272
272
|
})?;
|
|
273
|
-
let safety = crate::lifecycle::launch::effective_runtime_config(&spec)?;
|
|
274
273
|
// Bug 1 (0.4.2 P0): the rebuilt spec is the single source of truth for the
|
|
275
274
|
// active roster. Any agent in state.agents that is NOT in the rebuilt
|
|
276
275
|
// spec is a stale leftover (role doc was deleted between sessions) and
|
|
@@ -526,7 +525,6 @@ fn restart_with_selected_team_and_transport(
|
|
|
526
525
|
&selected.team_key,
|
|
527
526
|
&session_name,
|
|
528
527
|
transport,
|
|
529
|
-
&safety,
|
|
530
528
|
tmux_endpoint_source,
|
|
531
529
|
&state,
|
|
532
530
|
)
|
|
@@ -551,12 +549,25 @@ fn restart_with_selected_team_and_transport(
|
|
|
551
549
|
if let Some(parallel_result) = parallel_result {
|
|
552
550
|
match parallel_result {
|
|
553
551
|
Ok(pspawn) => {
|
|
552
|
+
let raw_agent = state
|
|
553
|
+
.get("agents")
|
|
554
|
+
.and_then(|v| v.get(decision.agent_id.as_str()))
|
|
555
|
+
.cloned()
|
|
556
|
+
.unwrap_or(serde_json::Value::Null);
|
|
557
|
+
let rehydrated = rehydrate_agent_command_context_from_spec(
|
|
558
|
+
spec_workspace,
|
|
559
|
+
&decision.agent_id,
|
|
560
|
+
&raw_agent,
|
|
561
|
+
);
|
|
562
|
+
let launched_bypass = matches!(
|
|
563
|
+
rehydrated.get("dangerously_skip_permissions"),
|
|
564
|
+
Some(serde_json::Value::Bool(true))
|
|
565
|
+
);
|
|
554
566
|
apply_marked_respawn(
|
|
555
567
|
&selected.run_workspace,
|
|
556
568
|
&selected.team_key,
|
|
557
569
|
&mut state,
|
|
558
570
|
transport,
|
|
559
|
-
&safety,
|
|
560
571
|
&phase_timer,
|
|
561
572
|
decision,
|
|
562
573
|
&pspawn.spawn,
|
|
@@ -565,6 +576,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
565
576
|
&mut successful_agents,
|
|
566
577
|
&mut failed_agents,
|
|
567
578
|
&mut fatal_resume_failure,
|
|
579
|
+
launched_bypass,
|
|
568
580
|
);
|
|
569
581
|
}
|
|
570
582
|
Err(error) => {
|
|
@@ -709,7 +721,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
709
721
|
session_id,
|
|
710
722
|
session_live,
|
|
711
723
|
transport,
|
|
712
|
-
|
|
724
|
+
None,
|
|
713
725
|
layout_placement.as_ref(),
|
|
714
726
|
None,
|
|
715
727
|
tmux_endpoint_source,
|
|
@@ -737,6 +749,10 @@ fn restart_with_selected_team_and_transport(
|
|
|
737
749
|
}
|
|
738
750
|
};
|
|
739
751
|
let verify_start = std::time::Instant::now();
|
|
752
|
+
let launched_bypass = matches!(
|
|
753
|
+
agent.get("dangerously_skip_permissions"),
|
|
754
|
+
Some(serde_json::Value::Bool(true))
|
|
755
|
+
);
|
|
740
756
|
if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
|
|
741
757
|
.and_then(|_| {
|
|
742
758
|
mark_agent_respawned(
|
|
@@ -745,7 +761,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
745
761
|
decision.restart_mode,
|
|
746
762
|
&spawn,
|
|
747
763
|
transport,
|
|
748
|
-
|
|
764
|
+
launched_bypass,
|
|
749
765
|
)
|
|
750
766
|
})
|
|
751
767
|
{
|
|
@@ -787,7 +803,16 @@ fn restart_with_selected_team_and_transport(
|
|
|
787
803
|
.and_then(|agents| agents.get_mut(decision.agent_id.as_str()))
|
|
788
804
|
.and_then(serde_json::Value::as_object_mut)
|
|
789
805
|
{
|
|
790
|
-
|
|
806
|
+
// 0.5.66 bypass 单源:policy 从 agent 行字段派生。
|
|
807
|
+
let agent_provider = agent
|
|
808
|
+
.get("provider")
|
|
809
|
+
.and_then(serde_json::Value::as_str)
|
|
810
|
+
.and_then(crate::provider::wire::parse_canonical_provider)
|
|
811
|
+
.unwrap_or(crate::model::enums::Provider::Codex);
|
|
812
|
+
crate::lifecycle::launch::persist_effective_approval_policy_from_agent(
|
|
813
|
+
agent,
|
|
814
|
+
agent_provider,
|
|
815
|
+
);
|
|
791
816
|
}
|
|
792
817
|
// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
|
|
793
818
|
// pair the state-side activity clear with a DB `agent_health` clear so
|
|
@@ -1958,7 +1983,7 @@ fn mark_agent_respawned(
|
|
|
1958
1983
|
restart_mode: StartMode,
|
|
1959
1984
|
spawn: &SpawnedAgentWindow,
|
|
1960
1985
|
transport: &dyn crate::transport::Transport,
|
|
1961
|
-
|
|
1986
|
+
launched_bypass: bool,
|
|
1962
1987
|
) -> Result<(), LifecycleError> {
|
|
1963
1988
|
let Some(agent) = state
|
|
1964
1989
|
.get_mut("agents")
|
|
@@ -2081,7 +2106,18 @@ fn mark_agent_respawned(
|
|
|
2081
2106
|
}
|
|
2082
2107
|
}
|
|
2083
2108
|
crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
|
|
2084
|
-
|
|
2109
|
+
// 0.5.66 bypass 单源:policy 从 agent 行字段派生,不再用全队 `DangerousApproval`。
|
|
2110
|
+
let agent_provider = agent
|
|
2111
|
+
.get("provider")
|
|
2112
|
+
.and_then(serde_json::Value::as_str)
|
|
2113
|
+
.and_then(crate::provider::wire::parse_canonical_provider)
|
|
2114
|
+
.unwrap_or(crate::model::enums::Provider::Codex);
|
|
2115
|
+
crate::lifecycle::launch::persist_effective_approval_policy_from_agent(agent, agent_provider);
|
|
2116
|
+
// 0.5.66 bypass 单源 §2.6:fresh spawn(restart)记录运行面 bypass 值。
|
|
2117
|
+
agent.insert(
|
|
2118
|
+
"as_launched_dangerously_skip_permissions".to_string(),
|
|
2119
|
+
serde_json::json!(launched_bypass),
|
|
2120
|
+
);
|
|
2085
2121
|
if let Some(placement) = spawn.layout_placement.as_ref() {
|
|
2086
2122
|
agent.insert(
|
|
2087
2123
|
"layout_window".to_string(),
|
|
@@ -2510,7 +2546,6 @@ fn run_bounded_parallel_worker_spawns(
|
|
|
2510
2546
|
team_key: &str,
|
|
2511
2547
|
session_name: &SessionName,
|
|
2512
2548
|
transport: &(dyn crate::transport::Transport),
|
|
2513
|
-
safety: &DangerousApproval,
|
|
2514
2549
|
tmux_endpoint_source: Option<&str>,
|
|
2515
2550
|
state: &serde_json::Value,
|
|
2516
2551
|
) -> Vec<Option<Result<ParallelSpawnResult, String>>> {
|
|
@@ -2589,7 +2624,7 @@ fn run_bounded_parallel_worker_spawns(
|
|
|
2589
2624
|
first_input.session_id.as_ref(),
|
|
2590
2625
|
first_session_live,
|
|
2591
2626
|
transport,
|
|
2592
|
-
|
|
2627
|
+
None,
|
|
2593
2628
|
first_input.layout_placement.as_ref(),
|
|
2594
2629
|
None,
|
|
2595
2630
|
tmux_endpoint_source,
|
|
@@ -2673,7 +2708,7 @@ fn run_bounded_parallel_worker_spawns(
|
|
|
2673
2708
|
input.session_id.as_ref(),
|
|
2674
2709
|
/* into_existing_session */ true,
|
|
2675
2710
|
transport,
|
|
2676
|
-
|
|
2711
|
+
None,
|
|
2677
2712
|
input.layout_placement.as_ref(),
|
|
2678
2713
|
None,
|
|
2679
2714
|
tmux_endpoint_source,
|
|
@@ -2716,7 +2751,6 @@ fn apply_marked_respawn(
|
|
|
2716
2751
|
team_key: &str,
|
|
2717
2752
|
state: &mut serde_json::Value,
|
|
2718
2753
|
transport: &(dyn crate::transport::Transport),
|
|
2719
|
-
safety: &DangerousApproval,
|
|
2720
2754
|
phase_timer: &RestartPhaseTimer,
|
|
2721
2755
|
decision: &RestartedAgent,
|
|
2722
2756
|
spawn: &SpawnedAgentWindow,
|
|
@@ -2725,6 +2759,7 @@ fn apply_marked_respawn(
|
|
|
2725
2759
|
successful_agents: &mut Vec<RestartedAgent>,
|
|
2726
2760
|
failed_agents: &mut Vec<RestartFailedAgent>,
|
|
2727
2761
|
fatal_resume_failure: &mut bool,
|
|
2762
|
+
launched_bypass: bool,
|
|
2728
2763
|
) {
|
|
2729
2764
|
let verify_start = std::time::Instant::now();
|
|
2730
2765
|
if let Err(error) =
|
|
@@ -2735,7 +2770,7 @@ fn apply_marked_respawn(
|
|
|
2735
2770
|
decision.restart_mode,
|
|
2736
2771
|
spawn,
|
|
2737
2772
|
transport,
|
|
2738
|
-
|
|
2773
|
+
launched_bypass,
|
|
2739
2774
|
)
|
|
2740
2775
|
})
|
|
2741
2776
|
{
|
|
@@ -2776,7 +2811,16 @@ fn apply_marked_respawn(
|
|
|
2776
2811
|
.and_then(|agents| agents.get_mut(decision.agent_id.as_str()))
|
|
2777
2812
|
.and_then(serde_json::Value::as_object_mut)
|
|
2778
2813
|
{
|
|
2779
|
-
|
|
2814
|
+
// 0.5.66 bypass 单源:policy 从 agent 行字段派生。
|
|
2815
|
+
let agent_provider = agent
|
|
2816
|
+
.get("provider")
|
|
2817
|
+
.and_then(serde_json::Value::as_str)
|
|
2818
|
+
.and_then(crate::provider::wire::parse_canonical_provider)
|
|
2819
|
+
.unwrap_or(crate::model::enums::Provider::Codex);
|
|
2820
|
+
crate::lifecycle::launch::persist_effective_approval_policy_from_agent(
|
|
2821
|
+
agent,
|
|
2822
|
+
agent_provider,
|
|
2823
|
+
);
|
|
2780
2824
|
}
|
|
2781
2825
|
let _ = crate::db::agent_health_capture::clear_agent_health_observation(
|
|
2782
2826
|
run_workspace,
|
|
@@ -3728,7 +3772,7 @@ tasks:
|
|
|
3728
3772
|
StartMode::Resumed,
|
|
3729
3773
|
&spawn,
|
|
3730
3774
|
&RespawnEpochTransport,
|
|
3731
|
-
|
|
3775
|
+
false,
|
|
3732
3776
|
)
|
|
3733
3777
|
.expect("mark respawned");
|
|
3734
3778
|
|
|
@@ -393,7 +393,7 @@ fn write_min_team(ws: &Path, name: &str) -> PathBuf {
|
|
|
393
393
|
.unwrap();
|
|
394
394
|
std::fs::write(
|
|
395
395
|
team.join("agents/worker_a.md"),
|
|
396
|
-
"---\nname: worker_a\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker.\n",
|
|
396
|
+
"---\nname: worker_a\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nWorker.\n",
|
|
397
397
|
)
|
|
398
398
|
.unwrap();
|
|
399
399
|
team
|
|
@@ -330,7 +330,7 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
|
|
|
330
330
|
let role_file = team_dir.join("w2-role.md");
|
|
331
331
|
std::fs::write(
|
|
332
332
|
&role_file,
|
|
333
|
-
"---\nname: w2\nrole: Worker Two\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker two.\n",
|
|
333
|
+
"---\nname: w2\nrole: Worker Two\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nWorker two.\n",
|
|
334
334
|
)
|
|
335
335
|
.unwrap();
|
|
336
336
|
let transport = OfflineTransport::new().with_session_present(true);
|
|
@@ -518,9 +518,9 @@ if [ "$track" != 1 ]; then
|
|
|
518
518
|
fi
|
|
519
519
|
# 0.5.39 Slice 2: worker spawn now goes through the worker shell
|
|
520
520
|
# wrapper (tmux-server-death-locate §7 Slice 2), which embeds a
|
|
521
|
-
# printf format literal containing "\n" bytes so the pane
|
|
522
|
-
#
|
|
523
|
-
#
|
|
521
|
+
# printf format literal containing "\n" bytes so the pane retains an
|
|
522
|
+
# inert sh tail with an explicit exit marker instead of collapsing to
|
|
523
|
+
# `[exited]`. Those embedded newlines land inside the
|
|
524
524
|
# tmux argv payload, so a naive `printf '%s\n' "$*"` writes multiple
|
|
525
525
|
# lines per tmux invocation and downstream `raw.lines()` scans see
|
|
526
526
|
# fake "argv" lines that carry only the marker text. Escape newlines
|
|
@@ -528,7 +528,7 @@ fn write_team(workspace: &Path, fixture: &TeamFixture<'_>) -> PathBuf {
|
|
|
528
528
|
std::fs::write(
|
|
529
529
|
team.join("agents").join(format!("{}.md", agent.id)),
|
|
530
530
|
format!(
|
|
531
|
-
"---\nname: {}\nrole: {}\nprovider: {}\n{model}auth_mode: {}\n{profile}
|
|
531
|
+
"---\nname: {}\nrole: {}\nprovider: {}\n{model}auth_mode: {}\n{profile}dangerously_skip_permissions: false\ntools:\n{tools}---\n\n{}\n",
|
|
532
532
|
agent.id, agent.role, agent.provider, agent.auth_mode, agent.body
|
|
533
533
|
),
|
|
534
534
|
)
|
|
@@ -152,14 +152,14 @@ fn write_claude_profile_team(ws: &Path, team_key: &str, agent_id: &str) -> PathB
|
|
|
152
152
|
std::fs::write(
|
|
153
153
|
team.join("TEAM.md"),
|
|
154
154
|
format!(
|
|
155
|
-
"---\nname: {team_key}\nobjective: Claude compatible config contract.\nprovider: claude\nauth_mode: compatible_api\n---\n\nTeam.\n"
|
|
155
|
+
"---\nname: {team_key}\nobjective: Claude compatible config contract.\nprovider: claude\nauth_mode: compatible_api\ndangerously_skip_permissions: false\n---\n\nTeam.\n"
|
|
156
156
|
),
|
|
157
157
|
)
|
|
158
158
|
.unwrap();
|
|
159
159
|
std::fs::write(
|
|
160
160
|
team.join("agents").join(format!("{agent_id}.md")),
|
|
161
161
|
format!(
|
|
162
|
-
"---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: compatible_api\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
|
|
162
|
+
"---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: compatible_api\ndangerously_skip_permissions: false\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
|
|
163
163
|
),
|
|
164
164
|
)
|
|
165
165
|
.unwrap();
|
|
@@ -218,14 +218,14 @@ fn write_claude_profile_team(ws: &Path, team_key: &str, agent_id: &str) -> PathB
|
|
|
218
218
|
std::fs::write(
|
|
219
219
|
team.join("TEAM.md"),
|
|
220
220
|
format!(
|
|
221
|
-
"---\nname: {team_key}\nobjective: Claude compatible profile launch contract.\nprovider: claude\nauth_mode: compatible_api\n---\n\nTeam.\n"
|
|
221
|
+
"---\nname: {team_key}\nobjective: Claude compatible profile launch contract.\nprovider: claude\nauth_mode: compatible_api\ndangerously_skip_permissions: false\n---\n\nTeam.\n"
|
|
222
222
|
),
|
|
223
223
|
)
|
|
224
224
|
.unwrap();
|
|
225
225
|
std::fs::write(
|
|
226
226
|
team.join("agents").join(format!("{agent_id}.md")),
|
|
227
227
|
format!(
|
|
228
|
-
"---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: compatible_api\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
|
|
228
|
+
"---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: compatible_api\ndangerously_skip_permissions: false\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
|
|
229
229
|
),
|
|
230
230
|
)
|
|
231
231
|
.unwrap();
|
|
@@ -247,14 +247,14 @@ fn write_claude_subscription_profile_team(ws: &Path, team_key: &str, agent_id: &
|
|
|
247
247
|
std::fs::write(
|
|
248
248
|
team.join("TEAM.md"),
|
|
249
249
|
format!(
|
|
250
|
-
"---\nname: {team_key}\nobjective: Claude subscription profile launch contract.\nprovider: claude\nauth_mode: subscription\n---\n\nTeam.\n"
|
|
250
|
+
"---\nname: {team_key}\nobjective: Claude subscription profile launch contract.\nprovider: claude\nauth_mode: subscription\ndangerously_skip_permissions: false\n---\n\nTeam.\n"
|
|
251
251
|
),
|
|
252
252
|
)
|
|
253
253
|
.unwrap();
|
|
254
254
|
std::fs::write(
|
|
255
255
|
team.join("agents").join(format!("{agent_id}.md")),
|
|
256
256
|
format!(
|
|
257
|
-
"---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: subscription\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
|
|
257
|
+
"---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: subscription\ndangerously_skip_permissions: false\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
|
|
258
258
|
),
|
|
259
259
|
)
|
|
260
260
|
.unwrap();
|
|
@@ -304,7 +304,7 @@ fn write_team_docs(workspace: &Path, provider: &str) {
|
|
|
304
304
|
std::fs::write(
|
|
305
305
|
workspace.join("agents").join(format!("{SOURCE}.md")),
|
|
306
306
|
format!(
|
|
307
|
-
"---\nname: {SOURCE}\nrole: {SOURCE}\nprovider: {provider}\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{SOURCE} body.\n"
|
|
307
|
+
"---\nname: {SOURCE}\nrole: {SOURCE}\nprovider: {provider}\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\n{SOURCE} body.\n"
|
|
308
308
|
),
|
|
309
309
|
)
|
|
310
310
|
.expect("write source role doc");
|
|
@@ -323,7 +323,7 @@ fn write_copilot_team_docs(workspace: &Path) -> PathBuf {
|
|
|
323
323
|
std::fs::write(
|
|
324
324
|
team_dir.join("agents").join(format!("{SOURCE}.md")),
|
|
325
325
|
format!(
|
|
326
|
-
"---\nname: {SOURCE}\nrole: Copilot Worker\nprovider: copilot\nmodel: gpt-5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nCopilot source body.\n"
|
|
326
|
+
"---\nname: {SOURCE}\nrole: Copilot Worker\nprovider: copilot\nmodel: gpt-5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nCopilot source body.\n"
|
|
327
327
|
),
|
|
328
328
|
)
|
|
329
329
|
.expect("write source role doc");
|
|
@@ -502,20 +502,16 @@ fn r8_copilot_fork_is_no_longer_capability_refused() {
|
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
/// R7 permission-clamp GUARDRAIL — a fork's effective approval policy is derived
|
|
505
|
-
/// from the LEADER and never escalates above it (MUST-16).
|
|
506
|
-
///
|
|
507
|
-
///
|
|
508
|
-
///
|
|
509
|
-
/// leader trips it. The true escalation scenario (a source declaring capability
|
|
510
|
-
/// above the leader) is a real-machine concern.
|
|
505
|
+
/// from the LEADER and never escalates above it (MUST-16). 0.5.66 bypass 单源:
|
|
506
|
+
/// fork 的 approval policy 由源角色的 `dangerously_skip_permissions` 派生
|
|
507
|
+
/// (本 fixture = false → `source=disabled`),不再依赖 leader argv 继承。任何
|
|
508
|
+
/// fork 都不得出现 `source=leader_process`(旧模型已删)或 capability 高于源。
|
|
511
509
|
#[test]
|
|
512
510
|
fn r7_fork_effective_permissions_are_clamped_to_leader_guardrail() {
|
|
513
511
|
// Claude source + success-path backing shim so the fork COMPLETES and a NEW
|
|
514
|
-
// row exists to inspect — no vacuous pass on a refused fork. The
|
|
515
|
-
//
|
|
516
|
-
// `
|
|
517
|
-
// actually detected, so the fixture must supply one instead of borrowing
|
|
518
|
-
// the host's.
|
|
512
|
+
// row exists to inspect — no vacuous pass on a refused fork. The source role
|
|
513
|
+
// declares `dangerously_skip_permissions: false`, so the fork's policy must
|
|
514
|
+
// resolve to `disabled` (fail-closed) — the new single-source model.
|
|
519
515
|
let case = Case::start("cf-r7");
|
|
520
516
|
let fork = case.fork(NEW);
|
|
521
517
|
assert_eq!(
|
|
@@ -538,11 +534,17 @@ fn r7_fork_effective_permissions_are_clamped_to_leader_guardrail() {
|
|
|
538
534
|
let above = policy
|
|
539
535
|
.get("worker_capability_above_leader")
|
|
540
536
|
.and_then(Value::as_bool);
|
|
541
|
-
|
|
537
|
+
assert_ne!(
|
|
542
538
|
source,
|
|
543
539
|
Some("leader_process"),
|
|
544
|
-
"
|
|
545
|
-
|
|
540
|
+
"0.5.66: fork policy must never be leader_process (old ancestry model removed); \
|
|
541
|
+
policy={policy}"
|
|
542
|
+
);
|
|
543
|
+
assert_eq!(
|
|
544
|
+
source,
|
|
545
|
+
Some("disabled"),
|
|
546
|
+
"guardrail: a fork's approval policy derives from the role's dangerously_skip_permissions \
|
|
547
|
+
(false → disabled), never raw-cloned escalation; MUST-16. policy={policy}"
|
|
546
548
|
);
|
|
547
549
|
assert_ne!(
|
|
548
550
|
above,
|
|
@@ -552,18 +554,9 @@ fn r7_fork_effective_permissions_are_clamped_to_leader_guardrail() {
|
|
|
552
554
|
);
|
|
553
555
|
}
|
|
554
556
|
|
|
555
|
-
/// R7b reverse positive control —
|
|
556
|
-
///
|
|
557
|
-
///
|
|
558
|
-
/// clamp (a clean runner == no dangerous ancestor) and proves r7 does not pass
|
|
559
|
-
/// by constant behavior: the same fixture flips leader_process↔disabled purely
|
|
560
|
-
/// on the pinned seam. Mirrors the deterministic product contract
|
|
561
|
-
/// `lifecycle/tests/core.rs` (clean mocked ancestry → Disabled).
|
|
562
|
-
///
|
|
563
|
-
/// NOTE deliberately NOT asserted with the seam UNSET: unpinned, the policy is
|
|
564
|
-
/// a function of the host process tree (the exact non-hermeticity this
|
|
565
|
-
/// revision removes), so an unset-seam assertion would be red on any host
|
|
566
|
-
/// whose own leader runs dangerously.
|
|
557
|
+
/// R7b reverse positive control — same source role (dangerously_skip_permissions:
|
|
558
|
+
/// false) must also resolve to `source=disabled` under any ancestry seam. 0.5.66
|
|
559
|
+
/// 起 policy 与 leader argv 无关:source 恒 `disabled`,fork 恒不升级(MUST-16)。
|
|
567
560
|
#[test]
|
|
568
561
|
fn r7b_fork_policy_under_restricted_leader_ancestry_is_disabled() {
|
|
569
562
|
let case = Case::start_with_ancestry("cf-r7b", RESTRICTED_LEADER_ARGV);
|
|
@@ -587,16 +580,15 @@ fn r7b_fork_policy_under_restricted_leader_ancestry_is_disabled() {
|
|
|
587
580
|
assert_eq!(
|
|
588
581
|
policy.get("source").and_then(Value::as_str),
|
|
589
582
|
Some("disabled"),
|
|
590
|
-
"
|
|
591
|
-
|
|
583
|
+
"fail-closed: source role declares dangerously_skip_permissions=false → fork policy must \
|
|
584
|
+
be disabled; policy={policy}"
|
|
592
585
|
);
|
|
593
586
|
assert_ne!(
|
|
594
587
|
policy
|
|
595
588
|
.get("worker_capability_above_leader")
|
|
596
589
|
.and_then(Value::as_bool),
|
|
597
590
|
Some(true),
|
|
598
|
-
"
|
|
599
|
-
policy={policy}"
|
|
591
|
+
"a fork must never yield a capability above the source; MUST-16. policy={policy}"
|
|
600
592
|
);
|
|
601
593
|
}
|
|
602
594
|
|
|
@@ -246,7 +246,7 @@ fn fixture(tag: &str, team_mode: Option<&str>) -> PathBuf {
|
|
|
246
246
|
std::fs::write(
|
|
247
247
|
root.join("agents").join(format!("worker_{id}.md")),
|
|
248
248
|
format!(
|
|
249
|
-
"---\nname: worker_{id}\nrole: Communication Worker\nprovider: claude\ncommunication_mode: {id}\ntools:\n - mcp_team\n---\n\nROLE BODY SENTINEL {id}\n"
|
|
249
|
+
"---\nname: worker_{id}\nrole: Communication Worker\nprovider: claude\ndangerously_skip_permissions: false\ncommunication_mode: {id}\ntools:\n - mcp_team\n---\n\nROLE BODY SENTINEL {id}\n"
|
|
250
250
|
),
|
|
251
251
|
)
|
|
252
252
|
.unwrap();
|
|
@@ -265,7 +265,7 @@ fn default_fixture(tag: &str) -> PathBuf {
|
|
|
265
265
|
}
|
|
266
266
|
std::fs::write(
|
|
267
267
|
root.join("agents").join("worker_default.md"),
|
|
268
|
-
"---\nname: worker_default\nrole: Communication Worker\nprovider: claude\ntools:\n - mcp_team\n---\n\nROLE BODY SENTINEL default\n",
|
|
268
|
+
"---\nname: worker_default\nrole: Communication Worker\nprovider: claude\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nROLE BODY SENTINEL default\n",
|
|
269
269
|
)
|
|
270
270
|
.unwrap();
|
|
271
271
|
root
|