mirror of
https://github.com/openai/codex.git
synced 2026-05-04 05:11:37 +03:00
core: route view_image through a sandbox-backed fs helper
This commit is contained in:
6
codex-rs/fs-ops/BUILD.bazel
Normal file
6
codex-rs/fs-ops/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "fs-ops",
|
||||
crate_name = "codex_fs_ops",
|
||||
)
|
||||
22
codex-rs/fs-ops/Cargo.toml
Normal file
22
codex-rs/fs-ops/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "codex-fs-ops"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_fs_ops"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
38
codex-rs/fs-ops/src/command.rs
Normal file
38
codex-rs/fs-ops/src/command.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FsCommand {
|
||||
ReadFile { path: PathBuf },
|
||||
}
|
||||
|
||||
pub fn parse_command_from_args(
|
||||
mut args: impl Iterator<Item = OsString>,
|
||||
) -> Result<FsCommand, String> {
|
||||
let Some(operation) = args.next() else {
|
||||
return Err("missing operation".to_string());
|
||||
};
|
||||
let Some(operation) = operation.to_str() else {
|
||||
return Err("operation must be valid UTF-8".to_string());
|
||||
};
|
||||
let Some(path) = args.next() else {
|
||||
return Err(format!("missing path for operation `{operation}`"));
|
||||
};
|
||||
if args.next().is_some() {
|
||||
return Err(format!(
|
||||
"unexpected extra arguments for operation `{operation}`"
|
||||
));
|
||||
}
|
||||
|
||||
let path = PathBuf::from(path);
|
||||
match operation {
|
||||
"read" => Ok(FsCommand::ReadFile { path }),
|
||||
_ => Err(format!(
|
||||
"unsupported filesystem operation `{operation}`; expected `read`"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "command_tests.rs"]
|
||||
mod tests;
|
||||
16
codex-rs/fs-ops/src/command_tests.rs
Normal file
16
codex-rs/fs-ops/src/command_tests.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use super::FsCommand;
|
||||
use super::parse_command_from_args;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse_read_command() {
|
||||
let command = parse_command_from_args(["read", "/tmp/example.png"].into_iter().map(Into::into))
|
||||
.expect("command should parse");
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
FsCommand::ReadFile {
|
||||
path: "/tmp/example.png".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
3
codex-rs/fs-ops/src/constants.rs
Normal file
3
codex-rs/fs-ops/src/constants.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
/// Special argv[1] flag used when the Codex executable self-invokes to run the
|
||||
/// internal sandbox-backed filesystem helper path.
|
||||
pub const CODEX_CORE_FS_OPS_ARG1: &str = "--codex-run-as-fs-ops";
|
||||
70
codex-rs/fs-ops/src/error.rs
Normal file
70
codex-rs/fs-ops/src/error.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FsErrorKind {
|
||||
NotFound,
|
||||
PermissionDenied,
|
||||
IsADirectory,
|
||||
InvalidData,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<ErrorKind> for FsErrorKind {
|
||||
fn from(value: ErrorKind) -> Self {
|
||||
match value {
|
||||
ErrorKind::NotFound => Self::NotFound,
|
||||
ErrorKind::PermissionDenied => Self::PermissionDenied,
|
||||
ErrorKind::IsADirectory => Self::IsADirectory,
|
||||
ErrorKind::InvalidData => Self::InvalidData,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FsErrorKind {
|
||||
pub fn to_io_error_kind(&self) -> ErrorKind {
|
||||
match self {
|
||||
Self::NotFound => ErrorKind::NotFound,
|
||||
Self::PermissionDenied => ErrorKind::PermissionDenied,
|
||||
Self::IsADirectory => ErrorKind::IsADirectory,
|
||||
Self::InvalidData => ErrorKind::InvalidData,
|
||||
Self::Other => ErrorKind::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct FsError {
|
||||
pub kind: FsErrorKind,
|
||||
pub message: String,
|
||||
pub raw_os_error: Option<i32>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FsError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl FsError {
|
||||
pub fn to_io_error(&self) -> std::io::Error {
|
||||
if let Some(raw_os_error) = self.raw_os_error {
|
||||
std::io::Error::from_raw_os_error(raw_os_error)
|
||||
} else {
|
||||
std::io::Error::new(self.kind.to_io_error_kind(), self.message.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for FsError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self {
|
||||
kind: error.kind().into(),
|
||||
message: error.to_string(),
|
||||
raw_os_error: error.raw_os_error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
13
codex-rs/fs-ops/src/lib.rs
Normal file
13
codex-rs/fs-ops/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
mod command;
|
||||
mod constants;
|
||||
mod error;
|
||||
mod runner;
|
||||
|
||||
pub use command::FsCommand;
|
||||
pub use command::parse_command_from_args;
|
||||
pub use constants::CODEX_CORE_FS_OPS_ARG1;
|
||||
pub use error::FsError;
|
||||
pub use error::FsErrorKind;
|
||||
pub use runner::execute;
|
||||
pub use runner::run_from_args;
|
||||
pub use runner::write_error;
|
||||
55
codex-rs/fs-ops/src/runner.rs
Normal file
55
codex-rs/fs-ops/src/runner.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use crate::FsCommand;
|
||||
use crate::FsError;
|
||||
use crate::parse_command_from_args;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use std::ffi::OsString;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
|
||||
pub fn run_from_args(
|
||||
args: impl Iterator<Item = OsString>,
|
||||
stdin: &mut impl Read,
|
||||
stdout: &mut impl Write,
|
||||
stderr: &mut impl Write,
|
||||
) -> Result<()> {
|
||||
let command = match parse_command_from_args(args) {
|
||||
Ok(command) => command,
|
||||
Err(error) => {
|
||||
writeln!(stderr, "{error}").context("failed to write fs helper usage error")?;
|
||||
anyhow::bail!("{error}");
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = execute(command, stdin, stdout) {
|
||||
write_error(stderr, &error)?;
|
||||
anyhow::bail!("{error}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
command: FsCommand,
|
||||
_stdin: &mut impl Read,
|
||||
stdout: &mut impl Write,
|
||||
) -> Result<(), FsError> {
|
||||
match command {
|
||||
FsCommand::ReadFile { path } => {
|
||||
let mut file = std::fs::File::open(path).map_err(FsError::from)?;
|
||||
std::io::copy(&mut file, stdout)
|
||||
.map(|_| ())
|
||||
.map_err(FsError::from)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_error(stderr: &mut impl Write, error: &FsError) -> Result<()> {
|
||||
serde_json::to_writer(&mut *stderr, error).context("failed to serialize fs error")?;
|
||||
writeln!(stderr).context("failed to terminate fs error with newline")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "runner_tests.rs"]
|
||||
mod tests;
|
||||
76
codex-rs/fs-ops/src/runner_tests.rs
Normal file
76
codex-rs/fs-ops/src/runner_tests.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use super::execute;
|
||||
use crate::FsCommand;
|
||||
use crate::FsErrorKind;
|
||||
use crate::run_from_args;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn run_from_args_streams_file_bytes_to_stdout() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
let path = tempdir.path().join("image.bin");
|
||||
let expected = b"hello\x00world".to_vec();
|
||||
std::fs::write(&path, &expected).expect("write test file");
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let mut stdin = std::io::empty();
|
||||
run_from_args(
|
||||
["read", path.to_str().expect("utf-8 test path")]
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
&mut stdin,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
)
|
||||
.expect("read should succeed");
|
||||
|
||||
assert_eq!(stdout, expected);
|
||||
assert_eq!(stderr, Vec::<u8>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_reports_directory_error() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
let mut stdout = Vec::new();
|
||||
let mut stdin = std::io::empty();
|
||||
|
||||
let error = execute(
|
||||
FsCommand::ReadFile {
|
||||
path: tempdir.path().to_path_buf(),
|
||||
},
|
||||
&mut stdin,
|
||||
&mut stdout,
|
||||
)
|
||||
.expect_err("reading a directory should fail");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(error.kind, FsErrorKind::PermissionDenied);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert_eq!(error.kind, FsErrorKind::IsADirectory);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_from_args_serializes_errors_to_stderr() {
|
||||
let tempdir = tempdir().expect("tempdir");
|
||||
let missing = tempdir.path().join("missing.txt");
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let mut stdin = std::io::empty();
|
||||
|
||||
let result = run_from_args(
|
||||
["read", missing.to_str().expect("utf-8 test path")]
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
&mut stdin,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
);
|
||||
|
||||
assert!(result.is_err(), "missing file should fail");
|
||||
assert_eq!(stdout, Vec::<u8>::new());
|
||||
|
||||
let error: crate::FsError = serde_json::from_slice(&stderr).expect("structured fs error");
|
||||
assert_eq!(error.kind, FsErrorKind::NotFound);
|
||||
assert!(error.raw_os_error.is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user