mirror of
https://github.com/openai/codex.git
synced 2026-05-01 18:06:47 +00:00
Compare commits
2 Commits
iceweasel/
...
mjr/cmdlin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1ddbf5e4f | ||
|
|
7d6cb71ec0 |
@@ -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>,
|
||||
|
||||
@@ -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 a session-scoped requirements file without changing persisted config. When the same requirement field is present in both the session file and an earlier requirement layer, the session file wins for that thread.
|
||||
- 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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -273,18 +273,40 @@ pub struct ConfigRequirementsWithSources {
|
||||
pub network: Option<Sourced<NetworkRequirementsToml>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum MergeBehavior {
|
||||
FillUnset,
|
||||
OverwriteExisting,
|
||||
}
|
||||
|
||||
impl ConfigRequirementsWithSources {
|
||||
pub fn merge_unset_fields(&mut self, source: RequirementSource, other: ConfigRequirementsToml) {
|
||||
// For every field in `other` that is `Some`, if the corresponding field
|
||||
// in `self` is `None`, copy the value from `other` into `self`.
|
||||
macro_rules! fill_missing_take {
|
||||
self.merge_fields(source, other, MergeBehavior::FillUnset);
|
||||
}
|
||||
|
||||
pub fn merge_overwrite_fields(
|
||||
&mut self,
|
||||
source: RequirementSource,
|
||||
other: ConfigRequirementsToml,
|
||||
) {
|
||||
self.merge_fields(source, other, MergeBehavior::OverwriteExisting);
|
||||
}
|
||||
|
||||
fn merge_fields(
|
||||
&mut self,
|
||||
source: RequirementSource,
|
||||
other: ConfigRequirementsToml,
|
||||
merge_behavior: MergeBehavior,
|
||||
) {
|
||||
macro_rules! merge_take {
|
||||
($base:expr, $other:expr, $source:expr, { $($field:ident),+ $(,)? }) => {
|
||||
// Destructure without `..` so adding fields to `ConfigRequirementsToml`
|
||||
// forces this merge logic to be updated.
|
||||
let ConfigRequirementsToml { $($field: _,)+ } = &$other;
|
||||
|
||||
$(
|
||||
if $base.$field.is_none()
|
||||
if (matches!(merge_behavior, MergeBehavior::OverwriteExisting)
|
||||
|| $base.$field.is_none())
|
||||
&& let Some(value) = $other.$field.take()
|
||||
{
|
||||
$base.$field = Some(Sourced::new(value, $source.clone()));
|
||||
@@ -294,7 +316,7 @@ impl ConfigRequirementsWithSources {
|
||||
}
|
||||
|
||||
let mut other = other;
|
||||
fill_missing_take!(
|
||||
merge_take!(
|
||||
self,
|
||||
other,
|
||||
source,
|
||||
@@ -732,6 +754,46 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_overwrite_fields_replaces_existing_values() -> Result<()> {
|
||||
let existing_source = RequirementSource::LegacyManagedConfigTomlFromMdm;
|
||||
let mut populated_target = ConfigRequirementsWithSources::default();
|
||||
let populated_requirements: ConfigRequirementsToml = from_str(
|
||||
r#"
|
||||
allowed_approval_policies = ["never"]
|
||||
"#,
|
||||
)?;
|
||||
populated_target.merge_unset_fields(existing_source, populated_requirements);
|
||||
|
||||
let source: ConfigRequirementsToml = from_str(
|
||||
r#"
|
||||
allowed_approval_policies = ["on-request"]
|
||||
"#,
|
||||
)?;
|
||||
let source_location = RequirementSource::MdmManagedPreferences {
|
||||
domain: "com.codex".to_string(),
|
||||
key: "allowed_approval_policies".to_string(),
|
||||
};
|
||||
populated_target.merge_overwrite_fields(source_location.clone(), source);
|
||||
|
||||
assert_eq!(
|
||||
populated_target,
|
||||
ConfigRequirementsWithSources {
|
||||
allowed_approval_policies: Some(Sourced::new(
|
||||
vec![AskForApproval::OnRequest],
|
||||
source_location,
|
||||
)),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constraint_error_includes_requirement_source() -> Result<()> {
|
||||
let source: ConfigRequirementsToml = from_str(
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -77,15 +77,19 @@ pub(crate) async fn first_layer_config_error_from_entries(
|
||||
.await
|
||||
}
|
||||
|
||||
/// To build up the set of admin-enforced constraints, we build up from multiple
|
||||
/// configuration layers in the following order, but a constraint defined in an
|
||||
/// earlier layer cannot be overridden by a later layer:
|
||||
/// To build up the set of baseline constraints, we merge the managed and
|
||||
/// system-provided requirement layers in the following order, but a constraint
|
||||
/// defined in an earlier layer cannot be overridden by a later one within this
|
||||
/// baseline stack:
|
||||
///
|
||||
/// - cloud: managed cloud requirements
|
||||
/// - admin: managed preferences (*)
|
||||
/// - system `/etc/codex/requirements.toml` (Unix) or
|
||||
/// `%ProgramData%\OpenAI\Codex\requirements.toml` (Windows)
|
||||
///
|
||||
/// If a session-scoped `requirements.toml` file is provided via
|
||||
/// `LoaderOverrides`, we apply it afterward as a per-thread override layer.
|
||||
///
|
||||
/// For backwards compatibility, we also load from
|
||||
/// `managed_config.toml` and map it to `requirements.toml`.
|
||||
///
|
||||
@@ -134,6 +138,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 +354,46 @@ 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,
|
||||
RequirementsMergeBehavior::FillUnset,
|
||||
)
|
||||
.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,
|
||||
RequirementsMergeBehavior::OverwriteExisting,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum MissingRequirementsTomlBehavior {
|
||||
Ignore,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum RequirementsMergeBehavior {
|
||||
FillUnset,
|
||||
OverwriteExisting,
|
||||
}
|
||||
|
||||
async fn load_requirements_toml_with_source(
|
||||
config_requirements_toml: &mut ConfigRequirementsWithSources,
|
||||
requirements_toml_file: impl AsRef<Path>,
|
||||
missing_behavior: MissingRequirementsTomlBehavior,
|
||||
merge_behavior: RequirementsMergeBehavior,
|
||||
) -> io::Result<()> {
|
||||
let requirements_toml_file =
|
||||
AbsolutePathBuf::from_absolute_path(requirements_toml_file.as_ref())?;
|
||||
@@ -361,15 +409,27 @@ async fn load_requirements_toml(
|
||||
),
|
||||
)
|
||||
})?;
|
||||
config_requirements_toml.merge_unset_fields(
|
||||
RequirementSource::SystemRequirementsToml {
|
||||
file: requirements_toml_file.clone(),
|
||||
},
|
||||
requirements_config,
|
||||
);
|
||||
let source = RequirementSource::SystemRequirementsToml {
|
||||
file: requirements_toml_file.clone(),
|
||||
};
|
||||
match merge_behavior {
|
||||
RequirementsMergeBehavior::FillUnset => {
|
||||
config_requirements_toml.merge_unset_fields(source, requirements_config);
|
||||
}
|
||||
RequirementsMergeBehavior::OverwriteExisting => {
|
||||
config_requirements_toml.merge_overwrite_fields(source, requirements_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 +440,6 @@ async fn load_requirements_toml(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::config_loader::ConfigRequirementsToml;
|
||||
use crate::config_loader::ConfigRequirementsWithSources;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::load_requirements_toml;
|
||||
use crate::config_loader::load_session_requirements_toml;
|
||||
use crate::config_loader::version_for_toml;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
@@ -203,6 +204,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 +242,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 +341,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 +396,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 +460,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 +561,116 @@ 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 session_requirements_toml_from_loader_overrides_overwrites_existing_requirements()
|
||||
-> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let requirements_file = tmp.path().join("session-requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_file,
|
||||
r#"
|
||||
allowed_approval_policies = ["on-request"]
|
||||
"#,
|
||||
)
|
||||
.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::new(async {
|
||||
Ok(Some(ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
layers.requirements_toml().allowed_approval_policies,
|
||||
Some(vec![AskForApproval::OnRequest])
|
||||
);
|
||||
assert_eq!(
|
||||
layers.requirements().approval_policy.value(),
|
||||
AskForApproval::OnRequest
|
||||
);
|
||||
|
||||
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 +682,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#"
|
||||
@@ -655,6 +772,53 @@ allowed_approval_policies = ["on-request"]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn load_session_requirements_toml_overwrites_existing_values() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let requirements_file = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_file,
|
||||
r#"
|
||||
allowed_approval_policies = ["on-request"]
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||
config_requirements_toml.merge_unset_fields(
|
||||
RequirementSource::CloudRequirements,
|
||||
ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
},
|
||||
);
|
||||
load_session_requirements_toml(&mut config_requirements_toml, &requirements_file).await?;
|
||||
|
||||
assert_eq!(
|
||||
config_requirements_toml
|
||||
.allowed_approval_policies
|
||||
.as_ref()
|
||||
.map(|sourced| sourced.value.clone()),
|
||||
Some(vec![AskForApproval::OnRequest])
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements_toml
|
||||
.allowed_approval_policies
|
||||
.as_ref()
|
||||
.map(|sourced| sourced.source.clone()),
|
||||
Some(RequirementSource::SystemRequirementsToml {
|
||||
file: AbsolutePathBuf::from_absolute_path(requirements_file.as_path())?,
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -116,6 +116,16 @@ const thread = codex.startThread({
|
||||
});
|
||||
```
|
||||
|
||||
### Session-scoped requirements
|
||||
|
||||
Use `requirementsToml` to apply a `requirements.toml` file to a single SDK thread without modifying saved Codex config. When the same requirement field is present in both this file and an earlier requirement layer, the session file wins for that thread.
|
||||
|
||||
```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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,6 +10,7 @@ export type ThreadOptions = {
|
||||
model?: string;
|
||||
sandboxMode?: SandboxMode;
|
||||
workingDirectory?: string;
|
||||
requirementsToml?: string;
|
||||
skipGitRepoCheck?: boolean;
|
||||
modelReasoningEffort?: ModelReasoningEffort;
|
||||
networkAccessEnabled?: boolean;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user