execpolicy struct

This commit is contained in:
kevin zhao
2025-12-04 06:34:37 +00:00
parent 249bd10726
commit 4488061a9d
8 changed files with 89 additions and 33 deletions

View File

@@ -25,6 +25,7 @@ use crate::util::error_or_panic;
use async_channel::Receiver;
use async_channel::Sender;
use codex_protocol::ConversationId;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::items::TurnItem;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::HasLegacyEvent;
@@ -875,7 +876,7 @@ impl Session {
/// commands can use the newly approved prefix.
pub(crate) async fn persist_execpolicy_amendment(
&self,
amendment: &[String],
amendment: &ExecPolicyAmendment,
) -> Result<(), ExecPolicyUpdateError> {
let features = self.features.clone();
let (codex_home, current_policy) = {
@@ -898,7 +899,7 @@ impl Session {
crate::exec_policy::append_execpolicy_amendment_and_update(
&codex_home,
&current_policy,
amendment,
&amendment.command,
)
.await?;
@@ -919,7 +920,7 @@ impl Session {
cwd: PathBuf,
reason: Option<String>,
risk: Option<SandboxCommandAssessment>,
proposed_execpolicy_amendment: Option<Vec<String>>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
) -> ReviewDecision {
let sub_id = turn_context.sub_id.clone();
// Add the tx_approve callback to the map before sending the request.

View File

@@ -12,6 +12,7 @@ use codex_execpolicy::Policy;
use codex_execpolicy::PolicyParser;
use codex_execpolicy::RuleMatch;
use codex_execpolicy::blocking_append_allow_prefix_rule;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use thiserror::Error;
@@ -156,11 +157,8 @@ pub(crate) async fn append_execpolicy_amendment_and_update(
/// on `prog1`, we return `Some(vec!["prog1", "--option1", "arg1"])`.
/// - execpolicy: contains a `prompt for prefix ["prog2"]` rule. For the same command as above,
/// we return `None` because an execpolicy prompt still applies even if we amend execpolicy to allow ["prog1", "--option1", "arg1"].
fn proposed_execpolicy_amendment(
evaluation: &Evaluation,
features: &Features,
) -> Option<Vec<String>> {
if !features.enabled(Feature::ExecPolicy) || evaluation.decision != Decision::Prompt {
fn proposed_execpolicy_amendment(evaluation: &Evaluation) -> Option<ExecPolicyAmendment> {
if evaluation.decision != Decision::Prompt {
return None;
}
@@ -179,7 +177,7 @@ fn proposed_execpolicy_amendment(
}
}
first_prompt_from_heuristics
first_prompt_from_heuristics.map(ExecPolicyAmendment::from)
}
/// Only return PROMPT_REASON when an execpolicy rule drove the prompt decision.
@@ -230,10 +228,11 @@ pub(crate) async fn create_exec_approval_requirement_for_command(
} else {
ExecApprovalRequirement::NeedsApproval {
reason: derive_prompt_reason(&evaluation),
proposed_execpolicy_amendment: proposed_execpolicy_amendment(
&evaluation,
features,
),
proposed_execpolicy_amendment: if features.enabled(Feature::ExecPolicy) {
proposed_execpolicy_amendment(&evaluation)
} else {
None
},
}
}
}
@@ -509,7 +508,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(command)
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command))
}
);
}
@@ -540,7 +539,9 @@ prefix_rule(pattern=["rm"], decision="forbidden")
.await,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(vec!["orange".to_string()])
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"orange".to_string()
]))
}
);
}
@@ -612,7 +613,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(command)
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command))
}
);
}
@@ -693,7 +694,10 @@ prefix_rule(pattern=["rm"], decision="forbidden")
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(vec!["cargo".to_string(), "build".to_string()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"cargo".to_string(),
"build".to_string()
])),
}
);
}
@@ -725,7 +729,9 @@ prefix_rule(pattern=["rm"], decision="forbidden")
.await,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(vec!["apple".to_string()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"apple".to_string()
])),
}
);
}

View File

