diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 7461bf41fb..7e0a1d685d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -871,6 +871,10 @@ impl Session { .await } + /// Adds a prefix rule to the exec policy + /// + /// This mutates the in-memory execpolicy so the current conversation can use the new + /// prefix and persists the change in default.execpolicy so new conversations will also allow the new prefix. pub(crate) async fn persist_command_allow_prefix( &self, prefix: &[String], diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 44328de14a..7c96deea90 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -16,12 +16,13 @@ use codex_protocol::protocol::SandboxPolicy; use thiserror::Error; use tokio::fs; use tokio::sync::RwLock; +use tokio::task::spawn_blocking; use crate::bash::parse_shell_lc_plain_commands; use crate::features::Feature; use crate::features::Features; use crate::sandboxing::SandboxPermissions; -use crate::tools::sandboxing::ApprovalRequirement; +use crate::tools::sandboxing::ExecApprovalRequirement; const FORBIDDEN_REASON: &str = "execpolicy forbids this command"; const PROMPT_REASON: &str = "execpolicy requires approval for this command"; @@ -55,6 +56,9 @@ pub enum ExecPolicyUpdateError { #[error("failed to update execpolicy file {path}: {source}")] AppendRule { path: PathBuf, source: AmendError }, + #[error("failed to join blocking execpolicy update task: {source}")] + JoinBlockingTask { source: tokio::task::JoinError }, + #[error("failed to update in-memory execpolicy: {source}")] AddRule { #[from] @@ -114,17 +118,23 @@ pub(crate) async fn append_allow_prefix_rule_and_update( prefix: &[String], ) -> Result<(), ExecPolicyUpdateError> { let policy_path = default_policy_path(codex_home); - blocking_append_allow_prefix_rule(&policy_path, prefix).map_err(|source| { - ExecPolicyUpdateError::AppendRule { - path: policy_path, - source, - } + let prefix = prefix.to_vec(); + spawn_blocking({ + let policy_path = policy_path.clone(); + let prefix = prefix.clone(); + move || blocking_append_allow_prefix_rule(&policy_path, &prefix) + }) + .await + .map_err(|source| ExecPolicyUpdateError::JoinBlockingTask { source })? + .map_err(|source| ExecPolicyUpdateError::AppendRule { + path: policy_path, + source, })?; current_policy .write() .await - .add_prefix_rule(prefix, Decision::Allow)?; + .add_prefix_rule(&prefix, Decision::Allow)?; Ok(()) } @@ -132,23 +142,23 @@ pub(crate) async fn append_allow_prefix_rule_and_update( fn requirement_from_decision( decision: Decision, approval_policy: AskForApproval, -) -> ApprovalRequirement { +) -> ExecApprovalRequirement { match decision { - Decision::Forbidden => ApprovalRequirement::Forbidden { + Decision::Forbidden => ExecApprovalRequirement::Forbidden { reason: FORBIDDEN_REASON.to_string(), }, Decision::Prompt => { let reason = PROMPT_REASON.to_string(); if matches!(approval_policy, AskForApproval::Never) { - ApprovalRequirement::Forbidden { reason } + ExecApprovalRequirement::Forbidden { reason } } else { - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: Some(reason), allow_prefix: None, } } } - Decision::Allow => ApprovalRequirement::Skip { + Decision::Allow => ExecApprovalRequirement::Skip { bypass_sandbox: true, }, } @@ -170,17 +180,16 @@ fn allow_prefix_if_applicable( } } -pub(crate) async fn create_approval_requirement_for_command( +pub(crate) async fn create_exec_approval_requirement_for_command( exec_policy: &Arc>, features: &Features, command: &[String], approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy, sandbox_permissions: SandboxPermissions, -) -> ApprovalRequirement { +) -> ExecApprovalRequirement { let commands = parse_shell_lc_plain_commands(command).unwrap_or_else(|| vec![command.to_vec()]); - let policy = exec_policy.read().await; - let evaluation = policy.check_multiple(commands.iter()); + let evaluation = exec_policy.read().await.check_multiple(commands.iter()); match evaluation { Evaluation::Match { decision, .. } => requirement_from_decision(decision, approval_policy), @@ -191,12 +200,12 @@ pub(crate) async fn create_approval_requirement_for_command( command, sandbox_permissions, ) { - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: None, allow_prefix: allow_prefix_if_applicable(&commands, features), } } else { - ApprovalRequirement::Skip { + ExecApprovalRequirement::Skip { bypass_sandbox: false, } } @@ -349,7 +358,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") "rm -rf /tmp".to_string(), ]; - let requirement = create_approval_requirement_for_command( + let requirement = create_exec_approval_requirement_for_command( &policy, &Features::with_defaults(), &forbidden_script, @@ -361,14 +370,14 @@ prefix_rule(pattern=["rm"], decision="forbidden") assert_eq!( requirement, - ApprovalRequirement::Forbidden { + ExecApprovalRequirement::Forbidden { reason: FORBIDDEN_REASON.to_string() } ); } #[tokio::test] - async fn approval_requirement_prefers_execpolicy_match() { + async fn exec_approval_requirement_prefers_execpolicy_match() { let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#; let mut parser = PolicyParser::new(); parser @@ -377,7 +386,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") let policy = Arc::new(RwLock::new(parser.build())); let command = vec!["rm".to_string()]; - let requirement = create_approval_requirement_for_command( + let requirement = create_exec_approval_requirement_for_command( &policy, &Features::with_defaults(), &command, @@ -389,7 +398,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") assert_eq!( requirement, - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: Some(PROMPT_REASON.to_string()), allow_prefix: None, } @@ -397,7 +406,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") } #[tokio::test] - async fn approval_requirement_respects_approval_policy() { + async fn exec_approval_requirement_respects_approval_policy() { let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#; let mut parser = PolicyParser::new(); parser @@ -406,7 +415,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") let policy = Arc::new(RwLock::new(parser.build())); let command = vec!["rm".to_string()]; - let requirement = create_approval_requirement_for_command( + let requirement = create_exec_approval_requirement_for_command( &policy, &Features::with_defaults(), &command, @@ -418,18 +427,18 @@ prefix_rule(pattern=["rm"], decision="forbidden") assert_eq!( requirement, - ApprovalRequirement::Forbidden { + ExecApprovalRequirement::Forbidden { reason: PROMPT_REASON.to_string() } ); } #[tokio::test] - async fn approval_requirement_falls_back_to_heuristics() { - let command = vec!["python".to_string()]; + async fn exec_approval_requirement_falls_back_to_heuristics() { + let command = vec!["cargo".to_string(), "build".to_string()]; let empty_policy = Arc::new(RwLock::new(Policy::empty())); - let requirement = create_approval_requirement_for_command( + let requirement = create_exec_approval_requirement_for_command( &empty_policy, &Features::with_defaults(), &command, @@ -441,7 +450,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") assert_eq!( requirement, - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: None, allow_prefix: Some(command) } @@ -499,10 +508,10 @@ prefix_rule(pattern=["rm"], decision="forbidden") #[tokio::test] async fn allow_prefix_is_present_for_single_command_without_policy_match() { - let command = vec!["python".to_string()]; + let command = vec!["cargo".to_string(), "build".to_string()]; let empty_policy = Arc::new(RwLock::new(Policy::empty())); - let requirement = create_approval_requirement_for_command( + let requirement = create_exec_approval_requirement_for_command( &empty_policy, &Features::with_defaults(), &command, @@ -514,7 +523,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") assert_eq!( requirement, - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: None, allow_prefix: Some(command) } @@ -523,12 +532,12 @@ prefix_rule(pattern=["rm"], decision="forbidden") #[tokio::test] async fn allow_prefix_is_disabled_when_execpolicy_feature_disabled() { - let command = vec!["python".to_string()]; + let command = vec!["cargo".to_string(), "build".to_string()]; let mut features = Features::with_defaults(); features.disable(Feature::ExecPolicy); - let requirement = create_approval_requirement_for_command( + let requirement = create_exec_approval_requirement_for_command( &Arc::new(RwLock::new(Policy::empty())), &features, &command, @@ -540,7 +549,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") assert_eq!( requirement, - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: None, allow_prefix: None, } @@ -557,7 +566,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") let policy = Arc::new(RwLock::new(parser.build())); let command = vec!["rm".to_string()]; - let requirement = create_approval_requirement_for_command( + let requirement = create_exec_approval_requirement_for_command( &policy, &Features::with_defaults(), &command, @@ -569,7 +578,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") assert_eq!( requirement, - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: Some(PROMPT_REASON.to_string()), allow_prefix: None, } @@ -581,9 +590,9 @@ prefix_rule(pattern=["rm"], decision="forbidden") let command = vec![ "bash".to_string(), "-lc".to_string(), - "python && echo ok".to_string(), + "cargo build && echo ok".to_string(), ]; - let requirement = create_approval_requirement_for_command( + let requirement = create_exec_approval_requirement_for_command( &Arc::new(RwLock::new(Policy::empty())), &Features::with_defaults(), &command, @@ -595,7 +604,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") assert_eq!( requirement, - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: None, allow_prefix: None, } diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 887441ba26..cd05d126bf 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use crate::codex::TurnContext; use crate::exec::ExecParams; use crate::exec_env::create_env; -use crate::exec_policy::create_approval_requirement_for_command; +use crate::exec_policy::create_exec_approval_requirement_for_command; use crate::function_tool::FunctionCallError; use crate::is_safe_command::is_known_safe_command; use crate::protocol::ExecCommandSource; @@ -232,7 +232,7 @@ impl ShellHandler { emitter.begin(event_ctx).await; let features = session.features(); - let approval_requirement = create_approval_requirement_for_command( + let exec_approval_requirement = create_exec_approval_requirement_for_command( &turn.exec_policy, &features, &exec_params.command, @@ -241,7 +241,7 @@ impl ShellHandler { SandboxPermissions::from(exec_params.with_escalated_permissions.unwrap_or(false)), ) .await; - + let req = ShellRequest { command: exec_params.command.clone(), cwd: exec_params.cwd.clone(), @@ -249,7 +249,7 @@ impl ShellHandler { env: exec_params.env.clone(), with_escalated_permissions: exec_params.with_escalated_permissions, justification: exec_params.justification.clone(), - approval_requirement, + exec_approval_requirement, }; let mut orchestrator = ToolOrchestrator::new(); let mut runtime = ShellRuntime::new(); diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 7510fc6aa4..5ac3c63509 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -11,14 +11,14 @@ use crate::error::get_error_message_ui; use crate::exec::ExecToolCallOutput; use crate::sandboxing::SandboxManager; use crate::tools::sandboxing::ApprovalCtx; -use crate::tools::sandboxing::ApprovalRequirement; +use crate::tools::sandboxing::ExecApprovalRequirement; use crate::tools::sandboxing::ProvidesSandboxRetryData; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::SandboxOverride; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; -use crate::tools::sandboxing::default_approval_requirement; +use crate::tools::sandboxing::default_exec_approval_requirement; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; @@ -54,17 +54,17 @@ impl ToolOrchestrator { // 1) Approval let mut already_approved = false; - let requirement = tool.approval_requirement(req).unwrap_or_else(|| { - default_approval_requirement(approval_policy, &turn_ctx.sandbox_policy) + let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| { + default_exec_approval_requirement(approval_policy, &turn_ctx.sandbox_policy) }); match requirement { - ApprovalRequirement::Skip { .. } => { + ExecApprovalRequirement::Skip { .. } => { otel.tool_decision(otel_tn, otel_ci, &ReviewDecision::Approved, otel_cfg); } - ApprovalRequirement::Forbidden { reason } => { + ExecApprovalRequirement::Forbidden { reason } => { return Err(ToolError::Rejected(reason)); } - ApprovalRequirement::NeedsApproval { reason, .. } => { + ExecApprovalRequirement::NeedsApproval { reason, .. } => { let mut risk = None; if let Some(metadata) = req.sandbox_retry_data() { diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index ed5ba8a0d8..48dc5b9990 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -9,7 +9,7 @@ use crate::sandboxing::execute_env; use crate::tools::runtimes::build_command_spec; use crate::tools::sandboxing::Approvable; use crate::tools::sandboxing::ApprovalCtx; -use crate::tools::sandboxing::ApprovalRequirement; +use crate::tools::sandboxing::ExecApprovalRequirement; use crate::tools::sandboxing::ProvidesSandboxRetryData; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::SandboxOverride; @@ -32,7 +32,7 @@ pub struct ShellRequest { pub env: std::collections::HashMap, pub with_escalated_permissions: Option, pub justification: Option, - pub approval_requirement: ApprovalRequirement, + pub exec_approval_requirement: ExecApprovalRequirement, } impl ProvidesSandboxRetryData for ShellRequest { @@ -114,7 +114,7 @@ impl Approvable for ShellRuntime { cwd, reason, risk, - req.approval_requirement.allow_prefix().cloned(), + req.exec_approval_requirement.allow_prefix().cloned(), ) .await }) @@ -122,15 +122,15 @@ impl Approvable for ShellRuntime { }) } - fn approval_requirement(&self, req: &ShellRequest) -> Option { - Some(req.approval_requirement.clone()) + fn exec_approval_requirement(&self, req: &ShellRequest) -> Option { + Some(req.exec_approval_requirement.clone()) } fn sandbox_mode_for_first_attempt(&self, req: &ShellRequest) -> SandboxOverride { if req.with_escalated_permissions.unwrap_or(false) || matches!( - req.approval_requirement, - ApprovalRequirement::Skip { + req.exec_approval_requirement, + ExecApprovalRequirement::Skip { bypass_sandbox: true } ) diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 0a23695bde..45d804d688 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -10,7 +10,7 @@ use crate::exec::ExecExpiration; use crate::tools::runtimes::build_command_spec; use crate::tools::sandboxing::Approvable; use crate::tools::sandboxing::ApprovalCtx; -use crate::tools::sandboxing::ApprovalRequirement; +use crate::tools::sandboxing::ExecApprovalRequirement; use crate::tools::sandboxing::ProvidesSandboxRetryData; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::SandboxOverride; @@ -36,7 +36,7 @@ pub struct UnifiedExecRequest { pub env: HashMap, pub with_escalated_permissions: Option, pub justification: Option, - pub approval_requirement: ApprovalRequirement, + pub exec_approval_requirement: ExecApprovalRequirement, } impl ProvidesSandboxRetryData for UnifiedExecRequest { @@ -66,7 +66,7 @@ impl UnifiedExecRequest { env: HashMap, with_escalated_permissions: Option, justification: Option, - approval_requirement: ApprovalRequirement, + exec_approval_requirement: ExecApprovalRequirement, ) -> Self { Self { command, @@ -74,7 +74,7 @@ impl UnifiedExecRequest { env, with_escalated_permissions, justification, - approval_requirement, + exec_approval_requirement, } } } @@ -132,7 +132,7 @@ impl Approvable for UnifiedExecRuntime<'_> { cwd, reason, risk, - req.approval_requirement.allow_prefix().cloned(), + req.exec_approval_requirement.allow_prefix().cloned(), ) .await }) @@ -140,15 +140,18 @@ impl Approvable for UnifiedExecRuntime<'_> { }) } - fn approval_requirement(&self, req: &UnifiedExecRequest) -> Option { - Some(req.approval_requirement.clone()) + fn exec_approval_requirement( + &self, + req: &UnifiedExecRequest, + ) -> Option { + Some(req.exec_approval_requirement.clone()) } fn sandbox_mode_for_first_attempt(&self, req: &UnifiedExecRequest) -> SandboxOverride { if req.with_escalated_permissions.unwrap_or(false) || matches!( - req.approval_requirement, - ApprovalRequirement::Skip { + req.exec_approval_requirement, + ExecApprovalRequirement::Skip { bypass_sandbox: true } ) diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 8efef10e59..793c162936 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -88,24 +88,24 @@ pub(crate) struct ApprovalCtx<'a> { // Specifies what tool orchestrator should do with a given tool call. #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) enum ApprovalRequirement { +pub(crate) enum ExecApprovalRequirement { /// No approval required for this tool call. Skip { /// The first attempt should skip sandboxing (e.g., when explicitly /// greenlit by policy). bypass_sandbox: bool, }, - /// Approval required for this tool call + /// Approval required for this tool call. NeedsApproval { reason: Option, /// Prefix that can be whitelisted via execpolicy to skip future approvals for similar commands allow_prefix: Option>, }, - /// Execution forbidden for this tool call + /// Execution forbidden for this tool call. Forbidden { reason: String }, } -impl ApprovalRequirement { +impl ExecApprovalRequirement { pub fn allow_prefix(&self) -> Option<&Vec> { match self { Self::NeedsApproval { @@ -120,10 +120,10 @@ impl ApprovalRequirement { /// - Never, OnFailure: do not ask /// - OnRequest: ask unless sandbox policy is DangerFullAccess /// - UnlessTrusted: always ask -pub(crate) fn default_approval_requirement( +pub(crate) fn default_exec_approval_requirement( policy: AskForApproval, sandbox_policy: &SandboxPolicy, -) -> ApprovalRequirement { +) -> ExecApprovalRequirement { let needs_approval = match policy { AskForApproval::Never | AskForApproval::OnFailure => false, AskForApproval::OnRequest => !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess), @@ -131,12 +131,12 @@ pub(crate) fn default_approval_requirement( }; if needs_approval { - ApprovalRequirement::NeedsApproval { + ExecApprovalRequirement::NeedsApproval { reason: None, allow_prefix: None, } } else { - ApprovalRequirement::Skip { + ExecApprovalRequirement::Skip { bypass_sandbox: false, } } @@ -168,10 +168,9 @@ pub(crate) trait Approvable { matches!(policy, AskForApproval::Never) } - /// Override the default approval requirement. Return `Some(_)` to specify - /// a custom requirement, or `None` to fall back to - /// policy-based default. - fn approval_requirement(&self, _req: &Req) -> Option { + /// Return `Some(_)` to specify a custom exec approval requirement, or `None` + /// to fall back to policy-based default. + fn exec_approval_requirement(&self, _req: &Req) -> Option { None } diff --git a/codex-rs/core/src/unified_exec/session_manager.rs b/codex-rs/core/src/unified_exec/session_manager.rs index b9b4ea8dbe..51e5626073 100644 --- a/codex-rs/core/src/unified_exec/session_manager.rs +++ b/codex-rs/core/src/unified_exec/session_manager.rs @@ -15,7 +15,7 @@ use crate::codex::TurnContext; use crate::exec::ExecToolCallOutput; use crate::exec::StreamOutput; use crate::exec_env::create_env; -use crate::exec_policy::create_approval_requirement_for_command; +use crate::exec_policy::create_exec_approval_requirement_for_command; use crate::protocol::BackgroundEventEvent; use crate::protocol::EventMsg; use crate::protocol::ExecCommandSource; @@ -559,7 +559,7 @@ impl UnifiedExecSessionManager { let features = context.session.features(); let mut orchestrator = ToolOrchestrator::new(); let mut runtime = UnifiedExecRuntime::new(self); - let approval_requirement = create_approval_requirement_for_command( + let exec_approval_requirement = create_exec_approval_requirement_for_command( &context.turn.exec_policy, &features, command, @@ -574,7 +574,7 @@ impl UnifiedExecSessionManager { env, with_escalated_permissions, justification, - approval_requirement, + exec_approval_requirement, ); let tool_ctx = ToolCtx { session: context.session.as_ref(), diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 9bb265c206..8ac31fb378 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -1559,29 +1559,29 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> { } #[tokio::test(flavor = "current_thread")] +#[cfg(unix)] async fn approving_allow_prefix_persists_policy_and_skips_future_prompts() -> Result<()> { let server = start_mock_server().await; let approval_policy = AskForApproval::UnlessTrusted; - let sandbox_policy = SandboxPolicy::DangerFullAccess; + let sandbox_policy = SandboxPolicy::ReadOnly; let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.approval_policy = approval_policy; config.sandbox_policy = sandbox_policy_for_config; }); let test = builder.build(&server).await?; + let allow_prefix_path = test.cwd.path().join("allow-prefix.txt"); + let _ = fs::remove_file(&allow_prefix_path); let call_id_first = "allow-prefix-first"; let (first_event, expected_command) = ActionKind::RunCommand { - command: "printf allow-prefix-ok", + command: "touch allow-prefix.txt", } .prepare(&test, &server, call_id_first, false) .await?; let expected_command = expected_command.expect("allow prefix scenario should produce a shell command"); - let expected_allow_prefix = expected_command - .split_whitespace() - .map(ToString::to_string) - .collect::>(); + let expected_allow_prefix = vec!["touch".to_string(), "allow-prefix.txt".to_string()]; let _ = mount_sse_once( &server, @@ -1626,7 +1626,7 @@ async fn approving_allow_prefix_persists_policy_and_skips_future_prompts() -> Re let policy_contents = fs::read_to_string(&policy_path)?; assert!( policy_contents - .contains(r#"prefix_rule(pattern=["printf", "allow-prefix-ok"], decision="allow")"#), + .contains(r#"prefix_rule(pattern=["touch", "allow-prefix.txt"], decision="allow")"#), "unexpected policy contents: {policy_contents}" ); @@ -1637,14 +1637,19 @@ async fn approving_allow_prefix_persists_policy_and_skips_future_prompts() -> Re ); assert_eq!(first_output.exit_code.unwrap_or(0), 0); assert!( - first_output.stdout.contains("allow-prefix-ok"), + first_output.stdout.is_empty(), "unexpected stdout: {}", first_output.stdout ); + assert_eq!( + fs::read_to_string(&allow_prefix_path)?, + "", + "unexpected file contents after first run" + ); let call_id_second = "allow-prefix-second"; let (second_event, second_command) = ActionKind::RunCommand { - command: "printf allow-prefix-ok", + command: "touch allow-prefix.txt", } .prepare(&test, &server, call_id_second, false) .await?; @@ -1685,10 +1690,15 @@ async fn approving_allow_prefix_persists_policy_and_skips_future_prompts() -> Re ); assert_eq!(second_output.exit_code.unwrap_or(0), 0); assert!( - second_output.stdout.contains("allow-prefix-ok"), + second_output.stdout.is_empty(), "unexpected stdout: {}", second_output.stdout ); + assert_eq!( + fs::read_to_string(&allow_prefix_path)?, + "", + "unexpected file contents after second run" + ); Ok(()) }