Merge 07a498e102 into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-06-29 11:37:35 -07:00
committed by GitHub
58 changed files with 334 additions and 262 deletions
@@ -124,7 +124,6 @@ pub(super) fn run_cleanup(
overrides.extend([
"analytics.enabled=false".to_string(),
"features.plugins=true".to_string(),
"features.remote_plugin=true".to_string(),
]);
let mut client = CodexClient::spawn_stdio(codex_bin, &overrides)?;
client.initialize()?;
@@ -199,7 +198,6 @@ fn spawn_client(
overrides.extend([
"analytics.enabled=true".to_string(),
"features.plugins=true".to_string(),
"features.remote_plugin=true".to_string(),
]);
let environment = vec![(
OsString::from(ANALYTICS_CAPTURE_ENV_VAR),
@@ -263,7 +263,6 @@ fn smoke_config_overrides(responses_base_url: &str) -> Result<Vec<String>> {
Ok(vec![
"analytics.enabled=true".to_string(),
"features.plugins=true".to_string(),
"features.remote_plugin=true".to_string(),
format!("model={}", quoted(MOCK_MODEL_SLUG)?),
format!("model_provider={}", quoted(MOCK_PROVIDER_ID)?),
format!(
+32 -7
View File
@@ -10,10 +10,12 @@ use codex_core::AttestationProvider;
use codex_core::GenerateAttestationFuture;
use serde::Serialize;
use tokio::time::Duration;
use tokio::time::timeout;
use tokio::time::Instant;
use tokio::time::timeout_at;
use tracing::warn;
use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::RequestSendError;
use crate::thread_state::ThreadStateManager;
const ATTESTATION_GENERATE_TIMEOUT: Duration = Duration::from_millis(100);
@@ -70,16 +72,39 @@ async fn request_attestation_header_value_with_timeout(
.first_attestation_capable_connection_for_thread(thread_id)
.await?;
let connection_ids = [connection_id];
let (request_id, rx) = outgoing
.send_request_to_connections(
Some(&connection_ids),
let deadline = Instant::now() + timeout_duration;
let (request_id, rx) = match outgoing
.send_request_to_connection_with_deadline(
connection_id,
ServerRequestPayload::AttestationGenerate(AttestationGenerateParams {}),
/*thread_id*/ None,
deadline,
)
.await;
.await
{
Ok(request) => request,
Err(RequestSendError::TimedOut) => {
warn!(
timeout_seconds = timeout_duration.as_secs_f64(),
"attestation generation request timed out before it was queued"
);
return app_server_attestation_header_value(
AppServerAttestationStatus::Timeout,
/*token*/ None,
);
}
Err(RequestSendError::Closed) => {
warn!(
"attestation generation request could not be queued because the client channel closed"
);
return app_server_attestation_header_value(
AppServerAttestationStatus::RequestFailed,
/*token*/ None,
);
}
};
let result = match timeout(timeout_duration, rx).await {
let result = match timeout_at(deadline, rx).await {
Ok(Ok(Ok(result))) => result,
Ok(Ok(Err(err))) => {
warn!(
+18 -5
View File
@@ -20,6 +20,7 @@ use tokio::time::timeout_at;
use crate::outgoing_message::ConnectionId;
use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::RequestSendError;
use crate::thread_state::ThreadStateManager;
const CURRENT_TIME_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
@@ -103,16 +104,28 @@ async fn request_current_time(
.subscribed_connection_ids(thread_id)
.await;
let connection_id = require_single_current_time_connection(&connection_ids)?;
let connection_ids = [connection_id];
let (request_id, rx) = outgoing
.send_request_to_connections(
Some(&connection_ids),
let (request_id, rx) = match outgoing
.send_request_to_connection_with_deadline(
connection_id,
ServerRequestPayload::CurrentTimeRead(CurrentTimeReadParams {
thread_id: thread_id.to_string(),
}),
/*thread_id*/ None,
deadline,
)
.await;
.await
{
Ok(request) => request,
Err(RequestSendError::TimedOut) => {
bail!(
"current-time request timed out after {}s",
CURRENT_TIME_REQUEST_TIMEOUT.as_secs()
);
}
Err(RequestSendError::Closed) => {
bail!("current-time request could not be queued because the client channel closed");
}
};
let result = match timeout_at(deadline, rx).await {
Ok(Ok(Ok(result))) => result,
@@ -21,6 +21,8 @@ use codex_protocol::request_permissions::RequestPermissionsResponse;
use tokio::sync::Mutex;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::time::Instant;
use tokio::time::timeout_at;
use tracing::Instrument;
use tracing::Span;
use tracing::warn;
@@ -38,6 +40,12 @@ use codex_protocol::account::PlanType;
pub(crate) type ClientRequestResult = std::result::Result<Result, JSONRPCErrorError>;
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum RequestSendError {
TimedOut,
Closed,
}
/// Stable identifier for a client request scoped to a transport connection.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(crate) struct ConnectionRequestId {
@@ -349,6 +357,53 @@ impl OutgoingMessageSender {
(outgoing_message_id, rx_approve)
}
pub(crate) async fn send_request_to_connection_with_deadline(
&self,
connection_id: ConnectionId,
request: ServerRequestPayload,
thread_id: Option<ThreadId>,
deadline: Instant,
) -> std::result::Result<(RequestId, oneshot::Receiver<ClientRequestResult>), RequestSendError>
{
let id = self.next_request_id();
let request = request.request_with_id(id.clone());
let outgoing_message = OutgoingMessage::Request(request.clone());
let permit = match timeout_at(deadline, self.sender.reserve()).await {
Ok(Ok(permit)) => permit,
Ok(Err(_)) => return Err(RequestSendError::Closed),
Err(_) => return Err(RequestSendError::TimedOut),
};
let (callback_sender, callback_receiver) = oneshot::channel();
let mut request_id_to_callback =
match timeout_at(deadline, self.request_id_to_callback.lock()).await {
Ok(request_id_to_callback) => request_id_to_callback,
Err(_) => return Err(RequestSendError::TimedOut),
};
if Instant::now() >= deadline {
return Err(RequestSendError::TimedOut);
}
request_id_to_callback.insert(
id.clone(),
PendingCallbackEntry {
callback: callback_sender,
thread_id,
request: request.clone(),
},
);
drop(request_id_to_callback);
permit.send(OutgoingEnvelope::ToConnection {
connection_id,
message: outgoing_message,
write_complete_tx: None,
});
self.analytics_events_client
.track_server_request(connection_id.0, request);
Ok((id, callback_receiver))
}
pub(crate) async fn replay_requests_to_connection_for_thread(
&self,
connection_id: ConnectionId,
@@ -756,6 +811,7 @@ mod tests {
use pretty_assertions::assert_eq;
use serde_json::json;
use std::sync::Arc;
use tokio::time::Instant;
use tokio::time::timeout;
use uuid::Uuid;
@@ -1086,6 +1142,43 @@ mod tests {
}
}
#[tokio::test]
async fn deadline_expires_while_waiting_for_outgoing_queue_capacity() {
let (tx, _rx) = mpsc::channel::<OutgoingEnvelope>(1);
tx.try_send(OutgoingEnvelope::Broadcast {
message: OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning(
ConfigWarningNotification {
summary: "queued".to_string(),
details: None,
path: None,
range: None,
},
)),
})
.expect("the queue should accept the filler message");
let outgoing =
OutgoingMessageSender::new(tx, codex_analytics::AnalyticsEventsClient::disabled());
let error = outgoing
.send_request_to_connection_with_deadline(
ConnectionId(42),
ServerRequestPayload::ApplyPatchApproval(ApplyPatchApprovalParams {
conversation_id: ThreadId::new(),
call_id: "call-id".to_string(),
file_changes: HashMap::new(),
reason: None,
grant_root: None,
}),
/*thread_id*/ None,
Instant::now() + Duration::from_millis(10),
)
.await
.expect_err("the full queue should make the request time out");
assert_eq!(error, RequestSendError::TimedOut);
assert!(outgoing.request_id_to_callback.lock().await.is_empty());
}
#[tokio::test]
async fn send_response_clears_registered_request_context() {
let (tx, _rx) = mpsc::channel::<OutgoingEnvelope>(4);
@@ -34,6 +34,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
upgrade: preset.upgrade.as_ref().map(Into::into),
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -138,7 +138,7 @@ async fn orchestrator_skill_can_read_referenced_resource_without_an_executor() -
let thread_start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
model: Some("gpt-5.5".to_string()),
environments: Some(Vec::new()),
..Default::default()
})
@@ -2027,7 +2027,6 @@ chatgpt_base_url = "{base_url}"
[features]
plugins = true
remote_plugin = true
"#
),
)
@@ -2057,7 +2056,6 @@ chatgpt_base_url = "{}/backend-api/"
[features]
plugins = true
remote_plugin = true
connectors = true
"#,
server.uri()
@@ -71,6 +71,23 @@ plugins = true
)
}
fn write_remote_plugins_disabled_config_with_base_url(
codex_home: &std::path::Path,
base_url: &str,
) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
format!(
r#"chatgpt_base_url = "{base_url}"
[features]
plugins = true
remote_plugin = false
"#,
),
)
}
#[tokio::test]
async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -197,7 +214,6 @@ async fn plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_en
[features]
plugins = true
remote_plugin = true
plugin_sharing = false
[plugins."linear@openai-curated"]
@@ -1987,10 +2003,11 @@ async fn plugin_list_uses_cached_global_remote_catalog_and_refreshes_it() -> Res
}
#[tokio::test]
async fn plugin_list_includes_openai_curated_remote_collection_when_requested() -> Result<()> {
async fn plugin_list_includes_openai_curated_remote_collection_when_remote_plugin_disabled_and_requested()
-> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
write_plugins_enabled_config_with_base_url(
write_remote_plugins_disabled_config_with_base_url(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
)?;
@@ -2092,10 +2109,11 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_requested()
}
#[tokio::test]
async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors() -> Result<()> {
async fn plugin_list_propagates_openai_curated_remote_collection_errors_when_remote_plugin_disabled()
-> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
write_plugins_enabled_config_with_base_url(
write_remote_plugins_disabled_config_with_base_url(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
)?;
@@ -2148,10 +2166,11 @@ async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors
}
#[tokio::test]
async fn plugin_list_skips_explicit_openai_curated_remote_collection_for_api_auth() -> Result<()> {
async fn plugin_list_skips_openai_curated_remote_collection_for_api_auth_when_remote_plugin_disabled()
-> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
write_plugins_enabled_config_with_base_url(
write_remote_plugins_disabled_config_with_base_url(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
)?;
@@ -2391,7 +2410,8 @@ async fn plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_ex
}
#[tokio::test]
async fn plugin_installed_includes_remote_shared_with_me_plugins() -> Result<()> {
async fn plugin_installed_includes_remote_shared_with_me_plugins_when_remote_plugin_disabled()
-> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
std::fs::write(
@@ -2493,7 +2513,8 @@ plugin_sharing = true
}
#[tokio::test]
async fn plugin_installed_includes_workspace_directory_without_plugin_sharing() -> Result<()> {
async fn plugin_installed_includes_workspace_directory_without_plugin_sharing_when_remote_plugin_disabled()
-> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
std::fs::write(
@@ -2590,7 +2611,6 @@ async fn plugin_installed_includes_created_by_me_when_remote_plugins_enabled() -
[features]
plugins = true
remote_plugin = true
plugin_sharing = false
"#,
server.uri()
@@ -2681,7 +2701,6 @@ async fn plugin_installed_starts_remote_installed_bundle_sync() -> Result<()> {
[features]
plugins = true
remote_plugin = true
plugin_sharing = false
"#,
server.uri()
@@ -2750,10 +2769,10 @@ plugin_sharing = false
}
#[tokio::test]
async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag() -> Result<()> {
async fn plugin_list_fetches_workspace_directory_kind_when_remote_plugin_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
write_plugins_enabled_config_with_base_url(
write_remote_plugins_disabled_config_with_base_url(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
)?;
@@ -2848,7 +2867,6 @@ async fn plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace()
[features]
plugins = true
remote_plugin = true
plugin_sharing = false
"#,
server.uri()
@@ -4032,7 +4050,6 @@ chatgpt_base_url = "{base_url}"
[features]
plugins = true
remote_plugin = true
"#
),
)
@@ -2132,7 +2132,6 @@ chatgpt_base_url = "{base_url}"
[features]
plugins = true
remote_plugin = true
"#
),
)
@@ -344,7 +344,6 @@ chatgpt_base_url = "{}/backend-api"
[features]
plugins = true
remote_plugin = true
plugin_sharing = false
"#,
server.uri()
@@ -1050,7 +1049,6 @@ chatgpt_base_url = "{}/backend-api"
[features]
plugins = true
remote_plugin = true
plugin_sharing = false
"#,
server.uri()
@@ -1204,7 +1202,6 @@ chatgpt_base_url = "{base_url}"
[features]
plugins = true
remote_plugin = true
"#
),
)
@@ -622,7 +622,6 @@ chatgpt_base_url = "{base_url}"
[features]
plugins = true
remote_plugin = true
"#
),
)
@@ -68,9 +68,7 @@ async fn first_turn_after_external_login_waits_for_recommended_plugins() -> Resu
let config = std::fs::read_to_string(&config_path)?;
std::fs::write(
config_path,
format!(
"{config}\n[features]\napps = true\nplugins = true\nremote_plugin = true\ntool_suggest = true\n"
),
format!("{config}\n[features]\napps = true\nplugins = true\ntool_suggest = true\n"),
)?;
let sqlite_home = codex_home.path().to_string_lossy();
@@ -79,23 +79,6 @@ plugins = true
)
}
fn write_remote_plugins_enabled_config_with_base_url(
codex_home: &std::path::Path,
base_url: &str,
) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
format!(
r#"chatgpt_base_url = "{base_url}"
[features]
plugins = true
remote_plugin = true
"#,
),
)
}
fn write_plugin_with_skill(
repo_root: &std::path::Path,
plugin_name: &str,
@@ -175,7 +158,7 @@ fn write_cached_local_curated_plugin_with_skill(codex_home: &std::path::Path) ->
}
#[tokio::test]
async fn runtime_remote_plugin_enablement_excludes_local_curated_plugin_skills() -> Result<()> {
async fn runtime_remote_plugin_toggle_updates_local_curated_plugin_skills() -> Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let server = MockServer::start().await;
@@ -206,6 +189,18 @@ enabled = true
let mut mcp = TestAppServer::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let disablement_request_id = mcp
.send_experimental_feature_enablement_set_request(ExperimentalFeatureEnablementSetParams {
enablement: BTreeMap::from([("remote_plugin".to_string(), false)]),
})
.await?;
let disablement_response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(disablement_request_id)),
)
.await??;
let _: ExperimentalFeatureEnablementSetResponse = to_response(disablement_response)?;
let initial_skills_list_request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
@@ -266,7 +261,7 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result
let server = MockServer::start().await;
let expected_skill_path =
std::fs::canonicalize(write_cached_remote_plugin_with_skill(codex_home.path())?)?;
write_remote_plugins_enabled_config_with_base_url(
write_plugins_enabled_config_with_base_url(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
)?;
@@ -80,6 +80,7 @@ async fn models_client_hits_models_endpoint() {
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -37,6 +37,13 @@ use wiremock::matchers::query_param;
#[tokio::test]
async fn returns_fallback_plugins_when_remote_disabled_for_codex_auth() {
let codex_home = tempdir().expect("tempdir should succeed");
write_file(
&codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = false
"#,
);
let curated_root = curated_plugins_repo_path(codex_home.path());
write_openai_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]);
@@ -69,8 +76,7 @@ async fn returns_api_curated_fallback_plugins_for_direct_provider_auth() {
let curated_root = curated_plugins_repo_path(codex_home.path());
write_openai_api_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]);
let mut plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
plugins.remote_plugin_enabled = true;
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
plugins_manager.set_auth_mode(Some(AuthMode::ApiKey));
let auth = CodexAuth::from_api_key("test-api-key");
@@ -153,7 +159,6 @@ async fn omits_openai_curated_but_keeps_configured_marketplaces_for_remote_codex
&format!(
r#"[features]
plugins = true
remote_plugin = true
[marketplaces.{bundled_marketplace_name}]
source_type = "git"
@@ -188,8 +193,7 @@ async fn includes_openai_curated_when_remote_enabled_without_auth() {
let curated_root = curated_plugins_repo_path(codex_home.path());
write_openai_curated_marketplace(&curated_root, &["slack"]);
let mut plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
plugins.remote_plugin_enabled = true;
let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let discoverable_plugins = list_discoverable_plugins(
&plugins_manager,
@@ -788,7 +792,6 @@ async fn expands_cached_remote_plugins_by_loaded_apps() {
&codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
"#,
);
@@ -1062,7 +1062,6 @@ async fn remote_installed_cache_ignores_plugins_missing_local_cache() {
&codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
"#,
);
@@ -1296,7 +1295,6 @@ async fn remote_global_catalog_ignores_local_curated_plugins() {
&codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
[plugins."linear@openai-curated"]
enabled = true
@@ -1347,7 +1345,6 @@ async fn remote_plugin_feature_keeps_local_curated_without_codex_backend() {
&codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
[plugins."linear@openai-curated"]
enabled = true
@@ -4878,7 +4875,6 @@ async fn remote_plugin_caches_refresh_warms_recommended_plugins_cache() {
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
"#,
);
@@ -4939,7 +4935,6 @@ async fn recommended_plugins_mode_deduplicates_concurrent_cache_misses() {
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
"#,
);
@@ -5017,7 +5012,6 @@ async fn recommended_plugin_candidates_filter_installed_and_disabled_plugins() {
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
"#,
);
let server = MockServer::start().await;
@@ -5090,7 +5084,6 @@ async fn recommended_plugins_mode_caches_explicit_false() {
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
"#,
);
@@ -5130,7 +5123,6 @@ async fn recommended_plugins_mode_retries_after_fetch_failure() {
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
"#,
);
+1 -1
View File
@@ -149,7 +149,7 @@ pub(crate) async fn load_plugins_config(codex_home: &Path, cwd: &Path) -> Plugin
feature_enabled(
&effective_config,
"remote_plugin",
/*default_enabled*/ false,
/*default_enabled*/ true,
),
"https://chatgpt.com/backend-api/".to_string(),
)
-8
View File
@@ -75,14 +75,6 @@ pub fn render_available_skills_body(skill_root_lines: &[String], skill_lines: &[
lines.push("### Available skills".to_string());
lines.extend(skill_lines.iter().cloned());
lines.push("### How to use skills".to_string());
let how_to_use = if skill_root_lines.is_empty() {
SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS
} else {
SKILLS_HOW_TO_USE_WITH_ALIASES
};
lines.push(how_to_use.to_string());
format!("\n{}\n", lines.join("\n"))
}
+1 -1
View File
@@ -169,7 +169,7 @@ pub(crate) struct CompactConversationRequestSettings {
fn reasoning_effort_for_request(effort: ReasoningEffortConfig) -> ReasoningEffortConfig {
match effort {
ReasoningEffortConfig::Ultra => ReasoningEffortConfig::Custom("max".to_string()),
ReasoningEffortConfig::Ultra => ReasoningEffortConfig::Max,
effort => effort,
}
}
+1 -4
View File
@@ -292,10 +292,7 @@ fn ultra_reasoning_uses_max_for_requests() {
super::reasoning_effort_for_request(ReasoningEffort::Ultra),
super::reasoning_effort_for_request(ReasoningEffort::High),
),
(
ReasoningEffort::Custom("max".to_string()),
ReasoningEffort::High,
)
(ReasoningEffort::Max, ReasoningEffort::High,)
);
}
@@ -1,4 +1,6 @@
use codex_core_skills::AvailableSkills;
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS;
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ALIASES;
use codex_core_skills::render_available_skills_body;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
@@ -20,13 +22,24 @@ impl AvailableSkillsInstructions {
skill_lines,
}
}
}
impl From<AvailableSkills> for AvailableSkillsInstructions {
fn from(available_skills: AvailableSkills) -> Self {
pub fn from_available_skills(
available_skills: &AvailableSkills,
include_skills_usage_instructions: bool,
) -> Self {
let mut skill_lines = available_skills.skill_lines.clone();
if include_skills_usage_instructions {
skill_lines.push("### How to use skills".to_string());
let instructions = if available_skills.skill_root_lines.is_empty() {
SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS
} else {
SKILLS_HOW_TO_USE_WITH_ALIASES
};
skill_lines.push(instructions.to_string());
}
Self {
skill_root_lines: available_skills.skill_root_lines,
skill_lines: available_skills.skill_lines,
skill_root_lines: available_skills.skill_root_lines.clone(),
skill_lines,
}
}
}
@@ -69,7 +69,6 @@ async fn list_tool_suggest_discoverable_plugins_includes_cached_remote_global_pl
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
"#,
);
@@ -306,7 +305,6 @@ remote_plugin = true
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
[tool_suggest]
disabled_tools = [
+4 -1
View File
@@ -3259,7 +3259,10 @@ impl Session {
);
if let Some(available_skills) = available_skills {
let warning_message = available_skills.warning_message.clone();
let skills_instructions = AvailableSkillsInstructions::from(available_skills);
let skills_instructions = AvailableSkillsInstructions::from_available_skills(
&available_skills,
turn_context.model_info.include_skills_usage_instructions,
);
if let Some(warning_message) = warning_message {
self.send_event_raw(Event {
id: String::new(),
@@ -701,6 +701,9 @@ impl Session {
&per_turn_config.to_models_manager_config(),
)
.await;
self.services
.thread_extension_data
.insert(model_info.clone());
let multi_agent_version = match multi_agent_runtime {
TurnMultiAgentRuntime::ResolveAndStore => {
@@ -674,8 +674,15 @@ fn spawn_agent_tool_description(
This spawn_agent tool provides you access to sub-agents that inherit your current model by default. Do not set the `model` field unless the user explicitly asks for a different model or there is a clear task-specific reason. You should follow the rules and guidelines below to use this tool.
Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.
Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn.
{agent_role_usage_hint}
### When to delegate vs. do the subtask yourself
- First, quickly analyze the overall user task and form a succinct high-level plan. Identify which tasks are immediate blockers on the critical path, and which tasks are sidecar tasks that are needed but can run in parallel without blocking the next local step. As part of that plan, explicitly decide what immediate task you should do locally right now. Do this planning step before delegating to agents so you do not hand off the immediate blocking task to a submodel and then waste time waiting on it.
- Use a subagent when a subtask is easy enough for it to handle and can run in parallel with your local work. Prefer delegating concrete, bounded sidecar tasks that materially advance the main task without blocking your immediate next local step.
- Do not delegate urgent blocking work when your immediate next step depends on that result. If the very next action is blocked on that task, the main rollout should usually do it locally to keep the critical path moving.
- Keep work local when the subtask is too difficult to delegate well and when it is tightly coupled, urgent, or likely to block your immediate next step.
### Designing delegated subtasks
- Subtasks must be concrete, well-defined, and self-contained.
- Delegated subtasks must materially advance the main task.
+1
View File
@@ -241,6 +241,7 @@ fn remote_model_with_auto_review_override(slug: &str, review_model: &str) -> Mod
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
+3 -3
View File
@@ -2171,7 +2171,7 @@ async fn skills_use_aliases_in_developer_message_under_budget_pressure() {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
async fn includes_configured_max_effort_in_request() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
@@ -2183,7 +2183,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
let TestCodex { codex, .. } = test_codex()
.with_model("gpt-5.4")
.with_config(|config| {
config.model_reasoning_effort = Some(ReasoningEffort::Medium);
config.model_reasoning_effort = Some(ReasoningEffort::Max);
})
.build(&server)
.await?;
@@ -2212,7 +2212,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
.get("reasoning")
.and_then(|t| t.get("effort"))
.and_then(|v| v.as_str()),
Some("medium")
Some("max")
);
Ok(())
@@ -124,6 +124,7 @@ fn test_model_info(
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -948,6 +949,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -352,6 +352,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
+2
View File
@@ -574,6 +574,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
personality_pragmatic: Some("Pragmatic variant".to_string()),
}),
}),
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -689,6 +690,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
personality_pragmatic: Some(remote_pragmatic_message.to_string()),
}),
}),
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
+5 -2
View File
@@ -329,7 +329,7 @@ async fn remote_models_long_model_slug_is_sent_with_custom_reasoning() -> Result
/*priority*/ 1_000,
TruncationPolicyConfig::bytes(/*limit*/ 10_000),
);
let custom_reasoning_effort = ReasoningEffort::Custom("max".to_string());
let custom_reasoning_effort = ReasoningEffort::Custom("future".to_string());
remote_model.default_reasoning_level = Some(custom_reasoning_effort.clone());
remote_model.supported_reasoning_levels = vec![
ReasoningEffortPreset {
@@ -391,7 +391,7 @@ async fn remote_models_long_model_slug_is_sent_with_custom_reasoning() -> Result
.and_then(|reasoning| reasoning.get("summary"))
.and_then(|value| value.as_str());
assert_eq!(body["model"].as_str(), Some(requested_model));
assert_eq!(reasoning_effort, Some("max"));
assert_eq!(reasoning_effort, Some("future"));
assert_eq!(reasoning_summary, Some("detailed"));
Ok(())
@@ -485,6 +485,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -738,6 +739,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
upgrade: None,
base_instructions: remote_base.to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -1232,6 +1234,7 @@ fn test_remote_model_with_policy(
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
+1
View File
@@ -1651,6 +1651,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
+2 -2
View File
@@ -827,7 +827,7 @@ async fn tool_search_returns_deferred_v1_multi_agent_tools() -> Result<()> {
assert!(
!first_request_body
.to_string()
.contains("Only use `spawn_agent` if and only if"),
.contains("### When to delegate vs. do the subtask yourself"),
"deferred v1 multi-agent guidance should stay out of initial developer context"
);
@@ -861,7 +861,7 @@ async fn tool_search_returns_deferred_v1_multi_agent_tools() -> Result<()> {
"Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work."
));
assert!(description.contains("### Designing delegated subtasks"));
assert!(!description.contains("### When to delegate vs. do the subtask yourself"));
assert!(description.contains("### When to delegate vs. do the subtask yourself"));
Ok(())
}
@@ -71,6 +71,7 @@ fn test_model_info(
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -218,8 +219,10 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() ->
"expected explicit authorization rule in spawn_agent description: {description:?}"
);
assert!(
!description.contains("### When to delegate vs. do the subtask yourself"),
"spawn_agent description should not include extra when-to-use delegation guidance: {description:?}"
description.contains(
"Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn."
) && description.contains("### When to delegate vs. do the subtask yourself"),
"expected delegation decision guidance in spawn_agent description: {description:?}"
);
assert!(
description.contains(
+1
View File
@@ -1374,6 +1374,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
+14 -2
View File
@@ -21,6 +21,7 @@ use codex_extension_api::TurnInputContributor;
use codex_extension_api::WorldStateContributionInput;
use codex_extension_api::WorldStateSectionContribution;
use codex_mcp::McpResourceClient;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::WarningEvent;
@@ -129,7 +130,10 @@ where
for warning in &catalog.warnings {
self.emit_warning(thread_store.level_id(), warning.clone());
}
available_skills_fragment(&catalog)
let include_usage = thread_store
.get::<ModelInfo>()
.is_some_and(|model_info| model_info.include_skills_usage_instructions);
available_skills_fragment(&catalog, include_usage)
.map(|fragment| PromptFragment::developer_capability(fragment.render()))
.into_iter()
.collect()
@@ -162,9 +166,14 @@ where
input
.turn_store
.insert(ExecutorSkillsStepState(catalog.clone()));
let include_usage = input
.thread_store
.get::<ModelInfo>()
.is_some_and(|model_info| model_info.include_skills_usage_instructions);
vec![executor_skills_world_state_section(
&catalog,
config.include_instructions,
include_usage,
)]
})
}
@@ -239,7 +248,10 @@ where
entry.authority.kind != SkillSourceKind::Executor
&& entry.authority.kind != SkillSourceKind::Orchestrator
});
if let Some(fragment) = available_skills_fragment(&turn_catalog) {
let include_usage = thread_store
.get::<ModelInfo>()
.is_some_and(|model_info| model_info.include_skills_usage_instructions);
if let Some(fragment) = available_skills_fragment(&turn_catalog, include_usage) {
fragments.push(Box::new(fragment));
}
}
+9 -1
View File
@@ -1,3 +1,4 @@
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS;
use codex_core_skills::render_available_skills_body;
use codex_extension_api::ContextualUserFragment;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
@@ -9,7 +10,14 @@ pub(crate) struct AvailableSkillsInstructions {
}
impl AvailableSkillsInstructions {
pub(crate) fn from_skill_lines(skill_lines: Vec<String>) -> Self {
pub(crate) fn from_skill_lines(
mut skill_lines: Vec<String>,
include_skills_usage_instructions: bool,
) -> Self {
if include_skills_usage_instructions {
skill_lines.push("### How to use skills".to_string());
skill_lines.push(SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS.to_string());
}
Self { skill_lines }
}
}
+5 -1
View File
@@ -21,6 +21,7 @@ pub(crate) const MAX_SKILL_PATH_BYTES: usize = 1_024;
)]
pub(crate) fn available_skills_fragment(
catalog: &SkillCatalog,
include_skills_usage_instructions: bool,
) -> Option<AvailableSkillsInstructions> {
let mut total_bytes = 0usize;
let mut omitted = 0usize;
@@ -56,7 +57,10 @@ pub(crate) fn available_skills_fragment(
));
}
Some(AvailableSkillsInstructions::from_skill_lines(skill_lines))
Some(AvailableSkillsInstructions::from_skill_lines(
skill_lines,
include_skills_usage_instructions,
))
}
pub(crate) fn truncate_catalog_skill_description(description: &str) -> Cow<'_, str> {
+3 -1
View File
@@ -17,9 +17,11 @@ const HIDDEN_EXECUTOR_SKILLS_BODY: &str = "\n## Skills update\nSelected-environm
pub(crate) fn executor_skills_world_state_section(
catalog: &SkillCatalog,
include_instructions: bool,
include_skills_usage_instructions: bool,
) -> WorldStateSectionContribution {
let body = if include_instructions {
available_skills_fragment(catalog).map(|fragment| fragment.body())
available_skills_fragment(catalog, include_skills_usage_instructions)
.map(|fragment| fragment.body())
} else {
None
};
@@ -5,7 +5,6 @@ use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use codex_core_skills::HostSkillsSnapshot;
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS;
use codex_core_skills::SKILLS_INTRO_WITH_ABSOLUTE_PATHS;
use codex_core_skills::SkillLoadOutcome;
use codex_core_skills::SkillMetadata;
@@ -125,7 +124,7 @@ async fn installed_extension_uses_host_service_snapshot() -> TestResult {
.await;
let expected_catalog = format!(
"{SKILLS_INSTRUCTIONS_OPEN_TAG}\n## Skills\n{SKILLS_INTRO_WITH_ABSOLUTE_PATHS}\n### Available skills\n- demo: Demo skill. (file: {skill_prompt_path})\n### How to use skills\n{SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS}\n{SKILLS_INSTRUCTIONS_CLOSE_TAG}"
"{SKILLS_INSTRUCTIONS_OPEN_TAG}\n## Skills\n{SKILLS_INTRO_WITH_ABSOLUTE_PATHS}\n### Available skills\n- demo: Demo skill. (file: {skill_prompt_path})\n{SKILLS_INSTRUCTIONS_CLOSE_TAG}"
);
let expected_skill = format!(
"<skill>\n<name>demo</name>\n<path>{skill_prompt_path}</path>\n{DEMO_SKILL_CONTENTS}\n</skill>"
+3 -3
View File
@@ -190,7 +190,7 @@ pub enum Feature {
///
/// Requirements-only gate: this should be set from requirements, not user config.
ComputerUse,
/// Temporary internal-only flag for PS-backed remote plugin catalog development.
/// Enable the PS-backed remote plugin catalog.
RemotePlugin,
/// Enable remote plugin sharing flows.
PluginSharing,
@@ -1145,8 +1145,8 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::RemotePlugin,
key: "remote_plugin",
stage: Stage::UnderDevelopment,
default_enabled: false,
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::PluginSharing,
@@ -70,7 +70,7 @@ fn gpt_5_6_bedrock_model(bedrock_slug: &str, display_name: &str, priority: i32)
model
.supported_reasoning_levels
.push(ReasoningEffortPreset {
effort: ReasoningEffort::Custom("max".to_string()),
effort: ReasoningEffort::Max,
description: "Maximum reasoning depth for the hardest problems".to_string(),
});
model
@@ -148,7 +148,7 @@ mod tests {
expected
.supported_reasoning_levels
.push(ReasoningEffortPreset {
effort: ReasoningEffort::Custom("max".to_string()),
effort: ReasoningEffort::Max,
description: "Maximum reasoning depth for the hardest problems".to_string(),
});
File diff suppressed because one or more lines are too long
@@ -82,6 +82,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
upgrade: None,
base_instructions: BASE_INSTRUCTIONS.to_string(),
model_messages: local_personality_messages_for_slug(slug),
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -20,8 +20,6 @@ const APPROVAL_POLICY_UNLESS_TRUSTED: &str =
include_str!("../templates/permissions/approval_policy/unless_trusted.md");
const APPROVAL_POLICY_ON_REQUEST_RULE: &str =
include_str!("../templates/permissions/approval_policy/on_request.md");
const APPROVAL_POLICY_ON_REQUEST_AUTO_REVIEW: &str =
include_str!("../templates/permissions/approval_policy/on_request_auto_review.md");
const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str =
include_str!("../templates/permissions/approval_policy/on_request_rule_request_permission.md");
const AUTO_REVIEW_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `auto_review`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval.";
@@ -208,8 +206,6 @@ fn approval_text(
let on_request_instructions = || {
let on_request_rule = if exec_permission_approvals_enabled {
APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string()
} else if approvals_reviewer == ApprovalsReviewer::AutoReview {
APPROVAL_POLICY_ON_REQUEST_AUTO_REVIEW.to_string()
} else {
APPROVAL_POLICY_ON_REQUEST_RULE.to_string()
};
@@ -237,98 +237,17 @@ fn on_request_includes_tool_guidance_alongside_inline_permission_guidance_when_b
#[test]
fn auto_review_approvals_append_auto_review_specific_guidance() {
let mut exec_policy = Policy::empty();
exec_policy
.add_prefix_rule(&["git".to_string(), "pull".to_string()], Decision::Allow)
.expect("add rule");
let text = approval_text(
AskForApproval::OnRequest,
ApprovalsReviewer::AutoReview,
&exec_policy,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
assert!(text.contains("`approvals_reviewer` is `auto_review`"));
assert!(text.contains("materially safer alternative"));
assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`"));
assert!(text.contains(
"When the sandbox is likely to block a command needed for the task, request escalation up front"
));
assert!(text.contains(
"When unsure, prefer requesting escalation unnecessarily over failing to request it when needed"
));
assert!(
text.contains(
"Request escalation for commands that need write access outside writable roots"
)
);
assert!(text.contains(
"Request escalation for git operations that may write lock files, such as updating the index or refs"
));
assert!(!text.contains("Request escalation for GUI commands"));
assert!(!text.contains("Use escalation when it is the direct or most reliable way"));
assert!(!text.contains("Do not spend extra turns running likely-to-fail sandbox probes first"));
assert!(text.contains("Do not include a `prefix_rule` parameter."));
assert!(text.contains(
"Request escalation for commands that may need network access, including HTTP calls, package registries, internal services, data-service APIs, remote queries, data fetches, or live probes"
));
assert!(text.contains(
"Request escalation for commands that may need remote authentication, cluster, cloud, or database access"
));
assert!(text.contains(
"Request escalation for commands that may need process, cache, or other environment access outside the sandbox"
));
assert!(
text.contains(
"including DNS, connection, authentication, retry, or service endpoint errors"
)
);
assert!(text.contains(
"Request escalation before potentially destructive actions, such as `rm` or `git reset`"
));
assert!(text.contains(
"If a command may be hanging on sandbox-blocked access, stop after a short timeout and rerun with `require_escalated`"
));
assert!(!text.contains("Be judicious with escalating"));
assert!(text.contains("Approved command prefixes"));
assert!(text.contains(r#"["git", "pull"]"#));
assert!(!text.contains("prefix_rule guidance"));
}
#[test]
fn normal_on_request_keeps_user_approval_wording() {
let text = approval_text(
AskForApproval::OnRequest,
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
assert!(text.contains("approved by the user"));
assert!(text.contains("Be judicious with escalating"));
assert!(!text.contains(
"When the sandbox is likely to block a command needed for the task, request escalation up front"
));
}
#[test]
fn auto_review_on_request_with_inline_permission_requests_keeps_suffix() {
let text = approval_text(
AskForApproval::OnRequest,
ApprovalsReviewer::AutoReview,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
);
assert!(text.contains("with_additional_permissions"));
assert!(text.contains("`approvals_reviewer` is `auto_review`"));
assert!(text.contains("materially safer alternative"));
assert!(!text.contains(
"When the sandbox is likely to block a command needed for the task, request escalation up front"
));
}
#[test]
@@ -1,45 +0,0 @@
# Escalation Requests
Commands are run outside the sandbox after approval. The command string is split into independent command segments at shell control operators, including but not limited to:
- Pipes: |
- Logical operators: &&, ||
- Command separators: ;
- Subshell boundaries: (...), $()
Each resulting segment is evaluated independently for sandbox restrictions and approval requirements.
Example:
git pull | tee output.txt
This is treated as two command segments:
["git", "pull"]
["tee", "output.txt"]
Commands that use more advanced shell features like redirection (>, >>, <), substitutions ($(...), ...), environment variables (FOO=bar), or wildcard patterns (*, ?) require care because each independent command segment is evaluated separately.
## How to request escalation
IMPORTANT: To request approval to execute a command that will require escalated privileges:
- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
- Include a concise `justification` parameter that explains why escalated privileges are needed.
- Do not include a `prefix_rule` parameter.
## When to request escalation
While commands are running inside the sandbox, here are some scenarios that justify escalation:
- When the sandbox is likely to block a command needed for the task, request escalation up front.
- Request escalation for commands that need write access outside writable roots, such as tests that write to `/var`.
- Request escalation for git operations that may write lock files, such as updating the index or refs.
- Request escalation for commands that may need network access, including HTTP calls, package registries, internal services, data-service APIs, remote queries, data fetches, or live probes.
- Request escalation for commands that may need remote authentication, cluster, cloud, or database access.
- Request escalation for commands that may need process, cache, or other environment access outside the sandbox.
- If a sandboxed attempt fails with sandboxing or likely network symptoms, including DNS, connection, authentication, retry, or service endpoint errors, rerun with `sandbox_permissions` set to `"require_escalated"` and include `justification`.
- If a command may be hanging on sandbox-blocked access, stop after a short timeout and rerun with `require_escalated`.
- Request escalation before potentially destructive actions, such as `rm` or `git reset`, that the user did not explicitly ask for.
- When unsure, prefer requesting escalation unnecessarily over failing to request it when needed.
+16 -4
View File
@@ -45,6 +45,7 @@ pub enum ReasoningEffort {
Medium,
High,
XHigh,
Max,
Ultra,
/// A model-defined effort value that this client does not know yet.
Custom(String),
@@ -60,6 +61,7 @@ impl ReasoningEffort {
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
Self::Max => "max",
Self::Ultra => "ultra",
Self::Custom(effort) => effort,
}
@@ -125,6 +127,7 @@ impl FromStr for ReasoningEffort {
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
"xhigh" => Ok(Self::XHigh),
"max" => Ok(Self::Max),
"ultra" => Ok(Self::Ultra),
"" => Err("reasoning_effort must not be empty".to_string()),
effort => Ok(Self::Custom(effort.to_string())),
@@ -370,6 +373,8 @@ pub struct ModelInfo {
pub base_instructions: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_messages: Option<ModelMessages>,
#[serde(default)]
pub include_skills_usage_instructions: bool,
pub supports_reasoning_summaries: bool,
#[serde(default)]
pub default_reasoning_summary: ReasoningSummary,
@@ -665,6 +670,7 @@ mod tests {
upgrade: None,
base_instructions: "base".to_string(),
model_messages: spec,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: ReasoningSummary::Auto,
support_verbosity: false,
@@ -700,30 +706,35 @@ mod tests {
#[test]
fn reasoning_effort_accepts_known_and_custom_values() {
let custom = ReasoningEffort::Custom("max".to_string());
let deserialized = from_str::<ReasoningEffort>(r#""max""#)
let custom = ReasoningEffort::Custom("future".to_string());
let deserialized = from_str::<ReasoningEffort>(r#""future""#)
.expect("custom reasoning effort should deserialize");
let serialized = to_string(&custom).expect("custom reasoning effort should serialize");
let serialized_max = to_string(&ReasoningEffort::Max).expect("Max should serialize");
let serialized_ultra = to_string(&ReasoningEffort::Ultra).expect("Ultra should serialize");
assert_eq!(
(
"high".parse(),
"ultra".parse(),
"max".parse(),
"ultra".parse(),
"future".parse(),
deserialized,
serialized,
serialized_max,
serialized_ultra,
custom.to_string(),
),
(
Ok(ReasoningEffort::High),
Ok(ReasoningEffort::Max),
Ok(ReasoningEffort::Ultra),
Ok(custom.clone()),
custom,
r#""future""#.to_string(),
r#""max""#.to_string(),
r#""ultra""#.to_string(),
"max".to_string(),
"future".to_string(),
)
);
}
@@ -948,6 +959,7 @@ mod tests {
.expect("deserialize model info");
assert_eq!(model.availability_nux, None);
assert!(!model.include_skills_usage_instructions);
assert!(!model.supports_image_detail_original);
assert_eq!(model.web_search_tool_type, WebSearchToolType::Text);
assert!(!model.supports_search_tool);
+1
View File
@@ -27,6 +27,7 @@ fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
upgrade: None,
base_instructions: String::new(),
model_messages: None,
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
default_reasoning_summary: Default::default(),
support_verbosity: false,
@@ -505,6 +505,7 @@ impl ChatWidget {
ReasoningEffortConfig::Medium => "Medium".to_string(),
ReasoningEffortConfig::High => "High".to_string(),
ReasoningEffortConfig::XHigh => "Extra high".to_string(),
ReasoningEffortConfig::Max => "Max".to_string(),
ReasoningEffortConfig::Ultra => "Ultra".to_string(),
ReasoningEffortConfig::Custom(value) => value.clone(),
}
@@ -225,7 +225,7 @@ mod tests {
#[test]
fn next_reasoning_effort_uses_advertised_order_for_custom_levels() {
let custom_effort = ReasoningEffortConfig::Custom("max".to_string());
let custom_effort = ReasoningEffortConfig::Custom("future".to_string());
let choices = vec![
ReasoningEffortConfig::High,
ReasoningEffortConfig::Low,
@@ -5,7 +5,7 @@ use codex_app_server_protocol::ModelSafetyBufferingUpdatedNotification;
const SAFETY_BUFFERING_PROMPT_VIEW_ID: &str = "safety-buffering-prompt";
const SAFETY_BUFFERING_MESSAGE_WITH_RETRY: &str = "This request requires additional safety checks, which can take extra time. You can retry with a faster model for a quicker response, though it may be less capable of handling complex requests.";
const SAFETY_BUFFERING_MESSAGE_WITH_RETRY: &str = "This request requires additional safety checks, which can take extra time. Hang tight or retry with a faster model for a quicker response, though it may be less capable of handling complex requests.";
const SAFETY_BUFFERING_MESSAGE_WITHOUT_RETRY: &str =
"This request requires additional safety checks, which can take extra time.";
@@ -6,7 +6,7 @@ expression: popup
1. Low Fast responses with lighter reasoning
2. Medium (default) Balances speed and reasoning depth for everyday tasks
3. max Maximum available reasoning
3. Max Maximum available reasoning
› 4. High (current) Greater reasoning depth for complex problems
5. Extra high Extra high reasoning depth for complex problems
@@ -4,8 +4,8 @@ expression: popup
---
Additional safety checks
This request requires additional safety checks, which can take extra time.
You can retry with a faster model for a quicker response, though it may be
less capable of handling complex requests.
Hang tight or retry with a faster model for a quicker response, though it
may be less capable of handling complex requests.
› 1. Retry with a faster model
2. Keep waiting
@@ -146,7 +146,7 @@ async fn safety_buffering_offers_one_retry_with_app_wording() {
}
#[tokio::test]
async fn safety_buffering_stops_retrying_after_agent_message_starts() {
async fn safety_buffering_remains_visible_until_turn_completes() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let (thread_id, turn_id, _) = start_safety_buffering_test_turn(&mut chat, &mut op_rx);
chat.handle_server_notification(
@@ -162,6 +162,11 @@ async fn safety_buffering_stops_retrying_after_agent_message_starts() {
chat.on_agent_message_delta("Visible response".to_string());
assert!(!chat.can_retry_safety_buffered_turn(turn_id));
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Additional safety checks"));
handle_turn_completed(&mut chat, turn_id, /*duration_ms*/ None);
assert!(!render_bottom_popup(&chat, /*width*/ 80).contains("Additional safety checks"));
}
#[tokio::test]
@@ -1200,9 +1200,10 @@ async fn plugins_popup_admin_disabled_available_plugin_has_view_only_hint() {
}
#[tokio::test]
async fn plugins_popup_remote_section_fallback_states_snapshot() {
async fn plugins_popup_remote_section_fallback_states_when_remote_plugin_disabled_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
chat.set_feature_enabled(Feature::RemotePlugin, /*enabled*/ false);
let select_tab_containing = |chat: &mut ChatWidget, visible_text: &str| -> String {
for _ in 0..8 {
@@ -1266,7 +1267,6 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() {
let (mut remote_chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
remote_chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
remote_chat.set_feature_enabled(Feature::RemotePlugin, /*enabled*/ true);
remote_chat.add_plugins_output();
let remote_cwd = remote_chat.config.cwd.clone();
remote_chat.on_plugins_loaded(
@@ -3191,7 +3191,7 @@ async fn model_reasoning_selection_popup_snapshot() {
preset.supported_reasoning_efforts.insert(
2,
ReasoningEffortPreset {
effort: ReasoningEffortConfig::Custom("max".to_string()),
effort: ReasoningEffortConfig::Max,
description: "Maximum available reasoning".to_string(),
},
);
@@ -3204,7 +3204,7 @@ async fn model_reasoning_selection_popup_snapshot() {
#[tokio::test]
async fn model_reasoning_selection_popup_applies_custom_effort() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
let custom_effort = ReasoningEffortConfig::Custom("max".to_string());
let custom_effort = ReasoningEffortConfig::Custom("future".to_string());
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
let mut preset = get_available_model(&chat, "gpt-5.4");
@@ -170,6 +170,7 @@ impl ChatWidget {
self.input_queue.user_turn_pending_start = false;
self.clear_active_hook_cell();
self.turn_lifecycle.finish();
self.clear_safety_buffering();
self.update_task_running_state();
self.running_commands.clear();
self.suppressed_exec_calls.clear();