codex-python-sdk 0.1.0__tar.gz → 0.2.0__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.
Files changed (29) hide show
  1. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/PKG-INFO +45 -25
  2. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/README.md +44 -24
  3. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/__init__.py +0 -4
  4. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/async_client.py +103 -22
  5. codex_python_sdk-0.2.0/codex_python_sdk/policy.py +308 -0
  6. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/sync_client.py +3 -0
  7. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/PKG-INFO +45 -25
  8. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/pyproject.toml +1 -1
  9. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/tests/test_codex_python_sdk.py +172 -10
  10. codex_python_sdk-0.2.0/tests/test_policy_engine.py +227 -0
  11. codex_python_sdk-0.1.0/codex_python_sdk/policy.py +0 -636
  12. codex_python_sdk-0.1.0/tests/test_policy_engine.py +0 -619
  13. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/LICENSE +0 -0
  14. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/_shared.py +0 -0
  15. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/errors.py +0 -0
  16. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/examples/__init__.py +0 -0
  17. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/examples/demo_smoke.py +0 -0
  18. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/factory.py +0 -0
  19. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/renderer.py +0 -0
  20. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/types.py +0 -0
  21. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/SOURCES.txt +0 -0
  22. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/dependency_links.txt +0 -0
  23. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/entry_points.txt +0 -0
  24. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/requires.txt +0 -0
  25. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/top_level.txt +0 -0
  26. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/setup.cfg +0 -0
  27. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/tests/test_integration_real.py +0 -0
  28. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/tests/test_renderer.py +0 -0
  29. {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/tests/test_shared.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-python-sdk
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python wrapper for Codex app-server JSON-RPC interface
5
5
  Author: Henry_spdcoding
6
6
  License-Expression: MIT
@@ -98,8 +98,8 @@ codex-python-sdk-demo --mode demo
98
98
  codex-python-sdk-demo --mode full
99
99
  ```
100
100
 
101
- Note: the demo runner uses permissive hooks (`accept` for command/file approvals and empty tool-input answers) so it can run unattended.
102
- Use stricter hooks or policy engines in production.
101
+ Note: the demo runner uses explicit permissive hooks (`accept` for command/file approvals and empty tool-input answers) so it can run unattended.
102
+ The SDK defaults are now fail-closed; keep permissive behavior explicit in demos and automation.
103
103
 
104
104
  ## Mental Model: How It Works
105
105
 
@@ -118,40 +118,53 @@ For a deeper walkthrough, see `docs/core_mechanism.md`.
118
118
  ## Safety Defaults (Important)
119
119
 
120
120
  Default behavior without hooks/policy:
121
- - Command approval: `accept`
122
- - File change approval: `accept`
121
+ - Command approval: `decline`
122
+ - File change approval: `decline`
123
+ - Permissions approval: empty grant with `turn` scope
124
+ - MCP elicitation: `decline`
123
125
  - Tool user input: empty answers
124
126
  - Tool call: failure response with explanatory text
125
127
 
126
- This is convenient for unattended demos, but not production-safe.
128
+ This is production-safer by default, but may block unattended workflows unless you opt into looser hooks.
127
129
 
128
- Recommended safer setup: enable LLM-judge policy with strict fallback decisions.
130
+ Recommended setup: rely on native automatic approval review and keep local policy deterministic.
129
131
 
130
132
  ```python
131
- from codex_python_sdk import PolicyJudgeConfig, create_client
132
-
133
- rubric = {
134
- "system_rubric": "Allow read-only operations. Decline unknown write operations.",
135
- "use_llm_judge": True,
136
- }
137
-
138
- judge_cfg = PolicyJudgeConfig(
139
- timeout_seconds=8.0,
140
- model="gpt-5",
141
- effort="low",
142
- fallback_command_decision="decline",
143
- fallback_file_change_decision="decline",
133
+ from codex_python_sdk import RuleBasedPolicyEngine, create_client
134
+
135
+ engine = RuleBasedPolicyEngine(
136
+ {
137
+ "system_rubric": "Allow read-only operations. Decline unknown write operations.",
138
+ "command_rules": [
139
+ {"name": "readonly-shell", "when": {"command_regex": r"^(pwd|ls|cat|rg)\\b"}, "decision": "accept"}
140
+ ],
141
+ "defaults": {"command": "decline", "file_change": "decline", "tool_input": "auto_empty"},
142
+ }
144
143
  )
145
144
 
146
145
  with create_client(
147
- policy_rubric=rubric,
148
- policy_judge_config=judge_cfg,
146
+ automatic_approval_review=True,
147
+ policy_engine=engine,
149
148
  ) as client:
150
149
  result = client.responses_create(prompt="Show git status.")
151
150
  print(result.text)
152
151
  ```
153
152
 
154
- Note: LLM-judge requires a real Codex runtime/account; for deterministic local tests, use `RuleBasedPolicyEngine`.
153
+ `automatic_approval_review=True` enables the runtime's native approval reviewer (`guardian_approval`).
154
+
155
+ Recommended default operating mode for most repository automation:
156
+ - `automatic_approval_review=True`
157
+ - thread sandbox remains `workspace-write`
158
+ - thread approval policy remains `on-request`
159
+
160
+ This gives the agent writable access inside the workspace while keeping sandboxing and approval flows intact.
161
+ It is usually the right default for coding agents that only need to read and write within the current repo.
162
+
163
+ Do not treat this as equivalent to bypass mode:
164
+ - `danger-full-access` removes sandbox restrictions for command execution
165
+ - `--dangerously-bypass-approvals-and-sandbox` skips both approvals and sandbox protections
166
+
167
+ Those higher-permission modes should stay explicit opt-ins for externally sandboxed or highly trusted environments.
155
168
 
156
169
  ## Install
157
170
 
@@ -190,6 +203,11 @@ Factory:
190
203
  - `create_client(**kwargs) -> CodexAgenticClient`
191
204
  - `create_async_client(**kwargs) -> AsyncCodexAgenticClient`
192
205
 
206
+ Important runtime kwargs:
207
+ - `automatic_approval_review=True`
208
+ - `enabled_features=[...]` / `disabled_features=[...]`
209
+ - `enable_web_search` as a compatibility alias for `web_search="live"`
210
+
193
211
  High-frequency response APIs:
194
212
  - `responses_create(...) -> AgentResponse`
195
213
  - `responses_events(...) -> Iterator[ResponseEvent] / AsyncIterator[ResponseEvent]`
@@ -198,6 +216,9 @@ High-frequency response APIs:
198
216
  Thread basics:
199
217
  - `thread_start`, `thread_read`, `thread_list`, `thread_archive`
200
218
 
219
+ Runtime discovery:
220
+ - `experimental_feature_list(limit=None, cursor=None)`
221
+
201
222
  Account basics:
202
223
  - `account_read`, `account_rate_limits_read`
203
224
 
@@ -223,8 +244,7 @@ English:
223
244
 
224
245
  - After `AppServerConnectionError`, recreate the client instead of relying on implicit reconnect behavior.
225
246
  - Internal app-server `stderr` buffering keeps only the latest 500 lines in SDK-captured diagnostics.
226
- - When using low-level server request handlers, method names must be exactly `item`, `tool`, or `requestUserInput`.
227
- - Policy LLM-judge parsing is strict JSON-only: judge output must be a pure JSON object; embedded JSON snippets in free text are rejected.
247
+ - `review_start(...)` is for code review flows; it is not the same feature as runtime approval review.
228
248
  - Invalid command/file policy decision values (allowed: `accept`, `acceptForSession`, `decline`, `cancel`) raise `CodexAgenticError`.
229
249
 
230
250
  ## Development
@@ -67,8 +67,8 @@ codex-python-sdk-demo --mode demo
67
67
  codex-python-sdk-demo --mode full
68
68
  ```
69
69
 
70
- Note: the demo runner uses permissive hooks (`accept` for command/file approvals and empty tool-input answers) so it can run unattended.
71
- Use stricter hooks or policy engines in production.
70
+ Note: the demo runner uses explicit permissive hooks (`accept` for command/file approvals and empty tool-input answers) so it can run unattended.
71
+ The SDK defaults are now fail-closed; keep permissive behavior explicit in demos and automation.
72
72
 
73
73
  ## Mental Model: How It Works
74
74
 
@@ -87,40 +87,53 @@ For a deeper walkthrough, see `docs/core_mechanism.md`.
87
87
  ## Safety Defaults (Important)
88
88
 
89
89
  Default behavior without hooks/policy:
90
- - Command approval: `accept`
91
- - File change approval: `accept`
90
+ - Command approval: `decline`
91
+ - File change approval: `decline`
92
+ - Permissions approval: empty grant with `turn` scope
93
+ - MCP elicitation: `decline`
92
94
  - Tool user input: empty answers
93
95
  - Tool call: failure response with explanatory text
94
96
 
95
- This is convenient for unattended demos, but not production-safe.
97
+ This is production-safer by default, but may block unattended workflows unless you opt into looser hooks.
96
98
 
97
- Recommended safer setup: enable LLM-judge policy with strict fallback decisions.
99
+ Recommended setup: rely on native automatic approval review and keep local policy deterministic.
98
100
 
99
101
  ```python
100
- from codex_python_sdk import PolicyJudgeConfig, create_client
101
-
102
- rubric = {
103
- "system_rubric": "Allow read-only operations. Decline unknown write operations.",
104
- "use_llm_judge": True,
105
- }
106
-
107
- judge_cfg = PolicyJudgeConfig(
108
- timeout_seconds=8.0,
109
- model="gpt-5",
110
- effort="low",
111
- fallback_command_decision="decline",
112
- fallback_file_change_decision="decline",
102
+ from codex_python_sdk import RuleBasedPolicyEngine, create_client
103
+
104
+ engine = RuleBasedPolicyEngine(
105
+ {
106
+ "system_rubric": "Allow read-only operations. Decline unknown write operations.",
107
+ "command_rules": [
108
+ {"name": "readonly-shell", "when": {"command_regex": r"^(pwd|ls|cat|rg)\\b"}, "decision": "accept"}
109
+ ],
110
+ "defaults": {"command": "decline", "file_change": "decline", "tool_input": "auto_empty"},
111
+ }
113
112
  )
114
113
 
115
114
  with create_client(
116
- policy_rubric=rubric,
117
- policy_judge_config=judge_cfg,
115
+ automatic_approval_review=True,
116
+ policy_engine=engine,
118
117
  ) as client:
119
118
  result = client.responses_create(prompt="Show git status.")
120
119
  print(result.text)
121
120
  ```
122
121
 
123
- Note: LLM-judge requires a real Codex runtime/account; for deterministic local tests, use `RuleBasedPolicyEngine`.
122
+ `automatic_approval_review=True` enables the runtime's native approval reviewer (`guardian_approval`).
123
+
124
+ Recommended default operating mode for most repository automation:
125
+ - `automatic_approval_review=True`
126
+ - thread sandbox remains `workspace-write`
127
+ - thread approval policy remains `on-request`
128
+
129
+ This gives the agent writable access inside the workspace while keeping sandboxing and approval flows intact.
130
+ It is usually the right default for coding agents that only need to read and write within the current repo.
131
+
132
+ Do not treat this as equivalent to bypass mode:
133
+ - `danger-full-access` removes sandbox restrictions for command execution
134
+ - `--dangerously-bypass-approvals-and-sandbox` skips both approvals and sandbox protections
135
+
136
+ Those higher-permission modes should stay explicit opt-ins for externally sandboxed or highly trusted environments.
124
137
 
125
138
  ## Install
126
139
 
@@ -159,6 +172,11 @@ Factory:
159
172
  - `create_client(**kwargs) -> CodexAgenticClient`
160
173
  - `create_async_client(**kwargs) -> AsyncCodexAgenticClient`
161
174
 
175
+ Important runtime kwargs:
176
+ - `automatic_approval_review=True`
177
+ - `enabled_features=[...]` / `disabled_features=[...]`
178
+ - `enable_web_search` as a compatibility alias for `web_search="live"`
179
+
162
180
  High-frequency response APIs:
163
181
  - `responses_create(...) -> AgentResponse`
164
182
  - `responses_events(...) -> Iterator[ResponseEvent] / AsyncIterator[ResponseEvent]`
@@ -167,6 +185,9 @@ High-frequency response APIs:
167
185
  Thread basics:
168
186
  - `thread_start`, `thread_read`, `thread_list`, `thread_archive`
169
187
 
188
+ Runtime discovery:
189
+ - `experimental_feature_list(limit=None, cursor=None)`
190
+
170
191
  Account basics:
171
192
  - `account_read`, `account_rate_limits_read`
172
193
 
@@ -192,8 +213,7 @@ English:
192
213
 
193
214
  - After `AppServerConnectionError`, recreate the client instead of relying on implicit reconnect behavior.
194
215
  - Internal app-server `stderr` buffering keeps only the latest 500 lines in SDK-captured diagnostics.
195
- - When using low-level server request handlers, method names must be exactly `item`, `tool`, or `requestUserInput`.
196
- - Policy LLM-judge parsing is strict JSON-only: judge output must be a pure JSON object; embedded JSON snippets in free text are rejected.
216
+ - `review_start(...)` is for code review flows; it is not the same feature as runtime approval review.
197
217
  - Invalid command/file policy decision values (allowed: `accept`, `acceptForSession`, `decline`, `cancel`) raise `CodexAgenticError`.
198
218
 
199
219
  ## Development
@@ -17,10 +17,8 @@ from .factory import create_async_client, create_client
17
17
  from .policy import (
18
18
  DEFAULT_POLICY_RUBRIC,
19
19
  DefaultPolicyEngine,
20
- LlmRubricPolicyEngine,
21
20
  PolicyContext,
22
21
  PolicyEngine,
23
- PolicyJudgeConfig,
24
22
  PolicyRubric,
25
23
  RuleBasedPolicyEngine,
26
24
  build_policy_engine_from_rubric,
@@ -40,11 +38,9 @@ __all__ = [
40
38
  "ExecStyleRenderer",
41
39
  "DEFAULT_POLICY_RUBRIC",
42
40
  "DefaultPolicyEngine",
43
- "LlmRubricPolicyEngine",
44
41
  "NotAuthenticatedError",
45
42
  "PolicyContext",
46
43
  "PolicyEngine",
47
- "PolicyJudgeConfig",
48
44
  "PolicyRubric",
49
45
  "ResponseEvent",
50
46
  "RuleBasedPolicyEngine",
@@ -21,13 +21,21 @@ from .errors import (
21
21
  from .types import AgentResponse, ResponseEvent
22
22
 
23
23
  if TYPE_CHECKING:
24
- from .policy import PolicyContext, PolicyEngine, PolicyJudgeConfig, PolicyRubric
24
+ from .policy import PolicyContext, PolicyEngine, PolicyRubric
25
25
 
26
26
  DEFAULT_CLI_COMMAND = "codex"
27
27
  DEFAULT_APP_SERVER_ARGS = ["app-server"]
28
28
  DEFAULT_NOTIFICATION_BUFFER_LIMIT = 1024
29
29
  DEFAULT_STDERR_BUFFER_LIMIT = 500
30
30
  DEFAULT_STREAM_IDLE_TIMEOUT_SECONDS = 60.0
31
+ DEFAULT_MCP_ELICITATION_RESULT = {"action": "decline"}
32
+ DEFAULT_PERMISSIONS_APPROVAL_RESULT = {"permissions": {}, "scope": "turn"}
33
+ DEFAULT_FILE_CHANGE_APPROVAL_RESULT = {"decision": "decline"}
34
+ DEFAULT_COMMAND_APPROVAL_RESULT = {"decision": "decline"}
35
+ DEFAULT_THREAD_BASELINE = {
36
+ "approvalPolicy": "on-request",
37
+ "sandbox": "workspace-write",
38
+ }
31
39
 
32
40
 
33
41
  class AsyncCodexAgenticClient:
@@ -42,16 +50,20 @@ class AsyncCodexAgenticClient:
42
50
  process_cwd: str | None = None,
43
51
  default_thread_params: dict[str, Any] | None = None,
44
52
  default_turn_params: dict[str, Any] | None = None,
53
+ automatic_approval_review: bool = True,
54
+ enabled_features: list[str] | None = None,
55
+ disabled_features: list[str] | None = None,
45
56
  enable_web_search: bool = True,
46
57
  server_config_overrides: dict[str, Any] | None = None,
47
58
  stream_idle_timeout_seconds: float | None = DEFAULT_STREAM_IDLE_TIMEOUT_SECONDS,
48
59
  on_command_approval: Callable[[dict[str, Any]], dict[str, Any] | Awaitable[dict[str, Any]]] | None = None,
49
60
  on_file_change_approval: Callable[[dict[str, Any]], dict[str, Any] | Awaitable[dict[str, Any]]] | None = None,
61
+ on_permissions_approval: Callable[[dict[str, Any]], dict[str, Any] | Awaitable[dict[str, Any]]] | None = None,
50
62
  on_tool_request_user_input: Callable[[dict[str, Any]], dict[str, Any] | Awaitable[dict[str, Any]]] | None = None,
63
+ on_mcp_elicitation_request: Callable[[dict[str, Any]], dict[str, Any] | Awaitable[dict[str, Any]]] | None = None,
51
64
  on_tool_call: Callable[[dict[str, Any]], dict[str, Any] | Awaitable[dict[str, Any]]] | None = None,
52
65
  policy_engine: "PolicyEngine | None" = None,
53
66
  policy_rubric: "PolicyRubric | dict[str, Any] | None" = None,
54
- policy_judge_config: "PolicyJudgeConfig | None" = None,
55
67
  ) -> None:
56
68
  """Create an async app-server client.
57
69
 
@@ -62,18 +74,23 @@ class AsyncCodexAgenticClient:
62
74
  process_cwd: Working directory used when launching app-server.
63
75
  default_thread_params: Baseline params for thread-level requests.
64
76
  default_turn_params: Baseline params for turn-level requests.
65
- enable_web_search: If true, appends ``--enable web_search`` at launch.
77
+ automatic_approval_review: If true, enables native runtime approval review via
78
+ ``guardian_approval``.
79
+ enabled_features: Extra app-server feature flags passed as ``--enable``.
80
+ disabled_features: Feature flags passed as ``--disable``.
81
+ enable_web_search: Compatibility alias for ``web_search="live"``.
66
82
  server_config_overrides: Config key-values serialized to ``-c key=value``.
67
83
  stream_idle_timeout_seconds: Max consecutive seconds without matching turn events
68
84
  before stream wait fails. Set ``None`` to disable this guard.
69
85
  on_command_approval: Handler for ``item/commandExecution/requestApproval``.
70
86
  on_file_change_approval: Handler for ``item/fileChange/requestApproval``.
87
+ on_permissions_approval: Handler for ``item/permissions/requestApproval``.
71
88
  on_tool_request_user_input: Handler for ``item/tool/requestUserInput``.
89
+ on_mcp_elicitation_request: Handler for ``mcpServer/elicitation/request``.
72
90
  on_tool_call: Handler for ``item/tool/call``.
73
91
  policy_engine: Optional policy engine used when explicit hooks are absent.
74
- policy_rubric: Optional rubric used to auto-build a policy engine when
92
+ policy_rubric: Optional rubric used to auto-build a rule-based policy engine when
75
93
  ``policy_engine`` is not provided.
76
- policy_judge_config: Optional LLM-judge settings when rubric builds an LLM policy.
77
94
  """
78
95
 
79
96
  self.codex_command = codex_command
@@ -81,28 +98,32 @@ class AsyncCodexAgenticClient:
81
98
  self.env = os.environ.copy() if env is None else env.copy()
82
99
 
83
100
  self.process_cwd = os.path.abspath(process_cwd or os.getcwd())
84
- self.default_thread_params = dict(default_thread_params or {})
101
+ self.default_thread_params = self._with_thread_baseline(default_thread_params)
85
102
  self.default_turn_params = dict(default_turn_params or {})
103
+ self.automatic_approval_review = automatic_approval_review
104
+ self.enabled_features = self._normalize_feature_flags(enabled_features)
105
+ self.disabled_features = self._normalize_feature_flags(disabled_features)
106
+ if self.automatic_approval_review:
107
+ if "guardian_approval" in self.disabled_features:
108
+ raise CodexAgenticError(
109
+ "automatic_approval_review=True conflicts with disabled feature 'guardian_approval'."
110
+ )
111
+ if "guardian_approval" not in self.enabled_features:
112
+ self.enabled_features.append("guardian_approval")
86
113
  self.enable_web_search = enable_web_search
87
114
  self.server_config_overrides = dict(server_config_overrides or {})
88
115
  self.stream_idle_timeout_seconds = stream_idle_timeout_seconds
89
116
  self.on_command_approval = on_command_approval
90
117
  self.on_file_change_approval = on_file_change_approval
118
+ self.on_permissions_approval = on_permissions_approval
91
119
  self.on_tool_request_user_input = on_tool_request_user_input
120
+ self.on_mcp_elicitation_request = on_mcp_elicitation_request
92
121
  self.on_tool_call = on_tool_call
93
122
  self.policy_engine = policy_engine
94
123
  if self.policy_engine is None and policy_rubric is not None:
95
124
  from .policy import build_policy_engine_from_rubric
96
125
 
97
- self.policy_engine = build_policy_engine_from_rubric(
98
- policy_rubric,
99
- judge_config=policy_judge_config,
100
- codex_command=codex_command,
101
- app_server_args=app_server_args,
102
- env=env,
103
- process_cwd=self.process_cwd,
104
- server_config_overrides=server_config_overrides,
105
- )
126
+ self.policy_engine = build_policy_engine_from_rubric(policy_rubric)
106
127
 
107
128
  self._proc: asyncio.subprocess.Process | None = None
108
129
  self._reader_task: asyncio.Task[None] | None = None
@@ -201,7 +222,7 @@ class AsyncCodexAgenticClient:
201
222
  init_result = await self._request(
202
223
  "initialize",
203
224
  {
204
- "clientInfo": {"name": "codex-python-sdk", "version": "0.1"},
225
+ "clientInfo": {"name": "codex-python-sdk", "version": "0.2.0"},
205
226
  "capabilities": {"experimentalApi": True},
206
227
  },
207
228
  ensure_connected=False,
@@ -277,12 +298,39 @@ class AsyncCodexAgenticClient:
277
298
 
278
299
  def _build_server_args(self) -> list[str]:
279
300
  args = self.app_server_args[:]
301
+ for feature in self.enabled_features:
302
+ args.extend(["--enable", feature])
303
+ for feature in self.disabled_features:
304
+ args.extend(["--disable", feature])
280
305
  if self.enable_web_search:
281
- args.extend(["--enable", "web_search"])
306
+ args.extend(["-c", 'web_search="live"'])
282
307
  for key, value in self.server_config_overrides.items():
283
308
  args.extend(["-c", f"{key}={self._to_toml_literal(value)}"])
284
309
  return args
285
310
 
311
+ @staticmethod
312
+ def _normalize_feature_flags(features: list[str] | None) -> list[str]:
313
+ if not features:
314
+ return []
315
+ normalized: list[str] = []
316
+ seen: set[str] = set()
317
+ for raw in features:
318
+ feature = str(raw).strip()
319
+ if not feature or feature in seen:
320
+ continue
321
+ seen.add(feature)
322
+ normalized.append(feature)
323
+ return normalized
324
+
325
+ @staticmethod
326
+ def _with_thread_baseline(params: dict[str, Any] | None) -> dict[str, Any]:
327
+ merged = dict(DEFAULT_THREAD_BASELINE)
328
+ if params:
329
+ for key, value in params.items():
330
+ if value is not None:
331
+ merged[key] = value
332
+ return merged
333
+
286
334
  @staticmethod
287
335
  def _to_toml_literal(value: Any) -> str:
288
336
  if isinstance(value, bool):
@@ -443,7 +491,9 @@ class AsyncCodexAgenticClient:
443
491
  handlers: dict[str, Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]]] = {
444
492
  "item/commandExecution/requestApproval": self._handle_command_approval_request,
445
493
  "item/fileChange/requestApproval": self._handle_file_change_request,
494
+ "item/permissions/requestApproval": self._handle_permissions_approval_request,
446
495
  "item/tool/requestUserInput": self._handle_tool_user_input_request,
496
+ "mcpServer/elicitation/request": self._handle_mcp_elicitation_request,
447
497
  "item/tool/call": self._handle_tool_call_request,
448
498
  }
449
499
  handler = handlers.get(method)
@@ -475,7 +525,7 @@ class AsyncCodexAgenticClient:
475
525
  "item/commandExecution/requestApproval",
476
526
  params,
477
527
  handler,
478
- {"decision": "accept"},
528
+ DEFAULT_COMMAND_APPROVAL_RESULT,
479
529
  )
480
530
 
481
531
  async def _handle_file_change_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
@@ -493,7 +543,16 @@ class AsyncCodexAgenticClient:
493
543
  "item/fileChange/requestApproval",
494
544
  params,
495
545
  handler,
496
- {"decision": "accept"},
546
+ DEFAULT_FILE_CHANGE_APPROVAL_RESULT,
547
+ )
548
+
549
+ async def _handle_permissions_approval_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
550
+ del method
551
+ return await self._resolve_server_request(
552
+ "item/permissions/requestApproval",
553
+ params,
554
+ self.on_permissions_approval,
555
+ DEFAULT_PERMISSIONS_APPROVAL_RESULT,
497
556
  )
498
557
 
499
558
  async def _handle_tool_user_input_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
@@ -514,6 +573,15 @@ class AsyncCodexAgenticClient:
514
573
  {"answers": {}},
515
574
  )
