mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
policy review
This commit is contained in:
@@ -4597,6 +4597,26 @@ async fn tool_call_metadata_stamps_escalated_review_decision_when_feature_enable
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tool_call_metadata_stamps_policy_source_without_review_decision_when_feature_enabled() {
|
||||
let (sess, tc, rx) = setup_tool_call_metadata_runtime_test().await;
|
||||
|
||||
sess.record_call_approval_outcome(
|
||||
"call-policy-1".to_string(),
|
||||
ApprovalOutcomeMetadata::policy(),
|
||||
)
|
||||
.await;
|
||||
sess.record_response_item_and_emit_turn_item(tc.as_ref(), function_call_item("call-policy-1"))
|
||||
.await;
|
||||
assert_next_emitted_function_call_metadata(
|
||||
&rx,
|
||||
true,
|
||||
None,
|
||||
Some(codex_protocol::models::ApprovalSourceMetadata::Policy),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tool_call_metadata_stamps_non_escalated_false_when_feature_enabled() {
|
||||
let (sess, tc, rx) = setup_tool_call_metadata_runtime_test().await;
|
||||
|
||||
@@ -72,6 +72,13 @@ pub(crate) struct ApprovalOutcomeMetadata {
|
||||
}
|
||||
|
||||
impl ApprovalOutcomeMetadata {
|
||||
pub(crate) fn policy() -> Self {
|
||||
Self {
|
||||
review_decision: None,
|
||||
approval_source: ApprovalSourceMetadata::Policy,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reviewed(
|
||||
decision: &ReviewDecision,
|
||||
approval_source: ApprovalSourceMetadata,
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::sandboxing::ExecRequest;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::state::ApprovalOutcomeMetadata;
|
||||
use crate::tools::runtimes::ExecveSessionApproval;
|
||||
use crate::tools::runtimes::build_command_spec;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
@@ -26,6 +27,7 @@ use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::ApprovalSourceMetadata;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
@@ -359,6 +361,26 @@ fn execve_prompt_is_rejected_by_policy(
|
||||
}
|
||||
}
|
||||
|
||||
fn approval_source_for_policy_resolved_decision(
|
||||
approval_policy: AskForApproval,
|
||||
decision: Decision,
|
||||
decision_source: &DecisionSource,
|
||||
) -> Option<ApprovalSourceMetadata> {
|
||||
match decision {
|
||||
Decision::Allow | Decision::Forbidden
|
||||
if matches!(decision_source, DecisionSource::PrefixRule) =>
|
||||
{
|
||||
Some(ApprovalSourceMetadata::Policy)
|
||||
}
|
||||
Decision::Prompt
|
||||
if execve_prompt_is_rejected_by_policy(approval_policy, decision_source).is_some() =>
|
||||
{
|
||||
Some(ApprovalSourceMetadata::Policy)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreShellActionProvider {
|
||||
fn decision_driven_by_policy(matched_rules: &[RuleMatch], decision: Decision) -> bool {
|
||||
matched_rules.iter().any(|rule_match| {
|
||||
@@ -522,12 +544,16 @@ impl CoreShellActionProvider {
|
||||
) -> anyhow::Result<EscalationDecision> {
|
||||
let action = match decision {
|
||||
Decision::Forbidden => {
|
||||
self.record_policy_approval_outcome_if_needed(decision, &decision_source)
|
||||
.await;
|
||||
EscalationDecision::deny(Some("Execution forbidden by policy".to_string()))
|
||||
}
|
||||
Decision::Prompt => {
|
||||
if execve_prompt_is_rejected_by_policy(self.approval_policy, &decision_source)
|
||||
.is_some()
|
||||
{
|
||||
self.record_policy_approval_outcome_if_needed(decision, &decision_source)
|
||||
.await;
|
||||
EscalationDecision::deny(Some("Execution forbidden by policy".to_string()))
|
||||
} else {
|
||||
match self
|
||||
@@ -600,6 +626,8 @@ impl CoreShellActionProvider {
|
||||
}
|
||||
}
|
||||
Decision::Allow => {
|
||||
self.record_policy_approval_outcome_if_needed(decision, &decision_source)
|
||||
.await;
|
||||
if needs_escalation {
|
||||
EscalationDecision::escalate(escalation_execution)
|
||||
} else {
|
||||
@@ -612,6 +640,27 @@ impl CoreShellActionProvider {
|
||||
);
|
||||
Ok(action)
|
||||
}
|
||||
|
||||
async fn record_policy_approval_outcome_if_needed(
|
||||
&self,
|
||||
decision: Decision,
|
||||
decision_source: &DecisionSource,
|
||||
) {
|
||||
if approval_source_for_policy_resolved_decision(
|
||||
self.approval_policy,
|
||||
decision,
|
||||
decision_source,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
self.session
|
||||
.record_call_approval_outcome(
|
||||
self.call_id.clone(),
|
||||
ApprovalOutcomeMetadata::policy(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shell-wrapper parsing is weaker than direct exec interception because it can
|
||||
|
||||
@@ -29,6 +29,7 @@ use codex_execpolicy::PolicyParser;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::ApprovalSourceMetadata;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
@@ -166,6 +167,60 @@ fn execve_prompt_rejection_keeps_unmatched_commands_on_sandbox_flag() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_resolved_approval_source_marks_rule_driven_allow_as_policy() {
|
||||
assert_eq!(
|
||||
super::approval_source_for_policy_resolved_decision(
|
||||
AskForApproval::OnRequest,
|
||||
Decision::Allow,
|
||||
&super::DecisionSource::PrefixRule,
|
||||
),
|
||||
Some(ApprovalSourceMetadata::Policy),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_resolved_approval_source_marks_rule_driven_forbidden_as_policy() {
|
||||
assert_eq!(
|
||||
super::approval_source_for_policy_resolved_decision(
|
||||
AskForApproval::OnRequest,
|
||||
Decision::Forbidden,
|
||||
&super::DecisionSource::PrefixRule,
|
||||
),
|
||||
Some(ApprovalSourceMetadata::Policy),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_resolved_approval_source_marks_prompt_rejected_by_policy_as_policy() {
|
||||
assert_eq!(
|
||||
super::approval_source_for_policy_resolved_decision(
|
||||
AskForApproval::Granular(GranularApprovalConfig {
|
||||
sandbox_approval: false,
|
||||
rules: true,
|
||||
skill_approval: true,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
Decision::Prompt,
|
||||
&super::DecisionSource::UnmatchedCommandFallback,
|
||||
),
|
||||
Some(ApprovalSourceMetadata::Policy),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_resolved_approval_source_does_not_mark_fallback_allow_as_policy() {
|
||||
assert_eq!(
|
||||
super::approval_source_for_policy_resolved_decision(
|
||||
AskForApproval::OnRequest,
|
||||
Decision::Allow,
|
||||
&super::DecisionSource::UnmatchedCommandFallback,
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_sandbox_permissions_only_downgrades_preapproved_additional_permissions() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user