codex-python 0.2.10__tar.gz → 0.2.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-python
3
- Version: 0.2.10
3
+ Version: 0.2.11
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -31,4 +31,4 @@ __all__ = [
31
31
  ]
32
32
 
33
33
  # Package version. Kept in sync with Cargo.toml via CI before builds.
34
- __version__ = "0.2.10"
34
+ __version__ = "0.2.11"
@@ -0,0 +1,242 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Any, cast
5
+
6
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field
7
+
8
+
9
+ class ApprovalPolicy(str, Enum):
10
+ """Approval policy for executing shell commands.
11
+
12
+ Matches Rust enum `AskForApproval` (serde kebab-case):
13
+ - "untrusted": auto-approve safe read-only commands, ask otherwise
14
+ - "on-failure": sandbox by default; ask only if the sandboxed run fails
15
+ - "on-request": model decides (default)
16
+ - "never": never ask the user
17
+ """
18
+
19
+ UNTRUSTED = "untrusted"
20
+ ON_FAILURE = "on-failure"
21
+ ON_REQUEST = "on-request"
22
+ NEVER = "never"
23
+
24
+
25
+ class SandboxMode(str, Enum):
26
+ """High-level sandbox mode override.
27
+
28
+ Matches Rust enum `SandboxMode` (serde kebab-case):
29
+ - "read-only"
30
+ - "workspace-write"
31
+ - "danger-full-access"
32
+ """
33
+
34
+ READ_ONLY = "read-only"
35
+ WORKSPACE_WRITE = "workspace-write"
36
+ DANGER_FULL_ACCESS = "danger-full-access"
37
+
38
+
39
+ class HistoryPersistence(str, Enum):
40
+ SAVE_ALL = "save-all"
41
+ NONE = "none"
42
+
43
+
44
+ class HistoryConfig(BaseModel):
45
+ persistence: HistoryPersistence | None = None
46
+ max_bytes: int | None = None
47
+
48
+ model_config = ConfigDict(extra="allow")
49
+
50
+
51
+ class SandboxWorkspaceWrite(BaseModel):
52
+ writable_roots: list[str] | None = None
53
+ network_access: bool | None = None
54
+ exclude_tmpdir_env_var: bool | None = None
55
+ exclude_slash_tmp: bool | None = None
56
+
57
+ model_config = ConfigDict(extra="allow")
58
+
59
+
60
+ class McpServerConfig(BaseModel):
61
+ command: str
62
+ args: list[str] | None = None
63
+ env: dict[str, str] | None = None
64
+ startup_timeout_ms: int | None = None
65
+
66
+ model_config = ConfigDict(extra="allow")
67
+
68
+
69
+ class WireApi(str, Enum):
70
+ CHAT = "chat"
71
+ RESPONSES = "responses"
72
+
73
+
74
+ class ModelProviderConfig(BaseModel):
75
+ name: str | None = None
76
+ base_url: str | None = None
77
+ env_key: str | None = None
78
+ wire_api: WireApi | None = None
79
+ query_params: dict[str, str] | None = None
80
+ http_headers: dict[str, str] | None = None
81
+ env_http_headers: dict[str, str] | None = None
82
+ request_max_retries: int | None = None
83
+ stream_max_retries: int | None = None
84
+ stream_idle_timeout_ms: int | None = None
85
+
86
+ model_config = ConfigDict(extra="allow")
87
+
88
+
89
+ class FileOpener(str, Enum):
90
+ VSCODE = "vscode"
91
+ VSCODE_INSIDERS = "vscode-insiders"
92
+ WINDSURF = "windsurf"
93
+ CURSOR = "cursor"
94
+ NONE = "none"
95
+
96
+
97
+ class ReasoningEffort(str, Enum):
98
+ MINIMAL = "minimal"
99
+ LOW = "low"
100
+ MEDIUM = "medium"
101
+ HIGH = "high"
102
+
103
+
104
+ class ReasoningSummary(str, Enum):
105
+ AUTO = "auto"
106
+ CONCISE = "concise"
107
+ DETAILED = "detailed"
108
+ NONE = "none"
109
+
110
+
111
+ class ReasoningSummaryFormat(str, Enum):
112
+ NONE = "none"
113
+ EXPERIMENTAL = "experimental"
114
+
115
+
116
+ class Verbosity(str, Enum):
117
+ LOW = "low"
118
+ MEDIUM = "medium"
119
+ HIGH = "high"
120
+
121
+
122
+ class ToolsConfig(BaseModel):
123
+ # Accept either "web_search" or legacy alias "web_search_request" on input
124
+ web_search: bool | None = Field(
125
+ default=None, validation_alias=AliasChoices("web_search", "web_search_request")
126
+ )
127
+ # Also expose view_image knob (defaults handled in Rust)
128
+ view_image: bool | None = None
129
+
130
+ model_config = ConfigDict(extra="allow")
131
+
132
+
133
+ class ProfileConfig(BaseModel):
134
+ model: str | None = None
135
+ model_provider: str | None = None
136
+ approval_policy: ApprovalPolicy | None = None
137
+ model_reasoning_effort: ReasoningEffort | None = None
138
+ model_reasoning_summary: ReasoningSummary | None = None
139
+ model_verbosity: Verbosity | None = None
140
+ chatgpt_base_url: str | None = None
141
+ experimental_instructions_file: str | None = None
142
+
143
+ model_config = ConfigDict(extra="allow")
144
+
145
+
146
+ class ProjectConfig(BaseModel):
147
+ trust_level: str | None = Field(
148
+ default=None, description='Only "trusted" is recognized by the core.'
149
+ )
150
+
151
+ model_config = ConfigDict(extra="allow")
152
+
153
+
154
+ class PreferredAuthMethod(str, Enum):
155
+ CHATGPT = "chatgpt"
156
+ APIKEY = "apikey"
157
+
158
+
159
+ class CodexConfig(BaseModel):
160
+ """Configuration overrides for Codex.
161
+
162
+ This mirrors `codex_core::config::ConfigOverrides` and is intentionally
163
+ conservative: only values present (not None) are passed to the native core.
164
+ """
165
+
166
+ # Model selection
167
+ model: str | None = Field(default=None, description="Model slug, e.g. 'gpt-5' or 'o3'.")
168
+ model_provider: str | None = Field(
169
+ default=None, description="Provider key from config, e.g. 'openai'."
170
+ )
171
+
172
+ # Safety/Execution
173
+ approval_policy: ApprovalPolicy | None = Field(default=None)
174
+ sandbox_mode: SandboxMode | None = Field(default=None)
175
+ sandbox_workspace_write: SandboxWorkspaceWrite | None = None
176
+
177
+ # Environment
178
+ cwd: str | None = Field(default=None, description="Working directory for the session.")
179
+ config_profile: str | None = Field(
180
+ default=None, description="Config profile key to use (from profiles.*)."
181
+ )
182
+ codex_linux_sandbox_exe: str | None = Field(
183
+ default=None, description="Absolute path to codex-linux-sandbox (Linux only)."
184
+ )
185
+
186
+ # UX / features
187
+ base_instructions: str | None = Field(default=None, description="Override base instructions.")
188
+ include_plan_tool: bool | None = Field(default=None)
189
+ include_apply_patch_tool: bool | None = Field(default=None)
190
+ include_view_image_tool: bool | None = Field(default=None)
191
+ show_raw_agent_reasoning: bool | None = Field(default=None)
192
+
193
+ # Model/runtime tuning
194
+ model_context_window: int | None = None
195
+ model_max_output_tokens: int | None = None
196
+ model_reasoning_effort: ReasoningEffort | None = None
197
+ model_reasoning_summary: ReasoningSummary | None = None
198
+ model_verbosity: Verbosity | None = None
199
+ model_supports_reasoning_summaries: bool | None = None
200
+ model_reasoning_summary_format: ReasoningSummaryFormat | None = None
201
+
202
+ # Auth/UI options
203
+ hide_agent_reasoning: bool | None = None
204
+ chatgpt_base_url: str | None = None
205
+ preferred_auth_method: PreferredAuthMethod | None = None
206
+ file_opener: FileOpener | None = None
207
+
208
+ # Config file composed sections
209
+ history: HistoryConfig | None = None
210
+ tui: dict[str, Any] | None = None
211
+ notify: list[str] | None = None
212
+ instructions: str | None = Field(
213
+ default=None,
214
+ description="Ignored by core; prefer AGENTS.md or experimental_instructions_file.",
215
+ )
216
+ mcp_servers: dict[str, McpServerConfig] | None = None
217
+ model_providers: dict[str, ModelProviderConfig] | None = None
218
+ project_doc_max_bytes: int | None = None
219
+ profile: str | None = None
220
+ profiles: dict[str, ProfileConfig] | None = None
221
+ tools: ToolsConfig | None = None
222
+ projects: dict[str, ProjectConfig] | None = None
223
+
224
+ # Experimental / internal
225
+ experimental_resume: str | None = None
226
+ experimental_instructions_file: str | None = None
227
+ experimental_use_exec_command_tool: bool | None = None
228
+ responses_originator_header_internal_override: str | None = None
229
+ disable_response_storage: bool | None = Field(
230
+ default=None, description="Accepted in some clients; ignored by core."
231
+ )
232
+
233
+ def to_dict(self) -> dict[str, Any]:
234
+ """Return overrides as a plain dict with None values removed.
235
+
236
+ Enum fields are emitted as their string values.
237
+ """
238
+ return cast(dict[str, Any], self.model_dump(exclude_none=True))
239
+
240
+ # Pydantic v2 config. `use_enum_values=True` ensures enums dump as strings.
241
+ # Place at end of class, extra='allow' per style.
242
+ model_config = ConfigDict(extra="allow", validate_assignment=True, use_enum_values=True)
@@ -253,7 +253,7 @@ dependencies = [
253
253
 
254
254
  [[package]]
255
255
  name = "codex_native"
256
- version = "0.2.10"
256
+ version = "0.2.11"
257
257
  dependencies = [
258
258
  "anyhow",
259
259
  "codex-core",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "codex_native"
3
- version = "0.2.10"
3
+ version = "0.2.11"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -1,11 +1,15 @@
1
1
  use anyhow::{Context, Result};
2
- use codex_core::config::{find_codex_home, Config, ConfigOverrides, ConfigToml};
2
+ use codex_core::config::{
3
+ find_codex_home, load_config_as_toml_with_cli_overrides, Config, ConfigOverrides, ConfigToml,
4
+ };
3
5
  use codex_core::protocol::{EventMsg, InputItem};
4
6
  use codex_core::{AuthManager, ConversationManager};
5
7
  // use of SandboxMode is handled within core::config; not needed here
6
8
  use pyo3::prelude::*;
7
9
  use pyo3::types::{PyDict, PyFloat, PyList, PyModule, PyString};
8
10
  use serde_json::Value as JsonValue;
11
+ use toml::value::Value as TomlValue;
12
+ use toml::value::Table as TomlTable;
9
13
  use std::path::PathBuf;
10
14
  use std::sync::{mpsc, Arc, Mutex};
11
15
  use std::thread;
@@ -173,7 +177,10 @@ fn build_config(
173
177
  load_default_config: bool,
174
178
  ) -> Result<Config> {
175
179
  let mut overrides_struct = ConfigOverrides::default();
180
+ let mut cli_overrides: Vec<(String, TomlValue)> = Vec::new();
181
+
176
182
  if let Some(dict) = overrides {
183
+ // Extract strongly-typed overrides first (these take precedence over CLI overrides).
177
184
  if let Some(model) = dict.get_item("model")? {
178
185
  overrides_struct.model = Some(model.extract()?);
179
186
  }
@@ -222,17 +229,124 @@ fn build_config(
222
229
  if let Some(v) = dict.get_item("tools_web_search_request")? {
223
230
  overrides_struct.tools_web_search_request = Some(v.extract()?);
224
231
  }
232
+
233
+ // Keys handled as strongly-typed above should not be duplicated in CLI overrides.
234
+ let typed_keys = [
235
+ "model",
236
+ "model_provider",
237
+ "config_profile",
238
+ "approval_policy",
239
+ "sandbox_mode",
240
+ "cwd",
241
+ "codex_linux_sandbox_exe",
242
+ "base_instructions",
243
+ "include_plan_tool",
244
+ "include_apply_patch_tool",
245
+ "include_view_image_tool",
246
+ "show_raw_agent_reasoning",
247
+ "tools_web_search_request",
248
+ ];
249
+
250
+ // Collect remaining extras and turn them into CLI-style dotted overrides.
251
+ for (key_obj, value_obj) in dict.iter() {
252
+ let key: String = match key_obj.extract() {
253
+ Ok(s) => s,
254
+ Err(_) => continue,
255
+ };
256
+ if typed_keys.contains(&key.as_str()) {
257
+ continue;
258
+ }
259
+
260
+ // Convert Python value -> TomlValue
261
+ let tv = match py_to_toml_value(value_obj)? {
262
+ Some(v) => v,
263
+ None => continue, // skip None/null values
264
+ };
265
+
266
+ if key.contains('.') {
267
+ // Already a dotted path: use as-is.
268
+ cli_overrides.push((key, tv));
269
+ } else {
270
+ // Flatten nested tables; otherwise add directly.
271
+ flatten_overrides(&mut cli_overrides, &key, tv);
272
+ }
273
+ }
225
274
  }
226
275
 
227
276
  if load_default_config {
228
- Ok(Config::load_with_cli_overrides(vec![], overrides_struct)?)
277
+ Ok(Config::load_with_cli_overrides(cli_overrides, overrides_struct)?)
229
278
  } else {
230
279
  let codex_home = find_codex_home()?;
231
- Ok(Config::load_from_base_config_with_overrides(
232
- ConfigToml::default(),
233
- overrides_struct,
234
- codex_home,
235
- )?)
280
+ let cfg = load_config_as_toml_with_cli_overrides(&codex_home, cli_overrides)?;
281
+ Ok(Config::load_from_base_config_with_overrides(cfg, overrides_struct, codex_home)?)
282
+ }
283
+ }
284
+
285
+ /// Convert a Python object into a TOML value. Returns Ok(None) for `None`.
286
+ fn py_to_toml_value(obj: Bound<'_, PyAny>) -> Result<Option<TomlValue>> {
287
+ use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
288
+
289
+ if obj.is_none() {
290
+ return Ok(None);
291
+ }
292
+
293
+ if let Ok(b) = obj.downcast::<PyBool>() {
294
+ return Ok(Some(TomlValue::Boolean(b.is_true())));
295
+ }
296
+ if let Ok(i) = obj.downcast::<PyInt>() {
297
+ let v: i64 = i.extract()?;
298
+ return Ok(Some(TomlValue::Integer(v.into())));
299
+ }
300
+ if let Ok(f) = obj.downcast::<PyFloat>() {
301
+ let v: f64 = f.extract()?;
302
+ return Ok(Some(TomlValue::Float(v.into())));
303
+ }
304
+ if let Ok(s) = obj.downcast::<PyString>() {
305
+ let v: String = s.extract()?;
306
+ return Ok(Some(TomlValue::String(v.into())));
307
+ }
308
+ if let Ok(list) = obj.downcast::<PyList>() {
309
+ let mut arr = Vec::with_capacity(list.len());
310
+ for item in list.iter() {
311
+ if let Some(tv) = py_to_toml_value(item)? {
312
+ arr.push(tv);
313
+ }
314
+ }
315
+ return Ok(Some(TomlValue::Array(arr)));
316
+ }
317
+ if let Ok(map) = obj.downcast::<PyDict>() {
318
+ let mut tbl = TomlTable::new();
319
+ for (k_obj, v_obj) in map.iter() {
320
+ let key: String = match k_obj.extract() {
321
+ Ok(s) => s,
322
+ Err(_) => continue,
323
+ };
324
+ if let Some(tv) = py_to_toml_value(v_obj)? {
325
+ tbl.insert(key, tv);
326
+ }
327
+ }
328
+ return Ok(Some(TomlValue::Table(tbl)));
329
+ }
330
+
331
+ // Fallback: use `str(obj)`
332
+ let s = obj.str()?.to_string_lossy().to_string();
333
+ Ok(Some(TomlValue::String(s.into())))
334
+ }
335
+
336
+ /// Recursively flatten a TOML value into dotted overrides.
337
+ fn flatten_overrides(out: &mut Vec<(String, TomlValue)>, prefix: &str, val: TomlValue) {
338
+ match val {
339
+ TomlValue::Table(tbl) => {
340
+ for (k, v) in tbl.into_iter() {
341
+ let key = if prefix.is_empty() {
342
+ k
343
+ } else {
344
+ format!("{prefix}.{k}")
345
+ };
346
+ flatten_overrides(out, &key, v);
347
+ }
348
+ }
349
+ other => out.push((prefix.to_string(), other)),
236
350
  }
237
351
  }
238
352
 
@@ -1,82 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from enum import Enum
4
- from typing import Any, cast
5
-
6
- from pydantic import BaseModel, ConfigDict, Field
7
-
8
-
9
- class ApprovalPolicy(str, Enum):
10
- """Approval policy for executing shell commands.
11
-
12
- Matches Rust enum `AskForApproval` (serde kebab-case):
13
- - "untrusted": auto-approve safe read-only commands, ask otherwise
14
- - "on-failure": sandbox by default; ask only if the sandboxed run fails
15
- - "on-request": model decides (default)
16
- - "never": never ask the user
17
- """
18
-
19
- UNTRUSTED = "untrusted"
20
- ON_FAILURE = "on-failure"
21
- ON_REQUEST = "on-request"
22
- NEVER = "never"
23
-
24
-
25
- class SandboxMode(str, Enum):
26
- """High-level sandbox mode override.
27
-
28
- Matches Rust enum `SandboxMode` (serde kebab-case):
29
- - "read-only"
30
- - "workspace-write"
31
- - "danger-full-access"
32
- """
33
-
34
- READ_ONLY = "read-only"
35
- WORKSPACE_WRITE = "workspace-write"
36
- DANGER_FULL_ACCESS = "danger-full-access"
37
-
38
-
39
- class CodexConfig(BaseModel):
40
- """Configuration overrides for Codex.
41
-
42
- This mirrors `codex_core::config::ConfigOverrides` and is intentionally
43
- conservative: only values present (not None) are passed to the native core.
44
- """
45
-
46
- # Model selection
47
- model: str | None = Field(default=None, description="Model slug, e.g. 'gpt-5' or 'o3'.")
48
- model_provider: str | None = Field(
49
- default=None, description="Provider key from config, e.g. 'openai'."
50
- )
51
-
52
- # Safety/Execution
53
- approval_policy: ApprovalPolicy | None = Field(default=None)
54
- sandbox_mode: SandboxMode | None = Field(default=None)
55
-
56
- # Environment
57
- cwd: str | None = Field(default=None, description="Working directory for the session.")
58
- config_profile: str | None = Field(
59
- default=None, description="Config profile key to use (from profiles.*)."
60
- )
61
- codex_linux_sandbox_exe: str | None = Field(
62
- default=None, description="Absolute path to codex-linux-sandbox (Linux only)."
63
- )
64
-
65
- # UX / features
66
- base_instructions: str | None = Field(default=None, description="Override base instructions.")
67
- include_plan_tool: bool | None = Field(default=None)
68
- include_apply_patch_tool: bool | None = Field(default=None)
69
- include_view_image_tool: bool | None = Field(default=None)
70
- show_raw_agent_reasoning: bool | None = Field(default=None)
71
- tools_web_search_request: bool | None = Field(default=None)
72
-
73
- def to_dict(self) -> dict[str, Any]:
74
- """Return overrides as a plain dict with None values removed.
75
-
76
- Enum fields are emitted as their string values.
77
- """
78
- return cast(dict[str, Any], self.model_dump(exclude_none=True))
79
-
80
- # Pydantic v2 config. `use_enum_values=True` ensures enums dump as strings.
81
- # Place at end of class, extra='allow' per style.
82
- model_config = ConfigDict(extra="allow", validate_assignment=True, use_enum_values=True)
File without changes
File without changes