Files
codex/prs/bolinfest/PR-1645.md
2025-09-02 15:17:45 -07:00

839 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PR #1645: Add support for custom base instructions
- URL: https://github.com/openai/codex/pull/1645
- Author: pakrym-oai
- Created: 2025-07-22 00:49:03 UTC
- Updated: 2025-07-22 16:42:30 UTC
- Changes: +265/-62, Files changed: 12, Commits: 9
## Description
Allows providing custom instructions file as a config parameter and custom instruction text via MCP tool call.
## Full Diff
```diff
diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs
index 3e3c2e7efa..94d09e7fd4 100644
--- a/codex-rs/core/src/client_common.rs
+++ b/codex-rs/core/src/client_common.rs
@@ -34,11 +34,18 @@ pub struct Prompt {
/// the "fully qualified" tool name (i.e., prefixed with the server name),
/// which should be reported to the model in place of Tool::name.
pub extra_tools: HashMap<String, mcp_types::Tool>,
+
+ /// Optional override for the built-in BASE_INSTRUCTIONS.
+ pub base_instructions_override: Option<String>,
}
impl Prompt {
pub(crate) fn get_full_instructions(&self, model: &str) -> Cow<'_, str> {
- let mut sections: Vec<&str> = vec![BASE_INSTRUCTIONS];
+ let base = self
+ .base_instructions_override
+ .as_deref()
+ .unwrap_or(BASE_INSTRUCTIONS);
+ let mut sections: Vec<&str> = vec![base];
if let Some(ref user) = self.user_instructions {
sections.push(user);
}
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 392e84ea10..6eb1715fbf 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -108,13 +108,15 @@ impl Codex {
let (tx_sub, rx_sub) = async_channel::bounded(64);
let (tx_event, rx_event) = async_channel::bounded(1600);
- let instructions = get_user_instructions(&config).await;
+ let user_instructions = get_user_instructions(&config).await;
+
let configure_session = Op::ConfigureSession {
provider: config.model_provider.clone(),
model: config.model.clone(),
model_reasoning_effort: config.model_reasoning_effort,
model_reasoning_summary: config.model_reasoning_summary,
- instructions,
+ user_instructions,
+ base_instructions: config.base_instructions.clone(),
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
disable_response_storage: config.disable_response_storage,
@@ -183,7 +185,8 @@ pub(crate) struct Session {
/// the model as well as sandbox policies are resolved against this path
/// instead of `std::env::current_dir()`.
cwd: PathBuf,
- instructions: Option<String>,
+ base_instructions: Option<String>,
+ user_instructions: Option<String>,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
shell_environment_policy: ShellEnvironmentPolicy,
@@ -577,7 +580,8 @@ async fn submission_loop(
model,
model_reasoning_effort,
model_reasoning_summary,
- instructions,
+ user_instructions,
+ base_instructions,
approval_policy,
sandbox_policy,
disable_response_storage,
@@ -625,15 +629,17 @@ async fn submission_loop(
let rollout_recorder = match rollout_recorder {
Some(rec) => Some(rec),
- None => match RolloutRecorder::new(&config, session_id, instructions.clone())
- .await
- {
- Ok(r) => Some(r),
- Err(e) => {
- warn!("failed to initialise rollout recorder: {e}");
- None
+ None => {
+ match RolloutRecorder::new(&config, session_id, user_instructions.clone())
+ .await
+ {
+ Ok(r) => Some(r),
+ Err(e) => {
+ warn!("failed to initialise rollout recorder: {e}");
+ None
+ }
}
- },
+ }
};
let client = ModelClient::new(
@@ -699,7 +705,8 @@ async fn submission_loop(
client,
tx_event: tx_event.clone(),
ctrl_c: Arc::clone(&ctrl_c),
- instructions,
+ user_instructions,
+ base_instructions,
approval_policy,
sandbox_policy,
shell_environment_policy: config.shell_environment_policy.clone(),
@@ -1067,9 +1074,10 @@ async fn run_turn(
let prompt = Prompt {
input,
prev_id,
- user_instructions: sess.instructions.clone(),
+ user_instructions: sess.user_instructions.clone(),
store,
extra_tools,
+ base_instructions_override: sess.base_instructions.clone(),
};
let mut retries = 0;
diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
index f1d0dd9d60..8ed06c45af 100644
--- a/codex-rs/core/src/config.rs
+++ b/codex-rs/core/src/config.rs
@@ -63,7 +63,10 @@ pub struct Config {
pub disable_response_storage: bool,
/// User-provided instructions from instructions.md.
- pub instructions: Option<String>,
+ pub user_instructions: Option<String>,
+
+ /// Base instructions override.
+ pub base_instructions: Option<String>,
/// Optional external notifier command. When set, Codex will spawn this
/// program after each completed *turn* (i.e. when the agent finishes
@@ -327,6 +330,9 @@ pub struct ConfigToml {
/// Experimental rollout resume path (absolute path to .jsonl; undocumented).
pub experimental_resume: Option<PathBuf>,
+
+ /// Experimental path to a file whose contents replace the built-in BASE_INSTRUCTIONS.
+ pub experimental_instructions_file: Option<PathBuf>,
}
impl ConfigToml {
@@ -359,6 +365,7 @@ pub struct ConfigOverrides {
pub model_provider: Option<String>,
pub config_profile: Option<String>,
pub codex_linux_sandbox_exe: Option<PathBuf>,
+ pub base_instructions: Option<String>,
}
impl Config {
@@ -369,7 +376,7 @@ impl Config {
overrides: ConfigOverrides,
codex_home: PathBuf,
) -> std::io::Result<Self> {
- let instructions = Self::load_instructions(Some(&codex_home));
+ let user_instructions = Self::load_instructions(Some(&codex_home));
// Destructure ConfigOverrides fully to ensure all overrides are applied.
let ConfigOverrides {
@@ -380,6 +387,7 @@ impl Config {
model_provider,
config_profile: config_profile_key,
codex_linux_sandbox_exe,
+ base_instructions,
} = overrides;
let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) {
@@ -457,6 +465,10 @@ impl Config {
let experimental_resume = cfg.experimental_resume;
+ let base_instructions = base_instructions.or(Self::get_base_instructions(
+ cfg.experimental_instructions_file.as_ref(),
+ ));
+
let config = Self {
model,
model_context_window,
@@ -475,7 +487,8 @@ impl Config {
.or(cfg.disable_response_storage)
.unwrap_or(false),
notify: cfg.notify,
- instructions,
+ user_instructions,
+ base_instructions,
mcp_servers: cfg.mcp_servers,
model_providers,
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
@@ -525,6 +538,15 @@ impl Config {
}
})
}
+
+ fn get_base_instructions(path: Option<&PathBuf>) -> Option<String> {
+ let path = path.as_ref()?;
+
+ std::fs::read_to_string(path)
+ .ok()
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ }
}
fn default_model() -> String {
@@ -801,7 +823,7 @@ disable_response_storage = true
sandbox_policy: SandboxPolicy::new_read_only_policy(),
shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: false,
- instructions: None,
+ user_instructions: None,
notify: None,
cwd: fixture.cwd(),
mcp_servers: HashMap::new(),
@@ -818,6 +840,7 @@ disable_response_storage = true
model_supports_reasoning_summaries: false,
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
experimental_resume: None,
+ base_instructions: None,
},
o3_profile_config
);
@@ -848,7 +871,7 @@ disable_response_storage = true
sandbox_policy: SandboxPolicy::new_read_only_policy(),
shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: false,
- instructions: None,
+ user_instructions: None,
notify: None,
cwd: fixture.cwd(),
mcp_servers: HashMap::new(),
@@ -865,6 +888,7 @@ disable_response_storage = true
model_supports_reasoning_summaries: false,
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
experimental_resume: None,
+ base_instructions: None,
};
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
@@ -910,7 +934,7 @@ disable_response_storage = true
sandbox_policy: SandboxPolicy::new_read_only_policy(),
shell_environment_policy: ShellEnvironmentPolicy::default(),
disable_response_storage: true,
- instructions: None,
+ user_instructions: None,
notify: None,
cwd: fixture.cwd(),
mcp_servers: HashMap::new(),
@@ -927,6 +951,7 @@ disable_response_storage = true
model_supports_reasoning_summaries: false,
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
experimental_resume: None,
+ base_instructions: None,
};
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
diff --git a/codex-rs/core/src/project_doc.rs b/codex-rs/core/src/project_doc.rs
index ab9d46186f..9f46159d1d 100644
--- a/codex-rs/core/src/project_doc.rs
+++ b/codex-rs/core/src/project_doc.rs
@@ -27,16 +27,16 @@ const PROJECT_DOC_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";
/// string of instructions.
pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
match find_project_doc(config).await {
- Ok(Some(project_doc)) => match &config.instructions {
+ Ok(Some(project_doc)) => match &config.user_instructions {
Some(original_instructions) => Some(format!(
"{original_instructions}{PROJECT_DOC_SEPARATOR}{project_doc}"
)),
None => Some(project_doc),
},
- Ok(None) => config.instructions.clone(),
+ Ok(None) => config.user_instructions.clone(),
Err(e) => {
error!("error trying to find project doc: {e:#}");
- config.instructions.clone()
+ config.user_instructions.clone()
}
}
}
@@ -159,7 +159,7 @@ mod tests {
config.cwd = root.path().to_path_buf();
config.project_doc_max_bytes = limit;
- config.instructions = instructions.map(ToOwned::to_owned);
+ config.user_instructions = instructions.map(ToOwned::to_owned);
config
}
diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs
index 08d55b9749..9f6e004b67 100644
--- a/codex-rs/core/src/protocol.rs
+++ b/codex-rs/core/src/protocol.rs
@@ -44,8 +44,12 @@ pub enum Op {
model_reasoning_effort: ReasoningEffortConfig,
model_reasoning_summary: ReasoningSummaryConfig,
- /// Model instructions
- instructions: Option<String>,
+ /// Model instructions that are appended to the base instructions.
+ user_instructions: Option<String>,
+
+ /// Base instructions override.
+ base_instructions: Option<String>,
+
/// When to escalate for approval for execution
approval_policy: AskForApproval,
/// How to sandbox commands executed in the system
diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs
index fe4710c89b..5a6b6100eb 100644
--- a/codex-rs/core/tests/client.rs
+++ b/codex-rs/core/tests/client.rs
@@ -1,5 +1,3 @@
-use std::time::Duration;
-
use codex_core::Codex;
use codex_core::ModelProviderInfo;
use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
@@ -11,7 +9,6 @@ mod test_support;
use tempfile::TempDir;
use test_support::load_default_config_for_test;
use test_support::load_sse_fixture_with_id;
-use tokio::time::timeout;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
@@ -86,21 +83,15 @@ async fn includes_session_id_and_model_headers_in_request() {
.await
.unwrap();
- let mut current_session_id = None;
- // Wait for TaskComplete
- loop {
- let ev = timeout(Duration::from_secs(1), codex.next_event())
+ let EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, .. }) =
+ test_support::wait_for_event(&codex, |ev| matches!(ev, EventMsg::SessionConfigured(_)))
.await
- .unwrap()
- .unwrap();
-
- if let EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, .. }) = ev.msg {
- current_session_id = Some(session_id.to_string());
- }
- if matches!(ev.msg, EventMsg::TaskComplete(_)) {
- break;
- }
- }
+ else {
+ unreachable!()
+ };
+
+ let current_session_id = Some(session_id.to_string());
+ test_support::wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
// get request from the server
let request = &server.received_requests().await.unwrap()[0];
@@ -108,6 +99,76 @@ async fn includes_session_id_and_model_headers_in_request() {
let originator = request.headers.get("originator").unwrap();
assert!(current_session_id.is_some());
- assert_eq!(request_body.to_str().unwrap(), &current_session_id.unwrap());
+ assert_eq!(
+ request_body.to_str().unwrap(),
+ current_session_id.as_ref().unwrap()
+ );
assert_eq!(originator.to_str().unwrap(), "codex_cli_rs");
}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn includes_base_instructions_override_in_request() {
+ #![allow(clippy::unwrap_used)]
+
+ // Mock server
+ let server = MockServer::start().await;
+
+ // First request must NOT include `previous_response_id`.
+ let first = ResponseTemplate::new(200)
+ .insert_header("content-type", "text/event-stream")
+ .set_body_raw(sse_completed("resp1"), "text/event-stream");
+
+ Mock::given(method("POST"))
+ .and(path("/v1/responses"))
+ .respond_with(first)
+ .expect(1)
+ .mount(&server)
+ .await;
+
+ let model_provider = ModelProviderInfo {
+ name: "openai".into(),
+ base_url: format!("{}/v1", server.uri()),
+ // Environment variable that should exist in the test environment.
+ // ModelClient will return an error if the environment variable for the
+ // provider is not set.
+ env_key: Some("PATH".into()),
+ env_key_instructions: None,
+ wire_api: codex_core::WireApi::Responses,
+ query_params: None,
+ http_headers: None,
+ env_http_headers: None,
+ request_max_retries: Some(0),
+ stream_max_retries: Some(0),
+ stream_idle_timeout_ms: None,
+ };
+
+ let codex_home = TempDir::new().unwrap();
+ let mut config = load_default_config_for_test(&codex_home);
+
+ config.base_instructions = Some("test instructions".to_string());
+ config.model_provider = model_provider;
+
+ let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
+ let (codex, ..) = Codex::spawn(config, ctrl_c.clone()).await.unwrap();
+
+ codex
+ .submit(Op::UserInput {
+ items: vec![InputItem::Text {
+ text: "hello".into(),
+ }],
+ })
+ .await
+ .unwrap();
+
+ test_support::wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
+
+ let request = &server.received_requests().await.unwrap()[0];
+ let request_body = request.body_json::<serde_json::Value>().unwrap();
+
+ assert!(
+ request_body["instructions"]
+ .as_str()
+ .unwrap()
+ .contains("test instructions")
+ );
+}
diff --git a/codex-rs/core/tests/test_support.rs b/codex-rs/core/tests/test_support.rs
index 7d1e3a7fef..83b8a14793 100644
--- a/codex-rs/core/tests/test_support.rs
+++ b/codex-rs/core/tests/test_support.rs
@@ -76,3 +76,24 @@ pub fn load_sse_fixture_with_id(path: impl AsRef<std::path::Path>, id: &str) ->
})
.collect()
}
+
+#[allow(dead_code)]
+pub async fn wait_for_event<F>(
+ codex: &codex_core::Codex,
+ mut predicate: F,
+) -> codex_core::protocol::EventMsg
+where
+ F: FnMut(&codex_core::protocol::EventMsg) -> bool,
+{
+ use tokio::time::Duration;
+ use tokio::time::timeout;
+ loop {
+ let ev = timeout(Duration::from_secs(1), codex.next_event())
+ .await
+ .expect("timeout waiting for event")
+ .expect("stream ended unexpectedly");
+ if predicate(&ev.msg) {
+ return ev.msg;
+ }
+ }
+}
diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs
index 769d3c3b01..620ab82327 100644
--- a/codex-rs/exec/src/lib.rs
+++ b/codex-rs/exec/src/lib.rs
@@ -110,6 +110,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)),
model_provider: None,
codex_linux_sandbox_exe,
+ base_instructions: None,
};
// Parse `-c` overrides.
let cli_kv_overrides = match config_overrides.parse_overrides() {
diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs
index 54d108c0fd..6357c94bd1 100644
--- a/codex-rs/mcp-server/src/codex_tool_config.rs
+++ b/codex-rs/mcp-server/src/codex_tool_config.rs
@@ -14,7 +14,7 @@ use std::path::PathBuf;
use crate::json_to_toml::json_to_toml;
/// Client-supplied configuration for a `codex` tool-call.
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "kebab-case")]
pub struct CodexToolCallParam {
/// The *initial user prompt* to start the Codex conversation.
@@ -46,6 +46,10 @@ pub struct CodexToolCallParam {
/// CODEX_HOME/config.toml.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<HashMap<String, serde_json::Value>>,
+
+ /// The set of instructions to use instead of the default ones.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub base_instructions: Option<String>,
}
/// Custom enum mirroring [`AskForApproval`], but has an extra dependency on
@@ -135,6 +139,7 @@ impl CodexToolCallParam {
approval_policy,
sandbox,
config: cli_overrides,
+ base_instructions,
} = self;
// Build the `ConfigOverrides` recognised by codex-core.
@@ -146,6 +151,7 @@ impl CodexToolCallParam {
sandbox_mode: sandbox.map(Into::into),
model_provider: None,
codex_linux_sandbox_exe,
+ base_instructions,
};
let cli_overrides = cli_overrides
@@ -268,6 +274,10 @@ mod tests {
"description": "The *initial user prompt* to start the Codex conversation.",
"type": "string"
},
+ "base-instructions": {
+ "description": "The set of instructions to use instead of the default ones.",
+ "type": "string"
+ },
},
"required": [
"prompt"
diff --git a/codex-rs/mcp-server/tests/elicitation.rs b/codex-rs/mcp-server/tests/codex_tool.rs
similarity index 81%
rename from codex-rs/mcp-server/tests/elicitation.rs
rename to codex-rs/mcp-server/tests/codex_tool.rs
index ac9435e874..d36813ce9f 100644
--- a/codex-rs/mcp-server/tests/elicitation.rs
+++ b/codex-rs/mcp-server/tests/codex_tool.rs
@@ -8,6 +8,7 @@ use std::path::PathBuf;
use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_core::protocol::FileChange;
use codex_core::protocol::ReviewDecision;
+use codex_mcp_server::CodexToolCallParam;
use codex_mcp_server::ExecApprovalElicitRequestParams;
use codex_mcp_server::ExecApprovalResponse;
use codex_mcp_server::PatchApprovalElicitRequestParams;
@@ -76,7 +77,10 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> {
// In turn, it should reply with a tool call, which the MCP should forward
// as an elicitation.
let codex_request_id = mcp_process
- .send_codex_tool_call(None, "run `git init`")
+ .send_codex_tool_call(CodexToolCallParam {
+ prompt: "run `git init`".to_string(),
+ ..Default::default()
+ })
.await?;
let elicitation_request = timeout(
DEFAULT_READ_TIMEOUT,
@@ -209,10 +213,11 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> {
// Send a "codex" tool request that will trigger the apply_patch command
let codex_request_id = mcp_process
- .send_codex_tool_call(
- Some(cwd.path().to_string_lossy().to_string()),
- "please modify the test file",
- )
+ .send_codex_tool_call(CodexToolCallParam {
+ cwd: Some(cwd.path().to_string_lossy().to_string()),
+ prompt: "please modify the test file".to_string(),
+ ..Default::default()
+ })
.await?;
let elicitation_request = timeout(
DEFAULT_READ_TIMEOUT,
@@ -279,6 +284,75 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> {
Ok(())
}
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_codex_tool_passes_base_instructions() {
+ if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
+ println!(
+ "Skipping test because it cannot execute when network is disabled in a Codex sandbox."
+ );
+ return;
+ }
+
+ // Apparently `#[tokio::test]` must return `()`, so we create a helper
+ // function that returns `Result` so we can use `?` in favor of `unwrap`.
+ if let Err(err) = codex_tool_passes_base_instructions().await {
+ panic!("failure: {err}");
+ }
+}
+
+async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> {
+ #![allow(clippy::unwrap_used)]
+
+ let server =
+ create_mock_chat_completions_server(vec![create_final_assistant_message_sse_response(
+ "Enjoy!",
+ )?])
+ .await;
+
+ // Run `codex mcp` with a specific config.toml.
+ let codex_home = TempDir::new()?;
+ create_config_toml(codex_home.path(), &server.uri())?;
+ let mut mcp_process = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, mcp_process.initialize()).await??;
+
+ // Send a "codex" tool request, which should hit the completions endpoint.
+ let codex_request_id = mcp_process
+ .send_codex_tool_call(CodexToolCallParam {
+ prompt: "How are you?".to_string(),
+ base_instructions: Some("You are a helpful assistant.".to_string()),
+ ..Default::default()
+ })
+ .await?;
+
+ let codex_response = timeout(
+ DEFAULT_READ_TIMEOUT,
+ mcp_process.read_stream_until_response_message(RequestId::Integer(codex_request_id)),
+ )
+ .await??;
+ assert_eq!(
+ JSONRPCResponse {
+ jsonrpc: JSONRPC_VERSION.into(),
+ id: RequestId::Integer(codex_request_id),
+ result: json!({
+ "content": [
+ {
+ "text": "Enjoy!",
+ "type": "text"
+ }
+ ]
+ }),
+ },
+ codex_response
+ );
+
+ let requests = server.received_requests().await.unwrap();
+ let request = requests[0].body_json::<serde_json::Value>().unwrap();
+ let instructions = request["messages"][0]["content"].as_str().unwrap();
+ assert!(instructions.starts_with("You are a helpful assistant."));
+
+ Ok(())
+}
+
fn create_expected_patch_approval_elicitation_request(
elicitation_request_id: RequestId,
changes: HashMap<PathBuf, FileChange>,
diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs
index df9cc98acf..a86deaab75 100644
--- a/codex-rs/mcp-server/tests/common/mcp_process.rs
+++ b/codex-rs/mcp-server/tests/common/mcp_process.rs
@@ -141,20 +141,11 @@ impl McpProcess {
/// correlating notifications.
pub async fn send_codex_tool_call(
&mut self,
- cwd: Option<String>,
- prompt: &str,
+ params: CodexToolCallParam,
) -> anyhow::Result<i64> {
let codex_tool_call_params = CallToolRequestParams {
name: "codex".to_string(),
- arguments: Some(serde_json::to_value(CodexToolCallParam {
- cwd,
- prompt: prompt.to_string(),
- model: None,
- profile: None,
- approval_policy: None,
- sandbox: None,
- config: None,
- })?),
+ arguments: Some(serde_json::to_value(params)?),
};
self.send_request(
mcp_types::CallToolRequest::METHOD,
diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs
index 4ca305b35e..05a55edc7b 100644
--- a/codex-rs/tui/src/lib.rs
+++ b/codex-rs/tui/src/lib.rs
@@ -75,6 +75,7 @@ pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> std::io::
model_provider: None,
config_profile: cli.config_profile.clone(),
codex_linux_sandbox_exe,
+ base_instructions: None,
};
// Parse `-c` overrides from the CLI.
let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
```
## Review Comments
### codex-rs/core/tests/client.rs
- Created: 2025-07-22 16:19:17 UTC | Link: https://github.com/openai/codex/pull/1645#discussion_r2223088011
```diff
@@ -11,7 +9,7 @@ mod test_support;
use tempfile::TempDir;
use test_support::load_default_config_for_test;
use test_support::load_sse_fixture_with_id;
-use tokio::time::timeout;
+
```
> remove blank line?
- Created: 2025-07-22 16:21:43 UTC | Link: https://github.com/openai/codex/pull/1645#discussion_r2223093798
```diff
@@ -86,28 +84,92 @@ async fn includes_session_id_and_model_headers_in_request() {
.await
.unwrap();
- let mut current_session_id = None;
- // Wait for TaskComplete
- loop {
- let ev = timeout(Duration::from_secs(1), codex.next_event())
+ let EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, .. }) =
+ test_support::wait_for_event(&codex, |ev| matches!(ev, EventMsg::SessionConfigured(_)))
.await
- .unwrap()
- .unwrap();
-
- if let EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, .. }) = ev.msg {
- current_session_id = Some(session_id.to_string());
- }
- if matches!(ev.msg, EventMsg::TaskComplete(_)) {
- break;
- }
- }
+ else {
+ unreachable!()
+ };
+
+ let current_session_id = Some(session_id.to_string());
+ test_support::wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
// get request from the server
let request = &server.received_requests().await.unwrap()[0];
let request_body = request.headers.get("session_id").unwrap();
let originator = request.headers.get("originator").unwrap();
assert!(current_session_id.is_some());
- assert_eq!(request_body.to_str().unwrap(), &current_session_id.unwrap());
+ assert_eq!(
+ request_body.to_str().unwrap(),
+ current_session_id.as_ref().unwrap()
+ );
assert_eq!(originator.to_str().unwrap(), "codex_cli_rs");
}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn includes_base_instructions_override_in_request() {
+ #![allow(clippy::unwrap_used)]
+
+ // Mock server
+ let server = MockServer::start().await;
+
+ // First request must NOT include `previous_response_id`.
+ let first = ResponseTemplate::new(200)
+ .insert_header("content-type", "text/event-stream")
+ .set_body_raw(sse_completed("resp1"), "text/event-stream");
+
+ Mock::given(method("POST"))
+ .and(path("/v1/responses"))
+ .respond_with(first)
+ .expect(1)
+ .mount(&server)
+ .await;
+
+ let model_provider = ModelProviderInfo {
+ name: "openai".into(),
+ base_url: format!("{}/v1", server.uri()),
+ // Environment variable that should exist in the test environment.
+ // ModelClient will return an error if the environment variable for the
+ // provider is not set.
+ env_key: Some("PATH".into()),
+ env_key_instructions: None,
+ wire_api: codex_core::WireApi::Responses,
+ query_params: None,
+ http_headers: None,
+ env_http_headers: None,
+ request_max_retries: Some(0),
+ stream_max_retries: Some(0),
+ stream_idle_timeout_ms: None,
+ };
+
+ let codex_home = TempDir::new().unwrap();
+ let mut config = load_default_config_for_test(&codex_home);
+
+ config.base_instructions = Some("test instructions".to_string());
+ config.model_provider = model_provider;
+
+ let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
+ let (codex, _init_id) = Codex::spawn(config, ctrl_c.clone()).await.unwrap();
```
> This tuple has a third value now, right?
### codex-rs/core/tests/test_support.rs
- Created: 2025-07-22 16:22:32 UTC | Link: https://github.com/openai/codex/pull/1645#discussion_r2223096525
```diff
@@ -76,3 +76,24 @@ pub fn load_sse_fixture_with_id(path: impl AsRef<std::path::Path>, id: &str) ->
})
.collect()
}
+
+#[allow(dead_code)]
```
> May we need an "integration tests util" crate so we don't have to do this `dead_code` thing, which is distracting/misleading.
### codex-rs/mcp-server/src/codex_tool_config.rs
- Created: 2025-07-22 16:23:13 UTC | Link: https://github.com/openai/codex/pull/1645#discussion_r2223099237
```diff
@@ -46,6 +46,10 @@ pub struct CodexToolCallParam {
/// CODEX_HOME/config.toml.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<HashMap<String, serde_json::Value>>,
+
+ /// The set of instructions to use instead of the default ones.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub base_instructions: Option<String>,
```
> So the idea is that this is the literal value of the instructions rather than the path to file on disk, correct?