2025-11-18 16:20:19 -08:00
|
|
|
//! This is an MCP that implements an alternative `shell` tool with fine-grained privilege
|
|
|
|
|
//! escalation based on a per-exec() policy.
|
|
|
|
|
//!
|
|
|
|
|
//! We spawn Bash process inside a sandbox. The Bash we spawn is patched to allow us to intercept
|
|
|
|
|
//! every exec() call it makes by invoking a wrapper program and passing in the arguments it would
|
|
|
|
|
//! have passed to exec(). The Bash process (and its descendants) inherit a communication socket
|
|
|
|
|
//! from us, and we give its fd number in the CODEX_ESCALATE_SOCKET environment variable.
|
|
|
|
|
//!
|
|
|
|
|
//! When we intercept an exec() call, we send a message over the socket back to the main
|
|
|
|
|
//! MCP process. The MCP process can then decide whether to allow the exec() call to proceed
|
|
|
|
|
//! or to escalate privileges and run the requested command with elevated permissions. In the
|
|
|
|
|
//! latter case, we send a message back to the child requesting that it forward its open FDs to us.
|
|
|
|
|
//! We then execute the requested command on its behalf, patching in the forwarded FDs.
|
|
|
|
|
//!
|
|
|
|
|
//!
|
|
|
|
|
//! ### The privilege escalation flow
|
|
|
|
|
//!
|
|
|
|
|
//! Child MCP Bash Escalate Helper
|
|
|
|
|
//! |
|
|
|
|
|
//! o----->o
|
|
|
|
|
//! | |
|
|
|
|
|
//! | o--(exec)-->o
|
|
|
|
|
//! | | |
|
|
|
|
|
//! |o<-(EscalateReq)--o
|
|
|
|
|
//! || | |
|
|
|
|
|
//! |o--(Escalate)---->o
|
|
|
|
|
//! || | |
|
|
|
|
|
//! |o<---------(fds)--o
|
|
|
|
|
//! || | |
|
|
|
|
|
//! o<-----o | |
|
|
|
|
|
//! | || | |
|
|
|
|
|
//! x----->o | |
|
|
|
|
|
//! || | |
|
|
|
|
|
//! |x--(exit code)--->o
|
|
|
|
|
//! | | |
|
|
|
|
|
//! | o<--(exit)--x
|
|
|
|
|
//! | |
|
|
|
|
|
//! o<-----x
|
|
|
|
|
//!
|
|
|
|
|
//! ### The non-escalation flow
|
|
|
|
|
//!
|
|
|
|
|
//! MCP Bash Escalate Helper Child
|
|
|
|
|
//! |
|
|
|
|
|
//! o----->o
|
|
|
|
|
//! | |
|
|
|
|
|
//! | o--(exec)-->o
|
|
|
|
|
//! | | |
|
|
|
|
|
//! |o<-(EscalateReq)--o
|
|
|
|
|
//! || | |
|
|
|
|
|
//! |o-(Run)---------->o
|
|
|
|
|
//! | | |
|
|
|
|
|
//! | | x--(exec)-->o
|
|
|
|
|
//! | | |
|
|
|
|
|
//! | o<--------------(exit)--x
|
|
|
|
|
//! | |
|
|
|
|
|
//! o<-----x
|
|
|
|
|
//!
|
|
|
|
|
use std::path::Path;
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
use std::path::PathBuf;
|
2025-12-04 21:55:54 -08:00
|
|
|
use std::sync::Arc;
|
2025-11-18 16:20:19 -08:00
|
|
|
|
2025-12-04 21:55:54 -08:00
|
|
|
use anyhow::Context as _;
|
2025-11-18 16:20:19 -08:00
|
|
|
use clap::Parser;
|
2025-12-04 21:55:54 -08:00
|
|
|
use codex_core::config::find_codex_home;
|
|
|
|
|
use codex_core::is_dangerous_command::command_might_be_dangerous;
|
2025-12-10 09:18:48 -08:00
|
|
|
use codex_core::sandboxing::SandboxPermissions;
|
2025-12-04 21:55:54 -08:00
|
|
|
use codex_execpolicy::Decision;
|
|
|
|
|
use codex_execpolicy::Policy;
|
|
|
|
|
use codex_execpolicy::RuleMatch;
|
|
|
|
|
use rmcp::ErrorData as McpError;
|
|
|
|
|
use tokio::sync::RwLock;
|
2025-11-18 16:20:19 -08:00
|
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
|
use tracing_subscriber::{self};
|
|
|
|
|
|
fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the
migration to execpolicy2:
- introduces a new trait, `EscalationPolicy`, whose `determine_action()`
method is responsible for producing the `EscalateAction`
- the existing `ExecPolicy` typedef is changed to return an intermediate
`ExecPolicyOutcome` instead of `EscalateAction`
- the default implementation of `EscalationPolicy`,
`McpEscalationPolicy`, composes `ExecPolicy`
- the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which
has a `Prompt` variant
- when `McpEscalationPolicy` gets `Decision::Prompt` back from
`ExecPolicy`, it prompts the user via an MCP elicitation and maps the
result into an `ElicitationAction`
- now that the end user can reply to an elicitation with `Decline` or
`Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the
client handles by returning exit code `1` without running anything
Note the way the elicitation is created is still not quite right, but I
will fix that once we have things running end-to-end for real in a
follow-up PR.
2025-11-19 13:55:29 -08:00
|
|
|
use crate::posix::mcp_escalation_policy::ExecPolicyOutcome;
|
2025-11-18 16:20:19 -08:00
|
|
|
|
|
|
|
|
mod escalate_client;
|
|
|
|
|
mod escalate_protocol;
|
|
|
|
|
mod escalate_server;
|
fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the
migration to execpolicy2:
- introduces a new trait, `EscalationPolicy`, whose `determine_action()`
method is responsible for producing the `EscalateAction`
- the existing `ExecPolicy` typedef is changed to return an intermediate
`ExecPolicyOutcome` instead of `EscalateAction`
- the default implementation of `EscalationPolicy`,
`McpEscalationPolicy`, composes `ExecPolicy`
- the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which
has a `Prompt` variant
- when `McpEscalationPolicy` gets `Decision::Prompt` back from
`ExecPolicy`, it prompts the user via an MCP elicitation and maps the
result into an `ElicitationAction`
- now that the end user can reply to an elicitation with `Decline` or
`Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the
client handles by returning exit code `1` without running anything
Note the way the elicitation is created is still not quite right, but I
will fix that once we have things running end-to-end for real in a
follow-up PR.
2025-11-19 13:55:29 -08:00
|
|
|
mod escalation_policy;
|
2025-11-18 16:20:19 -08:00
|
|
|
mod mcp;
|
fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the
migration to execpolicy2:
- introduces a new trait, `EscalationPolicy`, whose `determine_action()`
method is responsible for producing the `EscalateAction`
- the existing `ExecPolicy` typedef is changed to return an intermediate
`ExecPolicyOutcome` instead of `EscalateAction`
- the default implementation of `EscalationPolicy`,
`McpEscalationPolicy`, composes `ExecPolicy`
- the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which
has a `Prompt` variant
- when `McpEscalationPolicy` gets `Decision::Prompt` back from
`ExecPolicy`, it prompts the user via an MCP elicitation and maps the
result into an `ElicitationAction`
- now that the end user can reply to an elicitation with `Decline` or
`Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the
client handles by returning exit code `1` without running anything
Note the way the elicitation is created is still not quite right, but I
will fix that once we have things running end-to-end for real in a
follow-up PR.
2025-11-19 13:55:29 -08:00
|
|
|
mod mcp_escalation_policy;
|
2025-11-18 16:20:19 -08:00
|
|
|
mod socket;
|
2025-11-20 16:45:38 -08:00
|
|
|
mod stopwatch;
|
2025-11-18 16:20:19 -08:00
|
|
|
|
2025-12-06 22:39:38 -08:00
|
|
|
pub use mcp::ExecResult;
|
|
|
|
|
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
/// Default value of --execve option relative to the current executable.
|
|
|
|
|
/// Note this must match the name of the binary as specified in Cargo.toml.
|
|
|
|
|
const CODEX_EXECVE_WRAPPER_EXE_NAME: &str = "codex-execve-wrapper";
|
|
|
|
|
|
2025-11-18 16:20:19 -08:00
|
|
|
#[derive(Parser)]
|
2025-12-02 15:43:25 -08:00
|
|
|
#[clap(version)]
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
struct McpServerCli {
|
|
|
|
|
/// Executable to delegate execve(2) calls to in Bash.
|
|
|
|
|
#[arg(long = "execve")]
|
|
|
|
|
execve_wrapper: Option<PathBuf>,
|
|
|
|
|
|
|
|
|
|
/// Path to Bash that has been patched to support execve() wrapping.
|
|
|
|
|
#[arg(long = "bash")]
|
|
|
|
|
bash_path: Option<PathBuf>,
|
2025-12-04 21:55:54 -08:00
|
|
|
|
|
|
|
|
/// Preserve program paths when applying execpolicy (e.g., keep /usr/bin/echo instead of echo).
|
|
|
|
|
/// Note: this does change the actual program being run.
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
preserve_program_paths: bool,
|
2025-11-18 16:20:19 -08:00
|
|
|
}
|
|
|
|
|
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
#[tokio::main]
|
|
|
|
|
pub async fn main_mcp_server() -> anyhow::Result<()> {
|
|
|
|
|
tracing_subscriber::fmt()
|
|
|
|
|
.with_env_filter(EnvFilter::from_default_env())
|
|
|
|
|
.with_writer(std::io::stderr)
|
|
|
|
|
.with_ansi(false)
|
|
|
|
|
.init();
|
|
|
|
|
|
|
|
|
|
let cli = McpServerCli::parse();
|
|
|
|
|
let execve_wrapper = match cli.execve_wrapper {
|
|
|
|
|
Some(path) => path,
|
|
|
|
|
None => {
|
|
|
|
|
let cwd = std::env::current_exe()?;
|
|
|
|
|
cwd.parent()
|
|
|
|
|
.map(|p| p.join(CODEX_EXECVE_WRAPPER_EXE_NAME))
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
anyhow::anyhow!("failed to determine execve wrapper path from current exe")
|
|
|
|
|
})?
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let bash_path = match cli.bash_path {
|
|
|
|
|
Some(path) => path,
|
|
|
|
|
None => mcp::get_bash_path()?,
|
|
|
|
|
};
|
2025-12-04 21:55:54 -08:00
|
|
|
let policy = Arc::new(RwLock::new(load_exec_policy().await?));
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
|
|
|
|
|
tracing::info!("Starting MCP server");
|
2025-12-04 21:55:54 -08:00
|
|
|
let service = mcp::serve(
|
|
|
|
|
bash_path,
|
|
|
|
|
execve_wrapper,
|
|
|
|
|
policy,
|
|
|
|
|
cli.preserve_program_paths,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.inspect_err(|e| {
|
|
|
|
|
tracing::error!("serving error: {:?}", e);
|
|
|
|
|
})?;
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
|
|
|
|
|
service.waiting().await?;
|
|
|
|
|
Ok(())
|
2025-11-18 16:20:19 -08:00
|
|
|
}
|
|
|
|
|
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
#[derive(Parser)]
|
|
|
|
|
pub struct ExecveWrapperCli {
|
2025-11-18 16:20:19 -08:00
|
|
|
file: String,
|
|
|
|
|
|
|
|
|
|
#[arg(trailing_var_arg = true)]
|
|
|
|
|
argv: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::main]
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
pub async fn main_execve_wrapper() -> anyhow::Result<()> {
|
2025-11-18 16:20:19 -08:00
|
|
|
tracing_subscriber::fmt()
|
|
|
|
|
.with_env_filter(EnvFilter::from_default_env())
|
|
|
|
|
.with_writer(std::io::stderr)
|
|
|
|
|
.with_ansi(false)
|
|
|
|
|
.init();
|
|
|
|
|
|
chore: refactor exec-server to prepare it for standalone MCP use (#6944)
This PR reorganizes things slightly so that:
- Instead of a single multitool executable, `codex-exec-server`, we now
have two executables:
- `codex-exec-mcp-server` to launch the MCP server
- `codex-execve-wrapper` is the `execve(2)` wrapper to use with the
`BASH_EXEC_WRAPPER` environment variable
- `BASH_EXEC_WRAPPER` must be a single executable: it cannot be a
command string composed of an executable with args (i.e., it no longer
adds the `escalate` subcommand, as before)
- `codex-exec-mcp-server` takes `--bash` and `--execve` as options.
Though if `--execve` is not specified, the MCP server will check the
directory containing `std::env::current_exe()` and attempt to use the
file named `codex-execve-wrapper` within it. In development, this works
out since these executables are side-by-side in the `target/debug`
folder.
With respect to testing, this also fixes an important bug in
`dummy_exec_policy()`, as I was using `ends_with()` as if it applied to
a `String`, but in this case, it is used with a `&Path`, so the
semantics are slightly different.
Putting this all together, I was able to test this by running the
following:
```
~/code/codex/codex-rs$ npx @modelcontextprotocol/inspector \
./target/debug/codex-exec-mcp-server --bash ~/code/bash/bash
```
If I try to run `git status` in `/Users/mbolin/code/codex` via the
`shell` tool from the MCP server:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/9db6aea8-7fbc-4675-8b1f-ec446685d6c4"
/>
then I get prompted with the following elicitation, as expected:
<img width="1589" height="1335" alt="image"
src="https://github.com/user-attachments/assets/21b68fe0-494d-4562-9bad-0ddc55fc846d"
/>
Though a current limitation is that the `shell` tool defaults to a
timeout of 10s, which means I only have 10s to respond to the
elicitation. Ideally, the time spent waiting for a response from a human
should not count against the timeout for the command execution. I will
address this in a subsequent PR.
---
Note `~/code/bash/bash` was created by doing:
```
cd ~/code
git clone https://github.com/bminor/bash
cd bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
<apply the patch below>
./configure
make
```
The patch:
```
diff --git a/execute_cmd.c b/execute_cmd.c
index 070f5119..d20ad2b9 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
char sample[HASH_BANG_BUFSIZ];
size_t larray;
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
+ {
+ char *orig_command = command;
+
+ larray = strvec_len (args);
+
+ memmove (args + 2, args, (++larray) * sizeof (char *));
+ args[0] = exec_wrapper;
+ args[1] = orig_command;
+ command = exec_wrapper;
+ }
+
```
2025-11-19 16:38:14 -08:00
|
|
|
let ExecveWrapperCli { file, argv } = ExecveWrapperCli::parse();
|
|
|
|
|
let exit_code = escalate_client::run(file, argv).await?;
|
|
|
|
|
std::process::exit(exit_code);
|
2025-11-18 16:20:19 -08:00
|
|
|
}
|
fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the
migration to execpolicy2:
- introduces a new trait, `EscalationPolicy`, whose `determine_action()`
method is responsible for producing the `EscalateAction`
- the existing `ExecPolicy` typedef is changed to return an intermediate
`ExecPolicyOutcome` instead of `EscalateAction`
- the default implementation of `EscalationPolicy`,
`McpEscalationPolicy`, composes `ExecPolicy`
- the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which
has a `Prompt` variant
- when `McpEscalationPolicy` gets `Decision::Prompt` back from
`ExecPolicy`, it prompts the user via an MCP elicitation and maps the
result into an `ElicitationAction`
- now that the end user can reply to an elicitation with `Decline` or
`Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the
client handles by returning exit code `1` without running anything
Note the way the elicitation is created is still not quite right, but I
will fix that once we have things running end-to-end for real in a
follow-up PR.
2025-11-19 13:55:29 -08:00
|
|
|
|
2025-12-04 21:55:54 -08:00
|
|
|
/// Decide how to handle an exec() call for a specific command.
|
|
|
|
|
///
|
|
|
|
|
/// `file` is the absolute, canonical path to the executable to run, i.e. the first arg to exec.
|
|
|
|
|
/// `argv` is the argv, including the program name (`argv[0]`).
|
|
|
|
|
pub(crate) fn evaluate_exec_policy(
|
|
|
|
|
policy: &Policy,
|
|
|
|
|
file: &Path,
|
|
|
|
|
argv: &[String],
|
|
|
|
|
preserve_program_paths: bool,
|
|
|
|
|
) -> Result<ExecPolicyOutcome, McpError> {
|
|
|
|
|
let program_name = format_program_name(file, preserve_program_paths).ok_or_else(|| {
|
|
|
|
|
McpError::internal_error(
|
|
|
|
|
format!("failed to format program name for `{}`", file.display()),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
let command: Vec<String> = std::iter::once(program_name)
|
|
|
|
|
// Use the normalized program name instead of argv[0].
|
|
|
|
|
.chain(argv.iter().skip(1).cloned())
|
|
|
|
|
.collect();
|
|
|
|
|
let evaluation = policy.check(&command, &|cmd| {
|
|
|
|
|
if command_might_be_dangerous(cmd) {
|
|
|
|
|
Decision::Prompt
|
|
|
|
|
} else {
|
|
|
|
|
Decision::Allow
|
fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the
migration to execpolicy2:
- introduces a new trait, `EscalationPolicy`, whose `determine_action()`
method is responsible for producing the `EscalateAction`
- the existing `ExecPolicy` typedef is changed to return an intermediate
`ExecPolicyOutcome` instead of `EscalateAction`
- the default implementation of `EscalationPolicy`,
`McpEscalationPolicy`, composes `ExecPolicy`
- the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which
has a `Prompt` variant
- when `McpEscalationPolicy` gets `Decision::Prompt` back from
`ExecPolicy`, it prompts the user via an MCP elicitation and maps the
result into an `ElicitationAction`
- now that the end user can reply to an elicitation with `Decline` or
`Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the
client handles by returning exit code `1` without running anything
Note the way the elicitation is created is still not quite right, but I
will fix that once we have things running end-to-end for real in a
follow-up PR.
2025-11-19 13:55:29 -08:00
|
|
|
}
|
2025-12-04 21:55:54 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// decisions driven by policy should run outside sandbox
|
|
|
|
|
let decision_driven_by_policy = evaluation.matched_rules.iter().any(|rule_match| {
|
|
|
|
|
!matches!(rule_match, RuleMatch::HeuristicsRuleMatch { .. })
|
|
|
|
|
&& rule_match.decision() == evaluation.decision
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-10 09:18:48 -08:00
|
|
|
let sandbox_permissions = if decision_driven_by_policy {
|
|
|
|
|
SandboxPermissions::RequireEscalated
|
|
|
|
|
} else {
|
|
|
|
|
SandboxPermissions::UseDefault
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-04 21:55:54 -08:00
|
|
|
Ok(match evaluation.decision {
|
|
|
|
|
Decision::Forbidden => ExecPolicyOutcome::Forbidden,
|
|
|
|
|
Decision::Prompt => ExecPolicyOutcome::Prompt {
|
2025-12-10 09:18:48 -08:00
|
|
|
sandbox_permissions,
|
2025-12-04 21:55:54 -08:00
|
|
|
},
|
|
|
|
|
Decision::Allow => ExecPolicyOutcome::Allow {
|
2025-12-10 09:18:48 -08:00
|
|
|
sandbox_permissions,
|
2025-12-04 21:55:54 -08:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn format_program_name(path: &Path, preserve_program_paths: bool) -> Option<String> {
|
|
|
|
|
if preserve_program_paths {
|
|
|
|
|
path.to_str().map(str::to_string)
|
fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the
migration to execpolicy2:
- introduces a new trait, `EscalationPolicy`, whose `determine_action()`
method is responsible for producing the `EscalateAction`
- the existing `ExecPolicy` typedef is changed to return an intermediate
`ExecPolicyOutcome` instead of `EscalateAction`
- the default implementation of `EscalationPolicy`,
`McpEscalationPolicy`, composes `ExecPolicy`
- the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which
has a `Prompt` variant
- when `McpEscalationPolicy` gets `Decision::Prompt` back from
`ExecPolicy`, it prompts the user via an MCP elicitation and maps the
result into an `ElicitationAction`
- now that the end user can reply to an elicitation with `Decline` or
`Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the
client handles by returning exit code `1` without running anything
Note the way the elicitation is created is still not quite right, but I
will fix that once we have things running end-to-end for real in a
follow-up PR.
2025-11-19 13:55:29 -08:00
|
|
|
} else {
|
2025-12-04 21:55:54 -08:00
|
|
|
path.file_name()?.to_str().map(str::to_string)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn load_exec_policy() -> anyhow::Result<Policy> {
|
|
|
|
|
let codex_home = find_codex_home().context("failed to resolve codex_home for execpolicy")?;
|
2025-12-22 17:24:17 -08:00
|
|
|
|
|
|
|
|
// TODO(mbolin): At a minimum, `cwd` should be configurable via
|
|
|
|
|
// `codex/sandbox-state/update` or some other custom MCP call.
|
|
|
|
|
let cwd = None;
|
|
|
|
|
let cli_overrides = Vec::new();
|
|
|
|
|
let overrides = codex_core::config_loader::LoaderOverrides::default();
|
|
|
|
|
let config_layer_stack = codex_core::config_loader::load_config_layers_state(
|
|
|
|
|
&codex_home,
|
|
|
|
|
cwd,
|
|
|
|
|
&cli_overrides,
|
|
|
|
|
overrides,
|
2026-01-30 23:53:41 +00:00
|
|
|
codex_core::config_loader::CloudRequirementsLoader::default(),
|
2025-12-22 17:24:17 -08:00
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
codex_core::load_exec_policy(&config_layer_stack)
|
2025-12-04 21:55:54 -08:00
|
|
|
.await
|
|
|
|
|
.map_err(anyhow::Error::from)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2025-12-10 09:18:48 -08:00
|
|
|
use codex_core::sandboxing::SandboxPermissions;
|
2025-12-04 21:55:54 -08:00
|
|
|
use codex_execpolicy::Decision;
|
|
|
|
|
use codex_execpolicy::Policy;
|
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn evaluate_exec_policy_uses_heuristics_for_dangerous_commands() {
|
|
|
|
|
let policy = Policy::empty();
|
|
|
|
|
let file = Path::new("/bin/rm");
|
|
|
|
|
let argv = vec!["rm".to_string(), "-rf".to_string(), "/".to_string()];
|
|
|
|
|
|
|
|
|
|
let outcome = evaluate_exec_policy(&policy, file, &argv, false).expect("policy evaluation");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
outcome,
|
|
|
|
|
ExecPolicyOutcome::Prompt {
|
2025-12-10 09:18:48 -08:00
|
|
|
sandbox_permissions: SandboxPermissions::UseDefault
|
2025-12-04 21:55:54 -08:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn evaluate_exec_policy_respects_preserve_program_paths() {
|
|
|
|
|
let mut policy = Policy::empty();
|
|
|
|
|
policy
|
|
|
|
|
.add_prefix_rule(
|
|
|
|
|
&[
|
|
|
|
|
"/usr/local/bin/custom-cmd".to_string(),
|
|
|
|
|
"--flag".to_string(),
|
|
|
|
|
],
|
|
|
|
|
Decision::Allow,
|
|
|
|
|
)
|
|
|
|
|
.expect("policy rule should be added");
|
|
|
|
|
let file = Path::new("/usr/local/bin/custom-cmd");
|
|
|
|
|
let argv = vec![
|
|
|
|
|
"/usr/local/bin/custom-cmd".to_string(),
|
|
|
|
|
"--flag".to_string(),
|
|
|
|
|
"value".to_string(),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let outcome = evaluate_exec_policy(&policy, file, &argv, true).expect("policy evaluation");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
outcome,
|
|
|
|
|
ExecPolicyOutcome::Allow {
|
2025-12-10 09:18:48 -08:00
|
|
|
sandbox_permissions: SandboxPermissions::RequireEscalated
|
2025-12-04 21:55:54 -08:00
|
|
|
}
|
|
|
|
|
);
|
fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the
migration to execpolicy2:
- introduces a new trait, `EscalationPolicy`, whose `determine_action()`
method is responsible for producing the `EscalateAction`
- the existing `ExecPolicy` typedef is changed to return an intermediate
`ExecPolicyOutcome` instead of `EscalateAction`
- the default implementation of `EscalationPolicy`,
`McpEscalationPolicy`, composes `ExecPolicy`
- the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which
has a `Prompt` variant
- when `McpEscalationPolicy` gets `Decision::Prompt` back from
`ExecPolicy`, it prompts the user via an MCP elicitation and maps the
result into an `ElicitationAction`
- now that the end user can reply to an elicitation with `Decline` or
`Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the
client handles by returning exit code `1` without running anything
Note the way the elicitation is created is still not quite right, but I
will fix that once we have things running end-to-end for real in a
follow-up PR.
2025-11-19 13:55:29 -08:00
|
|
|
}
|
|
|
|
|
}
|