fix: run apply_patch calls through the sandbox (#1705)
Building on the work of https://github.com/openai/codex/pull/1702, this changes how a shell call to `apply_patch` is handled. Previously, a shell call to `apply_patch` was always handled in-process, never leveraging a sandbox. To determine whether the `apply_patch` operation could be auto-approved, the `is_write_patch_constrained_to_writable_paths()` function would check if all the paths listed in the paths were writable. If so, the agent would apply the changes listed in the patch. Unfortunately, this approach afforded a loophole: symlinks! * For a soft link, we could fix this issue by tracing the link and checking whether the target is in the set of writable paths, however... * ...For a hard link, things are not as simple. We can run `stat FILE` to see if the number of links is greater than 1, but then we would have to do something potentially expensive like `find . -inum <inode_number>` to find the other paths for `FILE`. Further, even if this worked, this approach runs the risk of a [TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use) race condition, so it is not robust. The solution, implemented in this PR, is to take the virtual execution of the `apply_patch` CLI into an _actual_ execution using `codex --codex-run-as-apply-patch PATCH`, which we can run under the sandbox the user specified, just like any other `shell` call. This, of course, assumes that the sandbox prevents writing through symlinks as a mechanism to write to folders that are not in the writable set configured by the sandbox. I verified this by testing the following on both Mac and Linux: ```shell #!/usr/bin/env bash set -euo pipefail # Can running a command in SANDBOX_DIR write a file in EXPLOIT_DIR? # Codex is run in SANDBOX_DIR, so writes should be constrianed to this directory. SANDBOX_DIR=$(mktemp -d -p "$HOME" sandboxtesttemp.XXXXXX) # EXPLOIT_DIR is outside of SANDBOX_DIR, so let's see if we can write to it. EXPLOIT_DIR=$(mktemp -d -p "$HOME" sandboxtesttemp.XXXXXX) echo "SANDBOX_DIR: $SANDBOX_DIR" echo "EXPLOIT_DIR: $EXPLOIT_DIR" cleanup() { # Only remove if it looks sane and still exists [[ -n "${SANDBOX_DIR:-}" && -d "$SANDBOX_DIR" ]] && rm -rf -- "$SANDBOX_DIR" [[ -n "${EXPLOIT_DIR:-}" && -d "$EXPLOIT_DIR" ]] && rm -rf -- "$EXPLOIT_DIR" } trap cleanup EXIT echo "I am the original content" > "${EXPLOIT_DIR}/original.txt" # Drop the -s to test hard links. ln -s "${EXPLOIT_DIR}/original.txt" "${SANDBOX_DIR}/link-to-original.txt" cat "${SANDBOX_DIR}/link-to-original.txt" if [[ "$(uname)" == "Linux" ]]; then SANDBOX_SUBCOMMAND=landlock else SANDBOX_SUBCOMMAND=seatbelt fi # Attempt the exploit cd "${SANDBOX_DIR}" codex debug "${SANDBOX_SUBCOMMAND}" bash -lc "echo pwned > ./link-to-original.txt" || true cat "${EXPLOIT_DIR}/original.txt" ``` Admittedly, this change merits a proper integration test, but I think I will have to do that in a follow-up PR.
This commit is contained in:
@@ -58,16 +58,24 @@ impl PartialEq for IoError {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum MaybeApplyPatch {
|
pub enum MaybeApplyPatch {
|
||||||
Body(Vec<Hunk>),
|
Body(ApplyPatchArgs),
|
||||||
ShellParseError(ExtractHeredocError),
|
ShellParseError(ExtractHeredocError),
|
||||||
PatchParseError(ParseError),
|
PatchParseError(ParseError),
|
||||||
NotApplyPatch,
|
NotApplyPatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Both the raw PATCH argument to `apply_patch` as well as the PATCH argument
|
||||||
|
/// parsed into hunks.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct ApplyPatchArgs {
|
||||||
|
pub patch: String,
|
||||||
|
pub hunks: Vec<Hunk>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
||||||
match argv {
|
match argv {
|
||||||
[cmd, body] if cmd == "apply_patch" => match parse_patch(body) {
|
[cmd, body] if cmd == "apply_patch" => match parse_patch(body) {
|
||||||
Ok(hunks) => MaybeApplyPatch::Body(hunks),
|
Ok(source) => MaybeApplyPatch::Body(source),
|
||||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||||
},
|
},
|
||||||
[bash, flag, script]
|
[bash, flag, script]
|
||||||
@@ -77,7 +85,7 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
|||||||
{
|
{
|
||||||
match extract_heredoc_body_from_apply_patch_command(script) {
|
match extract_heredoc_body_from_apply_patch_command(script) {
|
||||||
Ok(body) => match parse_patch(&body) {
|
Ok(body) => match parse_patch(&body) {
|
||||||
Ok(hunks) => MaybeApplyPatch::Body(hunks),
|
Ok(source) => MaybeApplyPatch::Body(source),
|
||||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||||
},
|
},
|
||||||
Err(e) => MaybeApplyPatch::ShellParseError(e),
|
Err(e) => MaybeApplyPatch::ShellParseError(e),
|
||||||
@@ -116,11 +124,19 @@ pub enum MaybeApplyPatchVerified {
|
|||||||
NotApplyPatch,
|
NotApplyPatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
/// ApplyPatchAction is the result of parsing an `apply_patch` command. By
|
/// ApplyPatchAction is the result of parsing an `apply_patch` command. By
|
||||||
/// construction, all paths should be absolute paths.
|
/// construction, all paths should be absolute paths.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct ApplyPatchAction {
|
pub struct ApplyPatchAction {
|
||||||
changes: HashMap<PathBuf, ApplyPatchFileChange>,
|
changes: HashMap<PathBuf, ApplyPatchFileChange>,
|
||||||
|
|
||||||
|
/// The raw patch argument that can be used with `apply_patch` as an exec
|
||||||
|
/// call. i.e., if the original arg was parsed in "lenient" mode with a
|
||||||
|
/// heredoc, this should be the value without the heredoc wrapper.
|
||||||
|
pub patch: String,
|
||||||
|
|
||||||
|
/// The working directory that was used to resolve relative paths in the patch.
|
||||||
|
pub cwd: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplyPatchAction {
|
impl ApplyPatchAction {
|
||||||
@@ -140,8 +156,28 @@ impl ApplyPatchAction {
|
|||||||
panic!("path must be absolute");
|
panic!("path must be absolute");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
let filename = path
|
||||||
|
.file_name()
|
||||||
|
.expect("path should not be empty")
|
||||||
|
.to_string_lossy();
|
||||||
|
let patch = format!(
|
||||||
|
r#"*** Begin Patch
|
||||||
|
*** Update File: {filename}
|
||||||
|
@@
|
||||||
|
+ {content}
|
||||||
|
*** End Patch"#,
|
||||||
|
);
|
||||||
let changes = HashMap::from([(path.to_path_buf(), ApplyPatchFileChange::Add { content })]);
|
let changes = HashMap::from([(path.to_path_buf(), ApplyPatchFileChange::Add { content })]);
|
||||||
Self { changes }
|
#[allow(clippy::expect_used)]
|
||||||
|
Self {
|
||||||
|
changes,
|
||||||
|
cwd: path
|
||||||
|
.parent()
|
||||||
|
.expect("path should have parent")
|
||||||
|
.to_path_buf(),
|
||||||
|
patch,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +185,7 @@ impl ApplyPatchAction {
|
|||||||
/// patch.
|
/// patch.
|
||||||
pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApplyPatchVerified {
|
pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApplyPatchVerified {
|
||||||
match maybe_parse_apply_patch(argv) {
|
match maybe_parse_apply_patch(argv) {
|
||||||
MaybeApplyPatch::Body(hunks) => {
|
MaybeApplyPatch::Body(ApplyPatchArgs { patch, hunks }) => {
|
||||||
let mut changes = HashMap::new();
|
let mut changes = HashMap::new();
|
||||||
for hunk in hunks {
|
for hunk in hunks {
|
||||||
let path = hunk.resolve_path(cwd);
|
let path = hunk.resolve_path(cwd);
|
||||||
@@ -183,7 +219,11 @@ pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MaybeApplyPatchVerified::Body(ApplyPatchAction { changes })
|
MaybeApplyPatchVerified::Body(ApplyPatchAction {
|
||||||
|
changes,
|
||||||
|
patch,
|
||||||
|
cwd: cwd.to_path_buf(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
MaybeApplyPatch::ShellParseError(e) => MaybeApplyPatchVerified::ShellParseError(e),
|
MaybeApplyPatch::ShellParseError(e) => MaybeApplyPatchVerified::ShellParseError(e),
|
||||||
MaybeApplyPatch::PatchParseError(e) => MaybeApplyPatchVerified::CorrectnessError(e.into()),
|
MaybeApplyPatch::PatchParseError(e) => MaybeApplyPatchVerified::CorrectnessError(e.into()),
|
||||||
@@ -264,7 +304,7 @@ pub fn apply_patch(
|
|||||||
stderr: &mut impl std::io::Write,
|
stderr: &mut impl std::io::Write,
|
||||||
) -> Result<(), ApplyPatchError> {
|
) -> Result<(), ApplyPatchError> {
|
||||||
let hunks = match parse_patch(patch) {
|
let hunks = match parse_patch(patch) {
|
||||||
Ok(hunks) => hunks,
|
Ok(source) => source.hunks,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
match &e {
|
match &e {
|
||||||
InvalidPatchError(message) => {
|
InvalidPatchError(message) => {
|
||||||
@@ -652,7 +692,7 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
match maybe_parse_apply_patch(&args) {
|
match maybe_parse_apply_patch(&args) {
|
||||||
MaybeApplyPatch::Body(hunks) => {
|
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, patch: _ }) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hunks,
|
hunks,
|
||||||
vec![Hunk::AddFile {
|
vec![Hunk::AddFile {
|
||||||
@@ -679,7 +719,7 @@ PATCH"#,
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
match maybe_parse_apply_patch(&args) {
|
match maybe_parse_apply_patch(&args) {
|
||||||
MaybeApplyPatch::Body(hunks) => {
|
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, patch: _ }) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hunks,
|
hunks,
|
||||||
vec![Hunk::AddFile {
|
vec![Hunk::AddFile {
|
||||||
@@ -954,7 +994,7 @@ PATCH"#,
|
|||||||
));
|
));
|
||||||
let patch = parse_patch(&patch).unwrap();
|
let patch = parse_patch(&patch).unwrap();
|
||||||
|
|
||||||
let update_file_chunks = match patch.as_slice() {
|
let update_file_chunks = match patch.hunks.as_slice() {
|
||||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||||
_ => panic!("Expected a single UpdateFile hunk"),
|
_ => panic!("Expected a single UpdateFile hunk"),
|
||||||
};
|
};
|
||||||
@@ -992,7 +1032,7 @@ PATCH"#,
|
|||||||
));
|
));
|
||||||
|
|
||||||
let patch = parse_patch(&patch).unwrap();
|
let patch = parse_patch(&patch).unwrap();
|
||||||
let chunks = match patch.as_slice() {
|
let chunks = match patch.hunks.as_slice() {
|
||||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||||
_ => panic!("Expected a single UpdateFile hunk"),
|
_ => panic!("Expected a single UpdateFile hunk"),
|
||||||
};
|
};
|
||||||
@@ -1029,7 +1069,7 @@ PATCH"#,
|
|||||||
));
|
));
|
||||||
|
|
||||||
let patch = parse_patch(&patch).unwrap();
|
let patch = parse_patch(&patch).unwrap();
|
||||||
let chunks = match patch.as_slice() {
|
let chunks = match patch.hunks.as_slice() {
|
||||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||||
_ => panic!("Expected a single UpdateFile hunk"),
|
_ => panic!("Expected a single UpdateFile hunk"),
|
||||||
};
|
};
|
||||||
@@ -1064,7 +1104,7 @@ PATCH"#,
|
|||||||
));
|
));
|
||||||
|
|
||||||
let patch = parse_patch(&patch).unwrap();
|
let patch = parse_patch(&patch).unwrap();
|
||||||
let chunks = match patch.as_slice() {
|
let chunks = match patch.hunks.as_slice() {
|
||||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||||
_ => panic!("Expected a single UpdateFile hunk"),
|
_ => panic!("Expected a single UpdateFile hunk"),
|
||||||
};
|
};
|
||||||
@@ -1110,7 +1150,7 @@ PATCH"#,
|
|||||||
|
|
||||||
// Extract chunks then build the unified diff.
|
// Extract chunks then build the unified diff.
|
||||||
let parsed = parse_patch(&patch).unwrap();
|
let parsed = parse_patch(&patch).unwrap();
|
||||||
let chunks = match parsed.as_slice() {
|
let chunks = match parsed.hunks.as_slice() {
|
||||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||||
_ => panic!("Expected a single UpdateFile hunk"),
|
_ => panic!("Expected a single UpdateFile hunk"),
|
||||||
};
|
};
|
||||||
@@ -1193,6 +1233,8 @@ g
|
|||||||
new_content: "updated session directory content\n".to_string(),
|
new_content: "updated session directory content\n".to_string(),
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
|
patch: argv[1].clone(),
|
||||||
|
cwd: session_dir.path().to_path_buf(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
//!
|
//!
|
||||||
//! The parser below is a little more lenient than the explicit spec and allows for
|
//! The parser below is a little more lenient than the explicit spec and allows for
|
||||||
//! leading/trailing whitespace around patch markers.
|
//! leading/trailing whitespace around patch markers.
|
||||||
|
use crate::ApplyPatchArgs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ pub struct UpdateFileChunk {
|
|||||||
pub is_end_of_file: bool,
|
pub is_end_of_file: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_patch(patch: &str) -> Result<Vec<Hunk>, ParseError> {
|
pub fn parse_patch(patch: &str) -> Result<ApplyPatchArgs, ParseError> {
|
||||||
let mode = if PARSE_IN_STRICT_MODE {
|
let mode = if PARSE_IN_STRICT_MODE {
|
||||||
ParseMode::Strict
|
ParseMode::Strict
|
||||||
} else {
|
} else {
|
||||||
@@ -150,7 +151,7 @@ enum ParseMode {
|
|||||||
Lenient,
|
Lenient,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_patch_text(patch: &str, mode: ParseMode) -> Result<Vec<Hunk>, ParseError> {
|
fn parse_patch_text(patch: &str, mode: ParseMode) -> Result<ApplyPatchArgs, ParseError> {
|
||||||
let lines: Vec<&str> = patch.trim().lines().collect();
|
let lines: Vec<&str> = patch.trim().lines().collect();
|
||||||
let lines: &[&str] = match check_patch_boundaries_strict(&lines) {
|
let lines: &[&str] = match check_patch_boundaries_strict(&lines) {
|
||||||
Ok(()) => &lines,
|
Ok(()) => &lines,
|
||||||
@@ -173,7 +174,8 @@ fn parse_patch_text(patch: &str, mode: ParseMode) -> Result<Vec<Hunk>, ParseErro
|
|||||||
line_number += hunk_lines;
|
line_number += hunk_lines;
|
||||||
remaining_lines = &remaining_lines[hunk_lines..]
|
remaining_lines = &remaining_lines[hunk_lines..]
|
||||||
}
|
}
|
||||||
Ok(hunks)
|
let patch = lines.join("\n");
|
||||||
|
Ok(ApplyPatchArgs { hunks, patch })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks the start and end lines of the patch text for `apply_patch`,
|
/// Checks the start and end lines of the patch text for `apply_patch`,
|
||||||
@@ -425,6 +427,7 @@ fn parse_update_file_chunk(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
fn test_parse_patch() {
|
fn test_parse_patch() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_patch_text("bad", ParseMode::Strict),
|
parse_patch_text("bad", ParseMode::Strict),
|
||||||
@@ -455,8 +458,10 @@ fn test_parse_patch() {
|
|||||||
"*** Begin Patch\n\
|
"*** Begin Patch\n\
|
||||||
*** End Patch",
|
*** End Patch",
|
||||||
ParseMode::Strict
|
ParseMode::Strict
|
||||||
),
|
)
|
||||||
Ok(Vec::new())
|
.unwrap()
|
||||||
|
.hunks,
|
||||||
|
Vec::new()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_patch_text(
|
parse_patch_text(
|
||||||
@@ -472,8 +477,10 @@ fn test_parse_patch() {
|
|||||||
+ return 123\n\
|
+ return 123\n\
|
||||||
*** End Patch",
|
*** End Patch",
|
||||||
ParseMode::Strict
|
ParseMode::Strict
|
||||||
),
|
)
|
||||||
Ok(vec![
|
.unwrap()
|
||||||
|
.hunks,
|
||||||
|
vec![
|
||||||
AddFile {
|
AddFile {
|
||||||
path: PathBuf::from("path/add.py"),
|
path: PathBuf::from("path/add.py"),
|
||||||
contents: "abc\ndef\n".to_string()
|
contents: "abc\ndef\n".to_string()
|
||||||
@@ -491,7 +498,7 @@ fn test_parse_patch() {
|
|||||||
is_end_of_file: false
|
is_end_of_file: false
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
);
|
);
|
||||||
// Update hunk followed by another hunk (Add File).
|
// Update hunk followed by another hunk (Add File).
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -504,8 +511,10 @@ fn test_parse_patch() {
|
|||||||
+content\n\
|
+content\n\
|
||||||
*** End Patch",
|
*** End Patch",
|
||||||
ParseMode::Strict
|
ParseMode::Strict
|
||||||
),
|
)
|
||||||
Ok(vec![
|
.unwrap()
|
||||||
|
.hunks,
|
||||||
|
vec![
|
||||||
UpdateFile {
|
UpdateFile {
|
||||||
path: PathBuf::from("file.py"),
|
path: PathBuf::from("file.py"),
|
||||||
move_path: None,
|
move_path: None,
|
||||||
@@ -520,7 +529,7 @@ fn test_parse_patch() {
|
|||||||
path: PathBuf::from("other.py"),
|
path: PathBuf::from("other.py"),
|
||||||
contents: "content\n".to_string()
|
contents: "content\n".to_string()
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update hunk without an explicit @@ header for the first chunk should parse.
|
// Update hunk without an explicit @@ header for the first chunk should parse.
|
||||||
@@ -533,8 +542,10 @@ fn test_parse_patch() {
|
|||||||
+bar
|
+bar
|
||||||
*** End Patch"#,
|
*** End Patch"#,
|
||||||
ParseMode::Strict
|
ParseMode::Strict
|
||||||
),
|
)
|
||||||
Ok(vec![UpdateFile {
|
.unwrap()
|
||||||
|
.hunks,
|
||||||
|
vec![UpdateFile {
|
||||||
path: PathBuf::from("file2.py"),
|
path: PathBuf::from("file2.py"),
|
||||||
move_path: None,
|
move_path: None,
|
||||||
chunks: vec![UpdateFileChunk {
|
chunks: vec![UpdateFileChunk {
|
||||||
@@ -543,7 +554,7 @@ fn test_parse_patch() {
|
|||||||
new_lines: vec!["import foo".to_string(), "bar".to_string()],
|
new_lines: vec!["import foo".to_string(), "bar".to_string()],
|
||||||
is_end_of_file: false,
|
is_end_of_file: false,
|
||||||
}],
|
}],
|
||||||
}])
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,7 +585,10 @@ fn test_parse_patch_lenient() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_patch_text(&patch_text_in_heredoc, ParseMode::Lenient),
|
parse_patch_text(&patch_text_in_heredoc, ParseMode::Lenient),
|
||||||
Ok(expected_patch.clone())
|
Ok(ApplyPatchArgs {
|
||||||
|
hunks: expected_patch.clone(),
|
||||||
|
patch: patch_text.to_string()
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let patch_text_in_single_quoted_heredoc = format!("<<'EOF'\n{patch_text}\nEOF\n");
|
let patch_text_in_single_quoted_heredoc = format!("<<'EOF'\n{patch_text}\nEOF\n");
|
||||||
@@ -584,7 +598,10 @@ fn test_parse_patch_lenient() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_patch_text(&patch_text_in_single_quoted_heredoc, ParseMode::Lenient),
|
parse_patch_text(&patch_text_in_single_quoted_heredoc, ParseMode::Lenient),
|
||||||
Ok(expected_patch.clone())
|
Ok(ApplyPatchArgs {
|
||||||
|
hunks: expected_patch.clone(),
|
||||||
|
patch: patch_text.to_string()
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let patch_text_in_double_quoted_heredoc = format!("<<\"EOF\"\n{patch_text}\nEOF\n");
|
let patch_text_in_double_quoted_heredoc = format!("<<\"EOF\"\n{patch_text}\nEOF\n");
|
||||||
@@ -594,7 +611,10 @@ fn test_parse_patch_lenient() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_patch_text(&patch_text_in_double_quoted_heredoc, ParseMode::Lenient),
|
parse_patch_text(&patch_text_in_double_quoted_heredoc, ParseMode::Lenient),
|
||||||
Ok(expected_patch.clone())
|
Ok(ApplyPatchArgs {
|
||||||
|
hunks: expected_patch.clone(),
|
||||||
|
patch: patch_text.to_string()
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let patch_text_in_mismatched_quotes_heredoc = format!("<<\"EOF'\n{patch_text}\nEOF\n");
|
let patch_text_in_mismatched_quotes_heredoc = format!("<<\"EOF'\n{patch_text}\nEOF\n");
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use std::future::Future;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use codex_core::CODEX_APPLY_PATCH_ARG1;
|
||||||
|
|
||||||
/// While we want to deploy the Codex CLI as a single executable for simplicity,
|
/// While we want to deploy the Codex CLI as a single executable for simplicity,
|
||||||
/// we also want to expose some of its functionality as distinct CLIs, so we use
|
/// we also want to expose some of its functionality as distinct CLIs, so we use
|
||||||
/// the "arg0 trick" to determine which CLI to dispatch. This effectively allows
|
/// the "arg0 trick" to determine which CLI to dispatch. This effectively allows
|
||||||
@@ -43,7 +45,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let argv1 = args.next().unwrap_or_default();
|
let argv1 = args.next().unwrap_or_default();
|
||||||
if argv1 == "--codex-run-as-apply-patch" {
|
if argv1 == CODEX_APPLY_PATCH_ARG1 {
|
||||||
let patch_arg = args.next().and_then(|s| s.to_str().map(|s| s.to_owned()));
|
let patch_arg = args.next().and_then(|s| s.to_str().map(|s| s.to_owned()));
|
||||||
let exit_code = match patch_arg {
|
let exit_code = match patch_arg {
|
||||||
Some(patch_arg) => {
|
Some(patch_arg) => {
|
||||||
@@ -55,7 +57,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Error: --codex-run-as-apply-patch requires a UTF-8 PATCH argument.");
|
eprintln!("Error: {CODEX_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,12 +18,34 @@ use std::collections::HashMap;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const CODEX_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch";
|
||||||
|
|
||||||
|
pub(crate) enum InternalApplyPatchInvocation {
|
||||||
|
/// The `apply_patch` call was handled programmatically, without any sort
|
||||||
|
/// of sandbox, because the user explicitly approved it. This is the
|
||||||
|
/// result to use with the `shell` function call that contained `apply_patch`.
|
||||||
|
Output(ResponseInputItem),
|
||||||
|
|
||||||
|
/// The `apply_patch` call was auto-approved, which means that, on the
|
||||||
|
/// surface, it appears to be safe, but it should be run in a sandbox if the
|
||||||
|
/// user has configured one because a path being written could be a hard
|
||||||
|
/// link to a file outside the writable folders, so only the sandbox can
|
||||||
|
/// faithfully prevent the write in that case.
|
||||||
|
DelegateToExec(ApplyPatchAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ResponseInputItem> for InternalApplyPatchInvocation {
|
||||||
|
fn from(item: ResponseInputItem) -> Self {
|
||||||
|
InternalApplyPatchInvocation::Output(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn apply_patch(
|
pub(crate) async fn apply_patch(
|
||||||
sess: &Session,
|
sess: &Session,
|
||||||
sub_id: String,
|
sub_id: &str,
|
||||||
call_id: String,
|
call_id: &str,
|
||||||
action: ApplyPatchAction,
|
action: ApplyPatchAction,
|
||||||
) -> ResponseInputItem {
|
) -> InternalApplyPatchInvocation {
|
||||||
let writable_roots_snapshot = {
|
let writable_roots_snapshot = {
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
let guard = sess.writable_roots.lock().unwrap();
|
let guard = sess.writable_roots.lock().unwrap();
|
||||||
@@ -36,34 +58,38 @@ pub(crate) async fn apply_patch(
|
|||||||
&writable_roots_snapshot,
|
&writable_roots_snapshot,
|
||||||
&sess.cwd,
|
&sess.cwd,
|
||||||
) {
|
) {
|
||||||
SafetyCheck::AutoApprove { .. } => true,
|
SafetyCheck::AutoApprove { .. } => {
|
||||||
|
return InternalApplyPatchInvocation::DelegateToExec(action);
|
||||||
|
}
|
||||||
SafetyCheck::AskUser => {
|
SafetyCheck::AskUser => {
|
||||||
// Compute a readable summary of path changes to include in the
|
// Compute a readable summary of path changes to include in the
|
||||||
// approval request so the user can make an informed decision.
|
// approval request so the user can make an informed decision.
|
||||||
let rx_approve = sess
|
let rx_approve = sess
|
||||||
.request_patch_approval(sub_id.clone(), call_id.clone(), &action, None, None)
|
.request_patch_approval(sub_id.to_owned(), call_id.to_owned(), &action, None, None)
|
||||||
.await;
|
.await;
|
||||||
match rx_approve.await.unwrap_or_default() {
|
match rx_approve.await.unwrap_or_default() {
|
||||||
ReviewDecision::Approved | ReviewDecision::ApprovedForSession => false,
|
ReviewDecision::Approved | ReviewDecision::ApprovedForSession => false,
|
||||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||||
return ResponseInputItem::FunctionCallOutput {
|
return ResponseInputItem::FunctionCallOutput {
|
||||||
call_id,
|
call_id: call_id.to_owned(),
|
||||||
output: FunctionCallOutputPayload {
|
output: FunctionCallOutputPayload {
|
||||||
content: "patch rejected by user".to_string(),
|
content: "patch rejected by user".to_string(),
|
||||||
success: Some(false),
|
success: Some(false),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SafetyCheck::Reject { reason } => {
|
SafetyCheck::Reject { reason } => {
|
||||||
return ResponseInputItem::FunctionCallOutput {
|
return ResponseInputItem::FunctionCallOutput {
|
||||||
call_id,
|
call_id: call_id.to_owned(),
|
||||||
output: FunctionCallOutputPayload {
|
output: FunctionCallOutputPayload {
|
||||||
content: format!("patch rejected: {reason}"),
|
content: format!("patch rejected: {reason}"),
|
||||||
success: Some(false),
|
success: Some(false),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,8 +109,8 @@ pub(crate) async fn apply_patch(
|
|||||||
|
|
||||||
let rx = sess
|
let rx = sess
|
||||||
.request_patch_approval(
|
.request_patch_approval(
|
||||||
sub_id.clone(),
|
sub_id.to_owned(),
|
||||||
call_id.clone(),
|
call_id.to_owned(),
|
||||||
&action,
|
&action,
|
||||||
reason.clone(),
|
reason.clone(),
|
||||||
Some(root.clone()),
|
Some(root.clone()),
|
||||||
@@ -96,12 +122,13 @@ pub(crate) async fn apply_patch(
|
|||||||
ReviewDecision::Approved | ReviewDecision::ApprovedForSession
|
ReviewDecision::Approved | ReviewDecision::ApprovedForSession
|
||||||
) {
|
) {
|
||||||
return ResponseInputItem::FunctionCallOutput {
|
return ResponseInputItem::FunctionCallOutput {
|
||||||
call_id,
|
call_id: call_id.to_owned(),
|
||||||
output: FunctionCallOutputPayload {
|
output: FunctionCallOutputPayload {
|
||||||
content: "patch rejected by user".to_string(),
|
content: "patch rejected by user".to_string(),
|
||||||
success: Some(false),
|
success: Some(false),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
// user approved, extend writable roots for this session
|
// user approved, extend writable roots for this session
|
||||||
@@ -112,9 +139,9 @@ pub(crate) async fn apply_patch(
|
|||||||
let _ = sess
|
let _ = sess
|
||||||
.tx_event
|
.tx_event
|
||||||
.send(Event {
|
.send(Event {
|
||||||
id: sub_id.clone(),
|
id: sub_id.to_owned(),
|
||||||
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||||
call_id: call_id.clone(),
|
call_id: call_id.to_owned(),
|
||||||
auto_approved,
|
auto_approved,
|
||||||
changes: convert_apply_patch_to_protocol(&action),
|
changes: convert_apply_patch_to_protocol(&action),
|
||||||
}),
|
}),
|
||||||
@@ -173,8 +200,8 @@ pub(crate) async fn apply_patch(
|
|||||||
));
|
));
|
||||||
let rx = sess
|
let rx = sess
|
||||||
.request_patch_approval(
|
.request_patch_approval(
|
||||||
sub_id.clone(),
|
sub_id.to_owned(),
|
||||||
call_id.clone(),
|
call_id.to_owned(),
|
||||||
&action,
|
&action,
|
||||||
reason.clone(),
|
reason.clone(),
|
||||||
Some(root.clone()),
|
Some(root.clone()),
|
||||||
@@ -204,9 +231,9 @@ pub(crate) async fn apply_patch(
|
|||||||
let _ = sess
|
let _ = sess
|
||||||
.tx_event
|
.tx_event
|
||||||
.send(Event {
|
.send(Event {
|
||||||
id: sub_id.clone(),
|
id: sub_id.to_owned(),
|
||||||
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
|
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
|
||||||
call_id: call_id.clone(),
|
call_id: call_id.to_owned(),
|
||||||
stdout: String::from_utf8_lossy(&stdout).to_string(),
|
stdout: String::from_utf8_lossy(&stdout).to_string(),
|
||||||
stderr: String::from_utf8_lossy(&stderr).to_string(),
|
stderr: String::from_utf8_lossy(&stderr).to_string(),
|
||||||
success: success_flag,
|
success: success_flag,
|
||||||
@@ -214,22 +241,23 @@ pub(crate) async fn apply_patch(
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
let item = match result {
|
||||||
Ok(_) => ResponseInputItem::FunctionCallOutput {
|
Ok(_) => ResponseInputItem::FunctionCallOutput {
|
||||||
call_id,
|
call_id: call_id.to_owned(),
|
||||||
output: FunctionCallOutputPayload {
|
output: FunctionCallOutputPayload {
|
||||||
content: String::from_utf8_lossy(&stdout).to_string(),
|
content: String::from_utf8_lossy(&stdout).to_string(),
|
||||||
success: None,
|
success: None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Err(e) => ResponseInputItem::FunctionCallOutput {
|
Err(e) => ResponseInputItem::FunctionCallOutput {
|
||||||
call_id,
|
call_id: call_id.to_owned(),
|
||||||
output: FunctionCallOutputPayload {
|
output: FunctionCallOutputPayload {
|
||||||
content: format!("error: {e:#}, stderr: {}", String::from_utf8_lossy(&stderr)),
|
content: format!("error: {e:#}, stderr: {}", String::from_utf8_lossy(&stderr)),
|
||||||
success: Some(false),
|
success: Some(false),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
InternalApplyPatchInvocation::Output(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the first path in `hunks` that is NOT under any of the
|
/// Return the first path in `hunks` that is NOT under any of the
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -30,6 +31,8 @@ use tracing::trace;
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::apply_patch::CODEX_APPLY_PATCH_ARG1;
|
||||||
|
use crate::apply_patch::InternalApplyPatchInvocation;
|
||||||
use crate::apply_patch::convert_apply_patch_to_protocol;
|
use crate::apply_patch::convert_apply_patch_to_protocol;
|
||||||
use crate::apply_patch::get_writable_roots;
|
use crate::apply_patch::get_writable_roots;
|
||||||
use crate::apply_patch::{self};
|
use crate::apply_patch::{self};
|
||||||
@@ -81,6 +84,7 @@ use crate::protocol::TaskCompleteEvent;
|
|||||||
use crate::rollout::RolloutRecorder;
|
use crate::rollout::RolloutRecorder;
|
||||||
use crate::safety::SafetyCheck;
|
use crate::safety::SafetyCheck;
|
||||||
use crate::safety::assess_command_safety;
|
use crate::safety::assess_command_safety;
|
||||||
|
use crate::safety::assess_safety_for_untrusted_command;
|
||||||
use crate::shell;
|
use crate::shell;
|
||||||
use crate::user_notification::UserNotification;
|
use crate::user_notification::UserNotification;
|
||||||
use crate::util::backoff;
|
use crate::util::backoff;
|
||||||
@@ -354,13 +358,19 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn notify_exec_command_begin(&self, sub_id: &str, call_id: &str, params: &ExecParams) {
|
async fn notify_exec_command_begin(
|
||||||
|
&self,
|
||||||
|
sub_id: &str,
|
||||||
|
call_id: &str,
|
||||||
|
command_for_display: Vec<String>,
|
||||||
|
command_cwd: &Path,
|
||||||
|
) {
|
||||||
let event = Event {
|
let event = Event {
|
||||||
id: sub_id.to_string(),
|
id: sub_id.to_string(),
|
||||||
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||||
call_id: call_id.to_string(),
|
call_id: call_id.to_string(),
|
||||||
command: params.command.clone(),
|
command: command_for_display,
|
||||||
cwd: params.cwd.clone(),
|
cwd: command_cwd.to_path_buf(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let _ = self.tx_event.send(event).await;
|
let _ = self.tx_event.send(event).await;
|
||||||
@@ -1420,38 +1430,77 @@ async fn handle_container_exec_with_params(
|
|||||||
call_id: String,
|
call_id: String,
|
||||||
) -> ResponseInputItem {
|
) -> ResponseInputItem {
|
||||||
// check if this was a patch, and apply it if so
|
// check if this was a patch, and apply it if so
|
||||||
match maybe_parse_apply_patch_verified(¶ms.command, ¶ms.cwd) {
|
let apply_patch_action_for_exec =
|
||||||
MaybeApplyPatchVerified::Body(changes) => {
|
match maybe_parse_apply_patch_verified(¶ms.command, ¶ms.cwd) {
|
||||||
return apply_patch::apply_patch(sess, sub_id, call_id, changes).await;
|
MaybeApplyPatchVerified::Body(changes) => {
|
||||||
}
|
match apply_patch::apply_patch(sess, &sub_id, &call_id, changes).await {
|
||||||
MaybeApplyPatchVerified::CorrectnessError(parse_error) => {
|
InternalApplyPatchInvocation::Output(item) => return item,
|
||||||
// It looks like an invocation of `apply_patch`, but we
|
InternalApplyPatchInvocation::DelegateToExec(action) => Some(action),
|
||||||
// could not resolve it into a patch that would apply
|
}
|
||||||
// cleanly. Return to model for resample.
|
}
|
||||||
return ResponseInputItem::FunctionCallOutput {
|
MaybeApplyPatchVerified::CorrectnessError(parse_error) => {
|
||||||
call_id,
|
// It looks like an invocation of `apply_patch`, but we
|
||||||
output: FunctionCallOutputPayload {
|
// could not resolve it into a patch that would apply
|
||||||
content: format!("error: {parse_error:#}"),
|
// cleanly. Return to model for resample.
|
||||||
success: None,
|
return ResponseInputItem::FunctionCallOutput {
|
||||||
},
|
call_id,
|
||||||
};
|
output: FunctionCallOutputPayload {
|
||||||
}
|
content: format!("error: {parse_error:#}"),
|
||||||
MaybeApplyPatchVerified::ShellParseError(error) => {
|
success: None,
|
||||||
trace!("Failed to parse shell command, {error:?}");
|
},
|
||||||
}
|
};
|
||||||
MaybeApplyPatchVerified::NotApplyPatch => (),
|
}
|
||||||
}
|
MaybeApplyPatchVerified::ShellParseError(error) => {
|
||||||
|
trace!("Failed to parse shell command, {error:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
MaybeApplyPatchVerified::NotApplyPatch => None,
|
||||||
|
};
|
||||||
|
|
||||||
// safety checks
|
let (params, safety, command_for_display) = match apply_patch_action_for_exec {
|
||||||
let safety = {
|
Some(ApplyPatchAction { patch, cwd, .. }) => {
|
||||||
let state = sess.state.lock().unwrap();
|
let path_to_codex = std::env::current_exe()
|
||||||
assess_command_safety(
|
.ok()
|
||||||
¶ms.command,
|
.map(|p| p.to_string_lossy().to_string());
|
||||||
sess.approval_policy,
|
let Some(path_to_codex) = path_to_codex else {
|
||||||
&sess.sandbox_policy,
|
return ResponseInputItem::FunctionCallOutput {
|
||||||
&state.approved_commands,
|
call_id,
|
||||||
)
|
output: FunctionCallOutputPayload {
|
||||||
|
content: "failed to determine path to codex executable".to_string(),
|
||||||
|
success: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let params = ExecParams {
|
||||||
|
command: vec![
|
||||||
|
path_to_codex,
|
||||||
|
CODEX_APPLY_PATCH_ARG1.to_string(),
|
||||||
|
patch.clone(),
|
||||||
|
],
|
||||||
|
cwd,
|
||||||
|
timeout_ms: params.timeout_ms,
|
||||||
|
env: HashMap::new(),
|
||||||
|
};
|
||||||
|
let safety =
|
||||||
|
assess_safety_for_untrusted_command(sess.approval_policy, &sess.sandbox_policy);
|
||||||
|
(params, safety, vec!["apply_patch".to_string(), patch])
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let safety = {
|
||||||
|
let state = sess.state.lock().unwrap();
|
||||||
|
assess_command_safety(
|
||||||
|
¶ms.command,
|
||||||
|
sess.approval_policy,
|
||||||
|
&sess.sandbox_policy,
|
||||||
|
&state.approved_commands,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let command_for_display = params.command.clone();
|
||||||
|
(params, safety, command_for_display)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let sandbox_type = match safety {
|
let sandbox_type = match safety {
|
||||||
SafetyCheck::AutoApprove { sandbox_type } => sandbox_type,
|
SafetyCheck::AutoApprove { sandbox_type } => sandbox_type,
|
||||||
SafetyCheck::AskUser => {
|
SafetyCheck::AskUser => {
|
||||||
@@ -1496,7 +1545,7 @@ async fn handle_container_exec_with_params(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sess.notify_exec_command_begin(&sub_id, &call_id, ¶ms)
|
sess.notify_exec_command_begin(&sub_id, &call_id, command_for_display.clone(), ¶ms.cwd)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let params = maybe_run_with_user_profile(params, sess);
|
let params = maybe_run_with_user_profile(params, sess);
|
||||||
@@ -1537,7 +1586,16 @@ async fn handle_container_exec_with_params(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(CodexErr::Sandbox(error)) => {
|
Err(CodexErr::Sandbox(error)) => {
|
||||||
handle_sandbox_error(error, sandbox_type, params, sess, sub_id, call_id).await
|
handle_sandbox_error(
|
||||||
|
error,
|
||||||
|
sandbox_type,
|
||||||
|
params,
|
||||||
|
command_for_display,
|
||||||
|
sess,
|
||||||
|
sub_id,
|
||||||
|
call_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Handle non-sandbox errors
|
// Handle non-sandbox errors
|
||||||
@@ -1556,6 +1614,7 @@ async fn handle_sandbox_error(
|
|||||||
error: SandboxErr,
|
error: SandboxErr,
|
||||||
sandbox_type: SandboxType,
|
sandbox_type: SandboxType,
|
||||||
params: ExecParams,
|
params: ExecParams,
|
||||||
|
command_for_display: Vec<String>,
|
||||||
sess: &Session,
|
sess: &Session,
|
||||||
sub_id: String,
|
sub_id: String,
|
||||||
call_id: String,
|
call_id: String,
|
||||||
@@ -1605,7 +1664,7 @@ async fn handle_sandbox_error(
|
|||||||
sess.notify_background_event(&sub_id, "retrying command without sandbox")
|
sess.notify_background_event(&sub_id, "retrying command without sandbox")
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
sess.notify_exec_command_begin(&sub_id, &call_id, ¶ms)
|
sess.notify_exec_command_begin(&sub_id, &call_id, command_for_display, ¶ms.cwd)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// This is an escalated retry; the policy will not be
|
// This is an escalated retry; the policy will not be
|
||||||
|
|||||||
@@ -43,4 +43,5 @@ pub mod shell;
|
|||||||
mod user_notification;
|
mod user_notification;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
|
||||||
pub use client_common::model_supports_reasoning_summaries;
|
pub use client_common::model_supports_reasoning_summaries;
|
||||||
|
|||||||
@@ -75,9 +75,6 @@ pub fn assess_command_safety(
|
|||||||
sandbox_policy: &SandboxPolicy,
|
sandbox_policy: &SandboxPolicy,
|
||||||
approved: &HashSet<Vec<String>>,
|
approved: &HashSet<Vec<String>>,
|
||||||
) -> SafetyCheck {
|
) -> SafetyCheck {
|
||||||
use AskForApproval::*;
|
|
||||||
use SandboxPolicy::*;
|
|
||||||
|
|
||||||
// A command is "trusted" because either:
|
// A command is "trusted" because either:
|
||||||
// - it belongs to a set of commands we consider "safe" by default, or
|
// - it belongs to a set of commands we consider "safe" by default, or
|
||||||
// - the user has explicitly approved the command for this session
|
// - the user has explicitly approved the command for this session
|
||||||
@@ -97,6 +94,16 @@ pub fn assess_command_safety(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assess_safety_for_untrusted_command(approval_policy, sandbox_policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn assess_safety_for_untrusted_command(
|
||||||
|
approval_policy: AskForApproval,
|
||||||
|
sandbox_policy: &SandboxPolicy,
|
||||||
|
) -> SafetyCheck {
|
||||||
|
use AskForApproval::*;
|
||||||
|
use SandboxPolicy::*;
|
||||||
|
|
||||||
match (approval_policy, sandbox_policy) {
|
match (approval_policy, sandbox_policy) {
|
||||||
(UnlessTrusted, _) => {
|
(UnlessTrusted, _) => {
|
||||||
// Even though the user may have opted into DangerFullAccess,
|
// Even though the user may have opted into DangerFullAccess,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use assert_cmd::prelude::*;
|
use assert_cmd::prelude::*;
|
||||||
|
use codex_core::CODEX_APPLY_PATCH_ARG1;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
@@ -16,7 +17,7 @@ fn test_standalone_exec_cli_can_use_apply_patch() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Command::cargo_bin("codex-exec")
|
Command::cargo_bin("codex-exec")
|
||||||
.context("should find binary for codex-exec")?
|
.context("should find binary for codex-exec")?
|
||||||
.arg("--codex-run-as-apply-patch")
|
.arg(CODEX_APPLY_PATCH_ARG1)
|
||||||
.arg(
|
.arg(
|
||||||
r#"*** Begin Patch
|
r#"*** Begin Patch
|
||||||
*** Update File: source.txt
|
*** Update File: source.txt
|
||||||
|
|||||||
Reference in New Issue
Block a user