mirror of
https://github.com/openai/codex.git
synced 2026-04-26 15:45:02 +00:00
refactor: delete exec-server and move execve wrapper into shell-escalation (#12632)
## Why We already plan to remove the shell-tool MCP path, and doing that cleanup first makes the follow-on `shell-escalation` work much simpler. This change removes the last remaining reason to keep `codex-rs/exec-server` around by moving the `codex-execve-wrapper` binary and shared shell test fixtures to the crates/tests that now own that functionality. ## What Changed ### Delete `codex-rs/exec-server` - Remove the `exec-server` crate, including the MCP server binary, MCP-specific modules, and its test support/test suite - Remove `exec-server` from the `codex-rs` workspace and update `Cargo.lock` ### Move `codex-execve-wrapper` into `codex-rs/shell-escalation` - Move the wrapper implementation into `shell-escalation` (`src/unix/execve_wrapper.rs`) - Add the `codex-execve-wrapper` binary entrypoint under `shell-escalation/src/bin/` - Update `shell-escalation` exports/module layout so the wrapper entrypoint is hosted there - Move the wrapper README content from `exec-server` to `shell-escalation/README.md` ### Move shared shell test fixtures to `app-server` - Move the DotSlash `bash`/`zsh` test fixtures from `exec-server/tests/suite/` to `app-server/tests/suite/` - Update `app-server` zsh-fork tests to reference the new fixture paths ### Keep `shell-tool-mcp` as a shell-assets package - Update `.github/workflows/shell-tool-mcp.yml` packaging so the npm artifact contains only patched Bash/Zsh payloads (no Rust binaries) - Update `shell-tool-mcp/package.json`, `shell-tool-mcp/src/index.ts`, and docs to reflect the shell-assets-only package shape - `shell-tool-mcp-ci.yml` does not need changes because it is already JS-only ## Verification - `cargo shear` - `cargo clippy -p codex-shell-escalation --tests` - `just clippy`
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
// Single integration test binary that aggregates all test modules.
|
||||
// The submodules live in `tests/suite/`.
|
||||
mod suite;
|
||||
@@ -1,7 +0,0 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "common",
|
||||
crate_name = "exec_server_test_support",
|
||||
crate_srcs = glob(["*.rs"]),
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "exec_server_test_support"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
rmcp = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -1,201 +0,0 @@
|
||||
use codex_core::MCP_SANDBOX_STATE_METHOD;
|
||||
use codex_core::SandboxState;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_cargo_bin::find_resource;
|
||||
use rmcp::ClientHandler;
|
||||
use rmcp::ErrorData as McpError;
|
||||
use rmcp::RoleClient;
|
||||
use rmcp::Service;
|
||||
use rmcp::model::ClientCapabilities;
|
||||
use rmcp::model::ClientInfo;
|
||||
use rmcp::model::ClientRequest;
|
||||
use rmcp::model::CreateElicitationRequestParams;
|
||||
use rmcp::model::CreateElicitationResult;
|
||||
use rmcp::model::CustomRequest;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::ServerResult;
|
||||
use rmcp::service::RunningService;
|
||||
use rmcp::transport::ConfigureCommandExt;
|
||||
use rmcp::transport::TokioChildProcess;
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tokio::process::Command;
|
||||
|
||||
pub async fn create_transport<P>(
|
||||
codex_home: P,
|
||||
dotslash_cache: P,
|
||||
) -> anyhow::Result<TokioChildProcess>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
// `bash` is a test resource rather than a binary target, so we must use
|
||||
// `find_resource!` to locate it instead of `cargo_bin()`.
|
||||
let bash = find_resource!("../suite/bash")?;
|
||||
|
||||
// Need to ensure the artifact associated with the bash DotSlash file is
|
||||
// available before it is run in a read-only sandbox.
|
||||
let status = Command::new("dotslash")
|
||||
.arg("--")
|
||||
.arg("fetch")
|
||||
.arg(bash.clone())
|
||||
.env("DOTSLASH_CACHE", dotslash_cache.as_ref())
|
||||
.status()
|
||||
.await?;
|
||||
assert!(status.success(), "dotslash fetch failed: {status:?}");
|
||||
|
||||
create_transport_with_shell_path(codex_home, dotslash_cache, bash).await
|
||||
}
|
||||
|
||||
pub async fn create_transport_with_shell_path<P, Q, R>(
|
||||
codex_home: P,
|
||||
dotslash_cache: Q,
|
||||
shell_path: R,
|
||||
) -> anyhow::Result<TokioChildProcess>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
R: AsRef<Path>,
|
||||
{
|
||||
let mcp_executable = codex_utils_cargo_bin::cargo_bin("codex-exec-mcp-server")?;
|
||||
let execve_wrapper = codex_utils_cargo_bin::cargo_bin("codex-execve-wrapper")?;
|
||||
|
||||
let transport = TokioChildProcess::new(Command::new(&mcp_executable).configure(|cmd| {
|
||||
cmd.arg("--bash").arg(shell_path.as_ref());
|
||||
cmd.arg("--execve").arg(&execve_wrapper);
|
||||
cmd.env("CODEX_HOME", codex_home.as_ref());
|
||||
cmd.env("DOTSLASH_CACHE", dotslash_cache.as_ref());
|
||||
|
||||
// Important: pipe stdio so rmcp can speak JSON-RPC over stdin/stdout
|
||||
cmd.stdin(Stdio::piped());
|
||||
cmd.stdout(Stdio::piped());
|
||||
|
||||
// Optional but very helpful while debugging:
|
||||
cmd.stderr(Stdio::inherit());
|
||||
}))?;
|
||||
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
pub async fn write_default_execpolicy<P>(policy: &str, codex_home: P) -> anyhow::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let policy_dir = codex_home.as_ref().join("rules");
|
||||
tokio::fs::create_dir_all(&policy_dir).await?;
|
||||
tokio::fs::write(policy_dir.join("default.rules"), policy).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn notify_readable_sandbox<P, S>(
|
||||
sandbox_cwd: P,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
service: &RunningService<RoleClient, S>,
|
||||
) -> anyhow::Result<ServerResult>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
S: Service<RoleClient> + ClientHandler,
|
||||
{
|
||||
let sandbox_state = SandboxState {
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
codex_linux_sandbox_exe,
|
||||
sandbox_cwd: sandbox_cwd.as_ref().to_path_buf(),
|
||||
use_linux_sandbox_bwrap: false,
|
||||
};
|
||||
send_sandbox_state_update(sandbox_state, service).await
|
||||
}
|
||||
|
||||
pub async fn notify_writable_sandbox_only_one_folder<P, S>(
|
||||
writable_folder: P,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
service: &RunningService<RoleClient, S>,
|
||||
) -> anyhow::Result<ServerResult>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
S: Service<RoleClient> + ClientHandler,
|
||||
{
|
||||
let sandbox_state = SandboxState {
|
||||
sandbox_policy: SandboxPolicy::WorkspaceWrite {
|
||||
// Note that sandbox_cwd will already be included as a writable root
|
||||
// when the sandbox policy is expanded.
|
||||
writable_roots: vec![],
|
||||
read_only_access: Default::default(),
|
||||
network_access: false,
|
||||
// Disable writes to temp dir because this is a test, so
|
||||
// writable_folder is likely also under /tmp and we want to be
|
||||
// strict about what is writable.
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
codex_linux_sandbox_exe,
|
||||
sandbox_cwd: writable_folder.as_ref().to_path_buf(),
|
||||
use_linux_sandbox_bwrap: false,
|
||||
};
|
||||
send_sandbox_state_update(sandbox_state, service).await
|
||||
}
|
||||
|
||||
async fn send_sandbox_state_update<S>(
|
||||
sandbox_state: SandboxState,
|
||||
service: &RunningService<RoleClient, S>,
|
||||
) -> anyhow::Result<ServerResult>
|
||||
where
|
||||
S: Service<RoleClient> + ClientHandler,
|
||||
{
|
||||
let response = service
|
||||
.send_request(ClientRequest::CustomRequest(CustomRequest::new(
|
||||
MCP_SANDBOX_STATE_METHOD,
|
||||
Some(serde_json::to_value(sandbox_state)?),
|
||||
)))
|
||||
.await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub struct InteractiveClient {
|
||||
pub elicitations_to_accept: HashSet<String>,
|
||||
pub elicitation_requests: Arc<Mutex<Vec<CreateElicitationRequestParams>>>,
|
||||
}
|
||||
|
||||
impl ClientHandler for InteractiveClient {
|
||||
fn get_info(&self) -> ClientInfo {
|
||||
let capabilities = ClientCapabilities::builder().enable_elicitation().build();
|
||||
ClientInfo {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_elicitation(
|
||||
&self,
|
||||
request: CreateElicitationRequestParams,
|
||||
_context: rmcp::service::RequestContext<RoleClient>,
|
||||
) -> impl std::future::Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_
|
||||
{
|
||||
self.elicitation_requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(request.clone());
|
||||
|
||||
let message = match &request {
|
||||
CreateElicitationRequestParams::FormElicitationParams { message, .. }
|
||||
| CreateElicitationRequestParams::UrlElicitationParams { message, .. } => message,
|
||||
};
|
||||
let accept = self.elicitations_to_accept.contains(message);
|
||||
async move {
|
||||
if accept {
|
||||
Ok(CreateElicitationResult {
|
||||
action: ElicitationAction::Accept,
|
||||
content: Some(json!({ "approve": true })),
|
||||
})
|
||||
} else {
|
||||
Ok(CreateElicitationResult {
|
||||
action: ElicitationAction::Decline,
|
||||
content: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
use codex_exec_server::ExecResult;
|
||||
use exec_server_test_support::InteractiveClient;
|
||||
use exec_server_test_support::create_transport;
|
||||
use exec_server_test_support::create_transport_with_shell_path;
|
||||
use exec_server_test_support::notify_readable_sandbox;
|
||||
use exec_server_test_support::write_default_execpolicy;
|
||||
use maplit::hashset;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::ServiceExt;
|
||||
use rmcp::model::CallToolRequestParams;
|
||||
use rmcp::model::CallToolResult;
|
||||
use rmcp::model::CreateElicitationRequestParams;
|
||||
use rmcp::model::EmptyResult;
|
||||
use rmcp::model::ServerResult;
|
||||
use rmcp::model::object;
|
||||
use serde_json::json;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::fs::symlink;
|
||||
use tempfile::TempDir;
|
||||
use tokio::process::Command;
|
||||
|
||||
const USE_LOGIN_SHELL: bool = false;
|
||||
|
||||
/// Verify that when using a read-only sandbox and an execpolicy that prompts,
|
||||
/// the proper elicitation is sent. Upon auto-approving the elicitation, the
|
||||
/// command should be run privileged outside the sandbox.
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn accept_elicitation_for_prompt_rule() -> Result<()> {
|
||||
// Configure a stdio transport that will launch the MCP server using
|
||||
// $CODEX_HOME with an execpolicy that prompts for `git init` commands.
|
||||
let codex_home = TempDir::new()?;
|
||||
write_default_execpolicy(
|
||||
r#"
|
||||
# Create a rule with `decision = "prompt"` to exercise the elicitation flow.
|
||||
prefix_rule(
|
||||
pattern = ["git", "init"],
|
||||
decision = "prompt",
|
||||
match = [
|
||||
"git init ."
|
||||
],
|
||||
)
|
||||
"#,
|
||||
codex_home.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
let dotslash_cache_temp_dir = TempDir::new()?;
|
||||
let dotslash_cache = dotslash_cache_temp_dir.path();
|
||||
let transport = create_transport(codex_home.as_ref(), dotslash_cache).await?;
|
||||
run_accept_elicitation_for_prompt_rule_with_transport(transport).await
|
||||
}
|
||||
|
||||
/// Verify the same prompt/escalation flow works when the server is launched
|
||||
/// with a patched zsh binary.
|
||||
///
|
||||
/// The suite resolves `tests/suite/zsh` via DotSlash on first use.
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn accept_elicitation_for_prompt_rule_with_zsh() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_default_execpolicy(
|
||||
r#"
|
||||
# Create a rule with `decision = "prompt"` to exercise the elicitation flow.
|
||||
prefix_rule(
|
||||
pattern = ["git", "init"],
|
||||
decision = "prompt",
|
||||
match = [
|
||||
"git init ."
|
||||
],
|
||||
)
|
||||
"#,
|
||||
codex_home.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
let dotslash_cache_temp_dir = TempDir::new()?;
|
||||
let dotslash_cache = dotslash_cache_temp_dir.path();
|
||||
let zsh_path = resolve_test_zsh_path(dotslash_cache).await?;
|
||||
eprintln!(
|
||||
"using zsh path for exec-server test: {}",
|
||||
zsh_path.display()
|
||||
);
|
||||
let transport =
|
||||
create_transport_with_shell_path(codex_home.as_ref(), dotslash_cache, &zsh_path).await?;
|
||||
run_accept_elicitation_for_prompt_rule_with_transport(transport).await
|
||||
}
|
||||
|
||||
async fn run_accept_elicitation_for_prompt_rule_with_transport(
|
||||
transport: rmcp::transport::TokioChildProcess,
|
||||
) -> Result<()> {
|
||||
// Create an MCP client that approves the expected elicitation message.
|
||||
let project_root = TempDir::new()?;
|
||||
let project_root_path = project_root.path().canonicalize().unwrap();
|
||||
let git_path = resolve_git_path(USE_LOGIN_SHELL).await?;
|
||||
let git_init_command = format!("{git_path} init --quiet .");
|
||||
let expected_elicitation_message = format!(
|
||||
"Allow agent to run `{git_path} init --quiet .` in `{}`?",
|
||||
project_root_path.display()
|
||||
);
|
||||
let elicitation_requests: Arc<Mutex<Vec<CreateElicitationRequestParams>>> = Default::default();
|
||||
let client = InteractiveClient {
|
||||
elicitations_to_accept: hashset! { expected_elicitation_message.clone() },
|
||||
elicitation_requests: elicitation_requests.clone(),
|
||||
};
|
||||
|
||||
// Start the MCP server.
|
||||
let service: rmcp::service::RunningService<rmcp::RoleClient, InteractiveClient> =
|
||||
client.serve(transport).await?;
|
||||
|
||||
// Notify the MCP server about the current sandbox state before making any
|
||||
// `shell` tool calls.
|
||||
let linux_sandbox_exe_folder = TempDir::new()?;
|
||||
let codex_linux_sandbox_exe = if cfg!(target_os = "linux") {
|
||||
let codex_linux_sandbox_exe = linux_sandbox_exe_folder.path().join("codex-linux-sandbox");
|
||||
let codex_cli = ensure_codex_cli()?;
|
||||
symlink(&codex_cli, &codex_linux_sandbox_exe)?;
|
||||
Some(codex_linux_sandbox_exe)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let response =
|
||||
notify_readable_sandbox(&project_root_path, codex_linux_sandbox_exe, &service).await?;
|
||||
let ServerResult::EmptyResult(EmptyResult {}) = response else {
|
||||
panic!("expected EmptyResult from sandbox state notification but found: {response:?}");
|
||||
};
|
||||
|
||||
// Call the shell tool and verify that an elicitation was created and
|
||||
// auto-approved.
|
||||
let CallToolResult {
|
||||
content, is_error, ..
|
||||
} = service
|
||||
.call_tool(CallToolRequestParams {
|
||||
meta: None,
|
||||
name: Cow::Borrowed("shell"),
|
||||
arguments: Some(object(json!(
|
||||
{
|
||||
"login": USE_LOGIN_SHELL,
|
||||
"command": git_init_command,
|
||||
"workdir": project_root_path.to_string_lossy(),
|
||||
}
|
||||
))),
|
||||
task: None,
|
||||
})
|
||||
.await?;
|
||||
let tool_call_content = content
|
||||
.first()
|
||||
.expect("expected non-empty content")
|
||||
.as_text()
|
||||
.expect("expected text content");
|
||||
let ExecResult {
|
||||
exit_code, output, ..
|
||||
} = serde_json::from_str::<ExecResult>(&tool_call_content.text)?;
|
||||
// `git init --quiet` is expected to suppress the usual initialization
|
||||
// banner, so assert on success and filesystem effects instead of output.
|
||||
assert!(
|
||||
output.is_empty(),
|
||||
"expected no output from `git init --quiet .`, got `{output}`"
|
||||
);
|
||||
assert_eq!(exit_code, 0, "command should succeed");
|
||||
assert_eq!(is_error, Some(false), "command should succeed");
|
||||
assert!(
|
||||
project_root_path.join(".git").is_dir(),
|
||||
"git repo should exist"
|
||||
);
|
||||
|
||||
let elicitation_messages = elicitation_requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| match r {
|
||||
rmcp::model::CreateElicitationRequestParams::FormElicitationParams {
|
||||
message, ..
|
||||
}
|
||||
| rmcp::model::CreateElicitationRequestParams::UrlElicitationParams {
|
||||
message, ..
|
||||
} => message.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(vec![expected_elicitation_message], elicitation_messages);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resolve_test_zsh_path(dotslash_cache: &std::path::Path) -> Result<PathBuf> {
|
||||
let dotslash_zsh = codex_utils_cargo_bin::find_resource!("tests/suite/zsh")?;
|
||||
core_test_support::fetch_dotslash_file(&dotslash_zsh, Some(dotslash_cache))
|
||||
.with_context(|| format!("failed to fetch test zsh from {}", dotslash_zsh.display()))
|
||||
}
|
||||
|
||||
fn ensure_codex_cli() -> Result<PathBuf> {
|
||||
let codex_cli = codex_utils_cargo_bin::cargo_bin("codex")?;
|
||||
|
||||
let metadata = codex_cli.metadata().with_context(|| {
|
||||
format!(
|
||||
"failed to read metadata for codex binary at {}",
|
||||
codex_cli.display()
|
||||
)
|
||||
})?;
|
||||
ensure!(
|
||||
metadata.is_file(),
|
||||
"expected codex binary at {} to be a file; run `cargo build -p codex-cli --bin codex` before this test",
|
||||
codex_cli.display()
|
||||
);
|
||||
|
||||
let mode = metadata.permissions().mode();
|
||||
ensure!(
|
||||
mode & 0o111 != 0,
|
||||
"codex binary at {} is not executable (mode {mode:o}); run `cargo build -p codex-cli --bin codex` before this test",
|
||||
codex_cli.display()
|
||||
);
|
||||
|
||||
Ok(codex_cli)
|
||||
}
|
||||
|
||||
async fn resolve_git_path(use_login_shell: bool) -> Result<String> {
|
||||
let bash_flag = if use_login_shell { "-lc" } else { "-c" };
|
||||
let git = Command::new("bash")
|
||||
.arg(bash_flag)
|
||||
.arg("command -v git")
|
||||
.output()
|
||||
.await
|
||||
.context("failed to resolve git via login shell")?;
|
||||
ensure!(
|
||||
git.status.success(),
|
||||
"failed to resolve git via login shell: {}",
|
||||
String::from_utf8_lossy(&git.stderr)
|
||||
);
|
||||
let git_path = String::from_utf8(git.stdout)
|
||||
.context("git path was not valid utf8")?
|
||||
.trim()
|
||||
.to_string();
|
||||
ensure!(!git_path.is_empty(), "git path should not be empty");
|
||||
Ok(git_path)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env dotslash
|
||||
|
||||
// This is an instance of the fork of Bash that we bundle with
|
||||
// https://www.npmjs.com/package/@openai/codex-shell-tool-mcp.
|
||||
// Fetching the prebuilt version via DotSlash makes it easier to write
|
||||
// integration tests for the MCP server.
|
||||
//
|
||||
// TODO(mbolin): Currently, we use a .tgz artifact that includes binaries for
|
||||
// multiple platforms, but we could save a bit of space by making arch-specific
|
||||
// artifacts available in the GitHub releases and referencing those here.
|
||||
{
|
||||
"name": "codex-bash",
|
||||
"platforms": {
|
||||
// macOS 13 builds (and therefore x86_64) were dropped in
|
||||
// https://github.com/openai/codex/pull/7295, so we only provide an
|
||||
// Apple Silicon build for now.
|
||||
"macos-aarch64": {
|
||||
"size": 37003612,
|
||||
"hash": "blake3",
|
||||
"digest": "d9cd5928c993b65c340507931c61c02bd6e9179933f8bf26a548482bb5fa53bb",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/aarch64-apple-darwin/bash/macos-15/bash",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.65.0/codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.65.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Note the `musl` parts of the Linux paths are misleading: the Bash
|
||||
// binaries are actually linked against `glibc`, but the
|
||||
// `codex-execve-wrapper` that invokes them is linked against `musl`.
|
||||
"linux-x86_64": {
|
||||
"size": 37003612,
|
||||
"hash": "blake3",
|
||||
"digest": "d9cd5928c993b65c340507931c61c02bd6e9179933f8bf26a548482bb5fa53bb",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/x86_64-unknown-linux-musl/bash/ubuntu-24.04/bash",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.65.0/codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.65.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"size": 37003612,
|
||||
"hash": "blake3",
|
||||
"digest": "d9cd5928c993b65c340507931c61c02bd6e9179933f8bf26a548482bb5fa53bb",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/aarch64-unknown-linux-musl/bash/ubuntu-24.04/bash",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.65.0/codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.65.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use exec_server_test_support::create_transport;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::ServiceExt;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::model::object;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Verify the list_tools call to the MCP server returns the expected response.
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn list_tools() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let policy_dir = codex_home.path().join("rules");
|
||||
fs::create_dir_all(&policy_dir)?;
|
||||
fs::write(
|
||||
policy_dir.join("default.rules"),
|
||||
r#"prefix_rule(pattern=["ls"], decision="prompt")"#,
|
||||
)?;
|
||||
let dotslash_cache_temp_dir = TempDir::new()?;
|
||||
let dotslash_cache = dotslash_cache_temp_dir.path();
|
||||
let transport = create_transport(codex_home.path(), dotslash_cache).await?;
|
||||
|
||||
let service = ().serve(transport).await?;
|
||||
let tools = service.list_tools(Default::default()).await?.tools;
|
||||
assert_eq!(
|
||||
vec![Tool {
|
||||
name: Cow::Borrowed("shell"),
|
||||
title: None,
|
||||
description: Some(Cow::Borrowed(
|
||||
"Runs a shell command and returns its output. You MUST provide the workdir as an absolute path."
|
||||
)),
|
||||
input_schema: Arc::new(object(json!({
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "The bash string to execute.",
|
||||
"type": "string",
|
||||
},
|
||||
"login": {
|
||||
"description": "Launch Bash with -lc instead of -c: defaults to true.",
|
||||
"nullable": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
"timeout_ms": {
|
||||
"description": "The timeout for the command in milliseconds.",
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
},
|
||||
"workdir": {
|
||||
"description": "The working directory to execute the command in. Must be an absolute path.",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"workdir",
|
||||
],
|
||||
"title": "ExecParams",
|
||||
"type": "object",
|
||||
}))),
|
||||
output_schema: None,
|
||||
execution: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None
|
||||
}],
|
||||
tools
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#[cfg(any(all(target_os = "macos", target_arch = "aarch64"), target_os = "linux"))]
|
||||
mod accept_elicitation;
|
||||
#[cfg(any(all(target_os = "macos", target_arch = "aarch64"), target_os = "linux"))]
|
||||
mod list_tools;
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env dotslash
|
||||
|
||||
// This is the patched zsh fork built by
|
||||
// `.github/workflows/shell-tool-mcp.yml` for the shell-tool-mcp package.
|
||||
// Fetching the prebuilt version via DotSlash makes it easier to write
|
||||
// integration tests that exercise the zsh fork behavior in exec-server tests.
|
||||
//
|
||||
// TODO(mbolin): Currently, we use a .tgz artifact that includes binaries for
|
||||
// multiple platforms, but we could save a bit of space by making arch-specific
|
||||
// artifacts available in the GitHub releases and referencing those here.
|
||||
{
|
||||
"name": "codex-zsh",
|
||||
"platforms": {
|
||||
// macOS 13 builds (and therefore x86_64) were dropped in
|
||||
// https://github.com/openai/codex/pull/7295, so we only provide an
|
||||
// Apple Silicon build for now.
|
||||
"macos-aarch64": {
|
||||
"size": 53771483,
|
||||
"hash": "blake3",
|
||||
"digest": "ff664f63f5e1fa62762c9aff0aafa66cf196faf9b157f98ec98f59c152fc7bd3",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/aarch64-apple-darwin/zsh/macos-15/zsh",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.104.0/codex-shell-tool-mcp-npm-0.104.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.104.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.104.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"size": 53771483,
|
||||
"hash": "blake3",
|
||||
"digest": "ff664f63f5e1fa62762c9aff0aafa66cf196faf9b157f98ec98f59c152fc7bd3",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/x86_64-unknown-linux-musl/zsh/ubuntu-24.04/zsh",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.104.0/codex-shell-tool-mcp-npm-0.104.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.104.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.104.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"size": 53771483,
|
||||
"hash": "blake3",
|
||||
"digest": "ff664f63f5e1fa62762c9aff0aafa66cf196faf9b157f98ec98f59c152fc7bd3",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/aarch64-unknown-linux-musl/zsh/ubuntu-24.04/zsh",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.104.0/codex-shell-tool-mcp-npm-0.104.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.104.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.104.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user