This commit is contained in:
kevin zhao
2025-12-03 17:47:45 +00:00
parent be1a90bb6e
commit bf0ba87200
9 changed files with 118 additions and 93 deletions

View File

@@ -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],

View File

@@ -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<RwLock<Policy>>,
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,
}

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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<String, String>,
pub with_escalated_permissions: Option<bool>,
pub justification: Option<String>,
pub approval_requirement: ApprovalRequirement,
pub exec_approval_requirement: ExecApprovalRequirement,
}
impl ProvidesSandboxRetryData for ShellRequest {
@@ -114,7 +114,7 @@ impl Approvable<ShellRequest> 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<ShellRequest> for ShellRuntime {
})
}
fn approval_requirement(&self, req: &ShellRequest) -> Option<ApprovalRequirement> {
Some(req.approval_requirement.clone())
fn exec_approval_requirement(&self, req: &ShellRequest) -> Option<ExecApprovalRequirement> {
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
}
)

View File

@@ -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<String, String>,
pub with_escalated_permissions: Option<bool>,
pub justification: Option<String>,
pub approval_requirement: ApprovalRequirement,
pub exec_approval_requirement: ExecApprovalRequirement,
}
impl ProvidesSandboxRetryData for UnifiedExecRequest {
@@ -66,7 +66,7 @@ impl UnifiedExecRequest {
env: HashMap<String, String>,
with_escalated_permissions: Option<bool>,
justification: Option<String>,
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<UnifiedExecRequest> 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<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
})
}
fn approval_requirement(&self, req: &UnifiedExecRequest) -> Option<ApprovalRequirement> {
Some(req.approval_requirement.clone())
fn exec_approval_requirement(
&self,
req: &UnifiedExecRequest,
) -> Option<ExecApprovalRequirement> {
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
}
)

View File

@@ -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<String>,
/// Prefix that can be whitelisted via execpolicy to skip future approvals for similar commands
allow_prefix: Option<Vec<String>>,
},
/// 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<String>> {
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<Req> {
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<ApprovalRequirement> {
/// 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<ExecApprovalRequirement> {
None
}

View File

@@ -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(),

View File

@@ -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::<Vec<_>>();
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(())
}