516
575
 
576
+ async def _handle_mcp_elicitation_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
577
+ del method
578
+ return await self._resolve_server_request(
579
+ "mcpServer/elicitation/request",
580
+ params,
581
+ self.on_mcp_elicitation_request,
582
+ DEFAULT_MCP_ELICITATION_RESULT,
583
+ )
584
+
517
585
  async def _handle_tool_call_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
518
586
  del method
519
587
  return await self._resolve_server_request(
@@ -873,7 +941,7 @@ class AsyncCodexAgenticClient:
873
941
  ``ResponseEvent`` objects in arrival order.
874
942
  """
875
943
 
876
- merged_thread_params = self._merge_params(self.default_thread_params, thread_params)
944
+ merged_thread_params = self._with_thread_baseline(self._merge_params(self.default_thread_params, thread_params))
877
945
  merged_turn_params = self._merge_params(self.default_turn_params, turn_params)
878
946
 
879
947
  if session_id is None:
@@ -1067,7 +1135,7 @@ class AsyncCodexAgenticClient:
1067
1135
  ) -> dict[str, Any]:
1068
1136
  """Create a new thread via ``thread/start``."""
1069
1137
 
1070
- return await self._request("thread/start", self._merge_params(self.default_thread_params, params))
1138
+ return await self._request("thread/start", self._with_thread_baseline(self._merge_params(self.default_thread_params, params)))
1071
1139
 
1072
1140
  async def thread_read(self, thread_id: str, *, include_turns: bool = False) -> dict[str, Any]:
1073
1141
  """Read one thread by id."""
@@ -1098,7 +1166,7 @@ class AsyncCodexAgenticClient:
1098
1166
  *,
1099
1167
  params: dict[str, Any] | None = None,
1100
1168
  ) -> dict[str, Any]:
1101
- merged = self._merge_params(self.default_thread_params, params)
1169
+ merged = self._with_thread_baseline(self._merge_params(self.default_thread_params, params))
1102
1170
  return await self._request("thread/fork", {**merged, "threadId": thread_id})
1103
1171
 
1104
1172
  async def thread_name_set(self, thread_id: str, name: str) -> dict[str, Any]:
@@ -1179,6 +1247,19 @@ class AsyncCodexAgenticClient:
1179
1247
  params["delivery"] = delivery
1180
1248
  return await self._request("review/start", params)
1181
1249
 
1250
+ async def experimental_feature_list(
1251
+ self,
1252
+ *,
1253
+ limit: int | None = None,
1254
+ cursor: str | None = None,
1255
+ ) -> dict[str, Any]:
1256
+ params: dict[str, Any] = {}
1257
+ if limit is not None:
1258
+ params["limit"] = limit
1259
+ if cursor is not None:
1260
+ params["cursor"] = cursor
1261
+ return await self._request("experimentalFeature/list", params)
1262
+
1182
1263
  async def model_list(self, *, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
1183
1264
  params: dict[str, Any] = {}
1184
1265
  if limit is not None: