Skip to content

Commit 5ba0f7b

Browse files
authored
Merge branch 'main' into add-github-action-for-nix
2 parents ecbd7fa + a7fda70 commit 5ba0f7b

File tree

5 files changed

+141
-135
lines changed

5 files changed

+141
-135
lines changed

codex-rs/core/src/bash.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ pub fn try_parse_word_only_commands_sequence(tree: &Tree, src: &str) -> Option<V
7373
}
7474
}
7575

76+
// Walk uses a stack (LIFO), so re-sort by position to restore source order.
77+
command_nodes.sort_by_key(|node| node.start_byte());
78+
7679
let mut commands = Vec::new();
7780
for node in command_nodes {
7881
if let Some(words) = parse_plain_command_from_node(node, src) {
@@ -150,10 +153,10 @@ mod tests {
150153
let src = "ls && pwd; echo 'hi there' | wc -l";
151154
let cmds = parse_seq(src).unwrap();
152155
let expected: Vec<Vec<String>> = vec![
153-
vec!["wc".to_string(), "-l".to_string()],
154-
vec!["echo".to_string(), "hi there".to_string()],
155-
vec!["pwd".to_string()],
156156
vec!["ls".to_string()],
157+
vec!["pwd".to_string()],
158+
vec!["echo".to_string(), "hi there".to_string()],
159+
vec!["wc".to_string(), "-l".to_string()],
157160
];
158161
assert_eq!(cmds, expected);
159162
}

codex-rs/core/src/codex.rs

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -456,8 +456,6 @@ impl Session {
456456
client,
457457
tools_config: ToolsConfig::new(&ToolsConfigParams {
458458
model_family: &config.model_family,
459-
approval_policy,
460-
sandbox_policy: sandbox_policy.clone(),
461459
include_plan_tool: config.include_plan_tool,
462460
include_apply_patch_tool: config.include_apply_patch_tool,
463461
include_web_search_request: config.tools_web_search_request,
@@ -1237,8 +1235,6 @@ async fn submission_loop(
12371235

12381236
let tools_config = ToolsConfig::new(&ToolsConfigParams {
12391237
model_family: &effective_family,
1240-
approval_policy: new_approval_policy,
1241-
sandbox_policy: new_sandbox_policy.clone(),
12421238
include_plan_tool: config.include_plan_tool,
12431239
include_apply_patch_tool: config.include_apply_patch_tool,
12441240
include_web_search_request: config.tools_web_search_request,
@@ -1325,8 +1321,6 @@ async fn submission_loop(
13251321
client,
13261322
tools_config: ToolsConfig::new(&ToolsConfigParams {
13271323
model_family: &model_family,
1328-
approval_policy,
1329-
sandbox_policy: sandbox_policy.clone(),
13301324
include_plan_tool: config.include_plan_tool,
13311325
include_apply_patch_tool: config.include_apply_patch_tool,
13321326
include_web_search_request: config.tools_web_search_request,
@@ -1553,8 +1547,6 @@ async fn spawn_review_thread(
15531547
.unwrap_or_else(|| parent_turn_context.client.get_model_family());
15541548
let tools_config = ToolsConfig::new(&ToolsConfigParams {
15551549
model_family: &review_model_family,
1556-
approval_policy: parent_turn_context.approval_policy,
1557-
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
15581550
include_plan_tool: false,
15591551
include_apply_patch_tool: config.include_apply_patch_tool,
15601552
include_web_search_request: false,
@@ -2724,6 +2716,21 @@ async fn handle_container_exec_with_params(
27242716
sub_id: String,
27252717
call_id: String,
27262718
) -> ResponseInputItem {
2719+
if params.with_escalated_permissions.unwrap_or(false)
2720+
&& !matches!(turn_context.approval_policy, AskForApproval::OnRequest)
2721+
{
2722+
return ResponseInputItem::FunctionCallOutput {
2723+
call_id,
2724+
output: FunctionCallOutputPayload {
2725+
content: format!(
2726+
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
2727+
policy = turn_context.approval_policy
2728+
),
2729+
success: None,
2730+
},
2731+
};
2732+
}
2733+
27272734
// check if this was a patch, and apply it if so
27282735
let apply_patch_exec = match maybe_parse_apply_patch_verified(&params.command, &params.cwd) {
27292736
MaybeApplyPatchVerified::Body(changes) => {
@@ -3345,6 +3352,7 @@ mod tests {
33453352
use mcp_types::ContentBlock;
33463353
use mcp_types::TextContent;
33473354
use pretty_assertions::assert_eq;
3355+
use serde::Deserialize;
33483356
use serde_json::json;
33493357
use std::path::PathBuf;
33503358
use std::sync::Arc;
@@ -3594,8 +3602,6 @@ mod tests {
35943602
);
35953603
let tools_config = ToolsConfig::new(&ToolsConfigParams {
35963604
model_family: &config.model_family,
3597-
approval_policy: config.approval_policy,
3598-
sandbox_policy: config.sandbox_policy.clone(),
35993605
include_plan_tool: config.include_plan_tool,
36003606
include_apply_patch_tool: config.include_apply_patch_tool,
36013607
include_web_search_request: config.tools_web_search_request,
@@ -3735,4 +3741,105 @@ mod tests {
37353741

37363742
(rollout_items, live_history.contents())
37373743
}
3744+
3745+
#[tokio::test]
3746+
async fn rejects_escalated_permissions_when_policy_not_on_request() {
3747+
use crate::exec::ExecParams;
3748+
use crate::protocol::AskForApproval;
3749+
use crate::protocol::SandboxPolicy;
3750+
use crate::turn_diff_tracker::TurnDiffTracker;
3751+
use std::collections::HashMap;
3752+
3753+
let (session, mut turn_context) = make_session_and_context();
3754+
// Ensure policy is NOT OnRequest so the early rejection path triggers
3755+
turn_context.approval_policy = AskForApproval::OnFailure;
3756+
3757+
let params = ExecParams {
3758+
command: if cfg!(windows) {
3759+
vec![
3760+
"cmd.exe".to_string(),
3761+
"/C".to_string(),
3762+
"echo hi".to_string(),
3763+
]
3764+
} else {
3765+
vec![
3766+
"/bin/sh".to_string(),
3767+
"-c".to_string(),
3768+
"echo hi".to_string(),
3769+
]
3770+
},
3771+
cwd: turn_context.cwd.clone(),
3772+
timeout_ms: Some(1000),
3773+
env: HashMap::new(),
3774+
with_escalated_permissions: Some(true),
3775+
justification: Some("test".to_string()),
3776+
};
3777+
3778+
let params2 = ExecParams {
3779+
with_escalated_permissions: Some(false),
3780+
..params.clone()
3781+
};
3782+
3783+
let mut turn_diff_tracker = TurnDiffTracker::new();
3784+
3785+
let sub_id = "test-sub".to_string();
3786+
let call_id = "test-call".to_string();
3787+
3788+
let resp = handle_container_exec_with_params(
3789+
params,
3790+
&session,
3791+
&turn_context,
3792+
&mut turn_diff_tracker,
3793+
sub_id,
3794+
call_id,
3795+
)
3796+
.await;
3797+
3798+
let ResponseInputItem::FunctionCallOutput { output, .. } = resp else {
3799+
panic!("expected FunctionCallOutput");
3800+
};
3801+
3802+
let expected = format!(
3803+
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
3804+
policy = turn_context.approval_policy
3805+
);
3806+
3807+
pretty_assertions::assert_eq!(output.content, expected);
3808+
3809+
// Now retry the same command WITHOUT escalated permissions; should succeed.
3810+
// Force DangerFullAccess to avoid platform sandbox dependencies in tests.
3811+
turn_context.sandbox_policy = SandboxPolicy::DangerFullAccess;
3812+
3813+
let resp2 = handle_container_exec_with_params(
3814+
params2,
3815+
&session,
3816+
&turn_context,
3817+
&mut turn_diff_tracker,
3818+
"test-sub".to_string(),
3819+
"test-call-2".to_string(),
3820+
)
3821+
.await;
3822+
3823+
let ResponseInputItem::FunctionCallOutput { output, .. } = resp2 else {
3824+
panic!("expected FunctionCallOutput on retry");
3825+
};
3826+
3827+
#[derive(Deserialize, PartialEq, Eq, Debug)]
3828+
struct ResponseExecMetadata {
3829+
exit_code: i32,
3830+
}
3831+
3832+
#[derive(Deserialize)]
3833+
struct ResponseExecOutput {
3834+
output: String,
3835+
metadata: ResponseExecMetadata,
3836+
}
3837+
3838+
let exec_output: ResponseExecOutput =
3839+
serde_json::from_str(&output.content).expect("valid exec output json");
3840+
3841+
pretty_assertions::assert_eq!(exec_output.metadata, ResponseExecMetadata { exit_code: 0 });
3842+
assert!(exec_output.output.contains("hi"));
3843+
pretty_assertions::assert_eq!(output.success, Some(true));
3844+
}
37383845
}

codex-rs/core/src/exec.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const AGGREGATE_BUFFER_INITIAL_CAPACITY: usize = 8 * 1024; // 8 KiB
4545
/// Aggregation still collects full output; only the live event stream is capped.
4646
pub(crate) const MAX_EXEC_OUTPUT_DELTAS_PER_CALL: usize = 10_000;
4747

48-
#[derive(Debug, Clone)]
48+
#[derive(Clone, Debug)]
4949
pub struct ExecParams {
5050
pub command: Vec<String>,
5151
pub cwd: PathBuf,

0 commit comments

Comments
 (0)