@@ -13,6 +13,7 @@ use crate::sandboxing::CommandSpec;
use crate::sandboxing::SandboxManager;
use crate::sandboxing::SandboxTransformError;
use crate::state::SessionServices;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReviewDecision;
use std::collections::HashMap;
@@ -100,14 +101,14 @@ pub(crate) enum ExecApprovalRequirement {
reason: Option<String>,
/// Proposed execpolicy amendment to skip future approvals for similar commands
/// See core/src/exec_policy.rs for more details on how proposed_execpolicy_amendment is determined.
proposed_execpolicy_amendment: Option<Vec<String>>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
},
/// Execution forbidden for this tool call.
Forbidden { reason: String },
}
impl ExecApprovalRequirement {
pub fn proposed_execpolicy_amendment(&self) -> Option<&Vec<String>> {
pub fn proposed_execpolicy_amendment(&self) -> Option<&ExecPolicyAmendment> {
match self {
Self::NeedsApproval {
proposed_execpolicy_amendment: Some(prefix),

View File

@@ -6,6 +6,7 @@ use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::EventMsg;
use codex_core::protocol::ExecApprovalRequestEvent;
use codex_core::protocol::ExecPolicyAmendment;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_protocol::config_types::ReasoningSummary;
@@ -1581,7 +1582,8 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
.await?;
let expected_command =
expected_command.expect("execpolicy amendment scenario should produce a shell command");
let expected_execpolicy_amendment = vec!["touch".to_string(), "allow-prefix.txt".to_string()];
let expected_execpolicy_amendment =
ExecPolicyAmendment::new(vec!["touch".to_string(), "allow-prefix.txt".to_string()]);
let _ = mount_sse_once(
&server,

View File

@@ -17,6 +17,34 @@ pub enum SandboxRiskLevel {
High,
}
/// Proposed execpolicy change to allow commands starting with this prefix.
///
/// The `command` tokens form the prefix that would be added as an execpolicy
/// `prefix_rule(..., decision="allow")`, letting the agent bypass approval for
/// commands that start with this token sequence.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(transparent)]
#[ts(type = "Array<string>")]
pub struct ExecPolicyAmendment {
pub command: Vec<String>,
}
impl ExecPolicyAmendment {
pub fn new(command: Vec<String>) -> Self {
Self { command }
}
pub fn command(&self) -> &[String] {
&self.command
}
}
impl From<Vec<String>> for ExecPolicyAmendment {
fn from(command: Vec<String>) -> Self {
Self { command }
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct SandboxCommandAssessment {
pub description: String,
@@ -53,8 +81,8 @@ pub struct ExecApprovalRequestEvent {
pub risk: Option<SandboxCommandAssessment>,
/// Proposed execpolicy amendment that can be applied to allow future runs.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional, type = "Array<string>")]
pub proposed_execpolicy_amendment: Option<Vec<String>>,
#[ts(optional)]
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
pub parsed_cmd: Vec<ParsedCommand>,
}

View File

@@ -39,6 +39,7 @@ use ts_rs::TS;
pub use crate::approvals::ApplyPatchApprovalRequestEvent;
pub use crate::approvals::ElicitationAction;
pub use crate::approvals::ExecApprovalRequestEvent;
pub use crate::approvals::ExecPolicyAmendment;
pub use crate::approvals::SandboxCommandAssessment;
pub use crate::approvals::SandboxRiskLevel;
@@ -1658,7 +1659,7 @@ pub enum ReviewDecision {
/// User has approved this command and wants to apply the proposed execpolicy
/// amendment so future matching commands are permitted.
ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: Vec<String>,
proposed_execpolicy_amendment: ExecPolicyAmendment,
},
/// User has approved this command and wants to automatically approve any

View File

@@ -17,6 +17,7 @@ use crate::render::highlight::highlight_bash_to_lines;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use codex_core::protocol::ElicitationAction;
use codex_core::protocol::ExecPolicyAmendment;
use codex_core::protocol::FileChange;
use codex_core::protocol::Op;
use codex_core::protocol::ReviewDecision;
@@ -43,7 +44,7 @@ pub(crate) enum ApprovalRequest {
command: Vec<String>,
reason: Option<String>,
risk: Option<SandboxCommandAssessment>,
proposed_execpolicy_amendment: Option<Vec<String>>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
},
ApplyPatch {
id: String,
@@ -440,7 +441,7 @@ enum ApprovalVariant {
Exec {
id: String,
command: Vec<String>,
proposed_execpolicy_amendment: Option<Vec<String>>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
},
ApplyPatch {
id: String,
@@ -473,7 +474,7 @@ impl ApprovalOption {
}
}
fn exec_options(proposed_execpolicy_amendment: Option<Vec<String>>) -> Vec<ApprovalOption> {
fn exec_options(proposed_execpolicy_amendment: Option<ExecPolicyAmendment>) -> Vec<ApprovalOption> {
vec![
ApprovalOption {
label: "Yes, proceed".to_string(),
@@ -602,7 +603,9 @@ mod tests {
command: vec!["echo".to_string()],
reason: None,
risk: None,
proposed_execpolicy_amendment: Some(vec!["echo".to_string()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".to_string(),
])),
},
tx,
);
@@ -613,7 +616,9 @@ mod tests {
assert_eq!(
decision,
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: vec!["echo".to_string()]
proposed_execpolicy_amendment: ExecPolicyAmendment::new(vec![
"echo".to_string()
])
}
);
saw_op = true;
@@ -622,7 +627,7 @@ mod tests {
}
assert!(
saw_op,
"expected approval decision to emit an op with allow prefix"
"expected approval decision to emit an op with command prefix"
);
}

View File

@@ -23,6 +23,7 @@ use codex_core::protocol::ExecApprovalRequestEvent;
use codex_core::protocol::ExecCommandBeginEvent;
use codex_core::protocol::ExecCommandEndEvent;
use codex_core::protocol::ExecCommandSource;
use codex_core::protocol::ExecPolicyAmendment;
use codex_core::protocol::ExitedReviewModeEvent;
use codex_core::protocol::FileChange;
use codex_core::protocol::Op;
@@ -1993,7 +1994,11 @@ fn approval_modal_exec_snapshot() {
"this is a test reason such as one that would be produced by the model".into(),
),
risk: None,
proposed_execpolicy_amendment: Some(vec!["echo".into(), "hello".into(), "world".into()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello".into(),
"world".into(),
])),
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -2040,7 +2045,11 @@ fn approval_modal_exec_without_reason_snapshot() {
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
reason: None,
risk: None,
proposed_execpolicy_amendment: Some(vec!["echo".into(), "hello".into(), "world".into()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello".into(),
"world".into(),
])),
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -2254,7 +2263,10 @@ fn status_widget_and_approval_modal_snapshot() {
"this is a test reason such as one that would be produced by the model".into(),
),
risk: None,
proposed_execpolicy_amendment: Some(vec!["echo".into(), "hello world".into()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello world".into(),
])),
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {