mirror of
https://github.com/openai/codex.git
synced 2026-02-02 06:57:03 +00:00
Compare commits
3 Commits
exec-run-a
...
dev/zhao/7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf0ba87200 | ||
|
|
be1a90bb6e | ||
|
|
4563dd0a17 |
@@ -179,6 +179,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
cwd,
|
||||
reason,
|
||||
risk,
|
||||
allow_prefix: _allow_prefix,
|
||||
parsed_cmd,
|
||||
}) => match api_version {
|
||||
ApiVersion::V1 => {
|
||||
|
||||
@@ -70,7 +70,9 @@ pub(crate) async fn apply_patch(
|
||||
)
|
||||
.await;
|
||||
match rx_approve.await.unwrap_or_default() {
|
||||
ReviewDecision::Approved | ReviewDecision::ApprovedForSession => {
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedAllowPrefix { .. }
|
||||
| ReviewDecision::ApprovedForSession => {
|
||||
InternalApplyPatchInvocation::DelegateToExec(ApplyPatchExec {
|
||||
action,
|
||||
user_explicitly_approved_this_action: true,
|
||||
|
||||
@@ -73,6 +73,7 @@ use crate::error::CodexErr;
|
||||
use crate::error::Result as CodexResult;
|
||||
#[cfg(test)]
|
||||
use crate::exec::StreamOutput;
|
||||
use crate::exec_policy::ExecPolicyUpdateError;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::openai_model_info::get_model_info;
|
||||
@@ -293,7 +294,7 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) final_output_json_schema: Option<Value>,
|
||||
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub(crate) tool_call_gate: Arc<ReadinessFlag>,
|
||||
pub(crate) exec_policy: Arc<ExecPolicy>,
|
||||
pub(crate) exec_policy: Arc<RwLock<ExecPolicy>>,
|
||||
pub(crate) truncation_policy: TruncationPolicy,
|
||||
}
|
||||
|
||||
@@ -349,7 +350,7 @@ pub(crate) struct SessionConfiguration {
|
||||
cwd: PathBuf,
|
||||
|
||||
/// Execpolicy policy, applied only when enabled by feature flag.
|
||||
exec_policy: Arc<ExecPolicy>,
|
||||
exec_policy: Arc<RwLock<ExecPolicy>>,
|
||||
|
||||
// TODO(pakrym): Remove config from here
|
||||
original_config_do_not_use: Arc<Config>,
|
||||
@@ -870,11 +871,48 @@ 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],
|
||||
) -> Result<(), ExecPolicyUpdateError> {
|
||||
let features = self.features.clone();
|
||||
let (codex_home, current_policy) = {
|
||||
let state = self.state.lock().await;
|
||||
(
|
||||
state
|
||||
.session_configuration
|
||||
.original_config_do_not_use
|
||||
.codex_home
|
||||
.clone(),
|
||||
state.session_configuration.exec_policy.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
if !features.enabled(Feature::ExecPolicy) {
|
||||
error!("attempted to append execpolicy rule while execpolicy feature is disabled");
|
||||
return Err(ExecPolicyUpdateError::FeatureDisabled);
|
||||
}
|
||||
|
||||
crate::exec_policy::append_allow_prefix_rule_and_update(
|
||||
&codex_home,
|
||||
¤t_policy,
|
||||
prefix,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emit an exec approval request event and await the user's decision.
|
||||
///
|
||||
/// The request is keyed by `sub_id`/`call_id` so matching responses are delivered
|
||||
/// to the correct in-flight turn. If the task is aborted, this returns the
|
||||
/// default `ReviewDecision` (`Denied`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn request_command_approval(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
@@ -883,6 +921,7 @@ impl Session {
|
||||
cwd: PathBuf,
|
||||
reason: Option<String>,
|
||||
risk: Option<SandboxCommandAssessment>,
|
||||
allow_prefix: Option<Vec<String>>,
|
||||
) -> ReviewDecision {
|
||||
let sub_id = turn_context.sub_id.clone();
|
||||
// Add the tx_approve callback to the map before sending the request.
|
||||
@@ -910,6 +949,7 @@ impl Session {
|
||||
cwd,
|
||||
reason,
|
||||
risk,
|
||||
allow_prefix,
|
||||
parsed_cmd,
|
||||
});
|
||||
self.send_event(turn_context, event).await;
|
||||
@@ -1079,6 +1119,10 @@ impl Session {
|
||||
self.features.enabled(feature)
|
||||
}
|
||||
|
||||
pub(crate) fn features(&self) -> Features {
|
||||
self.features.clone()
|
||||
}
|
||||
|
||||
async fn send_raw_response_items(&self, turn_context: &TurnContext, items: &[ResponseItem]) {
|
||||
for item in items {
|
||||
self.send_event(
|
||||
@@ -1513,6 +1557,7 @@ mod handlers {
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_rmcp_client::ElicitationAction;
|
||||
@@ -1627,7 +1672,21 @@ mod handlers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Propagate a user's exec approval decision to the session
|
||||
/// Also optionally whitelists command in execpolicy
|
||||
pub async fn exec_approval(sess: &Arc<Session>, id: String, decision: ReviewDecision) {
|
||||
if let ReviewDecision::ApprovedAllowPrefix { allow_prefix } = &decision
|
||||
&& let Err(err) = sess.persist_command_allow_prefix(allow_prefix).await
|
||||
{
|
||||
let message = format!("Failed to update execpolicy allow list: {err}");
|
||||
tracing::warn!("{message}");
|
||||
let warning = EventMsg::Warning(WarningEvent { message });
|
||||
sess.send_event_raw(Event {
|
||||
id: id.clone(),
|
||||
msg: warning,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
match decision {
|
||||
ReviewDecision::Abort => {
|
||||
sess.interrupt_task().await;
|
||||
@@ -2571,7 +2630,7 @@ mod tests {
|
||||
sandbox_policy: config.sandbox_policy.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
exec_policy: Arc::new(ExecPolicy::empty()),
|
||||
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
|
||||
session_source: SessionSource::Exec,
|
||||
};
|
||||
|
||||
@@ -2770,7 +2829,7 @@ mod tests {
|
||||
sandbox_policy: config.sandbox_policy.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
exec_policy: Arc::new(ExecPolicy::empty()),
|
||||
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
|
||||
session_source: SessionSource::Exec,
|
||||
};
|
||||
|
||||
@@ -2851,7 +2910,7 @@ mod tests {
|
||||
sandbox_policy: config.sandbox_policy.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
exec_policy: Arc::new(ExecPolicy::empty()),
|
||||
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
|
||||
session_source: SessionSource::Exec,
|
||||
};
|
||||
|
||||
|
||||
@@ -281,6 +281,7 @@ async fn handle_exec_approval(
|
||||
event.cwd,
|
||||
event.reason,
|
||||
event.risk,
|
||||
event.allow_prefix,
|
||||
);
|
||||
let decision = await_approval_with_cancel(
|
||||
approval_fut,
|
||||
|
||||
@@ -4,25 +4,31 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::command_safety::is_dangerous_command::requires_initial_appoval;
|
||||
use codex_execpolicy::AmendError;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Error as ExecPolicyRuleError;
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::PolicyParser;
|
||||
use codex_execpolicy::blocking_append_allow_prefix_rule;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
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";
|
||||
const POLICY_DIR_NAME: &str = "policy";
|
||||
const POLICY_EXTENSION: &str = "codexpolicy";
|
||||
const DEFAULT_POLICY_FILE: &str = "default.codexpolicy";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExecPolicyError {
|
||||
@@ -45,12 +51,30 @@ pub enum ExecPolicyError {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
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]
|
||||
source: ExecPolicyRuleError,
|
||||
},
|
||||
|
||||
#[error("cannot append execpolicy rule because execpolicy feature is disabled")]
|
||||
FeatureDisabled,
|
||||
}
|
||||
|
||||
pub(crate) async fn exec_policy_for(
|
||||
features: &Features,
|
||||
codex_home: &Path,
|
||||
) -> Result<Arc<Policy>, ExecPolicyError> {
|
||||
) -> Result<Arc<RwLock<Policy>>, ExecPolicyError> {
|
||||
if !features.enabled(Feature::ExecPolicy) {
|
||||
return Ok(Arc::new(Policy::empty()));
|
||||
return Ok(Arc::new(RwLock::new(Policy::empty())));
|
||||
}
|
||||
|
||||
let policy_dir = codex_home.join(POLICY_DIR_NAME);
|
||||
@@ -74,7 +98,7 @@ pub(crate) async fn exec_policy_for(
|
||||
})?;
|
||||
}
|
||||
|
||||
let policy = Arc::new(parser.build());
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
tracing::debug!(
|
||||
"loaded execpolicy from {} files in {}",
|
||||
policy_paths.len(),
|
||||
@@ -84,58 +108,107 @@ pub(crate) async fn exec_policy_for(
|
||||
Ok(policy)
|
||||
}
|
||||
|
||||
fn evaluate_with_policy(
|
||||
policy: &Policy,
|
||||
command: &[String],
|
||||
approval_policy: AskForApproval,
|
||||
) -> Option<ApprovalRequirement> {
|
||||
let commands = parse_shell_lc_plain_commands(command).unwrap_or_else(|| vec![command.to_vec()]);
|
||||
let evaluation = policy.check_multiple(commands.iter());
|
||||
pub(crate) fn default_policy_path(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join(POLICY_DIR_NAME).join(DEFAULT_POLICY_FILE)
|
||||
}
|
||||
|
||||
match evaluation {
|
||||
Evaluation::Match { decision, .. } => match decision {
|
||||
Decision::Forbidden => Some(ApprovalRequirement::Forbidden {
|
||||
reason: FORBIDDEN_REASON.to_string(),
|
||||
}),
|
||||
Decision::Prompt => {
|
||||
let reason = PROMPT_REASON.to_string();
|
||||
if matches!(approval_policy, AskForApproval::Never) {
|
||||
Some(ApprovalRequirement::Forbidden { reason })
|
||||
} else {
|
||||
Some(ApprovalRequirement::NeedsApproval {
|
||||
reason: Some(reason),
|
||||
})
|
||||
pub(crate) async fn append_allow_prefix_rule_and_update(
|
||||
codex_home: &Path,
|
||||
current_policy: &Arc<RwLock<Policy>>,
|
||||
prefix: &[String],
|
||||
) -> Result<(), ExecPolicyUpdateError> {
|
||||
let policy_path = default_policy_path(codex_home);
|
||||
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)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn requirement_from_decision(
|
||||
decision: Decision,
|
||||
approval_policy: AskForApproval,
|
||||
) -> ExecApprovalRequirement {
|
||||
match decision {
|
||||
Decision::Forbidden => ExecApprovalRequirement::Forbidden {
|
||||
reason: FORBIDDEN_REASON.to_string(),
|
||||
},
|
||||
Decision::Prompt => {
|
||||
let reason = PROMPT_REASON.to_string();
|
||||
if matches!(approval_policy, AskForApproval::Never) {
|
||||
ExecApprovalRequirement::Forbidden { reason }
|
||||
} else {
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: Some(reason),
|
||||
allow_prefix: None,
|
||||
}
|
||||
}
|
||||
Decision::Allow => Some(ApprovalRequirement::Skip {
|
||||
bypass_sandbox: true,
|
||||
}),
|
||||
}
|
||||
Decision::Allow => ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: true,
|
||||
},
|
||||
Evaluation::NoMatch { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create_approval_requirement_for_command(
|
||||
policy: &Policy,
|
||||
/// Return an allow-prefix option when a single plain command needs approval without
|
||||
/// any matching policy rule. We only surface the prefix opt-in when execpolicy did
|
||||
/// not already drive the decision (NoMatch) and when the command is a single
|
||||
/// unrolled command (multi-part scripts shouldn’t be whitelisted via prefix) and
|
||||
/// when execpolicy feature is enabled.
|
||||
fn allow_prefix_if_applicable(
|
||||
commands: &[Vec<String>],
|
||||
features: &Features,
|
||||
) -> Option<Vec<String>> {
|
||||
if features.enabled(Feature::ExecPolicy) && commands.len() == 1 {
|
||||
Some(commands[0].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Some(requirement) = evaluate_with_policy(policy, command, approval_policy) {
|
||||
return requirement;
|
||||
}
|
||||
) -> ExecApprovalRequirement {
|
||||
let commands = parse_shell_lc_plain_commands(command).unwrap_or_else(|| vec![command.to_vec()]);
|
||||
let evaluation = exec_policy.read().await.check_multiple(commands.iter());
|
||||
|
||||
if requires_initial_appoval(
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
command,
|
||||
sandbox_permissions,
|
||||
) {
|
||||
ApprovalRequirement::NeedsApproval { reason: None }
|
||||
} else {
|
||||
ApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
match evaluation {
|
||||
Evaluation::Match { decision, .. } => requirement_from_decision(decision, approval_policy),
|
||||
Evaluation::NoMatch { .. } => {
|
||||
if requires_initial_appoval(
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
command,
|
||||
sandbox_permissions,
|
||||
) {
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
allow_prefix: allow_prefix_if_applicable(&commands, features),
|
||||
}
|
||||
} else {
|
||||
ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +268,7 @@ mod tests {
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -209,7 +283,7 @@ mod tests {
|
||||
|
||||
let commands = [vec!["rm".to_string()]];
|
||||
assert!(matches!(
|
||||
policy.check_multiple(commands.iter()),
|
||||
policy.read().await.check_multiple(commands.iter()),
|
||||
Evaluation::NoMatch { .. }
|
||||
));
|
||||
assert!(!temp_dir.path().join(POLICY_DIR_NAME).exists());
|
||||
@@ -243,7 +317,7 @@ mod tests {
|
||||
.expect("policy result");
|
||||
let command = [vec!["rm".to_string()]];
|
||||
assert!(matches!(
|
||||
policy.check_multiple(command.iter()),
|
||||
policy.read().await.check_multiple(command.iter()),
|
||||
Evaluation::Match { .. }
|
||||
));
|
||||
}
|
||||
@@ -262,13 +336,13 @@ mod tests {
|
||||
.expect("policy result");
|
||||
let command = [vec!["ls".to_string()]];
|
||||
assert!(matches!(
|
||||
policy.check_multiple(command.iter()),
|
||||
policy.read().await.check_multiple(command.iter()),
|
||||
Evaluation::NoMatch { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluates_bash_lc_inner_commands() {
|
||||
#[tokio::test]
|
||||
async fn evaluates_bash_lc_inner_commands() {
|
||||
let policy_src = r#"
|
||||
prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
"#;
|
||||
@@ -276,7 +350,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = parser.build();
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
|
||||
let forbidden_script = vec![
|
||||
"bash".to_string(),
|
||||
@@ -284,30 +358,37 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
"rm -rf /tmp".to_string(),
|
||||
];
|
||||
|
||||
let requirement =
|
||||
evaluate_with_policy(&policy, &forbidden_script, AskForApproval::OnRequest)
|
||||
.expect("expected match for forbidden command");
|
||||
let requirement = create_exec_approval_requirement_for_command(
|
||||
&policy,
|
||||
&Features::with_defaults(),
|
||||
&forbidden_script,
|
||||
AskForApproval::OnRequest,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
SandboxPermissions::UseDefault,
|
||||
)
|
||||
.await;
|
||||
|
||||
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
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = parser.build();
|
||||
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,
|
||||
AskForApproval::OnRequest,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
@@ -317,24 +398,26 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ApprovalRequirement::NeedsApproval {
|
||||
reason: Some(PROMPT_REASON.to_string())
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: Some(PROMPT_REASON.to_string()),
|
||||
allow_prefix: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = parser.build();
|
||||
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,
|
||||
AskForApproval::Never,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
@@ -344,19 +427,20 @@ 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 = Policy::empty();
|
||||
let requirement = create_approval_requirement_for_command(
|
||||
let empty_policy = Arc::new(RwLock::new(Policy::empty()));
|
||||
let requirement = create_exec_approval_requirement_for_command(
|
||||
&empty_policy,
|
||||
&Features::with_defaults(),
|
||||
&command,
|
||||
AskForApproval::UnlessTrusted,
|
||||
&SandboxPolicy::ReadOnly,
|
||||
@@ -366,7 +450,164 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ApprovalRequirement::NeedsApproval { reason: None }
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
allow_prefix: Some(command)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_allow_prefix_rule_updates_policy_and_file() {
|
||||
let codex_home = tempdir().expect("create temp dir");
|
||||
let current_policy = Arc::new(RwLock::new(Policy::empty()));
|
||||
let prefix = vec!["echo".to_string(), "hello".to_string()];
|
||||
|
||||
append_allow_prefix_rule_and_update(codex_home.path(), ¤t_policy, &prefix)
|
||||
.await
|
||||
.expect("update policy");
|
||||
|
||||
let evaluation = current_policy.read().await.check(&[
|
||||
"echo".to_string(),
|
||||
"hello".to_string(),
|
||||
"world".to_string(),
|
||||
]);
|
||||
assert!(matches!(
|
||||
evaluation,
|
||||
Evaluation::Match {
|
||||
decision: Decision::Allow,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
let contents = fs::read_to_string(default_policy_path(codex_home.path()))
|
||||
.expect("policy file should have been created");
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"prefix_rule(pattern=["echo", "hello"], decision="allow")
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn append_allow_prefix_rule_rejects_empty_prefix() {
|
||||
let codex_home = tempdir().expect("create temp dir");
|
||||
let current_policy = Arc::new(RwLock::new(Policy::empty()));
|
||||
|
||||
let result =
|
||||
append_allow_prefix_rule_and_update(codex_home.path(), ¤t_policy, &[]).await;
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ExecPolicyUpdateError::AppendRule {
|
||||
source: AmendError::EmptyPrefix,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allow_prefix_is_present_for_single_command_without_policy_match() {
|
||||
let command = vec!["cargo".to_string(), "build".to_string()];
|
||||
|
||||
let empty_policy = Arc::new(RwLock::new(Policy::empty()));
|
||||
let requirement = create_exec_approval_requirement_for_command(
|
||||
&empty_policy,
|
||||
&Features::with_defaults(),
|
||||
&command,
|
||||
AskForApproval::UnlessTrusted,
|
||||
&SandboxPolicy::ReadOnly,
|
||||
SandboxPermissions::UseDefault,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
allow_prefix: Some(command)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allow_prefix_is_disabled_when_execpolicy_feature_disabled() {
|
||||
let command = vec!["cargo".to_string(), "build".to_string()];
|
||||
|
||||
let mut features = Features::with_defaults();
|
||||
features.disable(Feature::ExecPolicy);
|
||||
|
||||
let requirement = create_exec_approval_requirement_for_command(
|
||||
&Arc::new(RwLock::new(Policy::empty())),
|
||||
&features,
|
||||
&command,
|
||||
AskForApproval::UnlessTrusted,
|
||||
&SandboxPolicy::ReadOnly,
|
||||
SandboxPermissions::UseDefault,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
allow_prefix: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allow_prefix_is_omitted_when_policy_prompts() {
|
||||
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
let command = vec!["rm".to_string()];
|
||||
|
||||
let requirement = create_exec_approval_requirement_for_command(
|
||||
&policy,
|
||||
&Features::with_defaults(),
|
||||
&command,
|
||||
AskForApproval::OnRequest,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
SandboxPermissions::UseDefault,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: Some(PROMPT_REASON.to_string()),
|
||||
allow_prefix: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allow_prefix_is_omitted_for_multi_command_scripts() {
|
||||
let command = vec![
|
||||
"bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"cargo build && echo ok".to_string(),
|
||||
];
|
||||
let requirement = create_exec_approval_requirement_for_command(
|
||||
&Arc::new(RwLock::new(Policy::empty())),
|
||||
&Features::with_defaults(),
|
||||
&command,
|
||||
AskForApproval::UnlessTrusted,
|
||||
&SandboxPolicy::ReadOnly,
|
||||
SandboxPermissions::UseDefault,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
allow_prefix: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -231,8 +231,10 @@ impl ShellHandler {
|
||||
let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None);
|
||||
emitter.begin(event_ctx).await;
|
||||
|
||||
let approval_requirement = create_approval_requirement_for_command(
|
||||
let features = session.features();
|
||||
let exec_approval_requirement = create_exec_approval_requirement_for_command(
|
||||
&turn.exec_policy,
|
||||
&features,
|
||||
&exec_params.command,
|
||||
turn.approval_policy,
|
||||
&turn.sandbox_policy,
|
||||
@@ -247,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();
|
||||
|
||||
@@ -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 { .. } => {
|
||||
otel.tool_decision(otel_tn, otel_ci, ReviewDecision::Approved, otel_cfg);
|
||||
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() {
|
||||
@@ -88,13 +88,15 @@ impl ToolOrchestrator {
|
||||
};
|
||||
let decision = tool.start_approval_async(req, approval_ctx).await;
|
||||
|
||||
otel.tool_decision(otel_tn, otel_ci, decision, otel_user.clone());
|
||||
otel.tool_decision(otel_tn, otel_ci, &decision, otel_user.clone());
|
||||
|
||||
match decision {
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
return Err(ToolError::Rejected("rejected by user".to_string()));
|
||||
}
|
||||
ReviewDecision::Approved | ReviewDecision::ApprovedForSession => {}
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedAllowPrefix { .. }
|
||||
| ReviewDecision::ApprovedForSession => {}
|
||||
}
|
||||
already_approved = true;
|
||||
}
|
||||
@@ -169,13 +171,15 @@ impl ToolOrchestrator {
|
||||
};
|
||||
|
||||
let decision = tool.start_approval_async(req, approval_ctx).await;
|
||||
otel.tool_decision(otel_tn, otel_ci, decision, otel_user);
|
||||
otel.tool_decision(otel_tn, otel_ci, &decision, otel_user);
|
||||
|
||||
match decision {
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
return Err(ToolError::Rejected("rejected by user".to_string()));
|
||||
}
|
||||
ReviewDecision::Approved | ReviewDecision::ApprovedForSession => {}
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedAllowPrefix { .. }
|
||||
| ReviewDecision::ApprovedForSession => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
cwd,
|
||||
Some(reason),
|
||||
risk,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
} else if user_explicitly_approved {
|
||||
|
||||
@@ -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 {
|
||||
@@ -107,22 +107,30 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
Box::pin(async move {
|
||||
with_cached_approval(&session.services, key, move || async move {
|
||||
session
|
||||
.request_command_approval(turn, call_id, command, cwd, reason, risk)
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
risk,
|
||||
req.exec_approval_requirement.allow_prefix().cloned(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,22 +125,33 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
Box::pin(async move {
|
||||
with_cached_approval(&session.services, key, || async move {
|
||||
session
|
||||
.request_command_approval(turn, call_id, command, cwd, reason, risk)
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
risk,
|
||||
req.exec_approval_requirement.allow_prefix().cloned(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -88,26 +88,42 @@ 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
|
||||
NeedsApproval { reason: Option<String> },
|
||||
/// Execution forbidden 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.
|
||||
Forbidden { reason: String },
|
||||
}
|
||||
|
||||
impl ExecApprovalRequirement {
|
||||
pub fn allow_prefix(&self) -> Option<&Vec<String>> {
|
||||
match self {
|
||||
Self::NeedsApproval {
|
||||
allow_prefix: Some(prefix),
|
||||
..
|
||||
} => Some(prefix),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// - 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),
|
||||
@@ -115,9 +131,12 @@ pub(crate) fn default_approval_requirement(
|
||||
};
|
||||
|
||||
if needs_approval {
|
||||
ApprovalRequirement::NeedsApproval { reason: None }
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
allow_prefix: None,
|
||||
}
|
||||
} else {
|
||||
ApprovalRequirement::Skip {
|
||||
ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
}
|
||||
}
|
||||
@@ -149,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -556,10 +556,12 @@ impl UnifiedExecSessionManager {
|
||||
context: &UnifiedExecContext,
|
||||
) -> Result<UnifiedExecSession, UnifiedExecError> {
|
||||
let env = apply_unified_exec_env(create_env(&context.turn.shell_environment_policy));
|
||||
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,
|
||||
context.turn.approval_policy,
|
||||
&context.turn.sandbox_policy,
|
||||
@@ -572,7 +574,7 @@ impl UnifiedExecSessionManager {
|
||||
env,
|
||||
with_escalated_permissions,
|
||||
justification,
|
||||
approval_requirement,
|
||||
exec_approval_requirement,
|
||||
);
|
||||
let tool_ctx = ToolCtx {
|
||||
session: context.session.as_ref(),
|
||||
|
||||
@@ -1523,7 +1523,7 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> {
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: "0".into(),
|
||||
decision: *decision,
|
||||
decision: decision.clone(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1544,7 +1544,7 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> {
|
||||
test.codex
|
||||
.submit(Op::PatchApproval {
|
||||
id: "0".into(),
|
||||
decision: *decision,
|
||||
decision: decision.clone(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1557,3 +1557,148 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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::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: "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 = vec!["touch".to_string(), "allow-prefix.txt".to_string()];
|
||||
|
||||
let _ = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-allow-prefix-1"),
|
||||
first_event,
|
||||
ev_completed("resp-allow-prefix-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let first_results = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-allow-prefix-1", "done"),
|
||||
ev_completed("resp-allow-prefix-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
submit_turn(
|
||||
&test,
|
||||
"allow-prefix-first",
|
||||
approval_policy,
|
||||
sandbox_policy.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let approval = expect_exec_approval(&test, expected_command.as_str()).await;
|
||||
assert_eq!(approval.allow_prefix, Some(expected_allow_prefix.clone()));
|
||||
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: "0".into(),
|
||||
decision: ReviewDecision::ApprovedAllowPrefix {
|
||||
allow_prefix: expected_allow_prefix.clone(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
|
||||
let policy_path = test.home.path().join("policy").join("default.codexpolicy");
|
||||
let policy_contents = fs::read_to_string(&policy_path)?;
|
||||
assert!(
|
||||
policy_contents
|
||||
.contains(r#"prefix_rule(pattern=["touch", "allow-prefix.txt"], decision="allow")"#),
|
||||
"unexpected policy contents: {policy_contents}"
|
||||
);
|
||||
|
||||
let first_output = parse_result(
|
||||
&first_results
|
||||
.single_request()
|
||||
.function_call_output(call_id_first),
|
||||
);
|
||||
assert_eq!(first_output.exit_code.unwrap_or(0), 0);
|
||||
assert!(
|
||||
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: "touch allow-prefix.txt",
|
||||
}
|
||||
.prepare(&test, &server, call_id_second, false)
|
||||
.await?;
|
||||
assert_eq!(second_command.as_deref(), Some(expected_command.as_str()));
|
||||
|
||||
let _ = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-allow-prefix-3"),
|
||||
second_event,
|
||||
ev_completed("resp-allow-prefix-3"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let second_results = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-allow-prefix-2", "done"),
|
||||
ev_completed("resp-allow-prefix-4"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
submit_turn(
|
||||
&test,
|
||||
"allow-prefix-second",
|
||||
approval_policy,
|
||||
sandbox_policy.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
wait_for_completion_without_approval(&test).await;
|
||||
|
||||
let second_output = parse_result(
|
||||
&second_results
|
||||
.single_request()
|
||||
.function_call_output(call_id_second),
|
||||
);
|
||||
assert_eq!(second_output.exit_code.unwrap_or(0), 0);
|
||||
assert!(
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ async fn run_codex_tool_session_inner(
|
||||
call_id,
|
||||
reason: _,
|
||||
risk,
|
||||
allow_prefix: _,
|
||||
parsed_cmd,
|
||||
}) => {
|
||||
handle_exec_approval_request(
|
||||
|
||||
@@ -352,7 +352,7 @@ impl OtelEventManager {
|
||||
&self,
|
||||
tool_name: &str,
|
||||
call_id: &str,
|
||||
decision: ReviewDecision,
|
||||
decision: &ReviewDecision,
|
||||
source: ToolDecisionSource,
|
||||
) {
|
||||
tracing::event!(
|
||||
@@ -369,7 +369,7 @@ impl OtelEventManager {
|
||||
slug = %self.metadata.slug,
|
||||
tool_name = %tool_name,
|
||||
call_id = %call_id,
|
||||
decision = %decision.to_string().to_lowercase(),
|
||||
decision = %decision.clone().to_string().to_lowercase(),
|
||||
source = %source.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ pub struct ExecApprovalRequestEvent {
|
||||
/// Optional model-provided risk assessment describing the blocked command.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub risk: Option<SandboxCommandAssessment>,
|
||||
/// Prefix rule that can be added to the user's execpolicy to allow future runs.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional, type = "Array<string>")]
|
||||
pub allow_prefix: Option<Vec<String>>,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1649,14 +1649,16 @@ pub struct SessionConfiguredEvent {
|
||||
}
|
||||
|
||||
/// User's decision in response to an ExecApprovalRequest.
|
||||
#[derive(
|
||||
Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS,
|
||||
)]
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReviewDecision {
|
||||
/// User has approved this command and the agent should execute it.
|
||||
Approved,
|
||||
|
||||
/// User has approved this command and wants to add the command prefix to
|
||||
/// the execpolicy allow list so future matching commands are permitted.
|
||||
ApprovedAllowPrefix { allow_prefix: Vec<String> },
|
||||
|
||||
/// User has approved this command and wants to automatically approve any
|
||||
/// future identical instances (`command` and `cwd` match exactly) for the
|
||||
/// remainder of the session.
|
||||
|
||||
@@ -43,6 +43,7 @@ pub(crate) enum ApprovalRequest {
|
||||
command: Vec<String>,
|
||||
reason: Option<String>,
|
||||
risk: Option<SandboxCommandAssessment>,
|
||||
allow_prefix: Option<Vec<String>>,
|
||||
},
|
||||
ApplyPatch {
|
||||
id: String,
|
||||
@@ -104,8 +105,8 @@ impl ApprovalOverlay {
|
||||
header: Box<dyn Renderable>,
|
||||
) -> (Vec<ApprovalOption>, SelectionViewParams) {
|
||||
let (options, title) = match &variant {
|
||||
ApprovalVariant::Exec { .. } => (
|
||||
exec_options(),
|
||||
ApprovalVariant::Exec { allow_prefix, .. } => (
|
||||
exec_options(allow_prefix.clone()),
|
||||
"Would you like to run the following command?".to_string(),
|
||||
),
|
||||
ApprovalVariant::ApplyPatch { .. } => (
|
||||
@@ -160,12 +161,12 @@ impl ApprovalOverlay {
|
||||
return;
|
||||
};
|
||||
if let Some(variant) = self.current_variant.as_ref() {
|
||||
match (&variant, &option.decision) {
|
||||
(ApprovalVariant::Exec { id, command }, ApprovalDecision::Review(decision)) => {
|
||||
self.handle_exec_decision(id, command, *decision);
|
||||
match (variant, &option.decision) {
|
||||
(ApprovalVariant::Exec { id, command, .. }, ApprovalDecision::Review(decision)) => {
|
||||
self.handle_exec_decision(id, command, decision.clone());
|
||||
}
|
||||
(ApprovalVariant::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => {
|
||||
self.handle_patch_decision(id, *decision);
|
||||
self.handle_patch_decision(id, decision.clone());
|
||||
}
|
||||
(
|
||||
ApprovalVariant::McpElicitation {
|
||||
@@ -185,7 +186,7 @@ impl ApprovalOverlay {
|
||||
}
|
||||
|
||||
fn handle_exec_decision(&self, id: &str, command: &[String], decision: ReviewDecision) {
|
||||
let cell = history_cell::new_approval_decision_cell(command.to_vec(), decision);
|
||||
let cell = history_cell::new_approval_decision_cell(command.to_vec(), decision.clone());
|
||||
self.app_event_tx.send(AppEvent::InsertHistoryCell(cell));
|
||||
self.app_event_tx.send(AppEvent::CodexOp(Op::ExecApproval {
|
||||
id: id.to_string(),
|
||||
@@ -273,7 +274,7 @@ impl BottomPaneView for ApprovalOverlay {
|
||||
&& let Some(variant) = self.current_variant.as_ref()
|
||||
{
|
||||
match &variant {
|
||||
ApprovalVariant::Exec { id, command } => {
|
||||
ApprovalVariant::Exec { id, command, .. } => {
|
||||
self.handle_exec_decision(id, command, ReviewDecision::Abort);
|
||||
}
|
||||
ApprovalVariant::ApplyPatch { id, .. } => {
|
||||
@@ -336,6 +337,7 @@ impl From<ApprovalRequest> for ApprovalRequestState {
|
||||
command,
|
||||
reason,
|
||||
risk,
|
||||
allow_prefix,
|
||||
} => {
|
||||
let reason = reason.filter(|item| !item.is_empty());
|
||||
let has_reason = reason.is_some();
|
||||
@@ -355,7 +357,11 @@ impl From<ApprovalRequest> for ApprovalRequestState {
|
||||
}
|
||||
header.extend(full_cmd_lines);
|
||||
Self {
|
||||
variant: ApprovalVariant::Exec { id, command },
|
||||
variant: ApprovalVariant::Exec {
|
||||
id,
|
||||
command,
|
||||
allow_prefix,
|
||||
},
|
||||
header: Box::new(Paragraph::new(header).wrap(Wrap { trim: false })),
|
||||
}
|
||||
}
|
||||
@@ -431,6 +437,7 @@ enum ApprovalVariant {
|
||||
Exec {
|
||||
id: String,
|
||||
command: Vec<String>,
|
||||
allow_prefix: Option<Vec<String>>,
|
||||
},
|
||||
ApplyPatch {
|
||||
id: String,
|
||||
@@ -463,7 +470,7 @@ impl ApprovalOption {
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_options() -> Vec<ApprovalOption> {
|
||||
fn exec_options(allow_prefix: Option<Vec<String>>) -> Vec<ApprovalOption> {
|
||||
vec![
|
||||
ApprovalOption {
|
||||
label: "Yes, proceed".to_string(),
|
||||
@@ -472,18 +479,28 @@ fn exec_options() -> Vec<ApprovalOption> {
|
||||
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))],
|
||||
},
|
||||
ApprovalOption {
|
||||
label: "Yes, and don't ask again for this command".to_string(),
|
||||
label: "Yes, and don't ask again this session".to_string(),
|
||||
decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession),
|
||||
display_shortcut: None,
|
||||
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('a'))],
|
||||
},
|
||||
ApprovalOption {
|
||||
label: "No, and tell Codex what to do differently".to_string(),
|
||||
decision: ApprovalDecision::Review(ReviewDecision::Abort),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Esc)),
|
||||
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))],
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.chain(allow_prefix.map(|prefix| ApprovalOption {
|
||||
label: "Yes, and don't ask again for commands with this prefix".to_string(),
|
||||
decision: ApprovalDecision::Review(ReviewDecision::ApprovedAllowPrefix {
|
||||
allow_prefix: prefix,
|
||||
}),
|
||||
display_shortcut: None,
|
||||
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))],
|
||||
}))
|
||||
.chain([ApprovalOption {
|
||||
label: "No, and tell Codex what to do differently".to_string(),
|
||||
decision: ApprovalDecision::Review(ReviewDecision::Abort),
|
||||
display_shortcut: Some(key_hint::plain(KeyCode::Esc)),
|
||||
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))],
|
||||
}])
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn patch_options() -> Vec<ApprovalOption> {
|
||||
@@ -539,6 +556,7 @@ mod tests {
|
||||
command: vec!["echo".to_string(), "hi".to_string()],
|
||||
reason: Some("reason".to_string()),
|
||||
risk: None,
|
||||
allow_prefix: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,6 +589,40 @@ mod tests {
|
||||
assert!(saw_op, "expected approval decision to emit an op");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_prefix_option_emits_allow_prefix() {
|
||||
let (tx, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let mut view = ApprovalOverlay::new(
|
||||
ApprovalRequest::Exec {
|
||||
id: "test".to_string(),
|
||||
command: vec!["echo".to_string()],
|
||||
reason: None,
|
||||
risk: None,
|
||||
allow_prefix: Some(vec!["echo".to_string()]),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE));
|
||||
let mut saw_op = false;
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
if let AppEvent::CodexOp(Op::ExecApproval { decision, .. }) = ev {
|
||||
assert_eq!(
|
||||
decision,
|
||||
ReviewDecision::ApprovedAllowPrefix {
|
||||
allow_prefix: vec!["echo".to_string()]
|
||||
}
|
||||
);
|
||||
saw_op = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
saw_op,
|
||||
"expected approval decision to emit an op with allow prefix"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_includes_command_snippet() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
@@ -581,6 +633,7 @@ mod tests {
|
||||
command,
|
||||
reason: None,
|
||||
risk: None,
|
||||
allow_prefix: None,
|
||||
};
|
||||
|
||||
let view = ApprovalOverlay::new(exec_request, tx);
|
||||
|
||||
@@ -570,6 +570,7 @@ mod tests {
|
||||
command: vec!["echo".into(), "ok".into()],
|
||||
reason: None,
|
||||
risk: None,
|
||||
allow_prefix: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1074,6 +1074,7 @@ impl ChatWidget {
|
||||
command: ev.command,
|
||||
reason: ev.reason,
|
||||
risk: ev.risk,
|
||||
allow_prefix: ev.allow_prefix,
|
||||
};
|
||||
self.bottom_pane.push_approval_request(request);
|
||||
self.request_redraw();
|
||||
|
||||
@@ -10,7 +10,8 @@ expression: terminal.backend().vt100().screen().contents()
|
||||
$ echo hello world
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. Yes, and don't ask again for this command (a)
|
||||
3. No, and tell Codex what to do differently (esc)
|
||||
2. Yes, and don't ask again this session (a)
|
||||
3. Yes, and don't ask again for commands with this prefix (p)
|
||||
4. No, and tell Codex what to do differently (esc)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
|
||||
@@ -7,7 +7,8 @@ expression: terminal.backend().vt100().screen().contents()
|
||||
$ echo hello world
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. Yes, and don't ask again for this command (a)
|
||||
3. No, and tell Codex what to do differently (esc)
|
||||
2. Yes, and don't ask again this session (a)
|
||||
3. Yes, and don't ask again for commands with this prefix (p)
|
||||
4. No, and tell Codex what to do differently (esc)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
|
||||
@@ -15,7 +15,7 @@ Buffer {
|
||||
" $ echo hello world ",
|
||||
" ",
|
||||
"› 1. Yes, proceed (y) ",
|
||||
" 2. Yes, and don't ask again for this command (a) ",
|
||||
" 2. Yes, and don't ask again this session (a) ",
|
||||
" 3. No, and tell Codex what to do differently (esc) ",
|
||||
" ",
|
||||
" Press enter to confirm or esc to cancel ",
|
||||
@@ -30,8 +30,8 @@ Buffer {
|
||||
x: 7, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 9, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 21, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 48, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 49, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 44, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 45, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 48, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 51, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
assertion_line: 1548
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
@@ -13,7 +12,8 @@ expression: terminal.backend()
|
||||
" $ echo 'hello world' "
|
||||
" "
|
||||
"› 1. Yes, proceed (y) "
|
||||
" 2. Yes, and don't ask again for this command (a) "
|
||||
" 3. No, and tell Codex what to do differently (esc) "
|
||||
" 2. Yes, and don't ask again this session (a) "
|
||||
" 3. Yes, and don't ask again for commands with this prefix (p) "
|
||||
" 4. No, and tell Codex what to do differently (esc) "
|
||||
" "
|
||||
" Press enter to confirm or esc to cancel "
|
||||
|
||||
@@ -688,6 +688,7 @@ fn exec_approval_emits_proposed_command_and_decision_history() {
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
risk: None,
|
||||
allow_prefix: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
@@ -732,6 +733,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
risk: None,
|
||||
allow_prefix: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
@@ -782,6 +784,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
reason: None,
|
||||
risk: None,
|
||||
allow_prefix: None,
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
@@ -1990,6 +1993,7 @@ fn approval_modal_exec_snapshot() {
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
risk: None,
|
||||
allow_prefix: Some(vec!["echo".into(), "hello".into(), "world".into()]),
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
@@ -2036,6 +2040,7 @@ fn approval_modal_exec_without_reason_snapshot() {
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
reason: None,
|
||||
risk: None,
|
||||
allow_prefix: Some(vec!["echo".into(), "hello".into(), "world".into()]),
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
@@ -2249,6 +2254,7 @@ 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,
|
||||
allow_prefix: Some(vec!["echo".into(), "hello world".into()]),
|
||||
parsed_cmd: vec![],
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
|
||||
@@ -409,6 +409,19 @@ pub fn new_approval_decision_cell(
|
||||
],
|
||||
)
|
||||
}
|
||||
ApprovedAllowPrefix { .. } => {
|
||||
let snippet = Span::from(exec_snippet(&command)).dim();
|
||||
(
|
||||
"✔ ".green(),
|
||||
vec![
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
" codex to run ".into(),
|
||||
snippet,
|
||||
" and added its prefix to your allow list".bold(),
|
||||
],
|
||||
)
|
||||
}
|
||||
ApprovedForSession => {
|
||||
let snippet = Span::from(exec_snippet(&command)).dim();
|
||||
(
|
||||
|
||||
@@ -603,7 +603,7 @@ metadata above):
|
||||
- `codex.tool_decision`
|
||||
- `tool_name`
|
||||
- `call_id`
|
||||
- `decision` (`approved`, `approved_for_session`, `denied`, or `abort`)
|
||||
- `decision` (`approved`, `approved_allow_prefix`, `approved_for_session`, `denied`, or `abort`)
|
||||
- `source` (`config` or `user`)
|
||||
- `codex.tool_result`
|
||||
- `tool_name`
|
||||
|
||||
Reference in New Issue
Block a user