feat(core) RequestRule (#9489)

## Summary
Instead of trying to derive the prefix_rule for a command mechanically,
let's let the model decide for us.

## Testing
- [x] tested locally
This commit is contained in:
Dylan Hurd
2026-01-28 01:43:17 -07:00
committed by GitHub
Unverified
parent 9f79365691
commit 996e09ca24
20 changed files with 696 additions and 302 deletions
+61 -10
View File
@@ -190,6 +190,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::render_command_prefix_list;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::user_input::UserInput;
@@ -1226,6 +1227,8 @@ impl Session {
DeveloperInstructions::from_policy(
&next.sandbox_policy,
next.approval_policy,
self.services.exec_policy.current().as_ref(),
self.features.enabled(Feature::RequestRule),
&next.cwd,
)
.into(),
@@ -1416,6 +1419,44 @@ impl Session {
Ok(())
}
async fn turn_context_for_sub_id(&self, sub_id: &str) -> Option<Arc<TurnContext>> {
let active = self.active_turn.lock().await;
active
.as_ref()
.and_then(|turn| turn.tasks.get(sub_id))
.map(|task| Arc::clone(&task.turn_context))
}
pub(crate) async fn record_execpolicy_amendment_message(
&self,
sub_id: &str,
amendment: &ExecPolicyAmendment,
) {
let Some(prefixes) = render_command_prefix_list([amendment.command.as_slice()]) else {
warn!("execpolicy amendment for {sub_id} had no command prefix");
return;
};
let text = format!("Approved command prefix saved:\n{prefixes}");
let message: ResponseItem = DeveloperInstructions::new(text.clone()).into();
if let Some(turn_context) = self.turn_context_for_sub_id(sub_id).await {
self.record_conversation_items(&turn_context, std::slice::from_ref(&message))
.await;
return;
}
if self
.inject_response_items(vec![ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText { text }],
}])
.await
.is_err()
{
warn!("no active turn found to record execpolicy amendment message for {sub_id}");
}
}
/// 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
@@ -1749,6 +1790,8 @@ impl Session {
DeveloperInstructions::from_policy(
&turn_context.sandbox_policy,
turn_context.approval_policy,
self.services.exec_policy.current().as_ref(),
self.features.enabled(Feature::RequestRule),
&turn_context.cwd,
)
.into(),
@@ -2595,18 +2638,26 @@ mod handlers {
if let ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment,
} = &decision
&& let Err(err) = sess
{
match sess
.persist_execpolicy_amendment(proposed_execpolicy_amendment)
.await
{
let message = format!("Failed to apply execpolicy amendment: {err}");
tracing::warn!("{message}");
let warning = EventMsg::Warning(WarningEvent { message });
sess.send_event_raw(Event {
id: id.clone(),
msg: warning,
})
.await;
{
Ok(()) => {
sess.record_execpolicy_amendment_message(&id, proposed_execpolicy_amendment)
.await;
}
Err(err) => {
let message = format!("Failed to apply execpolicy amendment: {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 => {
+215 -120
View File
@@ -87,6 +87,15 @@ pub(crate) struct ExecPolicyManager {
policy: ArcSwap<Policy>,
}
pub(crate) struct ExecApprovalRequest<'a> {
pub(crate) features: &'a Features,
pub(crate) command: &'a [String],
pub(crate) approval_policy: AskForApproval,
pub(crate) sandbox_policy: &'a SandboxPolicy,
pub(crate) sandbox_permissions: SandboxPermissions,
pub(crate) prefix_rule: Option<Vec<String>>,
}
impl ExecPolicyManager {
pub(crate) fn new(policy: Arc<Policy>) -> Self {
Self {
@@ -112,12 +121,16 @@ impl ExecPolicyManager {
pub(crate) async fn create_exec_approval_requirement_for_command(
&self,
features: &Features,
command: &[String],
approval_policy: AskForApproval,
sandbox_policy: &SandboxPolicy,
sandbox_permissions: SandboxPermissions,
req: ExecApprovalRequest<'_>,
) -> ExecApprovalRequirement {
let ExecApprovalRequest {
features,
command,
approval_policy,
sandbox_policy,
sandbox_permissions,
prefix_rule,
} = req;
let exec_policy = self.current();
let commands =
parse_shell_lc_plain_commands(command).unwrap_or_else(|| vec![command.to_vec()]);
@@ -131,6 +144,12 @@ impl ExecPolicyManager {
};
let evaluation = exec_policy.check_multiple(commands.iter(), &exec_policy_fallback);
let requested_amendment = derive_requested_execpolicy_amendment(
features,
prefix_rule.as_ref(),
&evaluation.matched_rules,
);
match evaluation.decision {
Decision::Forbidden => ExecApprovalRequirement::Forbidden {
reason: derive_forbidden_reason(command, &evaluation),
@@ -144,9 +163,11 @@ impl ExecPolicyManager {
ExecApprovalRequirement::NeedsApproval {
reason: derive_prompt_reason(command, &evaluation),
proposed_execpolicy_amendment: if features.enabled(Feature::ExecPolicy) {
try_derive_execpolicy_amendment_for_prompt_rules(
&evaluation.matched_rules,
)
requested_amendment.or_else(|| {
try_derive_execpolicy_amendment_for_prompt_rules(
&evaluation.matched_rules,
)
})
} else {
None
},
@@ -382,6 +403,30 @@ fn try_derive_execpolicy_amendment_for_allow_rules(
})
}
fn derive_requested_execpolicy_amendment(
features: &Features,
prefix_rule: Option<&Vec<String>>,
matched_rules: &[RuleMatch],
) -> Option<ExecPolicyAmendment> {
if !features.enabled(Feature::ExecPolicy) {
return None;
}
let prefix_rule = prefix_rule?;
if prefix_rule.is_empty() {
return None;
}
if matched_rules
.iter()
.any(|rule_match| is_policy_match(rule_match) && rule_match.decision() == Decision::Prompt)
{
return None;
}
Some(ExecPolicyAmendment::new(prefix_rule.clone()))
}
/// Only return a reason when a policy rule drove the prompt decision.
fn derive_prompt_reason(command_args: &[String], evaluation: &Evaluation) -> Option<String> {
let command = render_shlex_command(command_args);
@@ -756,13 +801,14 @@ prefix_rule(pattern=["rm"], decision="forbidden")
let manager = ExecPolicyManager::new(policy);
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&forbidden_script,
AskForApproval::OnRequest,
&SandboxPolicy::DangerFullAccess,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &forbidden_script,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -790,17 +836,18 @@ prefix_rule(
let manager = ExecPolicyManager::new(policy);
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&[
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &[
"rm".to_string(),
"-rf".to_string(),
"/some/important/folder".to_string(),
],
AskForApproval::OnRequest,
&SandboxPolicy::DangerFullAccess,
SandboxPermissions::UseDefault,
)
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -823,13 +870,14 @@ prefix_rule(
let manager = ExecPolicyManager::new(policy);
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::OnRequest,
&SandboxPolicy::DangerFullAccess,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -853,13 +901,14 @@ prefix_rule(
let manager = ExecPolicyManager::new(policy);
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::Never,
&SandboxPolicy::DangerFullAccess,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::Never,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -876,13 +925,14 @@ prefix_rule(
let manager = ExecPolicyManager::default();
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::UnlessTrusted,
&SandboxPolicy::ReadOnly,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -894,6 +944,40 @@ prefix_rule(
);
}
#[tokio::test]
async fn request_rule_uses_prefix_rule() {
let command = vec![
"cargo".to_string(),
"install".to_string(),
"cargo-insta".to_string(),
];
let manager = ExecPolicyManager::default();
let mut features = Features::with_defaults();
features.enable(Feature::RequestRule);
let requirement = manager
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &features,
command: &command,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
})
.await;
assert_eq!(
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"cargo".to_string(),
"install".to_string(),
])),
}
);
}
#[tokio::test]
async fn heuristics_apply_when_other_commands_match_policy() {
let policy_src = r#"prefix_rule(pattern=["apple"], decision="allow")"#;
@@ -910,13 +994,14 @@ prefix_rule(
assert_eq!(
ExecPolicyManager::new(policy)
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::UnlessTrusted,
&SandboxPolicy::DangerFullAccess,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await,
ExecApprovalRequirement::NeedsApproval {
reason: None,
@@ -984,13 +1069,14 @@ prefix_rule(
let manager = ExecPolicyManager::default();
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::UnlessTrusted,
&SandboxPolicy::ReadOnly,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -1011,13 +1097,14 @@ prefix_rule(
let manager = ExecPolicyManager::default();
let requirement = manager
.create_exec_approval_requirement_for_command(
&features,
&command,
AskForApproval::UnlessTrusted,
&SandboxPolicy::ReadOnly,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &features,
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -1041,13 +1128,14 @@ prefix_rule(
let manager = ExecPolicyManager::new(policy);
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::OnRequest,
&SandboxPolicy::DangerFullAccess,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -1068,13 +1156,14 @@ prefix_rule(
];
let manager = ExecPolicyManager::default();
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::UnlessTrusted,
&SandboxPolicy::ReadOnly,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -1106,13 +1195,14 @@ prefix_rule(
assert_eq!(
ExecPolicyManager::new(policy)
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::UnlessTrusted,
&SandboxPolicy::ReadOnly,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await,
ExecApprovalRequirement::NeedsApproval {
reason: None,
@@ -1129,13 +1219,14 @@ prefix_rule(
let manager = ExecPolicyManager::default();
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::OnRequest,
&SandboxPolicy::ReadOnly,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -1159,13 +1250,14 @@ prefix_rule(
let manager = ExecPolicyManager::new(policy);
let requirement = manager
.create_exec_approval_requirement_for_command(
&Features::with_defaults(),
&command,
AskForApproval::OnRequest,
&SandboxPolicy::ReadOnly,
SandboxPermissions::UseDefault,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &Features::with_defaults(),
command: &command,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: SandboxPermissions::UseDefault,
prefix_rule: None,
})
.await;
assert_eq!(
@@ -1226,13 +1318,14 @@ prefix_rule(
assert_eq!(
expected_req,
policy
.create_exec_approval_requirement_for_command(
&features,
&sneaky_command,
AskForApproval::OnRequest,
&SandboxPolicy::ReadOnly,
permissions,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &features,
command: &sneaky_command,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: permissions,
prefix_rule: None,
})
.await,
"{pwsh_approval_reason}"
);
@@ -1249,13 +1342,14 @@ prefix_rule(
]))),
},
policy
.create_exec_approval_requirement_for_command(
&features,
&dangerous_command,
AskForApproval::OnRequest,
&SandboxPolicy::ReadOnly,
permissions,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &features,
command: &dangerous_command,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: permissions,
prefix_rule: None,
})
.await,
r#"On all platforms, a forbidden command should require approval
(unless AskForApproval::Never is specified)."#
@@ -1268,13 +1362,14 @@ prefix_rule(
reason: "`rm -rf /important/data` rejected: blocked by policy".to_string(),
},
policy
.create_exec_approval_requirement_for_command(
&features,
&dangerous_command,
AskForApproval::Never,
&SandboxPolicy::ReadOnly,
permissions,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &features,
command: &dangerous_command,
approval_policy: AskForApproval::Never,
sandbox_policy: &SandboxPolicy::ReadOnly,
sandbox_permissions: permissions,
prefix_rule: None,
})
.await,
r#"On all platforms, a forbidden command should require approval
(unless AskForApproval::Never is specified)."#
+8
View File
@@ -89,6 +89,8 @@ pub enum Feature {
WebSearchCached,
/// Gate the execpolicy enforcement for shell/unified exec.
ExecPolicy,
/// Allow the model to request approval and propose exec rules.
RequestRule,
/// Enable Windows sandbox (restricted token) on Windows.
WindowsSandbox,
/// Use the elevated Windows sandbox pipeline (setup + runner).
@@ -393,6 +395,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment,
default_enabled: true,
},
FeatureSpec {
id: Feature::RequestRule,
key: "request_rule",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::WindowsSandbox,
key: "experimental_windows_sandbox",
+71 -42
View File
@@ -6,6 +6,7 @@ use std::sync::Arc;
use crate::codex::TurnContext;
use crate::exec::ExecParams;
use crate::exec_env::create_env;
use crate::exec_policy::ExecApprovalRequest;
use crate::function_tool::FunctionCallError;
use crate::is_safe_command::is_known_safe_command;
use crate::protocol::ExecCommandSource;
@@ -28,16 +29,27 @@ pub struct ShellHandler;
pub struct ShellCommandHandler;
struct RunExecLikeArgs {
tool_name: String,
exec_params: ExecParams,
prefix_rule: Option<Vec<String>>,
session: Arc<crate::codex::Session>,
turn: Arc<TurnContext>,
tracker: crate::tools::context::SharedTurnDiffTracker,
call_id: String,
freeform: bool,
}
impl ShellHandler {
fn to_exec_params(params: ShellToolCallParams, turn_context: &TurnContext) -> ExecParams {
fn to_exec_params(params: &ShellToolCallParams, turn_context: &TurnContext) -> ExecParams {
ExecParams {
command: params.command,
command: params.command.clone(),
cwd: turn_context.resolve_path(params.workdir.clone()),
expiration: params.timeout_ms.into(),
env: create_env(&turn_context.shell_environment_policy),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: params.justification,
justification: params.justification.clone(),
arg0: None,
}
}
@@ -50,7 +62,7 @@ impl ShellCommandHandler {
}
fn to_exec_params(
params: ShellCommandToolCallParams,
params: &ShellCommandToolCallParams,
session: &crate::codex::Session,
turn_context: &TurnContext,
) -> ExecParams {
@@ -64,7 +76,7 @@ impl ShellCommandHandler {
env: create_env(&turn_context.shell_environment_policy),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: params.justification,
justification: params.justification.clone(),
arg0: None,
}
}
@@ -108,29 +120,32 @@ impl ToolHandler for ShellHandler {
match payload {
ToolPayload::Function { arguments } => {
let params: ShellToolCallParams = parse_arguments(&arguments)?;
let exec_params = Self::to_exec_params(params, turn.as_ref());
Self::run_exec_like(
tool_name.as_str(),
let prefix_rule = params.prefix_rule.clone();
let exec_params = Self::to_exec_params(&params, turn.as_ref());
Self::run_exec_like(RunExecLikeArgs {
tool_name: tool_name.clone(),
exec_params,
prefix_rule,
session,
turn,
tracker,
call_id,
false,
)
freeform: false,
})
.await
}
ToolPayload::LocalShell { params } => {
let exec_params = Self::to_exec_params(params, turn.as_ref());
Self::run_exec_like(
tool_name.as_str(),
let exec_params = Self::to_exec_params(&params, turn.as_ref());
Self::run_exec_like(RunExecLikeArgs {
tool_name: tool_name.clone(),
exec_params,
prefix_rule: None,
session,
turn,
tracker,
call_id,
false,
)
freeform: false,
})
.await
}
_ => Err(FunctionCallError::RespondToModel(format!(
@@ -181,30 +196,43 @@ impl ToolHandler for ShellCommandHandler {
};
let params: ShellCommandToolCallParams = parse_arguments(&arguments)?;
let exec_params = Self::to_exec_params(params, session.as_ref(), turn.as_ref());
ShellHandler::run_exec_like(
tool_name.as_str(),
let prefix_rule = params.prefix_rule.clone();
let exec_params = Self::to_exec_params(&params, session.as_ref(), turn.as_ref());
ShellHandler::run_exec_like(RunExecLikeArgs {
tool_name,
exec_params,
prefix_rule,
session,
turn,
tracker,
call_id,
true,
)
freeform: true,
})
.await
}
}
impl ShellHandler {
async fn run_exec_like(
tool_name: &str,
exec_params: ExecParams,
session: Arc<crate::codex::Session>,
turn: Arc<TurnContext>,
tracker: crate::tools::context::SharedTurnDiffTracker,
call_id: String,
freeform: bool,
) -> Result<ToolOutput, FunctionCallError> {
async fn run_exec_like(args: RunExecLikeArgs) -> Result<ToolOutput, FunctionCallError> {
let RunExecLikeArgs {
tool_name,
exec_params,
prefix_rule,
session,
turn,
tracker,
call_id,
freeform,
} = args;
let features = session.features();
let request_rule_enabled = features.enabled(crate::features::Feature::RequestRule);
let prefix_rule = if request_rule_enabled {
prefix_rule
} else {
None
};
// Approval policy guard for explicit escalation in non-OnRequest modes.
if exec_params
.sandbox_permissions
@@ -214,9 +242,9 @@ impl ShellHandler {
codex_protocol::protocol::AskForApproval::OnRequest
)
{
let approval_policy = turn.approval_policy;
return Err(FunctionCallError::RespondToModel(format!(
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
policy = turn.approval_policy
"approval policy is {approval_policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {approval_policy:?}"
)));
}
@@ -229,7 +257,7 @@ impl ShellHandler {
turn.as_ref(),
Some(&tracker),
&call_id,
tool_name,
tool_name.as_str(),
)
.await?
{
@@ -246,17 +274,17 @@ impl ShellHandler {
let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None);
emitter.begin(event_ctx).await;
let features = session.features();
let exec_approval_requirement = session
.services
.exec_policy
.create_exec_approval_requirement_for_command(
&features,
&exec_params.command,
turn.approval_policy,
&turn.sandbox_policy,
exec_params.sandbox_permissions,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &features,
command: &exec_params.command,
approval_policy: turn.approval_policy,
sandbox_policy: &turn.sandbox_policy,
sandbox_permissions: exec_params.sandbox_permissions,
prefix_rule,
})
.await;
let req = ShellRequest {
@@ -274,7 +302,7 @@ impl ShellHandler {
session: session.as_ref(),
turn: turn.as_ref(),
call_id: call_id.clone(),
tool_name: tool_name.to_string(),
tool_name,
};
let out = orchestrator
.run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy)
@@ -377,10 +405,11 @@ mod tests {
login,
timeout_ms,
sandbox_permissions: Some(sandbox_permissions),
prefix_rule: None,
justification: justification.clone(),
};
let exec_params = ShellCommandHandler::to_exec_params(params, &session, &turn_context);
let exec_params = ShellCommandHandler::to_exec_params(&params, &session, &turn_context);
// ExecParams cannot derive Eq due to the CancellationToken field, so we manually compare the fields.
assert_eq!(exec_params.command, expected_command);
@@ -43,6 +43,8 @@ struct ExecCommandArgs {
sandbox_permissions: SandboxPermissions,
#[serde(default)]
justification: Option<String>,
#[serde(default)]
prefix_rule: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
@@ -135,19 +137,28 @@ impl ToolHandler for UnifiedExecHandler {
max_output_tokens,
sandbox_permissions,
justification,
prefix_rule,
..
} = args;
let features = session.features();
let request_rule_enabled = features.enabled(crate::features::Feature::RequestRule);
let prefix_rule = if request_rule_enabled {
prefix_rule
} else {
None
};
if sandbox_permissions.requires_escalated_permissions()
&& !matches!(
context.turn.approval_policy,
codex_protocol::protocol::AskForApproval::OnRequest
)
{
let approval_policy = context.turn.approval_policy;
manager.release_process_id(&process_id).await;
return Err(FunctionCallError::RespondToModel(format!(
"approval policy is {policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {policy:?}",
policy = context.turn.approval_policy
"approval policy is {approval_policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {approval_policy:?}"
)));
}
@@ -183,6 +194,7 @@ impl ToolHandler for UnifiedExecHandler {
tty,
sandbox_permissions,
justification,
prefix_rule,
},
&context,
)
+1
View File
@@ -114,6 +114,7 @@ impl ToolRouter {
workdir: exec.working_directory,
timeout_ms: exec.timeout_ms,
sandbox_permissions: Some(SandboxPermissions::UseDefault),
prefix_rule: None,
justification: None,
};
Ok(Some(ToolCall {
+60 -54
View File
@@ -30,6 +30,7 @@ pub(crate) struct ToolsConfig {
pub web_search_mode: Option<WebSearchMode>,
pub collab_tools: bool,
pub collaboration_modes_tools: bool,
pub request_rule_enabled: bool,
pub experimental_supported_tools: Vec<String>,
}
@@ -49,6 +50,7 @@ impl ToolsConfig {
let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform);
let include_collab_tools = features.enabled(Feature::Collab);
let include_collaboration_modes_tools = features.enabled(Feature::CollaborationModes);
let request_rule_enabled = features.enabled(Feature::RequestRule);
let shell_type = if !features.enabled(Feature::ShellTool) {
ConfigShellToolType::Disabled
@@ -81,6 +83,7 @@ impl ToolsConfig {
web_search_mode: *web_search_mode,
collab_tools: include_collab_tools,
collaboration_modes_tools: include_collaboration_modes_tools,
request_rule_enabled,
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
}
}
@@ -142,8 +145,50 @@ impl From<JsonSchema> for AdditionalProperties {
}
}
fn create_exec_command_tool() -> ToolSpec {
let properties = BTreeMap::from([
fn create_approval_parameters(include_prefix_rule: bool) -> BTreeMap<String, JsonSchema> {
let mut properties = BTreeMap::from([
(
"sandbox_permissions".to_string(),
JsonSchema::String {
description: Some(
"Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
.to_string(),
),
},
),
(
"justification".to_string(),
JsonSchema::String {
description: Some(
r#"Only set if sandbox_permissions is \"require_escalated\".
Request approval from the user to run this command outside the sandbox.
Phrased as a simple question that summarizes the purpose of the
command as it relates to the task at hand - e.g. 'Do you want to
fetch and pull the latest version of this git branch?'"#
.to_string(),
),
},
),
]);
if include_prefix_rule {
properties.insert(
"prefix_rule".to_string(),
JsonSchema::Array {
items: Box::new(JsonSchema::String { description: None }),
description: Some(
r#"Only specify when sandbox_permissions is `require_escalated`.
Suggest a prefix command pattern that will allow you to fulfill similar requests from the user in the future.
Should be a short but reasonable prefix, e.g. [\"git\", \"pull\"] or [\"uv\", \"run\"] or [\"pytest\"]."#.to_string(),
),
});
}
properties
}
fn create_exec_command_tool(include_prefix_rule: bool) -> ToolSpec {
let mut properties = BTreeMap::from([
(
"cmd".to_string(),
JsonSchema::String {
@@ -199,25 +244,8 @@ fn create_exec_command_tool() -> ToolSpec {
),
},
),
(
"sandbox_permissions".to_string(),
JsonSchema::String {
description: Some(
"Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
.to_string(),
),
},
),
(
"justification".to_string(),
JsonSchema::String {
description: Some(
"Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command."
.to_string(),
),
},
),
]);
properties.extend(create_approval_parameters(include_prefix_rule));
ToolSpec::Function(ResponsesApiTool {
name: "exec_command".to_string(),
@@ -280,8 +308,8 @@ fn create_write_stdin_tool() -> ToolSpec {
})
}
fn create_shell_tool() -> ToolSpec {
let properties = BTreeMap::from([
fn create_shell_tool(include_prefix_rule: bool) -> ToolSpec {
let mut properties = BTreeMap::from([
(
"command".to_string(),
JsonSchema::Array {
@@ -301,19 +329,8 @@ fn create_shell_tool() -> ToolSpec {
description: Some("The timeout for the command in milliseconds".to_string()),
},
),
(
"sandbox_permissions".to_string(),
JsonSchema::String {
description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()),
},
),
(
"justification".to_string(),
JsonSchema::String {
description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()),
},
),
]);
properties.extend(create_approval_parameters(include_prefix_rule));
let description = if cfg!(windows) {
r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"].
@@ -344,8 +361,8 @@ Examples of valid command strings:
})
}
fn create_shell_command_tool() -> ToolSpec {
let properties = BTreeMap::from([
fn create_shell_command_tool(include_prefix_rule: bool) -> ToolSpec {
let mut properties = BTreeMap::from([
(
"command".to_string(),
JsonSchema::String {
@@ -375,19 +392,8 @@ fn create_shell_command_tool() -> ToolSpec {
description: Some("The timeout for the command in milliseconds".to_string()),
},
),
(
"sandbox_permissions".to_string(),
JsonSchema::String {
description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()),
},
),
(
"justification".to_string(),
JsonSchema::String {
description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()),
},
),
]);
properties.extend(create_approval_parameters(include_prefix_rule));
let description = if cfg!(windows) {
r#"Runs a Powershell command (Windows) and returns its output.
@@ -1292,13 +1298,13 @@ pub(crate) fn build_specs(
match &config.shell_type {
ConfigShellToolType::Default => {
builder.push_spec(create_shell_tool());
builder.push_spec(create_shell_tool(config.request_rule_enabled));
}
ConfigShellToolType::Local => {
builder.push_spec(ToolSpec::LocalShell {});
}
ConfigShellToolType::UnifiedExec => {
builder.push_spec(create_exec_command_tool());
builder.push_spec(create_exec_command_tool(config.request_rule_enabled));
builder.push_spec(create_write_stdin_tool());
builder.register_handler("exec_command", unified_exec_handler.clone());
builder.register_handler("write_stdin", unified_exec_handler);
@@ -1307,7 +1313,7 @@ pub(crate) fn build_specs(
// Do nothing.
}
ConfigShellToolType::ShellCommand => {
builder.push_spec(create_shell_command_tool());
builder.push_spec(create_shell_command_tool(config.request_rule_enabled));
}
}
@@ -1579,7 +1585,7 @@ mod tests {
// Build expected from the same helpers used by the builder.
let mut expected: BTreeMap<String, ToolSpec> = BTreeMap::from([]);
for spec in [
create_exec_command_tool(),
create_exec_command_tool(false),
create_write_stdin_tool(),
create_list_mcp_resources_tool(),
create_list_mcp_resource_templates_tool(),
@@ -2413,7 +2419,7 @@ mod tests {
#[test]
fn test_shell_tool() {
let tool = super::create_shell_tool();
let tool = super::create_shell_tool(false);
let ToolSpec::Function(ResponsesApiTool {
description, name, ..
}) = &tool
@@ -2443,7 +2449,7 @@ Examples of valid command strings:
#[test]
fn test_shell_command_tool() {
let tool = super::create_shell_command_tool();
let tool = super::create_shell_command_tool(false);
let ToolSpec::Function(ResponsesApiTool {
description, name, ..
}) = &tool
+2
View File
@@ -82,6 +82,7 @@ pub(crate) struct ExecCommandRequest {
pub tty: bool,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
pub prefix_rule: Option<Vec<String>>,
}
#[derive(Debug)]
@@ -205,6 +206,7 @@ mod tests {
tty: true,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
prefix_rule: None,
},
&context,
)
@@ -11,9 +11,9 @@ use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use crate::exec_env::create_env;
use crate::exec_policy::ExecApprovalRequest;
use crate::protocol::ExecCommandSource;
use crate::sandboxing::ExecEnv;
use crate::sandboxing::SandboxPermissions;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
use crate::tools::events::ToolEventStage;
@@ -123,14 +123,7 @@ impl UnifiedExecProcessManager {
.unwrap_or_else(|| context.turn.cwd.clone());
let process = self
.open_session_with_sandbox(
&request.command,
cwd.clone(),
request.sandbox_permissions,
request.justification,
request.tty,
context,
)
.open_session_with_sandbox(&request, cwd.clone(), context)
.await;
let process = match process {
@@ -486,11 +479,8 @@ impl UnifiedExecProcessManager {
pub(super) async fn open_session_with_sandbox(
&self,
command: &[String],
request: &ExecCommandRequest,
cwd: PathBuf,
sandbox_permissions: SandboxPermissions,
justification: Option<String>,
tty: bool,
context: &UnifiedExecContext,
) -> Result<UnifiedExecProcess, UnifiedExecError> {
let env = apply_unified_exec_env(create_env(&context.turn.shell_environment_policy));
@@ -501,21 +491,22 @@ impl UnifiedExecProcessManager {
.session
.services
.exec_policy
.create_exec_approval_requirement_for_command(
&features,
command,
context.turn.approval_policy,
&context.turn.sandbox_policy,
sandbox_permissions,
)
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
features: &features,
command: &request.command,
approval_policy: context.turn.approval_policy,
sandbox_policy: &context.turn.sandbox_policy,
sandbox_permissions: request.sandbox_permissions,
prefix_rule: request.prefix_rule.clone(),
})
.await;
let req = UnifiedExecToolRequest::new(
command.to_vec(),
request.command.clone(),
cwd,
env,
tty,
sandbox_permissions,
justification,
request.tty,
request.sandbox_permissions,
request.justification.clone(),
exec_approval_requirement,
);
let tool_ctx = ToolCtx {