add requirements.toml commandline option

This commit is contained in:
MJR
2026-03-02 11:18:45 -05:00
parent c2e126f92a
commit 7d6cb71ec0
37 changed files with 646 additions and 35 deletions
+10
View File
@@ -89,6 +89,16 @@ codex --sandbox danger-full-access
The same setting can be persisted in `~/.codex/config.toml` via the top-level `sandbox_mode = "MODE"` key, e.g. `sandbox_mode = "workspace-write"`.
### Session-scoped requirements
To apply an additional `requirements.toml` file to just the current interactive session, pass:
```shell
codex --requirements-toml /path/to/requirements.toml
```
This only affects the current session and does not modify saved config.
## Code Organization
This folder is the root of a Cargo workspace. It contains quite a bit of experimental code, but here are the key crates:
@@ -1923,6 +1923,12 @@
"null"
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -2145,6 +2151,12 @@
}
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -2284,6 +2296,12 @@
}
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -12713,6 +12713,12 @@
"null"
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -13696,6 +13702,12 @@
}
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -13907,6 +13919,12 @@
}
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -102,6 +102,12 @@
"null"
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -900,6 +900,12 @@
}
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -141,6 +141,12 @@
}
]
},
"requirementsToml": {
"type": [
"string",
"null"
]
},
"sandbox": {
"anyOf": [
{
@@ -5,4 +5,4 @@ import type { AskForApproval } from "./AskForApproval";
import type { SandboxMode } from "./SandboxMode";
import type { JsonValue } from "./serde_json/JsonValue";
export type NewConversationParams = { model: string | null, modelProvider: string | null, profile: string | null, cwd: string | null, approvalPolicy: AskForApproval | null, sandbox: SandboxMode | null, config: { [key in string]?: JsonValue } | null, baseInstructions: string | null, developerInstructions: string | null, compactPrompt: string | null, includeApplyPatchTool: boolean | null, };
export type NewConversationParams = { model: string | null, modelProvider: string | null, profile: string | null, requirementsToml: string | null, cwd: string | null, approvalPolicy: AskForApproval | null, sandbox: SandboxMode | null, config: { [key in string]?: JsonValue } | null, baseInstructions: string | null, developerInstructions: string | null, compactPrompt: string | null, includeApplyPatchTool: boolean | null, };
@@ -21,7 +21,7 @@ export type ThreadForkParams = {threadId: string, /**
path?: string | null, /**
* Configuration overrides for the forked thread, if any.
*/
model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, /**
model?: string | null, modelProvider?: string | null, requirementsToml?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, /**
* If true, persist additional rollout EventMsg variants required to
* reconstruct a richer thread history on subsequent resume/fork/read.
*/
@@ -30,7 +30,7 @@ history?: Array<ResponseItem> | null, /**
path?: string | null, /**
* Configuration overrides for the resumed thread, if any.
*/
model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
model?: string | null, modelProvider?: string | null, requirementsToml?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
* If true, persist additional rollout EventMsg variants required to
* reconstruct a richer thread history on subsequent resume/fork/read.
*/
@@ -6,7 +6,7 @@ import type { JsonValue } from "../serde_json/JsonValue";
import type { AskForApproval } from "./AskForApproval";
import type { SandboxMode } from "./SandboxMode";
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, requirementsToml?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
* If true, opt into emitting raw Responses API items on the event stream.
* This is for internal use only (e.g. Codex Cloud).
*/
@@ -919,6 +919,7 @@ mod tests {
model: Some("gpt-5.1-codex-max".to_string()),
model_provider: None,
profile: None,
requirements_toml: None,
cwd: None,
approval_policy: Some(AskForApproval::OnRequest),
sandbox: None,
@@ -937,6 +938,7 @@ mod tests {
"model": "gpt-5.1-codex-max",
"modelProvider": null,
"profile": null,
"requirementsToml": null,
"cwd": null,
"approvalPolicy": "on-request",
"sandbox": null,
@@ -70,6 +70,7 @@ pub struct NewConversationParams {
pub model: Option<String>,
pub model_provider: Option<String>,
pub profile: Option<String>,
pub requirements_toml: Option<String>,
pub cwd: Option<String>,
pub approval_policy: Option<AskForApproval>,
pub sandbox: Option<SandboxMode>,
@@ -1789,6 +1789,8 @@ pub struct ThreadStartParams {
#[ts(optional = nullable)]
pub model_provider: Option<String>,
#[ts(optional = nullable)]
pub requirements_toml: Option<String>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
@@ -1892,6 +1894,8 @@ pub struct ThreadResumeParams {
#[ts(optional = nullable)]
pub model_provider: Option<String>,
#[ts(optional = nullable)]
pub requirements_toml: Option<String>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
@@ -1952,6 +1956,8 @@ pub struct ThreadForkParams {
#[ts(optional = nullable)]
pub model_provider: Option<String>,
#[ts(optional = nullable)]
pub requirements_toml: Option<String>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
+9 -3
View File
@@ -63,6 +63,7 @@ Use the thread APIs to create, list, or archive conversations. Drive a conversat
- Initialize once per connection: Immediately after opening a transport connection, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request on that connection before this handshake gets rejected.
- Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and you’ll also get a `thread/started` notification. If you’re continuing an existing conversation, call `thread/resume` with its ID instead. If you want to branch from an existing conversation, call `thread/fork` to create a new thread id with copied history.
The returned `thread.ephemeral` flag tells you whether the session is intentionally in-memory only; when it is `true`, `thread.path` is `null`.
`requirementsToml` is available on `thread/start`, `thread/resume`, and `thread/fork` to apply an additional session-scoped requirements file without changing persisted config.
- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, etc. This immediately returns the new turn object and triggers a `turn/started` notification.
- Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. You’ll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes).
- Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage.
@@ -175,6 +176,7 @@ Start a fresh thread when you need a new Codex conversation.
// current config settings.
"model": "gpt-5.1-codex",
"cwd": "/Users/me/project",
"requirementsToml": "/Users/me/policies/session-requirements.toml",
"approvalPolicy": "never",
"sandbox": "workspaceWrite",
"personality": "friendly",
@@ -207,12 +209,13 @@ Start a fresh thread when you need a new Codex conversation.
Valid `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. When `"none"` is selected, the personality placeholder is replaced with an empty string.
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted. You can also pass the same configuration overrides supported by `thread/start`, such as `personality`:
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted. You can also pass the same configuration overrides supported by `thread/start`, such as `personality` or `requirementsToml`:
```json
{ "method": "thread/resume", "id": 11, "params": {
"threadId": "thr_123",
"personality": "friendly"
"personality": "friendly",
"requirementsToml": "/Users/me/policies/session-requirements.toml"
} }
{ "id": 11, "result": { "thread": { "id": "thr_123", … } } }
```
@@ -220,7 +223,10 @@ To continue a stored session, call `thread/resume` with the `thread.id` you prev
To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it:
```json
{ "method": "thread/fork", "id": 12, "params": { "threadId": "thr_123" } }
{ "method": "thread/fork", "id": 12, "params": {
"threadId": "thr_123",
"requirementsToml": "/Users/me/policies/session-requirements.toml"
} }
{ "id": 12, "result": { "thread": { "id": "thr_456", … } } }
{ "method": "thread/started", "params": { "thread": { … } } }
```
@@ -211,6 +211,7 @@ use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::types::McpServerTransportConfig;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::LoaderOverrides;
use codex_core::default_client::get_codex_user_agent;
use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::error::CodexErr;
@@ -376,6 +377,7 @@ pub(crate) struct CodexMessageProcessor {
config: Arc<Config>,
single_client_mode: bool,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
active_login: Arc<Mutex<Option<ActiveLogin>>>,
pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
@@ -417,6 +419,7 @@ pub(crate) struct CodexMessageProcessorArgs {
pub(crate) arg0_paths: Arg0DispatchPaths,
pub(crate) config: Arc<Config>,
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
pub(crate) loader_overrides: LoaderOverrides,
pub(crate) cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
pub(crate) single_client_mode: bool,
pub(crate) feedback: CodexFeedback,
@@ -462,6 +465,7 @@ impl CodexMessageProcessor {
arg0_paths,
config,
cli_overrides,
loader_overrides,
cloud_requirements,
single_client_mode,
feedback,
@@ -474,6 +478,7 @@ impl CodexMessageProcessor {
config,
single_client_mode,
cli_overrides,
loader_overrides,
cloud_requirements,
active_login: Arc::new(Mutex::new(None)),
pending_thread_unloads: Arc::new(Mutex::new(HashSet::new())),
@@ -489,6 +494,8 @@ impl CodexMessageProcessor {
let cloud_requirements = self.current_cloud_requirements();
let mut config = codex_core::config::ConfigBuilder::default()
.cli_overrides(self.cli_overrides.clone())
.loader_overrides(self.loader_overrides.clone())
.fallback_cwd(Some(self.config.codex_home.clone()))
.cloud_requirements(cloud_requirements)
.build()
.await
@@ -509,6 +516,12 @@ impl CodexMessageProcessor {
.unwrap_or_default()
}
fn loader_overrides_for_session(&self, requirements_toml: Option<String>) -> LoaderOverrides {
let mut loader_overrides = self.loader_overrides.clone();
loader_overrides.requirements_toml_file = requirements_toml.map(PathBuf::from);
loader_overrides
}
/// If a client sends `developer_instructions: null` during a mode switch,
/// use the built-in instructions for that mode.
fn normalize_turn_start_collaboration_mode(
@@ -1134,6 +1147,7 @@ impl CodexMessageProcessor {
let chatgpt_base_url = self.config.chatgpt_base_url.clone();
let codex_home = self.config.codex_home.clone();
let cli_overrides = self.cli_overrides.clone();
let loader_overrides = self.loader_overrides.clone();
let auth_url = server.auth_url.clone();
tokio::spawn(async move {
let (success, error_msg) = match tokio::time::timeout(
@@ -1167,10 +1181,12 @@ impl CodexMessageProcessor {
cloud_requirements.as_ref(),
auth_manager.clone(),
chatgpt_base_url,
codex_home,
codex_home.clone(),
);
sync_default_client_residency_requirement(
&cli_overrides,
&codex_home,
&loader_overrides,
cloud_requirements.as_ref(),
)
.await;
@@ -1242,6 +1258,7 @@ impl CodexMessageProcessor {
let chatgpt_base_url = self.config.chatgpt_base_url.clone();
let codex_home = self.config.codex_home.clone();
let cli_overrides = self.cli_overrides.clone();
let loader_overrides = self.loader_overrides.clone();
let auth_url = server.auth_url.clone();
tokio::spawn(async move {
let (success, error_msg) = match tokio::time::timeout(
@@ -1275,10 +1292,12 @@ impl CodexMessageProcessor {
cloud_requirements.as_ref(),
auth_manager.clone(),
chatgpt_base_url,
codex_home,
codex_home.clone(),
);
sync_default_client_residency_requirement(
&cli_overrides,
&codex_home,
&loader_overrides,
cloud_requirements.as_ref(),
)
.await;
@@ -1449,6 +1468,8 @@ impl CodexMessageProcessor {
);
sync_default_client_residency_requirement(
&self.cli_overrides,
&self.config.codex_home,
&self.loader_overrides,
self.cloud_requirements.as_ref(),
)
.await;
@@ -1939,6 +1960,7 @@ impl CodexMessageProcessor {
model,
model_provider,
profile,
requirements_toml,
cwd,
approval_policy,
sandbox: sandbox_mode,
@@ -1989,6 +2011,7 @@ impl CodexMessageProcessor {
&self.cli_overrides,
Some(request_overrides),
typesafe_overrides,
self.loader_overrides_for_session(requirements_toml),
&cloud_requirements,
)
.await
@@ -2047,6 +2070,7 @@ impl CodexMessageProcessor {
let ThreadStartParams {
model,
model_provider,
requirements_toml,
cwd,
approval_policy,
sandbox,
@@ -2073,6 +2097,7 @@ impl CodexMessageProcessor {
);
typesafe_overrides.ephemeral = ephemeral;
let cli_overrides = self.cli_overrides.clone();
let loader_overrides = self.loader_overrides_for_session(requirements_toml);
let cloud_requirements = self.current_cloud_requirements();
let listener_task_context = ListenerTaskContext {
thread_manager: Arc::clone(&self.thread_manager),
@@ -2088,6 +2113,7 @@ impl CodexMessageProcessor {
Self::thread_start_task(
listener_task_context,
cli_overrides,
loader_overrides,
cloud_requirements,
request_id,
config,
@@ -2105,6 +2131,7 @@ impl CodexMessageProcessor {
async fn thread_start_task(
listener_task_context: ListenerTaskContext,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
request_id: ConnectionRequestId,
config_overrides: Option<HashMap<String, serde_json::Value>>,
@@ -2118,6 +2145,7 @@ impl CodexMessageProcessor {
&cli_overrides,
config_overrides,
typesafe_overrides,
loader_overrides,
&cloud_requirements,
)
.await
@@ -3023,6 +3051,7 @@ impl CodexMessageProcessor {
path,
model,
model_provider,
requirements_toml,
cwd,
approval_policy,
sandbox,
@@ -3070,6 +3099,7 @@ impl CodexMessageProcessor {
request_overrides,
typesafe_overrides,
history_cwd,
self.loader_overrides_for_session(requirements_toml),
&cloud_requirements,
)
.await
@@ -3190,6 +3220,16 @@ impl CodexMessageProcessor {
.await;
return true;
}
if params.requirements_toml.is_some() {
self.send_invalid_request_error(
request_id,
format!(
"cannot resume running thread {existing_thread_id} with requirementsToml while it is already running"
),
)
.await;
return true;
}
let rollout_path = if let Some(path) = existing_thread.rollout_path() {
if path.exists() {
@@ -3460,6 +3500,7 @@ impl CodexMessageProcessor {
path,
model,
model_provider,
requirements_toml,
cwd,
approval_policy,
sandbox,
@@ -3554,6 +3595,7 @@ impl CodexMessageProcessor {
request_overrides,
typesafe_overrides,
history_cwd,
self.loader_overrides_for_session(requirements_toml),
&cloud_requirements,
)
.await
@@ -4478,12 +4520,13 @@ impl CodexMessageProcessor {
};
let history_cwd = thread_history.session_cwd();
let (typesafe_overrides, request_overrides) = match overrides {
let (typesafe_overrides, request_overrides, loader_overrides) = match overrides {
Some(overrides) => {
let NewConversationParams {
model,
model_provider,
profile,
requirements_toml,
cwd,
approval_policy,
sandbox: sandbox_mode,
@@ -4529,7 +4572,11 @@ impl CodexMessageProcessor {
include_apply_patch_tool,
..Default::default()
};
(typesafe_overrides, Some(request_overrides))
(
typesafe_overrides,
Some(request_overrides),
self.loader_overrides_for_session(requirements_toml),
)
}
None => (
ConfigOverrides {
@@ -4538,6 +4585,7 @@ impl CodexMessageProcessor {
..Default::default()
},
None,
self.loader_overrides.clone(),
),
};
@@ -4547,6 +4595,7 @@ impl CodexMessageProcessor {
request_overrides,
typesafe_overrides,
history_cwd,
loader_overrides,
&cloud_requirements,
)
.await
@@ -4670,12 +4719,13 @@ impl CodexMessageProcessor {
read_history_cwd_from_state_db(&self.config, source_thread_id, rollout_path.as_path())
.await;
let (typesafe_overrides, request_overrides) = match overrides {
let (typesafe_overrides, request_overrides, loader_overrides) = match overrides {
Some(overrides) => {
let NewConversationParams {
model,
model_provider,
profile,
requirements_toml,
cwd,
approval_policy,
sandbox: sandbox_mode,
@@ -4727,7 +4777,11 @@ impl CodexMessageProcessor {
..Default::default()
};
(overrides, request_overrides)
(
overrides,
request_overrides,
self.loader_overrides_for_session(requirements_toml),
)
}
None => (
ConfigOverrides {
@@ -4736,6 +4790,7 @@ impl CodexMessageProcessor {
..Default::default()
},
None,
self.loader_overrides.clone(),
),
};
@@ -4745,6 +4800,7 @@ impl CodexMessageProcessor {
request_overrides,
typesafe_overrides,
history_cwd,
loader_overrides,
&cloud_requirements,
)
.await
@@ -7426,6 +7482,8 @@ fn replace_cloud_requirements_loader(
async fn sync_default_client_residency_requirement(
cli_overrides: &[(String, TomlValue)],
codex_home: &Path,
loader_overrides: &LoaderOverrides,
cloud_requirements: &RwLock<CloudRequirementsLoader>,
) {
let loader = cloud_requirements
@@ -7434,6 +7492,8 @@ async fn sync_default_client_residency_requirement(
.unwrap_or_default();
match codex_core::config::ConfigBuilder::default()
.cli_overrides(cli_overrides.to_vec())
.loader_overrides(loader_overrides.clone())
.fallback_cwd(Some(codex_home.to_path_buf()))
.cloud_requirements(loader)
.build()
.await
@@ -7460,6 +7520,7 @@ async fn derive_config_from_params(
cli_overrides: &[(String, TomlValue)],
request_overrides: Option<HashMap<String, serde_json::Value>>,
typesafe_overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
cloud_requirements: &CloudRequirementsLoader,
) -> std::io::Result<Config> {
let merged_cli_overrides = cli_overrides
@@ -7476,6 +7537,7 @@ async fn derive_config_from_params(
codex_core::config::ConfigBuilder::default()
.cli_overrides(merged_cli_overrides)
.harness_overrides(typesafe_overrides)
.loader_overrides(loader_overrides)
.cloud_requirements(cloud_requirements.clone())
.build()
.await
@@ -7486,6 +7548,7 @@ async fn derive_config_for_cwd(
request_overrides: Option<HashMap<String, serde_json::Value>>,
typesafe_overrides: ConfigOverrides,
cwd: Option<PathBuf>,
loader_overrides: LoaderOverrides,
cloud_requirements: &CloudRequirementsLoader,
) -> std::io::Result<Config> {
let merged_cli_overrides = cli_overrides
@@ -7502,6 +7565,7 @@ async fn derive_config_for_cwd(
codex_core::config::ConfigBuilder::default()
.cli_overrides(merged_cli_overrides)
.harness_overrides(typesafe_overrides)
.loader_overrides(loader_overrides)
.fallback_cwd(cwd)
.cloud_requirements(cloud_requirements.clone())
.build()
@@ -198,6 +198,7 @@ impl MessageProcessor {
arg0_paths,
config: Arc::clone(&config),
cli_overrides: cli_overrides.clone(),
loader_overrides: loader_overrides.clone(),
cloud_requirements: cloud_requirements.clone(),
single_client_mode,
feedback,
@@ -3,6 +3,7 @@ use app_test_support::McpProcess;
use app_test_support::create_fake_rollout;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::to_response;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
@@ -191,15 +192,67 @@ async fn thread_fork_rejects_unmaterialized_thread() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_fork_applies_requirements_toml_per_session() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml_with_approval_policy(codex_home.path(), &server.uri(), "on-request")?;
let requirements_toml = codex_home.path().join("session-requirements.toml");
std::fs::write(
&requirements_toml,
"allowed_approval_policies = [\"never\"]\n",
)?;
let conversation_id = create_fake_rollout(
codex_home.path(),
"2025-01-05T12-00-00",
"2025-01-05T12:00:00Z",
"Saved user message",
Some("mock_provider"),
None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let fork_id = mcp
.send_thread_fork_request(ThreadForkParams {
thread_id: conversation_id,
requirements_toml: Some(requirements_toml.display().to_string()),
..Default::default()
})
.await?;
let fork_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(fork_id)),
)
.await??;
let ThreadForkResponse {
approval_policy, ..
} = to_response::<ThreadForkResponse>(fork_resp)?;
assert_eq!(approval_policy, AskForApproval::Never);
Ok(())
}
// Helper to create a config.toml pointing at the mock model server.
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
create_config_toml_with_approval_policy(codex_home, server_uri, "never")
}
fn create_config_toml_with_approval_policy(
codex_home: &Path,
server_uri: &str,
approval_policy: &str,
) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "mock-model"
approval_policy = "never"
approval_policy = "{approval_policy}"
sandbox_mode = "read-only"
model_provider = "mock_provider"
@@ -173,6 +173,51 @@ async fn thread_resume_returns_rollout_history() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_resume_applies_requirements_toml_per_session() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml_with_approval_policy(codex_home.path(), &server.uri(), "on-request")?;
let requirements_toml = codex_home.path().join("session-requirements.toml");
std::fs::write(
&requirements_toml,
"allowed_approval_policies = [\"never\"]\n",
)?;
let conversation_id = create_fake_rollout_with_text_elements(
codex_home.path(),
"2025-01-05T12-00-00",
"2025-01-05T12:00:00Z",
"Saved user message",
Vec::new(),
Some("mock_provider"),
None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: conversation_id,
requirements_toml: Some(requirements_toml.display().to_string()),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse {
approval_policy, ..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(approval_policy, AskForApproval::Never);
Ok(())
}
#[tokio::test]
async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
@@ -1361,13 +1406,21 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
// Helper to create a config.toml pointing at the mock model server.
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
create_config_toml_with_approval_policy(codex_home, server_uri, "never")
}
fn create_config_toml_with_approval_policy(
codex_home: &std::path::Path,
server_uri: &str,
approval_policy: &str,
) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "gpt-5.2-codex"
approval_policy = "never"
approval_policy = "{approval_policy}"
sandbox_mode = "read-only"
model_provider = "mock_provider"
@@ -2,6 +2,7 @@ use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::to_response;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
@@ -190,6 +191,42 @@ async fn thread_start_accepts_metrics_service_name() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_start_applies_requirements_toml_per_session() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml_with_approval_policy(codex_home.path(), &server.uri(), "on-request")?;
let requirements_toml = codex_home.path().join("session-requirements.toml");
std::fs::write(
&requirements_toml,
"allowed_approval_policies = [\"never\"]\n",
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let req_id = mcp
.send_thread_start_request(ThreadStartParams {
requirements_toml: Some(requirements_toml.display().to_string()),
..Default::default()
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let ThreadStartResponse {
approval_policy, ..
} = to_response::<ThreadStartResponse>(resp)?;
assert_eq!(approval_policy, AskForApproval::Never);
Ok(())
}
#[tokio::test]
async fn thread_start_ephemeral_remains_pathless() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
@@ -273,13 +310,21 @@ async fn thread_start_fails_when_required_mcp_server_fails_to_initialize() -> Re
// Helper to create a config.toml pointing at the mock model server.
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
create_config_toml_with_approval_policy(codex_home, server_uri, "never")
}
fn create_config_toml_with_approval_policy(
codex_home: &Path,
server_uri: &str,
approval_policy: &str,
) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "mock-model"
approval_policy = "never"
approval_policy = "{approval_policy}"
sandbox_mode = "read-only"
model_provider = "mock_provider"
@@ -124,14 +124,11 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> {
#[tokio::test]
async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<()> {
#[cfg(target_os = "windows")]
let shell_command = vec![
"powershell".to_string(),
"-Command".to_string(),
"Start-Sleep -Seconds 10".to_string(),
"python3".to_string(),
"-c".to_string(),
"print(42)".to_string(),
];
#[cfg(not(target_os = "windows"))]
let shell_command = vec!["sleep".to_string(), "10".to_string()];
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
@@ -143,7 +140,7 @@ async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<()
shell_command.clone(),
Some(&working_directory),
Some(10_000),
"call_sleep_approval",
"call_python_approval",
)?])
.await;
create_config_toml(&codex_home, &server.uri(), "untrusted")?;
@@ -168,7 +165,7 @@ async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<()
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run sleep".to_string(),
text: "run python".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(working_directory),
@@ -190,7 +187,7 @@ async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<()
let ServerRequest::CommandExecutionRequestApproval { request_id, params } = request else {
panic!("expected CommandExecutionRequestApproval request");
};
assert_eq!(params.item_id, "call_sleep_approval");
assert_eq!(params.item_id, "call_python_approval");
assert_eq!(params.thread_id, thread.id);
assert_eq!(params.turn_id, turn.id);
+43
View File
@@ -1070,6 +1070,9 @@ fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli)
if let Some(cwd) = subcommand_cli.cwd {
interactive.cwd = Some(cwd);
}
if let Some(requirements_toml) = subcommand_cli.requirements_toml {
interactive.requirements_toml = Some(requirements_toml);
}
if subcommand_cli.web_search {
interactive.web_search = true;
}
@@ -1379,6 +1382,26 @@ mod tests {
assert_eq!(interactive.resume_session_id.as_deref(), Some("sid"));
}
#[test]
fn resume_subcommand_requirements_toml_overrides_root_value() {
let interactive = finalize_resume_from_args(
[
"codex",
"--requirements-toml",
"/tmp/root.toml",
"resume",
"--requirements-toml",
"/tmp/resume.toml",
]
.as_ref(),
);
assert_eq!(
interactive.requirements_toml.as_deref(),
Some(std::path::Path::new("/tmp/resume.toml"))
);
}
#[test]
fn resume_merges_dangerously_bypass_flag() {
let interactive = finalize_resume_from_args(
@@ -1429,6 +1452,26 @@ mod tests {
assert!(interactive.fork_show_all);
}
#[test]
fn fork_subcommand_requirements_toml_overrides_root_value() {
let interactive = finalize_fork_from_args(
[
"codex",
"--requirements-toml",
"/tmp/root.toml",
"fork",
"--requirements-toml",
"/tmp/fork.toml",
]
.as_ref(),
);
assert_eq!(
interactive.requirements_toml.as_deref(),
Some(std::path::Path::new("/tmp/fork.toml"))
);
}
#[test]
fn app_server_analytics_default_disabled_without_flag() {
let app_server = app_server_from_args(["codex", "app-server"].as_ref());
+1
View File
@@ -17,6 +17,7 @@ use toml::Value as TomlValue;
#[derive(Debug, Default, Clone)]
pub struct LoaderOverrides {
pub managed_config_path: Option<PathBuf>,
pub requirements_toml_file: Option<PathBuf>,
//TODO(gt): Add a macos_ prefix to this field and remove the target_os check.
#[cfg(target_os = "macos")]
pub managed_preferences_base64: Option<String>,
+18 -1
View File
@@ -665,12 +665,27 @@ pub async fn load_config_as_toml_with_cli_overrides(
codex_home: &Path,
cwd: &AbsolutePathBuf,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<ConfigToml> {
load_config_as_toml_with_cli_overrides_and_loader_overrides(
codex_home,
cwd,
cli_overrides,
LoaderOverrides::default(),
)
.await
}
pub async fn load_config_as_toml_with_cli_overrides_and_loader_overrides(
codex_home: &Path,
cwd: &AbsolutePathBuf,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
) -> std::io::Result<ConfigToml> {
let config_layer_stack = load_config_layers_state(
codex_home,
Some(cwd.clone()),
&cli_overrides,
LoaderOverrides::default(),
loader_overrides,
CloudRequirementsLoader::default(),
)
.await?;
@@ -3488,6 +3503,7 @@ profile = "project"
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
@@ -3619,6 +3635,7 @@ profile = "project"
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
+5
View File
@@ -880,6 +880,7 @@ personality = true
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
@@ -955,6 +956,7 @@ personality = true
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
@@ -1060,6 +1062,7 @@ personality = true
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
@@ -1109,6 +1112,7 @@ personality = true
cli_overrides,
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
@@ -1167,6 +1171,7 @@ personality = true
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
+43 -2
View File
@@ -134,6 +134,10 @@ pub async fn load_config_layers_state(
// Honor the system requirements.toml location.
let requirements_toml_file = system_requirements_toml_file()?;
load_requirements_toml(&mut config_requirements_toml, requirements_toml_file).await?;
if let Some(requirements_toml_file) = overrides.requirements_toml_file.as_ref() {
load_session_requirements_toml(&mut config_requirements_toml, requirements_toml_file)
.await?;
}
// Make a best-effort to support the legacy `managed_config.toml` as a
// requirements specification.
@@ -346,6 +350,37 @@ async fn load_config_toml_for_required_layer(
async fn load_requirements_toml(
config_requirements_toml: &mut ConfigRequirementsWithSources,
requirements_toml_file: impl AsRef<Path>,
) -> io::Result<()> {
load_requirements_toml_with_source(
config_requirements_toml,
requirements_toml_file,
MissingRequirementsTomlBehavior::Ignore,
)
.await
}
async fn load_session_requirements_toml(
config_requirements_toml: &mut ConfigRequirementsWithSources,
requirements_toml_file: impl AsRef<Path>,
) -> io::Result<()> {
load_requirements_toml_with_source(
config_requirements_toml,
requirements_toml_file,
MissingRequirementsTomlBehavior::Error,
)
.await
}
#[derive(Clone, Copy)]
enum MissingRequirementsTomlBehavior {
Ignore,
Error,
}
async fn load_requirements_toml_with_source(
config_requirements_toml: &mut ConfigRequirementsWithSources,
requirements_toml_file: impl AsRef<Path>,
missing_behavior: MissingRequirementsTomlBehavior,
) -> io::Result<()> {
let requirements_toml_file =
AbsolutePathBuf::from_absolute_path(requirements_toml_file.as_ref())?;
@@ -369,7 +404,14 @@ async fn load_requirements_toml(
);
}
Err(e) => {
if e.kind() != io::ErrorKind::NotFound {
if e.kind() == io::ErrorKind::NotFound
&& matches!(missing_behavior, MissingRequirementsTomlBehavior::Ignore)
{
return Ok(());
}
if e.kind() != io::ErrorKind::NotFound
|| matches!(missing_behavior, MissingRequirementsTomlBehavior::Error)
{
return Err(io::Error::new(
e.kind(),
format!(
@@ -380,7 +422,6 @@ async fn load_requirements_toml(
}
}
}
Ok(())
}
+69
View File
@@ -203,6 +203,7 @@ extra = true
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
@@ -240,6 +241,7 @@ async fn returns_empty_when_all_layers_missing() {
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
requirements_toml_file: None,
#[cfg(target_os = "macos")]
// Force managed preferences to resolve as empty so this test does not
// inherit non-empty machine-specific managed state.
@@ -338,6 +340,7 @@ flag = false
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
requirements_toml_file: None,
managed_preferences_base64: Some(
base64::prelude::BASE64_STANDARD.encode(raw_managed_preferences.as_bytes()),
),
@@ -392,6 +395,7 @@ async fn managed_preferences_requirements_are_applied() -> anyhow::Result<()> {
&[] as &[(String, TomlValue)],
LoaderOverrides {
managed_config_path: Some(tmp.path().join("managed_config.toml")),
requirements_toml_file: None,
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
@@ -455,6 +459,7 @@ async fn managed_preferences_requirements_take_precedence() -> anyhow::Result<()
&[] as &[(String, TomlValue)],
LoaderOverrides {
managed_config_path: Some(managed_path),
requirements_toml_file: None,
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
@@ -555,6 +560,69 @@ enforce_residency = "us"
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn load_requirements_toml_from_loader_overrides() -> anyhow::Result<()> {
let tmp = tempdir()?;
let requirements_file = tmp.path().join("session-requirements.toml");
tokio::fs::write(
&requirements_file,
r#"
allowed_approval_policies = ["never"]
"#,
)
.await?;
let layers = load_config_layers_state(
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
LoaderOverrides {
requirements_toml_file: Some(requirements_file.clone()),
..LoaderOverrides::default()
},
CloudRequirementsLoader::default(),
)
.await?;
assert_eq!(
layers.requirements_toml().allowed_approval_policies,
Some(vec![AskForApproval::Never])
);
assert_eq!(
layers.requirements().approval_policy.value(),
AskForApproval::Never
);
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn missing_session_requirements_toml_from_loader_overrides_errors() -> anyhow::Result<()> {
let tmp = tempdir()?;
let requirements_file = tmp.path().join("missing-requirements.toml");
let err = load_config_layers_state(
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
LoaderOverrides {
requirements_toml_file: Some(requirements_file.clone()),
..LoaderOverrides::default()
},
CloudRequirementsLoader::default(),
)
.await
.expect_err("missing session requirements should fail");
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
assert!(
err.to_string()
.contains(&requirements_file.display().to_string())
);
Ok(())
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn cloud_requirements_take_precedence_over_mdm_requirements() -> anyhow::Result<()> {
@@ -566,6 +634,7 @@ async fn cloud_requirements_take_precedence_over_mdm_requirements() -> anyhow::R
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
LoaderOverrides {
requirements_toml_file: None,
macos_managed_config_requirements_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
+2 -1
View File
@@ -58,6 +58,7 @@ Request `newConversation` params (subset):
- `model`: string model id (e.g. "o3", "gpt-5.1", "gpt-5.1-codex")
- `profile`: optional named profile
- `requirementsToml`: optional path to a session-scoped `requirements.toml`
- `cwd`: optional working directory
- `approvalPolicy`: `untrusted` | `on-request` | `on-failure` (deprecated) | `never`
- `sandbox`: `read-only` | `workspace-write` | `external-sandbox` (honors `networkAccess` restricted/enabled) | `danger-full-access`
@@ -164,7 +165,7 @@ For the complete request/response shapes and flow examples, see the [“Auth end
## Example: start and send a message
```json
{ "jsonrpc": "2.0", "id": 1, "method": "newConversation", "params": { "model": "gpt-5.1", "approvalPolicy": "on-request" } }
{ "jsonrpc": "2.0", "id": 1, "method": "newConversation", "params": { "model": "gpt-5.1", "approvalPolicy": "on-request", "requirementsToml": "/path/to/requirements.toml" } }
```
Server responds:
+21
View File
@@ -63,6 +63,15 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
/// Apply an additional requirements.toml file to this session only.
#[arg(
long = "requirements-toml",
value_name = "FILE",
value_hint = clap::ValueHint::FilePath,
global = true
)]
pub requirements_toml: Option<PathBuf>,
/// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", global = true, default_value_t = false)]
pub skip_git_repo_check: bool,
@@ -273,6 +282,8 @@ mod tests {
"--json",
"--model",
"gpt-5.2-codex",
"--requirements-toml",
"/tmp/session-requirements.toml",
"--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check",
"--ephemeral",
@@ -280,6 +291,10 @@ mod tests {
]);
assert!(cli.ephemeral);
assert_eq!(
cli.requirements_toml,
Some(PathBuf::from("/tmp/session-requirements.toml"))
);
let Some(Command::Resume(args)) = cli.command else {
panic!("expected resume command");
};
@@ -300,6 +315,8 @@ mod tests {
"codex-exec",
"resume",
"session-123",
"--requirements-toml",
"/tmp/resume-output-requirements.toml",
"-o",
"/tmp/resume-output.md",
PROMPT,
@@ -309,6 +326,10 @@ mod tests {
cli.last_message_file,
Some(PathBuf::from("/tmp/resume-output.md"))
);
assert_eq!(
cli.requirements_toml,
Some(PathBuf::from("/tmp/resume-output-requirements.toml"))
);
let Some(Command::Resume(args)) = cli.command else {
panic!("expected resume command");
};
+10 -2
View File
@@ -26,9 +26,10 @@ use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::config::load_config_as_toml_with_cli_overrides_and_loader_overrides;
use codex_core::config::resolve_oss_provider;
use codex_core::config_loader::ConfigLoadError;
use codex_core::config_loader::LoaderOverrides;
use codex_core::config_loader::format_config_error_with_source;
use codex_core::format_exec_policy_error_with_source;
use codex_core::git_info::get_git_repo_root;
@@ -109,6 +110,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
full_auto,
dangerously_bypass_approvals_and_sandbox,
cwd,
requirements_toml,
skip_git_repo_check,
add_dir,
ephemeral,
@@ -185,6 +187,10 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
None => AbsolutePathBuf::current_dir()?,
};
let loader_overrides = LoaderOverrides {
requirements_toml_file: requirements_toml.clone(),
..Default::default()
};
// we load config.toml here to determine project state.
#[allow(clippy::print_stderr)]
@@ -197,10 +203,11 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
};
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
let config_toml = match load_config_as_toml_with_cli_overrides_and_loader_overrides(
&codex_home,
&config_cwd,
cli_kv_overrides.clone(),
loader_overrides.clone(),
)
.await
{
@@ -294,6 +301,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
let config = ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)
.harness_overrides(overrides)
.loader_overrides(loader_overrides)
.cloud_requirements(cloud_requirements)
.build()
.await?;
+7
View File
@@ -48,6 +48,7 @@ use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::types::ModelAvailabilityNuxConfig;
use codex_core::config_loader::ConfigLayerStackOrdering;
use codex_core::config_loader::LoaderOverrides;
use codex_core::features::Feature;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_core::models_manager::manager::RefreshStrategy;
@@ -635,6 +636,7 @@ pub(crate) struct App {
pub(crate) active_profile: Option<String>,
cli_kv_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
runtime_approval_policy_override: Option<AskForApproval>,
runtime_sandbox_policy_override: Option<SandboxPolicy>,
@@ -751,6 +753,7 @@ impl App {
.codex_home(self.config.codex_home.clone())
.cli_overrides(self.cli_kv_overrides.clone())
.harness_overrides(overrides)
.loader_overrides(self.loader_overrides.clone())
.build()
.await
.wrap_err_with(|| format!("Failed to rebuild config for cwd {cwd_display}"))
@@ -1495,6 +1498,7 @@ impl App {
mut config: Config,
cli_kv_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
active_profile: Option<String>,
initial_prompt: Option<String>,
initial_images: Vec<PathBuf>,
@@ -1704,6 +1708,7 @@ impl App {
active_profile,
cli_kv_overrides,
harness_overrides,
loader_overrides,
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
file_search,
@@ -4364,6 +4369,7 @@ mod tests {
active_profile: None,
cli_kv_overrides: Vec::new(),
harness_overrides: ConfigOverrides::default(),
loader_overrides: LoaderOverrides::default(),
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
file_search,
@@ -4424,6 +4430,7 @@ mod tests {
active_profile: None,
cli_kv_overrides: Vec::new(),
harness_overrides: ConfigOverrides::default(),
loader_overrides: LoaderOverrides::default(),
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
file_search,
+4
View File
@@ -94,6 +94,10 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
/// Load session-scoped requirements from the specified TOML file.
#[arg(long = "requirements-toml", value_name = "FILE", value_hint = ValueHint::FilePath)]
pub requirements_toml: Option<PathBuf>,
/// Enable live web search. When enabled, the native Responses `web_search` tool is available to the model (no per‑call approval).
#[arg(long = "search", default_value_t = false)]
pub web_search: bool,
+25 -4
View File
@@ -20,10 +20,11 @@ use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::config::load_config_as_toml_with_cli_overrides_and_loader_overrides;
use codex_core::config::resolve_oss_provider;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::ConfigLoadError;
use codex_core::config_loader::LoaderOverrides;
use codex_core::config_loader::format_config_error_with_source;
use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::find_thread_path_by_id_str;
@@ -278,16 +279,21 @@ pub async fn run_main(mut cli: Cli, arg0_paths: Arg0DispatchPaths) -> std::io::R
};
let cwd = cli.cwd.clone();
let loader_overrides = LoaderOverrides {
requirements_toml_file: cli.requirements_toml.clone(),
..Default::default()
};
let config_cwd = match cwd.as_deref() {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
None => AbsolutePathBuf::current_dir()?,
};
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
let config_toml = match load_config_as_toml_with_cli_overrides_and_loader_overrides(
&codex_home,
&config_cwd,
cli_kv_overrides.clone(),
loader_overrides.clone(),
)
.await
{
@@ -386,6 +392,7 @@ pub async fn run_main(mut cli: Cli, arg0_paths: Arg0DispatchPaths) -> std::io::R
let config = load_config_or_exit(
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
cloud_requirements.clone(),
)
.await;
@@ -522,6 +529,7 @@ pub async fn run_main(mut cli: Cli, arg0_paths: Arg0DispatchPaths) -> std::io::R
config,
overrides,
cli_kv_overrides,
loader_overrides,
cloud_requirements,
feedback,
)
@@ -534,6 +542,7 @@ async fn run_ratatui_app(
initial_config: Config,
overrides: ConfigOverrides,
cli_kv_overrides: Vec<(String, toml::Value)>,
loader_overrides: LoaderOverrides,
mut cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
) -> color_eyre::Result<AppExitInfo> {
@@ -634,6 +643,7 @@ async fn run_ratatui_app(
load_config_or_exit(
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
cloud_requirements.clone(),
)
.await
@@ -885,6 +895,7 @@ async fn run_ratatui_app(
load_config_or_exit_with_fallback_cwd(
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
cloud_requirements.clone(),
fallback_cwd,
)
@@ -926,6 +937,7 @@ async fn run_ratatui_app(
config,
cli_kv_overrides.clone(),
overrides.clone(),
loader_overrides.clone(),
active_profile,
prompt,
images,
@@ -1119,15 +1131,23 @@ fn get_login_status(config: &Config) -> LoginStatus {
async fn load_config_or_exit(
cli_kv_overrides: Vec<(String, toml::Value)>,
overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
) -> Config {
load_config_or_exit_with_fallback_cwd(cli_kv_overrides, overrides, cloud_requirements, None)
.await
load_config_or_exit_with_fallback_cwd(
cli_kv_overrides,
overrides,
loader_overrides,
cloud_requirements,
None,
)
.await
}
async fn load_config_or_exit_with_fallback_cwd(
cli_kv_overrides: Vec<(String, toml::Value)>,
overrides: ConfigOverrides,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
fallback_cwd: Option<PathBuf>,
) -> Config {
@@ -1135,6 +1155,7 @@ async fn load_config_or_exit_with_fallback_cwd(
match ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)
.harness_overrides(overrides)
.loader_overrides(loader_overrides)
.cloud_requirements(cloud_requirements)
.fallback_cwd(fallback_cwd)
.build()
+10
View File
@@ -116,6 +116,16 @@ const thread = codex.startThread({
});
```
### Session-scoped requirements
Use `requirementsToml` to apply an additional `requirements.toml` file to a single SDK thread without modifying saved Codex config.
```typescript
const thread = codex.startThread({
requirementsToml: "/path/to/session-requirements.toml",
});
```
### Controlling the Codex CLI environment
By default, the Codex CLI inherits the Node.js process environment. Provide the optional `env` parameter when instantiating the
+6
View File
@@ -19,6 +19,8 @@ export type CodexExecArgs = {
sandboxMode?: SandboxMode;
// --cd
workingDirectory?: string;
// --requirements-toml
requirementsToml?: string;
// --add-dir
additionalDirectories?: string[];
// --skip-git-repo-check
@@ -90,6 +92,10 @@ export class CodexExec {
commandArgs.push("--cd", args.workingDirectory);
}
if (args.requirementsToml) {
commandArgs.push("--requirements-toml", args.requirementsToml);
}
if (args.additionalDirectories?.length) {
for (const dir of args.additionalDirectories) {
commandArgs.push("--add-dir", dir);
+1
View File
@@ -83,6 +83,7 @@ export class Thread {
model: options?.model,
sandboxMode: options?.sandboxMode,
workingDirectory: options?.workingDirectory,
requirementsToml: options?.requirementsToml,
skipGitRepoCheck: options?.skipGitRepoCheck,
outputSchemaFile: schemaPath,
modelReasoningEffort: options?.modelReasoningEffort,
+1
View File
@@ -10,6 +10,7 @@ export type ThreadOptions = {
model?: string;
sandboxMode?: SandboxMode;
workingDirectory?: string;
requirementsToml?: string;
skipGitRepoCheck?: boolean;
modelReasoningEffort?: ModelReasoningEffort;
networkAccessEnabled?: boolean;
+63
View File
@@ -410,6 +410,37 @@ describe("Codex", () => {
}
});
it("passes requirementsToml to exec", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,
responseBodies: [
sse(
responseStarted("response_1"),
assistantMessage("Requirements applied", "item_1"),
responseCompleted("response_1"),
),
],
});
const { args: spawnArgs, restore } = codexExecSpy();
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
requirementsToml: "/tmp/session-requirements.toml",
});
await thread.run("test requirements");
const commandArgs = spawnArgs[0];
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--requirements-toml", "/tmp/session-requirements.toml"]);
} finally {
restore();
await close();
}
});
it("passes CodexOptions config overrides as TOML --config flags", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,
@@ -774,6 +805,38 @@ describe("Codex", () => {
}
});
it("passes requirementsToml when resuming a thread", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,
responseBodies: [
sse(
responseStarted("response_1"),
assistantMessage("Requirements applied", "item_1"),
responseCompleted("response_1"),
),
],
});
const { args: spawnArgs, restore } = codexExecSpy();
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.resumeThread("thread_123", {
requirementsToml: "/tmp/session-requirements.toml",
});
await thread.run("resume with requirements");
const commandArgs = spawnArgs[0];
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--requirements-toml", "/tmp/session-requirements.toml"]);
expectPair(commandArgs, ["resume", "thread_123"]);
} finally {
restore();
await close();
}
});
it("sets the codex sdk originator header", async () => {
const { url, close, requests } = await startResponsesTestProxy({
statusCode: 200,