mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
Merge branch 'main' into owen/separate_v2_namespace_take_two
This commit is contained in:
@@ -75,11 +75,13 @@ Codex CLI supports a rich set of configuration options, with preferences stored
|
||||
|
||||
- [**Getting started**](./docs/getting-started.md)
|
||||
- [CLI usage](./docs/getting-started.md#cli-usage)
|
||||
- [Slash Commands](./docs/slash_commands.md)
|
||||
- [Running with a prompt as input](./docs/getting-started.md#running-with-a-prompt-as-input)
|
||||
- [Example prompts](./docs/getting-started.md#example-prompts)
|
||||
- [Custom prompts](./docs/prompts.md)
|
||||
- [Memory with AGENTS.md](./docs/getting-started.md#memory-with-agentsmd)
|
||||
- [**Configuration**](./docs/config.md)
|
||||
- [Example config](./docs/example-config.md)
|
||||
- [**Sandbox & approvals**](./docs/sandbox.md)
|
||||
- [**Authentication**](./docs/authentication.md)
|
||||
- [Auth methods](./docs/authentication.md#forcing-a-specific-auth-method-advanced)
|
||||
|
||||
Generated
+2
@@ -994,6 +994,7 @@ dependencies = [
|
||||
"supports-color",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1471,6 +1472,7 @@ dependencies = [
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"shlex",
|
||||
"strum 0.27.2",
|
||||
"strum_macros 0.27.2",
|
||||
|
||||
@@ -31,8 +31,8 @@ use std::process::Command;
|
||||
use ts_rs::TS;
|
||||
|
||||
const HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
|
||||
type JsonSchemaEmitter = fn(&Path) -> Result<RootSchema>;
|
||||
|
||||
type JsonSchemaEmitter = fn(&Path) -> Result<RootSchema>;
|
||||
pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
||||
generate_ts(out_dir, prettier)?;
|
||||
generate_json(out_dir)?;
|
||||
|
||||
@@ -548,6 +548,11 @@ server_notification_definitions! {
|
||||
#[strum(serialize = "item/mcpToolCall/progress")]
|
||||
McpToolCallProgress(v2::McpToolCallProgressNotification),
|
||||
|
||||
#[serde(rename = "account/updated")]
|
||||
#[ts(rename = "account/updated")]
|
||||
#[strum(serialize = "account/updated")]
|
||||
AccountUpdated(v2::AccountUpdatedNotification),
|
||||
|
||||
#[serde(rename = "account/rateLimits/updated")]
|
||||
#[ts(rename = "account/rateLimits/updated")]
|
||||
#[strum(serialize = "account/rateLimits/updated")]
|
||||
|
||||
@@ -400,6 +400,7 @@ pub struct SessionConfiguredNotification {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// Deprecated notification. Use AccountUpdatedNotification instead.
|
||||
pub struct AuthStatusChangeNotification {
|
||||
pub auth_method: Option<AuthMode>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::protocol::common::AuthMode;
|
||||
use codex_protocol::ConversationId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::config_types::ReasoningEffort;
|
||||
@@ -239,9 +240,7 @@ pub struct FeedbackUploadResponse {
|
||||
}
|
||||
|
||||
// === Threads, Turns, and Items ===
|
||||
|
||||
// Thread APIs
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -657,4 +656,8 @@ impl From<codex_protocol::protocol::RateLimitWindow> for RateLimitWindow {
|
||||
resets_at: value.resets_at,
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccountUpdatedNotification {
|
||||
pub auth_method: Option<AuthMode>,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::models::supported_models;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::outgoing_message::OutgoingNotification;
|
||||
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AddConversationListenerParams;
|
||||
use codex_app_server_protocol::AddConversationSubscriptionResponse;
|
||||
use codex_app_server_protocol::ApplyPatchApprovalParams;
|
||||
@@ -248,8 +249,7 @@ impl CodexMessageProcessor {
|
||||
request_id,
|
||||
params: _,
|
||||
} => {
|
||||
self.send_unimplemented_error(request_id, "account/logout")
|
||||
.await;
|
||||
self.logout_v2(request_id).await;
|
||||
}
|
||||
ClientRequest::GetAccount {
|
||||
request_id,
|
||||
@@ -298,7 +298,7 @@ impl CodexMessageProcessor {
|
||||
request_id,
|
||||
params: _,
|
||||
} => {
|
||||
self.logout_chatgpt(request_id).await;
|
||||
self.logout_v1(request_id).await;
|
||||
}
|
||||
ClientRequest::GetAuthStatus { request_id, params } => {
|
||||
self.get_auth_status(request_id, params).await;
|
||||
@@ -542,9 +542,9 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn logout_chatgpt(&mut self, request_id: RequestId) {
|
||||
async fn logout_common(&mut self) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError> {
|
||||
// Cancel any active login attempt.
|
||||
{
|
||||
// Cancel any active login attempt.
|
||||
let mut guard = self.active_login.lock().await;
|
||||
if let Some(active) = guard.take() {
|
||||
active.drop();
|
||||
@@ -552,31 +552,61 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
|
||||
if let Err(err) = self.auth_manager.logout() {
|
||||
let error = JSONRPCErrorError {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("logout failed: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
codex_app_server_protocol::LogoutChatGptResponse {},
|
||||
)
|
||||
.await;
|
||||
// Reflect the current auth method after logout (likely None).
|
||||
Ok(self.auth_manager.auth().map(|auth| auth.mode))
|
||||
}
|
||||
|
||||
// Send auth status change notification reflecting the current auth mode
|
||||
// after logout.
|
||||
let current_auth_method = self.auth_manager.auth().map(|auth| auth.mode);
|
||||
let payload = AuthStatusChangeNotification {
|
||||
auth_method: current_auth_method,
|
||||
};
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AuthStatusChange(payload))
|
||||
.await;
|
||||
async fn logout_v1(&mut self, request_id: RequestId) {
|
||||
match self.logout_common().await {
|
||||
Ok(current_auth_method) => {
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
codex_app_server_protocol::LogoutChatGptResponse {},
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload = AuthStatusChangeNotification {
|
||||
auth_method: current_auth_method,
|
||||
};
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AuthStatusChange(payload))
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn logout_v2(&mut self, request_id: RequestId) {
|
||||
match self.logout_common().await {
|
||||
Ok(current_auth_method) => {
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
codex_app_server_protocol::LogoutAccountResponse {},
|
||||
)
|
||||
.await;
|
||||
|
||||
let payload_v2 = AccountUpdatedNotification {
|
||||
auth_method: current_auth_method,
|
||||
};
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::AccountUpdated(payload_v2))
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_auth_status(
|
||||
|
||||
@@ -141,6 +141,8 @@ pub(crate) struct OutgoingError {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::LoginChatGptCompleteNotification;
|
||||
use codex_app_server_protocol::RateLimitSnapshot;
|
||||
use codex_app_server_protocol::RateLimitWindow;
|
||||
@@ -210,4 +212,24 @@ mod tests {
|
||||
"ensure the notification serializes correctly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_account_updated_notification_serialization() {
|
||||
let notification = ServerNotification::AccountUpdated(AccountUpdatedNotification {
|
||||
auth_method: Some(AuthMode::ApiKey),
|
||||
});
|
||||
|
||||
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "account/updated",
|
||||
"params": {
|
||||
"authMethod": "apikey"
|
||||
},
|
||||
}),
|
||||
serde_json::to_value(jsonrpc_notification)
|
||||
.expect("ensure the notification serializes correctly"),
|
||||
"ensure the notification serializes correctly"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +381,11 @@ impl McpProcess {
|
||||
self.send_request("logoutChatGpt", None).await
|
||||
}
|
||||
|
||||
/// Send an `account/logout` JSON-RPC request.
|
||||
pub async fn send_logout_account_request(&mut self) -> anyhow::Result<i64> {
|
||||
self.send_request("account/logout", None).await
|
||||
}
|
||||
|
||||
/// Send a `fuzzyFileSearch` JSON-RPC request.
|
||||
pub async fn send_fuzzy_file_search_request(
|
||||
&mut self,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::GetAuthStatusParams;
|
||||
use codex_app_server_protocol::GetAuthStatusResponse;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LogoutAccountResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_core::auth::AuthCredentialsStoreMode;
|
||||
use codex_login::login_with_api_key;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
// Helper to create a minimal config.toml for the app server
|
||||
fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "http://127.0.0.1:0/v1"
|
||||
wire_api = "chat"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_account_removes_auth_and_notifies() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
login_with_api_key(
|
||||
codex_home.path(),
|
||||
"sk-test-key",
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
assert!(codex_home.path().join("auth.json").exists());
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let id = mcp.send_logout_account_request().await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(id)),
|
||||
)
|
||||
.await??;
|
||||
let _ok: LogoutAccountResponse = to_response(resp)?;
|
||||
|
||||
let note = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/updated"),
|
||||
)
|
||||
.await??;
|
||||
let parsed: ServerNotification = note.try_into()?;
|
||||
let ServerNotification::AccountUpdated(payload) = parsed else {
|
||||
bail!("unexpected notification: {parsed:?}");
|
||||
};
|
||||
assert!(
|
||||
payload.auth_method.is_none(),
|
||||
"auth_method should be None after logout"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!codex_home.path().join("auth.json").exists(),
|
||||
"auth.json should be deleted"
|
||||
);
|
||||
|
||||
let status_id = mcp
|
||||
.send_get_auth_status_request(GetAuthStatusParams {
|
||||
include_token: Some(true),
|
||||
refresh_token: Some(false),
|
||||
})
|
||||
.await?;
|
||||
let status_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(status_id)),
|
||||
)
|
||||
.await??;
|
||||
let status: GetAuthStatusResponse = to_response(status_resp)?;
|
||||
assert_eq!(status.auth_method, None);
|
||||
assert_eq!(status.auth_token, None);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod account;
|
||||
mod model_list;
|
||||
mod rate_limits;
|
||||
mod thread_archive;
|
||||
|
||||
@@ -39,6 +39,7 @@ ctor = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
supports-color = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
|
||||
@@ -124,6 +124,7 @@ async fn run_command_under_sandbox(
|
||||
let cwd_clone = cwd.clone();
|
||||
let env_map = env.clone();
|
||||
let command_vec = command.clone();
|
||||
let base_dir = config.codex_home.clone();
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
run_windows_sandbox_capture(
|
||||
policy_str,
|
||||
@@ -132,6 +133,7 @@ async fn run_command_under_sandbox(
|
||||
&cwd_clone,
|
||||
env_map,
|
||||
None,
|
||||
Some(base_dir.as_path()),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -510,15 +510,21 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
||||
Some(Subcommand::Features(FeaturesCli { sub })) => match sub {
|
||||
FeaturesSubcommand::List => {
|
||||
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
|
||||
let cli_kv_overrides = root_config_overrides
|
||||
let mut cli_kv_overrides = root_config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
// Honor `--search` via the new feature toggle.
|
||||
if interactive.web_search {
|
||||
cli_kv_overrides.push((
|
||||
"features.web_search_request".to_string(),
|
||||
toml::Value::Boolean(true),
|
||||
));
|
||||
}
|
||||
|
||||
// Thread through relevant top-level flags (at minimum, `--profile`).
|
||||
// Also honor `--search` since it maps to a feature toggle.
|
||||
let overrides = ConfigOverrides {
|
||||
config_profile: interactive.config_profile.clone(),
|
||||
tools_web_search_request: interactive.web_search.then_some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
|
||||
.context("failed to load configuration")?;
|
||||
|
||||
if !config.features.enabled(Feature::RmcpClient) {
|
||||
bail!("OAuth login is only supported when [feature].rmcp_client is true in config.toml.");
|
||||
bail!("OAuth login is only supported when [features].rmcp_client is true in config.toml.");
|
||||
}
|
||||
|
||||
let LoginArgs { name, scopes } = login_args;
|
||||
|
||||
@@ -1773,19 +1773,14 @@ pub(crate) async fn run_task(
|
||||
sess.clone_history().await.get_history_for_prompt()
|
||||
};
|
||||
|
||||
let turn_input_messages: Vec<String> = turn_input
|
||||
let turn_input_messages = turn_input
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ResponseItem::Message { content, .. } => Some(content),
|
||||
.filter_map(|item| match parse_turn_item(item) {
|
||||
Some(TurnItem::UserMessage(user_message)) => Some(user_message),
|
||||
_ => None,
|
||||
})
|
||||
.flat_map(|content| {
|
||||
content.iter().filter_map(|item| match item {
|
||||
ContentItem::OutputText { text } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.map(|user_message| user_message.message())
|
||||
.collect::<Vec<String>>();
|
||||
match run_turn(
|
||||
Arc::clone(&sess),
|
||||
Arc::clone(&turn_context),
|
||||
|
||||
@@ -516,8 +516,8 @@ fn truncate_formatted_exec_output(content: &str, total_lines: usize) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
/// Anything that is not a system message or "reasoning" message is considered
|
||||
/// an API message.
|
||||
/// API messages include every non-system item (user/assistant messages, reasoning,
|
||||
/// tool calls, tool outputs, shell calls, and web-search calls).
|
||||
fn is_api_message(message: &ResponseItem) -> bool {
|
||||
match message {
|
||||
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
||||
@@ -542,6 +542,8 @@ mod tests {
|
||||
use codex_protocol::models::LocalShellAction;
|
||||
use codex_protocol::models::LocalShellExecAction;
|
||||
use codex_protocol::models::LocalShellStatus;
|
||||
use codex_protocol::models::ReasoningItemContent;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn assistant_msg(text: &str) -> ResponseItem {
|
||||
@@ -570,10 +572,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_msg(text: &str) -> ResponseItem {
|
||||
ResponseItem::Reasoning {
|
||||
id: String::new(),
|
||||
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
content: Some(vec![ReasoningItemContent::ReasoningText {
|
||||
text: text.to_string(),
|
||||
}]),
|
||||
encrypted_content: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_non_api_messages() {
|
||||
let mut h = ConversationHistory::default();
|
||||
// System message is not an API message; Other is ignored.
|
||||
// System message is not API messages; Other is ignored.
|
||||
let system = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "system".to_string(),
|
||||
@@ -581,7 +596,8 @@ mod tests {
|
||||
text: "ignored".to_string(),
|
||||
}],
|
||||
};
|
||||
h.record_items([&system, &ResponseItem::Other]);
|
||||
let reasoning = reasoning_msg("thinking...");
|
||||
h.record_items([&system, &reasoning, &ResponseItem::Other]);
|
||||
|
||||
// User and assistant should be retained.
|
||||
let u = user_msg("hi");
|
||||
@@ -592,6 +608,16 @@ mod tests {
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![
|
||||
ResponseItem::Reasoning {
|
||||
id: String::new(),
|
||||
summary: vec![ReasoningItemReasoningSummary::SummaryText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
content: Some(vec![ReasoningItemContent::ReasoningText {
|
||||
text: "thinking...".to_string(),
|
||||
}]),
|
||||
encrypted_content: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
|
||||
@@ -171,6 +171,7 @@ async fn exec_windows_sandbox(
|
||||
params: ExecParams,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
) -> Result<RawExecToolCallOutput> {
|
||||
use crate::config::find_codex_home;
|
||||
use codex_windows_sandbox::run_windows_sandbox_capture;
|
||||
|
||||
let ExecParams {
|
||||
@@ -188,8 +189,17 @@ async fn exec_windows_sandbox(
|
||||
};
|
||||
|
||||
let sandbox_cwd = cwd.clone();
|
||||
let logs_base_dir = find_codex_home().ok();
|
||||
let spawn_res = tokio::task::spawn_blocking(move || {
|
||||
run_windows_sandbox_capture(policy_str, &sandbox_cwd, command, &cwd, env, timeout_ms)
|
||||
run_windows_sandbox_capture(
|
||||
policy_str,
|
||||
&sandbox_cwd,
|
||||
command,
|
||||
&cwd,
|
||||
env,
|
||||
timeout_ms,
|
||||
logs_base_dir.as_deref(),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ pub mod util;
|
||||
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
|
||||
pub use command_safety::is_safe_command;
|
||||
pub use safety::get_platform_sandbox;
|
||||
pub use safety::set_windows_sandbox_enabled;
|
||||
// Re-export the protocol types from the standalone `codex-protocol` crate so existing
|
||||
// `codex_core::protocol::...` references continue to work across the workspace.
|
||||
pub use codex_protocol::protocol;
|
||||
|
||||
@@ -10,6 +10,8 @@ use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExitedReviewModeEvent;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
use codex_protocol::protocol::ReasoningContentDeltaEvent;
|
||||
use codex_protocol::protocol::ReasoningRawContentDeltaEvent;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -122,7 +124,9 @@ async fn process_review_events(
|
||||
..
|
||||
})
|
||||
| EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { .. })
|
||||
| EventMsg::AgentMessageContentDelta(AgentMessageContentDeltaEvent { .. }) => {}
|
||||
| EventMsg::AgentMessageContentDelta(AgentMessageContentDeltaEvent { .. })
|
||||
| EventMsg::ReasoningContentDelta(ReasoningContentDeltaEvent { .. })
|
||||
| EventMsg::ReasoningRawContentDelta(ReasoningRawContentDeltaEvent { .. }) => {}
|
||||
EventMsg::TaskComplete(task_complete) => {
|
||||
// Parse review output from the last agent message (if present).
|
||||
let out = task_complete
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_util::either::Either;
|
||||
@@ -53,13 +54,16 @@ impl ToolCallRuntime {
|
||||
let turn = Arc::clone(&self.turn_context);
|
||||
let tracker = Arc::clone(&self.tracker);
|
||||
let lock = Arc::clone(&self.parallel_execution);
|
||||
let aborted_response = Self::aborted_response(&call);
|
||||
let started = Instant::now();
|
||||
let readiness = self.turn_context.tool_call_gate.clone();
|
||||
|
||||
let handle: AbortOnDropHandle<Result<ResponseInputItem, FunctionCallError>> =
|
||||
AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = cancellation_token.cancelled() => Ok(aborted_response),
|
||||
_ = cancellation_token.cancelled() => {
|
||||
let secs = started.elapsed().as_secs_f32().max(0.1);
|
||||
Ok(Self::aborted_response(&call, secs))
|
||||
},
|
||||
res = async {
|
||||
tracing::info!("waiting for tool gate");
|
||||
readiness.wait_ready().await;
|
||||
@@ -71,7 +75,7 @@ impl ToolCallRuntime {
|
||||
};
|
||||
|
||||
router
|
||||
.dispatch_tool_call(session, turn, tracker, call)
|
||||
.dispatch_tool_call(session, turn, tracker, call.clone())
|
||||
.await
|
||||
} => res,
|
||||
}
|
||||
@@ -91,23 +95,32 @@ impl ToolCallRuntime {
|
||||
}
|
||||
|
||||
impl ToolCallRuntime {
|
||||
fn aborted_response(call: &ToolCall) -> ResponseInputItem {
|
||||
fn aborted_response(call: &ToolCall, secs: f32) -> ResponseInputItem {
|
||||
match &call.payload {
|
||||
ToolPayload::Custom { .. } => ResponseInputItem::CustomToolCallOutput {
|
||||
call_id: call.call_id.clone(),
|
||||
output: "aborted".to_string(),
|
||||
output: Self::abort_message(call, secs),
|
||||
},
|
||||
ToolPayload::Mcp { .. } => ResponseInputItem::McpToolCallOutput {
|
||||
call_id: call.call_id.clone(),
|
||||
result: Err("aborted".to_string()),
|
||||
result: Err(Self::abort_message(call, secs)),
|
||||
},
|
||||
_ => ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call.call_id.clone(),
|
||||
output: FunctionCallOutputPayload {
|
||||
content: "aborted".to_string(),
|
||||
content: Self::abort_message(call, secs),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn abort_message(call: &ToolCall, secs: f32) -> String {
|
||||
match call.tool_name.as_str() {
|
||||
"shell" | "container.exec" | "local_shell" | "unified_exec" => {
|
||||
format!("Wall time: {secs:.1} seconds\naborted by user")
|
||||
}
|
||||
_ => format!("aborted by user after {secs:.1}s"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ pub mod fs_wait {
|
||||
if path.exists() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(anyhow!("timed out waiting for {:?}", path))
|
||||
Err(anyhow!("timed out waiting for {path:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ pub mod fs_wait {
|
||||
if let Some(found) = scan_for_match(&root, predicate) {
|
||||
Ok(found)
|
||||
} else {
|
||||
Err(anyhow!("timed out waiting for matching file in {:?}", root))
|
||||
Err(anyhow!("timed out waiting for matching file in {root:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use assert_matches::assert_matches;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -13,6 +14,7 @@ use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event_with_timeout;
|
||||
use regex_lite::Regex;
|
||||
use serde_json::json;
|
||||
|
||||
/// Integration test: spawn a long‑running shell tool via a mocked Responses SSE
|
||||
@@ -123,6 +125,7 @@ async fn interrupt_tool_records_history_entries() {
|
||||
)
|
||||
.await;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs_f32(0.1)).await;
|
||||
codex.submit(Op::Interrupt).await.unwrap();
|
||||
|
||||
wait_for_event_with_timeout(
|
||||
@@ -159,9 +162,26 @@ async fn interrupt_tool_records_history_entries() {
|
||||
response_mock.saw_function_call(call_id),
|
||||
"function call not recorded in responses payload"
|
||||
);
|
||||
assert_eq!(
|
||||
response_mock.function_call_output_text(call_id).as_deref(),
|
||||
Some("aborted"),
|
||||
"aborted function call output not recorded in responses payload"
|
||||
let output = response_mock
|
||||
.function_call_output_text(call_id)
|
||||
.expect("missing function_call_output text");
|
||||
let re = Regex::new(r"^Wall time: ([0-9]+(?:\.[0-9])?) seconds\naborted by user$")
|
||||
.expect("compile regex");
|
||||
let captures = re.captures(&output);
|
||||
assert_matches!(
|
||||
captures.as_ref(),
|
||||
Some(caps) if caps.get(1).is_some(),
|
||||
"aborted message with elapsed seconds"
|
||||
);
|
||||
let secs: f32 = captures
|
||||
.expect("aborted message with elapsed seconds")
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert!(
|
||||
secs >= 0.1,
|
||||
"expected at least one tenth of a second of elapsed time, got {secs}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::matchers::any;
|
||||
|
||||
@@ -61,6 +64,12 @@ echo -n "${@: -1}" > $(dirname "${0}")/notify.txt"#,
|
||||
|
||||
// We fork the notify script, so we need to wait for it to write to the file.
|
||||
fs_wait::wait_for_path_exists(¬ify_file, Duration::from_secs(5)).await?;
|
||||
let notify_payload_raw = tokio::fs::read_to_string(¬ify_file).await?;
|
||||
let payload: Value = serde_json::from_str(¬ify_payload_raw)?;
|
||||
|
||||
assert_eq!(payload["type"], json!("agent-turn-complete"));
|
||||
assert_eq!(payload["input-messages"], json!(["hello world"]));
|
||||
assert_eq!(payload["last-assistant-message"], json!("Done"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -159,6 +159,8 @@ pub fn run(
|
||||
.threads(num_walk_builder_threads)
|
||||
// Allow hidden entries.
|
||||
.hidden(false)
|
||||
// Follow symlinks to search their contents.
|
||||
.follow_links(true)
|
||||
// Don't require git to be present to apply to apply git-related ignore rules.
|
||||
.require_git(false);
|
||||
if !respect_gitignore {
|
||||
|
||||
@@ -812,12 +812,8 @@ impl TokenUsage {
|
||||
(self.non_cached_input() + self.output_tokens.max(0)).max(0)
|
||||
}
|
||||
|
||||
/// For estimating what % of the model's context window is used, we need to account
|
||||
/// for reasoning output tokens from prior turns being dropped from the context window.
|
||||
/// We approximate this here by subtracting reasoning output tokens from the total.
|
||||
/// This will be off for the current turn and pending function calls.
|
||||
pub fn tokens_in_context_window(&self) -> i64 {
|
||||
(self.total_tokens - self.reasoning_output_tokens).max(0)
|
||||
self.total_tokens
|
||||
}
|
||||
|
||||
/// Estimate the remaining user-controllable percentage of the model's context window.
|
||||
|
||||
@@ -40,12 +40,23 @@ curl --fail --silent --show-error "${PROXY_BASE_URL}/shutdown"
|
||||
## CLI
|
||||
|
||||
```
|
||||
codex-responses-api-proxy [--port <PORT>] [--server-info <FILE>] [--http-shutdown]
|
||||
codex-responses-api-proxy [--port <PORT>] [--server-info <FILE>] [--http-shutdown] [--upstream-url <URL>]
|
||||
```
|
||||
|
||||
- `--port <PORT>`: Port to bind on `127.0.0.1`. If omitted, an ephemeral port is chosen.
|
||||
- `--server-info <FILE>`: If set, the proxy writes a single line of JSON with `{ "port": <PORT>, "pid": <PID> }` once listening.
|
||||
- `--http-shutdown`: If set, enables `GET /shutdown` to exit the process with code `0`.
|
||||
- `--upstream-url <URL>`: Absolute URL to forward requests to. Defaults to `https://api.openai.com/v1/responses`.
|
||||
- Authentication is fixed to `Authorization: Bearer <key>` to match the Codex CLI expectations.
|
||||
|
||||
For Azure, for example (ensure your deployment accepts `Authorization: Bearer <key>`):
|
||||
|
||||
```shell
|
||||
printenv AZURE_OPENAI_API_KEY | env -u AZURE_OPENAI_API_KEY codex-responses-api-proxy \
|
||||
--http-shutdown \
|
||||
--server-info /tmp/server-info.json \
|
||||
--upstream-url "https://YOUR_PROJECT_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT/responses?api-version=2025-04-01-preview"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -57,7 +68,7 @@ codex-responses-api-proxy [--port <PORT>] [--server-info <FILE>] [--http-shutdow
|
||||
Care is taken to restrict access/copying to the value of `OPENAI_API_KEY` retained in memory:
|
||||
|
||||
- We leverage [`codex_process_hardening`](https://github.com/openai/codex/blob/main/codex-rs/process-hardening/README.md) so `codex-responses-api-proxy` is run with standard process-hardening techniques.
|
||||
- At startup, we allocate a `1024` byte buffer on the stack and write `"Bearer "` as the first `7` bytes.
|
||||
- At startup, we allocate a `1024` byte buffer on the stack and copy `"Bearer "` into the start of the buffer.
|
||||
- We then read from `stdin`, copying the contents into the buffer after `"Bearer "`.
|
||||
- After verifying the key matches `/^[a-zA-Z0-9_-]+$/` (and does not exceed the buffer), we create a `String` from that buffer (so the data is now on the heap).
|
||||
- We zero out the stack-allocated buffer using https://crates.io/crates/zeroize so it is not optimized away by the compiler.
|
||||
|
||||
@@ -12,6 +12,7 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use reqwest::Url;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use reqwest::header::HOST;
|
||||
@@ -44,6 +45,10 @@ pub struct Args {
|
||||
/// Enable HTTP shutdown endpoint at GET /shutdown
|
||||
#[arg(long)]
|
||||
pub http_shutdown: bool,
|
||||
|
||||
/// Absolute URL the proxy should forward requests to (defaults to OpenAI).
|
||||
#[arg(long, default_value = "https://api.openai.com/v1/responses")]
|
||||
pub upstream_url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -52,10 +57,29 @@ struct ServerInfo {
|
||||
pid: u32,
|
||||
}
|
||||
|
||||
struct ForwardConfig {
|
||||
upstream_url: Url,
|
||||
host_header: HeaderValue,
|
||||
}
|
||||
|
||||
/// Entry point for the library main, for parity with other crates.
|
||||
pub fn run_main(args: Args) -> Result<()> {
|
||||
let auth_header = read_auth_header_from_stdin()?;
|
||||
|
||||
let upstream_url = Url::parse(&args.upstream_url).context("parsing --upstream-url")?;
|
||||
let host = match (upstream_url.host_str(), upstream_url.port()) {
|
||||
(Some(host), Some(port)) => format!("{host}:{port}"),
|
||||
(Some(host), None) => host.to_string(),
|
||||
_ => return Err(anyhow!("upstream URL must include a host")),
|
||||
};
|
||||
let host_header =
|
||||
HeaderValue::from_str(&host).context("constructing Host header from upstream URL")?;
|
||||
|
||||
let forward_config = Arc::new(ForwardConfig {
|
||||
upstream_url,
|
||||
host_header,
|
||||
});
|
||||
|
||||
let (listener, bound_addr) = bind_listener(args.port)?;
|
||||
if let Some(path) = args.server_info.as_ref() {
|
||||
write_server_info(path, bound_addr.port())?;
|
||||
@@ -75,13 +99,14 @@ pub fn run_main(args: Args) -> Result<()> {
|
||||
let http_shutdown = args.http_shutdown;
|
||||
for request in server.incoming_requests() {
|
||||
let client = client.clone();
|
||||
let forward_config = forward_config.clone();
|
||||
std::thread::spawn(move || {
|
||||
if http_shutdown && request.method() == &Method::Get && request.url() == "/shutdown" {
|
||||
let _ = request.respond(Response::new_empty(StatusCode(200)));
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if let Err(e) = forward_request(&client, auth_header, request) {
|
||||
if let Err(e) = forward_request(&client, auth_header, &forward_config, request) {
|
||||
eprintln!("forwarding error: {e}");
|
||||
}
|
||||
});
|
||||
@@ -115,7 +140,12 @@ fn write_server_info(path: &Path, port: u16) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn forward_request(client: &Client, auth_header: &'static str, mut req: Request) -> Result<()> {
|
||||
fn forward_request(
|
||||
client: &Client,
|
||||
auth_header: &'static str,
|
||||
config: &ForwardConfig,
|
||||
mut req: Request,
|
||||
) -> Result<()> {
|
||||
// Only allow POST /v1/responses exactly, no query string.
|
||||
let method = req.method().clone();
|
||||
let url_path = req.url().to_string();
|
||||
@@ -157,11 +187,10 @@ fn forward_request(client: &Client, auth_header: &'static str, mut req: Request)
|
||||
auth_header_value.set_sensitive(true);
|
||||
headers.insert(AUTHORIZATION, auth_header_value);
|
||||
|
||||
headers.insert(HOST, HeaderValue::from_static("api.openai.com"));
|
||||
headers.insert(HOST, config.host_header.clone());
|
||||
|
||||
let upstream = "https://api.openai.com/v1/responses";
|
||||
let upstream_resp = client
|
||||
.post(upstream)
|
||||
.post(config.upstream_url.clone())
|
||||
.headers(headers)
|
||||
.body(body)
|
||||
.send()
|
||||
|
||||
@@ -121,7 +121,7 @@ where
|
||||
if total_read == capacity && !saw_newline && !saw_eof {
|
||||
buf.zeroize();
|
||||
return Err(anyhow!(
|
||||
"OPENAI_API_KEY is too large to fit in the 512-byte buffer"
|
||||
"API key is too large to fit in the {BUFFER_SIZE}-byte buffer"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ where
|
||||
if total == AUTH_HEADER_PREFIX.len() {
|
||||
buf.zeroize();
|
||||
return Err(anyhow!(
|
||||
"OPENAI_API_KEY must be provided via stdin (e.g. printenv OPENAI_API_KEY | codex responses-api-proxy)"
|
||||
"API key must be provided via stdin (e.g. printenv OPENAI_API_KEY | codex responses-api-proxy)"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ fn validate_auth_header_bytes(key_bytes: &[u8]) -> Result<()> {
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"OPENAI_API_KEY may only contain ASCII letters, numbers, '-' or '_'"
|
||||
"API key may only contain ASCII letters, numbers, '-' or '_'"
|
||||
))
|
||||
}
|
||||
|
||||
@@ -290,7 +290,9 @@ mod tests {
|
||||
})
|
||||
.unwrap_err();
|
||||
let message = format!("{err:#}");
|
||||
assert!(message.contains("OPENAI_API_KEY is too large to fit in the 512-byte buffer"));
|
||||
let expected_error =
|
||||
format!("API key is too large to fit in the {BUFFER_SIZE}-byte buffer");
|
||||
assert!(message.contains(&expected_error));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -317,9 +319,7 @@ mod tests {
|
||||
.unwrap_err();
|
||||
|
||||
let message = format!("{err:#}");
|
||||
assert!(
|
||||
message.contains("OPENAI_API_KEY may only contain ASCII letters, numbers, '-' or '_'")
|
||||
);
|
||||
assert!(message.contains("API key may only contain ASCII letters, numbers, '-' or '_'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -337,8 +337,6 @@ mod tests {
|
||||
.unwrap_err();
|
||||
|
||||
let message = format!("{err:#}");
|
||||
assert!(
|
||||
message.contains("OPENAI_API_KEY may only contain ASCII letters, numbers, '-' or '_'")
|
||||
);
|
||||
assert!(message.contains("API key may only contain ASCII letters, numbers, '-' or '_'"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,3 +106,4 @@ insta = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
vt100 = { workspace = true }
|
||||
serial_test = { workspace = true }
|
||||
|
||||
@@ -14,6 +14,12 @@ use textwrap::Options;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?";
|
||||
|
||||
fn is_word_separator(ch: char) -> bool {
|
||||
WORD_SEPARATORS.contains(ch)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TextElement {
|
||||
range: Range<usize>,
|
||||
@@ -841,21 +847,23 @@ impl TextArea {
|
||||
|
||||
pub(crate) fn beginning_of_previous_word(&self) -> usize {
|
||||
let prefix = &self.text[..self.cursor_pos];
|
||||
let Some((first_non_ws_idx, _)) = prefix
|
||||
let Some((first_non_ws_idx, ch)) = prefix
|
||||
.char_indices()
|
||||
.rev()
|
||||
.find(|&(_, ch)| !ch.is_whitespace())
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
let before = &prefix[..first_non_ws_idx];
|
||||
let candidate = before
|
||||
.char_indices()
|
||||
.rev()
|
||||
.find(|&(_, ch)| ch.is_whitespace())
|
||||
.map(|(idx, ch)| idx + ch.len_utf8())
|
||||
.unwrap_or(0);
|
||||
self.adjust_pos_out_of_elements(candidate, true)
|
||||
let is_separator = is_word_separator(ch);
|
||||
let mut start = first_non_ws_idx;
|
||||
for (idx, ch) in prefix[..first_non_ws_idx].char_indices().rev() {
|
||||
if ch.is_whitespace() || is_word_separator(ch) != is_separator {
|
||||
start = idx + ch.len_utf8();
|
||||
break;
|
||||
}
|
||||
start = idx;
|
||||
}
|
||||
self.adjust_pos_out_of_elements(start, true)
|
||||
}
|
||||
|
||||
pub(crate) fn end_of_next_word(&self) -> usize {
|
||||
@@ -864,11 +872,19 @@ impl TextArea {
|
||||
return self.text.len();
|
||||
};
|
||||
let word_start = self.cursor_pos + first_non_ws;
|
||||
let candidate = match self.text[word_start..].find(|c: char| c.is_whitespace()) {
|
||||
Some(rel_idx) => word_start + rel_idx,
|
||||
None => self.text.len(),
|
||||
let mut iter = self.text[word_start..].char_indices();
|
||||
let Some((_, first_ch)) = iter.next() else {
|
||||
return word_start;
|
||||
};
|
||||
self.adjust_pos_out_of_elements(candidate, false)
|
||||
let is_separator = is_word_separator(first_ch);
|
||||
let mut end = self.text.len();
|
||||
for (idx, ch) in iter {
|
||||
if ch.is_whitespace() || is_word_separator(ch) != is_separator {
|
||||
end = word_start + idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.adjust_pos_out_of_elements(end, false)
|
||||
}
|
||||
|
||||
fn adjust_pos_out_of_elements(&self, pos: usize, prefer_start: bool) -> usize {
|
||||
@@ -1239,6 +1255,56 @@ mod tests {
|
||||
assert_eq!(t.cursor(), elem_range.start);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_backward_word_respects_word_separators() {
|
||||
let mut t = ta_with("path/to/file");
|
||||
t.set_cursor(t.text().len());
|
||||
t.delete_backward_word();
|
||||
assert_eq!(t.text(), "path/to/");
|
||||
assert_eq!(t.cursor(), t.text().len());
|
||||
|
||||
t.delete_backward_word();
|
||||
assert_eq!(t.text(), "path/to");
|
||||
assert_eq!(t.cursor(), t.text().len());
|
||||
|
||||
let mut t = ta_with("foo/ ");
|
||||
t.set_cursor(t.text().len());
|
||||
t.delete_backward_word();
|
||||
assert_eq!(t.text(), "foo");
|
||||
assert_eq!(t.cursor(), 3);
|
||||
|
||||
let mut t = ta_with("foo /");
|
||||
t.set_cursor(t.text().len());
|
||||
t.delete_backward_word();
|
||||
assert_eq!(t.text(), "foo ");
|
||||
assert_eq!(t.cursor(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_forward_word_respects_word_separators() {
|
||||
let mut t = ta_with("path/to/file");
|
||||
t.set_cursor(0);
|
||||
t.delete_forward_word();
|
||||
assert_eq!(t.text(), "/to/file");
|
||||
assert_eq!(t.cursor(), 0);
|
||||
|
||||
t.delete_forward_word();
|
||||
assert_eq!(t.text(), "to/file");
|
||||
assert_eq!(t.cursor(), 0);
|
||||
|
||||
let mut t = ta_with("/ foo");
|
||||
t.set_cursor(0);
|
||||
t.delete_forward_word();
|
||||
assert_eq!(t.text(), " foo");
|
||||
assert_eq!(t.cursor(), 0);
|
||||
|
||||
let mut t = ta_with(" /foo");
|
||||
t.set_cursor(0);
|
||||
t.delete_forward_word();
|
||||
assert_eq!(t.text(), "foo");
|
||||
assert_eq!(t.cursor(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yank_restores_last_kill() {
|
||||
let mut t = ta_with("hello");
|
||||
|
||||
+44
-5
@@ -16,6 +16,7 @@ use codex_core::auth::enforce_login_restrictions;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::find_conversation_path_by_id_str;
|
||||
use codex_core::get_platform_sandbox;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_ollama::DEFAULT_OSS_MODEL;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
@@ -93,7 +94,7 @@ use std::io::Write as _;
|
||||
// (tests access modules directly within the crate)
|
||||
|
||||
pub async fn run_main(
|
||||
cli: Cli,
|
||||
mut cli: Cli,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
) -> std::io::Result<AppExitInfo> {
|
||||
let (sandbox_mode, approval_policy) = if cli.full_auto {
|
||||
@@ -113,6 +114,13 @@ pub async fn run_main(
|
||||
)
|
||||
};
|
||||
|
||||
// Map the legacy --search flag to the new feature toggle.
|
||||
if cli.web_search {
|
||||
cli.config_overrides
|
||||
.raw_overrides
|
||||
.push("features.web_search_request=true".to_string());
|
||||
}
|
||||
|
||||
// When using `--oss`, let the bootstrapper pick the model (defaulting to
|
||||
// gpt-oss:20b) and ensure it is present locally. Also, force the built‑in
|
||||
// `oss` model provider.
|
||||
@@ -148,7 +156,7 @@ pub async fn run_main(
|
||||
compact_prompt: None,
|
||||
include_apply_patch_tool: None,
|
||||
show_raw_agent_reasoning: cli.oss.then_some(true),
|
||||
tools_web_search_request: cli.web_search.then_some(true),
|
||||
tools_web_search_request: None,
|
||||
experimental_sandbox_command_assessment: None,
|
||||
additional_writable_roots: additional_dirs,
|
||||
};
|
||||
@@ -512,8 +520,8 @@ async fn load_config_or_exit(
|
||||
/// or if the current cwd project is already trusted. If not, we need to
|
||||
/// show the trust screen.
|
||||
fn should_show_trust_screen(config: &Config) -> bool {
|
||||
if cfg!(target_os = "windows") {
|
||||
// Native Windows cannot enforce sandboxed write access without WSL; skip the trust prompt entirely.
|
||||
if cfg!(target_os = "windows") && get_platform_sandbox().is_none() {
|
||||
// If the experimental sandbox is not enabled, Native Windows cannot enforce sandboxed write access without WSL; skip the trust prompt entirely.
|
||||
return false;
|
||||
}
|
||||
if config.did_user_set_custom_approval_policy_or_sandbox_mode {
|
||||
@@ -557,10 +565,13 @@ mod tests {
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::ConfigToml;
|
||||
use codex_core::config::ProjectConfig;
|
||||
use codex_core::set_windows_sandbox_enabled;
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn windows_skips_trust_prompt() -> std::io::Result<()> {
|
||||
#[serial]
|
||||
fn windows_skips_trust_prompt_without_sandbox() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
@@ -569,6 +580,7 @@ mod tests {
|
||||
)?;
|
||||
config.did_user_set_custom_approval_policy_or_sandbox_mode = false;
|
||||
config.active_project = ProjectConfig { trust_level: None };
|
||||
set_windows_sandbox_enabled(false);
|
||||
|
||||
let should_show = should_show_trust_screen(&config);
|
||||
if cfg!(target_os = "windows") {
|
||||
@@ -584,4 +596,31 @@ mod tests {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
#[serial]
|
||||
fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
temp_dir.path().to_path_buf(),
|
||||
)?;
|
||||
config.did_user_set_custom_approval_policy_or_sandbox_mode = false;
|
||||
config.active_project = ProjectConfig { trust_level: None };
|
||||
set_windows_sandbox_enabled(true);
|
||||
|
||||
let should_show = should_show_trust_screen(&config);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
should_show,
|
||||
"Windows trust prompt should be shown on native Windows with sandbox enabled"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
should_show,
|
||||
"Non-Windows should still show trust prompt when project is untrusted"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ expression: sanitized
|
||||
│ Agents.md: <none> │
|
||||
│ │
|
||||
│ Token usage: 1.9K total (1K input + 900 output) │
|
||||
│ Context window: 100% left (2.1K used / 272K) │
|
||||
│ Context window: 100% left (2.25K used / 272K) │
|
||||
│ 5h limit: [███████████████░░░░░] 72% used (resets 03:14) │
|
||||
│ Weekly limit: [█████████░░░░░░░░░░░] 45% used (resets 03:24) │
|
||||
╰─────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ expression: sanitized
|
||||
│ Agents.md: <none> │
|
||||
│ │
|
||||
│ Token usage: 1.9K total (1K input + 900 output) │
|
||||
│ Context window: 100% left (2.1K used / 272K) │
|
||||
│ Context window: 100% left (2.25K used / 272K) │
|
||||
│ 5h limit: [███████████████░░░░░] 72% used (resets 03:14) │
|
||||
│ Weekly limit: [████████░░░░░░░░░░░░] 40% used (resets 03:34) │
|
||||
│ Warning: limits may be stale - start new turn to refresh. │
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ expression: sanitized
|
||||
│ Agents.md: <none> │
|
||||
│ │
|
||||
│ Token usage: 1.9K total (1K input + │
|
||||
│ Context window: 100% left (2.1K used / │
|
||||
│ Context window: 100% left (2.25K used / │
|
||||
│ 5h limit: [███████████████░░░░░] │
|
||||
│ (resets 03:14) │
|
||||
╰────────────────────────────────────────────╯
|
||||
|
||||
@@ -182,6 +182,7 @@ mod windows_impl {
|
||||
cwd: &Path,
|
||||
mut env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
logs_base_dir: Option<&Path>,
|
||||
) -> Result<CaptureResult> {
|
||||
let policy = SandboxPolicy::parse(policy_json_or_preset)?;
|
||||
normalize_null_device_env(&mut env_map);
|
||||
@@ -191,7 +192,7 @@ mod windows_impl {
|
||||
let current_dir = cwd.to_path_buf();
|
||||
// for now, don't fail if we detect world-writable directories
|
||||
// audit::audit_everyone_writable(¤t_dir, &env_map)?;
|
||||
log_start(&command);
|
||||
log_start(&command, logs_base_dir);
|
||||
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
|
||||
match &policy.0 {
|
||||
SandboxMode::ReadOnly => {
|
||||
@@ -295,7 +296,7 @@ mod windows_impl {
|
||||
env_block.len(),
|
||||
si.dwFlags,
|
||||
);
|
||||
debug_log(&dbg);
|
||||
debug_log(&dbg, logs_base_dir);
|
||||
unsafe {
|
||||
CloseHandle(in_r);
|
||||
CloseHandle(in_w);
|
||||
@@ -395,9 +396,9 @@ mod windows_impl {
|
||||
};
|
||||
|
||||
if exit_code == 0 {
|
||||
log_success(&command);
|
||||
log_success(&command, logs_base_dir);
|
||||
} else {
|
||||
log_failure(&command, &format!("exit code {}", exit_code));
|
||||
log_failure(&command, &format!("exit code {}", exit_code), logs_base_dir);
|
||||
}
|
||||
|
||||
if !persist_aces {
|
||||
@@ -446,6 +447,7 @@ mod stub {
|
||||
_cwd: &Path,
|
||||
_env_map: HashMap<String, String>,
|
||||
_timeout_ms: Option<u64>,
|
||||
_logs_base_dir: Option<&Path>,
|
||||
) -> Result<CaptureResult> {
|
||||
bail!("Windows sandbox is only available on Windows")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const LOG_COMMAND_PREVIEW_LIMIT: usize = 200;
|
||||
pub const LOG_FILE_NAME: &str = "sandbox_commands.rust.log";
|
||||
@@ -13,35 +15,43 @@ fn preview(command: &[String]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn append_line(line: &str) {
|
||||
if let Ok(mut f) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(LOG_FILE_NAME)
|
||||
{
|
||||
let _ = writeln!(f, "{}", line);
|
||||
fn log_file_path(base_dir: &Path) -> Option<PathBuf> {
|
||||
if base_dir.is_dir() {
|
||||
Some(base_dir.join(LOG_FILE_NAME))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_start(command: &[String]) {
|
||||
let p = preview(command);
|
||||
append_line(&format!("START: {}", p));
|
||||
fn append_line(line: &str, base_dir: Option<&Path>) {
|
||||
if let Some(dir) = base_dir {
|
||||
if let Some(path) = log_file_path(dir) {
|
||||
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let _ = writeln!(f, "{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_success(command: &[String]) {
|
||||
pub fn log_start(command: &[String], base_dir: Option<&Path>) {
|
||||
let p = preview(command);
|
||||
append_line(&format!("SUCCESS: {}", p));
|
||||
append_line(&format!("START: {p}"), base_dir);
|
||||
}
|
||||
|
||||
pub fn log_failure(command: &[String], detail: &str) {
|
||||
pub fn log_success(command: &[String], base_dir: Option<&Path>) {
|
||||
let p = preview(command);
|
||||
append_line(&format!("FAILURE: {} ({})", p, detail));
|
||||
append_line(&format!("SUCCESS: {p}"), base_dir);
|
||||
}
|
||||
|
||||
pub fn log_failure(command: &[String], detail: &str, base_dir: Option<&Path>) {
|
||||
let p = preview(command);
|
||||
append_line(&format!("FAILURE: {p} ({detail})"), base_dir);
|
||||
}
|
||||
|
||||
// Debug logging helper. Emits only when SBX_DEBUG=1 to avoid noisy logs.
|
||||
pub fn debug_log(msg: &str) {
|
||||
pub fn debug_log(msg: &str, base_dir: Option<&Path>) {
|
||||
if std::env::var("SBX_DEBUG").ok().as_deref() == Some("1") {
|
||||
append_line(&format!("DEBUG: {}", msg));
|
||||
eprintln!("{}", msg);
|
||||
append_line(&format!("DEBUG: {msg}"), base_dir);
|
||||
eprintln!("{msg}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ pub unsafe fn create_process_as_user(
|
||||
argv: &[String],
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
logs_base_dir: Option<&Path>,
|
||||
) -> Result<(PROCESS_INFORMATION, STARTUPINFOW)> {
|
||||
let cmdline_str = argv
|
||||
.iter()
|
||||
@@ -142,7 +143,7 @@ pub unsafe fn create_process_as_user(
|
||||
env_block.len(),
|
||||
si.dwFlags,
|
||||
);
|
||||
logging::debug_log(&msg);
|
||||
logging::debug_log(&msg, logs_base_dir);
|
||||
return Err(anyhow!("CreateProcessAsUserW failed: {}", err));
|
||||
}
|
||||
Ok((pi, si))
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
# Example config.toml
|
||||
|
||||
Use this example configuration as a starting point. For an explanation of each field and additional context, see [Configuration](./config.md). Copy the snippet below to `~/.codex/config.toml` and adjust values as needed.
|
||||
|
||||
```toml
|
||||
# Codex example configuration (config.toml)
|
||||
#
|
||||
# This file lists all keys Codex reads from config.toml, their default values,
|
||||
# and concise explanations. Values here mirror the effective defaults compiled
|
||||
# into the CLI. Adjust as needed.
|
||||
#
|
||||
# Notes
|
||||
# - Root keys must appear before tables in TOML.
|
||||
# - Optional keys that default to "unset" are shown commented out with notes.
|
||||
# - MCP servers, profiles, and model providers are examples; remove or edit.
|
||||
|
||||
################################################################################
|
||||
# Core Model Selection
|
||||
################################################################################
|
||||
|
||||
# Primary model used by Codex. Default differs by OS; non-Windows defaults here.
|
||||
# Linux/macOS default: "gpt-5-codex"; Windows default: "gpt-5".
|
||||
model = "gpt-5-codex"
|
||||
|
||||
# Model used by the /review feature (code reviews). Default: "gpt-5-codex".
|
||||
review_model = "gpt-5-codex"
|
||||
|
||||
# Provider id selected from [model_providers]. Default: "openai".
|
||||
model_provider = "openai"
|
||||
|
||||
# Optional manual model metadata. When unset, Codex auto-detects from model.
|
||||
# Uncomment to force values.
|
||||
# model_context_window = 128000 # tokens; default: auto for model
|
||||
# model_max_output_tokens = 8192 # tokens; default: auto for model
|
||||
# model_auto_compact_token_limit = 0 # disable/override auto; default: model family specific
|
||||
|
||||
################################################################################
|
||||
# Reasoning & Verbosity (Responses API capable models)
|
||||
################################################################################
|
||||
|
||||
# Reasoning effort: minimal | low | medium | high (default: medium)
|
||||
model_reasoning_effort = "medium"
|
||||
|
||||
# Reasoning summary: auto | concise | detailed | none (default: auto)
|
||||
model_reasoning_summary = "auto"
|
||||
|
||||
# Text verbosity for GPT-5 family (Responses API): low | medium | high (default: medium)
|
||||
model_verbosity = "medium"
|
||||
|
||||
# Force-enable reasoning summaries for current model (default: false)
|
||||
model_supports_reasoning_summaries = false
|
||||
|
||||
# Force reasoning summary format: none | experimental (default: none)
|
||||
model_reasoning_summary_format = "none"
|
||||
|
||||
################################################################################
|
||||
# Instruction Overrides
|
||||
################################################################################
|
||||
|
||||
# Additional user instructions appended after AGENTS.md. Default: unset.
|
||||
# developer_instructions = ""
|
||||
|
||||
# Optional legacy base instructions override (prefer AGENTS.md). Default: unset.
|
||||
# instructions = ""
|
||||
|
||||
# Inline override for the history compaction prompt. Default: unset.
|
||||
# compact_prompt = ""
|
||||
|
||||
# Override built-in base instructions with a file path. Default: unset.
|
||||
# experimental_instructions_file = "/absolute/or/relative/path/to/instructions.txt"
|
||||
|
||||
# Load the compact prompt override from a file. Default: unset.
|
||||
# experimental_compact_prompt_file = "/absolute/or/relative/path/to/compact_prompt.txt"
|
||||
|
||||
################################################################################
|
||||
# Approval & Sandbox
|
||||
################################################################################
|
||||
|
||||
# When to ask for command approval:
|
||||
# - untrusted: only known-safe read-only commands auto-run; others prompt
|
||||
# - on-failure: auto-run in sandbox; prompt only on failure for escalation
|
||||
# - on-request: model decides when to ask (default)
|
||||
# - never: never prompt (risky)
|
||||
approval_policy = "on-request"
|
||||
|
||||
# Filesystem/network sandbox policy for tool calls:
|
||||
# - read-only (default)
|
||||
# - workspace-write
|
||||
# - danger-full-access (no sandbox; extremely risky)
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
# Extra settings used only when sandbox_mode = "workspace-write".
|
||||
[sandbox_workspace_write]
|
||||
# Additional writable roots beyond the workspace (cwd). Default: []
|
||||
writable_roots = []
|
||||
# Allow outbound network access inside the sandbox. Default: false
|
||||
network_access = false
|
||||
# Exclude $TMPDIR from writable roots. Default: false
|
||||
exclude_tmpdir_env_var = false
|
||||
# Exclude /tmp from writable roots. Default: false
|
||||
exclude_slash_tmp = false
|
||||
|
||||
################################################################################
|
||||
# Shell Environment Policy for spawned processes
|
||||
################################################################################
|
||||
|
||||
[shell_environment_policy]
|
||||
# inherit: all (default) | core | none
|
||||
inherit = "all"
|
||||
# Skip default excludes for names containing KEY/TOKEN (case-insensitive). Default: false
|
||||
ignore_default_excludes = false
|
||||
# Case-insensitive glob patterns to remove (e.g., "AWS_*", "AZURE_*"). Default: []
|
||||
exclude = []
|
||||
# Explicit key/value overrides (always win). Default: {}
|
||||
set = {}
|
||||
# Whitelist; if non-empty, keep only matching vars. Default: []
|
||||
include_only = []
|
||||
# Experimental: run via user shell profile. Default: false
|
||||
experimental_use_profile = false
|
||||
|
||||
################################################################################
|
||||
# History & File Opener
|
||||
################################################################################
|
||||
|
||||
[history]
|
||||
# save-all (default) | none
|
||||
persistence = "save-all"
|
||||
# Maximum bytes for history file (currently not enforced). Example: 5242880
|
||||
# max_bytes = 0
|
||||
|
||||
# URI scheme for clickable citations: vscode (default) | vscode-insiders | windsurf | cursor | none
|
||||
file_opener = "vscode"
|
||||
|
||||
################################################################################
|
||||
# UI, Notifications, and Misc
|
||||
################################################################################
|
||||
|
||||
[tui]
|
||||
# Desktop notifications from the TUI: boolean or filtered list. Default: false
|
||||
# Examples: true | ["agent-turn-complete", "approval-requested"]
|
||||
notifications = false
|
||||
|
||||
# Suppress internal reasoning events from output (default: false)
|
||||
hide_agent_reasoning = false
|
||||
|
||||
# Show raw reasoning content when available (default: false)
|
||||
show_raw_agent_reasoning = false
|
||||
|
||||
# Disable burst-paste detection in the TUI (default: false)
|
||||
disable_paste_burst = false
|
||||
|
||||
# Track Windows onboarding acknowledgement (Windows only). Default: false
|
||||
windows_wsl_setup_acknowledged = false
|
||||
|
||||
# External notifier program (argv array). When unset: disabled.
|
||||
# Example: notify = ["notify-send", "Codex"]
|
||||
# notify = [ ]
|
||||
|
||||
# In-product notices (mostly set automatically by Codex).
|
||||
[notice]
|
||||
# hide_full_access_warning = true
|
||||
|
||||
################################################################################
|
||||
# Authentication & Login
|
||||
################################################################################
|
||||
|
||||
# Where to persist CLI login credentials: file (default) | keyring | auto
|
||||
cli_auth_credentials_store = "file"
|
||||
|
||||
# Base URL for ChatGPT auth flow (not OpenAI API). Default:
|
||||
chatgpt_base_url = "https://chatgpt.com/backend-api/"
|
||||
|
||||
# Restrict ChatGPT login to a specific workspace id. Default: unset.
|
||||
# forced_chatgpt_workspace_id = ""
|
||||
|
||||
# Force login mechanism when Codex would normally auto-select. Default: unset.
|
||||
# Allowed values: chatgpt | api
|
||||
# forced_login_method = "chatgpt"
|
||||
|
||||
################################################################################
|
||||
# Project Documentation Controls
|
||||
################################################################################
|
||||
|
||||
# Max bytes from AGENTS.md to embed into first-turn instructions. Default: 32768
|
||||
project_doc_max_bytes = 32768
|
||||
|
||||
# Ordered fallbacks when AGENTS.md is missing at a directory level. Default: []
|
||||
project_doc_fallback_filenames = []
|
||||
|
||||
################################################################################
|
||||
# Tools (legacy toggles kept for compatibility)
|
||||
################################################################################
|
||||
|
||||
[tools]
|
||||
# Enable web search tool (alias: web_search_request). Default: false
|
||||
web_search = false
|
||||
|
||||
# Enable the view_image tool so the agent can attach local images. Default: true
|
||||
view_image = true
|
||||
|
||||
# (Alias accepted) You can also write:
|
||||
# web_search_request = false
|
||||
|
||||
################################################################################
|
||||
# Centralized Feature Flags (preferred)
|
||||
################################################################################
|
||||
|
||||
[features]
|
||||
# Leave this table empty to accept defaults. Set explicit booleans to opt in/out.
|
||||
unified_exec = false
|
||||
streamable_shell = false
|
||||
rmcp_client = false
|
||||
apply_patch_freeform = false
|
||||
view_image_tool = true
|
||||
web_search_request = false
|
||||
experimental_sandbox_command_assessment = false
|
||||
ghost_commit = false
|
||||
enable_experimental_windows_sandbox = false
|
||||
|
||||
################################################################################
|
||||
# Experimental toggles (legacy; prefer [features])
|
||||
################################################################################
|
||||
|
||||
# Use experimental exec command tool (streamable shell). Default: false
|
||||
experimental_use_exec_command_tool = false
|
||||
|
||||
# Use experimental unified exec tool. Default: false
|
||||
experimental_use_unified_exec_tool = false
|
||||
|
||||
# Use experimental Rust MCP client (enables OAuth for HTTP MCP). Default: false
|
||||
experimental_use_rmcp_client = false
|
||||
|
||||
# Include apply_patch via freeform editing path (affects default tool set). Default: false
|
||||
experimental_use_freeform_apply_patch = false
|
||||
|
||||
# Enable model-based sandbox command assessment. Default: false
|
||||
experimental_sandbox_command_assessment = false
|
||||
|
||||
################################################################################
|
||||
# MCP (Model Context Protocol) servers
|
||||
################################################################################
|
||||
|
||||
# Preferred store for MCP OAuth credentials: auto (default) | file | keyring
|
||||
mcp_oauth_credentials_store = "auto"
|
||||
|
||||
# Define MCP servers under this table. Leave empty to disable.
|
||||
[mcp_servers]
|
||||
|
||||
# --- Example: STDIO transport ---
|
||||
# [mcp_servers.docs]
|
||||
# command = "docs-server" # required
|
||||
# args = ["--port", "4000"] # optional
|
||||
# env = { "API_KEY" = "value" } # optional key/value pairs copied as-is
|
||||
# env_vars = ["ANOTHER_SECRET"] # optional: forward these from the parent env
|
||||
# cwd = "/path/to/server" # optional working directory override
|
||||
# startup_timeout_sec = 10.0 # optional; default 10.0 seconds
|
||||
# # startup_timeout_ms = 10000 # optional alias for startup timeout (milliseconds)
|
||||
# tool_timeout_sec = 60.0 # optional; default 60.0 seconds
|
||||
# enabled_tools = ["search", "summarize"] # optional allow-list
|
||||
# disabled_tools = ["slow-tool"] # optional deny-list (applied after allow-list)
|
||||
|
||||
# --- Example: Streamable HTTP transport ---
|
||||
# [mcp_servers.github]
|
||||
# url = "https://github-mcp.example.com/mcp" # required
|
||||
# bearer_token_env_var = "GITHUB_TOKEN" # optional; Authorization: Bearer <token>
|
||||
# http_headers = { "X-Example" = "value" } # optional static headers
|
||||
# env_http_headers = { "X-Auth" = "AUTH_ENV" } # optional headers populated from env vars
|
||||
# startup_timeout_sec = 10.0 # optional
|
||||
# tool_timeout_sec = 60.0 # optional
|
||||
# enabled_tools = ["list_issues"] # optional allow-list
|
||||
|
||||
################################################################################
|
||||
# Model Providers (extend/override built-ins)
|
||||
################################################################################
|
||||
|
||||
# Built-ins include:
|
||||
# - openai (Responses API; requires login or OPENAI_API_KEY via auth flow)
|
||||
# - oss (Chat Completions API; defaults to http://localhost:11434/v1)
|
||||
|
||||
[model_providers]
|
||||
|
||||
# --- Example: override OpenAI with explicit base URL or headers ---
|
||||
# [model_providers.openai]
|
||||
# name = "OpenAI"
|
||||
# base_url = "https://api.openai.com/v1" # default if unset
|
||||
# wire_api = "responses" # "responses" | "chat" (default varies)
|
||||
# # requires_openai_auth = true # built-in OpenAI defaults to true
|
||||
# # request_max_retries = 4 # default 4; max 100
|
||||
# # stream_max_retries = 5 # default 5; max 100
|
||||
# # stream_idle_timeout_ms = 300000 # default 300_000 (5m)
|
||||
# # experimental_bearer_token = "sk-example" # optional dev-only direct bearer token
|
||||
# # http_headers = { "X-Example" = "value" }
|
||||
# # env_http_headers = { "OpenAI-Organization" = "OPENAI_ORGANIZATION", "OpenAI-Project" = "OPENAI_PROJECT" }
|
||||
|
||||
# --- Example: Azure (Chat/Responses depending on endpoint) ---
|
||||
# [model_providers.azure]
|
||||
# name = "Azure"
|
||||
# base_url = "https://YOUR_PROJECT_NAME.openai.azure.com/openai"
|
||||
# wire_api = "responses" # or "chat" per endpoint
|
||||
# query_params = { api-version = "2025-04-01-preview" }
|
||||
# env_key = "AZURE_OPENAI_API_KEY"
|
||||
# # env_key_instructions = "Set AZURE_OPENAI_API_KEY in your environment"
|
||||
|
||||
# --- Example: Local OSS (e.g., Ollama-compatible) ---
|
||||
# [model_providers.ollama]
|
||||
# name = "Ollama"
|
||||
# base_url = "http://localhost:11434/v1"
|
||||
# wire_api = "chat"
|
||||
|
||||
################################################################################
|
||||
# Profiles (named presets)
|
||||
################################################################################
|
||||
|
||||
# Active profile name. When unset, no profile is applied.
|
||||
# profile = "default"
|
||||
|
||||
[profiles]
|
||||
|
||||
# [profiles.default]
|
||||
# model = "gpt-5-codex"
|
||||
# model_provider = "openai"
|
||||
# approval_policy = "on-request"
|
||||
# sandbox_mode = "read-only"
|
||||
# model_reasoning_effort = "medium"
|
||||
# model_reasoning_summary = "auto"
|
||||
# model_verbosity = "medium"
|
||||
# chatgpt_base_url = "https://chatgpt.com/backend-api/"
|
||||
# experimental_compact_prompt_file = "compact_prompt.txt"
|
||||
# include_apply_patch_tool = false
|
||||
# experimental_use_unified_exec_tool = false
|
||||
# experimental_use_exec_command_tool = false
|
||||
# experimental_use_rmcp_client = false
|
||||
# experimental_use_freeform_apply_patch = false
|
||||
# experimental_sandbox_command_assessment = false
|
||||
# tools_web_search = false
|
||||
# tools_view_image = true
|
||||
# features = { unified_exec = false }
|
||||
|
||||
################################################################################
|
||||
# Projects (trust levels)
|
||||
################################################################################
|
||||
|
||||
# Mark specific worktrees as trusted. Only "trusted" is recognized.
|
||||
[projects]
|
||||
# [projects."/absolute/path/to/project"]
|
||||
# trust_level = "trusted"
|
||||
|
||||
################################################################################
|
||||
# OpenTelemetry (OTEL) – disabled by default
|
||||
################################################################################
|
||||
|
||||
[otel]
|
||||
# Include user prompt text in logs. Default: false
|
||||
log_user_prompt = false
|
||||
# Environment label applied to telemetry. Default: "dev"
|
||||
environment = "dev"
|
||||
# Exporter: none (default) | otlp-http | otlp-grpc
|
||||
exporter = "none"
|
||||
|
||||
# Example OTLP/HTTP exporter configuration
|
||||
# [otel]
|
||||
# exporter = { otlp-http = {
|
||||
# endpoint = "https://otel.example.com/v1/logs",
|
||||
# protocol = "binary", # "binary" | "json"
|
||||
# headers = { "x-otlp-api-key" = "${OTLP_TOKEN}" }
|
||||
# }}
|
||||
|
||||
# Example OTLP/gRPC exporter configuration
|
||||
# [otel]
|
||||
# exporter = { otlp-grpc = {
|
||||
# endpoint = "https://otel.example.com:4317",
|
||||
# headers = { "x-otlp-meta" = "abc123" }
|
||||
# }}
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
## Slash Commands
|
||||
|
||||
### What are slash commands?
|
||||
|
||||
Slash commands are special commands you can type that start with `/`.
|
||||
|
||||
---
|
||||
|
||||
### Built-in slash commands
|
||||
|
||||
Control Codex’s behavior during an interactive session with slash commands.
|
||||
|
||||
| Command | Purpose |
|
||||
| ------------ | ----------------------------------------------------------- |
|
||||
| `/model` | choose what model and reasoning effort to use |
|
||||
| `/approvals` | choose what Codex can do without approval |
|
||||
| `/review` | review my current changes and find issues |
|
||||
| `/new` | start a new chat during a conversation |
|
||||
| `/init` | create an AGENTS.md file with instructions for Codex |
|
||||
| `/compact` | summarize conversation to prevent hitting the context limit |
|
||||
| `/undo` | ask Codex to undo a turn |
|
||||
| `/diff` | show git diff (including untracked files) |
|
||||
| `/mention` | mention a file |
|
||||
| `/status` | show current session configuration and token usage |
|
||||
| `/mcp` | list configured MCP tools |
|
||||
| `/logout` | log out of Codex |
|
||||
| `/quit` | exit Codex |
|
||||
| `/exit` | exit Codex |
|
||||
| `/feedback` | send logs to maintainers |
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user