mirror of
https://github.com/openai/codex.git
synced 2026-02-02 06:57:03 +00:00
Compare commits
1 Commits
feat-sessi
...
dev/zhao/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8832459f7f |
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -1254,6 +1254,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"clap",
|
||||
"codex-core",
|
||||
"codex-execpolicy",
|
||||
"libc",
|
||||
"path-absolutize",
|
||||
"pretty_assertions",
|
||||
@@ -1263,6 +1264,7 @@ dependencies = [
|
||||
"shlex",
|
||||
"socket2 0.6.0",
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
|
||||
@@ -16,7 +16,7 @@ mod codex_conversation;
|
||||
mod compact_remote;
|
||||
pub use codex_conversation::CodexConversation;
|
||||
mod codex_delegate;
|
||||
mod command_safety;
|
||||
pub mod command_safety;
|
||||
pub mod config;
|
||||
pub mod config_loader;
|
||||
mod context_manager;
|
||||
|
||||
@@ -24,6 +24,7 @@ anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-core = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
path-absolutize = { workspace = true }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
@@ -51,6 +52,7 @@ tokio = { workspace = true, features = [
|
||||
"signal",
|
||||
] }
|
||||
tokio-util = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
||||
|
||||
|
||||
@@ -55,19 +55,21 @@
|
||||
//! | |
|
||||
//! o<-----x
|
||||
//!
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::{self};
|
||||
|
||||
use crate::posix::mcp_escalation_policy::ExecPolicyOutcome;
|
||||
use crate::posix::exec_policy::ExecPolicyEvaluator;
|
||||
use crate::posix::exec_policy::load_policy_from_codex_home;
|
||||
use crate::posix::mcp_escalation_policy::SharedExecPolicy;
|
||||
|
||||
mod escalate_client;
|
||||
mod escalate_protocol;
|
||||
mod escalate_server;
|
||||
mod escalation_policy;
|
||||
mod exec_policy;
|
||||
mod mcp;
|
||||
mod mcp_escalation_policy;
|
||||
mod socket;
|
||||
@@ -114,7 +116,10 @@ pub async fn main_mcp_server() -> anyhow::Result<()> {
|
||||
};
|
||||
|
||||
tracing::info!("Starting MCP server");
|
||||
let service = mcp::serve(bash_path, execve_wrapper, dummy_exec_policy)
|
||||
let policy = load_policy_from_codex_home().await?;
|
||||
let policy: SharedExecPolicy = std::sync::Arc::new(ExecPolicyEvaluator::new(policy));
|
||||
|
||||
let service = mcp::serve(bash_path, execve_wrapper, policy)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
tracing::error!("serving error: {:?}", e);
|
||||
@@ -144,27 +149,3 @@ pub async fn main_execve_wrapper() -> anyhow::Result<()> {
|
||||
let exit_code = escalate_client::run(file, argv).await?;
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
// TODO: replace with execpolicy
|
||||
|
||||
fn dummy_exec_policy(file: &Path, argv: &[String], _workdir: &Path) -> ExecPolicyOutcome {
|
||||
if file.ends_with("rm") {
|
||||
ExecPolicyOutcome::Forbidden
|
||||
} else if file.ends_with("git") {
|
||||
ExecPolicyOutcome::Prompt {
|
||||
run_with_escalated_permissions: false,
|
||||
}
|
||||
} else if file == Path::new("/opt/homebrew/bin/gh")
|
||||
&& let [_, arg1, arg2, ..] = argv
|
||||
&& arg1 == "issue"
|
||||
&& arg2 == "list"
|
||||
{
|
||||
ExecPolicyOutcome::Allow {
|
||||
run_with_escalated_permissions: true,
|
||||
}
|
||||
} else {
|
||||
ExecPolicyOutcome::Allow {
|
||||
run_with_escalated_permissions: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
266
codex-rs/exec-server/src/posix/exec_policy.rs
Normal file
266
codex-rs/exec-server/src/posix/exec_policy.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core::command_safety::is_dangerous_command::command_might_be_dangerous;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::PolicyParser;
|
||||
use thiserror::Error;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::posix::mcp_escalation_policy::ExecPolicy;
|
||||
use crate::posix::mcp_escalation_policy::ExecPolicyOutcome;
|
||||
|
||||
const POLICY_DIR_NAME: &str = "policy";
|
||||
const POLICY_EXTENSION: &str = "codexpolicy";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum ExecPolicyError {
|
||||
#[error("failed to resolve CODEX_HOME: {source}")]
|
||||
CodexHome { source: std::io::Error },
|
||||
|
||||
#[error("failed to read execpolicy files from {dir}: {source}")]
|
||||
ReadDir {
|
||||
dir: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("failed to read execpolicy file {path}: {source}")]
|
||||
ReadFile {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("failed to parse execpolicy file {path}: {source}")]
|
||||
ParsePolicy {
|
||||
path: String,
|
||||
source: codex_execpolicy::Error,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) async fn load_policy_from_codex_home() -> Result<Arc<Policy>, ExecPolicyError> {
|
||||
let codex_home = codex_core::config::find_codex_home()
|
||||
.map_err(|source| ExecPolicyError::CodexHome { source })?;
|
||||
|
||||
let policy_dir = codex_home.join(POLICY_DIR_NAME);
|
||||
let policy_paths = collect_policy_files(&policy_dir).await?;
|
||||
|
||||
let mut parser = PolicyParser::new();
|
||||
for policy_path in &policy_paths {
|
||||
let contents =
|
||||
fs::read_to_string(policy_path)
|
||||
.await
|
||||
.map_err(|source| ExecPolicyError::ReadFile {
|
||||
path: policy_path.clone(),
|
||||
source,
|
||||
})?;
|
||||
let identifier = policy_path.to_string_lossy().to_string();
|
||||
parser
|
||||
.parse(&identifier, &contents)
|
||||
.map_err(|source| ExecPolicyError::ParsePolicy {
|
||||
path: identifier,
|
||||
source,
|
||||
})?;
|
||||
}
|
||||
|
||||
let policy = Arc::new(parser.build());
|
||||
tracing::debug!(
|
||||
"loaded execpolicy from {} files in {}",
|
||||
policy_paths.len(),
|
||||
policy_dir.display()
|
||||
);
|
||||
Ok(policy)
|
||||
}
|
||||
|
||||
pub(crate) struct ExecPolicyEvaluator {
|
||||
policy: Arc<Policy>,
|
||||
}
|
||||
|
||||
impl ExecPolicyEvaluator {
|
||||
pub(crate) fn new(policy: Arc<Policy>) -> Self {
|
||||
Self { policy }
|
||||
}
|
||||
|
||||
fn command_for(file: &Path, argv: &[String]) -> Vec<String> {
|
||||
let cmd0 = argv
|
||||
.first()
|
||||
.and_then(|s| Path::new(s).file_name())
|
||||
.and_then(|s| s.to_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(std::string::ToString::to_string)
|
||||
.or_else(|| {
|
||||
file.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(std::string::ToString::to_string)
|
||||
})
|
||||
.unwrap_or_else(|| file.display().to_string());
|
||||
|
||||
let mut command = Vec::with_capacity(argv.len().max(1));
|
||||
command.push(cmd0);
|
||||
if argv.len() > 1 {
|
||||
command.extend(argv.iter().skip(1).cloned());
|
||||
}
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecPolicy for ExecPolicyEvaluator {
|
||||
fn evaluate(&self, file: &Path, argv: &[String], _workdir: &Path) -> ExecPolicyOutcome {
|
||||
let command = Self::command_for(file, argv);
|
||||
|
||||
match self.policy.check_multiple(std::iter::once(&command)) {
|
||||
Evaluation::Match { decision, .. } => match decision {
|
||||
Decision::Forbidden => ExecPolicyOutcome::Forbidden,
|
||||
Decision::Prompt => ExecPolicyOutcome::Prompt {
|
||||
run_with_escalated_permissions: true,
|
||||
},
|
||||
Decision::Allow => ExecPolicyOutcome::Allow {
|
||||
run_with_escalated_permissions: true,
|
||||
},
|
||||
},
|
||||
Evaluation::NoMatch { .. } => {
|
||||
if command_might_be_dangerous(&command) {
|
||||
ExecPolicyOutcome::Prompt {
|
||||
run_with_escalated_permissions: false,
|
||||
}
|
||||
} else {
|
||||
ExecPolicyOutcome::Allow {
|
||||
run_with_escalated_permissions: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_policy_files(dir: &Path) -> Result<Vec<PathBuf>, ExecPolicyError> {
|
||||
let mut read_dir = match fs::read_dir(dir).await {
|
||||
Ok(read_dir) => read_dir,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(Vec::new()),
|
||||
Err(source) => {
|
||||
return Err(ExecPolicyError::ReadDir {
|
||||
dir: dir.to_path_buf(),
|
||||
source,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mut policy_paths = Vec::new();
|
||||
while let Some(entry) =
|
||||
read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|source| ExecPolicyError::ReadDir {
|
||||
dir: dir.to_path_buf(),
|
||||
source,
|
||||
})?
|
||||
{
|
||||
let path = entry.path();
|
||||
let file_type = entry
|
||||
.file_type()
|
||||
.await
|
||||
.map_err(|source| ExecPolicyError::ReadDir {
|
||||
dir: dir.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
if path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_some_and(|ext| ext == POLICY_EXTENSION)
|
||||
&& file_type.is_file()
|
||||
{
|
||||
policy_paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
policy_paths.sort();
|
||||
Ok(policy_paths)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
original: Option<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &std::ffi::OsStr) -> Self {
|
||||
let original = std::env::var_os(key);
|
||||
unsafe {
|
||||
std::env::set_var(key, value);
|
||||
}
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
match &self.original {
|
||||
Some(value) => std::env::set_var(self.key, value),
|
||||
None => std::env::remove_var(self.key),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_policy_bypasses_sandbox() {
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse(
|
||||
"test.codexpolicy",
|
||||
r#"prefix_rule(pattern=["echo"], decision="allow")"#,
|
||||
)
|
||||
.expect("parse policy");
|
||||
let evaluator = ExecPolicyEvaluator::new(Arc::new(parser.build()));
|
||||
|
||||
let outcome = evaluator.evaluate(
|
||||
Path::new("/bin/echo"),
|
||||
&["echo".to_string()],
|
||||
Path::new("/"),
|
||||
);
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
ExecPolicyOutcome::Allow {
|
||||
run_with_escalated_permissions: true
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_match_dangerous_command_prompts() {
|
||||
let evaluator = ExecPolicyEvaluator::new(Arc::new(Policy::empty()));
|
||||
let outcome = evaluator.evaluate(
|
||||
Path::new("/bin/rm"),
|
||||
&["rm".to_string(), "-rf".to_string(), "/".to_string()],
|
||||
Path::new("/"),
|
||||
);
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
ExecPolicyOutcome::Prompt {
|
||||
run_with_escalated_permissions: false
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_policy_dir_loads_empty() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let _guard = EnvVarGuard::set("CODEX_HOME", dir.path().as_os_str());
|
||||
let policy = load_policy_from_codex_home().await.expect("load policy");
|
||||
assert!(matches!(
|
||||
policy.check_multiple(std::iter::once(&vec!["rm".to_string()])),
|
||||
Evaluation::NoMatch { .. }
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ use tracing::debug;
|
||||
|
||||
use crate::posix::escalate_server::EscalateServer;
|
||||
use crate::posix::escalate_server::{self};
|
||||
use crate::posix::mcp_escalation_policy::ExecPolicy;
|
||||
use crate::posix::mcp_escalation_policy::McpEscalationPolicy;
|
||||
use crate::posix::mcp_escalation_policy::SharedExecPolicy;
|
||||
use crate::posix::stopwatch::Stopwatch;
|
||||
|
||||
/// Path to our patched bash.
|
||||
@@ -78,13 +78,13 @@ pub struct ExecTool {
|
||||
tool_router: ToolRouter<ExecTool>,
|
||||
bash_path: PathBuf,
|
||||
execve_wrapper: PathBuf,
|
||||
policy: ExecPolicy,
|
||||
policy: SharedExecPolicy,
|
||||
sandbox_state: Arc<RwLock<Option<SandboxState>>>,
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
impl ExecTool {
|
||||
pub fn new(bash_path: PathBuf, execve_wrapper: PathBuf, policy: ExecPolicy) -> Self {
|
||||
pub fn new(bash_path: PathBuf, execve_wrapper: PathBuf, policy: SharedExecPolicy) -> Self {
|
||||
Self {
|
||||
tool_router: Self::tool_router(),
|
||||
bash_path,
|
||||
@@ -121,7 +121,7 @@ impl ExecTool {
|
||||
let escalate_server = EscalateServer::new(
|
||||
self.bash_path.clone(),
|
||||
self.execve_wrapper.clone(),
|
||||
McpEscalationPolicy::new(self.policy, context, stopwatch.clone()),
|
||||
McpEscalationPolicy::new(self.policy.clone(), context, stopwatch.clone()),
|
||||
);
|
||||
|
||||
let result = escalate_server
|
||||
@@ -198,7 +198,7 @@ impl ServerHandler for ExecTool {
|
||||
pub(crate) async fn serve(
|
||||
bash_path: PathBuf,
|
||||
execve_wrapper: PathBuf,
|
||||
policy: ExecPolicy,
|
||||
policy: SharedExecPolicy,
|
||||
) -> Result<RunningService<RoleServer, ExecTool>, rmcp::service::ServerInitializeError> {
|
||||
let tool = ExecTool::new(bash_path, execve_wrapper, policy);
|
||||
tool.serve(stdio()).await
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rmcp::ErrorData as McpError;
|
||||
use rmcp::RoleServer;
|
||||
@@ -18,7 +19,11 @@ use crate::posix::stopwatch::Stopwatch;
|
||||
/// `argv` is the argv, including the program name (`argv[0]`).
|
||||
/// `workdir` is the absolute, canonical path to the working directory in which to execute the
|
||||
/// command.
|
||||
pub(crate) type ExecPolicy = fn(file: &Path, argv: &[String], workdir: &Path) -> ExecPolicyOutcome;
|
||||
pub(crate) trait ExecPolicy: Send + Sync {
|
||||
fn evaluate(&self, file: &Path, argv: &[String], workdir: &Path) -> ExecPolicyOutcome;
|
||||
}
|
||||
|
||||
pub(crate) type SharedExecPolicy = Arc<dyn ExecPolicy>;
|
||||
|
||||
pub(crate) enum ExecPolicyOutcome {
|
||||
Allow {
|
||||
@@ -33,14 +38,14 @@ pub(crate) enum ExecPolicyOutcome {
|
||||
/// ExecPolicy with access to the MCP RequestContext so that it can leverage
|
||||
/// elicitations.
|
||||
pub(crate) struct McpEscalationPolicy {
|
||||
policy: ExecPolicy,
|
||||
policy: SharedExecPolicy,
|
||||
context: RequestContext<RoleServer>,
|
||||
stopwatch: Stopwatch,
|
||||
}
|
||||
|
||||
impl McpEscalationPolicy {
|
||||
pub(crate) fn new(
|
||||
policy: ExecPolicy,
|
||||
policy: SharedExecPolicy,
|
||||
context: RequestContext<RoleServer>,
|
||||
stopwatch: Stopwatch,
|
||||
) -> Self {
|
||||
@@ -103,7 +108,7 @@ impl EscalationPolicy for McpEscalationPolicy {
|
||||
argv: &[String],
|
||||
workdir: &Path,
|
||||
) -> Result<EscalateAction, rmcp::ErrorData> {
|
||||
let outcome = (self.policy)(file, argv, workdir);
|
||||
let outcome = self.policy.evaluate(file, argv, workdir);
|
||||
let action = match outcome {
|
||||
ExecPolicyOutcome::Allow {
|
||||
run_with_escalated_permissions,
|
||||
|
||||
Reference in New Issue
Block a user