Merge branch 'main' into codex/add-multi-turn-support-to-exec-crate

This commit is contained in:
oai-ragona
2025-05-23 06:57:00 -07:00
committed by GitHub
32 changed files with 687 additions and 144 deletions

View File

@@ -100,6 +100,8 @@ jobs:
id: test
continue-on-error: true
run: cargo test --all-features --target ${{ matrix.target }}
env:
RUST_BACKTRACE: 1
# Fail the job if any of the previous steps failed.
- name: verify all steps passed

View File

@@ -16,14 +16,23 @@
*/
import { spawnSync } from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath, pathToFileURL } from "url";
// Determine whether the user explicitly wants the Rust CLI.
const wantsNative =
process.env.CODEX_RUST != null
// __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// For the @native release of the Node module, the `use-native` file is added,
// indicating we should default to the native binary. For other releases,
// setting CODEX_RUST=1 will opt-in to the native binary, if included.
const wantsNative = fs.existsSync(path.join(__dirname, "use-native")) ||
(process.env.CODEX_RUST != null
? ["1", "true", "yes"].includes(process.env.CODEX_RUST.toLowerCase())
: false;
: false);
// Try native binary if requested.
if (wantsNative) {
@@ -63,10 +72,6 @@ if (wantsNative) {
throw new Error(`Unsupported platform: ${platform} (${arch})`);
}
// __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`);
const result = spawnSync(binaryPath, process.argv.slice(2), {
stdio: "inherit",
@@ -78,10 +83,6 @@ if (wantsNative) {
// Fallback: execute the original JavaScript CLI.
// Determine this script's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Resolve the path to the compiled CLI bundle
const cliPath = path.resolve(__dirname, "../dist/cli.js");
const cliUrl = pathToFileURL(cliPath).href;

View File

@@ -65,7 +65,7 @@ mkdir -p "$BIN_DIR"
# Until we start publishing stable GitHub releases, we have to grab the binaries
# from the GitHub Action that created them. Update the URL below to point to the
# appropriate workflow run:
WORKFLOW_URL="https://github.com/openai/codex/actions/runs/15087655786"
WORKFLOW_URL="https://github.com/openai/codex/actions/runs/15192425904"
WORKFLOW_ID="${WORKFLOW_URL##*/}"
ARTIFACTS_DIR="$(mktemp -d)"

View File

@@ -122,6 +122,7 @@ jq --arg version "$VERSION" \
if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then
./scripts/install_native_deps.sh "$TMPDIR" --full-native
touch "${TMPDIR}/bin/use-native"
else
./scripts/install_native_deps.sh "$TMPDIR"
fi
@@ -130,11 +131,12 @@ popd >/dev/null
echo "Staged version $VERSION for release in $TMPDIR"
echo "Test Node:"
echo " node ${TMPDIR}/bin/codex.js --help"
if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then
echo "Test Rust:"
echo " CODEX_RUST=1 node ${TMPDIR}/bin/codex.js --help"
echo " node ${TMPDIR}/bin/codex.js --help"
else
echo "Test Node:"
echo " node ${TMPDIR}/bin/codex.js --help"
fi
# Print final hint for convenience

8
codex-rs/Cargo.lock generated
View File

@@ -526,6 +526,7 @@ dependencies = [
"futures",
"landlock",
"libc",
"maplit",
"mcp-types",
"mime_guess",
"openssl-sys",
@@ -548,6 +549,7 @@ dependencies = [
"tree-sitter",
"tree-sitter-bash",
"uuid",
"wildmatch",
"wiremock",
]
@@ -4309,6 +4311,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "wildmatch"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd"
[[package]]
name = "winapi"
version = "0.3.9"

View File

@@ -222,6 +222,49 @@ Currently, customers whose accounts are set to use Zero Data Retention (ZDR) mus
disable_response_storage = true
```
### shell_environment_policy
Codex spawns subprocesses (e.g. when executing a `local_shell` tool-call suggested by the assistant). By default it passes **only a minimal core subset** of your environment to those subprocesses to avoid leaking credentials. You can tune this behavior via the **`shell_environment_policy`** block in
`config.toml`:
```toml
[shell_environment_policy]
# inherit can be "core" (default), "all", or "none"
inherit = "core"
# set to true to *skip* the filter for `"*KEY*"` and `"*TOKEN*"`
ignore_default_excludes = false
# exclude patterns (case-insensitive globs)
exclude = ["AWS_*", "AZURE_*"]
# force-set / override values
set = { CI = "1" }
# if provided, *only* vars matching these patterns are kept
include_only = ["PATH", "HOME"]
```
| Field | Type | Default | Description |
| ------------------------- | -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `inherit` | string | `core` | Starting template for the environment:<br>`core` (`HOME`, `PATH`, `USER`, …), `all` (clone full parent env), or `none` (start empty). |
| `ignore_default_excludes` | boolean | `false` | When `false`, Codex removes any var whose **name** contains `KEY`, `SECRET`, or `TOKEN` (case-insensitive) before other rules run. |
| `exclude` | array&lt;string&gt; | `[]` | Case-insensitive glob patterns to drop after the default filter.<br>Examples: `"AWS_*"`, `"AZURE_*"`. |
| `set` | table&lt;string,string&gt; | `{}` | Explicit key/value overrides or additions always win over inherited values. |
| `include_only` | array&lt;string&gt; | `[]` | If non-empty, a whitelist of patterns; only variables that match _one_ pattern survive the final step. (Generally used with `inherit = "all"`.) |
The patterns are **glob style**, not full regular expressions: `*` matches any
number of characters, `?` matches exactly one, and character classes like
`[A-Z]`/`[^0-9]` are supported. Matching is always **case-insensitive**. This
syntax is documented in code as `EnvironmentVariablePattern` (see
`core/src/config_types.rs`).
If you just need a clean slate with a few custom entries you can write:
```toml
[shell_environment_policy]
inherit = "none"
set = { PATH = "/usr/bin", MY_FLAG = "1" }
```
Currently, `CODEX_SANDBOX_NETWORK_DISABLED=1` is also added to the environment, assuming network is disabled. This is not configurable.
### notify
Specify a program that will be executed to get notified about events generated by Codex. Note that the program will receive the notification argument as a string of JSON, e.g.:

View File

@@ -3,27 +3,29 @@
//! On Linux the command is executed inside a Landlock + seccomp sandbox by
//! calling the low-level `exec_linux` helper from `codex_core::linux`.
use codex_core::config::Config;
use codex_core::exec::StdioPolicy;
use codex_core::exec::spawn_child_sync;
use codex_core::exec_linux::apply_sandbox_policy_to_current_thread;
use codex_core::protocol::SandboxPolicy;
use std::process::ExitStatus;
use crate::exit_status::handle_exit_status;
/// Execute `command` in a Linux sandbox (Landlock + seccomp) the way Codex
/// would.
pub fn run_landlock(command: Vec<String>, sandbox_policy: SandboxPolicy) -> anyhow::Result<()> {
pub fn run_landlock(command: Vec<String>, config: &Config) -> anyhow::Result<()> {
if command.is_empty() {
anyhow::bail!("command args are empty");
}
// Spawn a new thread and apply the sandbox policies there.
let env = codex_core::exec_env::create_env(&config.shell_environment_policy);
let sandbox_policy = config.sandbox_policy.clone();
let handle = std::thread::spawn(move || -> anyhow::Result<ExitStatus> {
let cwd = std::env::current_dir()?;
apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd)?;
let mut child = spawn_child_sync(command, cwd, &sandbox_policy, StdioPolicy::Inherit)?;
let mut child = spawn_child_sync(command, cwd, &sandbox_policy, StdioPolicy::Inherit, env)?;
let status = child.wait()?;
Ok(status)
});

View File

@@ -10,6 +10,8 @@ fn main() -> anyhow::Result<()> {
use codex_cli::LandlockCommand;
use codex_cli::create_sandbox_policy;
use codex_cli::landlock;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
let LandlockCommand {
full_auto,
@@ -17,6 +19,10 @@ fn main() -> anyhow::Result<()> {
command,
} = LandlockCommand::parse();
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
landlock::run_landlock(command, sandbox_policy)?;
let config = Config::load_with_overrides(ConfigOverrides {
sandbox_policy: Some(sandbox_policy),
..Default::default()
})?;
landlock::run_landlock(command, &config)?;
Ok(())
}

View File

@@ -1,9 +1,13 @@
use std::path::PathBuf;
use clap::Parser;
use codex_cli::LandlockCommand;
use codex_cli::SeatbeltCommand;
use codex_cli::create_sandbox_policy;
use codex_cli::proto;
use codex_cli::seatbelt;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_exec::Cli as ExecCli;
use codex_tui::Cli as TuiCli;
@@ -64,17 +68,23 @@ struct ReplProto {}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let codex_linux_sandbox_exe: Option<PathBuf> = if cfg!(target_os = "linux") {
std::env::current_exe().ok()
} else {
None
};
let cli = MultitoolCli::parse();
match cli.subcommand {
None => {
codex_tui::run_main(cli.interactive)?;
codex_tui::run_main(cli.interactive, codex_linux_sandbox_exe)?;
}
Some(Subcommand::Exec(exec_cli)) => {
codex_exec::run_main(exec_cli).await?;
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
}
Some(Subcommand::Mcp) => {
codex_mcp_server::run_main().await?;
codex_mcp_server::run_main(codex_linux_sandbox_exe).await?;
}
Some(Subcommand::Proto(proto_cli)) => {
proto::run_main(proto_cli).await?;
@@ -86,7 +96,11 @@ async fn main() -> anyhow::Result<()> {
full_auto,
}) => {
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
seatbelt::run_seatbelt(command, sandbox_policy).await?;
let config = Config::load_with_overrides(ConfigOverrides {
sandbox_policy: Some(sandbox_policy),
..Default::default()
})?;
seatbelt::run_seatbelt(command, &config).await?;
}
#[cfg(unix)]
DebugCommand::Landlock(LandlockCommand {
@@ -95,7 +109,11 @@ async fn main() -> anyhow::Result<()> {
full_auto,
}) => {
let sandbox_policy = create_sandbox_policy(full_auto, sandbox);
codex_cli::landlock::run_landlock(command, sandbox_policy)?;
let config = Config::load_with_overrides(ConfigOverrides {
sandbox_policy: Some(sandbox_policy),
..Default::default()
})?;
codex_cli::landlock::run_landlock(command, &config)?;
}
#[cfg(not(unix))]
DebugCommand::Landlock(_) => {

View File

@@ -1,16 +1,21 @@
use codex_core::config::Config;
use codex_core::exec::StdioPolicy;
use codex_core::exec::spawn_command_under_seatbelt;
use codex_core::protocol::SandboxPolicy;
use codex_core::exec_env::create_env;
use crate::exit_status::handle_exit_status;
pub async fn run_seatbelt(
command: Vec<String>,
sandbox_policy: SandboxPolicy,
) -> anyhow::Result<()> {
pub async fn run_seatbelt(command: Vec<String>, config: &Config) -> anyhow::Result<()> {
let cwd = std::env::current_dir()?;
let mut child =
spawn_command_under_seatbelt(command, &sandbox_policy, cwd, StdioPolicy::Inherit).await?;
let env = create_env(&config.shell_environment_policy);
let mut child = spawn_command_under_seatbelt(
command,
&config.sandbox_policy,
cwd,
StdioPolicy::Inherit,
env,
)
.await?;
let status = child.wait().await?;
handle_exit_status(status);
}

View File

@@ -46,6 +46,7 @@ tracing = { version = "0.1.41", features = ["log"] }
tree-sitter = "0.25.3"
tree-sitter-bash = "0.23.3"
uuid = { version = "1", features = ["serde", "v4"] }
wildmatch = "2.4.0"
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2.172"
@@ -58,6 +59,7 @@ openssl-sys = { version = "*", features = ["vendored"] }
[dev-dependencies]
assert_cmd = "2"
maplit = "1.0.2"
predicates = "3"
pretty_assertions = "1.4.1"
tempfile = "3"

View File

@@ -37,6 +37,7 @@ use crate::client::ModelClient;
use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::config::Config;
use crate::config_types::ShellEnvironmentPolicy;
use crate::conversation_history::ConversationHistory;
use crate::error::CodexErr;
use crate::error::Result as CodexResult;
@@ -45,6 +46,7 @@ use crate::exec::ExecParams;
use crate::exec::ExecToolCallOutput;
use crate::exec::SandboxType;
use crate::exec::process_exec_tool_call;
use crate::exec_env::create_env;
use crate::flags::OPENAI_STREAM_MAX_RETRIES;
use crate::mcp_connection_manager::McpConnectionManager;
use crate::mcp_connection_manager::try_parse_fully_qualified_tool_name;
@@ -171,6 +173,7 @@ pub(crate) struct Session {
instructions: Option<String>,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
shell_environment_policy: ShellEnvironmentPolicy,
writable_roots: Mutex<Vec<PathBuf>>,
/// Manager for external MCP servers/tools.
@@ -634,6 +637,7 @@ async fn submission_loop(
instructions,
approval_policy,
sandbox_policy,
shell_environment_policy: config.shell_environment_policy.clone(),
cwd,
writable_roots,
mcp_connection_manager,
@@ -1124,6 +1128,7 @@ fn to_exec_params(params: ShellToolCallParams, sess: &Session) -> ExecParams {
command: params.command,
cwd: sess.resolve_path(params.workdir.clone()),
timeout_ms: params.timeout_ms,
env: create_env(&sess.shell_environment_policy),
}
}

View File

@@ -1,6 +1,11 @@
use crate::config_profile::ConfigProfile;
use crate::config_types::History;
use crate::config_types::McpServerConfig;
use crate::config_types::ShellEnvironmentPolicy;
use crate::config_types::ShellEnvironmentPolicyToml;
use crate::config_types::Tui;
use crate::config_types::UriBasedFileOpener;
use crate::flags::OPENAI_DEFAULT_MODEL;
use crate::mcp_server_config::McpServerConfig;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::built_in_model_providers;
use crate::protocol::AskForApproval;
@@ -34,6 +39,8 @@ pub struct Config {
pub sandbox_policy: SandboxPolicy,
pub shell_environment_policy: ShellEnvironmentPolicy,
/// Disable server-side response storage (sends the full conversation
/// context with every request). Currently necessary for OpenAI customers
/// who have opted into Zero Data Retention (ZDR).
@@ -91,75 +98,14 @@ pub struct Config {
/// Collection of settings that are specific to the TUI.
pub tui: Tui,
}
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct History {
/// If true, history entries will not be written to disk.
pub persistence: HistoryPersistence,
/// If set, the maximum size of the history file in bytes.
/// TODO(mbolin): Not currently honored.
pub max_bytes: Option<usize>,
}
#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum HistoryPersistence {
/// Save all history entries to disk.
#[default]
SaveAll,
/// Do not write history to disk.
None,
}
/// Collection of settings that are specific to the TUI.
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct Tui {
/// By default, mouse capture is enabled in the TUI so that it is possible
/// to scroll the conversation history with a mouse. This comes at the cost
/// of not being able to use the mouse to select text in the TUI.
/// (Most terminals support a modifier key to allow this. For example,
/// text selection works in iTerm if you hold down the `Option` key while
/// clicking and dragging.)
/// Path to the `codex-linux-sandbox` executable. This must be set if
/// [`crate::exec::SandboxType::LinuxSeccomp`] is used. Note that this
/// cannot be set in the config file: it must be set in code via
/// [`ConfigOverrides`].
///
/// Setting this option to `true` disables mouse capture, so scrolling with
/// the mouse is not possible, though the keyboard shortcuts e.g. `b` and
/// `space` still work. This allows the user to select text in the TUI
/// using the mouse without needing to hold down a modifier key.
pub disable_mouse_capture: bool,
}
#[derive(Deserialize, Debug, Copy, Clone, PartialEq)]
pub enum UriBasedFileOpener {
#[serde(rename = "vscode")]
VsCode,
#[serde(rename = "vscode-insiders")]
VsCodeInsiders,
#[serde(rename = "windsurf")]
Windsurf,
#[serde(rename = "cursor")]
Cursor,
/// Option to disable the URI-based file opener.
#[serde(rename = "none")]
None,
}
impl UriBasedFileOpener {
pub fn get_scheme(&self) -> Option<&str> {
match self {
UriBasedFileOpener::VsCode => Some("vscode"),
UriBasedFileOpener::VsCodeInsiders => Some("vscode-insiders"),
UriBasedFileOpener::Windsurf => Some("windsurf"),
UriBasedFileOpener::Cursor => Some("cursor"),
UriBasedFileOpener::None => None,
}
}
/// When this program is invoked, arg0 will be set to `codex-linux-sandbox`.
pub codex_linux_sandbox_exe: Option<PathBuf>,
}
/// Base config deserialized from ~/.codex/config.toml.
@@ -174,6 +120,9 @@ pub struct ConfigToml {
/// Default approval policy for executing commands.
pub approval_policy: Option<AskForApproval>,
#[serde(default)]
pub shell_environment_policy: ShellEnvironmentPolicyToml,
// The `default` attribute ensures that the field is treated as `None` when
// the key is omitted from the TOML. Without it, Serde treats the field as
// required because we supply a custom deserializer.
@@ -281,6 +230,7 @@ pub struct ConfigOverrides {
pub disable_response_storage: Option<bool>,
pub model_provider: Option<String>,
pub config_profile: Option<String>,
pub codex_linux_sandbox_exe: Option<PathBuf>,
}
impl Config {
@@ -317,6 +267,7 @@ impl Config {
disable_response_storage,
model_provider,
config_profile: config_profile_key,
codex_linux_sandbox_exe,
} = overrides;
let config_profile = match config_profile_key.or(cfg.profile) {
@@ -368,6 +319,8 @@ impl Config {
})?
.clone();
let shell_environment_policy = cfg.shell_environment_policy.into();
let resolved_cwd = {
use std::env;
@@ -402,6 +355,7 @@ impl Config {
.or(cfg.approval_policy)
.unwrap_or_else(AskForApproval::default),
sandbox_policy,
shell_environment_policy,
disable_response_storage: disable_response_storage
.or(config_profile.disable_response_storage)
.or(cfg.disable_response_storage)
@@ -415,6 +369,7 @@ impl Config {
history,
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
tui: cfg.tui.unwrap_or_default(),
codex_linux_sandbox_exe,
};
Ok(config)
}
@@ -523,6 +478,8 @@ pub fn parse_sandbox_permission_with_base_path(
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use crate::config_types::HistoryPersistence;
use super::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -741,6 +698,7 @@ disable_response_storage = true
model_provider: fixture.openai_provider.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: false,
instructions: None,
notify: None,
@@ -752,6 +710,7 @@ disable_response_storage = true
history: History::default(),
file_opener: UriBasedFileOpener::VsCode,
tui: Tui::default(),
codex_linux_sandbox_exe: None,
},
o3_profile_config
);
@@ -778,6 +737,7 @@ disable_response_storage = true
model_provider: fixture.openai_chat_completions_provider.clone(),
approval_policy: AskForApproval::UnlessAllowListed,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: false,
instructions: None,
notify: None,
@@ -789,6 +749,7 @@ disable_response_storage = true
history: History::default(),
file_opener: UriBasedFileOpener::VsCode,
tui: Tui::default(),
codex_linux_sandbox_exe: None,
};
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
@@ -830,6 +791,7 @@ disable_response_storage = true
model_provider: fixture.openai_provider.clone(),
approval_policy: AskForApproval::OnFailure,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: true,
instructions: None,
notify: None,
@@ -841,6 +803,7 @@ disable_response_storage = true
history: History::default(),
file_opener: UriBasedFileOpener::VsCode,
tui: Tui::default(),
codex_linux_sandbox_exe: None,
};
assert_eq!(expected_zdr_profile_config, zdr_profile_config);

View File

@@ -0,0 +1,177 @@
//! Types used to define the fields of [`crate::config::Config`].
// Note this file should generally be restricted to simple struct/enum
// definitions that do not contain business logic.
use std::collections::HashMap;
use wildmatch::WildMatchPattern;
use serde::Deserialize;
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct McpServerConfig {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: Option<HashMap<String, String>>,
}
#[derive(Deserialize, Debug, Copy, Clone, PartialEq)]
pub enum UriBasedFileOpener {
#[serde(rename = "vscode")]
VsCode,
#[serde(rename = "vscode-insiders")]
VsCodeInsiders,
#[serde(rename = "windsurf")]
Windsurf,
#[serde(rename = "cursor")]
Cursor,
/// Option to disable the URI-based file opener.
#[serde(rename = "none")]
None,
}
impl UriBasedFileOpener {
pub fn get_scheme(&self) -> Option<&str> {
match self {
UriBasedFileOpener::VsCode => Some("vscode"),
UriBasedFileOpener::VsCodeInsiders => Some("vscode-insiders"),
UriBasedFileOpener::Windsurf => Some("windsurf"),
UriBasedFileOpener::Cursor => Some("cursor"),
UriBasedFileOpener::None => None,
}
}
}
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct History {
/// If true, history entries will not be written to disk.
pub persistence: HistoryPersistence,
/// If set, the maximum size of the history file in bytes.
/// TODO(mbolin): Not currently honored.
pub max_bytes: Option<usize>,
}
#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum HistoryPersistence {
/// Save all history entries to disk.
#[default]
SaveAll,
/// Do not write history to disk.
None,
}
/// Collection of settings that are specific to the TUI.
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct Tui {
/// By default, mouse capture is enabled in the TUI so that it is possible
/// to scroll the conversation history with a mouse. This comes at the cost
/// of not being able to use the mouse to select text in the TUI.
/// (Most terminals support a modifier key to allow this. For example,
/// text selection works in iTerm if you hold down the `Option` key while
/// clicking and dragging.)
///
/// Setting this option to `true` disables mouse capture, so scrolling with
/// the mouse is not possible, though the keyboard shortcuts e.g. `b` and
/// `space` still work. This allows the user to select text in the TUI
/// using the mouse without needing to hold down a modifier key.
pub disable_mouse_capture: bool,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub enum ShellEnvironmentPolicyInherit {
/// "Core" environment variables for the platform. On UNIX, this would
/// include HOME, LOGNAME, PATH, SHELL, and USER, among others.
#[default]
Core,
/// Inherits the full environment from the parent process.
All,
/// Do not inherit any environment variables from the parent process.
None,
}
/// Policy for building the `env` when spawning a process via either the
/// `shell` or `local_shell` tool.
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct ShellEnvironmentPolicyToml {
pub inherit: Option<ShellEnvironmentPolicyInherit>,
pub ignore_default_excludes: Option<bool>,
/// List of regular expressions.
pub exclude: Option<Vec<String>>,
pub r#set: Option<HashMap<String, String>>,
/// List of regular expressions.
pub include_only: Option<Vec<String>>,
}
pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>;
/// Deriving the `env` based on this policy works as follows:
/// 1. Create an initial map based on the `inherit` policy.
/// 2. If `ignore_default_excludes` is false, filter the map using the default
/// exclude pattern(s), which are: `"*KEY*"` and `"*TOKEN*"`.
/// 3. If `exclude` is not empty, filter the map using the provided patterns.
/// 4. Insert any entries from `r#set` into the map.
/// 5. If non-empty, filter the map using the `include_only` patterns.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ShellEnvironmentPolicy {
/// Starting point when building the environment.
pub inherit: ShellEnvironmentPolicyInherit,
/// True to skip the check to exclude default environment variables that
/// contain "KEY" or "TOKEN" in their name.
pub ignore_default_excludes: bool,
/// Environment variable names to exclude from the environment.
pub exclude: Vec<EnvironmentVariablePattern>,
/// (key, value) pairs to insert in the environment.
pub r#set: HashMap<String, String>,
/// Environment variable names to retain in the environment.
pub include_only: Vec<EnvironmentVariablePattern>,
}
impl From<ShellEnvironmentPolicyToml> for ShellEnvironmentPolicy {
fn from(toml: ShellEnvironmentPolicyToml) -> Self {
let inherit = toml.inherit.unwrap_or(ShellEnvironmentPolicyInherit::Core);
let ignore_default_excludes = toml.ignore_default_excludes.unwrap_or(false);
let exclude = toml
.exclude
.unwrap_or_default()
.into_iter()
.map(|s| EnvironmentVariablePattern::new_case_insensitive(&s))
.collect();
let r#set = toml.r#set.unwrap_or_default();
let include_only = toml
.include_only
.unwrap_or_default()
.into_iter()
.map(|s| EnvironmentVariablePattern::new_case_insensitive(&s))
.collect();
Self {
inherit,
ignore_default_excludes,
exclude,
r#set,
include_only,
}
}
}

View File

@@ -1,6 +1,7 @@
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
use std::collections::HashMap;
use std::io;
use std::path::Path;
use std::path::PathBuf;
@@ -59,6 +60,7 @@ pub struct ExecParams {
pub command: Vec<String>,
pub cwd: PathBuf,
pub timeout_ms: Option<u64>,
pub env: HashMap<String, String>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -87,12 +89,14 @@ pub async fn process_exec_tool_call(
command,
cwd,
timeout_ms,
env,
} = params;
let child = spawn_command_under_seatbelt(
command,
sandbox_policy,
cwd,
StdioPolicy::RedirectForShellTool,
env,
)
.await?;
consume_truncated_output(child, ctrl_c, timeout_ms).await
@@ -145,9 +149,10 @@ pub async fn spawn_command_under_seatbelt(
sandbox_policy: &SandboxPolicy,
cwd: PathBuf,
stdio_policy: StdioPolicy,
env: HashMap<String, String>,
) -> std::io::Result<Child> {
let seatbelt_command = create_seatbelt_command(command, sandbox_policy, &cwd);
spawn_child_async(seatbelt_command, cwd, sandbox_policy, stdio_policy).await
spawn_child_async(seatbelt_command, cwd, sandbox_policy, stdio_policy, env).await
}
fn create_seatbelt_command(
@@ -233,6 +238,7 @@ async fn exec(
command,
cwd,
timeout_ms,
env,
}: ExecParams,
sandbox_policy: &SandboxPolicy,
ctrl_c: Arc<Notify>,
@@ -242,6 +248,7 @@ async fn exec(
cwd,
sandbox_policy,
StdioPolicy::RedirectForShellTool,
env,
)
.await?;
consume_truncated_output(child, ctrl_c, timeout_ms).await
@@ -259,7 +266,8 @@ macro_rules! configure_command {
$command: expr,
$cwd: expr,
$sandbox_policy: expr,
$stdio_policy: expr
$stdio_policy: expr,
$env_map: expr
) => {{
// For now, we take `SandboxPolicy` as a parameter to spawn_child() because
// we need to determine whether to set the
@@ -279,6 +287,38 @@ macro_rules! configure_command {
cmd.args(&$command[1..]);
cmd.current_dir($cwd);
// Previously, to update the env for `cmd`, we did the straightforward
// thing of calling `env_clear()` followed by `envs(&env_map)` so
// that the spawned process inherited *only* the variables explicitly
// provided by the caller. On Linux, the combination of `env_clear()`
// and Landlock/seccomp caused a permission error whereas this more
// "surgical" approach of setting variables individually appears to
// work fine. More time with `strace` and friends is merited to fully
// debug thus, though we will soon use a helper binary like we do for
// Seatbelt, which will simplify this logic.
// Iterate through the current process environment first so we can
// decide, for every variable that already exists, whether we need to
// override its value.
let mut remaining_overrides = $env_map.clone();
for (key, current_val) in std::env::vars() {
if let Some(desired_val) = remaining_overrides.remove(&key) {
// The caller provided a value for this variable. Override it
// only if the value differs from what is currently set.
if desired_val != current_val {
cmd.env(&key, desired_val);
}
}
// If the variable was not in `env_map`, we leave it unchanged.
}
// Any entries still left in `remaining_overrides` were not present in
// the parent environment. Add them now so that the child process sees
// the complete set requested by the caller.
for (key, val) in remaining_overrides {
cmd.env(key, val);
}
if !$sandbox_policy.has_full_network_access() {
cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
}
@@ -313,8 +353,9 @@ pub(crate) async fn spawn_child_async(
cwd: PathBuf,
sandbox_policy: &SandboxPolicy,
stdio_policy: StdioPolicy,
env: HashMap<String, String>,
) -> std::io::Result<Child> {
let mut cmd = configure_command!(Command, command, cwd, sandbox_policy, stdio_policy)?;
let mut cmd = configure_command!(Command, command, cwd, sandbox_policy, stdio_policy, env)?;
cmd.kill_on_drop(true).spawn()
}
@@ -326,13 +367,15 @@ pub fn spawn_child_sync(
cwd: PathBuf,
sandbox_policy: &SandboxPolicy,
stdio_policy: StdioPolicy,
env: HashMap<String, String>,
) -> std::io::Result<std::process::Child> {
let mut cmd = configure_command!(
std::process::Command,
command,
cwd,
sandbox_policy,
stdio_policy
stdio_policy,
env
)?;
cmd.spawn()
}

View File

@@ -0,0 +1,196 @@
use crate::config_types::EnvironmentVariablePattern;
use crate::config_types::ShellEnvironmentPolicy;
use crate::config_types::ShellEnvironmentPolicyInherit;
use std::collections::HashMap;
use std::collections::HashSet;
/// Construct an environment map based on the rules in the specified policy. The
/// resulting map can be passed directly to `Command::envs()` after calling
/// `env_clear()` to ensure no unintended variables are leaked to the spawned
/// process.
///
/// The derivation follows the algorithm documented in the struct-level comment
/// for [`ShellEnvironmentPolicy`].
pub fn create_env(policy: &ShellEnvironmentPolicy) -> HashMap<String, String> {
populate_env(std::env::vars(), policy)
}
fn populate_env<I>(vars: I, policy: &ShellEnvironmentPolicy) -> HashMap<String, String>
where
I: IntoIterator<Item = (String, String)>,
{
// Step 1 determine the starting set of variables based on the
// `inherit` strategy.
let mut env_map: HashMap<String, String> = match policy.inherit {
ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(),
ShellEnvironmentPolicyInherit::None => HashMap::new(),
ShellEnvironmentPolicyInherit::Core => {
const CORE_VARS: &[&str] = &[
"HOME", "LOGNAME", "PATH", "SHELL", "USER", "USERNAME", "TMPDIR", "TEMP", "TMP",
];
let allow: HashSet<&str> = CORE_VARS.iter().copied().collect();
vars.into_iter()
.filter(|(k, _)| allow.contains(k.as_str()))
.collect()
}
};
// Internal helper does `name` match **any** pattern in `patterns`?
let matches_any = |name: &str, patterns: &[EnvironmentVariablePattern]| -> bool {
patterns.iter().any(|pattern| pattern.matches(name))
};
// Step 2 Apply the default exclude if not disabled.
if !policy.ignore_default_excludes {
let default_excludes = vec![
EnvironmentVariablePattern::new_case_insensitive("*KEY*"),
EnvironmentVariablePattern::new_case_insensitive("*SECRET*"),
EnvironmentVariablePattern::new_case_insensitive("*TOKEN*"),
];
env_map.retain(|k, _| !matches_any(k, &default_excludes));
}
// Step 3 Apply custom excludes.
if !policy.exclude.is_empty() {
env_map.retain(|k, _| !matches_any(k, &policy.exclude));
}
// Step 4 Apply user-provided overrides.
for (key, val) in &policy.r#set {
env_map.insert(key.clone(), val.clone());
}
// Step 5 If include_only is non-empty, keep *only* the matching vars.
if !policy.include_only.is_empty() {
env_map.retain(|k, _| matches_any(k, &policy.include_only));
}
env_map
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use crate::config_types::ShellEnvironmentPolicyInherit;
use maplit::hashmap;
fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn test_core_inherit_and_default_excludes() {
let vars = make_vars(&[
("PATH", "/usr/bin"),
("HOME", "/home/user"),
("API_KEY", "secret"),
("SECRET_TOKEN", "t"),
]);
let policy = ShellEnvironmentPolicy::default(); // inherit Core, default excludes on
let result = populate_env(vars, &policy);
let expected: HashMap<String, String> = hashmap! {
"PATH".to_string() => "/usr/bin".to_string(),
"HOME".to_string() => "/home/user".to_string(),
};
assert_eq!(result, expected);
}
#[test]
fn test_include_only() {
let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]);
let policy = ShellEnvironmentPolicy {
// skip default excludes so nothing is removed prematurely
ignore_default_excludes: true,
include_only: vec![EnvironmentVariablePattern::new_case_insensitive("*PATH")],
..Default::default()
};
let result = populate_env(vars, &policy);
let expected: HashMap<String, String> = hashmap! {
"PATH".to_string() => "/usr/bin".to_string(),
};
assert_eq!(result, expected);
}
#[test]
fn test_set_overrides() {
let vars = make_vars(&[("PATH", "/usr/bin")]);
let mut policy = ShellEnvironmentPolicy {
ignore_default_excludes: true,
..Default::default()
};
policy.r#set.insert("NEW_VAR".to_string(), "42".to_string());
let result = populate_env(vars, &policy);
let expected: HashMap<String, String> = hashmap! {
"PATH".to_string() => "/usr/bin".to_string(),
"NEW_VAR".to_string() => "42".to_string(),
};
assert_eq!(result, expected);
}
#[test]
fn test_inherit_all() {
let vars = make_vars(&[("PATH", "/usr/bin"), ("FOO", "bar")]);
let policy = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::All,
ignore_default_excludes: true, // keep everything
..Default::default()
};
let result = populate_env(vars.clone(), &policy);
let expected: HashMap<String, String> = vars.into_iter().collect();
assert_eq!(result, expected);
}
#[test]
fn test_inherit_all_with_default_excludes() {
let vars = make_vars(&[("PATH", "/usr/bin"), ("API_KEY", "secret")]);
let policy = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::All,
..Default::default()
};
let result = populate_env(vars, &policy);
let expected: HashMap<String, String> = hashmap! {
"PATH".to_string() => "/usr/bin".to_string(),
};
assert_eq!(result, expected);
}
#[test]
fn test_inherit_none() {
let vars = make_vars(&[("PATH", "/usr/bin"), ("HOME", "/home")]);
let mut policy = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::None,
ignore_default_excludes: true,
..Default::default()
};
policy
.r#set
.insert("ONLY_VAR".to_string(), "yes".to_string());
let result = populate_env(vars, &policy);
let expected: HashMap<String, String> = hashmap! {
"ONLY_VAR".to_string() => "yes".to_string(),
};
assert_eq!(result, expected);
}
}

View File

@@ -34,6 +34,7 @@ pub fn exec_linux(
command,
cwd,
timeout_ms,
env,
} = params;
apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd)?;
let child = spawn_child_async(
@@ -41,6 +42,7 @@ pub fn exec_linux(
cwd,
&sandbox_policy,
StdioPolicy::RedirectForShellTool,
env,
)
.await?;
consume_truncated_output(child, ctrl_c_copy, timeout_ms).await

View File

@@ -143,20 +143,29 @@ mod tests {
#![expect(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use crate::config_types::ShellEnvironmentPolicy;
use crate::exec::ExecParams;
use crate::exec::SandboxType;
use crate::exec::process_exec_tool_call;
use crate::exec_env::create_env;
use crate::protocol::SandboxPolicy;
use std::collections::HashMap;
use std::sync::Arc;
use tempfile::NamedTempFile;
use tokio::sync::Notify;
fn create_env_from_core_vars() -> HashMap<String, String> {
let policy = ShellEnvironmentPolicy::default();
create_env(&policy)
}
#[allow(clippy::print_stdout)]
async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64) {
let params = ExecParams {
command: cmd.iter().map(|elm| elm.to_string()).collect(),
cwd: std::env::current_dir().expect("cwd should exist"),
timeout_ms: Some(timeout_ms),
env: create_env_from_core_vars(),
};
let sandbox_policy =
@@ -236,9 +245,10 @@ mod tests {
let params = ExecParams {
command: cmd.iter().map(|s| s.to_string()).collect(),
cwd: std::env::current_dir().expect("cwd should exist"),
// Give the tool a generous 2second timeout so even slow DNS timeouts
// Give the tool a generous 2-second timeout so even slow DNS timeouts
// do not stall the suite.
timeout_ms: Some(2_000),
env: create_env_from_core_vars(),
};
let sandbox_policy = SandboxPolicy::new_read_only_policy();

View File

@@ -13,16 +13,17 @@ pub use codex::Codex;
pub mod codex_wrapper;
pub mod config;
pub mod config_profile;
pub mod config_types;
mod conversation_history;
pub mod error;
pub mod exec;
pub mod exec_env;
pub mod exec_linux;
mod flags;
mod is_safe_command;
#[cfg(target_os = "linux")]
pub mod landlock;
mod mcp_connection_manager;
pub mod mcp_server_config;
mod mcp_tool_call;
mod message_history;
mod model_provider_info;

View File

@@ -19,7 +19,7 @@ use mcp_types::Tool;
use tokio::task::JoinSet;
use tracing::info;
use crate::mcp_server_config::McpServerConfig;
use crate::config_types::McpServerConfig;
/// Delimiter used to separate the server name from the tool name in a fully
/// qualified tool name.

View File

@@ -1,14 +0,0 @@
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct McpServerConfig {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: Option<HashMap<String, String>>,
}

View File

@@ -28,7 +28,7 @@ use tokio::io::AsyncReadExt;
use uuid::Uuid;
use crate::config::Config;
use crate::config::HistoryPersistence;
use crate::config_types::HistoryPersistence;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;

View File

@@ -1,5 +1,6 @@
use chrono::Utc;
use codex_common::elapsed::format_elapsed;
use codex_core::config::Config;
use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::BackgroundEventEvent;
use codex_core::protocol::ErrorEvent;
@@ -13,7 +14,6 @@ use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::PatchApplyBeginEvent;
use codex_core::protocol::PatchApplyEndEvent;
use codex_core::protocol::SessionConfiguredEvent;
use codex_core::protocol::TaskCompleteEvent;
use owo_colors::OwoColorize;
use owo_colors::Style;
use shlex::try_join;
@@ -103,9 +103,36 @@ macro_rules! ts_println {
}};
}
/// Print a concise summary of the effective configuration that will be used
/// for the session. This mirrors the information shown in the TUI welcome
/// screen.
pub(crate) fn print_config_summary(config: &Config, with_ansi: bool) {
let bold = if with_ansi {
Style::new().bold()
} else {
Style::new()
};
ts_println!("OpenAI Codex (research preview)\n--------");
let entries = vec![
("workdir", config.cwd.display().to_string()),
("model", config.model.clone()),
("provider", config.model_provider_id.clone()),
("approval", format!("{:?}", config.approval_policy)),
("sandbox", format!("{:?}", config.sandbox_policy)),
];
for (key, value) in entries {
println!("{} {}", format!("{key}: ").style(bold), value);
}
println!("--------\n");
}
impl EventProcessor {
pub(crate) fn process_event(&mut self, event: Event) {
let Event { id, msg } = event;
let Event { id: _, msg } = event;
match msg {
EventMsg::Error(ErrorEvent { message }) => {
let prefix = "ERROR:".style(self.red);
@@ -114,15 +141,8 @@ impl EventProcessor {
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
ts_println!("{}", message.style(self.dimmed));
}
EventMsg::TaskStarted => {
let msg = format!("Task started: {id}");
ts_println!("{}", msg.style(self.dimmed));
}
EventMsg::TaskComplete(TaskCompleteEvent {
last_agent_message: _,
}) => {
let msg = format!("Task complete: {id}");
ts_println!("{}", msg.style(self.bold));
EventMsg::TaskStarted | EventMsg::TaskComplete(_) => {
// Ignore.
}
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
let prefix = "Agent message:".style(self.bold);
@@ -385,7 +405,15 @@ impl EventProcessor {
history_log_id: _,
history_entry_count: _,
} = session_configured_event;
println!("session {session_id} with model {model}");
ts_println!(
"{} {}",
"codex session".style(self.magenta).style(self.bold),
session_id.to_string().style(self.dimmed)
);
ts_println!("model: {}", model);
println!();
}
EventMsg::GetHistoryEntryResponse(_) => {
// Currently ignored in exec output.

View File

@@ -3,6 +3,7 @@ mod event_processor;
use std::io::IsTerminal;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
pub use cli::Cli;
@@ -18,13 +19,14 @@ use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::util::is_inside_git_repo;
use event_processor::EventProcessor;
use event_processor::print_config_summary;
use tracing::debug;
use tracing::error;
use tracing::info;
use tracing_subscriber::EnvFilter;
use std::io::Write;
pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
let Cli {
images,
model,
@@ -70,8 +72,11 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
},
cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)),
model_provider: None,
codex_linux_sandbox_exe,
};
let config = Config::load_with_overrides(overrides)?;
// Print the effective configuration so users can see what Codex is using.
print_config_summary(&config, stdout_with_ansi);
if !skip_git_repo_check && !is_inside_git_repo(&config) {
eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified.");

View File

@@ -1,11 +1,19 @@
use std::path::PathBuf;
use clap::Parser;
use codex_exec::Cli;
use codex_exec::run_main;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let codex_linux_sandbox_exe: Option<PathBuf> = if cfg!(target_os = "linux") {
std::env::current_exe().ok()
} else {
None
};
let cli = Cli::parse();
run_main(cli).await?;
run_main(cli, codex_linux_sandbox_exe).await?;
Ok(())
}

View File

@@ -144,7 +144,10 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
impl CodexToolCallParam {
/// Returns the initial user prompt to start the Codex conversation and the
/// Config.
pub fn into_config(self) -> std::io::Result<(String, codex_core::config::Config)> {
pub fn into_config(
self,
codex_linux_sandbox_exe: Option<PathBuf>,
) -> std::io::Result<(String, codex_core::config::Config)> {
let Self {
prompt,
model,
@@ -167,6 +170,7 @@ impl CodexToolCallParam {
sandbox_policy,
disable_response_storage,
model_provider: None,
codex_linux_sandbox_exe,
};
let cfg = codex_core::config::Config::load_with_overrides(overrides)?;

View File

@@ -2,6 +2,7 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]
use std::io::Result as IoResult;
use std::path::PathBuf;
use mcp_types::JSONRPCMessage;
use tokio::io::AsyncBufReadExt;
@@ -24,7 +25,7 @@ use crate::message_processor::MessageProcessor;
/// plenty for an interactive CLI.
const CHANNEL_CAPACITY: usize = 128;
pub async fn run_main() -> IoResult<()> {
pub async fn run_main(codex_linux_sandbox_exe: Option<PathBuf>) -> IoResult<()> {
// Install a simple subscriber so `tracing` output is visible. Users can
// control the log level with `RUST_LOG`.
tracing_subscriber::fmt()
@@ -61,7 +62,7 @@ pub async fn run_main() -> IoResult<()> {
// Task: process incoming messages.
let processor_handle = tokio::spawn({
let mut processor = MessageProcessor::new(outgoing_tx.clone());
let mut processor = MessageProcessor::new(outgoing_tx.clone(), codex_linux_sandbox_exe);
async move {
while let Some(msg) = incoming_rx.recv().await {
match msg {

View File

@@ -1,7 +1,15 @@
use std::path::PathBuf;
use codex_mcp_server::run_main;
#[tokio::main]
async fn main() -> std::io::Result<()> {
run_main().await?;
let codex_linux_sandbox_exe: Option<PathBuf> = if cfg!(target_os = "linux") {
std::env::current_exe().ok()
} else {
None
};
run_main(codex_linux_sandbox_exe).await?;
Ok(())
}

View File

@@ -1,3 +1,5 @@
use std::path::PathBuf;
use crate::codex_tool_config::CodexToolCallParam;
use crate::codex_tool_config::create_tool_for_codex_tool_call_param;
@@ -28,15 +30,20 @@ use tokio::task;
pub(crate) struct MessageProcessor {
outgoing: mpsc::Sender<JSONRPCMessage>,
initialized: bool,
codex_linux_sandbox_exe: Option<PathBuf>,
}
impl MessageProcessor {
/// Create a new `MessageProcessor`, retaining a handle to the outgoing
/// `Sender` so handlers can enqueue messages to be written to stdout.
pub(crate) fn new(outgoing: mpsc::Sender<JSONRPCMessage>) -> Self {
pub(crate) fn new(
outgoing: mpsc::Sender<JSONRPCMessage>,
codex_linux_sandbox_exe: Option<PathBuf>,
) -> Self {
Self {
outgoing,
initialized: false,
codex_linux_sandbox_exe,
}
}
@@ -339,7 +346,7 @@ impl MessageProcessor {
let (initial_prompt, config): (String, CodexConfig) = match arguments {
Some(json_val) => match serde_json::from_value::<CodexToolCallParam>(json_val) {
Ok(tool_cfg) => match tool_cfg.into_config() {
Ok(tool_cfg) => match tool_cfg.into_config(self.codex_linux_sandbox_exe.clone()) {
Ok(cfg) => cfg,
Err(e) => {
let result = CallToolResult {

View File

@@ -10,6 +10,7 @@ use codex_core::protocol::SandboxPolicy;
use codex_core::util::is_inside_git_repo;
use log_layer::TuiLogLayer;
use std::fs::OpenOptions;
use std::path::PathBuf;
use tracing_appender::non_blocking;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
@@ -36,7 +37,7 @@ mod user_approval_widget;
pub use cli::Cli;
pub fn run_main(cli: Cli) -> std::io::Result<()> {
pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> std::io::Result<()> {
let (sandbox_policy, approval_policy) = if cli.full_auto {
(
Some(SandboxPolicy::new_full_auto_policy()),
@@ -61,6 +62,7 @@ pub fn run_main(cli: Cli) -> std::io::Result<()> {
cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)),
model_provider: None,
config_profile: cli.config_profile.clone(),
codex_linux_sandbox_exe,
};
#[allow(clippy::print_stderr)]
match Config::load_with_overrides(overrides) {

View File

@@ -1,10 +1,18 @@
use std::path::PathBuf;
use clap::Parser;
use codex_tui::Cli;
use codex_tui::run_main;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let codex_linux_sandbox_exe: Option<PathBuf> = if cfg!(target_os = "linux") {
std::env::current_exe().ok()
} else {
None
};
let cli = Cli::parse();
run_main(cli)?;
run_main(cli, codex_linux_sandbox_exe)?;
Ok(())
}

View File

@@ -1,5 +1,5 @@
use codex_core::config::Config;
use codex_core::config::UriBasedFileOpener;
use codex_core::config_types::UriBasedFileOpener;
use ratatui::text::Line;
use ratatui::text::Span;
use std::borrow::Cow;