Merge branch 'main' into owen/separate_v2_namespace_take_two

This commit is contained in:
Owen Lin
2025-11-04 09:21:09 -08:00
40 changed files with 942 additions and 125 deletions
+2
View File
@@ -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)
+2
View File
@@ -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",
+1 -1
View File
@@ -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;
+1
View File
@@ -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",
+2
View File
@@ -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;
+9 -3
View File
@@ -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()
};
+1 -1
View File
@@ -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;
+5 -10
View File
@@ -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),
+30 -4
View File
@@ -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(),
+11 -1
View File
@@ -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;
+1
View File
@@ -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;
+5 -1
View File
@@ -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
+20 -7
View File
@@ -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"),
}
}
}
+2 -2
View File
@@ -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:?}"))
}
}
+24 -4
View File
@@ -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(&notify_file, Duration::from_secs(5)).await?;
let notify_payload_raw = tokio::fs::read_to_string(&notify_file).await?;
let payload: Value = serde_json::from_str(&notify_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(())
}
+2
View File
@@ -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 {
+1 -5
View File
@@ -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.
+13 -2
View File
@@ -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.
+34 -5
View File
@@ -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 '_'"));
}
}
+1
View File
@@ -106,3 +106,4 @@ insta = { workspace = true }
pretty_assertions = { workspace = true }
rand = { workspace = true }
vt100 = { workspace = true }
serial_test = { workspace = true }
+79 -13
View File
@@ -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
View File
@@ -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(())
}
}
@@ -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) │
╰─────────────────────────────────────────────────────────────────────╯
@@ -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. │
@@ -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) │
╰────────────────────────────────────────────╯
+6 -4
View File
@@ -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(&current_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")
}
+27 -17
View File
@@ -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}");
}
}
+2 -1
View File
@@ -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))
+374
View File
@@ -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" }
# }}
```
+31
View File
@@ -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 |
---