Historically, we spawned the Seatbelt and Landlock sandboxes in substantially different ways: For **Seatbelt**, we would run `/usr/bin/sandbox-exec` with our policy specified as an arg followed by the original command:d1de7bb383/codex-rs/core/src/exec.rs (L147-L219)For **Landlock/Seccomp**, we would do `tokio::runtime::Builder::new_current_thread()`, _invoke Landlock/Seccomp APIs to modify the permissions of that new thread_, and then spawn the command:d1de7bb383/codex-rs/core/src/exec_linux.rs (L28-L49)While it is neat that Landlock/Seccomp supports applying a policy to only one thread without having to apply it to the entire process, it requires us to maintain two different codepaths and is a bit harder to reason about. The tipping point was https://github.com/openai/codex/pull/1061, in which we had to start building up the `env` in an unexpected way for the existing Landlock/Seccomp approach to continue to work. This PR overhauls things so that we do similar things for Mac and Linux. It turned out that we were already building our own "helper binary" comparable to Mac's `sandbox-exec` as part of the `cli` crate:d1de7bb383/codex-rs/cli/Cargo.toml (L10-L12)We originally created this to build a small binary to include with the Node.js version of the Codex CLI to provide support for Linux sandboxing. Though the sticky bit is that, at this point, we still want to deploy the Rust version of Codex as a single, standalone binary rather than a CLI and a supporting sandboxing binary. To satisfy this goal, we use "the arg0 trick," in which we: * use `std::env::current_exe()` to get the path to the CLI that is currently running * use the CLI as the `program` for the `Command` * set `"codex-linux-sandbox"` as arg0 for the `Command` A CLI that supports sandboxing should check arg0 at the start of the program. If it is `"codex-linux-sandbox"`, it must invoke `codex_linux_sandbox::run_main()`, which runs the CLI as if it were `codex-linux-sandbox`. When acting as `codex-linux-sandbox`, we make the appropriate Landlock/Seccomp API calls and then use `execvp(3)` to spawn the original command, so do _replace_ the process rather than spawn a subprocess. Incidentally, we do this before starting the Tokio runtime, so the process should only have one thread when `execvp(3)` is called. Because the `core` crate that needs to spawn the Linux sandboxing is not a CLI in its own right, this means that every CLI that includes `core` and relies on this behavior has to (1) implement it and (2) provide the path to the sandboxing executable. While the path is almost always `std::env::current_exe()`, we needed to make this configurable for integration tests, so `Config` now has a `codex_linux_sandbox_exe: Option<PathBuf>` property to facilitate threading this through, introduced in https://github.com/openai/codex/pull/1089. This common pattern is now captured in `codex_linux_sandbox::run_with_sandbox()` and all of the `main.rs` functions that should use it have been updated as part of this PR. The `codex-linux-sandbox` crate added to the Cargo workspace as part of this PR now has the bulk of the Landlock/Seccomp logic, which makes `core` a bit simpler. Indeed, `core/src/exec_linux.rs` and `core/src/landlock.rs` were removed/ported as part of this PR. I also moved the unit tests for this code into an integration test, `linux-sandbox/tests/landlock.rs`, in which I use `env!("CARGO_BIN_EXE_codex-linux-sandbox")` as the value for `codex_linux_sandbox_exe` since `std::env::current_exe()` is not appropriate in that case.
135 lines
4.3 KiB
Rust
135 lines
4.3 KiB
Rust
use reqwest::StatusCode;
|
||
use serde_json;
|
||
use std::io;
|
||
use thiserror::Error;
|
||
use tokio::task::JoinError;
|
||
|
||
pub type Result<T> = std::result::Result<T, CodexErr>;
|
||
|
||
#[derive(Error, Debug)]
|
||
pub enum SandboxErr {
|
||
/// Error from sandbox execution
|
||
#[error("sandbox denied exec error, exit code: {0}, stdout: {1}, stderr: {2}")]
|
||
Denied(i32, String, String),
|
||
|
||
/// Error from linux seccomp filter setup
|
||
#[cfg(target_os = "linux")]
|
||
#[error("seccomp setup error")]
|
||
SeccompInstall(#[from] seccompiler::Error),
|
||
|
||
/// Error from linux seccomp backend
|
||
#[cfg(target_os = "linux")]
|
||
#[error("seccomp backend error")]
|
||
SeccompBackend(#[from] seccompiler::BackendError),
|
||
|
||
/// Command timed out
|
||
#[error("command timed out")]
|
||
Timeout,
|
||
|
||
/// Command was killed by a signal
|
||
#[error("command was killed by a signal")]
|
||
Signal(i32),
|
||
|
||
/// Error from linux landlock
|
||
#[error("Landlock was not able to fully enforce all sandbox rules")]
|
||
LandlockRestrict,
|
||
}
|
||
|
||
#[derive(Error, Debug)]
|
||
pub enum CodexErr {
|
||
/// Returned by ResponsesClient when the SSE stream disconnects or errors out **after** the HTTP
|
||
/// handshake has succeeded but **before** it finished emitting `response.completed`.
|
||
///
|
||
/// The Session loop treats this as a transient error and will automatically retry the turn.
|
||
#[error("stream disconnected before completion: {0}")]
|
||
Stream(String),
|
||
|
||
/// Returned by run_command_stream when the spawned child process timed out (10s).
|
||
#[error("timeout waiting for child process to exit")]
|
||
Timeout,
|
||
|
||
/// Returned by run_command_stream when the child could not be spawned (its stdout/stderr pipes
|
||
/// could not be captured). Analogous to the previous `CodexError::Spawn` variant.
|
||
#[error("spawn failed: child stdout/stderr not captured")]
|
||
Spawn,
|
||
|
||
/// Returned by run_command_stream when the user pressed Ctrl‑C (SIGINT). Session uses this to
|
||
/// surface a polite FunctionCallOutput back to the model instead of crashing the CLI.
|
||
#[error("interrupted (Ctrl-C)")]
|
||
Interrupted,
|
||
|
||
/// Unexpected HTTP status code.
|
||
#[error("unexpected status {0}: {1}")]
|
||
UnexpectedStatus(StatusCode, String),
|
||
|
||
/// Retry limit exceeded.
|
||
#[error("exceeded retry limit, last status: {0}")]
|
||
RetryLimit(StatusCode),
|
||
|
||
/// Agent loop died unexpectedly
|
||
#[error("internal error; agent loop died unexpectedly")]
|
||
InternalAgentDied,
|
||
|
||
/// Sandbox error
|
||
#[error("sandbox error: {0}")]
|
||
Sandbox(#[from] SandboxErr),
|
||
|
||
#[error("codex-linux-sandbox was required but not provided")]
|
||
LandlockSandboxExecutableNotProvided,
|
||
|
||
// -----------------------------------------------------------------
|
||
// Automatic conversions for common external error types
|
||
// -----------------------------------------------------------------
|
||
#[error(transparent)]
|
||
Io(#[from] io::Error),
|
||
|
||
#[error(transparent)]
|
||
Reqwest(#[from] reqwest::Error),
|
||
|
||
#[error(transparent)]
|
||
Json(#[from] serde_json::Error),
|
||
|
||
#[cfg(target_os = "linux")]
|
||
#[error(transparent)]
|
||
LandlockRuleset(#[from] landlock::RulesetError),
|
||
|
||
#[cfg(target_os = "linux")]
|
||
#[error(transparent)]
|
||
LandlockPathFd(#[from] landlock::PathFdError),
|
||
|
||
#[error(transparent)]
|
||
TokioJoin(#[from] JoinError),
|
||
|
||
#[error("{0}")]
|
||
EnvVar(EnvVarError),
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct EnvVarError {
|
||
/// Name of the environment variable that is missing.
|
||
pub var: String,
|
||
|
||
/// Optional instructions to help the user get a valid value for the
|
||
/// variable and set it.
|
||
pub instructions: Option<String>,
|
||
}
|
||
|
||
impl std::fmt::Display for EnvVarError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
write!(f, "Missing environment variable: `{}`.", self.var)?;
|
||
if let Some(instructions) = &self.instructions {
|
||
write!(f, " {instructions}")?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl CodexErr {
|
||
/// Minimal shim so that existing `e.downcast_ref::<CodexErr>()` checks continue to compile
|
||
/// after replacing `anyhow::Error` in the return signature. This mirrors the behavior of
|
||
/// `anyhow::Error::downcast_ref` but works directly on our concrete enum.
|
||
pub fn downcast_ref<T: std::any::Any>(&self) -> Option<&T> {
|
||
(self as &dyn std::any::Any).downcast_ref::<T>()
|
||
}
|
||
}
|