mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
add requirements.toml commandline option
This commit is contained in:
@@ -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 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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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#"
|
||||
|
||||
@@ -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,
|
||||
|
||||
+25
-4
@@ -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 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
|
||||
|
||||
@@ -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