core: add a sandbox-backed fs helper

This commit is contained in:
Michael Bolin
2026-03-19 20:11:12 -07:00
parent a3e59e9e85
commit 81ed2df7b0
17 changed files with 499 additions and 37 deletions
+1
View File
@@ -38,6 +38,7 @@ codex-login = { workspace = true }
codex-shell-command = { workspace = true }
codex-skills = { workspace = true }
codex-execpolicy = { workspace = true }
codex-fs-ops = { workspace = true }
codex-file-search = { workspace = true }
codex-git = { workspace = true }
codex-hooks = { workspace = true }
+105 -37
View File
@@ -319,6 +319,62 @@ pub(crate) async fn execute_exec_request(
stdout_stream: Option<StdoutStream>,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> Result<ExecToolCallOutput> {
let PreparedExecRequest {
params,
sandbox,
file_system_sandbox_policy,
network_sandbox_policy,
} = prepare_exec_request(exec_request);
let start = Instant::now();
let raw_output_result = exec(
params,
sandbox,
sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
stdout_stream,
after_spawn,
)
.await;
let duration = start.elapsed();
Ok(normalize_exec_result(raw_output_result, sandbox, duration)?.to_utf8_lossy_output())
}
pub(crate) async fn execute_exec_request_raw_output(
exec_request: ExecRequest,
sandbox_policy: &SandboxPolicy,
stdout_stream: Option<StdoutStream>,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> Result<ExecToolCallRawOutput> {
let PreparedExecRequest {
params,
sandbox,
file_system_sandbox_policy,
network_sandbox_policy,
} = prepare_exec_request(exec_request);
let start = Instant::now();
let raw_output_result = exec(
params,
sandbox,
sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
stdout_stream,
after_spawn,
)
.await;
let duration = start.elapsed();
normalize_exec_result(raw_output_result, sandbox, duration)
}
struct PreparedExecRequest {
params: ExecParams,
sandbox: SandboxType,
file_system_sandbox_policy: FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
}
fn prepare_exec_request(exec_request: ExecRequest) -> PreparedExecRequest {
let ExecRequest {
command,
cwd,
@@ -338,33 +394,24 @@ pub(crate) async fn execute_exec_request(
} = exec_request;
let _ = _sandbox_policy_from_env;
let params = ExecParams {
command,
cwd,
expiration,
capture_policy,
env,
network: network.clone(),
sandbox_permissions,
windows_sandbox_level,
windows_sandbox_private_desktop,
justification,
arg0,
};
let start = Instant::now();
let raw_output_result = exec(
params,
PreparedExecRequest {
params: ExecParams {
command,
cwd,
expiration,
capture_policy,
env,
network,
sandbox_permissions,
windows_sandbox_level,
windows_sandbox_private_desktop,
justification,
arg0,
},
sandbox,
sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
stdout_stream,
after_spawn,
)
.await;
let duration = start.elapsed();
finalize_exec_result(raw_output_result, sandbox, duration)
file_system_sandbox_policy,
}
}
#[cfg(target_os = "windows")]
@@ -558,11 +605,11 @@ async fn exec_windows_sandbox(
})
}
fn finalize_exec_result(
fn normalize_exec_result(
raw_output_result: std::result::Result<RawExecToolCallOutput, CodexErr>,
sandbox_type: SandboxType,
duration: Duration,
) -> Result<ExecToolCallOutput> {
) -> Result<ExecToolCallRawOutput> {
match raw_output_result {
Ok(raw_output) => {
#[allow(unused_mut)]
@@ -584,27 +631,25 @@ fn finalize_exec_result(
exit_code = EXEC_TIMEOUT_EXIT_CODE;
}
let stdout = raw_output.stdout.from_utf8_lossy();
let stderr = raw_output.stderr.from_utf8_lossy();
let aggregated_output = raw_output.aggregated_output.from_utf8_lossy();
let exec_output = ExecToolCallOutput {
let exec_output = ExecToolCallRawOutput {
exit_code,
stdout,
stderr,
aggregated_output,
stdout: raw_output.stdout,
stderr: raw_output.stderr,
aggregated_output: raw_output.aggregated_output,
duration,
timed_out,
};
if timed_out {
return Err(CodexErr::Sandbox(SandboxErr::Timeout {
output: Box::new(exec_output),
output: Box::new(exec_output.to_utf8_lossy_output()),
}));
}
if is_likely_sandbox_denied(sandbox_type, &exec_output) {
let string_output = exec_output.to_utf8_lossy_output();
if is_likely_sandbox_denied(sandbox_type, &string_output) {
return Err(CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(exec_output),
output: Box::new(string_output),
network_policy_decision: None,
}));
}
@@ -796,6 +841,16 @@ pub struct ExecToolCallOutput {
pub timed_out: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct ExecToolCallRawOutput {
pub exit_code: i32,
pub stdout: StreamOutput<Vec<u8>>,
pub stderr: StreamOutput<Vec<u8>>,
pub aggregated_output: StreamOutput<Vec<u8>>,
pub duration: Duration,
pub timed_out: bool,
}
impl Default for ExecToolCallOutput {
fn default() -> Self {
Self {
@@ -809,6 +864,19 @@ impl Default for ExecToolCallOutput {
}
}
impl ExecToolCallRawOutput {
fn to_utf8_lossy_output(&self) -> ExecToolCallOutput {
ExecToolCallOutput {
exit_code: self.exit_code,
stdout: self.stdout.from_utf8_lossy(),
stderr: self.stderr.from_utf8_lossy(),
aggregated_output: self.aggregated_output.from_utf8_lossy(),
duration: self.duration,
timed_out: self.timed_out,
}
}
}
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
async fn exec(
params: ExecParams,
+1
View File
@@ -122,6 +122,7 @@ pub mod default_client {
pub mod project_doc;
mod rollout;
pub(crate) mod safety;
mod sandboxed_fs;
pub mod seatbelt;
pub mod shell;
pub mod shell_snapshot;
+145
View File
@@ -0,0 +1,145 @@
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecExpiration;
use crate::exec::ExecToolCallRawOutput;
use crate::sandboxing::CommandSpec;
use crate::sandboxing::SandboxPermissions;
use crate::sandboxing::execute_env_raw_output;
use crate::sandboxing::merge_permission_profiles;
use crate::tools::sandboxing::SandboxAttempt;
use crate::tools::sandboxing::SandboxablePreference;
use codex_fs_ops::CODEX_CORE_FS_OPS_ARG1;
use codex_fs_ops::READ_FILE_OPERATION_ARG;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
/// An [crate::sandboxing::ExecRequest] must have an `expiration` set, so we
/// specify a generous timeout for the operations specified by
/// [SandboxedFsOperation].
const SANDBOXED_FS_TIMEOUT: Duration = Duration::from_secs(30);
/// Read the contents of the specified file subject to the sandbox constraints
/// imposed by the provided session and turn context.
#[allow(dead_code)]
pub(crate) async fn read_file(
path: AbsolutePathBuf,
session: &Arc<Session>,
turn: &Arc<TurnContext>,
) -> Result<Vec<u8>, SandboxedFsError> {
let output = perform_operation(SandboxedFsOperation::Read { path }, session, turn).await?;
Ok(output.stdout.text)
}
/// Operations supported by the [CODEX_CORE_FS_OPS_ARG1] sandbox helper.
enum SandboxedFsOperation {
Read { path: AbsolutePathBuf },
}
async fn perform_operation(
operation: SandboxedFsOperation,
session: &Arc<Session>,
turn: &Arc<TurnContext>,
) -> Result<ExecToolCallRawOutput, SandboxedFsError> {
let exe = std::env::current_exe().map_err(|error| SandboxedFsError::ResolveExe {
message: error.to_string(),
})?;
let additional_permissions = effective_granted_permissions(session).await;
let sandbox_manager = crate::sandboxing::SandboxManager::new();
let attempt = SandboxAttempt {
sandbox: sandbox_manager.select_initial(
&turn.file_system_sandbox_policy,
turn.network_sandbox_policy,
SandboxablePreference::Auto,
turn.windows_sandbox_level,
/*has_managed_network_requirements*/ false,
),
policy: &turn.sandbox_policy,
file_system_policy: &turn.file_system_sandbox_policy,
network_policy: turn.network_sandbox_policy,
enforce_managed_network: false,
manager: &sandbox_manager,
sandbox_cwd: &turn.cwd,
codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock: turn.features.use_legacy_landlock(),
windows_sandbox_level: turn.windows_sandbox_level,
windows_sandbox_private_desktop: turn.config.permissions.windows_sandbox_private_desktop,
};
let args = match operation {
SandboxedFsOperation::Read { ref path } => vec![
CODEX_CORE_FS_OPS_ARG1.to_string(),
READ_FILE_OPERATION_ARG.to_string(),
path.to_string_lossy().to_string(),
],
};
let exec_request = attempt
.env_for(
CommandSpec {
program: exe.to_string_lossy().to_string(),
args,
cwd: turn.cwd.clone(),
env: HashMap::new(),
expiration: ExecExpiration::Timeout(SANDBOXED_FS_TIMEOUT),
capture_policy: ExecCapturePolicy::FullBuffer,
sandbox_permissions: SandboxPermissions::UseDefault,
additional_permissions,
justification: None,
},
/*network*/ None,
)
.map_err(|error| SandboxedFsError::ProcessFailed {
exit_code: -1,
message: error.to_string(),
})?;
let output = execute_env_raw_output(exec_request, /*stdout_stream*/ None)
.await
.map_err(|error| SandboxedFsError::ProcessFailed {
exit_code: 1,
message: error.to_string(),
})?;
if output.exit_code == 0 {
Ok(output)
} else {
Err(parse_helper_failure(
output.exit_code,
&output.stderr.text,
&output.stdout.text,
))
}
}
async fn effective_granted_permissions(session: &Session) -> Option<PermissionProfile> {
let granted_session_permissions = session.granted_session_permissions().await;
let granted_turn_permissions = session.granted_turn_permissions().await;
merge_permission_profiles(
granted_session_permissions.as_ref(),
granted_turn_permissions.as_ref(),
)
}
fn parse_helper_failure(exit_code: i32, stderr: &[u8], stdout: &[u8]) -> SandboxedFsError {
let stderr = String::from_utf8_lossy(stderr);
let stdout = String::from_utf8_lossy(stdout);
let message = if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else if !stdout.trim().is_empty() {
stdout.trim().to_string()
} else {
"no error details emitted".to_string()
};
SandboxedFsError::ProcessFailed { exit_code, message }
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum SandboxedFsError {
#[error("failed to determine codex executable: {message}")]
ResolveExe { message: String },
#[error("sandboxed fs helper exited with code {exit_code}: {message}")]
ProcessFailed { exit_code: i32, message: String },
}
+16
View File
@@ -11,9 +11,11 @@ pub(crate) mod macos_permissions;
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecExpiration;
use crate::exec::ExecToolCallOutput;
use crate::exec::ExecToolCallRawOutput;
use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec::execute_exec_request;
use crate::exec::execute_exec_request_raw_output;
use crate::landlock::allow_network_for_proxy;
use crate::landlock::create_linux_sandbox_command_args_for_policies;
use crate::protocol::SandboxPolicy;
@@ -742,6 +744,20 @@ pub async fn execute_env(
.await
}
pub(crate) async fn execute_env_raw_output(
exec_request: ExecRequest,
stdout_stream: Option<StdoutStream>,
) -> crate::error::Result<ExecToolCallRawOutput> {
let effective_policy = exec_request.sandbox_policy.clone();
execute_exec_request_raw_output(
exec_request,
&effective_policy,
stdout_stream,
/*after_spawn*/ None,
)
.await
}
pub async fn execute_exec_request_with_after_spawn(
exec_request: ExecRequest,
stdout_stream: Option<StdoutStream>,